Compare commits

..

30 Commits

Author SHA1 Message Date
Tim
d98c3644a6 fix: 添加GitHub action 2025-10-28 19:55:29 +08:00
Tim
dbb63a4039 feat(bot): add open source reply bot 2025-10-28 19:53:07 +08:00
Tim
8db928b9a8 Merge pull request #1119 from nagisa77/codex/update-lottery-time-from-23-to-15
Adjust coffee bot draw schedule
2025-10-28 18:58:05 +08:00
Tim
46f6ccb3a8 Adjust coffee draw schedule 2025-10-28 18:57:53 +08:00
Tim
87dcebf052 fix: tools 重写 2025-10-28 18:50:34 +08:00
Tim
0ad4f4feff fix: fix tools 2025-10-28 18:48:32 +08:00
Tim
a227ac77fb Merge pull request #1118 from nagisa77/codex/add-daily-weather-lookup-for-cities
fix: 透传token
2025-10-28 18:45:30 +08:00
Tim
ef53a40ed5 fix: 透传token 2025-10-28 18:44:43 +08:00
Tim
7d8c9b68bd Merge pull request #1117 from nagisa77/codex/add-daily-weather-lookup-for-cities
Integrate weather MCP into Coffee Bot
2025-10-28 18:43:15 +08:00
Tim
dbc3d54fa1 fix: should add weather mcp 2025-10-28 18:42:47 +08:00
Tim
4c0b9e744a feat: weather mcp 2025-10-28 18:39:58 +08:00
Tim
4b4d1a2a86 Merge branch 'main' into codex/add-daily-weather-lookup-for-cities
# Conflicts:
#	bots/instance/coffee_bot.ts
2025-10-28 18:39:44 +08:00
Tim
6990aa93ed Integrate weather MCP into Coffee Bot 2025-10-28 18:32:42 +08:00
Tim
421b8b6b4f fix: prompt 修改 2025-10-28 18:08:19 +08:00
Tim
e55acc6dc4 fix: 解决时区问题 2025-10-28 18:01:03 +08:00
Tim
33ce56aa31 fix: 解决时区问题 2025-10-28 17:58:13 +08:00
Tim
339c39c6ca fix: 时区设置 2025-10-28 17:55:36 +08:00
Tim
389961c922 fix: 修正prompt 2025-10-28 17:47:00 +08:00
Tim
6db53274fb fix: 修正日期 2025-10-28 17:44:14 +08:00
Tim
a413c0be35 fix: 修正语法 2025-10-28 17:35:01 +08:00
Tim
06ecd39c8b Merge branch 'main' of github.com:nagisa77/OpenIsle
# Conflicts:
#	bots/instance/coffee_bot.ts
2025-10-28 17:34:47 +08:00
Tim
f0ba00b7e8 fix: 修正抽奖贴问题 2025-10-28 17:33:23 +08:00
Tim
092c4c36c2 Merge pull request #1116 from nagisa77/codex/create-post-for-coffee-bot-with-categories-and-tags
Update coffee bot post metadata instructions
2025-10-28 17:27:52 +08:00
Tim
db13f8145d Update coffee bot post metadata instructions 2025-10-28 17:27:37 +08:00
Tim
3be396976a Merge pull request #1115 from nagisa77/codex/fix-coffee-bot-mcp-service-error
Validate MCP post creation inputs
2025-10-28 17:23:21 +08:00
Tim
3fbaa332fc Validate required fields for MCP post creation 2025-10-28 17:22:58 +08:00
Tim
4e6cb59753 fix: 修正语法问题 2025-10-28 16:49:44 +08:00
Tim
1c6c17e577 fix: 修正语法问题 2025-10-28 16:47:00 +08:00
Tim
c968efa42a Revert "Fix search client reply argument order"
This reverts commit 7a2cf829c7.
2025-10-28 16:38:53 +08:00
Tim
0cd5ded39b Merge pull request #1114 from nagisa77/codex/fix-syntaxerror-in-search_client.py
Fix reply methods argument order in MCP search client
2025-10-28 16:33:05 +08:00
8 changed files with 158 additions and 51 deletions

