mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-11 01:21:09 +08:00
Compare commits
4 Commits
feature/bo
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f036eb4fe | ||
|
|
56fc05cb3c | ||
|
|
a55a15659b | ||
|
|
ccf6e0c7ce |
5
.github/workflows/reply-bots.yml
vendored
5
.github/workflows/reply-bots.yml
vendored
@@ -7,6 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
run-reply-bot:
|
||||
environment: Bots
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -19,10 +20,10 @@ jobs:
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --no-save @openai/agents ts-node typescript
|
||||
run: npm install --no-save @openai/agents tsx typescript
|
||||
|
||||
- name: Run reply bot
|
||||
env:
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN }}
|
||||
run: npx ts-node --esm bots/reply_bots.ts
|
||||
run: npx tsx bots/reply_bots.ts
|
||||
|
||||
137
bots/bot_father.ts
Normal file
137
bots/bot_father.ts
Normal file
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user