From 6f036eb4fef15514e4be5012527ec2d873ad36b4 Mon Sep 17 00:00:00 2001 From: Tim <135014430+nagisa77@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:54:49 +0800 Subject: [PATCH] Refactor reply bot with BotFather base class --- bots/bot_father.ts | 137 ++++++++++++++++++++++++++++++++++++++++++ bots/reply_bots.ts | 146 ++++++++------------------------------------- 2 files changed, 163 insertions(+), 120 deletions(-) create mode 100644 bots/bot_father.ts diff --git a/bots/bot_father.ts b/bots/bot_father.ts new file mode 100644 index 000000000..bebf1334e --- /dev/null +++ b/bots/bot_father.ts @@ -0,0 +1,137 @@ +import { Agent, Runner, hostedMcpTool, withTrace } from "@openai/agents"; + +export type WorkflowInput = { input_as_text: string }; + +export abstract class BotFather { + protected readonly allowedMcpTools = [ + "search", + "reply_to_post", + "reply_to_comment", + "recent_posts", + "get_post", + "list_unread_messages", + "mark_notifications_read", + ]; + + protected readonly mcp = hostedMcpTool({ + serverLabel: "openisle_mcp", + serverUrl: "https://www.open-isle.com/mcp", + allowedTools: this.allowedMcpTools, + requireApproval: "never", + }); + + protected readonly openisleToken = process.env.OPENISLE_TOKEN ?? ""; + protected readonly agent: Agent; + + constructor(protected readonly name: string) { + console.log(`✅ ${this.name} starting...`); + console.log( + "🛠️ Configured Hosted MCP tools:", + this.allowedMcpTools.join(", ") + ); + + console.log( + this.openisleToken + ? "🔑 OPENISLE_TOKEN detected in environment." + : "🔓 OPENISLE_TOKEN not set; agent will request it if required." + ); + + this.agent = new Agent({ + name: this.name, + instructions: this.buildInstructions(), + tools: [this.mcp], + model: "gpt-4o", + modelSettings: { + temperature: 0.7, + topP: 1, + maxTokens: 2048, + toolChoice: "auto", + store: true, + }, + }); + } + + protected buildInstructions(): string { + const instructions = [ + ...this.getBaseInstructions(), + ...this.getAdditionalInstructions(), + ].filter(Boolean); + return instructions.join("\n"); + } + + protected getBaseInstructions(): string[] { + return [ + "You are a helpful assistant for https://www.open-isle.com.", + "Finish tasks end-to-end before replying. If multiple MCP tools are needed, call them sequentially until the task is truly done.", + "When presenting the result, reply in Chinese with a concise summary and include any important URLs or IDs.", + this.openisleToken + ? `If tools require auth, use this token exactly where the tool schema expects it: ${this.openisleToken}` + : "If a tool requires auth, ask me to provide OPENISLE_TOKEN via env.", + "After finishing replies, call mark_notifications_read with all processed notification IDs to keep the inbox clean.", + ]; + } + + protected getAdditionalInstructions(): string[] { + return []; + } + + protected createRunner(): Runner { + return new Runner({ + workflowName: this.name, + traceMetadata: { + __trace_source__: "agent-builder", + workflow_id: "wf_69003cbd47e08190928745d3c806c0b50d1a01cfae052be8", + }, + }); + } + + public async runWorkflow(workflow: WorkflowInput) { + if (!process.env.OPENAI_API_KEY) { + throw new Error("Missing OPENAI_API_KEY"); + } + + const runner = this.createRunner(); + + return await withTrace(`${this.name} run`, async () => { + const preview = workflow.input_as_text.trim(); + console.log( + "📝 Received workflow input (preview):", + preview.length > 200 ? `${preview.slice(0, 200)}…` : preview + ); + + console.log("🚦 Starting agent run with maxTurns=16..."); + const result = await runner.run(this.agent, workflow.input_as_text, { + maxTurns: 16, + }); + + console.log("📬 Agent run completed. Result keys:", Object.keys(result)); + + if (!result.finalOutput) { + throw new Error("Agent result is undefined (no final output)."); + } + + const openisleBotResult = { output_text: String(result.finalOutput) }; + + console.log( + "🤖 Agent result (length=%d):\n%s", + openisleBotResult.output_text.length, + openisleBotResult.output_text + ); + return openisleBotResult; + }); + } + + protected abstract getCliQuery(): string; + + public async runCli(): Promise { + try { + const query = this.getCliQuery(); + console.log("🔍 Running workflow..."); + await this.runWorkflow({ input_as_text: query }); + process.exit(0); + } catch (err: any) { + console.error("❌ Agent failed:", err?.stack || err); + process.exit(1); + } + } +} diff --git a/bots/reply_bots.ts b/bots/reply_bots.ts index 4242e86c8..f07a68a0d 100644 --- a/bots/reply_bots.ts +++ b/bots/reply_bots.ts @@ -1,131 +1,37 @@ // reply_bot.ts -import { Agent, Runner, hostedMcpTool, withTrace } from "@openai/agents"; +import { BotFather, WorkflowInput } from "./bot_father"; -console.log("✅ Reply bot starting..."); - -const allowedMcpTools = [ - "search", - "reply_to_post", - "reply_to_comment", - "recent_posts", - "get_post", - "list_unread_messages", - "mark_notifications_read", -]; - -console.log("🛠️ Configured Hosted MCP tools:", allowedMcpTools.join(", ")); - -// ---- MCP 工具(Hosted MCP) ---- -// 关键点:requireApproval 设为 "never",避免卡在人工批准。 -const mcp = hostedMcpTool({ - serverLabel: "openisle_mcp", - serverUrl: "https://www.open-isle.com/mcp", - allowedTools: allowedMcpTools, - requireApproval: "never", -}); - -type WorkflowInput = { input_as_text: string }; - -// 从环境变量读取你的站点鉴权令牌(可选) -const OPENISLE_TOKEN = process.env.OPENISLE_TOKEN ?? ""; - -console.log( - OPENISLE_TOKEN - ? "🔑 OPENISLE_TOKEN detected in environment." - : "🔓 OPENISLE_TOKEN not set; agent will request it if required." -); - -// ---- 定义 Agent ---- -const openisleBot = new Agent({ - name: "OpenIsle Bot", - instructions: [ - "You are a helpful and cute assistant for https://www.open-isle.com. Please use plenty of kawaii kaomoji (颜表情), such as (๑˃ᴗ˂)ﻭ, (•̀ω•́)✧, (。•ᴗ-)_♡, (⁎⁍̴̛ᴗ⁍̴̛⁎), etc., in your replies to create a friendly, adorable vibe.", - "Finish tasks end-to-end before replying. If multiple MCP tools are needed, call them sequentially until the task is truly done.", - "When presenting the result, reply in Chinese with a concise, cute summary filled with kaomoji and include any important URLs or IDs.", - OPENISLE_TOKEN - ? `If tools require auth, use this token exactly where the tool schema expects it: ${OPENISLE_TOKEN}` - : "If a tool requires auth, ask me to provide OPENISLE_TOKEN via env.", - "After finishing replies, call mark_notifications_read with all processed notification IDs to keep the inbox clean.", - ].join("\n"), - tools: [mcp], - model: "gpt-4o", - modelSettings: { - temperature: 0.7, - topP: 1, - maxTokens: 2048, - toolChoice: "auto", - store: true, - }, -}); - -// ---- 入口函数:跑到拿到 finalOutput 为止,然后输出并退出 ---- -export const runWorkflow = async (workflow: WorkflowInput) => { - // 强烈建议在外部(shell)设置 OPENAI_API_KEY - if (!process.env.OPENAI_API_KEY) { - throw new Error("Missing OPENAI_API_KEY"); +class ReplyBot extends BotFather { + constructor() { + super("OpenIsle Bot"); } - const runner = new Runner({ - workflowName: "OpenIsle Bot", - traceMetadata: { - __trace_source__: "agent-builder", - workflow_id: "wf_69003cbd47e08190928745d3c806c0b50d1a01cfae052be8", - }, - // 如需完全禁用上报可加:tracingDisabled: true - }); + protected override getAdditionalInstructions(): string[] { + return [ + "You are a helpful and cute assistant for https://www.open-isle.com. Please use plenty of kawaii kaomoji (颜表情), such as (๑˃ᴗ˂)ﻭ, (•̀ω•́)✧, (。•ᴗ-)_♡, (⁎⁍̴̛ᴗ⁍̴̛⁎), etc., in your replies to create a friendly, adorable vibe.", + "When presenting the result, reply in Chinese with a concise, cute summary filled with kaomoji and include any important URLs or IDs.", + ]; + } - return await withTrace("OpenIsle Bot run", async () => { - const preview = workflow.input_as_text.trim(); - console.log( - "📝 Received workflow input (preview):", - preview.length > 200 ? `${preview.slice(0, 200)}…` : preview - ); + protected override getCliQuery(): string { + return ` + 【AUTO】无需确认,自动处理所有未读的提及与评论: + 1)调用 list_unread_messages; + 2)依次处理每条“提及/评论”:如需上下文则使用 get_post 获取,生成简明中文回复;如有 commentId 则用 reply_to_comment,否则用 reply_to_post; + 3)跳过关注和系统事件; + 4)保证幂等性:如该贴最后一条是你自己发的回复,则跳过; + 5)调用 mark_notifications_read,传入本次已处理的通知 ID 清理已读; + 6)最多只处理最新10条;结束时仅输出简要摘要(包含URL或ID)。 + `.trim(); + } +} - // Runner.run 会自动循环执行:LLM → 工具 → 直至 finalOutput - console.log("🚦 Starting agent run with maxTurns=16..."); - const result = await runner.run(openisleBot, workflow.input_as_text, { - maxTurns: 16, // 允许更复杂任务多轮调用 MCP - // stream: true // 如需边跑边看事件可打开,然后消费流事件 - }); +const replyBot = new ReplyBot(); - console.log("📬 Agent run completed. Result keys:", Object.keys(result)); - - if (!result.finalOutput) { - // 若没产出最终结果,通常是启用了人工批准/工具失败/达到 maxTurns - throw new Error("Agent result is undefined (no final output)."); - } - - const openisleBotResult = { output_text: String(result.finalOutput) }; - - console.log( - "🤖 Agent result (length=%d):\n%s", - openisleBotResult.output_text.length, - openisleBotResult.output_text - ); - return openisleBotResult; - }); +export const runWorkflow = async (workflow: WorkflowInput) => { + return replyBot.runWorkflow(workflow); }; -// ---- CLI 运行(示范)---- if (require.main === module) { - (async () => { - try { - const query = ` - 【AUTO】无需确认,自动处理所有未读的提及与评论: - 1)调用 list_unread_messages; - 2)依次处理每条“提及/评论”:如需上下文则使用 get_post 获取,生成简明中文回复;如有 commentId 则用 reply_to_comment,否则用 reply_to_post; - 3)跳过关注和系统事件; - 4)保证幂等性:如该贴最后一条是你自己发的回复,则跳过; - 5)调用 mark_notifications_read,传入本次已处理的通知 ID 清理已读; - 6)最多只处理最新10条;结束时仅输出简要摘要(包含URL或ID)。 - `; - - console.log("🔍 Running workflow..."); - await runWorkflow({ input_as_text: query }); - process.exit(0); - } catch (err: any) { - console.error("❌ Agent failed:", err?.stack || err); - process.exit(1); - } - })(); + replyBot.runCli(); }