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