Browse Source
- 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
11 changed files with 1209 additions and 0 deletions
Binary file not shown.
@ -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 }, |
|||
}; |
|||
} |
|||
}, |
|||
}; |
|||
@ -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; |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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 }, |
|||
}; |
|||
} |
|||
}, |
|||
}; |
|||
@ -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; |
|||
} |
|||
@ -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 }, |
|||
}; |
|||
} |
|||
}, |
|||
}; |
|||
@ -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; |
|||
} |
|||
@ -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 }, |
|||
}; |
|||
} |
|||
}, |
|||
}; |
|||
Loading…
Reference in new issue