File size: 7,395 Bytes
762b1c8 96f34e3 762b1c8 96f34e3 762b1c8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 | # 飞书(Feishu/Lark)机器人集成
## 概述
Codev 内置飞书(国内版)和 Lark(国际版)机器人集成,允许用户在即时通讯中与 AI CLI 进行对话。机器人通过 `@larksuite/channel` SDK 建立长连接 WebSocket,支持私聊、群聊、语音回复、引用回复等功能。
---
## 架构
```
Feishu WebSocket (LarkChannel)
│
▼
FeishuService (singleton)
├── PendingQueue (600ms 去抖)
├── Access Control (DM/Group 策略)
├── Slash Command Router (/stop, /reset, /status, /help)
├── Quoted Context Fetcher
└── TTS Engine (Edge TTS / VoxCPM)
│
▼
messageQueueManager.enqueue()
└── origin: { kind: 'channel', server: 'feishu' }
│
▼
useFeishuBridge (React hook)
├── 收集 AI 回复
└── sendMarkdown() / sendVoice() 返回飞书
```
---
## 连接方式
采用 **WebSocket 长连接**(非 Webhook),基于 `@larksuite/channel` 包:
- **国内版**: `https://open.feishu.cn`
- **国际版 (Lark)**: `https://open.larksuite.com`
- 通过配置中的 `tenant` 字段选择端点(`'lark'` 使用国际版)
### Channel 配置
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `respectProxyEnv` | `true` | 遵循 `HTTP_PROXY`/`HTTPS_PROXY` |
| `pingTimeout` | `3` | SDK ping 看门狗 |
| `handshakeTimeoutMs` | `8000` | 连接握手超时 |
| `httpTimeoutMs` | `30000` | API 调用超时 |
| `policy.dmMode` | `'open'` | SDK 级 DM 策略(上层另有自定义) |
| `safety.chatQueue.enabled` | `false` | 禁用 SDK 内部队列,使用自定义 PendingQueue |
### 生命周期事件
- `message` — 收到消息
- `error` — 连接错误
- `reconnecting` — 重连中
- `reconnected` — 重连成功
---
## 消息处理流水线
```
Feishu WS → NormalizedMessage → PendingQueue → Access Control → Slash Router → Inbound Listeners → enqueue()
```
### PendingQueue(去抖队列)
文件:`vendor/bot/pending-queue.ts`
- 每个 `chatId` 独立队列
- 消息静默 600ms 后批量提交
- 支持 `block(scope)` / `unblock(scope)` — AI 回复期间阻塞新消息
- 以 `/` 开头的命令跳过队列立即处理
### Access Control(访问控制)
文件:`FeishuService.ts`
**DM 权限**(`canUseDm`):
- 机器人拥有者(Owner)— 始终允许
- 管理员(`admins[]`)— 始终允许
- `allowedUsers[]` 为空(默认)— 所有人可 DM
- `allowedUsers[]` 有值 — 仅白名单用户可 DM
**群聊权限**(`canUseGroup`):
- 拥有者和管理员始终允许
- 仅 `allowedChats[]` 中的群被允许
**@ 提及策略**:
- `requireMentionInGroup` 默认为 `true` — 群聊需要 @bot
- 可配置为 `false` 响应所有群消息
### 内置命令
| 命令 | 说明 |
|------|------|
| `/stop` | 停止当前处理 |
| `/reset` | 重置会话 |
| `/status` | 显示机器人状态、App ID、Owner、策略 |
| `/help` | 显示帮助信息 |
未知命令自动传递给 AI 处理。
---
## 引用回复
当用户回复某条历史消息时,`fetchQuotedContext()`(`vendor/bot/quote.ts`)会通过 `channel.fetchRawMessage()` 获取原文,包装为 XML 注入 AI 上下文:
```xml
<quoted_message id="..." sender_id="..." sender_name="..." type="text">
Original message content here
</quoted_message>
```
支持类型:纯文本、合并转发、交互式卡片(CardKit v1 和 v2)。
---
## 消息格式输出
### 文本
`sendText(chatId, text)` — 发送纯文本。
### Markdown(主要方式)
`sendMarkdown(chatId, markdown)` — 发送前通过 `feishuMarkdown.ts` 优化:
- **标题降级**: H1→H4, H2-H6→H5(飞书卡片 H1-H3 渲染有 bug)
- **Schema 2.0 间距**: 在连续标题、表格前后、代码块周围插入 `<br>`
- **图片过滤**: 非 `img_*` 图片移除(防 CardKit 200570 错误)
- **表格限制**: 超过 3 个表格降级为代码块(防 230099/11310 错误)
### 语音
`sendVoice(chatId, text)` — 启用 TTS 时自动调用。
---
## 语音(TTS)系统
飞书语音为**单向输出**(AI 回复 → 语音),用户始终通过文字输入。
### Edge TTS(默认)
- 使用 `edge-tts` CLI 工具
- 默认语音:`zh-CN-XiaoxiaoNeural`
- 输出转码为 OGG (Opus) 后发送
### VoxCPM(自定义语音克隆)
- 使用 `.venv/bin/voxcpm` CLI
- 需要 `ttsReferenceAudio`(WAV/MP3 参考音频)
- 长文本按 ~150 字在句边界分块
- 每块独立合成,WAV→OGG 转码,ffmpeg concat 合并
### TTS 配置
| 字段 | 类型 | 说明 |
|------|------|------|
| `ttsEnabled` | boolean | 主开关 |
| `ttsProvider` | `'edge' \| 'voxcpm'` | 引擎选择 |
| `ttsVoice` | string | Edge TTS 语音名称 |
| `ttsReferenceAudio` | string | VoxCPM 参考音频路径 |
---
## QR 码注册向导
支持一键创建飞书应用,无需手动在开发者后台操作:
1. 调用 `@larksuite/channel` 的 `registerApp({ source: 'codev' })`
2. 返回 QR 码 URL,终端用 ASCII 渲染
3. 用户使用飞书手机端扫码授权
4. `client_id` 和 `client_secret` 自动保存到配置
5. 机器人立即启动
---
## 配置
文件路径: `~/.claude/adapters.json`(`feishu` 键下)
| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `appId` | string | ✅ | 飞书开放平台 App ID |
| `appSecret` | string | ✅ | 飞书开放平台 App Secret |
| `tenant` | `'feishu' \| 'lark'` | ❌ | API 端点选择 |
| `encryptKey` | string | ❌ | 事件加密 |
| `verificationToken` | string | ❌ | 事件 URL 验证 |
| `allowedUsers` | string[] | ❌ | DM 白名单(空=开放) |
| `admins` | string[] | ❌ | 管理员 open_id |
| `allowedChats` | string[] | ❌ | 群聊白名单 |
| `requireMentionInGroup` | boolean | ❌ | 默认 true |
| `ttsEnabled` | boolean | ❌ | 语音回复开关 |
| `ttsProvider` | `'edge' \| 'voxcpm'` | ❌ | TTS 引擎 |
| `ttsVoice` | string | ❌ | Edge TTS 语音 |
| `ttsReferenceAudio` | string | ❌ | VoxCPM 参考音频 |
---
## Keepalive(保活机制)
文件:`vendor/bot/keepalive.ts`
独立于 SDK 内部 ping 的防御性看门狗:
- **间隔**: 每 15 秒
- **防风暴**: 5 秒内跳过重复 tick
- **睡眠检测**: 距上次 tick 超过 30 秒重置计数器
- **HTTP 探针**: 重连前 HEAD 请求检测网络可达性
- **死连接阈值**: 连续 3 个 tick 确认 WS 断开才强制重连
---
## 桥接 Hook
`useFeishuBridge`(`src/hooks/useFeishuBridge.ts`)
React hook,负责:
1. 订阅 FeishuService 的入站事件
2. 监听 `messages` 数组,识别飞书来源的消息(`origin.kind === 'channel' && origin.server === 'feishu'`)
3. 收集后续 AI 回复
4. 在 `isLoading` 从 true→false 时:
- 主: `sendMarkdown()` 发送完整回复
- 次: `sendVoice()` 发送 TTS 语音
---
## 与 FriendService 的对比
| 方面 | FeishuService | FriendService |
|------|--------------|---------------|
| 通信方式 | WebSocket (`@larksuite/channel`) | 同进程 HTTP + SSE |
| 消息输入 | 文字(飞书 IM) | 文字 / 语音(VAD + STT) |
| 语音方向 | 仅输出(TTS) | 双向(VAD → STT → AI → TTS) |
| TTS 引擎 | Edge TTS, VoxCPM | Edge TTS, Qwen TTS |
| 配置存储 | `~/.claude/adapters.json` | `getPrefs()` |
| 服务位置 | `src/services/feishu/` | `src/friend/` |
|