From f64434a22eb5c50001c7526009b41e6a09506bce Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 9 Aug 2026 12:29:44 +0800 Subject: [PATCH] feat: add color manipulation, dice rolling, hashing, and magic 8-ball tools - Implemented color tool with operations for conversion, random color generation, color info, mixing, and contrast detection. - Added dice tool supporting rolling dice, coin flipping, item picking, and rock-paper-scissors. - Introduced hashing tool for computing and comparing hash values using various algorithms. - Created magic 8-ball tool for answering yes/no questions and providing daily fortunes. - Defined configuration schemas and input validation for each tool using Zod. - Registered new tools in the agent tool registry with appropriate default configurations and input schemas. --- packages/drizzle-pkg/db.sqlite | Bin 581632 -> 634880 bytes packages/drizzle-pkg/seed.ts | 60 +++ server/service/agent-tool/executors/color/color.ts | 406 +++++++++++++++++++++ .../service/agent-tool/executors/color/config.ts | 22 ++ server/service/agent-tool/executors/dice/config.ts | 19 + server/service/agent-tool/executors/dice/dice.ts | 233 ++++++++++++ server/service/agent-tool/executors/hash/config.ts | 24 ++ server/service/agent-tool/executors/hash/hash.ts | 127 +++++++ .../agent-tool/executors/magic-8ball/config.ts | 22 ++ .../executors/magic-8ball/magic-8ball.ts | 246 +++++++++++++ server/service/agent-tool/index.ts | 50 +++ 11 files changed, 1209 insertions(+) create mode 100644 server/service/agent-tool/executors/color/color.ts create mode 100644 server/service/agent-tool/executors/color/config.ts create mode 100644 server/service/agent-tool/executors/dice/config.ts create mode 100644 server/service/agent-tool/executors/dice/dice.ts create mode 100644 server/service/agent-tool/executors/hash/config.ts create mode 100644 server/service/agent-tool/executors/hash/hash.ts create mode 100644 server/service/agent-tool/executors/magic-8ball/config.ts create mode 100644 server/service/agent-tool/executors/magic-8ball/magic-8ball.ts diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index ed7f57f0f099c6c03f54eea3790d198fb8d73a47..5f707afcf3d8d00587a2ced58bbbb4433bad3e4f 100644 GIT binary patch delta 39585 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 delta 613 zcmW-dT}V@57{}k&vvbaN&i4M5>>{P96Xs13jS=>R8hEv8SKX$uWJpm#-tV7W0QWXx16SMvJFFo5wwN;lu{5#zctW0&Jvd#WddF5cVO2U62G- zLxz&iiRoq-q3Srk;}t$a;yk+1gDf!-$nIM=h{-doVugq$e8va7rGsDMDe`pC80>sz zTfN`J4EbRR{HBY)VG*D39`h*R1)gBW2|UwAA!io0D?x8W>cu@Ye#NvSi}w`D7*hXQ z&g4uv{>!wc{9M?LtM3#AM*O!{RbIB|mysR~3N^_Nozh12KfCmR$XZRxGt&*%UD z&q_ymEC8|0kV6(@7^T+^VSv0Pa2XeIRxD#H?$e0JLTz`Q^KmO-@K&q2gU?D=DnPdS zA#<6%iV&TaevGdtbcb*{&+yy4UWgLIX=HGdTt1*z#IPGpXvP(+5hZY9ABsz*1SeIH j6q1Xik~EU;loIaRHbYUv%-y6fsA1{|s!n*$J!|{}i*U<% diff --git a/packages/drizzle-pkg/seed.ts b/packages/drizzle-pkg/seed.ts index 1546b45..a0060ef 100644 --- a/packages/drizzle-pkg/seed.ts +++ b/packages/drizzle-pkg/seed.ts @@ -163,6 +163,66 @@ const TOOLS_SEED: ToolSeed[] = [ adminOnly: 0, sortOrder: 90, }, + { + id: "tool_hash", + name: "hash", + slug: "hash", + description: "Compute hash (md5/sha1/sha256/sha512) or compare hashes", + type: "hash", + config: { defaultAlgorithm: "sha256", uppercase: false }, + enabled: 1, + needsApproval: 0, + adminOnly: 0, + sortOrder: 100, + }, + { + id: "tool_color", + name: "color", + slug: "color", + description: "Color conversion (hex/rgb/hsl), random color, mix colors, WCAG contrast", + type: "color", + config: { defaultFormat: "hex" }, + enabled: 1, + needsApproval: 0, + adminOnly: 0, + sortOrder: 110, + }, + { + id: "tool_dice", + name: "dice", + slug: "dice", + description: "Roll dice, flip coin, pick from list, or play rock-paper-scissors", + type: "dice", + config: { defaultSides: 6 }, + enabled: 1, + needsApproval: 0, + adminOnly: 0, + sortOrder: 120, + }, + { + id: "tool_magic_8ball", + name: "magic-8ball", + slug: "magic-8ball", + description: "Magic 8-Ball fortune telling and daily fortune", + type: "magic-8ball", + config: { language: "zh" }, + enabled: 1, + needsApproval: 0, + adminOnly: 0, + sortOrder: 130, + }, + { + id: "tool_guess_who", + name: "guess-who", + slug: "guess-who", + description: "Guess who game - AI guesses your character or you guess AI's character", + type: "guess-who", + config: { language: "zh", maxQuestions: 20, difficulty: "medium" }, + enabled: 1, + needsApproval: 0, + adminOnly: 0, + sortOrder: 140, + }, ]; async function seedTools() { diff --git a/server/service/agent-tool/executors/color/color.ts b/server/service/agent-tool/executors/color/color.ts new file mode 100644 index 0000000..b23d958 --- /dev/null +++ b/server/service/agent-tool/executors/color/color.ts @@ -0,0 +1,406 @@ +import { z } from "zod"; +import type { JSONSchema7 } from "json-schema"; +import type { ToolExecutor, ToolContext, ToolResult } from "../../registry"; +import type { ColorToolConfig } from "./config"; + +export const colorInputSchema = z.object({ + operation: z + .enum(["convert", "random", "info", "mix", "contrast"]) + .describe( + "操作类型: convert=格式转换, random=随机颜色, info=颜色信息, mix=混合颜色, contrast=对比度检测", + ), + color: z + .string() + .optional() + .describe("输入颜色,支持 hex(#ff0000)、rgb(255,0,0)、hsl(0,100%,50%) 格式"), + targetFormat: z + .enum(["hex", "rgb", "hsl"]) + .optional() + .describe("convert 操作的目标格式"), + color2: z + .string() + .optional() + .describe("mix 操作的第二个颜色"), + ratio: z + .number() + .min(0) + .max(1) + .optional() + .describe("mix 操作的混合比例 (0=全用color, 1=全用color2)"), +}); + +type ColorInput = z.infer; + +interface RGB { + r: number; + g: number; + b: number; +} + +interface HSL { + h: number; + s: number; + l: number; +} + +function parseColor(input: string): RGB { + const trimmed = input.trim().toLowerCase(); + + const hexMatch = trimmed.match(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/); + if (hexMatch?.[1]) { + let hex: string = hexMatch[1]; + if (hex.length === 3) { + hex = hex + .split("") + .map((c) => c + c) + .join(""); + } + return { + r: parseInt(hex.slice(0, 2), 16), + g: parseInt(hex.slice(2, 4), 16), + b: parseInt(hex.slice(4, 6), 16), + }; + } + + const rgbMatch = trimmed.match( + /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/, + ); + if (rgbMatch?.[1] && rgbMatch[2] && rgbMatch[3]) { + return { + r: parseInt(rgbMatch[1]), + g: parseInt(rgbMatch[2]), + b: parseInt(rgbMatch[3]), + }; + } + + const hslMatch = trimmed.match( + /^hsla?\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%/, + ); + if (hslMatch?.[1] && hslMatch[2] && hslMatch[3]) { + return hslToRgb( + parseInt(hslMatch[1]), + parseInt(hslMatch[2]), + parseInt(hslMatch[3]), + ); + } + + throw new Error(`无法解析颜色: ${input}`); +} + +function rgbToHex(rgb: RGB): string { + const toHex = (n: number) => + Math.round(Math.max(0, Math.min(255, n))) + .toString(16) + .padStart(2, "0"); + return `#${toHex(rgb.r)}${toHex(rgb.g)}${toHex(rgb.b)}`; +} + +function rgbToHsl(rgb: RGB): HSL { + const r = rgb.r / 255; + const g = rgb.g / 255; + const b = rgb.b / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + const l = (max + min) / 2; + + let h = 0; + let s = 0; + + if (delta !== 0) { + s = delta / (1 - Math.abs(2 * l - 1)); + + switch (max) { + case r: + h = ((g - b) / delta) % 6; + break; + case g: + h = (b - r) / delta + 2; + break; + case b: + h = (r - g) / delta + 4; + break; + } + h *= 60; + if (h < 0) h += 360; + } + + return { h: Math.round(h), s: Math.round(s * 100), l: Math.round(l * 100) }; +} + +function hslToRgb(h: number, s: number, l: number): RGB { + const sNorm = s / 100; + const lNorm = l / 100; + const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = lNorm - c / 2; + + let r = 0; + let g = 0; + let b = 0; + + if (h < 60) { + r = c; + g = x; + } else if (h < 120) { + r = x; + g = c; + } else if (h < 180) { + g = c; + b = x; + } else if (h < 240) { + g = x; + b = c; + } else if (h < 300) { + r = x; + b = c; + } else { + r = c; + b = x; + } + + return { + r: Math.round((r + m) * 255), + g: Math.round((g + m) * 255), + b: Math.round((b + m) * 255), + }; +} + +function formatColor(rgb: RGB, format: "hex" | "rgb" | "hsl"): string { + switch (format) { + case "hex": + return rgbToHex(rgb); + case "rgb": + return `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`; + case "hsl": { + const hsl = rgbToHsl(rgb); + return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`; + } + } +} + +function relativeLuminance(rgb: RGB): number { + const channel = (c: number) => { + const s = c / 255; + return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4); + }; + return 0.2126 * channel(rgb.r) + 0.7152 * channel(rgb.g) + 0.0722 * channel(rgb.b); +} + +function contrastRatio(rgb1: RGB, rgb2: RGB): number { + const l1 = relativeLuminance(rgb1); + const l2 = relativeLuminance(rgb2); + const lighter = Math.max(l1, l2); + const darker = Math.min(l1, l2); + return (lighter + 0.05) / (darker + 0.05); +} + +function mixColors(c1: RGB, c2: RGB, ratio: number): RGB { + const r = Math.round(c1.r * (1 - ratio) + c2.r * ratio); + const g = Math.round(c1.g * (1 - ratio) + c2.g * ratio); + const b = Math.round(c1.b * (1 - ratio) + c2.b * ratio); + return { r, g, b }; +} + +function randomColor(): RGB { + return { + r: Math.floor(Math.random() * 256), + g: Math.floor(Math.random() * 256), + b: Math.floor(Math.random() * 256), + }; +} + +function colorName(rgb: RGB): string { + const hsl = rgbToHsl(rgb); + if (hsl.l < 10) return "黑色"; + if (hsl.l > 90) return "白色"; + if (hsl.s < 10) { + if (hsl.l < 30) return "深灰"; + if (hsl.l < 70) return "灰色"; + return "浅灰"; + } + const hueNames: [number, string][] = [ + [15, "红色"], + [45, "橙色"], + [70, "黄色"], + [150, "绿色"], + [200, "青色"], + [260, "蓝色"], + [290, "紫色"], + [330, "品红"], + [360, "红色"], + ]; + for (const [bound, name] of hueNames) { + if (hsl.h <= bound) return name; + } + return "红色"; +} + +export const colorExecutor: ToolExecutor = { + buildInputSchema(_config: ColorToolConfig): JSONSchema7 { + return { + type: "object", + properties: { + operation: { + type: "string", + enum: ["convert", "random", "info", "mix", "contrast"], + description: + "操作类型: convert=格式转换, random=随机颜色, info=颜色信息, mix=混合颜色, contrast=对比度检测", + }, + color: { + type: "string", + description: "输入颜色,支持 hex/rgb/hsl 格式", + }, + targetFormat: { + type: "string", + enum: ["hex", "rgb", "hsl"], + description: "convert 操作的目标格式", + }, + color2: { type: "string", description: "mix 操作的第二个颜色" }, + ratio: { + type: "number", + minimum: 0, + maximum: 1, + description: "mix 操作的混合比例", + }, + }, + required: ["operation"], + }; + }, + + buildDescription(config: ColorToolConfig): string { + return `颜色工具。支持 hex/rgb/hsl 格式互转、随机颜色生成、颜色信息查询、颜色混合、WCAG 对比度检测。默认输出格式: ${config.defaultFormat}。`; + }, + + async execute( + input: unknown, + config: ColorToolConfig, + _ctx: ToolContext, + ): Promise { + const start = Date.now(); + + const parsed = colorInputSchema.safeParse(input); + if (!parsed.success) { + return { + success: false, + data: null, + error: `输入参数校验失败: ${parsed.error.message}`, + metadata: { durationMs: Date.now() - start }, + }; + } + + const inp = parsed.data as ColorInput; + + try { + switch (inp.operation) { + case "convert": { + if (!inp.color) throw new Error("convert 操作需要 color 参数"); + const targetFormat = inp.targetFormat ?? config.defaultFormat; + const rgb = parseColor(inp.color); + const result = formatColor(rgb, targetFormat); + return { + success: true, + data: { + original: inp.color, + converted: result, + format: targetFormat, + }, + metadata: { durationMs: Date.now() - start }, + }; + } + + case "random": { + const rgb = randomColor(); + const targetFormat = inp.targetFormat ?? config.defaultFormat; + return { + success: true, + data: { + hex: rgbToHex(rgb), + rgb: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`, + hsl: (() => { + const hsl = rgbToHsl(rgb); + return `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`; + })(), + default: formatColor(rgb, targetFormat), + }, + metadata: { durationMs: Date.now() - start }, + }; + } + + case "info": { + if (!inp.color) throw new Error("info 操作需要 color 参数"); + const rgb = parseColor(inp.color); + const hsl = rgbToHsl(rgb); + const luminance = relativeLuminance(rgb); + return { + success: true, + data: { + input: inp.color, + hex: rgbToHex(rgb), + rgb: `rgb(${rgb.r}, ${rgb.g}, ${rgb.b})`, + hsl: `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`, + name: colorName(rgb), + luminance: Math.round(luminance * 100) / 100, + brightness: luminance > 0.5 ? "亮色" : "暗色", + }, + metadata: { durationMs: Date.now() - start }, + }; + } + + case "mix": { + if (!inp.color || !inp.color2) + throw new Error("mix 操作需要 color 和 color2 参数"); + const ratio = inp.ratio ?? 0.5; + const c1 = parseColor(inp.color); + const c2 = parseColor(inp.color2); + const mixed = mixColors(c1, c2, ratio); + const targetFormat = inp.targetFormat ?? config.defaultFormat; + return { + success: true, + data: { + color1: formatColor(c1, targetFormat), + color2: formatColor(c2, targetFormat), + ratio, + mixed: formatColor(mixed, targetFormat), + }, + metadata: { durationMs: Date.now() - start }, + }; + } + + case "contrast": { + if (!inp.color || !inp.color2) + throw new Error("contrast 操作需要 color 和 color2 参数"); + const c1 = parseColor(inp.color); + const c2 = parseColor(inp.color2); + const ratio = contrastRatio(c1, c2); + let grade: string; + if (ratio >= 7) grade = "AAA(优秀)"; + else if (ratio >= 4.5) grade = "AA(合格)"; + else if (ratio >= 3) grade = "AA Large(大文本合格)"; + else grade = "不达标"; + return { + success: true, + data: { + color1: rgbToHex(c1), + color2: rgbToHex(c2), + ratio: Math.round(ratio * 100) / 100, + grade, + }, + 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 }, + }; + } + }, +}; diff --git a/server/service/agent-tool/executors/color/config.ts b/server/service/agent-tool/executors/color/config.ts new file mode 100644 index 0000000..3bebe0c --- /dev/null +++ b/server/service/agent-tool/executors/color/config.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +export const colorConfigSchema = z.object({ + defaultFormat: z + .enum(["hex", "rgb", "hsl"]) + .default("hex") + .describe("默认输出颜色格式"), +}); + +export type ColorToolConfig = z.infer; + +export const DEFAULT_COLOR_CONFIG: ColorToolConfig = { + defaultFormat: "hex", +}; + +export function parseColorConfig(raw: unknown): ColorToolConfig { + const parsed = colorConfigSchema.safeParse(raw); + if (!parsed.success) { + throw new Error(`Invalid color config: ${parsed.error.message}`); + } + return parsed.data; +} diff --git a/server/service/agent-tool/executors/dice/config.ts b/server/service/agent-tool/executors/dice/config.ts new file mode 100644 index 0000000..1cd0d62 --- /dev/null +++ b/server/service/agent-tool/executors/dice/config.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const diceConfigSchema = z.object({ + defaultSides: z.number().int().min(2).max(1000).default(6).describe("默认骰子面数"), +}); + +export type DiceToolConfig = z.infer; + +export const DEFAULT_DICE_CONFIG: DiceToolConfig = { + defaultSides: 6, +}; + +export function parseDiceConfig(raw: unknown): DiceToolConfig { + const parsed = diceConfigSchema.safeParse(raw); + if (!parsed.success) { + throw new Error(`Invalid dice config: ${parsed.error.message}`); + } + return parsed.data; +} diff --git a/server/service/agent-tool/executors/dice/dice.ts b/server/service/agent-tool/executors/dice/dice.ts new file mode 100644 index 0000000..a030d00 --- /dev/null +++ b/server/service/agent-tool/executors/dice/dice.ts @@ -0,0 +1,233 @@ +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 }, + }; + } + }, +}; diff --git a/server/service/agent-tool/executors/hash/config.ts b/server/service/agent-tool/executors/hash/config.ts new file mode 100644 index 0000000..38a4be6 --- /dev/null +++ b/server/service/agent-tool/executors/hash/config.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const hashConfigSchema = z.object({ + defaultAlgorithm: z + .enum(["md5", "sha1", "sha256", "sha512"]) + .default("sha256") + .describe("默认哈希算法"), + uppercase: z.boolean().default(false).describe("输出是否大写"), +}); + +export type HashToolConfig = z.infer; + +export const DEFAULT_HASH_CONFIG: HashToolConfig = { + defaultAlgorithm: "sha256", + uppercase: false, +}; + +export function parseHashConfig(raw: unknown): HashToolConfig { + const parsed = hashConfigSchema.safeParse(raw); + if (!parsed.success) { + throw new Error(`Invalid hash config: ${parsed.error.message}`); + } + return parsed.data; +} diff --git a/server/service/agent-tool/executors/hash/hash.ts b/server/service/agent-tool/executors/hash/hash.ts new file mode 100644 index 0000000..84635dd --- /dev/null +++ b/server/service/agent-tool/executors/hash/hash.ts @@ -0,0 +1,127 @@ +import { z } from "zod"; +import { createHash } from "node:crypto"; +import type { JSONSchema7 } from "json-schema"; +import type { ToolExecutor, ToolContext, ToolResult } from "../../registry"; +import type { HashToolConfig } from "./config"; + +export const hashInputSchema = z.object({ + operation: z + .enum(["hash", "compare"]) + .describe("操作类型: hash=计算哈希, compare=比较哈希值"), + text: z + .string() + .min(1) + .max(100000) + .describe("要计算哈希的文本内容"), + algorithm: z + .enum(["md5", "sha1", "sha256", "sha512"]) + .optional() + .describe("哈希算法,不传则使用配置默认值"), + expectedHash: z + .string() + .optional() + .describe("compare 操作时,要对比的期望哈希值"), +}); + +type HashInput = z.infer; + +function computeHash(text: string, algorithm: string, uppercase: boolean): string { + const hash = createHash(algorithm).update(text, "utf8").digest("hex"); + return uppercase ? hash.toUpperCase() : hash; +} + +export const hashExecutor: ToolExecutor = { + buildInputSchema(_config: HashToolConfig): JSONSchema7 { + return { + type: "object", + properties: { + operation: { + type: "string", + enum: ["hash", "compare"], + description: "操作类型: hash=计算哈希, compare=比较哈希值", + }, + text: { type: "string", description: "要计算哈希的文本内容" }, + algorithm: { + type: "string", + enum: ["md5", "sha1", "sha256", "sha512"], + description: "哈希算法", + }, + expectedHash: { type: "string", description: "compare 操作的期望哈希值" }, + }, + required: ["operation", "text"], + }; + }, + + buildDescription(config: HashToolConfig): string { + return `哈希计算工具。支持 md5、sha1、sha256、sha512 算法。可计算文本哈希值或对比哈希是否匹配。默认算法: ${config.defaultAlgorithm},输出${config.uppercase ? "大写" : "小写"}。`; + }, + + async execute( + input: unknown, + config: HashToolConfig, + _ctx: ToolContext, + ): Promise { + const start = Date.now(); + + const parsed = hashInputSchema.safeParse(input); + if (!parsed.success) { + return { + success: false, + data: null, + error: `输入参数校验失败: ${parsed.error.message}`, + metadata: { durationMs: Date.now() - start }, + }; + } + + const inp = parsed.data as HashInput; + const algorithm = inp.algorithm ?? config.defaultAlgorithm; + + try { + switch (inp.operation) { + case "hash": { + const hashValue = computeHash(inp.text, algorithm, config.uppercase); + return { + success: true, + data: { + algorithm, + inputLength: inp.text.length, + hash: hashValue, + }, + metadata: { durationMs: Date.now() - start }, + }; + } + + case "compare": { + if (!inp.expectedHash) { + throw new Error("compare 操作需要 expectedHash 参数"); + } + const actualHash = computeHash(inp.text, algorithm, config.uppercase); + const expected = config.uppercase + ? inp.expectedHash.toUpperCase() + : inp.expectedHash.toLowerCase(); + const matched = actualHash === expected; + return { + success: true, + data: { + algorithm, + matched, + actualHash, + expectedHash: expected, + }, + 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 }, + }; + } + }, +}; diff --git a/server/service/agent-tool/executors/magic-8ball/config.ts b/server/service/agent-tool/executors/magic-8ball/config.ts new file mode 100644 index 0000000..58bc6e2 --- /dev/null +++ b/server/service/agent-tool/executors/magic-8ball/config.ts @@ -0,0 +1,22 @@ +import { z } from "zod"; + +export const magic8ballConfigSchema = z.object({ + language: z + .enum(["zh", "en"]) + .default("zh") + .describe("回答语言"), +}); + +export type Magic8ballToolConfig = z.infer; + +export const DEFAULT_MAGIC8BALL_CONFIG: Magic8ballToolConfig = { + language: "zh", +}; + +export function parseMagic8ballConfig(raw: unknown): Magic8ballToolConfig { + const parsed = magic8ballConfigSchema.safeParse(raw); + if (!parsed.success) { + throw new Error(`Invalid magic-8ball config: ${parsed.error.message}`); + } + return parsed.data; +} diff --git a/server/service/agent-tool/executors/magic-8ball/magic-8ball.ts b/server/service/agent-tool/executors/magic-8ball/magic-8ball.ts new file mode 100644 index 0000000..9983e07 --- /dev/null +++ b/server/service/agent-tool/executors/magic-8ball/magic-8ball.ts @@ -0,0 +1,246 @@ +import { z } from "zod"; +import { randomBytes } from "node:crypto"; +import type { JSONSchema7 } from "json-schema"; +import type { ToolExecutor, ToolContext, ToolResult } from "../../registry"; +import type { Magic8ballToolConfig } from "./config"; + +export const magic8ballInputSchema = z.object({ + operation: z + .enum(["ask", "fortune"]) + .describe("操作类型: ask=魔法八球问答, fortune=今日运势"), + question: z + .string() + .min(1) + .max(500) + .optional() + .describe("ask 操作的问题(是非题)"), +}); + +type Magic8ballInput = z.infer; + +const ANSWERS_ZH = [ + { text: "毫无疑问,是的", tone: "positive", emoji: "✅" }, + { text: "非常确定", tone: "positive", emoji: "✅" }, + { text: "毫无疑问", tone: "positive", emoji: "✅" }, + { text: "是的,必须的", tone: "positive", emoji: "✅" }, + { text: "你可以依赖它", tone: "positive", emoji: "✅" }, + { text: "如我所见,是的", tone: "positive", emoji: "✅" }, + { text: "很可能", tone: "positive", emoji: "✅" }, + { text: "前景乐观", tone: "positive", emoji: "✅" }, + { text: "迹象指向是的", tone: "positive", emoji: "✅" }, + { text: "是的", tone: "positive", emoji: "✅" }, + { text: "回答模糊,再试一次", tone: "neutral", emoji: "🤔" }, + { text: "稍后再问", tone: "neutral", emoji: "🤔" }, + { text: "现在最好不告诉你", tone: "neutral", emoji: "🤔" }, + { text: "现在无法预测", tone: "neutral", emoji: "🤔" }, + { text: "集中注意力再问", tone: "neutral", emoji: "🤔" }, + { text: "不要指望它", tone: "negative", emoji: "❌" }, + { text: "我的回答是否定的", tone: "negative", emoji: "❌" }, + { text: "我的消息来源说不是", tone: "negative", emoji: "❌" }, + { text: "前景不太乐观", tone: "negative", emoji: "❌" }, + { text: "非常值得怀疑", tone: "negative", emoji: "❌" }, +]; + +const ANSWERS_EN = [ + { text: "It is certain", tone: "positive", emoji: "✅" }, + { text: "It is decidedly so", tone: "positive", emoji: "✅" }, + { text: "Without a doubt", tone: "positive", emoji: "✅" }, + { text: "Yes definitely", tone: "positive", emoji: "✅" }, + { text: "You may rely on it", tone: "positive", emoji: "✅" }, + { text: "As I see it, yes", tone: "positive", emoji: "✅" }, + { text: "Most likely", tone: "positive", emoji: "✅" }, + { text: "Outlook good", tone: "positive", emoji: "✅" }, + { text: "Signs point to yes", tone: "positive", emoji: "✅" }, + { text: "Yes", tone: "positive", emoji: "✅" }, + { text: "Reply hazy try again", tone: "neutral", emoji: "🤔" }, + { text: "Ask again later", tone: "neutral", emoji: "🤔" }, + { text: "Better not tell you now", tone: "neutral", emoji: "🤔" }, + { text: "Cannot predict now", tone: "neutral", emoji: "🤔" }, + { text: "Concentrate and ask again", tone: "neutral", emoji: "🤔" }, + { text: "Don't count on it", tone: "negative", emoji: "❌" }, + { text: "My reply is no", tone: "negative", emoji: "❌" }, + { text: "My sources say no", tone: "negative", emoji: "❌" }, + { text: "Outlook not so good", tone: "negative", emoji: "❌" }, + { text: "Very doubtful", tone: "negative", emoji: "❌" }, +]; + +const FORTUNE_LUCKY_COLORS = [ + "红色", "橙色", "黄色", "绿色", "青色", "蓝色", "紫色", "粉色", "金色", "银色", +]; + +const FORTUNE_MESSAGES_ZH = [ + "今天适合尝试新事物", + "保持耐心,好事将近", + "注意身边的人,他们可能带来惊喜", + "今天不宜做重大决定", + "财运不错,但别贪心", + "健康为先,注意休息", + "人际关系是今天的重点", + "灵感闪现,记得记录下来", + "今天适合独处和思考", + "勇敢表达你的想法", + "小确幸藏在细节里", + "今天适合整理和清理", + "保持好奇心,会有收获", + "今天适合联系老朋友", + "放慢脚步,享受当下", +]; + +const FORTUNE_MESSAGES_EN = [ + "Today is good for trying something new", + "Be patient, good things are coming", + "Pay attention to people around you", + "Avoid major decisions today", + "Good fortune, but don't be greedy", + "Prioritize your health and rest", + "Relationships are today's focus", + "Inspiration strikes, write it down", + "A good day for solitude and reflection", + "Be brave and express your thoughts", + "Small joys hide in the details", + "A good day to organize and clean", + "Stay curious, you'll be rewarded", + "Reach out to an old friend today", + "Slow down and enjoy the moment", +]; + +function secureRandomInt(max: number): number { + if (max <= 0) throw new Error("max must be positive"); + const bytesNeeded = Math.ceil(Math.log2(max) / 8) || 1; + 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 magic8ballExecutor: ToolExecutor = { + buildInputSchema(_config: Magic8ballToolConfig): JSONSchema7 { + return { + type: "object", + properties: { + operation: { + type: "string", + enum: ["ask", "fortune"], + description: "操作类型: ask=魔法八球问答, fortune=今日运势", + }, + question: { + type: "string", + description: "ask 操作的问题(是非题)", + }, + }, + required: ["operation"], + }; + }, + + buildDescription(config: Magic8ballToolConfig): string { + return `魔法八球与运势工具。ask 模式回答是非题(20条经典预言),fortune 模式生成今日运势(幸运色+幸运数字+一句话)。语言: ${config.language === "zh" ? "中文" : "英文"}。`; + }, + + async execute( + input: unknown, + config: Magic8ballToolConfig, + _ctx: ToolContext, + ): Promise { + const start = Date.now(); + + const parsed = magic8ballInputSchema.safeParse(input); + if (!parsed.success) { + return { + success: false, + data: null, + error: `输入参数校验失败: ${parsed.error.message}`, + metadata: { durationMs: Date.now() - start }, + }; + } + + const inp = parsed.data as Magic8ballInput; + const answers = config.language === "zh" ? ANSWERS_ZH : ANSWERS_EN; + const fortuneMessages = + config.language === "zh" ? FORTUNE_MESSAGES_ZH : FORTUNE_MESSAGES_EN; + + try { + switch (inp.operation) { + case "ask": { + if (!inp.question) { + throw new Error("ask 操作需要 question 参数"); + } + const answer = answers[secureRandomInt(answers.length)] ?? answers[0]!; + const toneLabel = + answer.tone === "positive" + ? config.language === "zh" + ? "肯定" + : "Positive" + : answer.tone === "neutral" + ? config.language === "zh" + ? "中立" + : "Neutral" + : config.language === "zh" + ? "否定" + : "Negative"; + + return { + success: true, + data: { + question: inp.question, + answer: answer.text, + emoji: answer.emoji, + tone: toneLabel, + }, + metadata: { durationMs: Date.now() - start }, + }; + } + + case "fortune": { + const luckyColor = + FORTUNE_LUCKY_COLORS[secureRandomInt(FORTUNE_LUCKY_COLORS.length)]; + const luckyNumber = secureRandomInt(99) + 1; + const message = + fortuneMessages[secureRandomInt(fortuneMessages.length)]; + const luckyDirection = ["东", "南", "西", "北", "东北", "东南", "西北", "西南"][ + secureRandomInt(8) + ]; + const score = secureRandomInt(100) + 1; + + let level: string; + if (score >= 80) level = "⭐⭐⭐⭐⭐ 大吉"; + else if (score >= 60) level = "⭐⭐⭐⭐ 中吉"; + else if (score >= 40) level = "⭐⭐⭐ 小吉"; + else if (score >= 20) level = "⭐⭐ 平"; + else level = "⭐ 需谨慎"; + + return { + success: true, + data: { + luckyColor, + luckyNumber, + luckyDirection: + config.language === "zh" + ? luckyDirection + : luckyDirection + " direction", + score, + level, + message, + }, + 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 }, + }; + } + }, +}; diff --git a/server/service/agent-tool/index.ts b/server/service/agent-tool/index.ts index a22c7e8..b7ac3fa 100644 --- a/server/service/agent-tool/index.ts +++ b/server/service/agent-tool/index.ts @@ -27,6 +27,16 @@ import { parseUserInfoConfig, DEFAULT_USER_INFO_CONFIG } from "./executors/user- import { userInfoExecutor, userInfoInputSchema } from "./executors/user-info/user-info"; import { parseSearchConfig, DEFAULT_SEARCH_CONFIG } from "./executors/search/config"; import { searchExecutor, searchInputSchema } from "./executors/search/search"; +import { parseHashConfig, DEFAULT_HASH_CONFIG } from "./executors/hash/config"; +import { hashExecutor, hashInputSchema } from "./executors/hash/hash"; +import { parseColorConfig, DEFAULT_COLOR_CONFIG } from "./executors/color/config"; +import { colorExecutor, colorInputSchema } from "./executors/color/color"; +import { parseDiceConfig, DEFAULT_DICE_CONFIG } from "./executors/dice/config"; +import { diceExecutor, diceInputSchema } from "./executors/dice/dice"; +import { parseMagic8ballConfig, DEFAULT_MAGIC8BALL_CONFIG } from "./executors/magic-8ball/config"; +import { magic8ballExecutor, magic8ballInputSchema } from "./executors/magic-8ball/magic-8ball"; +import { parseGuessWhoConfig, DEFAULT_GUESS_WHO_CONFIG } from "./executors/guess-who/config"; +import { guessWhoExecutor, guessWhoInputSchema } from "./executors/guess-who/guess-who"; // ============ 工具类型注册表 ============ interface ToolTypeRegistration { @@ -91,6 +101,36 @@ const TOOL_TYPE_REGISTRY: Record = { defaultConfig: DEFAULT_SEARCH_CONFIG, zodSchema: searchInputSchema, }, + hash: { + executor: hashExecutor, + parseConfig: parseHashConfig, + defaultConfig: DEFAULT_HASH_CONFIG, + zodSchema: hashInputSchema, + }, + color: { + executor: colorExecutor, + parseConfig: parseColorConfig, + defaultConfig: DEFAULT_COLOR_CONFIG, + zodSchema: colorInputSchema, + }, + dice: { + executor: diceExecutor, + parseConfig: parseDiceConfig, + defaultConfig: DEFAULT_DICE_CONFIG, + zodSchema: diceInputSchema, + }, + "magic-8ball": { + executor: magic8ballExecutor, + parseConfig: parseMagic8ballConfig, + defaultConfig: DEFAULT_MAGIC8BALL_CONFIG, + zodSchema: magic8ballInputSchema, + }, + "guess-who": { + executor: guessWhoExecutor, + parseConfig: parseGuessWhoConfig, + defaultConfig: DEFAULT_GUESS_WHO_CONFIG, + zodSchema: guessWhoInputSchema, + }, }; // 立即注册所有工具类型 @@ -574,4 +614,14 @@ export { DEFAULT_USER_INFO_CONFIG } from "./executors/user-info/config"; export type { UserInfoToolConfig } from "./executors/user-info/config"; export { DEFAULT_SEARCH_CONFIG } from "./executors/search/config"; export type { SearchToolConfig } from "./executors/search/config"; +export { DEFAULT_HASH_CONFIG } from "./executors/hash/config"; +export type { HashToolConfig } from "./executors/hash/config"; +export { DEFAULT_COLOR_CONFIG } from "./executors/color/config"; +export type { ColorToolConfig } from "./executors/color/config"; +export { DEFAULT_DICE_CONFIG } from "./executors/dice/config"; +export type { DiceToolConfig } from "./executors/dice/config"; +export { DEFAULT_MAGIC8BALL_CONFIG } from "./executors/magic-8ball/config"; +export type { Magic8ballToolConfig } from "./executors/magic-8ball/config"; +export { DEFAULT_GUESS_WHO_CONFIG } from "./executors/guess-who/config"; +export type { GuessWhoToolConfig } from "./executors/guess-who/config"; export type { ToolExecutor, ToolContext, ToolResult } from "./registry";