View File

@@ -2,7 +2,7 @@ name: Coffee Bot
on:
schedule:
- cron: "0 1 * * *"
- cron: "0 1 * * 1-5"
workflow_dispatch:
jobs:
@@ -26,4 +26,5 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN }}
APIFY_API_TOKEN: ${{ secrets.APIFY_API_TOKEN }}
run: npx tsx bots/instance/coffee_bot.ts

View File

@@ -0,0 +1,30 @@
name: Open Source Reply Bot
on:
schedule:
- cron: "*/30 * * * *"
workflow_dispatch:
jobs:
run-open-source-reply-bot:
environment: Bots
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Install dependencies
run: npm install --no-save @openai/agents tsx typescript
- name: Run open source reply bot
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN_BOT_1 }}
APIFY_API_TOKEN: ${{ secrets.APIFY_API_TOKEN }}
run: npx tsx bots/instance/open_source_reply_bot.ts

View File

@@ -26,4 +26,5 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENISLE_TOKEN: ${{ secrets.OPENISLE_TOKEN }}
APIFY_API_TOKEN: ${{ secrets.APIFY_API_TOKEN }}
run: npx tsx bots/instance/reply_bot.ts

View File

@@ -3,40 +3,31 @@ import { Agent, Runner, hostedMcpTool, withTrace } from "@openai/agents";
export type WorkflowInput = { input_as_text: string };
export abstract class BotFather {
protected readonly allowedMcpTools = [
"search",
"create_post",
"reply_to_post",
"reply_to_comment",
"recent_posts",
"get_post",
"list_unread_messages",
"mark_notifications_read",
"create_post",
];
protected readonly openisleToken = (process.env.OPENISLE_TOKEN ?? "").trim();
protected readonly weatherToken = (process.env.APIFY_API_TOKEN ?? "").trim();
protected readonly mcp = this.createHostedMcpTool();
protected readonly weatherMcp = this.createWeatherMcpTool();
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; it will be attached to MCP requests."
: "🔓 OPENISLE_TOKEN not set; authenticated MCP tools may be unavailable."
);
console.log(
this.weatherToken
? "☁️ APIFY_API_TOKEN detected; weather MCP server will be available."
: "🌥️ APIFY_API_TOKEN not set; weather updates will be unavailable."
);
this.agent = new Agent({
name: this.name,
instructions: this.buildInstructions(),
tools: [this.mcp],
tools: [this.mcp, this.weatherMcp],
model: "gpt-4o",
modelSettings: {
temperature: 0.7,
@@ -78,12 +69,35 @@ export abstract class BotFather {
return hostedMcpTool({
serverLabel: "openisle_mcp",
serverUrl: "https://www.open-isle.com/mcp",
allowedTools: this.allowedMcpTools,
allowedTools: [
"search", // 用于搜索帖子、内容等
"create_post", // 创建新帖子
"reply_to_post", // 回复帖子
"reply_to_comment", // 回复评论
"recent_posts", // 获取最新帖子
"get_post", // 获取特定帖子的详细信息
"list_unread_messages", // 列出未读消息或通知
"mark_notifications_read", // 标记通知为已读
],
requireApproval: "never",
...authConfig,
});
}
private createWeatherMcpTool(): ReturnType<typeof hostedMcpTool> {
return hostedMcpTool({
serverLabel: "weather_mcp_server",
serverUrl: "https://jiri-spilka--weather-mcp-server.apify.actor/mcp",
requireApproval: "never",
allowedTools: [
"get_current_weather", // 天气 MCP 工具
],
headers: {
Authorization: `Bearer ${this.weatherToken || ""}`,
},
});
}
protected getAdditionalInstructions(): string[] {
return [];
}

View File

@@ -14,39 +14,31 @@ class CoffeeBot extends BotFather {
"正文需亲切友好,简洁明了,鼓励社区成员互动。",
"开奖说明需明确告知中奖者需私聊站长 @nagisa 领取奖励。",
"确保只发布一个帖子,避免重复调用 create_post。",
"使用标签为 weather_mcp_server 的 MCP 工具获取北京、上海、广州、深圳当天的天气信息,并把结果写入早安问候之后。",
];
}
protected override getCliQuery(): string {
const now = new Date();
const beijingNow = new Date(
now.toLocaleString("en-US", { timeZone: "Asia/Shanghai" })
);
const weekday = WEEKDAY_NAMES[beijingNow.getDay()];
const drawTime = new Date(beijingNow);
const now = new Date(Date.now() + 8 * 60 * 60 * 1000);
const weekday = WEEKDAY_NAMES[now.getDay()];
const drawTime = new Date(now);
drawTime.setHours(15, 0, 0, 0);
const drawTimeText = drawTime
.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
timeZone: "Asia/Shanghai",
})
.replace(/^24:/, "00:");
return `
请立即在 https://www.open-isle.com 使用 create_post 发表一篇全新帖子,遵循以下要求:
请立即在 https://www.open-isle.com 使用 create_post 发表一篇帖子,遵循以下要求:
1. 标题固定为「大家星期${weekday}早安--抽一杯咖啡」。
2. 正文包含:
- 亲切的早安问候;
- 明确奖品写作“Coffee x 1”
- 奖品图片链接https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/0d6a9b33e9ca4fe5a90540187d3f9ecb.png
- 公布开奖时间为今天下午 15:00北京时间写成 ${drawTimeText}
- 标注“领奖请私聊站长 @nagisa”
- 早安问候后立即列出北京、上海、广州、深圳当天的天气信息,每行格式为“城市:天气描述,最低温~最高温”;天气需调用 weather_mcp_server 获取
- 标注“领奖请私聊站长 @[nagisa]”
- 鼓励大家留言互动。
3. 帖子语言使用简体中文,格式可用 Markdown使关键信息醒目。
4. 完成后只输出“已发布咖啡抽奖贴”,不额外生成总结。
3. 奖品信息
- 明确奖品写作“Coffee”
- 帖子类型必须为 LOTTERY
- 奖品图片链接https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/0d6a9b33e9ca4fe5a90540187d3f9ecb.png
- 公布开奖时间为 ${drawTime}, 直接传UTC时间给接口不要考虑时区问题
- categoryId 固定为 10tagIds 设为 [36]。
4. 帖子语言使用简体中文。
`.trim();
}
}

