Browse Source

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.
feat/agent-independence
npmrun 2 months ago
parent
commit
f64434a22e
  1. BIN
      packages/drizzle-pkg/db.sqlite
  2. 60
      packages/drizzle-pkg/seed.ts
  3. 406
      server/service/agent-tool/executors/color/color.ts
  4. 22
      server/service/agent-tool/executors/color/config.ts
  5. 19
      server/service/agent-tool/executors/dice/config.ts
  6. 233
      server/service/agent-tool/executors/dice/dice.ts
  7. 24
      server/service/agent-tool/executors/hash/config.ts
  8. 127
      server/service/agent-tool/executors/hash/hash.ts
  9. 22
      server/service/agent-tool/executors/magic-8ball/config.ts
  10. 246
      server/service/agent-tool/executors/magic-8ball/magic-8ball.ts
  11. 50
      server/service/agent-tool/index.ts

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

60
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() {

406
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<typeof colorInputSchema>;
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<ColorToolConfig> = {
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<ToolResult> {
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 },
};
}
},
};

22
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<typeof colorConfigSchema>;
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;
}

19
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<typeof diceConfigSchema>;
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;
}

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

24
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<typeof hashConfigSchema>;
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;
}

127
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<typeof hashInputSchema>;
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<HashToolConfig> = {
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<ToolResult> {
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 },
};
}
},
};

22
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<typeof magic8ballConfigSchema>;
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;
}

246
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<typeof magic8ballInputSchema>;
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<Magic8ballToolConfig> = {
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<ToolResult> {
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 },
};
}
},
};

50
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<string, ToolTypeRegistration> = {
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";

Loading…
Cancel
Save