Feat/multi channel im - #892
Conversation
梳理微信 clawbot(=openclaw 微信通道)接入 botmux 的完整方案: 现状盘点、openclaw 机制、botmux 通道接线点调研、推荐架构(方案A)、 分阶段实施步骤、备选项与风险。app_id 等敏感信息已脱敏。 Co-Authored-By: Claude <noreply@anthropic.com>
- src/im/weixin/:微信 Clawbot 通道(client/adapter/index),镜像 openclaw-weixin 协议长轮询 getUpdates + sendmessage,context_token 按用户 缓存落盘;ImAdapter 实现,复用 Telegram 骨架,无卡片文本渲染。 - BotConfig 新增 channel?: 'lark'|'weixin'(缺省 lark,飞书主路径字节不变)。 - Telegram 适配器:修正 StreamStatus 导入路径、senderId 类型转换、状态徽标 用合法 StreamStatus 值,使 pnpm build 恢复绿。 - .gitignore 加固:忽略 bots.json/token/secret 等敏感文件,防 fork 公开仓泄漏。 微信凭证 token 运行时从 openclaw 账户文件读取,不入库。daemon 会话接线为后续增量。
上游已内置 Telegram/Weixin 两个 ImAdapter(src/im/telegram、src/im/weixin), 但 daemon 消息路由仍走飞书硬编码路径,channel 字段此前只是配置面。本次让 channel 字段真正可路由: - bot-registry: channel 联合类型由 'lark'|'weixin' 扩展为含 'telegram', 新增 telegramBotToken / wechatClawbotId 两个通道凭证字段 - 新增 src/im/channel-factory.ts:按 channel 构造对应 ImAdapter(telegram 用 token,weixin 从 openclaw 账户文件读凭证),lark 返回 undefined 保持 现有飞书路径字节不变 - 新增 telegram adapter 单测 test/telegram-adapter.test.ts(mock Bot API, 覆盖 getMe/派发/sendMessage/updateMessage/addReaction,4 用例全绿) - bots.json.example 演示 telegram/weixin 配置示例 影响面:仅新增配置字段与接线层,未触碰 daemon 现有飞书路径(回归面最小)。 验证:pnpm build 通过;telegram 单测 4/4;完整单测 14342 用例中 20 个失败 均为上游环境相关用例(终端宽度渲染/PID 命名空间/worker 时序),与本改动无关。
承接 f21f583 的 channel-factory 接线层,本次让 weixin/telegram bot 真正跑通「入站 -> 会话/worker -> 出站」全链路: - 新增 src/im/channel-registry.ts:按 larkAppId 保存非飞书 bot 的 ImAdapter,作为出站路由数据源 - 新增 src/im/channel-bridge.ts:把 ImMessage 合成最小飞书事件 data + RoutingContext 灌进既有 botEventHandlers(复用全部会话/worker/CLI 逻辑);按 isSessionOwner 决定续话 vs 新话题;卡片 JSON -> 纯文本 - src/im/lark/client.ts:sendMessage/replyMessage/updateMessage/ addReaction/removeReaction/deleteMessage 顶部加通道分支——注册了 非飞书 adapter 的 bot 路由到 adapter,否则走原飞书路径(字节不变) - src/daemon.ts:按 isLarkChannel 跳过非飞书 bot 的飞书专属启动步骤 (open_id 探测 / scope 校验 / 改名改头像 / 文档订阅 / WSClient 订阅), 改由 createChannelAdapter + adapter.start() 启动对应通道 - 新增单测 test/channel-bridge.test.ts(7 用例全绿);tsc --noEmit 绿 - docs:微信 clawbot 方案状态更新为 Track 2 接线完成(v0.3) 影响面:飞书 bot 从不注册 adapter,所有通道分支对其不可达,回归面最小。 Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7ee38f7c00
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| * 微信凭证长轮询,消息进 botmux 会话机制。当前 `'weixin'`/`'telegram'` 通道的 | ||
| * 完整 daemon 会话接线仍在推进:本字段是配置面,缺省不改变任何现行为。 | ||
| */ | ||
| channel?: 'lark' | 'telegram' | 'weixin'; |
There was a problem hiding this comment.
Preserve the channel fields while normalizing bot configs
loadBotConfigs constructs each BotConfig explicitly around line 2664 but never copies channel, telegramBotToken, or wechatClawbotId. Consequently every entry loaded from bots.json reaches the daemon with channel === undefined, is treated as Lark, and never starts either new adapter; copy and validate all three fields during normalization.
AGENTS.md reference: AGENTS.md:L52-L59
Useful? React with 👍 / 👎.
| "allowedUsers": ["erin@company.com"], | ||
| "channel": "telegram", | ||
| "larkAppId": "tg_my_bot1", | ||
| "larkAppSecret": "", |
There was a problem hiding this comment.
Exempt non-Lark bots from the Lark credential lifecycle
The documented Telegram/Weixin entries use an empty larkAppSecret, but loadBotConfigs rejects every non-apiOnly empty secret at lines 2403-2408. Even after relaxing that validation, registerBot(cfg) runs before the new channel check and constructs a Lark SDK client for every non-apiOnly bot, which requires a secret. Treat non-Lark channels like transport-disabled bots in both validation and client construction so these examples can boot.
AGENTS.md reference: AGENTS.md:L52-L59
Useful? React with 👍 / 👎.
| const [chatId, msgId] = this.splitMessageKey(messageId); | ||
| const msg = await this.client.sendMessage(Number(chatId), content, { | ||
| parseMode: format === 'rich' ? 'MarkdownV2' : 'Markdown', | ||
| replyToMessageId: Number(msgId), |
There was a problem hiding this comment.
Accept Telegram thread keys when replying
Every Telegram inbound is bridged as a thread-scoped session anchored at msg.threadId, such as tg:888, so normal session output calls this method with that value. splitMessageKey('tg:888') produces ['tg', '888'], and Number(chatId) becomes NaN; the Bot API therefore receives an invalid chat ID and the first reply/card cannot be delivered. Parse tg:<chatId>[:<threadId>] anchors separately or carry a distinct reply target.
AGENTS.md reference: AGENTS.md:L54-L58
Useful? React with 👍 / 👎.
| parseMode: format === 'rich' ? 'MarkdownV2' : 'Markdown', | ||
| replyToMessageId: tgThread, | ||
| }); | ||
| return String(msg.message_id); |
There was a problem hiding this comment.
Return a routable Telegram message key
sendMessage returns only Telegram's message ID, while updateMessage, addReaction, and removeReaction require the composite <chatId>:<messageId> key. Any non-thread outbound path that persists this result—such as a scheduled/dashboard message or card—later interprets the message ID as both chat and message ID and edits/reacts in the wrong chat; return this.messageKey(msg.chat.id, msg.message_id) consistently.
Useful? React with 👍 / 👎.
| } | ||
| registerChannelAdapter(cfg.larkAppId, adapter); | ||
| logger.info(`[channel] ${cfg.larkAppId} 启动 ${cfg.channel} 通道(account=${adapter.getBotUserId() ?? '-'})`); | ||
| void adapter.start(buildChannelImEventHandler(botEventHandlers, cfg.larkAppId)); |
There was a problem hiding this comment.
Observe adapter startup failures
For Telegram, adapter.start() awaits getMe() before polling, but this promise is discarded. With an invalid token or a transient startup network failure, the rejection is unhandled after the adapter has already been registered and logged as started, potentially terminating the daemon or leaving all outbound traffic routed to a channel that never polls; await or attach a rejection handler and only advertise/register a successfully started adapter.
Useful? React with 👍 / 👎.
| "larkAppSecret": "", | ||
| "wechatClawbotId": "wxid_xxxx", | ||
| "cliId": "codex", | ||
| "allowedUsers": ["zhuchengcheng@bytedance.com"], |
There was a problem hiding this comment.
Replace the real user identity in the public example
This example embeds a company email that appears to identify a real person, despite the repository rule prohibiting real group-member identities from entering public git history. Replace it with a neutral placeholder such as weixin-user-id@example.com.
AGENTS.md reference: AGENTS.md:L67-L67
Useful? React with 👍 / 👎.
|
感谢这个 PR——多通道(微信/Telegram)接入的整体架构是清晰的:channel-registry / factory / bridge 把非飞书通道桥接进现有会话逻辑,通过 channel 分支尽量保持既有飞书行为,本次 build 与所跑回归测试均绿,新增单测也都通过。以下是一份自动评审的初步意见,最终以维护者审阅为准;只陈述技术事实与修改建议。合并前建议先处理以下几点(按严重度)。 需要说明的是:新增的 11 个单测只覆盖 adapter 单体,没有覆盖 阻断级1. Telegram 正常会话回复必失败(chat_id=NaN) 2. 微信白名单身份空间冲突(配了必锁死 / 不配则放开) 3. Telegram 群聊入口绕过权限/寻址层 应修4. daemon 启动期仍残留飞书依赖(不止一行) 5. 微信 context_token / sync cursor 未持久化,重启后连续性断档 6. Telegram 出站契约还不闭环 清理级(可作为附带修改)7. 仓库卫生
另外,分支目前与 |
No description provided.