diff --git a/packages/drizzle-pkg/db.sqlite b/packages/drizzle-pkg/db.sqlite index ed7f57f..5f707af 100644 Binary files a/packages/drizzle-pkg/db.sqlite and b/packages/drizzle-pkg/db.sqlite differ 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";