You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

233 lines
6.9 KiB

import { z } from "zod";
import { randomBytes } from "node:crypto";
import type { JSONSchema7 } from "json-schema";
import type { ToolExecutor, ToolContext, ToolResult } from "../../registry";
import type { DiceToolConfig } from "./config";
export const diceInputSchema = z.object({
operation: z
.enum(["roll", "coin", "pick", "rps"])
.describe(
"操作类型: roll=掷骰子, coin=抛硬币, pick=从列表中随机抽取, rps=石头剪刀布",
),
sides: z
.number()
.int()
.min(2)
.max(1000)
.optional()
.describe("roll 操作的骰子面数,默认使用配置值"),
count: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe("roll 操作的骰子数量,默认1个"),
items: z
.array(z.string())
.min(2)
.max(100)
.optional()
.describe("pick 操作的候选列表"),
pickCount: z
.number()
.int()
.min(1)
.max(50)
.optional()
.describe("pick 操作要抽取的数量,默认1个"),
choice: z
.enum(["rock", "paper", "scissors"])
.optional()
.describe("rps 操作中用户的选择"),
});
type DiceInput = z.infer<typeof diceInputSchema>;
function secureRandomInt(max: number): number {
if (max <= 0) throw new Error("max must be positive");
const bytesNeeded = Math.ceil(Math.log2(max) / 8);
const maxValid = Math.floor(Math.pow(256, bytesNeeded) / max) * max;
const buf = randomBytes(bytesNeeded);
let val = 0;
for (let i = 0; i < bytesNeeded; i++) {
val = val * 256 + (buf[i] ?? 0);
}
if (val < maxValid) {
return val % max;
}
return secureRandomInt(max);
}
export const diceExecutor: ToolExecutor<DiceToolConfig> = {
buildInputSchema(_config: DiceToolConfig): JSONSchema7 {
return {
type: "object",
properties: {
operation: {
type: "string",
enum: ["roll", "coin", "pick", "rps"],
description:
"操作类型: roll=掷骰子, coin=抛硬币, pick=从列表中随机抽取, rps=石头剪刀布",
},
sides: { type: "number", description: "roll 操作的骰子面数" },
count: { type: "number", description: "roll 操作的骰子数量" },
items: {
type: "array",
items: { type: "string" },
description: "pick 操作的候选列表",
},
pickCount: { type: "number", description: "pick 操作要抽取的数量" },
choice: {
type: "string",
enum: ["rock", "paper", "scissors"],
description: "rps 操作中用户的选择",
},
},
required: ["operation"],
};
},
buildDescription(config: DiceToolConfig): string {
return `随机决策工具。支持掷骰子(默认${config.defaultSides}面)、抛硬币、从列表随机抽取、石头剪刀布。使用加密级随机数。`;
},
async execute(
input: unknown,
config: DiceToolConfig,
_ctx: ToolContext,
): Promise<ToolResult> {
const start = Date.now();
const parsed = diceInputSchema.safeParse(input);
if (!parsed.success) {
return {
success: false,
data: null,
error: `输入参数校验失败: ${parsed.error.message}`,
metadata: { durationMs: Date.now() - start },
};
}
const inp = parsed.data as DiceInput;
try {
switch (inp.operation) {
case "roll": {
const sides = inp.sides ?? config.defaultSides;
const count = inp.count ?? 1;
const results: number[] = [];
for (let i = 0; i < count; i++) {
results.push(secureRandomInt(sides) + 1);
}
const total = results.reduce((a, b) => a + b, 0);
return {
success: true,
data: {
sides,
count,
results,
total,
max: sides * count,
min: count,
},
metadata: { durationMs: Date.now() - start },
};
}
case "coin": {
const result = secureRandomInt(2) === 0 ? "正面" : "反面";
return {
success: true,
data: { result, emoji: result === "正面" ? "🪙" : "🌑" },
metadata: { durationMs: Date.now() - start },
};
}
case "pick": {
if (!inp.items || inp.items.length < 2) {
throw new Error("pick 操作需要 items 参数(至少2项)");
}
const pickCount = Math.min(
inp.pickCount ?? 1,
inp.items.length,
);
const pool = [...inp.items];
const picked: string[] = [];
for (let i = 0; i < pickCount; i++) {
const idx = secureRandomInt(pool.length);
const item = pool[idx];
if (item) picked.push(item);
pool.splice(idx, 1);
}
return {
success: true,
data: {
from: inp.items,
picked,
pickCount,
unique: pickCount === inp.items.length ? "全部抽完" : "部分抽取",
},
metadata: { durationMs: Date.now() - start },
};
}
case "rps": {
const choices = ["rock", "paper", "scissors"] as const;
const choiceNames = { rock: "石头", paper: "布", scissors: "剪刀" };
const choiceEmojis = { rock: "🪨", paper: "📄", scissors: "✂️" };
const aiChoice = choices[secureRandomInt(3)] ?? "rock";
const userChoice = inp.choice;
if (!userChoice) {
return {
success: true,
data: {
aiChoice: choiceNames[aiChoice],
aiEmoji: choiceEmojis[aiChoice],
message: "请提供你的选择(rock/paper/scissors)来对战",
},
metadata: { durationMs: Date.now() - start },
};
}
let outcome: string;
if (userChoice === aiChoice) {
outcome = "平局";
} else if (
(userChoice === "rock" && aiChoice === "scissors") ||
(userChoice === "paper" && aiChoice === "rock") ||
(userChoice === "scissors" && aiChoice === "paper")
) {
outcome = "你赢了";
} else {
outcome = "你输了";
}
return {
success: true,
data: {
yourChoice: choiceNames[userChoice],
yourEmoji: choiceEmojis[userChoice],
aiChoice: choiceNames[aiChoice],
aiEmoji: choiceEmojis[aiChoice],
outcome,
},
metadata: { durationMs: Date.now() - start },
};
}
default:
throw new Error(`未知操作: ${inp.operation}`);
}
} catch (e) {
return {
success: false,
data: null,
error: e instanceof Error ? e.message : String(e),
metadata: { durationMs: Date.now() - start },
};
}
},
};