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

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