You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
8.8 KiB
246 lines
8.8 KiB
import { z } from "zod";
|
|
import { randomBytes } from "node:crypto";
|
|
import type { JSONSchema7 } from "json-schema";
|
|
import type { ToolExecutor, ToolContext, ToolResult } from "../../registry";
|
|
import type { 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 },
|
|
};
|
|
}
|
|
},
|
|
};
|
|
|