Skip to content

Commit 243724a

Browse files
7418claude
andcommitted
feat: 飞书(Feishu/Lark)Bridge 适配器 + 发布 v0.25.0
新增飞书 IM 通道,通过 @larksuiteoapi/node-sdk WSClient 接收消息、REST Client 发送回复。 适配器核心 (feishu-adapter.ts): - WSClient 长连接接收 im.message.receive_v1 事件 - 消息去重 (message_id LRU 1000)、授权校验、群聊策略 (open/allowlist/disabled) - @提及检测 (bot open_id/user_id/union_id 匹配) - 图片/文件/音频/视频/富文本(post) 下载 → base64 FileAttachment - Typing 指示器 (Openclaw 方案: emoji reaction) - /perm 文本命令处理权限审批 渲染策略 (markdown/feishu.ts): - 代码块/表格 → schema 2.0 interactive card (markdown 元素) - 纯文本 → post (md tag), 支持粗体/斜体/行内代码/链接 - 权限卡片 → schema 2.0 card + /perm 文本命令 (无按钮, 见下方决策) - 每层降级: card → post → text 关键决策 — 权限交互无按钮: - 飞书 card.action.trigger 回调需 HTTP webhook, WSClient 不支持 - schema 2.0 不支持 action 标签 (错误码 200861) - schema 1.0 按钮可渲染但点击报 200340 (无 webhook 端点) - Openclaw 用 http.createServer + Lark.adaptDefault 解决, CodePilot 桌面端无公网端点 - 最终方案: 格式化卡片 + /perm 文本命令, 用户复制发送即可审批 其他变更: - permission-broker.ts: permissionRequestId 去重 (30s TTL) - telegram-bot.ts: bridge 模式互斥 (globalThis 标志) - bridge-manager.ts: 飞书渲染分发 (deliverResponse feishu 分支) - UI: FeishuBridgeSection 设置页 + 侧边栏入口 + 通道开关 - i18n: 中英文飞书配置文案 - 交接文档: bridge-system.md 更新飞书架构、数据流、6 项设计决策 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b1a043a commit 243724a

File tree

18 files changed

+1844
-38
lines changed

18 files changed

+1844
-38
lines changed

docs/handover/bridge-system.md

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
```
3558
Telegram 消息 → 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

Comments
 (0)