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; 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 = { 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 { 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 }, }; } }, };