@@ -19,18 +19,41 @@ src/lib/bridge/
1919│ ├── ir.ts # Markdown → IR 中间表示解析器(基于 markdown-it)
2020│ ├── render.ts # IR → 格式化输出的通用标记渲染器
2121│ └── telegram.ts # Telegram HTML 渲染 + 文件引用保护 + render-first 分片
22+ ├── markdown/
23+ │ ├── ... # (见上)
24+ │ └── feishu.ts # 飞书 Markdown 处理:hasComplexMarkdown / buildCardContent / buildPostContent / htmlToFeishuMarkdown
2225├── adapters/
2326│ ├── index.ts # Adapter 目录文件(side-effect import 自注册所有 adapter)
2427│ ├── telegram-adapter.ts # Telegram 长轮询 + offset 安全水位 + 图片/相册处理 + 自注册
2528│ ├── telegram-media.ts # Telegram 图片下载、尺寸选择、base64 转换
26- │ └── telegram-utils.ts # callTelegramApi / sendMessageDraft / escapeHtml / splitMessage
29+ │ ├── telegram-utils.ts # callTelegramApi / sendMessageDraft / escapeHtml / splitMessage
30+ │ └── feishu-adapter.ts # 飞书 WSClient + REST 消息收发 + typing 指示器 + 自注册
2731└── security/
2832 ├── rate-limiter.ts # 按 chat 滑动窗口限流(20 条/分钟)
2933 └── validators.ts # 路径/SessionID/危险输入校验
3034```
3135
3236## 数据流
3337
38+ ### 飞书
39+
40+ ```
41+ 飞书消息 → WSClient(WebSocket) → EventDispatcher
42+ → im.message.receive_v1 → handleIncomingEvent()
43+ → 去重(message_id LRU 1000) → 授权检查 → 群策略过滤 → @提及检查
44+ → text → parseTextContent() → enqueue()
45+ → image → downloadResource(stream/writeFile) → base64 FileAttachment → enqueue()
46+ → post → parsePostContent() 提取文本+图片 → enqueue()
47+ → /perm 文本命令 → 构造 callbackData → enqueue()
48+ → BridgeManager.runAdapterLoop() → handleMessage()
49+ → deliverResponse():
50+ → hasComplexMarkdown(代码块/表格)? → sendAsCard() [schema 2.0 markdown]
51+ → 纯文本? → sendAsPost() [msg_type: post, md tag]
52+ → 权限请求 → sendPermissionCard() [schema 2.0 卡片 + /perm 文本命令]
53+ ```
54+
55+ ### Telegram
56+
3457```
3558Telegram 消息 → TelegramAdapter.pollLoop()
3659 → 纯文本/caption → enqueue()
@@ -117,6 +140,32 @@ Claude 的回复是 Markdown 格式,Telegram 仅支持有限 HTML 标签(b/i
117140- ** 降级** :` sendPreview ` 返回 ` 'sent'|'skip'|'degrade' ` 三态。400/404(API 不支持)→ 永久降级该 chatId;429/网络错误 → 仅跳过本次。` previewDegraded ` Set 在 adapter ` stop() ` 时清空。
118141- ** 线程安全** :` processWithSessionLock ` 保证同 session 串行 → 同时刻只有一个 ` previewState ` 。多个 in-flight ` sendMessageDraft ` 安全:Telegram 对同 ` draft_id ` last-write-wins。
119142
143+ ** 13. 飞书适配器 — WSClient + 渲染分流**
144+ 飞书使用 ` @larksuiteoapi/node-sdk ` 的 ` WSClient ` (长连接 WebSocket)接收事件,` Client ` (REST)发送消息和下载资源。与 Telegram 的 HTTP 长轮询不同,WSClient 由 SDK 管理重连。消息去重使用内存 Map LRU(上限 1000),无需持久化 offset。
145+
146+ ** 14. 飞书渲染策略 — Card vs Post**
147+ Claude 回复按内容分流渲染(对齐 Openclaw 方案):
148+ - 含代码块(` ``` ` )或表格 → ` msg_type: 'interactive' ` ,schema 2.0 卡片(` { tag: 'markdown', content } ` 元素),代码高亮和表格正常渲染。
149+ - 纯文本 → ` msg_type: 'post' ` ,` { tag: 'md', text } ` 格式,渲染粗体、斜体、行内代码、链接。
150+ - 每层发送失败自动降级:card → post → text。
151+
152+ ` markdown/feishu.ts ` 的 ` hasComplexMarkdown() ` 负责路由判断,` buildCardContent() ` / ` buildPostContent() ` 构建消息体。
153+
154+ ** 15. 飞书权限交互 — 无按钮,文本命令兜底**
155+ ** 关键限制** :飞书卡片交互回调(card.action.trigger)需要 HTTP webhook 端点,不支持通过 WSClient 长连接接收。Openclaw 通过 ` http.createServer() ` + ` Lark.adaptDefault() ` 暴露公网 webhook 解决。CodePilot 是桌面应用无公网端点,因此:
156+ - Schema 2.0 不支持 ` action ` 标签(错误码 200861)
157+ - Schema 1.0 的 ` action ` 标签可渲染按钮,但点击报 200340(无 webhook 端点接收回调)
158+ - ** 最终方案** :权限卡片使用 schema 2.0 markdown 展示信息 + ` /perm ` 文本命令。用户复制命令发送即可审批。` processIncomingEvent() ` 检测 ` /perm ` 前缀并构造 ` callbackData ` ,走 ` permission-broker.handlePermissionCallback() ` 标准流程。
159+
160+ ** 16. 飞书 Typing 指示器 — Emoji Reaction**
161+ 飞书无 typing indicator API。使用 Openclaw 方案:` onMessageStart() ` 在用户消息上添加 "Typing" emoji reaction(` im.messageReaction.create ` ),` onMessageEnd() ` 删除。` lastIncomingMessageId ` Map 追踪每个 chat 的最新消息 ID。非关键路径,fire-and-forget。
162+
163+ ** 17. 飞书 @提及检测**
164+ 通过 ` /bot/v3/info/ ` REST API 获取 bot 的 ` open_id ` /` bot_id ` ,存入 ` botIds ` Set。群聊消息检查 ` event.message.mentions ` 数组中是否有匹配的 ID。文本中的 ` @_user_N ` 占位符由 ` stripMentionMarkers() ` 清理。
165+
166+ ** 18. Telegram 通知模式互斥**
167+ ` telegram-bot.ts ` 的通知功能(UI 会话通知)与 bridge 模式互斥。通过 ` globalThis.__codepilot_bridge_mode_active ` 标志协调(存 globalThis 防 HMR 重置)。Bridge 启动时设 ` true ` ,4 个 notify 函数检查此标志后提前返回。
168+
120169## 设置项(settings 表)
121170
122171| Key | 说明 |
@@ -135,6 +184,14 @@ Claude 的回复是 Markdown 格式,Telegram 仅支持有限 HTML 标签(b/i
135184| bridge_telegram_stream_min_delta_chars | 最小增量字符数(默认 20) |
136185| bridge_telegram_stream_max_chars | 草稿截断阈值(默认 3900) |
137186| bridge_telegram_stream_private_only | 仅私聊启用预览(默认 true,群聊自动跳过) |
187+ | bridge_feishu_enabled | 飞书通道开关 |
188+ | bridge_feishu_app_id | 飞书应用 App ID |
189+ | bridge_feishu_app_secret | 飞书应用 App Secret(API 返回脱敏) |
190+ | bridge_feishu_domain | 平台域名:` feishu ` (默认)或 ` lark ` |
191+ | bridge_feishu_allowed_users | 允许的 open_id/chat_id(逗号分隔,空=不限) |
192+ | bridge_feishu_group_policy | 群消息策略:` open ` (默认)/ ` allowlist ` / ` disabled ` |
193+ | bridge_feishu_group_allow_from | 群聊白名单 chat_id(逗号分隔) |
194+ | bridge_feishu_require_mention | 群聊需要 @bot 才触发(默认 true) |
138195
139196## API 路由
140197
@@ -163,7 +220,11 @@ Claude 的回复是 Markdown 格式,Telegram 仅支持有限 HTML 标签(b/i
163220- ` src/lib/telegram-bot.ts ` — 通知模式(UI 发起会话的通知),与 bridge 模式互斥
164221- ` src/lib/permission-registry.ts ` — 权限 Promise 注册表,bridge 和 UI 共用
165222- ` src/lib/claude-client.ts ` — streamClaude(),bridge 和 UI 共用
166- - ` src/components/bridge/BridgeSection.tsx ` — Bridge 设置 UI(一级导航 /bridge)
223+ - ` src/components/bridge/BridgeSection.tsx ` — Bridge 设置 UI(一级导航 /bridge),含 Telegram/飞书通道开关
224+ - ` src/components/bridge/BridgeLayout.tsx ` — 侧边栏导航(Telegram + Feishu 入口)
167225- ` src/components/bridge/TelegramBridgeSection.tsx ` — Telegram 凭据 + 白名单设置 UI(/bridge#telegram)
226+ - ` src/components/bridge/FeishuBridgeSection.tsx ` — 飞书凭据 + 群聊策略 + 域名选择 UI(/bridge#feishu)
227+ - ` src/app/api/settings/feishu/route.ts ` — 飞书设置读写 API
228+ - ` src/app/api/settings/feishu/verify/route.ts ` — 飞书凭据验证 API(测试 token 获取 + bot info)
168229- ` electron/main.ts ` — 窗口关闭时 bridge 活跃则保持后台运行;启动时通过 POST ` auto-start ` 触发桥接恢复
169230- ` src/app/api/settings/telegram/verify/route.ts ` — 支持 ` register_commands ` action 注册 Telegram 命令菜单
0 commit comments