Bundle
dsh-feishu-assistant
给同事的运维请求通道:同事在飞书私聊机器人提一件事,你在审批卡上点「同意」后,由你指定的那条 DSH 会话带着你配好的工具与人设去办,结果以卡片回到提交人的飞书(Markdown 正常渲染),不需要公网入口。四个特点:配对码把发送者绑成管理员,机器人只服务指定账号;非管理员请求走审批卡片,同意/拒绝都校验点击人身份;请求串行排队,前一条整轮 turn 跑完才注入下一条;目标会话启动时主动 resume,不用先有人在界面上点开。
- Source
- wanjiaju3108
- License
- MIT
- Updated
- Updated 4 days ago
Readme
# dsh-feishu-assistant
**给同事的运维请求通道**:同事在飞书私聊机器人提一件事,你在审批卡上点「同意」后,由你指定的那条 DSH 会话带着你配好的工具与人设去办,结果以卡片回到提交人的飞书(Markdown 正常渲染)。不需要公网入口。
## 它解决什么
- **把运维事务从你手里分流出去**:同事在飞书里说一句要办什么,由你指定的那条 DSH 会话去执行——那条会话带着你 profile 里配好的整套 MCP/工具(配置、日志、发布、项目、文档)和你注入的人设,不用你亲手一件件做
- **你只保留放行权**:非管理员的消息只进审批卡,你点「同意」才跑,不该办的直接拒——放行权在你,执行不在你
- **口径统一**:所有人共用同一条会话和同一份人设,回答风格、操作习惯、注意事项都是一套,不用给每个人讲一遍
- **一次只跑一件**:串行队列 + 等目标会话空闲,不会几个人同时动同一套环境,也不会跟你在界面里的轮次打架
- **结果直接回到请求人**:同事自己看到办没办成,不用你转述
不适用:想给每个同事各自一条独立会话、各自积累上下文的场景——那是 per-user 的 IM 桥,不是这个插件。
## 装
```bash
dsh plugin --profile web add dsh-feishu-assistant
```
装完重启 `dsh web`。
> **DSH 版本**:当前 `0.1.6` 需要 DSH `0.1.7-rc.1` 及以上。
> 还在 DSH 0.1.5 上的,装最后一个支持它的版本:
>
> ```sh
> dsh plugin --profile web add dsh-feishu-assistant@0.1.4
> ```
> **装的时候如果收尾报 `ERR_PNPM_IGNORED_BUILDS: protobufjs`,插件其实没装上。**
> `@larksuiteoapi/node-sdk` 的依赖里有 `protobufjs`,它带 postinstall 脚本,pnpm 默认不跑;dsh 把 pnpm 的非零退出当成整体失败,于是没把插件登记进 profile。先放行再装一次:
>
> ```yaml
> # ~/.dsh/profiles/<profile>/pnpm-workspace.yaml
> allowBuilds:
> protobufjs: true
> ```
## 做一个飞书AI助理(从零到能聊天)
### 第 1 步:飞书开放平台建应用
1. 到[飞书开放平台](https://open.feishu.cn/app) → 创建**企业自建应用**;
2. 「凭证与基础信息」里抄下 **App ID** 和 **App Secret**;
3. 「添加应用能力」里开启**机器人**;
4. 「权限管理」里加上六条权限:
- `im:message.p2p_msg:readonly` —— 接收私聊消息
- `im:message:send_as_bot` —— 以机器人身份发消息
- `cardkit:card:write` —— 回答卡片(建卡片实体、整块更新组件正文)。不加也能跑,只是回答退回"每段一张普通卡片"
- `contact:contact:readonly` —— 读通讯录,用来在审批卡片上把请求人显示成姓名。不加也能跑,卡片上会退回显示 open_id
- `im:message.urgent` —— 应用内加急:审批卡片发出去时顺手把这条消息标成「急」。不加也能跑,只是少了这一档提醒
- `im:message.urgent:sms` —— 短信加急:审批卡片超过 5 分钟没人处理时升级一次。不加也能跑,只是升级那一步会失败并记日志
5. 用 `contact:contact:readonly` 的话,还要把请求人放进应用的**通讯录权限范围**(开发配置 → 权限管理 → 数据权限):`contact/v3/users` 只返回权限范围内的用户,范围外的照样取不到名字,接口会报 `41050 no user authority`
6. 「事件与回调」里把**订阅方式设为长连接**,然后添加:
- 事件 `im.message.receive_v1`(收到消息)
- 回调 `card.action.trigger`(审批卡片的按钮点击)
7. 「版本管理与发布」里创建版本并申请发布 —— 企业自建应用要管理员通过后才对用户生效。
### 第 2 步:DSH 里装上并配好
打开 设置 → 「飞书AI助理」,四项:
| 项 | 填什么 | 存哪 |
|---|---|---|
| App ID / App Secret | 第 1 步抄下来的 | `$DSH_HOME/.credentials.yaml` |
| 目标会话 | 见下面「选一条会话」 | `$DSH_HOME/settings.yaml` |
| 人设文件 | 上传一份本地 md/txt;**不上传就等于这个模式没开** | `$DSH_HOME/settings.yaml` |
| 管理员 | 不手填,用配对码绑 | `$DSH_HOME/settings.yaml` |
凭据在 `.credentials.yaml` 里的引用名是 `FEISHU_ASSISTANT_APP_ID` / `FEISHU_ASSISTANT_APP_SECRET`。
凭据写入后不再回显;被环境变量或 `.env` 遮蔽时,页面上显示为只读。
**必须把 DSH 权限设成「完全权限」**:
飞书来的消息是**无人值守**执行的——你在手机上,应答不了 DSH 的权限审批。而 DSH 的审批是 **fail-close** 的:没有应答方就直接拒绝。权限不放开的后果是,飞书发来的请求一碰到写文件、跑命令就失败,而你在手机上只看到一句报错,不知道为什么。
两处都要设,缺一处都不行:
| 哪里 | 设成什么 | 管什么 |
|---|---|---|
| 设置 → 通用 → 权限 | **完全权限** | 以后**新建**会话的默认档位 |
| 在会话里打 `/permission` | **完全权限** | **当前**这条会话的档位 |
顺序有讲究:**先改通用设置、再建目标会话**,新会话就直接是完全权限;要是会话已经建好了,改默认值不会回头生效,得进去用 `/permission` 补一次。
> 附带后果:开了完全权限,等于机器人能在这台机器上做任何事。所以**「管理员」那一项别漏**——它决定谁能跟机器人说话。
**选一条会话**:
1. 在 Web UI 里新建一条会话;
2. 随便发一句话让它落盘 —— 有日志的会话才会出现在「目标会话」下拉里;
3. 到 设置 → 「飞书AI助理」→「目标会话」,选中它。
建议**专开一条会话**给飞书用,别跟自己写代码的会话共用:飞书来的消息会进同一条会话历史。
**绑管理员**:点「生成配对码」,把 8 位口令**私聊发给机器人**,发送者即被设为管理员。口令 8 位、10 分钟有效、用一次即废。
### 第 3 步:验证
在飞书里私聊机器人发一句话。管理员的消息直接进队列;回答以**卡片**回。设置页「状态」区能看到长连接、目标会话、人设三块的状态。
## 人设文件
「飞书AI助理模式」这个能力跟插件一起装,但人设内容不由插件提供:上传一份本地 md/txt 就行。
- 上传后内容是**插件自己配置里的一份副本**(`$DSH_HOME/settings.yaml` 的 `feishu-assistant.persona`),**插件不会动你的原文件**
- 上限 64 KB;空文件或超限,设置页直接拒绝
- 内容为空时飞书消息不处理(管理员收到一句指路,其他人静默丢弃,审批卡片也不发)
- 内容会**盖掉该会话原本的个人设定**
- 上传后不用重启、也不用新建会话:下一条飞书消息就是新人设
- 代价:上传之后再改**原文件**,插件不会跟过来,要重新上传一次
写什么随意。典型内容是助手的身份、语气,以及"正文直接用 Markdown"这类格式约定。
## 行为
```
飞书私聊文本
├─ 配对码 → 把发送者设为管理员,回执
├─ 人设内容为空 → 管理员收到指路;其他人静默丢弃
├─ 管理员 → 直接进请求队列
└─ 其他人 → 发审批卡片给管理员;同意后进请求队列,拒绝则回执
(待审批最多 20 条;发卡时顺手应用内加急,超过 5 分钟没人处理升级一次短信加急;15 分钟过期,作废时回消息告知)
```
- **请求队列串行**:先进先出,前一条整轮 `turn/end` 结束才注入下一条
- **重复投递只答一次**:飞书长连接是"至少一次"投递,断线重连可能把同一条消息再送一遍;按 `message_id` 去重(`lru-cache`,记最近 200 条),重投只记一条日志
- **注入前等目标会话空下来**:目标会话是共享的,DSH 界面里那一轮可能正在跑。飞书这条会先挂在队列里,等会话真空下来(当前轮跑完、没有排队输入,靠 `agent.whenIdle()`)再注入——这样它就是下一轮,"这一轮跑完了"的判定必然落在自己身上,不会和界面里的轮次交错。等待期间请求人已经在卡片上看到「正在处理」
- **回写粒度是 step**:插件只消费已提交的 `assistant/message`,所以每产出一条就更新一次那张卡片(上面那条);退回普通卡片时才是每段发一条、超过 3000 字按换行分片。DSH 另有 token 级的 `agent/assistant-stream`(网页界面用它逐字渲染),插件**没有**接
- **回答走一张卡片,整块替换**:文本消息不渲染 Markdown,所以回答用卡片回;而且**一条回答只用一张卡**——先发一张写着「正在处理」的卡,之后每段把 markdown 组件**整块换掉**(`cardkit.cardElement.update`),客户端**立刻显示**。**不用卡片流式接口**:那是给逐字生成准备的,客户端会按打字机慢慢打,一段几百字要打好几秒,收尾还会把没打完的一次性刷出来("打几个字突然整段蹦出")。正文超过卡片上限(30 KB)会自动**翻页**再接一张卡
- **卡片开不出来就退回普通卡片**:没 `cardkit:card:write` 权限或接口失败时,退回"每段一张普通卡片"的老行为;卡片中途失败会把没上屏的内容补发成普通消息,不丢内容
- **回写重试**:单条失败重试 3 次(500ms / 1000ms 退避)
- **直发有回执**:管理员私聊发来的请求先回一句「正在处理」再跑,长回答期间不会让你以为它死了(审批通过的请求人由审批流程回同一句)
- **没产出会说明**:这一轮结束时一个字都没产出(例如跑到一半出错)就回一条失败文案,不再静默;管理员还会看到具体失败原因
- **不是飞书发起的轮次也推给你**:目标会话是共享的——你可能在 DSH 界面上继续问同一件事。插件按「这一轮里有没有自己注入的那条消息」认领轮次:不是它发起的,就把回答以卡片**主动私聊推给管理员**,而不是静默丢掉(也没有可回复的飞书消息,所以用主动私聊)
- **会话主动打开**:启动、换目标会话、收到消息时都会 `sessionController.resolveAgent()` 把会话拉起来
- **会话失效有反馈**:目标会话打不开(例如被删掉)时,设置页「状态」区会出现红色的「会话错误」并写明原因;同一条失败还会主动私聊告知管理员一次,恢复后再坏会重新告知。确认打不开之后的后续消息**不再白开一张卡片**(直接回一句报错);请求照常入队,会话恢复后照常处理——那一轮回答先以普通卡片回来,下一轮恢复成回答卡片
- **人设有状态**:设置页「状态」区显示「飞书AI助理模式 已导入 <文件名>(N / 65536 字节)」,没上传则显示未启用
- **助手反问会提示**:助手调用提问工具(`ask_user_question`)时会往飞书回一条提示——那个问题只在 DSH 界面上等人回答,飞书看不到也答不了;提示里会建议用人设禁止它提问、改用回答正文
- **审批人校验**:卡片回调校验点击人的 open_id / user_id / union_id,非管理员点不了
## 配置项
设置页改的都会即时生效(`applies: 'live'`),不需要重启。
补丁层也可以给初始值:
```yaml
- id: feishu-assistant
name: dsh-feishu-assistant
config:
sessionId: ''
managerId: ''
persona: ''
```
## 怎么验证
```bash
npm test # 等价于 node test/all.mjs
```
六个用例、79 条断言,全部用**假飞书 SDK 跑真实的 `apply()`**(不打真接口、不需要凭据):
| 用例 | 覆盖 |
|---|---|
| `outbound-check` | 出站返回值语义:卡片成功 / 被拒退回文本 / 没有收件人不发 |
| `answer-card-check` | 回答卡片:整块替换、翻页、失败补发、收尾不多调接口 |
| `plugin-routing-check` | 轮次归属:飞书请求回原消息、外部轮次走私聊、反问提示 |
| `idle-hold-check` | 注入前等会话空闲:忙时不注入、被插队继续等、前提被破坏时告警 |
| `broken-session-check` | 会话打不开:第一次闪一下、之后不再白开卡片、修好后恢复 |
| `inbound-dedupe-check` | 重复投递只答一次 |
测试放在 `test/`,**不随 npm 包发布**(`package.json` 的 `files` 只有 `lib` 和 `cordis.patch.yml`)。
## 已知边界
- 只处理**私聊**文本消息:群聊、图片、文件、富文本、语音一律忽略
- 目标会话必须能被本进程 resume;被另一个进程占着时会失败(DSH 会话是单进程独占的)
- 待审批缓存是内存态,DSH 重启即丢
- 加急只能催**机器人自己发的消息**,所以只对审批卡片有效;加急失败只写日志,不影响审批本身
- 回答卡片用的是**卡片 JSON 2.0,要求飞书客户端 7.20 及以上**:低于 7.20 时卡片正文会显示成"升级提示"占位图
- 目标会话忙着(界面里那一轮正在跑)时,飞书请求要等它跑完才开始;等待时间等于那一轮的时长。等待期间请求人的卡片显示「正在处理」
- 回答卡片是**普通卡片**(非流式),没有「生成中」状态,生成期间也能转发
- 每段正文是**整块替换**、不是逐字流式:答案会在每个 step 结束时成段出现(这是有意的,见上)
- 没有发送者限流;没有 `/` 命令
## 依赖的 DSH 服务
`agents`、`settings`、`credentials`、`webServer` 列在 `inject` 里,缺一个插件就不会加载。
四个都是官方包(`@deepseek-ai/dsh-agent` / `dsh-settings` / `dsh-credentials` / `dsh-host-webserver`)。前三个随 `dsh-base` 走,任何 profile 都有;**`webServer` 只有 `dsh-web-app` 提供**,所以:
> **这个插件只能在 web profile 里用。** 装进 `acp`、`headless`、`sdk` 之类的 profile 时,因为 `webServer` 不存在,插件会**静默不加载**——不报错,就是没反应。安装时 `--profile web` 不要省。
## 更新日志
见 [CHANGELOG.md](./CHANGELOG.md)。
Install
dsh plugin --profile web add github:wanjiaju3108/dsh-feishu-assistant
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-feishu-assistant from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.