# 飞书自建应用机器人通道技术方案 ## 1. 需求背景 Mao 目前通过「微信通道对话」支持用户在微信内与 AI Agent 对话并执行任务(`backend-ts/src/weixin/`:ilink 网关长轮询 → 入站处理 → Agent 执行 → 回复发送;每用户一个固定云端工作区与会话)。该通道使用体验良好,用户希望将同能力扩展到飞书。 本次需求:通过**飞书自建应用机器人**,在飞书内与 Mao Agent 对话并执行云端任务,覆盖**私聊**与**群聊@机器人**两种场景,并支持**在管理后台配置多个飞书机器人、为每个机器人绑定独立的 Agent 与模型**。飞书先行落地;钉钉后续参考飞书最佳实践再单独设计。 ### 1.1 现状与差异 | 维度 | 微信通道(现状) | 飞书通道(本次) | |------|------------------|------------------| | 消息接入 | ilink 网关 HTTP 长轮询(`monitor.service.ts`) | 飞书开放平台 **WebSocket 长连接**(事件订阅长连接模式) | | 机器人实例 | 单账号(设置页扫码绑定,`weixin_channel_account` 表) | **多机器人**:管理后台添加与管理(`feishu_bot` 表,DB 动态管理,长连接按机器人启停) | | 身份绑定 | 设置页扫码绑定,`weixin_channel_account` 表 | **双入口绑定**(飞书内引导链接 + 桌面端设置页授权),以 `union_id`(跨应用全局一致)为身份锚,绑定一次全机器人通用 | | Agent/模型 | 全局设置键 `weixin.agentId` / `weixin.modelId` | **每机器人独立**:`feishu_bot.agent_id` / `feishu_bot.model_id`,管理后台配置 | | 工作区 | 每用户一个固定工作区+会话(`projectKey='weixin-bot'`) | 私聊:**机器人×用户**各一个工作区;群聊:**机器人×群**各一个共享工作区(独立命名空间) | | 群聊上下文 | 不做 | 独立日志表全量持久化群消息,@触发时按需注入(不写会话消息表) | | 消息类型 | 文本、图片、文件、语音 | 文本、图片(多模态)、文件(落工作区);出站统一文本 | | 已具备的基础 | — | 飞书 OAuth 登录(`feishu-auth.service.ts`,登录应用凭证 `app_id/app_secret` 已配置)、飞书 Webhook 通知、admin 管理后台(Vue3 + 权限体系) | ## 2. 需求描述 ### 2.1 做(本期范围) 1. **多飞书机器人接入(WebSocket 长连接模式,管理后台管理)** - 机器人实例存 DB(`feishu_bot` 表),管理后台提供添加、编辑、启用/停用、删除(软删)能力;`app_secret` 加密存储。 - 新增 `FeishuMonitorService`:周期扫描启用机器人(仿 `WeixinMonitorService` reconcile 机制),为每个机器人建立一条长连接:获取该应用的 `tenant_access_token`(`POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal`,官方标准端点)→ 连接飞书事件长连接网关 → 分发事件。新增/启停热生效。 - 每机器人一条长连接,独立断线重连(指数退避)、token 自动续期;长连接仅在后端单实例上运行(生产为单进程部署,见 `scripts/start-backend.sh`),通过全局配置开关控制是否启用。 - 事件接收不需要 URL 验证与签名校验(长连接模式无回调地址;安全由 token 通道保证)。 2. **管理后台「飞书机器人」管理** - 新增 admin 页面与路由:机器人列表(名称、app_id、状态、绑定的 Agent/模型、绑定用户数、最近事件时间)、添加/编辑表单(名称、app_id、app_secret、Agent 下拉、模型下拉)、启用/停用、删除。 - REST 接口(admin 鉴权,复用 `@RequirePermission` 权限体系):`/v1/admin/feishu-bots` CRUD + 启停。 - 每机器人独立 Agent/模型:`feishu_bot.agent_id` / `feishu_bot.model_id`(可空 → 回退默认 Agent / 默认模型);配置修改后,会话复用时按新配置热切换(复用微信通道 session 切换 Agent/模型逻辑)。 3. **用户身份绑定(双入口,以 union_id 为身份锚)** - 身份锚说明:飞书 `open_id` 是**应用维度**(同一用户在不同机器人下 open_id 不同),`union_id` 是**跨应用全局一致**。绑定以 `union_id` 关联到 Mao 用户,绑定一次,所有机器人通用;机器人事件侧利用 `sender_id` 中的 `union_id` 解析 Mao 用户。 - 入口一:飞书内引导。未绑定用户在飞书内私聊机器人(或在群里@机器人)时,机器人回复绑定引导消息(含绑定链接)。点击后走现有飞书 OAuth 授权回调(登录应用),完成绑定。 - 入口二:桌面端设置页新增「飞书机器人」绑定项,展示各启用机器人的绑定状态,提供绑定/解绑。 - 绑定映射:新增 `feishu_binding` 表,`union_id(唯一)↔ userId`,并冗余登录应用侧 `open_id / user_id` 便于调试。 - 一个飞书身份(union_id)绑定一个 Mao 用户;解绑后原工作区与会话保留(对齐微信通道解绑语义)。 4. **私聊场景(机器人 × 用户 固定工作区)** - 用户与机器人私聊,逻辑对齐微信通道:每个(机器人 app_key, 用户)一个固定云端工作区与会话(`projectKey='feishu-{appKey}-private'`,CLOUD、FULL 权限),不随消息数增长重复建会话;同一会话串行执行,新消息到达时按代际取消当前执行并纠偏(复用 `AgentWeixinInboundHandler` 的 generation/sessionLock 机制)。 - Agent/模型来自该机器人配置(`feishu_bot.agent_id` / `feishu_bot.model_id`)。 5. **群聊场景(机器人 × 群 共享工作区)** - 群内成员 @机器人 触发任务对话;未@机器人不触发 Agent,但消息全量持久化(见第 6 条)。 - 工作区:每个(机器人 app_key, 群)一个独立目录 `{workspaceRoot}/feishu-chat/{appKey}/{chatId}/`(独立命名空间,不挂任何普通用户目录);`feishu_chat` 表维护 `app_id+chat_id ↔ 会话/工作区`,`feishu_chat_member` 表维护群成员白名单。 - 会话归属:`session.userId` 记为群内**首位完成绑定的成员**(负责人),工作区文件落群独立目录;权限校验在飞书入站处理器内完成:触发者必须是已绑定成员且在群成员白名单中,否则按「未绑定处理」逻辑响应。 - 首次群内触发:懒创建 `feishu_chat` 记录、工作区目录与群会话;此后复用。 - 群消息处理同样采用代际取消 + 串行锁,同一时刻仅执行一条触发消息。 6. **群聊上下文持久化与注入** - 新增 `feishu_group_message_log` 表,全量持久化群内消息(含未@机器人的),字段含 app_id、chat_id、发送人 open_id/显示名、类型、内容、飞书 file_key/image_key、消息 ID、是否@机器人、时间。 - @机器人触发时,注入「最近 30 条 / 最近 2 小时(取两者较小的窗口,可配置)」的群聊记录,拼接为群聊上下文附在本次触发消息之前,格式:`[HH:mm] 发送人显示名:内容`(图片标注 `[图片]`、文件标注 `[文件:名称]`)。 - **不写入 session 的 message 表**:非触发消息不进会话历史,避免污染标题生成、上下文压缩与消息展示。 - Agent 感知用户关系的范围:仅发送人标记(显示名+时间),不调用飞书通讯录 API 获取职位/部门。 7. **消息类型与媒体** - 入站:文本(直接处理);图片(通过 `im/v1/messages/{message_id}/resources/{file_key}?type=image` 下载,转 data URI 走多模态,复用微信 `media.service` 思路);文件(下载后保存到对应工作区(私聊→该机器人用户工作区,群聊→该机器人群工作区),消息内容以 `@{路径}@` 引用,供 Agent 读取)。 - 出站:统一文本。群聊以**引用回复**(`reply_id` 指向触发消息)发送,私聊直接文本发送;单条回复超过 2000 字截断并追加「…(回复过长已截断)」。 - 回复目标:私聊 `receive_id=open_id`;群聊 `receive_id=chat_id`;按触发消息所在机器人路由(各机器人用自己的 tenant_access_token 发送)。 8. **未绑定成员的群聊处理** - 未绑定成员 @机器人:引用回复「请先完成账号绑定后再使用」,附绑定引导链接;不执行 Agent、不创建群工作区/会话;该条触发消息仍写入群日志(作为上下文素材)。 ### 2.2 不做(本期明确排除) - **不做钉钉**:仅飞书落地;钉钉后续参考飞书最佳实践另行设计。 - **不做群聊组织架构/职位信息注入**:不调用通讯录 API,Agent 仅通过发送人标记感知成员。 - **不做出站媒体/富交互卡片**:Agent 回复一律文本(不出图、不出文件、不做 interactive card)。 - **不做语音**:飞书语音消息入站不支持,收到后提示暂不支持。 - **不做主动推送**:机器人仅回复触发消息,不主动向用户/群推送任务进展。 - **不做私聊多会话切换**:每个(机器人, 用户)固定一个工作区一个会话。 - **不做多实例长连接分发**:长连接单实例运行(生产单进程),不引入分布式锁/选主。 - **不做群权限管理后台**:群成员白名单自动维护(触发过机器人且已绑定的成员自动入表),管理后台不提供群成员手工增删。 - **不做机器人级资源配额**:不限制单机器人并发/用量。 ## 3. 技术选型 | 组件 | 选型 | 说明 | |------|------|------| | 事件接入 | 飞书开放平台 WebSocket 长连接 | 无需公网回调地址;服务端常驻连接 + 自动重连;每机器人一条连接 | | 机器人管理 | admin 管理后台 + `feishu_bot` 表 | DB 动态管理,长连接 reconcile 热生效 | | HTTP/API 客户端 | 内置 `fetch`(Node 22) | 与现有 `feishu-auth.service.ts` 一致,不引入飞书官方 SDK | | token 获取 | 每机器人 `tenant_access_token`(官方标准端点) | 用各自 app_id/app_secret | | 入站处理 | 新增 `backend-ts/src/feishu/` 模块 | 仿 `weixin/` 分层:监控、长连接、绑定、入站、会话、发送、文件 | | 会话/Agent 执行 | 复用 `SessionService` / `HarnessService` / `AgentLoop` | 不重造执行引擎 | | 数据库 | MySQL 8 + Flyway 迁移(V084) | 表结构见 §5 | | 敏感配置 | `app_secret` 加密存储 | 复用 `crypto/aes-gcm.ts`,密钥来自新增配置 `APP_FEISHU_BOT_SECRET` | ## 4. 总体架构 ``` 飞书客户端(私聊 / 群聊@机器人,多机器人各自独立) │ 消息(im.message.receive_v1) ▼ 飞书开放平台事件网关 │ WebSocket 长连接(服务端主动连接,每机器人一条) ▼ backend-ts/src/feishu/ ├─ feishu-monitor.service.ts # reconcile:扫描启用机器人 → 启动/停止长连接(仿 weixin monitor) ├─ long-connection.service.ts # 单机器人长连接生命周期:token 获取/续期、连接、心跳、断线重连 ├─ event-dispatcher.ts # 事件分发:识别 im.message.receive_v1,路由到私聊/群聊处理器 ├─ binding.service.ts # 绑定:绑定链接生成、OAuth 回调落库(union_id 锚)、解绑 ├─ private-chat.handler.ts # 私聊处理:union_id→用户 → 会话/工作区 → Agent 执行 → 回复 ├─ group-chat.handler.ts # 群聊处理:@检测 → 日志持久化 → 白名单校验 → 上下文注入 → Agent 执行 → 引用回复 ├─ group-context.service.ts # 群日志读写 + 注入窗口组装 ├─ session.service.ts # 私聊/群会话与工作区的获取/创建(仿 weixin/session.service.ts) ├─ inbound-processor.ts # 入站消息标准化:文本/图片/文件下载 ├─ send.service.ts # 发送:文本、引用回复 ├─ media.service.ts # 飞书资源下载(image/file)→ data URI / 工作区落盘 ├─ admin.routes.ts # 管理后台 REST:机器人 CRUD + 启停 + Agent/模型配置 └─ feishu.routes.ts # 设置页绑定 REST 接口 │ ▼ 复用现有能力:SessionService / HarnessService(AgentLoop、ToolRegistry)/ admin 权限体系 / 文件存储 ``` 消息处理链路(与微信通道对齐): ``` reconcile 扫描 feishu_bot → 每机器人一条长连接收事件 → event-dispatcher(按事件 app 定位机器人) ├─ 私聊:union_id 解析用户 → 绑定校验 → 懒创建/复用该机器人该用户工作区与会话 │ → 保存 USER 消息 → prepareMessage └─ 群聊:写入 group_message_log → 检测是否@机器人 ├─ 未@:仅持久化,结束 └─ @:union_id 解析用户 → 白名单校验(未绑定→引导回复)→ 懒创建/复用该机器人该群工作区与会话 → 组装群聊上下文 + 触发消息 → 保存 USER 消息 → prepareMessage Agent 执行(代际取消 + 串行锁,复用 weixin handler 机制) 发送:私聊直接文本 / 群聊引用回复(截断 2000 字),走对应机器人 tenant_access_token ``` ## 5. 数据库设计(Flyway `V084__feishu_bot_channel.sql`) ```sql -- 飞书机器人配置(管理后台管理;多机器人) CREATE TABLE IF NOT EXISTS `feishu_bot` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `app_key` VARCHAR(64) NOT NULL COMMENT '内部唯一标识(如 feishu-bot-1)', `name` VARCHAR(128) NOT NULL COMMENT '机器人显示名称', `app_id` VARCHAR(128) NOT NULL COMMENT '飞书应用 app_id(唯一)', `app_secret` VARCHAR(512) NOT NULL COMMENT '飞书应用 app_secret(AES-GCM 加密存储)', `agent_id` BIGINT NULL COMMENT '绑定 Agent(可空→默认 Agent)', `model_id` BIGINT NULL COMMENT '绑定模型(可空→默认模型)', `enabled` TINYINT NOT NULL DEFAULT 1, `deleted` TINYINT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_app_key` (`app_key`), UNIQUE KEY `uk_app_id` (`app_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='飞书机器人配置'; -- 用户绑定:以 union_id(跨应用全局一致)为身份锚,绑定一次全机器人通用 CREATE TABLE IF NOT EXISTS `feishu_binding` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT 'Mao 系统用户ID', `union_id` VARCHAR(128) NOT NULL COMMENT '飞书 union_id(全局唯一)', `open_id` VARCHAR(128) NULL COMMENT '登录应用侧 open_id(调试冗余)', `user_id_fs` VARCHAR(128) NULL COMMENT '登录应用侧 user_id(调试冗余)', `deleted` TINYINT NOT NULL DEFAULT 0, `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_union_id` (`union_id`), UNIQUE KEY `uk_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='飞书用户绑定(union_id 锚)'; -- 群会话映射:机器人×群 → 会话/工作区/负责人 CREATE TABLE IF NOT EXISTS `feishu_chat` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `app_id` VARCHAR(128) NOT NULL COMMENT '飞书应用 app_id(机器人维度)', `chat_id` VARCHAR(128) NOT NULL COMMENT '飞书群 open_chat_id', `session_id` BIGINT NOT NULL COMMENT 'Mao 会话ID', `owner_user_id` BIGINT NOT NULL COMMENT '群内首位绑定成员(session.userId 归属)', `workspace` VARCHAR(512) NULL COMMENT '群工作区路径', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_app_chat` (`app_id`, `chat_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='飞书群会话映射'; -- 群成员白名单:自动维护(已绑定成员触发过即入表) CREATE TABLE IF NOT EXISTS `feishu_chat_member` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `app_id` VARCHAR(128) NOT NULL, `chat_id` VARCHAR(128) NOT NULL, `user_id` BIGINT NOT NULL COMMENT '绑定后的 Mao 用户ID', `open_id` VARCHAR(128) NOT NULL COMMENT '该机器人应用下成员 open_id', `display_name` VARCHAR(128) NULL COMMENT '飞书显示名快照', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_app_chat_member` (`app_id`, `chat_id`, `open_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='飞书群成员白名单'; -- 群聊消息日志:全量持久化(含未@消息),@触发时按窗口注入 CREATE TABLE IF NOT EXISTS `feishu_group_message_log` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `app_id` VARCHAR(128) NOT NULL, `chat_id` VARCHAR(128) NOT NULL, `sender_open_id` VARCHAR(128) NOT NULL, `sender_name` VARCHAR(128) NOT NULL COMMENT '飞书显示名快照', `msg_type` VARCHAR(32) NOT NULL DEFAULT 'text' COMMENT 'text/image/file', `content` TEXT NULL COMMENT '文本内容;图片/文件为名称或摘要', `file_key` VARCHAR(255) NULL COMMENT '飞书 file_key/image_key', `message_id` VARCHAR(128) NULL COMMENT '飞书消息ID(引用回复用)', `is_mention` TINYINT NOT NULL DEFAULT 0 COMMENT '是否@机器人', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY `idx_app_chat_time` (`app_id`, `chat_id`, `created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='飞书群聊消息日志'; ``` 说明: - `session` 表不改结构:群会话 `userId` 记 `owner_user_id`,`workspace` 记群独立目录,`projectKey` 记 `feishu-chat-{appKey}-{chatId}`;私聊 `projectKey` 记 `feishu-{appKey}-private`。 - 私聊不新增表:`feishu_binding` 解析用户后,按 `projectKey` 查询/创建会话(对齐微信通道 `findActiveByUserAndProjectKey`)。 ## 6. 模块详细设计 ### 6.1 机器人监控与长连接(`feishu-monitor.service.ts` + `long-connection.service.ts`) - `FeishuMonitorService`(仿 `WeixinMonitorService`): - `start()`:全局开关 `cfg.feishu.bot.enabled && longConnection.enabled` 时启动;`reconcile` 周期(默认 5s)扫描 `feishu_bot` 中 `enabled=1` 的机器人:新增的启动长连接、被停用/删除的停止连接;热生效。 - `shutdown()`:关闭全部机器人连接。 - `LongConnectionService`(单机器人实例): - 生命周期:用该机器人 app_id/app_secret(解密后)获取 `tenant_access_token`(缓存至过期前 5 分钟自动续期)→ 连接飞书事件长连接网关 → 收事件 JSON → 交 `event-dispatcher`(带 app 标识)。 - 断线重连:指数退避(1s/2s/4s/…上限 30s),连续失败超过阈值告警日志;token 无效时强制刷新后重连。 - 心跳:长连接自带心跳(按飞书协议),本地超时兜底主动断开重连。 - 具体连接端点与握手协议以飞书开放平台官方文档为准,实现时对账官方文档,不在此臆测。 ### 6.2 事件分发(`event-dispatcher.ts`) - 解析事件:`event.type === 'im.message.receive_v1'` 时取 `event.message.chat_type`(p2p / group)、`event.sender.sender_id`(`open_id` + `union_id`)、`event.message.message_id`、`message_type`、`content`(JSON 字符串)。 - 路由:p2p → 私聊处理器;group → 群聊处理器;其他事件类型(如加群、欢迎)记录日志后忽略。 - 解析 @机器人 提及:飞书文本消息 `content` 中提及元素(`mention`)含 `key`(机器人自身 key 以应用 id 标识),命中即 `is_mention=true`;同时从 `content` 剔除 @ 语法残留,保留正文。 ### 6.3 绑定服务(`binding.service.ts`) - 身份解析:事件侧 `union_id`(全局一致)→ `feishu_binding` 命中 → Mao 用户;未命中 → 未绑定。 - 绑定链接:设置页或飞书内引导链接携带 `state`(复用 `feishu_oauth_state` 机制);OAuth 回调成功后以 union_id 写入 `feishu_binding`。 - 解绑:置 `deleted=1`(保留历史),私聊会话与工作区保留。 - 设置页接口(`feishu.routes.ts`,需登录): - `GET /v1/feishu/binding/status`:绑定状态(bound、union_id、绑定时间) - `POST /v1/feishu/binding`:生成绑定链接(OAuth 授权) - `DELETE /v1/feishu/binding`:解绑 - 飞书内引导:未绑定用户私聊/群聊触发时,机器人回复绑定引导文本(含链接),绑定完成后可正常使用;群内成员绑定后下次触发时自动登记进该群 `feishu_chat_member`。 ### 6.4 管理后台(`admin.routes.ts` + admin 前端) - REST(admin 鉴权,复用 `@RequirePermission`): - `GET /v1/admin/feishu-bots`:列表(含绑定用户数、enabled、agent/model 名称) - `POST /v1/admin/feishu-bots`:添加(app_key 唯一、app_id 唯一;app_secret 加密入库) - `PUT /v1/admin/feishu-bots/:id`:编辑(名称、app_secret、agent_id、model_id) - `DELETE /v1/admin/feishu-bots/:id`:软删(停用并移除长连接) - `POST /v1/admin/feishu-bots/:id/enable` / `/disable`:启停(reconcile 热生效) - Agent/模型下拉数据源:现有 agent / model 服务列表接口。 - admin 前端:新增「飞书机器人」页面(列表 + 表单弹窗),路由挂管理后台菜单(对齐现有 admin 页面风格,Vue3 `