# DSH Feishu Plugin 将飞书机器人连接到 [DeepSeek Harness](https://github.com/deepseek-ai/DeepSeek-Harness) 的本地 Cordis 插件。用户在飞书中发送文字消息后,插件会把消息作为任务交给指定的 `dsh` profile 执行,并将结果回复到原会话。 > 社区标识:`dsh-plugin` · 支持 Node.js 20+ · MIT License ## 功能 - 通过飞书开放平台长连接接收消息,无需公网回调地址 - 使用 `open_id` 白名单限制可执行任务的用户 - 支持用户在飞书内申请授权及管理员审批、撤销和查询 - 动态授权持久化保存,重启后继续生效 - 执行期间定时更新同一条飞书消息,显示运行时长和最新输出 - 在排队、执行和结果消息中显示当前 Provider 与模型 - 按飞书会话和用户隔离并持久化最近对话上下文 - 主动运行必要脚本,并通过飞书处理 Harness 原生一次性权限审批 - 支持飞书文件入站下载与 Harness 生成文件自动回传 - 串行任务队列,避免多个任务同时操作同一工作区 - 任务接收、开始、完成、失败和队列深度日志 - 自动过滤重复消息 - 执行超时和输出长度限制 - 自动拆分过长的飞书回复 - 插件停止时终止仍在运行的子进程 目前只支持文字消息。 ## 前置条件 - 已从源码安装 DeepSeek Harness,并可通过 `dsh` 命令运行 - Node.js 20 或更高版本 - pnpm - 一个已启用机器人能力的飞书企业自建应用 ## 安装 ### 从 GitHub 安装(推荐) 将主插件直接添加到 Harness 的 `web` profile: ```bash dsh plugin --profile web add github:yangzhaofeng496/dsh-feishu-plugin#main ``` 主插件默认处于禁用状态,且用户白名单为空。安装完成后仍需按下文保存飞书凭据、配置管理员和工作区,再手动启用。 ### 从源码安装 克隆仓库并安装依赖: ```bash git clone https://github.com/yangzhaofeng496/dsh-feishu-plugin.git cd dsh-feishu-plugin pnpm install pnpm check ``` 将本地插件添加到 Harness 的 `web` profile: ```bash dsh plugin --profile web add "$(pwd)" ``` 将配套的执行轨迹插件添加到 `headless` profile: ```bash dsh plugin --profile headless add "$(pwd)/headless-trace" ``` 第一个命令负责飞书连接和任务调度;第二个命令让 headless Agent 输出结构化的模型请求与工具执行事件。未安装 trace 子插件时,任务仍能执行,但飞书只能显示运行心跳和最终输出。 > `headless-trace` 当前作为仓库内的可选配套插件提供;如需结构化执行轨迹,请使用源码安装方式添加该子目录。 ## 配置飞书应用 在[飞书开放平台](https://open.feishu.cn/)创建企业自建应用,然后完成以下设置: 1. 添加“机器人”应用能力。 2. 在“权限管理”中开通接收消息和以应用身份发送消息所需的即时通讯权限。 3. 在“事件与回调”中选择“使用长连接接收事件/回调”。 4. 添加事件 `im.message.receive_v1`(接收消息)。 5. 确保机器人拥有读取消息资源、上传文件和发送消息所需权限。 6. 创建版本并发布应用,使机器人和权限配置生效。 具体权限名称可能随飞书开放平台界面更新而变化。发布前请确认机器人能够接收用户消息,并能向原会话发送消息。 ## 保存凭据 不要把 App Secret 写入 Git 仓库或 `cordis.patch.yml`。将它保存到 Harness 凭据文件 `~/.dsh/.credentials.yaml`: ```yaml FEISHU_APP_ID: cli_xxxxxxxxxxxxxxxx FEISHU_APP_SECRET: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` 限制该文件只对当前用户可读: ```bash chmod 600 ~/.dsh/.credentials.yaml ``` ## 启用插件 编辑 `~/.dsh/profiles/web/cordis.patch.yml`,加入或覆盖 `feishu` 配置: ```yaml - id: feishu disabled: false config: appIdEnv: FEISHU_APP_ID appSecretEnv: FEISHU_APP_SECRET adminOpenIds: - ou_admin_xxxxxxxxxxxxxxxxxxxxxxxxxx allowedOpenIds: - ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx authorizationFile: ~/.dsh/feishu-authorizations.json contextFile: ~/.dsh/feishu-contexts.json contextMaxTurns: 8 contextMaxChars: 20000 attachmentDirectory: .dsh-feishu/inbox maxInboundFileBytes: 31457280 maxOutboundFileBytes: 31457280 maxOutboundFiles: 5 dshCommand: /absolute/path/to/dsh dshProfile: headless providerName: deepseek-official modelName: deepseek-v4-pro workspace: /absolute/path/to/your/workspace timeoutMs: 900000 maxOutputChars: 50000 streamUpdateIntervalMs: 5000 streamHeartbeatIntervalMs: 30000 streamPreviewChars: 2400 maxMessageEdits: 15 approvalTimeoutMs: 600000 ``` 配置字段: | 字段 | 说明 | 默认值 | | --- | --- | --- | | `appIdEnv` | App ID 在凭据文件中的键名 | `FEISHU_APP_ID` | | `appSecretEnv` | App Secret 在凭据文件中的键名 | `FEISHU_APP_SECRET` | | `adminOpenIds` | 可以审批和撤销授权的管理员 `open_id` 列表 | 空列表 | | `allowedOpenIds` | 允许发布任务的飞书用户 `open_id` 列表 | 空列表 | | `authorizationFile` | 动态授权和待审批申请的持久化文件 | `~/.dsh/feishu-authorizations.json` | | `contextFile` | 对话上下文持久化文件 | `~/.dsh/feishu-contexts.json` | | `contextMaxTurns` | 每个会话保留的最近对话轮数 | `8` | | `contextMaxChars` | 每个会话保留的最大字符数 | `20000` | | `attachmentDirectory` | 飞书入站文件在工作区内的保存目录 | `.dsh-feishu/inbox` | | `maxInboundFileBytes` | 单个入站文件大小上限 | `31457280`(30 MB) | | `maxOutboundFileBytes` | 单个回传文件大小上限 | `31457280`(30 MB) | | `maxOutboundFiles` | 单个任务最多自动回传的文件数量 | `5` | | `dshCommand` | `dsh` 可执行文件路径 | `dsh` | | `dshProfile` | 实际执行任务的 Harness profile | `headless` | | `providerName` | 状态消息中显示的 Provider 名称 | `deepseek-official` | | `modelName` | 收到运行时事件前显示的模型名称 | `deepseek-v4-pro` | | `workspace` | 执行任务时使用的工作目录 | 当前目录 | | `timeoutMs` | 单个任务超时时间,单位毫秒 | `900000` | | `maxOutputChars` | 捕获的最大输出字符数 | `50000` | | `streamUpdateIntervalMs` | 飞书进度消息刷新间隔,单位毫秒,最小 1000 | `5000` | | `streamHeartbeatIntervalMs` | 没有新事件时更新运行时长的心跳间隔 | `30000` | | `streamPreviewChars` | 进度消息中保留的最新输出字符数 | `2400` | | `maxMessageEdits` | 主动更换进度消息前的最大编辑次数 | `15` | | `approvalTimeoutMs` | 等待飞书权限决定的最长时间,单位毫秒 | `600000` | `allowedOpenIds` 默认为空,因此没有普通用户能够执行任务。`adminOpenIds` 中的管理员始终拥有执行权限。未授权用户可以在飞书内发起授权申请,由管理员审批,无需重启插件。 ## 飞书内授权 未授权用户发送: ```text 申请授权 ``` 或: ```text /request ``` 机器人会记录申请,并回复申请人的 `open_id`。管理员可以使用: ```text /requests /approve ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx /revoke ou_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx /users ``` 命令说明: | 命令 | 权限 | 作用 | | --- | --- | --- | | `申请授权`、`/request` | 所有人 | 创建或更新自己的授权申请 | | `/requests` | 管理员 | 查看待审批申请 | | `/approve ` | 管理员 | 批准用户执行任务 | | `/revoke ` | 管理员 | 撤销普通用户权限 | | `/users` | 管理员 | 查看管理员和已授权用户 | 动态授权默认保存在 `~/.dsh/feishu-authorizations.json`。该文件以 `0600` 权限创建,不应提交到 Git。配置文件中的管理员不能通过飞书命令撤销,必须由本机管理员修改 `adminOpenIds` 并重启服务。 ## 对话上下文 每个成功任务都会保存用户原始需求和 Harness 最终回答。下一条任务执行时,插件会把最近上下文注入新的 headless Agent,从而理解“继续刚才的任务”“修改上一版”等指代。 上下文使用 `chat_id + open_id` 隔离:不同群聊、不同单聊和不同用户互不可见。默认最多保留最近 8 轮、20000 字符,超出后从最早内容开始裁剪。失败或超时任务不会写入上下文。 可在飞书中使用: ```text /new 新建会话并清除此前上下文 /clear 清除当前上下文 /history 查看最近上下文摘要 /context 查看上下文轮数和字符用量 ``` 上下文默认保存在 `~/.dsh/feishu-contexts.json`,以 `0600` 权限创建。该文件可能包含任务和回答中的敏感内容,请勿提交到 Git 或发送给他人。 ## 脚本执行与权限审批 插件会在任务前加入执行指导:能够通过 Harness 工具完成的操作应直接执行,不要求用户复制脚本后自行运行。普通工作区读写和沙箱内命令按 Harness 当前权限策略执行。 当工具使用 `sandbox_permissions` 请求更高权限时,headless trace 会接管 Harness 原生 `approval/request`,并在飞书发送: ```text 🔐 任务 12ab34cd 请求一次性权限 工具:bash 原因:需要安装项目依赖 审批编号:a1b2c3d4 批准:/allow a1b2c3d4 拒绝:/deny a1b2c3d4 ``` 任务发起者或管理员可以回复: ```text /allow a1b2c3d4 ``` 或: ```text /deny a1b2c3d4 ``` 批准仅对该次工具调用有效,不会永久提升权限。审批通道位于任务专属随机临时目录,任务结束后自动删除;默认等待 10 分钟,超时后按不可用处理并保持拒绝。其他普通用户不能审批不属于自己的任务。 ## 文件传输 ### 从飞书发送给 Harness 授权用户可以直接向机器人发送文件、图片、音频或视频。插件通过消息资源接口下载文件,并保存到: ```text /.dsh-feishu/inbox// ``` 随后插件自动创建任务,把工作区相对路径交给 Harness。文件名会移除路径和危险分隔符;超过 `maxInboundFileBytes` 或为空的文件会被拒绝。入站目录必须位于工作区内部。 ### 从 Harness 回传飞书 任务指导要求 Harness 在生成交付文件后,在最终回答中输出: ```text DSH_FILE: outputs/report.pdf ``` 每个文件单独一行。插件会检查文件真实路径、普通文件类型、大小和工作区边界,然后上传并发送到原飞书会话。符号链接解析后如果越出工作区会被拒绝。 用户也可以手动要求机器人发送已有文件: ```text /send outputs/report.pdf ``` 只接受工作区内部路径;默认单任务最多自动发送 5 个文件,每个文件不超过 30 MB。飞书平台的文件上传接口本身也限制单文件不超过 30 MB。 ## 启动 ```bash dsh web --port 3081 ``` 成功启动时终端会显示类似日志: ```text [feishu] bridge started with 1 authorized user(s), 1 admin(s), 0 pending request(s) [ws] ws client ready dsh web: http://127.0.0.1:3081 ``` 随后在飞书中直接向机器人发送文字任务。也可以发送 `/help` 查看简要说明。 ## 流式进度 收到任务后,插件先发送一条排队消息。任务开始执行后,同一条消息会按 `streamUpdateIntervalMs` 定时更新: ```text ⚙️ Harness 正在执行 任务:12ab34cd 模型:deepseek-official / deepseek-v4-pro 已运行:15 秒 最新输出: 正在检查项目结构… ``` 任务结束时,该消息会更新为完成或失败状态,完整结果另发一条消息。trace 子插件会展示 Agent 生命周期、模型请求、工具名称、脱敏且限长的工具参数和成功/失败状态,不包含模型内部隐藏推理。包含 `secret`、`token`、`password`、`authorization`、`apiKey` 或 `credential` 的参数字段会替换为 `[REDACTED]`。输出中的 ANSI 终端控制字符会在发送到飞书前清理。 插件每 5 秒检查一次状态,但只有出现新事件时才编辑消息;没有新事件时默认每 30 秒发送一次运行心跳。为规避飞书单条消息编辑次数限制,达到 `maxMessageEdits` 后会自动发送一条“进度续接”消息并继续更新。如果飞书提前返回错误码 `230072`,插件也会立即切换到新消息。无需将检查间隔设置得更低。 `providerName` 和 `modelName` 是任务排队及收到首个运行时事件前的备用展示值。trace 子插件收到 `agent_created` 或 `model_request` 后,会自动改用 Harness 实际报告的 Provider 和模型,因此能识别 settings 层的动态覆盖。 ## 运行日志 插件会在运行 `dsh web` 的终端输出日志: ```text [feishu] received message om_xxx; type=text; sender=ou_xxx [feishu] queued task 12ab34cd; queue depth=1 [feishu] task 12ab34cd started [feishu] task 12ab34cd completed in 15230ms; output=2048 chars ``` 日志不会输出 App Secret,但可能包含用户 `open_id`、飞书消息 ID 和任务执行错误。共享日志前请按需脱敏。 ## 常见问题 ### 飞书显示“链接测试成功”,终端却没有收到消息 - 确认订阅方式为长连接。 - 确认已订阅 `im.message.receive_v1`。 - 确认包含机器人能力和最新权限的应用版本已经发布。 - 确认终端出现 `ws client ready`。 ### 提示“此用户尚未获准执行任务” 发送“申请授权”,然后请管理员使用 `/requests` 和 `/approve ` 完成审批。也可以继续将固定用户写入 `allowedOpenIds` 后重启服务。 ### 插件启动失败并提示缺少凭据 确认 `~/.dsh/.credentials.yaml` 中的键名与 `appIdEnv`、`appSecretEnv` 完全一致。 ### 任务执行失败 在同一台机器上手动验证以下命令和目录: ```bash /absolute/path/to/dsh --profile headless "连接测试" ``` 同时确认 `workspace` 存在,并且运行 Harness 的系统用户有权访问该目录。 ## 安全建议 - 始终配置 `allowedOpenIds`,不要将机器人作为无鉴权的远程命令入口。 - 为 Harness 设置独立且范围受限的工作区。 - 不要在仓库中提交 App Secret、API Key 或 `.credentials.yaml`。 - 对删除文件、联网和 Shell 执行等高风险能力使用受限 profile。 - 定期检查飞书应用权限和授权用户列表。 ## 开发 ```bash pnpm install pnpm check ``` 主要文件: - `src/index.mjs`:飞书长连接、任务队列及 Harness 调用 - `cordis.patch.yml`:DSH bundle 插件声明 - `headless-trace/`:headless profile 的结构化执行轨迹插件 ## License [MIT](LICENSE)