🕊️ dsh-lark-bridge
把 DeepSeek Harness 的编码智能搬进飞书
原生思考过程、审批卡片、实时 goal/todo 卡片、子代理 fan-out、双语 slash 面板——不需要公网回调地址。
English · 快速开始 · 能力 · 斜杠命令 · 配置 · 架构 · 开发
---
## 这是什么?
`dsh-lark-bridge` 是一个 **飞书/Lark 即时通讯机器人通道**,让 DeepSeek Harness 的编码代理直接在聊天里工作:
- 每条会话(私聊 / 群聊)驱动一个独立的 dsh agent
- **思考过程实时可见** —— 用飞书原生的"思考中"消息渲染 reasoning,工具调用带图标、结果以代码块展示,不再黑盒
- **审批卡片** —— 需要确认的操作变成可点击的卡片(允许一次 / 拒绝),卡片回写决策人与结果
- **实时卡片流** —— goal 阶段变化、todo 快照、子代理 fan-out、上下文压缩结果,都在聊天里实时可见
- **reaction 反馈** —— 收到 `OK` → 思考 `THINKING` → 完成 `DONE` / 失败 `ERROR`,一眼看清状态
- 走 WebSocket 长连接,**不需要公网回调地址**
本质是"嫁接":飞书只是载体,真正干活的还是 DeepSeek Harness 本体。模型组以 dsh 为基准 —— DeepSeek API 有思考链,飞书就必须显示思考链,功能不减。
## ✨ 能力
| | |
|---|---|
| 🧠 **原生思考过程** | `cot` 模式下,模型的 reasoning 渲染为飞书原生"思考中"消息,工具调用带图标、结果以代码块展示;旧客户端可用 `stream` 打字机卡片 |
| ✅ **Live Reaction** | 每条消息实时反馈:收到 `OK` → 思考 `THINKING` → 完成 `DONE`(失败 `ERROR`),状态互替不堆叠,可配置 |
| 🗂️ **一会话一 Agent** | `sessionScope` 控制粒度:整个 chat / 话题 thread / 单 sender;会话持久化,重启后恢复 |
| 📋 **审批卡片** | host 的审批问题渲染为「允许一次 / 拒绝」按钮卡片,点击即决策,卡片回写决策人与结果 |
| 🎯 **Goal 卡片** | goal 阶段(active/paused/blocked/complete)变化实时更新聊天卡片;`autoResumeGoals` 让重启后活跃 goal 自动恢复 |
| ✅ **Todo 卡片** | `todo_write` 快照实时更新聊天卡片,长任务不再静默 |
| 🧑💻 **子代理 fan-out** | workflow 运行以文本流呈现:run 开始、子代理开启/结束、run 结束 |
| 📦 **压缩透明化** | "正在压缩…" → 摘要文本 + 释放 token 数;修剪报告删除条数 |
| ⏰ **定时提醒** | `schedule_create/list/delete` 工具 + `/schedules` 视图(需在 dsh profile 组合 `@deepseek-ai/dsh-schedule`;桥已实现完整监听与渲染) |
| 🔍 **会话历史检索** | `/sessions <关键词>` 对本聊天已存历史做全文检索并展示摘要 |
| ⚙️ **后台任务通知** | `run_in_background` 任务与直接子代理结束时在聊天里播报结果 |
| ⚡ **完整 Slash 面板** | `/stop /help /preset /sessions /tools /schedules /jobs /feedback /context /audit /config` + 宿主命令(`goal`、`plan`、`compact`、`permission`) |
| 🌐 **双语命令** | slash 面板与 `/help` 的描述按平台自动选语言:Lark(国际版)英文、飞书(国内版)中文;`locale` 可强制指定 |
| 🖼️ **图片输入(可选)** | `attachImages` 下载聊天图片进 host 附件库,随模型请求发送 |
| 📎 **文件发送** | Agent 的 `send_file` 带 caption 直接投递到聊天 |
| 🔑 **扫码注册** | 首次启动打印二维码,扫码自动创建飞书应用(含事件订阅),凭据持久化 |
| 🔒 **授权窄化** | `senderAllowlist` / `groupAllowlist` / `approvers` 可在 app 可见范围内进一步收窄 |
| 🧩 **深度 dsh 适配** | 所有能力走 host 服务契约:`agents` / `agentPresets` / `agentDefaultModel` / `settings` / `workspaceRegistry` / `loader` / `invariants` / `approval` / `goals`,包自包含,无需 host 源码 |
## 🚀 快速开始
```sh
npx @deepseek-ai/dsh plugin --profile web add github:moyu-good/dsh-lark-bridge \
&& npx @deepseek-ai/dsh web
```
控制台打印二维码 → 用飞书扫码创建应用 → 在 Settings → Models 填入 DeepSeek API Key → 私聊 bot 或群里 @ 它。
> 已经在用 `dsh`?去掉 `npx @deepseek-ai/` 前缀即可。
**无需构建**:包已提交编译产物(`lib/` 进仓库),安装即用。`prepare` 钩子仅在编译产物缺失时(例如源码 clone 且无产物)自动重建。
## 💬 斜杠命令
| 命令 | 说明 |
|---|---|
| `/stop` | 取消当前任务 |
| `/help` | 显示本列表 |
| `/preset` | 查看/切换 agent 模式(standard / code / minimal / cordis) |
| `/sessions` | 查看本聊天的会话历史 |
| `/tools` | 运行时查看/禁用/恢复工具 |
| `/schedules` | 查看本聊天的定时提醒 |
| `/jobs` | 查看本会话的后台任务 |
| `/audit` | 本会话的操作审计摘要 |
| `/context` | 查看当前上下文 token 压力 |
| `/config` | 查看桥的当前配置 |
| `/goal` | 查看/设置目标(宿主命令) |
| `/plan` | 进入/退出计划模式(宿主命令) |
| `/compact` | 压缩较早对话历史(宿主命令) |
| `/feedback` | 给上一条回答评分:`positive`/`negative` + 可选备注 |
| `/permission` | 切换权限模式(宿主命令) |
面板描述自动双语:平台域名为 `open.larksuite.com`(国际版 Lark)显示**英文**,`open.feishu.cn`(国内版飞书)显示**中文**;`locale: zh|en` 可强制指定。
## 与其他飞书/Lark 桥对比
| 能力 | **dsh-lark-bridge** | xmanrui/dsh-im | omdsh-dev/dsh-lark | AX1202/ax-feishu-bridge |
|---|---|---|---|---|
| 定位 | 深度 Harness 通道 | 多平台网关 | 扫码即用 | Pi + DSH 双桥 |
| 原生思考过程(飞书 CoT) | ✅ | — | — | — |
| 审批卡片 + 决策人留痕 | ✅ | — | — | 远程批准 |
| 实时 goal/todo 卡片 | ✅ | — | — | — |
| 工作流展开 + 阶段/日志行 | ✅ | — | — | — |
| 压缩透明化 | ✅ | — | — | — |
| 重启后 goal 自动续跑 | ✅ | — | 崩溃安全网 | — |
| 双语斜杠面板同步 | ✅ | — | — | 面板按钮 |
## ⚙️ 配置
| 字段 | 默认 | 含义 |
|---|---|---|
| `appId`、`appSecret` | 首次启动扫码注册 | 飞书/Lark 应用凭证 |
| `domain` | 飞书 | 开放平台域名;Lark 用 `https://open.larksuite.com` |
| `locale` | `auto` | 命令描述语言:`auto`(Lark→英文,飞书→中文)/ `zh` / `en` |
| `cwd` | 宿主进程 cwd | 会话 Agent 的绝对工作目录 |
| `provider`、`model` | 宿主 `agentDefaultModel` | 会话 Agent 的模型路由 |
| `preset` | roster 默认 | 部署组合了 roster 时,会话 Agent 加入的 preset |
| `sessionScope` | `chat` | `chat`(整个会话共用一个)/ `chat-thread`(每个话题各自一个)/ `chat-sender`(共享会话里每人一个) |
| `output` | `cot` | `cot`(原生思考过程 + markdown 答案)或 `stream`(每轮一张打字机卡片) |
| `showProcess` | `true` | 展示 Agent 的推理与工具调用;关闭则只发答案 |
| `reactionFeedback` | `true` | 实时 reaction 反馈(OK → THINKING → DONE/ERROR) |
| `hideProcessWhenDone` | `false` | 运行结束后让平台收起该过程(仅 `cot`) |
| `attachImages` | `false` | 是否把图片传给模型。仅用于确实支持图片的路由 |
| `syncSlashCommands` | `true` | 把会话可用的命令注册到机器人 `/` 面板(幂等 reconcile:创建缺失、移除过期、刷新漂移描述) |
| `autoResumeGoals` | `false` | 重启后会话回来时自动 re-arm 活跃 goal,部署不再静默杀死进行中的任务 |
| `approvalReminderMs` | `0` | 审批卡此毫秒数未处理时发提醒(0 = 关闭) |
| `denyTools` | `[]` | 会话 Agent 不可调用的工具 |
| `requireMention` | `true` | 群聊中仅在被 @ 时响应 |
| `senderAllowlist` | `[]` | 允许私聊的 open id;留空则服务应用可用范围内的任何人 |
| `groupAllowlist` | `[]` | 非空时仅服务这些 `oc_…` 群会话;空=任意群 |
| `approvers` | `[]` | 允许作答审批的 open id;空=能驱动该会话的人都可以 |
| `outbound.allowedFileDirs` | 未配置 → 文件发送禁用 | `send_file` 允许读取**本地路径**的目录。发送生成的产物(HTML 报告、截图、文档)必须配置。示例:`outbound: { allowedFileDirs: ['/home/user/work'] }` |
> ⚠️ **文件发送默认拒绝。** 不配 `outbound.allowedFileDirs` 时,`send_file` 传本地路径会报 `local file source requires outbound.allowedFileDirs to be configured`——Agent 看起来发了,实际没送达。URL 和原始 buffer 始终可用。
凭据三层解析,后者覆盖前者:bundle patch 配置 → settings 文档插件区 → 首次扫码注册。
## 🔐 应用必需权限
**新创建**的飞书应用,面板和消息功能需要以下权限上线。扫码注册流程会自动授予;**手动创建的应用**必须在开发者后台 → 权限管理 开通,然后**创建版本并发布**(最后发布之后新加的权限,API 不认,必须随新版本上线):
| 权限 | 用途 |
|---|---|
| `application:app_slash_command`(read + write) | 斜杠命令面板——缺它时 `syncSlashPanel` 报 `99991672`,`/` 列表永远为空 |
| `im:message` | 发送与接收消息 |
| `im:message:readonly` | 读取消息内容 |
| `im:message.receive_v1` 事件 | 接收消息事件(事件与回调 → 长连接) |
| `im:resource` | 上传/发送图片和文件 |
| `im:chat:read` | 群信息(群聊场景) |
| `im:message.reactions:read` / `write_only` | 实时 reaction 反馈 |
用 API 直接调试——后台显示的是**已勾选**,API 显示的是**线上版本**实际带走的:
```sh
# 1. 拿 token
curl -s -X POST https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal \
-H 'Content-Type: application/json' \
-d "{\"app_id\":\"$APP_ID\",\"app_secret\":\"$APP_SECRET\"}" | jq -r .tenant_access_token
# 2. 斜杠命令列表(同步后应能看到命令)
curl -s "https://open.feishu.cn/open-apis/application/v7/app_slash_commands?page_size=50" \
-H "Authorization: Bearer $TOKEN"
# 3. 线上版本 scopes(确认 application:app_slash_command 在里面)
curl -s "https://open.feishu.cn/open-apis/application/v6/applications/$APP_ID/app_versions?lang=zh_cn" \
-H "Authorization: Bearer $TOKEN"
```
面板同步在会话 create/resume 时触发——开通权限后,给 bot 发一条消息即可触发。
## 🧭 架构
```
飞书 / Lark ── WebSocket 长连接 ──► dsh-lark-bridge(dsh 进程内的 feishu-channel 插件)
(聊天/审批/图片) │
▼
host 服务契约: agents / sessions / tools / approval /
goal / workspace / settings / commands
│
▼
DeepSeek Harness 本体
```
桥运行在 **dsh 进程内部**,是 `feishu-channel` 插件——不是独立服务器。`npx @deepseek-ai/dsh web`(或 `--profile chat`)启动 dsh 并组合本插件;插件打开 WebSocket 长连接并从那里驱动一切。任意启动器(shell 脚本、systemd、supervisor)都可以托管它,不依赖任何其他 agent 框架。
## 🛠️ 开发
```sh
pnpm install
pnpm run build # clean + tsc + tsdown(产物进 lib/,已提交仓库)
pnpm test # vitest(272 tests)
node plugin-contract-test.mjs # 独立契约测试
```
仓库自包含:仅依赖已发布的 `@deepseek-ai/cordis`、`@deepseek-ai/schemastery` 与 `@larksuite/channel`,从不需要宿主源码检出。
**打包说明**(为什么 `lib/` 进仓库):
- git 依赖安装(`github:user/repo`)不会跑构建;没有已提交的 `lib/` 时插件启动即崩(`ERR_MODULE_NOT_FOUND`)——已通过提交编译产物修复
- `prepare` 钩子是源码 clone 的安全网:`lib/` 存在时立即退出,仅在真正缺失时重建
- `build` 先清空 `lib/`(tsdown 自身 `clean: false`,因为它的 entry 在输出目录内)
## 📋 已知限制
- 通道级配置(appId/appSecret/requireMention/白名单)由 transport 持有,改动需重启;其余配置编辑 profile 的 `cordis.patch.yml` 后由 dsh 的 Config-only HMR 自动生效(`/config` 可查看当前生效值)
- 长连接中断期间到达的事件不重放(传输层无游标;出站发送由 replay 队列兜底)
- 飞书 app 需要把事件订阅方式设为**长连接**(自建应用),webhook 模式收不到事件
- `schedule_create/list/delete` 工具需要在 dsh profile 里组合 `@deepseek-ai/dsh-schedule`(桥已监听 `schedule/change` 并渲染 `/schedules`;工具是模型侧的另一半)
## 📄 许可
BSD-3-Clause。架构启发自 [dsh-lark](https://github.com/Roy-oss1/dsh-lark)(同为 BSD-3-Clause)。