View File

@@ -0,0 +1,65 @@
import { readFileSync } from "node:fs";
import path from "node:path";
import { BotFather, WorkflowInput } from "../bot_father";
class OpenSourceReplyBot extends BotFather {
constructor() {
super("OpenSource Reply Bot");
}
protected override getAdditionalInstructions(): string[] {
const knowledgeBase = this.loadKnowledgeBase();
return [
"You are OpenSourceReplyBot, a professional helper who focuses on answering open-source development and code-related questions for the OpenIsle community.",
"Respond in Chinese using well-structured Markdown sections such as 标题、列表、代码块等,让回复清晰易读。",
"保持语气专业、耐心、详尽,绝不使用表情符号或颜文字,也不要卖萌。",
"优先解答与项目代码、贡献流程、架构设计或排错相关的问题;如果消息与此无关,请礼貌说明并跳过。",
"在需要时引用 README.md 与 CONTRIBUTING.md 中的要点,帮助用户快速定位文档位置。",
knowledgeBase,
].filter(Boolean);
}
protected override getCliQuery(): string {
return `
【AUTO】每30分钟自动巡检未读提及与评论严格遵守以下流程
1调用 list_unread_messages 获取待处理的“提及/评论”;
2按时间从新到旧逐条处理最多10条如需上下文请调用 get_post
3仅对与开源项目、代码实现或贡献流程直接相关的问题生成详尽的 Markdown 中文回复,
若与主题无关则礼貌说明并跳过;
4回复时引用 README 或 CONTRIBUTING 中的要点(如适用),并优先给出可执行的排查步骤或代码建议;
5回复评论使用 reply_to_comment回复帖子使用 reply_to_post
6若某通知最后一条已由本 bot 回复,则跳过避免重复;
7整理已处理通知 ID 调用 mark_notifications_read
8结束时输出包含处理条目概览URL或ID的总结。`.trim();
}
private loadKnowledgeBase(): string {
const docs = ["../../README.md", "../../CONTRIBUTING.md"];
const sections: string[] = [];
for (const relativePath of docs) {
try {
const absolutePath = path.resolve(__dirname, relativePath);
const content = readFileSync(absolutePath, "utf-8").trim();
if (content) {
sections.push(`以下是 ${path.basename(absolutePath)} 的内容:\n${content}`);
}
} catch (error) {
sections.push(`未能加载 ${relativePath},请检查文件路径或权限。`);
}
}
return sections.join("\n\n");
}
}
const openSourceReplyBot = new OpenSourceReplyBot();
export const runWorkflow = async (workflow: WorkflowInput) => {
return openSourceReplyBot.runWorkflow(workflow);
};
if (require.main === module) {
openSourceReplyBot.runCli();
}

View File

@@ -74,7 +74,7 @@ class SearchClient:
def _build_headers(
self,
*,
token: str | None = None,
token: str,
accept: str = "application/json",
include_json: bool = False,
) -> dict[str, str]:
@@ -111,9 +111,8 @@ class SearchClient:
async def reply_to_comment(
self,
comment_id: int,
token: str,
content: str,
*,
token: str | None = None,
captcha: str | None = None,
) -> dict[str, Any]:
"""Reply to an existing comment and return the created reply."""
@@ -145,9 +144,8 @@ class SearchClient:
async def reply_to_post(
self,
post_id: int,
token: str,
content: str,
*,
token: str | None = None,
captcha: str | None = None,
) -> dict[str, Any]:
"""Create a comment on a post and return the backend payload."""
@@ -180,7 +178,7 @@ class SearchClient:
self,
payload: dict[str, Any],
*,
token: str | None = None,
token: str,
) -> dict[str, Any]:
"""Create a new post and return the detailed backend payload."""
@@ -200,7 +198,7 @@ class SearchClient:
)
response.raise_for_status()
body = self._ensure_dict(response.json())
logger.info("Post creation succeeded with id=%s", body.get("id"))
logger.info("Post creation succeeded with id=%s, token=%s", body.get("id"), token)
return body
async def recent_posts(self, minutes: int) -> list[dict[str, Any]]:
@@ -251,7 +249,7 @@ class SearchClient:
*,
page: int = 0,
size: int = 30,
token: str | None = None,
token: str,
) -> list[dict[str, Any]]:
"""Return unread notifications for the authenticated user."""
@@ -287,7 +285,7 @@ class SearchClient:
self,
ids: list[int],
*,
token: str | None = None,
token: str
) -> None:
"""Mark the provided notifications as read for the authenticated user."""

View File

@@ -509,6 +509,8 @@ async def create_post(
raise ValueError("Category identifier must be an integer.") from exc
if sanitized_category_id <= 0:
raise ValueError("Category identifier must be a positive integer.")
if sanitized_category_id is None:
raise ValueError("A category identifier is required to create a post.")
sanitized_tag_ids: list[int] | None = None
if tag_ids is not None:
@@ -525,6 +527,10 @@ async def create_post(
sanitized_tag_ids.append(converted)
if not sanitized_tag_ids:
sanitized_tag_ids = None
if not sanitized_tag_ids:
raise ValueError("At least one tag identifier is required to create a post.")
if len(sanitized_tag_ids) > 2:
raise ValueError("At most two tag identifiers can be provided for a post.")
sanitized_post_type = post_type.strip() if isinstance(post_type, str) else None
if sanitized_post_type == "":