dsh-channels
DeepSeek Harness(DSH)飞书渠道插件:一个 dsh 实例对应一个飞书机器人,用户只需与飞书机器人对话即可使用 dsh。
- 对话入口:私聊、群聊 @ 机器人、话题群 @ 机器人
- 收到触发消息:立刻加 “Typing” 表情 → 以话题形式回复(创建话题)→ 绑定 DSH session → 卡片 2.0 流式回复 → 成功时更新为 “DONE”,失败时更新为 “No”
- 话题内后续消息无需再 @,自动延续该 session
- 工作区归属:每用户一个工作区(私聊 = 用户;群聊 = 群 × 用户,同群不同用户互不影响);每个话题在工作区内拥有独立工作目录(默认
topics/<话题 id>/,Git 仓库工作区可自动改用 worktree),因此话题之间的文件互不干扰,而工作区数量只随用户增长
- 入站附件(v2,零等待):收到消息时会话、话题、流式卡片立即建立,不等附件下载。触发消息中的图片自动预取为首轮原生
ImageBlock,模型直接可见;文件默认不下载,prompt 用 <feishu_context> 清单声明“附件未加载”,模型需要时调用 feishu_attachment_fetch 按 message_id 懒取;引用消息与话题历史以 <feishu_quote> / <feishu_history> 摘录,更早的消息用 feishu_topic_messages 翻页(发现的资源同时获得 fetch 授权)。只发附件、不带说明的消息完全静默:不回复、不建会话、不下载、不加表情,本地也不留任何状态;要让模型看到它,请引用那条消息再提问(多个文件可合并转发后引用),话题内的附件也可由模型用 feishu_topic_messages 读到。所有入站图片/文件只存入 DSH 原生附件库(不可变、内容寻址去重、读取校验、随 Session 回放;插件不承担入站附件清理);插件把原生附件库列为必需依赖,不再有工作区 attachments/ 落盘后端。完整设计见 docs/attachment-pipeline-v2.md。
- 把生成的文件发给用户:模型在话题工作目录里生成图片、报告、表格等文件后,可调用
feishu_send_file 直接发回当前话题/会话(可识别图片 ≤10 MB 以图片消息发送,其余以文件消息发送,文件上限 30 MB)。只能发送当前话题工作目录内的普通文件(.. 与符号链接都按真实路径校验,不能读到其他话题或系统文件);需开通 im:resource 权限,缺失时工具返回可读的 PERMISSION_DENIED,回合不受影响。开关:feishuTools.sendFile(默认开)
- 知识库读写:开启
feishuTools.wikiRead / feishuTools.wikiWrite(默认都关)并在 wikiRoots 里配置知识空间链接 https://<租户>.feishu.cn/wiki/space/<space_id> 或节点链接 https://<租户>.feishu.cn/wiki/<node_token> 后,模型可用 feishu_wiki_list 浏览、feishu_wiki_read 读取飞书文档(Markdown,或带 block id 的飞书文档 XML,支持目录/章节/区间/关键词局部读取)、feishu_wiki_create 在范围内新建飞书文档、电子表格或多维表格(新节点也可作为子目录继续往下建)、feishu_wiki_edit 修改本应用创建的文档(追加、在块后插入、替换/删除块、文本替换、整篇重写;必须带上读取时的 revision_id,文档被他人改过会被拒绝)。电子表格用 feishu_wiki_sheet_read 按子表和 A1 范围读成 CSV(每行带 [row=N] 真实行号),feishu_wiki_sheet_edit 向本应用创建的表格写入单元格值或公式、在数据末尾追加行(追加位置会越过空行找到真正的最后一行,目标单元格非空时失败、不会覆盖;同样校验 revision_id;不改样式、图表和行列结构)。多维表格用 feishu_wiki_base_read 列数据表、读字段和记录(支持筛选、排序、关键词搜索、分页),feishu_wiki_base_edit 增改删记录:本应用创建的多维表格可增改删,其他多维表格只能新增记录(不改表和字段结构)。所有调用都用应用身份,并且:读取类工具只有管理员或打开了「知识库读取」开关(/auth set wiki-read on @用户)的人发起的话题能用,新建与修改类工具要求**「知识库写入」开关**(/auth set wiki-write on @用户),两个开关互相独立;wikiRoots 之外一律拒绝;打开某个文档或表格、每次新建、每次修改都会在话题里弹出授权卡,只有话题发起人能点(读取授权在本话题内对该文档持续有效,/renew 或重启后重新询问),超时(approvalTimeoutMs)自动拒绝;浏览目录不弹卡。需开通 wiki:wiki、docx:document(只读可用 wiki:wiki:readonly / docx:document:readonly),电子表格和多维表格另需细分权限 sheets:spreadsheet:read / sheets:spreadsheet:write_only、base:table:read / base:field:read / base:record:read / base:record:create|update|delete(旧版 sheets:spreadsheet / bitable:app 不生效),且应用要是目标知识空间的成员(可把包含机器人的群加为空间成员/管理员)
- HITL 审批:当模型请求需要授权的动作(如 bash 沙箱升级
sandbox_permissions)时,插件监听主机 approval/request 事件,在同一话题里新发一张独立审批决策卡:卡片标题带工具名;工具在审批请求里给了说明时,说明按行显示为卡片主体(标签:值 行显示成标签和值,缩进行显示成列表,不按 markdown 解析),bash 等带 command 参数的工具另外显示将执行的命令,其余参数按 名称: 值 收进默认折叠的「调用参数」,没有说明和命令时直接展开显示;提权时写明提权目标。提供 ✅ 允许 / ❌ 拒绝 / ♾️ 本话题不再询问 三个按钮;也可以在话题里直接回复「允许」「拒绝」「全部允许」,回复其他内容视为拒绝并作为下一条指令。等待期间流式卡片状态行显示「✋ 等待审批:工具名」。超时(approvalTimeoutMs,默认 5 分钟)自动拒绝(fail-closed),非会话发起人点击或回复无效
- 结构化提问:模型调用
ask_user_question 时,插件监听主机 user-questions/request 瀑布并发一张问答卡(每个问题一组选项按钮,多选先勾选再「提交本题选择」),答案回填给模型;主机自带的应答者只有 Web 客户端,没有本插件时纯飞书会话会以 NO_PROVIDER 失败。超时(questionTimeoutMs,默认 5 分钟)或回合取消时本轮提问失败(fail-closed);没有可选项的问题直接提示用文字作答,阻塞期间普通文本回复即作为该问题的自由文本答案(同时支持多选后的自定义文本)
- 准入与授权(默认开启):准入只看一张扁平的用户表,每人有四个独立开关:对话准入、知识库读取、知识库写入、代码改动。管理员也是用户表里的一行:打开「管理员」开关的人拥有全部权限(其他开关对他不起作用),可以有多个,只能在 Web 设置面板设置。对话准入打开的人可以私聊机器人,也可以在任何群里 @ 它(包括自己把机器人拉进的群),不再有群白名单;对话准入关闭等于停用,其他开关一并失效。新加入的人只打开对话准入。不在表里的人私聊只收到一条固定提示(
/userid 例外),在群里 @ 机器人或把它拉进群时,管理员私聊收到用户授权申请卡。授权入口:飞书 /auth 命令、授权卡、Web 设置面板;管理员只能在面板用户表里设置(私聊 /userid 获取 user_id),飞书里不能授予或移除管理员。用户表里没有管理员时 guarded 拒绝所有对话
- 代码改动开关:飞书会话(含它派生的子代理)调用
codeWriteTools 里的工具(默认是 dsh-repo 的推送、MR 与 Issue 写操作,以及观象的 SKILL 提交)之前,插件检查当前这一轮的发言人是否为管理员或打开了「代码改动」开关;没有时工具直接返回拒绝原因,模型会转告用户。本地工作区里的修改不受影响。共享话题里谁发的消息谁就是发言人;没有新消息开启的回合(子代理结果、后台任务)沿用上一位发言人。检查在工具自己的审批之前进行,所以不管 dsh-repo 的 tools.writes 设为 ask 还是 allow 都生效
- 提交记录飞书发言人:飞书会话(含子代理)通过 dsh-repo 推送工作区时,插件监听 dsh-repo 的
repo/commit-trailers 事件,在提交说明的 DSH-Session: 之后加一行 Feishu-User: 名字 <user_id>,记录的是上面同一位发言人。名字取通讯录(需 contact:user.base:readonly),读不到时取用户表里记下的名字,都没有时只写 user_id。需要 dsh-repo 支持该事件,旧版本 dsh-repo 不加这一行
- 提交作者显示为发言人:管理员在设置面板「访问授权」用户表里点用户行的「未设 Git 邮箱」,给这位用户填 Git 邮箱(填本人 GitHub/GitLab 账号已验证的邮箱,留空清除)。之后这位用户在飞书里让机器人推送代码时,插件通过同一事件的
setAuthor 把提交作者设为「名字 <Git 邮箱>」,提交者仍是 dsh-repo 的令牌账号,Feishu-User: 尾注照常保留;平台按作者邮箱显示本人的头像和链接。没设 Git 邮箱的用户作者仍是令牌账号;管理员同样在自己那一行填 Git 邮箱。dsh-repo 不支持 setAuthor 时只加尾注
- PR/MR 动态唤醒飞书会话:dsh-repo 跟踪的 PR/MR 有 CI 结果、评论、评审或合并时会唤醒发起它的会话。插件以
prepend: true 监听 dsh-repo 的 repo/resolve-agent 瀑布:飞书会话已经不在内存里时,由插件按飞书的组合恢复(带飞书工具和回复规范),其他会话原样 next()。没有 dsh-repo 时这个事件不会触发
- 全部基于飞书官方 SDK
@larksuiteoapi/node-sdk(WebSocket 长连接,免公网)
- Web 设置面板:Web/桌面端左侧栏 飞书渠道 入口(主区整页),顶部状态头 + 7 个标签页(连接 / 会话 / 模型 / 附件与工具 / 访问授权 / 知识库 / 高级);凭据不完整时插件不连接、面板显示三步引导,填写保存后自动连接(保存写入本机覆盖文件,部分字段需重连生效);详见下文「Web 设置面板」章节
- 对话里的飞书来源:从飞书发起的会话在 Web/桌面端对话标题旁多一个来源按钮(群名或「飞书私聊」),点开在右侧栏「飞书来源」标签页查看群聊/私聊、话题、发起人、开始与最近活跃时间,并可跳回飞书;其他会话不显示;详见下文「对话里的飞书来源」章节
设计与可行性论证见 docs/feasibility-and-design.md;工作区粒度与会话隔离的取舍见 docs/workspace-isolation-design.md;入站附件管线(存储 / 清单 / 工具 / 引用与历史 / 降级 / 隐私边界)见 docs/attachment-pipeline-v2.md。
斜杠命令
/help:列出当前支持的斜杠命令,无需绑定 Session;私聊和普通群聊中直接回复、不创建话题,话题群或已有话题内则回复在该话题中
/userid:仅在私聊中直接回复发送者自己的 user_id,无需绑定 Session,也不创建话题;任何私聊用户都可以用(准入配置/授权引导用,不消耗 token)
/groupid:仅在群聊中直接回复当前群组 ID,无需绑定 Session,也不创建话题;普通群聊遵循 requireMention,话题内无需重复 @
/health:显示 Harness 与飞书通道健康状态,无需绑定 Session;回复方式同 /help(直接回复,话题内回在话题中)
/status:显示当前 Session 状态与性能指标
/workspace:显示当前 Session 工作目录
/effort:查看当前 Session 推理强度;当前模型声明了可选档位时,回复卡附档位按钮,点一下即切换;/effort <档位-id> 切换,/effort reset 恢复配置默认
/compact:调用 Harness 原生命令压缩当前 Session 上下文
/renew:在当前话题创建并绑定新 Session
/stop:中断当前正在运行的回合(不冷恢复 Session;排队中的后续消息保留并接着运行;没有运行中的回合时明确提示)。流式卡片上的「⏹ 停止」按钮效果相同
/permissions:查看当前 Session 的权限预设(沙箱 + 审批策略)与可选清单;仅在 allowPermissionSwitch: true 时支持 /permissions <name> 切换,此时回复卡附预设按钮
/auth:仅管理员。/auth status 查看准入状态和各开关人数;/auth add @用户 把人加入用户表并打开对话准入(已在表里的人只重新打开对话准入,其他开关不变);/auth remove @用户 移出用户表;/auth set <chat|wiki-read|wiki-write|code> <on|off> @用户 打开或关闭某个开关(人必须已在表里);/auth list 列出用户表与待处理申请。都可以直接写 user_id 代替 @,@ 时自动取被 @ 成员的 user_id、忽略对机器人的 @;外部用户没有 user_id,无法授权
/history:显示当前话题的 Session 历史(含 ctx.sessionQuery 折叠出的会话标题与 live 标记)
/export:导出当前 Session 的逻辑日志
选择卡的选项单列排列;按钮等同于在该卡下输入对应命令(如点「high」= /effort high),走同样的准入、归属与校验,执行结果原地替换该选择卡(更新失败时才另发一张结果卡);只有话题发起人点击、且卡片仍在原聊天里时才生效,话题换绑 Session 后旧卡上的按钮失效。按钮最多显示 12 个,其余可直接输入命令。
/new 是 /renew 的别名。/help 由命令注册表按分组生成,新增命令只需在注册表里加一行。
不在上面命令表中的 / 开头消息(如 /some-skill …)不由插件处理,而是和普通消息一样经过准入后作为 prompt 交给 DSH,由 DSH 展开对应的用户可调用 skill;没有同名 skill 时按普通文本处理。
飞书原生斜杠指令面板(可选,slashCommandPanel: true 开启,默认关闭;部分企业不允许应用添加斜杠指令):开启后每次连上飞书,插件在后台调用 application/v7/app_slash_commands 把命令注册表同步到飞书原生面板,用户在聊天框输入 / 即可直接选择命令(飞书客户端有约 5 分钟缓存)。同步是全量对齐且幂等:缺的创建、描述变了的原地更新、注册表里已没有的或重复的删除(别名不单独列出),注册表不变时再次连接不产生任何写操作;超时(30 秒)或断开后不再发起新的写操作。需要可选权限 application:app_slash_command:read(读取现有命令)与 application:app_slash_command:write(增删改),两项只在开启时列入权限自检。关闭时插件不读写面板。同步失败只影响面板列表,直接输入命令照常可用;失败原因显示在 Web 设置「连接」页并打印一条 warn。
流式卡片
卡片 2.0,按状态行 → 过程 → 正文 → 停止按钮 → 页脚组织,默认 500ms 刷新(refreshMs 可调):
- 状态行与过程:
- 状态行(卡片第一行,只在当前正在流式输出的那张卡上渲染,回合结束后显示在主卡):只由 DSH 回合事件驱动,用词与 DSH Web 的会话状态一致,进行中的状态带已运行时长(
statusRefreshMs,默认 5s 慢心跳刷新),优先级从高到低:
✋ 等待审批:bash(审批卡片未决定)→ ❓ 等待回答(ask_user_question 卡片未回答)→ 🗜 正在压缩上下文(compaction/start 到 compaction/end)
- 模型请求重试:DSH(
dsh-llm-retry)在限流、服务端错误、超时等可重试失败后会在同一回合内重跑失败的那一步,状态行显示 ⚠️ 模型请求失败(RATE_LIMIT),N 秒后第 k/5 次重试,等待结束后变为 🔁 模型请求重试中(第 k/5 次),重试的请求开始输出即恢复。失败那次已流出的半截正文/推理不进入模型历史,卡片随即撤回,重试从头输出。只有 turn/end 才结束卡片;重试用尽仍失败时,失败提示后附「已自动重试 N 次」
🔧 调用工具:bash · 已运行 Ns(tool/call 到 tool/result;subagent 工具显示 🤖 调用子代理: 加其 description,workflow 工具显示 🧵 调用工作流: 加 meta.name)
🧠 思考中(当前这次请求在输出推理)→ ✍️ 输出中(在输出正文)→ 其余时间 ⚙️ 进行中
- 结束后:
✅ 已完成 · 用时 1分5s、⏹ 已停止 · 用时 …(经 /stop 或停止按钮)、❌ 失败 · 用时 …
- 思考过程折叠面板(
collapsible_panel):带边框、默认折叠,右侧箭头随折叠状态翻转(折叠向下、展开向上,icon_expanded_angle: 180)。标题固定为「思考过程」,结束后附推理用时(如「思考过程 · 12.3s」);只在有推理内容时渲染,不表示状态
- 委托进度行(小字,只在当前正在流式输出的那张卡上渲染,与状态行互相独立):
tool-workflow/run-start / agent-start / agent-end / run-end(工作流进度:完成数/总数、阶段、活动成员)→ run 结束转为 🧵 工作流 xxx 结束(完成) 并保留到最终卡片;子代理进度(跨会话映射):子代理是独立 session(header.parentSession 指向主会话),其 subagent/descriptor(label)、tool/call(当前工具)被折叠为 🤖 子代理执行中:检索实现方案 (read) · 已运行 45s,并行子代理显示计数,完成后在最终卡片汇总为 🤖 子代理完成:a、b。只显示状态,不显示输出——子代理正文是父模型的中间产物,绝不进卡片正文
- 回合进行中还没有正文时,卡片只显示状态行,不放「正在生成」之类的占位;结束时仍没有正文才显示「(本次无正文输出)」
- 正文:富文本 markdown;超长正文自动分段投放——单卡片预算用满后按干净断点(段落 / 标题 / 代码块边界)切分,且每张卡最多 5 个表格(飞书硬限制,错误 11310);断点会避开代码围栏与表格,单个表格/代码块小于单卡预算时不会被从中间切断(单块本身超过单卡预算时只能硬切,内容仍不丢失)。续片以「📄 回复分段 n」卡片跟进同一话题,全文无丢失;续卡被飞书拒收时自动降级为纯文本消息(内容仍送达)。续卡只留正文:
📄 回复分段 n → 状态行与委托进度行(仅当它是当前活动卡)→ 正文,无面板、无页脚、无分割线
- 停止按钮:回合进行中,当前正在流式输出的那张卡(主卡或最新续卡)正文下方显示「⏹ 停止」,效果同
/stop。首帧不带按钮;排在后面的消息的卡片在轮到它运行时才出现按钮。只有话题发起人在原聊天里点击才生效,点击后按钮变为不可点的「正在停止…」;其他人点击只会在按钮下加一行「仅发起该会话的用户可停止」。回合结束后按钮移除;经 或按钮停止的回合状态行为「⏹ 已停止」。
回复写法规范:每个飞书会话的系统提示词里有一段「Replying in Feishu」(dsh-channels:feishu-replies),要求模型的最终回复先给结果和可点击链接、通常一到三句、技术细节放进 MR 描述或文件、需要决定时用 ask_user_question 问一个具体问题、如实说明未完成或失败、回复语言跟随用户消息、群聊里不转述私聊或其他会话的内容。它只约束最终回复的文字,卡片的状态行和思考过程不变。会话先被其他组合恢复(例如 Web 端先打开了它)时,插件接手时也会补上这一段和附件说明,每个 Agent 只注册一次。replyGuidance: false 关闭,修改后从下一次模型请求开始生效。
Web 设置面板(左侧栏 → 飞书渠道)
包内含可选的浏览器半区(lib/client.js,DSH client module):在 Web/桌面端左侧栏注册「飞书渠道」全局入口(sidebar.panellist),点击后在主区打开整页面板(main 槽)。宿主没有 webServer 服务(headless profile)时不加载面板,飞书通道本身不受影响。
连接时机:启用插件时不会无条件连接飞书。插件启动先读取 profile 配置与 Web 覆盖,再检查 appId / appSecret 两个凭据引用能否解析出值:
- 凭据完整 → 建立飞书长连接(重启 DSH、重新加载插件时同样先检查再连接)。
- 凭据不完整 → 插件正常加载、不建连接、不报错,面板显示「待配置」与三步引导;在「连接」标签页填好两项凭据并保存后自动连接。
- 连接失败(凭据错误、网络等)→ 记录在面板状态卡,插件保持加载;修好凭据或网络后点「重试」。
- 本机另一个实例已连接同一个机器人 → 插件保持加载、不建连接,面板显示「已被占用」,可点「接管连接」(见下文)。
面板结构:顶部固定状态头(标题、阶段徽标:待配置 / 未连接 / 连接中 / 已连接 / 连接失败 / 已被占用、插件版本、运行时长、刷新按钮),下方按标签页分区。每一项是单列一行:名称 + 一句话说明 + 控件,两值选项用开关,较长的说明收在「详情」里;数值项按显示单位输入(天 / 秒 / ms / 条 / 字节,字节项旁显示 MiB 读数)。
| 标签页 | 内容 |
|---|
| 连接 | 状态块(当前阶段唯一的操作按钮:连接 / 重试 / 重启连接,连接后显示话题路由 / 活动会话 / 在途回复 / Bot open_id 尾号)、自检(权限缺口可展开逐项开通、斜杠指令面板同步结果)、飞书应用凭据(更换 / 移除);未配置时显示三步引导,有需重连才生效的已保存项时顶部显示「重启连接」横幅 |
| 会话 | 「触发」:私聊、普通群需要 @、话题被拒时降级为会话级(均为开关);「工作区」:工作区根目录、话题工作目录隔离方式(topicIsolation);「话题与卡片」:话题过期(routeTtlMs,按天)、审批 / 提问卡片等待(approvalTimeoutMs / questionTimeoutMs,按秒)、流式刷新与状态行保活间隔;「飞书侧功能」:首次私聊引导、后台唤醒卡片、回复写法规范、斜杠指令面板同步、权限切换 |
| 会话记录 | 飞书发起的全部会话:群聊按「群 → 用户 → 会话」、私聊按「用户 → 会话」分组,显示飞书群名、姓名和头像;顶部搜索框按群名、姓名或会话标题筛选;点会话行在主区进入该对话,用户行的「文件」只读浏览该用户的工作区(见下文「会话记录」) |
| 模型 | 服务商、模型、推理档位、单次输出上限(maxTokens,留空使用模型默认)、Agent 预设 |
| 附件与工具 | 「入站附件」:接收图片和文件(附件存入 DSH 原生附件库)、图片预取、首轮话题历史;「工具」:feishu_topic_messages / feishu_send_file 开关(feishu_attachment_fetch 随「接收图片和文件」挂载);「限额与超时」:预取上限、读取附件上限与超时、读取话题消息超时、引用摘录与历史超时(共 8 项,默认收起,有改动或错误时自动展开) |
| 访问授权 | 待处理申请(置顶,一键授权 / 忽略)、调试模式(accessMode,开关)、申请有效期;用户表:每人一行,「管理员」加四个开关(对话 / 读知识库 / 写知识库 / 改代码)点一下即切换,管理员或对话关闭时其余开关变灰(添加表单默认收起) |
| 知识库 | 知识库读 / 写工具开关(feishuTools.wikiRead / feishuTools.wikiWrite)、可访问范围 wikiRoots(每条链接一个输入框,逐条校验);「可使用的人」:用户表里读 / 写知识库开关的人数,在「访问授权」里逐人设置 |
| 高级 | SDK 日志级别、Web 覆盖文件路径与「恢复 profile 配置」 |
调试模式:「访问授权 → 调试模式」打开即 accessMode: open,所有人都能和机器人聊天;打开期间面板顶部常驻黄色警告。默认关闭(guarded)。
入站附件与模型工具(v2):「附件与工具」标签页的字段与 profile 配置同名、写入同一 Web 覆盖文件($DSH_HOME/dsh-channels/settings.json):
- 入站附件:附件接收(
acceptAttachments,同时决定是否挂载 feishu_attachment_fetch)、触发消息图片预取(prefetchImages)、首轮话题历史条数(historyBootstrapLimit)。
- 工具:
feishu_topic_messages(feishuTools.topicMessages)、feishu_send_file(feishuTools.sendFile)开关。
- 限额与超时:预取单图 / 总量 / 整段超时(
prefetchImageMaxBytes / prefetchTotalMaxBytes / prefetchTimeoutMs)、读取附件资源数 / 字节预算 / 超时(feishuTools.attachmentFetchMaxResources / attachmentFetchMaxBytes / attachmentFetchTimeoutMs)、读取话题消息超时(feishuTools.topicMessagesTimeoutMs)、被动引用 / 历史读取预算(promptContextTimeoutMs)。
feishuTools.* 是嵌套配置:面板只把改动过的子键写进覆盖层,其余子键保持原值(Web 覆盖或 profile 配置),不会因部分保存变成未定义。字段默认值、边界与降级规则见下文配置示例和 docs/attachment-pipeline-v2.md。
同一台机器只有一个实例连接同一个机器人:飞书会把一个应用的事件随机推给它的某一条长连接,同一台机器上两个 dsh 实例(不同 profile,或 Web 与桌面端同时开着)连同一个机器人时,各自只收到一部分消息,话题后续消息和卡片按钮可能落到不认识这个话题的实例上。因此插件连接飞书前先按 App ID 占用一个本机 IPC 端点(Linux 为不落盘的 abstract socket,Windows 为命名管道,macOS 为系统临时目录下的 socket 文件),覆盖当前系统用户的所有 profile 和 $DSH_HOME。进程退出、崩溃或被杀时端点由系统释放,无需心跳或 PID 文件。拿不到端点的实例照常加载、不连接飞书,面板显示「已被占用」和占用方(pid、$DSH_HOME、插件版本);点「接管连接」会让占用方断开飞书并释放端点,本实例随即连接(会中断对方进行中的回复,那些卡片在对方下次连接时按中断处理)。只支持单机部署:不同机器上的实例无法互相发现。
连接状态:阶段徽标(待配置 / 未连接 / 连接中 / 已连接 / 连接失败 / 已被占用)、飞书长连接状态、Bot open_id 尾号、运行时长、话题路由数 / 活动会话 / 在途回复、最近一次连接失败原因;每 5 秒自动刷新,也可手动刷新或重启飞书连接(会断开当前长连接并中断进行中的回复)。徽标不是健康时会额外显示「健康判定」原因——健康等级由宿主所有 Cordis fiber 折叠而来,因此「降级」也可能来自其它插件入口仍在等待依赖/加载(与飞书通道无关),原因行会列出对应插件名;「连接失败」只针对飞书连接本身,插件入口保持加载。
飞书应用凭据:在「连接 → 飞书应用凭据」卡片里填写 App ID / App Secret 并保存,值经宿主 ctx.credentials 写入 DSH 凭据库(默认 $DSH_HOME/.credentials.yaml 的 refs: 段,仍遵循凭据层的解析优先级),面板只显示「已配置 / 未配置」与来源,永不回显已存值。两项齐全且通道未运行时保存后自动建立连接;已连接时保存只提示重连;只填了一项时保存成功但不会连接(状态保持「待配置」),补齐另一项后自动连接。每项凭据一行,点「更换」展开输入框,已存值可以单独「移除」;行内显示的是凭据引用名(profile 配置里的 appId / appSecret),换引用名仍需改 profile 配置。
统一保存:除连接类操作外,可编辑字段共用一个跨标签页的草稿;有未保存改动时页面底部浮出保存栏(「N 项未保存」、其中需重连的项数、「放弃 / 保存」),没有改动时不显示。只提交改动过的字段,标注「需重连」的项在下次连接时生效;保存后「连接」标签页顶部横幅列出待生效项并提供一键重连。
模型选择器:模型与推理在面板里是独立的「模型」标签页:宿主 LLM 服务可用时,provider / model / reasoningEffort 与 agentPreset 渲染为下拉选择而不是手填:provider 来自 ctx.llm.listProviders(),model 来自所选 provider 的 listModels(),推理档位来自 resolveModelInfo(provider, model).reasoning.efforts,Agent 预设来自 agentPresets.list()。每个选择器的首项是「留空 = 部署默认 / 适配器默认 / roster 默认」,有部署默认模型(agentDefaultModel)或 roster 默认预设时会在标签里带上具体值。选择按依赖级联:先选 provider 才会加载 model,选了 model 才会加载推理档位;宿主暂时无法枚举的项(服务未挂载、适配器查询失败/超时)自动退回原来的自由文本框,已保存但当前列表里没有的值会作为「当前配置,不在宿主机列表中」选项保留,不会在打开面板时被静默改掉。级联查询走只读的 GET /api/dsh-channels.catalog?provider=…&model=…(Host 侧短 TTL 缓存,单次查询 4 秒上限)。provider 与 model 在宿主侧是一条完整的请求路由,因此面板保存时要求两者同时设置或同时留空,不允许只选一半。
baseDir 是模型/工具产生文件的落点(入站附件只进 DSH 原生附件库,不落这里):每个用户拥有一个独立工作区(私聊 <baseDir>/p2p/<user_id>/,群聊 <baseDir>/groups/<chat_id>/<user_id>/,同一群里不同用户互不影响;旧版本的「用户 × 聊天」共享工作区正好复用为这个新工作区)。每个话题在工作区内拥有独立工作目录 <工作区>/topics/<话题 id>/(无话题的降级会话用 session id),所以话题之间的文件互不干扰,而工作区数量只随用户增长。面板里只接受绝对路径(~ 开头按家目录展开,拒绝文件系统根目录),保存时会创建并校验该目录,不可用时当场报错,不会等到飞书里提示「会话创建失败」。已有 Session / 目录不会被迁移,因此切到新目录后旧话题继续用旧目录(话题内 /renew 也一样),只有之后新开的会话使用新目录。
可视化选择工作区根目录:输入框旁的「选择目录…」按钮会调用宿主机的系统目录选择框(macOS 的 NSOpenPanel / Windows 的文件夹对话框),选中后把绝对路径填进草稿,仍需点「保存更改」才落盘。该能力转发自宿主可选的 ctx.directoryPicker 服务:桌面端/本机 GUI 会话(native 后端)提供按钮;headless、远程或 SSH 会话没有可服务的显示会话(browse 后端),面板不显示按钮、保留手写输入,插件在缺少该服务的旧宿主上照常加载。选择框打开期间请求保持挂起;取消是正常结果,不会报错。
访问授权:「访问授权」标签页集中管理准入:调试模式、授权申请有效期;「用户表」支持添加(user_id 可用 /userid 查看)、移除和逐个切换「管理员」与四个开关;「待处理申请」可一键授权/忽略(授权后只打开对话准入)。打开「管理员」的用户在任何私聊/群聊都直接放行、拥有全部权限,可以有多个;管理员只能在这里设置,飞书 /auth 不能授予或移除。guarded 且用户表里没有管理员时,标签页顶部会红字提示“当前会拒绝所有对话”。用户表、待处理申请在每次进入页面时实时读取飞书头像和名字(GET /api/dsh-channels.profiles?users=…&chats=…,每类最多 200 个,Host 侧缓存 30 秒):用户需要 contact:user.base:readonly 且在应用的通讯录权限范围内,群需要 im:chat:readonly;读不到时显示授权时记下的名字或 ID,并在标签页顶部注明失败原因。用户表与申请记录保存在插件 storageDomain(channels_routes_v2 域),重启后保留;未建立飞书连接时用户表尚未加载,标签页会提示「连接建立后加载授权列表」并禁用写操作。
会话记录:「会话记录」标签页列出插件记录过来源的全部会话(存储表 session_origins,包括已 /renew 或过期的旧会话),群聊下再按发起用户分组,每一层按最近活跃排序。群名、姓名与头像和「飞书来源」使用同一套实时查询与 30 秒缓存,读不到时显示 ID 并在顶部注明原因。会话标题与工作区根目录来自宿主 sessionQuery(会话日志的标题和 header cwd),宿主没有该服务时标题留空、工作区退回到当前路由记录的目录。顶部显示群数、用户数和会话数,搜索框按群名、姓名、ID 或会话标题筛选(命中群或用户时保留其全部会话,搜索期间所有层级展开)。时间显示为相对时间(刚刚、N 分钟前、昨天 HH:mm 等),悬停可看完整的开始与最近活跃时间。绿点和「当前话题」标记表示飞书里的这个话题仍然接着该会话,「群共享」表示群内共享会话。点会话行通过 Web 壳的 uiWorkspace.openSession 在主区打开对话,壳不提供该服务时会话行禁用。用户行的「文件」进入只读文件浏览:左栏为面包屑和目录列表(目录优先排序,按文件类型显示图标,单个目录最多列 2000 项),右栏预览选中的文件(UTF-8 文本带行号,最多 512 KB;png/jpg/gif/webp/bmp/svg 图片最多 5 MB 内联显示;其余只提供下载),面板较窄时两栏上下排列;下载单个文件上限 100 MB。文件路由只服务飞书会话的工作区根目录,路径必须是根目录内的相对路径,跟随符号链接后仍要落在根目录内,否则返回 403;接口为 GET /api/dsh-channels.sessionRecords、/api/dsh-channels.workspaceFiles、/api/dsh-channels.workspaceFilePreview、/api/dsh-channels.workspaceFileDownload(?workspace=<根目录>&path=<相对路径>),与其它面板路由一样需要 DSH 登录。
配置层级:configSchema 默认值 < profile cordis.patch.yml 的 config < Web 面板覆盖。面板只把可编辑字段写入 $DSH_HOME/dsh-channels/settings.json;「高级 → 恢复 profile 配置」确认后删除该文件并回落到 profile 配置。baseDir 与 topicIsolation 在「会话 → 工作区」编辑(只影响之后新建的话题);话题过期、审批 / 提问卡片等待与 maxTokens 保存后立即生效,不需要重连。凭据值在「连接 → 飞书应用凭据」编辑,凭据引用名(appId / appSecret 配置项)仍由 profile 配置决定。已移除的目录清理配置 workspaceRetention / workspaceRetentionAuto 与覆盖文件路径配置 settingsFile 若仍留在 profile 或覆盖文件中会被静默忽略,面板下次保存时从覆盖文件中去掉。
为什么落在插件自己的文件而不是 profile 的 cordis.patch.yml? DSH 提供官方 ctx.configEditor.edit(),可以安全写 profile patch(完整 Config 校验、保留注释与 !!js、原子替换、失败回滚、与 HMR 串行);但它是「完整配置编辑」通道——本插件的 Config 是 zod,没有 schemastery .volatile() 标记,普通字段写入会触发 Loader 重载插件,等于每次保存都断开并重连飞书;而且该服务只在有 profileContext 的启动器(桌面端 / dsh web)存在。$DSH_HOME/<plugin>/ 是 DSH 插件保存运行时数据的惯例(如 dsh-pet 的 $DSH_HOME/dsh-pet/),因此面板偏好写入 $DSH_HOME/dsh-channels/settings.json:保存即生效(需重连字段除外)、可一键清除回退、不依赖 profileContext;profile 的 cordis.patch.yml 继续作为部署级基础配置层。若将来要求「所有配置统一进 profile」,迁移路径是改用 ctx.configEditor.edit() 并接受保存触发插件重载。
安全边界:面板路由都通过宿主 connection.fetch 注册为 DSH /api 下的精确路由 /api/dsh-channels.<方法>(读用 GET,写一律 POST),由 DSH 的 /api 统一做 Host/Origin 校验和登录 cookie 鉴权:未登录返回 401,Host/Origin 不受信任返回 403,只有通过的请求才会到达插件;宿主没有 connection 服务时面板路由不挂载。浏览器按相对路径请求(DSH 页面带 <base href="./">),DSH 挂在反向代理前缀下(如 /dsh/)时请求自动带上前缀。用户表与待处理申请同样面板可写,因此“已登录 DSH Web = 可授权”;非回环绑定时登录 cookie 走明文 HTTP,建议给 DSH Web 服务配置 TLS。凭据值只走写路径、响应永不回显,也不会写进 Web 覆盖文件。baseDir 可写意味着同源页面能改变模型文件的落点(面板已拒绝文件系统根目录)。「会话记录」的文件路由只读,同样意味着“已登录 DSH Web = 可读取飞书会话工作区里的文件”。
构建:DSH 只加载已构建的浏览器 bundle,改了 src/client/ 或 src/settings/contract.ts 后需要重新构建并重启应用:
pnpm build # 产出 lib/index.js(Host 入口)与 lib/client.js(浏览器半区)
pnpm verify:host # 以纯 ESM 导入 Host bundle,检查 Cordis 插件面
pnpm verify:client # 用 __ModuleLoader__ shim 做 loader 冒烟
对话里的飞书来源
从飞书发起的会话(话题、会话级降级、/renew 新开的会话、其他插件经 feishuTopics 发起的群共享话题)在 Web/桌面端对话标题旁会出现一个来源按钮:群聊显示群名,私聊显示「私聊 · 对方姓名」(读不到姓名时为「飞书私聊」)。点击后在右侧栏打开「飞书来源」标签页,内容包括:
- 标题:群聊为群头像和群名,私聊为对方头像和姓名;
- 来源:私聊 / 群聊 / 群聊 · 话题;群名读不到时显示群 ID;
- 发起人:头像、姓名 +
user_id(群共享话题显示发起人,并注明群内成员都可以继续);
- 开始于 / 最近活跃:飞书侧已经换到新会话(
/renew、/new)或路由过期后,「最近活跃」换成「状态:飞书侧已切换到新会话或已过期」;
- 在飞书中打开(飞书 applink 打开对应聊天)与「刷新」。
Web 里直接新建的会话不显示按钮。来源在 Host 侧按会话 id 持久保存(存储表 session_origins,路由过期后仍可查;升级前已有的路由在启动时补写)。群名、群头像、姓名与头像在每次打开「飞书来源」标签页时实时读取(Host 侧缓存 30 秒,标题旁按钮与标签页同时打开只查一次):群用 im:chat:readonly,用户优先用通讯录 contact:user.base:readonly,缺失时群聊从群成员列表查姓名(没有头像),私聊只显示 user_id。接口为 GET /api/dsh-channels.sessionOrigin?sessionId=…,与设置面板一样需要 DSH 登录。右侧栏不可用的宿主(如 headless)不注册这部分。
发布
版本号由 semantic-release 按 Conventional Commits 计算。版本号始终保持 0.x:破坏性变更(BREAKING CHANGE)只升次版本号(如 0.7 → 0.8),不会升到 1.0.0。推送(含合并 PR)到 dev 在检查通过后自动发布 beta 版;正式版只在手动触发时发布,推送到 main 只跑检查。
| 分支 | 触发 | 版本 | npm dist-tag |
|---|
main | 手动 | 正式版,如 0.5.0 | latest |
dev | 推送自动 | 预发布版,如 0.5.0-beta.1 | beta |
发布正式版:在 GitHub Actions 的 pipeline workflow 点击 Run workflow 并选择 main。dry_run 默认勾选,只预览下一个版本号;取消勾选才真正发布 npm 包、打 tag 并创建 GitHub Release。在 dev 上手动触发同样可用,用来预览或补发 beta。安装 beta 版用 @modesto_ma/dsh-channels@beta。
目录结构
src/
├── index.ts # 插件入口 apply(ctx):effect + 导出 barrel
├── config.ts # 配置 schema 与凭据引用校验
├── service.ts # FeishuService 门面:生命周期、事件接线、入站与命令编排
├── access/ # 准入与授权:纯策略、持久化用户表、授权卡、申请生命周期、代码改动闸门
├── dsh-types.ts # 宿主类型桥 barrel → src/host/
├── host/ # DSH 表面(纯类型,无运行时依赖)
│ ├── core.ts # 身份 / LLM 消息与流 / Session 日志与事件
│ ├── agent.ts # Agent、注册表、agent preset
│ ├── attachments.ts # ctx.attachments 窄类型:Image/File 引用、内容块、错误码
│ ├── commands.ts # ctx.commands 表面
│ ├── interaction.ts # approval/request + user-questions/request
│ └── services.ts # 工作区 / 存储域 / 凭据 / token / llm / 投影 / HostContext
├── cards.ts # 卡片 barrel → src/cards/
├── cards/ # 卡片 2.0 JSON 渲染
│ ├── primitives.ts # 信封、markdown、折叠面板、按钮、分栏等原语
│ ├── streaming.ts # 流式回复卡构成(面板/活动行/正文/停止按钮/结算页脚)、分段、字节/表格预算
│ ├── picker.ts # /effort、/permissions 选择卡与按钮回传解析
│ ├── onboarding.ts # 私聊首次触达引导卡
│ ├── topic.ts # 群话题服务的根卡片(引导句、链接表格、状态标签)
│ ├── approval.ts # 审批决策卡
│ ├── missed.ts # 断连补偿的待处理卡
│ └── question.ts # 结构化问答卡
├── approvals.ts # 审批 barrel → src/interactions/
├── questions.ts # 问答 barrel → src/interactions/
├── interactions/ # HITL 交互
│ ├── pending.ts # 待决卡共享生命周期(登记/送达/门禁/超时/settle-once)
│ ├── approvals.ts # 审批应答器
│ └── questions.ts # 结构化提问应答器
├── session/ # 会话侧基础设施
│ ├── routes.ts # 路由存储 + TTL 墓碑
│ ├── repair.ts # inflight 快照 + 重启恢复(重放 / 补画 / 继续)
│ ├── manager.ts # live Agent、冷恢复、preset、/renew 事务
│ ├── isolation.ts # 话题工作目录编排:workingDirectory 切换 / Git worktree + 降级
│ └── request-policy.ts # 会话请求策略:配置的 provider/model 与推理强度(含 /effort 覆盖)
├── feishu/
│ ├── delivery.ts # 出站投递:首帧话题卡、结果卡、分段降级、表情生命周期
│ ├── native-attachments.ts # 原生附件存储:saveImage/saveFileStream、结构化失败、文件句柄文本
│ ├── inbound-attachments.ts # 入站附件:不下载分类、富文本文件恢复、图片预取
│ ├── manifest.ts # <feishu_context> 清单:只列未加载附件、去重、转义、行上限
│ ├── message-read.ts # 消息读取:get/mget/list、引用摘录、首轮历史、统一渲染
│ ├── history-events.ts # 把消息历史还原成长连接事件(断连补偿用)
│ ├── slash-commands.ts # 连接后把命令注册表幂等同步到飞书原生斜杠指令面板
│ ├── tools.ts # feishu_attachment_fetch / feishu_topic_messages 与每 route 白名单
│ ├── send-file.ts # feishu_send_file:把话题工作目录内的图片/文件发回当前会话
│ ├── wiki/ # 知识库工具:links(wikiRoots 链接解析)、api(wiki/docx/docs_ai 接口)、sheet-api / base-api(电子表格、多维表格接口)、scope(范围判定)、consent(授权卡)、shared(失败码与公共步骤)、tools / sheet-tools / base-tools、a1(A1 范围)
│ └── agent-context.ts # agent 作用域 system prompt:附件指引与回复写法
├── routing/
│ ├── inbound.ts # 入站路由:去重、准入、路由决策树、建/续会话、首触引导
│ └── missed.ts # 断连补偿:最后在线时间、读回漏掉的消息、待处理卡
├── commands.ts # 斜杠命令解析、agent 作用域注册与结果卡
├── commands/registry.ts # 命令注册表:词表 / 准入规则 / 分组 help 的单一事实来源
├── session-commands.ts # /status /workspace /renew /history /compact 渲染
├── stats.ts # 宿主投影优先的 token / 缓存 / 上下文折叠
├── domain.ts # storageDomain 域声明与路由记录 schema
├── outbox.ts # 收尾持久重试:卡片终态、表情切换、续卡
├── health.ts # 健康快照与卡片
├── session-export.ts # 逻辑日志导出
├── feishu-channel.ts # 官方 SDK Channel 适配(可注入 stub)
├── client/ # 浏览器半区:左侧栏「飞书渠道」主区页面(构建到 lib/client.js)
│ ├── index.ts # client 插件入口:sidebar.panellist + main 注册、飞书来源(右侧栏标签 + 会话头部入口)、会话导航 + 样式
│ ├── origin.tsx # 对话里的飞书来源:会话头部按钮与右侧栏标签页
│ ├── origin-cache.ts # 按会话缓存的来源查询
│ ├── navigation.ts # 保存壳的 uiWorkspace 服务,供「会话记录」打开会话
│ ├── panel/ # 页面外壳 + 八个标签页
│ │ ├── index.tsx # 加载 / 5s 状态轮询 / 全局草稿 / catalog 级联 / 顶部状态头 / Tab
│ │ ├── shared.tsx # 行 / 开关 / 分组 / 保存栏等渲染原语、阶段徽标、草稿校验
│ │ ├── connection-tab.tsx # 连接:三步引导 / 状态块 / 自检 / 待生效提醒 / 凭据
│ │ ├── credentials-card.tsx # 凭据行:更换 / 移除(不回显)
│ │ ├── fields-tab.tsx # 会话 / 模型 / 附件与工具:纯字段分组页
│ │ ├── records-tab.tsx # 会话记录:搜索、群 → 用户 → 会话分组、点击打开会话
│ │ ├── workspace-browser.tsx # 会话记录的工作区文件浏览:目录列表与带行号的预览
│ │ ├── records-format.ts # 会话记录的相对时间、大小、搜索过滤与文件类型
│ │ ├── records-icons.tsx # 会话记录与文件浏览用的线性图标
│ │ ├── access-tab.tsx / wiki-tab.tsx / advanced-tab.tsx
│ │ └── whitelist.tsx # 用户表(权限开关)与待处理申请列表
│ ├── number-field.ts # 数值字段显示单位换算、上下界校验、字节读数
│ ├── catalog.ts # 选择器目录折叠:Host 字段 + 实时目录 → 表单字段
│ ├── api.ts # 同源 /dsh-channels/* 请求封装
│ └── styles.ts # 语义 token 样式(注入一次)
├── settings/ # Web 设置面板 Host 半区
│ ├── contract.ts # 双端共享 wire 类型 / 字段清单 / 路由常量
│ ├── overlay.ts # settings.json 读写、校验、合并
│ ├── catalog.ts # 宿主选择目录:provider/model/推理档位/preset
│ ├── directory-picker.ts # 系统目录选择框适配(ctx.directoryPicker native 后端)
│ ├── surface.ts # 状态 / 面板视图模型 + 保存 / 清除 / 重连 / 选目录
│ └── routes.ts # HTTP 路由 + DSH 登录鉴权 + 同源写保护
├── workspace-files.ts # 「会话记录」的只读工作区文件访问:路径约束、目录列表、预览、下载
└── workspaces.ts # 用户工作区 / 话题目录路径解析
lib/index.js 与 lib/client.js 是构建产物(pnpm build):前者是安装后 profile 直接加载的 Host 入口(普通 ESM,不依赖 TypeScript 运行时转换),后者是设置面板的浏览器 bundle。scripts/verify-host-loader.mjs / scripts/verify-client-loader.mjs 分别做冒烟。
接入 DSH
插件安装进 dsh 的 profile($DSH_HOME/profiles/<name>/,未设置 DSH_HOME 时默认 ~/.dsh),web 界面即 web profile。已在真实 dsh 上实测通过的完整流程见 docs/feishu-onboarding.md(含飞书控制台配置、逐通道联调清单与故障排查),这里给出最小步骤:
安装已发布的 npm 版本(推荐):
# 在 deepseek-harness 仓库内:把 npm 包安装进 web profile
DSH_HOME=$DSH_HOME pnpm dsh plugin --profile web add @modesto_ma/dsh-channels
本地 checkout 开发时:
pnpm build
DSH_HOME=$DSH_HOME pnpm dsh plugin --profile web add link:/path/to/dsh_channels
npm 发行包已包含构建产物:Host 入口是 lib/index.js(profile 直接以 ESM 加载,不经过 TypeScript strip;package.json 的 main 指向它),设置面板是 lib/client.js。本地 link: 安装前仍必须先执行 pnpm build。安装或修改后重启 dsh(桌面端重启应用,dsh web 重启服务并刷新页面)。DSH 只加载已构建产物,直接改 src/ 不会生效;没有 web 载体的 profile 不注册面板,飞书通道照常运行。
包内的 cordis.patch.yml 会自动把 @modesto_ma/dsh-channels 加入 profile 的 bundle 列表,并提供稳定 id: dsh-channels 的 Loader 行;profile 可在 $DSH_HOME/profiles/web/cordis.patch.yml 中按该 id 提供实例配置(不提供时使用默认凭据引用名):
appId / appSecret 也各自有默认引用名 FEISHU_APP_ID / FEISHU_APP_SECRET:凭据库使用这两个名字时,bundle 行即使暂时没有 config 也能完成 Loader 校验并启动 Host 半侧,从而先打开设置面板补全凭据;使用自定义引用名时再按需覆盖。
- id: dsh-channels
config:
appId: 'FEISHU_APP_ID' # 凭据引用名(非密钥本身),经 ctx.credentials 解析
appSecret: 'FEISHU_APP_SECRET'
baseDir: './channels-data' # 工作区根目录(按 私聊用户 / 群×用户 组织;可在 Web 面板覆盖,面板要求绝对路径)
provider: 'deepseek-official' # 可选:省略则用部署默认
model: 'deepseek-v4-flash' # 可选
reasoningEffort: 'high' # 可选:模型声明的精确档位 id;省略则用适配器/provider 默认
agentPreset: 'standard' # 可选:省略则挂 roster 默认 preset(web profile 下工具只在 preset 里)
refreshMs: 500 # 卡片刷新频率
statusRefreshMs: 5000 # 活动状态行保活间隔(工具/子代理/工作流等待期的计时刷新)
requireMention: true # 普通群需 @ 机器人
dmMode: 'open'
threadFallback: 'degrade' # 话题被拒时降级为会话级路由
topicIsolation: 'auto' # 话题目录隔离:auto(Git 仓库用 worktree,否则子目录)/ subdir / worktree
routeTtlMs: 2592000000 # 话题路由 30 天无活动即过期;后续消息明确拒绝,不新建 Session(0 = 关闭)
approvalTimeoutMs: 300000 # HITL 审批决策卡等待时长(默认 5 分钟;超时自动拒绝)
questionTimeoutMs: 300000 # ask_user_question 问答卡等待时长(默认 5 分钟;超时本轮提问失败)
acceptAttachments: true # 接收飞书图片/文件/合并转发记录(图片预取、文件由 feishu_attachment_fetch 懒取、纯附件静默不回复;false 时附件消息完全惰性且不挂 fetch 工具)
prefetchImages: true # 触发消息图片预取为首轮 ImageBlock(native 生效;关闭后图片也需工具懒取)
prefetchImageMaxBytes: 20971520 # 单张预取图片上限(20 MiB)
prefetchTotalMaxBytes: 52428800 # 单次触发消息预取总字节预算(50 MiB)
prefetchTimeoutMs: 10000 # 图片预取整段超时(10 s;到期保留已取回图片,其余降级清单)
historyBootstrapLimit: 10 # 新 Session 首轮附带本话题最近 N 条消息摘要(0 关闭,上限 20)
promptContextTimeoutMs: 3000 # 被动引用摘录 / 首轮历史的整段读取预算(3 s;超时静默省略)
feishuTools: # 模型工具(Web 面板按子键合并,不会把未改子键写空)
attachmentFetchMaxResources: 10 # 单次最多取回资源数(1–50)
attachmentFetchMaxBytes: 104857600 # 单次总字节预算(100 MiB;文件流式计量)
attachmentFetchTimeoutMs: 120000 # 单次工具调用整段超时(120 s)
topicMessages: true # feishu_topic_messages:读取引用消息 / 话题历史并授权其附件
topicMessagesTimeoutMs: 20000 # 单次话题读取整段超时(20 s;被动摘录用独立 3 s 预算)
sendFile: true # feishu_send_file:把话题工作目录内生成的图片/文件发给用户(需 im:resource)
wikiRead: false # feishu_wiki_list / feishu_wiki_read / feishu_wiki_sheet_read / feishu_wiki_base_read:浏览 wikiRoots 内的知识库,读取文档、电子表格、多维表格
wikiWrite: false # feishu_wiki_create / feishu_wiki_edit / feishu_wiki_sheet_edit / feishu_wiki_base_edit:新建文档/表格,修改本应用创建的文档和表格,向多维表格写记录
wikiRoots: # 知识库范围(空 = 不挂载知识库工具;最多 20 条)
- 'https://mi.feishu.cn/wiki/space/7xxxxxxxxxxxxxxxxx' # 整个知识空间
- 'https://mi.feishu.cn/wiki/AbCdEfGhIjKlMn' # 某个节点及其下所有子节点
welcomeOnboarding: true # 私聊首次触达发一张引导卡(按 chat 持久化去重)
hostTurnCards: true # 子代理 / 后台任务 / 定时跟进 / dsh 客户端输入唤醒会话时,在话题里新发一张流式卡
replyGuidance: true # 在飞书会话的系统提示词里加回复写法规范(先给结果、保持简短),每次模型请求时读取
replay: # 重启恢复:pending 重放还没轮到处理的消息;interrupted: ask(带继续按钮)| notify
pending: true
interrupted: 'ask'
missed: # 断连补偿:重启或断线重连后读回期间漏掉的消息
enabled: true
windowMs: 1800000 # 30 分钟内的消息直接处理;更早的发一张待处理卡(全部处理 / 忽略)
heartbeatMs: 60000 # 多久记录一次「最后在线时间」,崩溃重启时从这里往回读
maxMessages: 200 # 每次补偿最多读多少条
slashCommandPanel: false # 连接时把命令同步到飞书原生 / 面板(需 app_slash_command 读写权限;改后需重连)
allowPermissionSwitch: false # 是否允许 /permissions <name> 从飞书侧切换权限预设(安全敏感)
accessMode: 'guarded' # 准入模式:guarded(默认,仅管理员 + 用户表里对话准入打开的人)| open(任何人可对话,不安全;知识库与代码改动开关仍生效)
codeWriteTools: # 需要「代码改动」开关的工具名(默认如下;飞书会话之外的调用不受影响)
- repo_workspace_push
- repo_mr_create
- repo_mr_update
- repo_mr_comment
- repo_mr_merge
- repo_issue_create
- repo_issue_update
- repo_issue_comment
- logalyze_improve_skill
- logalyze_submit_init
authRequestTtlDays: 7 # 入群/用户授权申请卡有效期(天,1–30)
loggerLevel: 'info'
飞书里不提供切换模型的命令:模型只在设置面板「模型」标签页(即上面的 provider / model)配置,对插件创建的所有会话统一生效;留空时使用 DSH 部署默认模型。reasoningEffort 是每个 Session 的初始/重置默认值。DSH 0.2.2 起模型选择归 Agent 所有(agent.model),插件在新建、恢复、借用 Agent 以及 /effort 成功后用 agent.model.select() 记录生效的 provider/model/推理强度;DSH 0.2.1 没有 agent.model,插件改为在该 Agent 作用域内以 prepend 外层覆盖 agent/request,因此胜过已存在的模型选择与 provider 默认。两种宿主自动识别,无需配置;借用外部 Agent 时,插件关闭只移除这条频道策略,不销毁外部 Agent。话题所有者可用 /effort <档位-id> 覆盖当前 Session,/effort 查看生效来源,/effort reset 恢复该配置值(未配置时恢复模型适配器/provider 默认)。成功选择由 Session 自带的 command/run / command/done 日志恢复,重启后仍生效;新建 Session 不继承旧 Session 的覆盖。
推理档位是适配器拥有的不透明 id(常见如 off、low、high、max),命令不会猜测或预校验 provider 的能力;不受支持的值会在下一次模型请求前被 Harness 拒绝,并按失败终态显示错误。
从旧版手写 Loader 行迁移时,删除原来的 insert / id: channels 条目;不要让旧行与 bundle 提供的 id: dsh-channels 同时存在,否则会启动两个插件实例。
凭据放在 $DSH_HOME/.credentials.yaml(dsh 受管凭据库,热加载,与 DEEPSEEK_API_KEY 同库):
FEISHU_APP_ID: cli_xxxxxxxx
FEISHU_APP_SECRET: xxxxxxxx
也可以不手改文件:安装并在 Web 左侧栏打开 飞书渠道,在「飞书应用凭据」卡片填入 App ID / App Secret 保存即可(值同样写进该凭据库)。
文件必须仅属主可读(dsh 强制校验,否则拒绝启动):chmod 600 $DSH_HOME/.credentials.yaml。解析优先级:进程环境变量 > .credentials.yaml > <cwd>/.env > $DSH_HOME/.env(由 dsh-credentials-local 提供,热加载)。配置里只写引用名,明文密钥会被 schema 拒绝。改动凭据后:Web 面板保存会即时写入,未连接时自动建立连接、已连接时提示重连;手改 .credentials.yaml 由凭据层热加载,但正在运行的连接不会自动使用新值,需在面板点「重启飞书连接」。
验证组合(不启动服务):DSH_HOME=$DSH_HOME pnpm dsh web --dump-config | grep dsh-channels
启动成功标志(控制台):[dsh-channels] feishu long connection established, bot open_id: ou_xxx;凭据不完整时不会连接,日志会提示缺少的凭据引用(如 飞书应用凭据未配置完整(缺少 FEISHU_APP_SECRET…)),打开 Web 左侧栏 飞书渠道 → 连接 补齐即可。
宿主 profile 已内置所需服务(agent-loop、llm-deepseek、session 持久化、storage-domain、workspace、working-directory);web profile 默认组合还包含 DSH 原生附件库 dsh-attachment-local 与 dsh-tools(ctx.attachments / ctx.tools)。原生附件库是插件的必需依赖(inject 含 attachments):组合里缺少它时插件不会启动。推荐确认 dsh-token-meter 已启用(卡片上下文占用显示)。Git worktree 隔离模式额外依赖可选的 @deepseek-ai/dsh-experimental-worktree 实验插件,缺失时自动回退为普通话题子目录。
工具与 agent preset(web profile):web profile 把 bash/fs/todo 等工具行全部标为 disabled: true,工具只存在于 agent preset 里,且 agents.create() 不会自动应用 preset——插件因此在创建/恢复会话时通过 setup 回调挂载 preset(默认 standard,可用 agentPreset 指定更精简的 preset,如只含 bash/fs 的自定义项),并把 preset id 记录进会话头。附件管线的两个模型工具在 preset setup 之后挂载:feishu_attachment_fetch 在 acceptAttachments 打开时挂载,feishu_topic_messages 总是挂载(均为 inject(['tools']));宿主缺 tools 服务时工具 no-op + 一次性告警,不阻塞会话(详见 docs/attachment-pipeline-v2.md)。若机器人回复把 <tool_calls>… 当纯文本吐出,即该会话没挂上 preset;修好后再看会话的 request/header 应带 tools 字段、日志出现 tool/call 事件。无 preset roster 的部署(如 headless)不做此步骤,工具本就位于宿主层。
飞书侧准备
-
飞书开放平台 创建企业自建应用,开启机器人能力。
-
权限(至少):
im:message(获取与发送单聊、群组消息;附件资源下载 messageResource.get 也走消息资源接口,具体 scope 以控制台该接口要求为准)
im:message.p2p_msg:readonly(读取用户发给机器人的单聊消息)
contact:user.employee_id:readonly(获取用户 user ID,必需):插件用 user_id 识别用户(准入、话题归属、工作区目录)。未开通时事件不带 user_id,所有用户消息都会被丢弃并打印 warn;外部联系人/跨租户用户没有本租户 user_id,同样无法使用
im:message:readonly(读取消息内容:引用摘录、话题历史、feishu_topic_messages)
im:message.group_msg(读取群组消息,敏感权限,需企业管理员审批):读取群/话题历史(含机器人入群前的可见历史)。未开通时引用/历史块静默省略、普通对话不受影响,按“机器人可见范围”降级
- 事件:接收消息 im.message.receive_v1(含“接收群聊中 @ 机器人消息”)
- 事件:机器人进群 im.chat.member.bot.added_v1(入群后私聊管理员发群信息卡);对应的群信息读取权限(如
im:chat:readonly,以控制台要求为准)
- 私聊需要能主动给管理员发消息(
im:message 已覆盖;管理员需在应用可用范围内)
- 回调:卡片回传交互 card.action.trigger(流式卡停止按钮、
/effort 等选择卡、HITL 审批 / ask_user_question 问答按钮的回传事件,同样走长连接)
im:resource(获取与上传图片或文件资源,可选;im:resource:upload 亦可):feishu_send_file 上传图片/文件后发给用户。未开通时该工具返回可读的 PERMISSION_DENIED;关闭 feishuTools.sendFile 后自检不再检查此项
wiki:wiki、docx:document(可选,仅开启知识库工具时需要;只读可用 wiki:wiki:readonly / docx:document:readonly):浏览知识库节点,读取/新建/修改飞书文档。电子表格和多维表格走飞书新版接口,只认细分权限,旧版 sheets:spreadsheet / bitable:app 不算:电子表格读 sheets:spreadsheet:read、写 sheets:spreadsheet:write_only;多维表格读 base:table:read + base:field:read + base:record:read,写 base:record:create / base:record:update / base:record:delete。应用还必须是目标知识空间的成员,否则调用返回 NO_ACCESS
im:message.reactions:write_only(发送、删除消息表情回复)
im:message:update(更新卡片)
消息读取权限缺失时不会报死错:引用摘录与首轮话题历史静默省略(插件生命周期内首次降级输出一条 warn,之后 debug 级),显式调用 feishu_topic_messages 返回可读的 PERMISSION_DENIED。附件下载仍依赖 一类的消息资源权限;下载被拒时工具会返回可读原因。
:每次连上飞书后,插件调用 读取应用的租户权限授予状态,与上面的清单对比(私聊关闭时不检查 , 时 升为必需; 关闭时不检查 ;知识库工具未挂载时不检查知识库、文档、电子表格和多维表格权限)。缺失项在 Web 设置「连接」页以红字列出(标注必需/可选与用途,每项附开发者后台权限页深链,另有一键打开全部缺失项的链接),并在日志打印一条 warn;读取失败时显示飞书返回的原因。自检只做提示,不阻断连接;开通权限后需发布新版本并重新连接才会刷新。
工作区与会话隔离
工作区(DSH workspace registry 中的「项目」)按人建立,只在首次需要时创建;每个话题在工作区内拥有独立的实际工作目录:
| 场景 | 工作区目录(header cwd) | 每个话题的工作目录(实际 cwd) |
|---|
| 私聊 | <baseDir>/p2p/<user_id>/ | <工作区>/topics/<话题 id>/ |
| 群聊 | <baseDir>/groups/<chat_id>/<user_id>/ | 同上 |
其他插件经 feishuTopics 发起的话题 | <baseDir>/<调用方给的路径段>/ | 同上 |
- 群聊必须是「群 × 用户」:同一个人在不同群之间不共享文件,避免 A 群会话把 B 群内容带出来。
- 会话创建时以工作区为 header cwd(保证 GUI 里归属该项目、沙箱边界稳定),随后通过
ctx.workingDirectory 把实际工作目录切到话题目录;冷恢复会从 Session 日志还原该目录。因此话题之间的文件互不干扰,而工作区数量只随用户增长。
topicIsolation 控制隔离形式:auto(默认)在用户工作区是 Git 仓库(含 .git)时给每个话题建一个 feishu-<话题> 分支 + worktree(适合多个话题并发改同一个代码仓库),否则用普通子目录;subdir 一律子目录;worktree 一律尝试 worktree,失败告警并回退子目录。
- 隔离强度:这是默认工作目录层面的隔离(bash / fs / 终端都从话题目录出发),不是防恶意的硬沙箱——沙箱写边界仍是用户工作区。若需要硬边界,可把话题目录直接作为 Session cwd,但会话会失去工作区归属(GUI 显示为未分组)。
- 入站附件:图片/文件只存入 DSH 原生附件库,不写入话题目录,因此工作区与话题目录只承载 Agent 产物。旧版本写下的
<话题目录>/attachments/ 与 <baseDir>/_staging/ 不会被迁移或删除,前者仍是普通工作区文件,后者已无人读取,可手动清理。/renew 复用同一话题目录。
- 插件不删除任何工作区或话题目录。需要回收磁盘时手动删除不再需要的话题目录即可:该话题下次对话时会重建一个空目录继续使用(worktree 模式下改建新的 worktree 分支)。手动删除 worktree checkout 后,可在仓库里
git worktree prune / git branch -D 回收 Git 记录。
- 旧版本「一个话题一个工作区」(
<baseDir>/topics/...)不会自动迁移:旧话题继续用原目录;旧版「用户 × 聊天」共享工作区正好作为新的用户工作区复用。
- 设计取舍与约束详见 docs/workspace-isolation-design.md。
群话题服务(feishuTopics)
插件向其他 DSH 插件提供 Cordis 服务 feishuTopics:调用方给出目标群、发起人、根卡片内容、工作区和 prompt,插件在群里发一张根卡片,在卡片下开话题,把 prompt 原样交给一个新 Session(prompt 开头的斜杠命令由 DSH 展开成对应 skill),过程在话题内流式输出,和用户自己发起的会话一样。首轮结束后根卡片的状态标签随之更新。群里通过准入的成员都可以在该话题内追问。
调用方式:类型在包子路径 @modesto_ma/dsh-channels/topics(只有类型,没有运行时导出),调用方用 import type 引用,在每次调用时用 ctx.get('feishuTopics') 取服务(本插件未加载或正在重载时取不到),并检查 version(当前为 1)。不要把 feishuTopics 写进调用方插件的主 inject:那样本插件每次重载(例如改配置后重连)都会连带重载调用方。需要随服务上下线挂载路由时,在子作用域里 ctx.inject(['feishuTopics'], …)。
const topics = ctx.get('feishuTopics') as FeishuTopicService | undefined
const result = await topics?.launch({
key, fingerprint, chatId, requesterUserId,
card: { title: '日志分析', taskName: '分析', lead: [{ text: '于 … 发起了对 ' }, { text: 'camera', bold: true }, …], links },
workspace: { segments: ['analyze', 'camera', 'E1001'], title: '日志分析 · camera · E1001' },
prompt,
})
key / fingerprint:幂等键(1–200 个可见 ASCII 字符)与调用方定义的请求指纹。同一个键再次发起(包括并发)返回第一次的根卡片(replayed: true),指纹不同则拒绝(key-conflict)。记录永久保存在 channels_routes_v2 域的 analyze_requests 表(沿用旧接口的表名);根卡片发出后的失败不会回滚记录,根卡片显示「失败」和原因,需要重试时换一个新键。
chatId:目标群,机器人需在群内。
requesterUserId:发起人,必须是管理员或对话准入打开的用户(/auth add 或面板「访问授权」);accessMode: open 时不校验。
card:标题、任务名(状态文字里用,如「分析中」「分析已完成」)、跟在 @发起人 后的引导句(可加粗片段)、可选的链接表格(最多 50 行,每页 10 行)。
workspace:<baseDir> 下的 1–8 个路径段(只允许 A-Z a-z 0-9 . _ -,第一段不能是本插件自用的 p2p、groups、topics、_staging)和它在 DSH 工作区列表里的标题。路径段相同的发起共用一个工作区,各有一个话题目录。
prompt:1–100000 字符,原样作为首条用户消息。
- 结果:成功时返回根卡片
messageId;失败时返回 code 和中文 message:invalid(参数不合法)、not-connected(飞书未连接或正在关闭)、forbidden-chat(发起人没有对话权限;沿用旧名)、key-conflict、send-failed(根卡片发送失败,如机器人不在群里)。
发起之后:调用方可以读到每次发起后来的状态,用来把结果回传给外部系统(回传本身由调用方负责)。
state(key):按幂等键返回 { key, status, reason?, sessionId?, workDir? }。status 与根卡片一致:running(首轮进行中)、done、stopped、error(首轮失败)、failed(话题或 Session 没能建立,reason 写明原因)、interrupted(首轮进行中 DSH 重启)。sessionId 是话题路由当前指向的 Session(/renew 后为新 Session),workDir 是该话题的实际工作目录(话题目录或 worktree);路由发布前、建立失败后、路由过期后两者都没有。键不存在或飞书未连接时返回 undefined。
launchKeyOf(sessionId):该 Session 是某次发起的话题当前的 Session 时返回那次发起的幂等键,否则返回 undefined。调用方据此把 DSH 的 session/event(如 turn/end)对应到自己的请求,话题里的追问也能对上。
- 事件
feishu-topics/status:每次记录根卡片状态后(首轮结束、建立失败、启动时标记为已中断)用 ctx.emit 发出,参数是新的 state。监听者抛错只记日志,不影响话题。
- 这两个方法和事件是在契约版本
1 内新增的;需要兼容旧版本本插件的调用方,调用前先检查方法是否存在。
群里看到的:先是根卡片「@发起人 <引导句>」,下面是链接表格,标题栏标签为「<任务名>中」;紧接着话题内一张流式卡片(它同时创建话题),首轮在这里流式输出,带停止按钮。首轮结束后根卡片的标签变成「已完成」「已停止」或「失败」,并加上 DONE / No 表情;之后的追问不再改动根卡片。启动失败时话题里的流式卡片和根卡片都会写明原因。过程中出现的审批卡、提问卡发在话题内。DSH 在首轮进行中重启或重连飞书时,话题里的流式卡片照常由中断修复处理;重新连上后,插件把仍是「<任务名>中」的根卡片改成「已中断」(橙色),提示在话题内回复即可继续,原来的 Session 会接着追问。根卡片状态同时记在发起记录里(连同重画卡片所需的内容)。
追问与权限:这类话题的路由是群内共享的(路由记录 shared: true):群里任何通过准入的成员(管理员,或对话准入打开的用户)都可以在话题内直接回复追问(无需 @),也可以点停止按钮、回答审批卡与提问卡、在话题里执行会话命令;未授权的人仍被静默忽略。追问的回复照常流式输出;代码改动工具按每条追问的发言人检查「代码改动」开关。话题之间互不读取,和普通话题一样。结果怎么回传给外部系统由调用方插件负责,本插件只提供上面的状态查询和事件。
行为与降级
-
入站附件(v2:零等待 + 清单 + 图片预取 + 文件懒取):收到触发消息时先建会话 / 话题 / 卡片第一帧并持久化路由,不在建立前下载任何附件字节。随后组装的 prompt 依次为:说明文字 → <feishu_quote> 引用摘录 → 预取成功的 ImageBlock → <feishu_context> 未加载附件清单 → (首轮)<feishu_history> 话题历史;空块不产生 token。触发消息中的图片在路由建立后、followup 前预取进首轮(单图 prefetchImageMaxBytes、总量 prefetchTotalMaxBytes、整段 prefetchTimeoutMs,失败/超限/超时降级为清单项);文件默认不下载,清单只给 message_id / file_key / 类型 / 文件名,模型需要时调用 feishu_attachment_fetch 懒取(图片回 ImageBlock,文件流式入库 + FileBlock + 只读路径兜底)。富文本(post)消息 files[] 里 SDK 归一化会丢的文件也会被恢复并进入清单/预取。只发附件、不带说明的消息完全静默:不回复、不建会话、不下载、不加表情,本地不写任何记录。插件不扫描历史去找这类消息:用户需要引用那条消息再提问(多个文件先合并转发再引用),被引用消息的附件进入清单与 fetch 白名单;话题内的附件也可由模型调用 feishu_topic_messages 读到并获得授权。
-
引用与话题历史:触发消息的 parent_id 解析为 <feishu_quote> 摘录(单条 ≤400 字符、整块 ≤4k,合并转发展开子消息并标注容器 id),被引用消息的附件同时进入清单与 fetch 白名单;新 Session 首轮自动带最近 historyBootstrapLimit 条(默认 10、上限 20、0 关闭)的 <feishu_history>(单条 ≤400 字符、整块 ≤8k,排除触发消息与卡片)。更早的消息用 feishu_topic_messages 按 limit(默认 10、上限 50)与 cursor 翻页;读到的消息与附件即获得 fetch 授权。被动读取使用独立的 promptContextTimeoutMs(默认 3 s)预算,缺权限/超时/失败时对应块静默省略,回合照常运行(首次降级一条 warn)。
-
附件存储与开关:入站图片/文件只存入 DSH 原生附件库(ctx.attachments,插件必需依赖)。acceptAttachments=true(默认)时预取图片、列清单并挂载 feishu_attachment_fetch;acceptAttachments=false 时附件消息完全不保存、不触发、不列入清单,混合消息只保留剥离占位符后的文字,feishu_attachment_fetch 不挂载,feishu_topic_messages 仍挂载(描述会标注附件不可取)。>100 MB 文件先探测大小再按 32 MiB Range 分块流式写入原生附件库。
-
附件与目录保留:DSH 原生附件库永不自动清理,插件也不提供清理入口(D6;磁盘增长用户已接受)。插件同样不删除工作区、话题目录与 Session 日志。旧版本写下的话题目录 attachments/ 不会被新会话枚举,但 Session 历史仍可按原路径读取;旧版 <baseDir>/_staging/ 不再被读取,可手动删除。
-
三类会话行为矩阵(v2):
| 场景 | 私聊 | 对话群(未 @) | 对话群(@) | 已绑定话题(无 @) |
|---|
| 纯附件消息 | 静默(不建会话/不回复/不下载;下一轮列入清单) | 静默(后续 @ 建话题时列入清单) | 静默(纯附件不触发) | 静默(下一轮续聊时列入清单) |
已知取舍
- v2 附件(图片预取的等待窗口):卡片、话题与会话先建立,但首轮
followup 会等图片预取(默认整段 ≤10 s,prefetchTimeoutMs 可调);这是“首轮图片直接可见”与首字延迟之间的取舍,慢网下可调小或关闭 prefetchImages。不做后台预取/预下载。
- v2 附件(文件懒取依赖模型调用):文件默认不在 prompt 里,模型必须主动调用
feishu_attachment_fetch;插件用清单“未加载”措辞、工具描述与 guidance 收敛漏调风险,但不做预下载兜底(决定 D4)。
- v2 附件(授权白名单进程内):每 route 的已知消息 LRU 上限 128,进程重启后白名单清空;本人在本话题(会话级路由为聊天主线)发的消息由 fetch 工具回读飞书校验后仍可授权;话题之间不互通;他人的引用/历史消息需要重新出现在引用或历史中才可取。附件密集翻页仍可能挤掉更早条目。
- v2 附件(清理与保留):DSH 原生附件库不可自动清理、插件不提供清理入口,磁盘随使用增长;插件也不删除工作区与话题目录,需要时由运维手动删除。旧版
_staging 一次性迁入已移除,遗留目录可手动删除。
- v2 附件(未触发的附件需要引用):只发附件、不带文字的消息不登记、也不在下一轮自动列出;“先发文件、再单独提问”而不引用时模型看不到该文件。私聊与普通群需要引用那条消息(多个文件可合并转发后引用);话题内模型还可以用
feishu_topic_messages 读到。首次私聊引导卡写明了这一用法。
- v2 附件(权限降级):缺
im:message:readonly / im:message.group_msg 时引用摘录与首轮历史静默省略(首次一条 warn),显式调用 feishu_topic_messages 返回可读 PERMISSION_DENIED;普通对话不受影响。入群前历史只保证“机器人可见范围”。
- v2 附件(升级连续性):archive 后端已移除,旧配置里的
attachmentStore 与 feishuTools.attachmentFetch 会被忽略。历史话题里已有的 attachments/ 不会被新会话枚举;Session 历史仍可按原路径读取,旧目录可手动删除。
- 准入(升级连续性):升级到用户表后,原用户白名单里的人变成只打开对话准入的用户,从此可以私聊、也可以在任何群里使用机器人(以前私聊要开
dmAllowWhitelist、群聊要群已授权)。原群白名单和知识库白名单不迁移:数据留在存储里但不再读取,原来知识库白名单里的人需要管理员逐个打开「知识库读取」或「知识库写入」。待处理的群授权申请不再显示,旧卡片点了也不生效。dmAllowWhitelist 配置项和 /auth allow|revoke、/auth user …、/auth wiki … 命令已删除,旧配置里的 dmAllowWhitelist 会被忽略。代码改动开关默认关闭,非管理员需要管理员打开后才能在飞书里推送代码或提交 MR。
- 代码改动开关的范围:只拦
codeWriteTools 里列出的工具,不拦本地文件编辑和 shell 命令;发言人记在内存里;重启后重放的消息带着随快照保存的发送者,升级前留下的快照没有发送者,重放的那一轮拒绝所有代码改动工具,用户重新发一条消息即可;重启后还没有新消息时按路由所有者判断。插件最多记 2000 条还没被认领的消息的发言人,超出时丢掉最早的;丢掉的那条消息所在的会话之后认领到发言人不明的消息时,那一轮拒绝所有代码改动工具,直到这个会话认领到一条记得发言人的消息。共享话题里谁都可以点代码类工具的审批卡,但工具本身只认发言人的开关。
/renew 的会话语义:同一话题内新建 Session 沿用话题目录、复用附件;但新 Session 首轮不注入 <feishu_history>(仅 startSession 路径注入首轮历史,当前语义保留,若产品期望“新会话即带话题历史”需另行确认)。
- degraded 群会话的读取容器:话题创建失败降级为 chat 路由后,其容器是整群主时间线——同群他人主时间线消息可被读取/登记(topic 消息已明确拒绝);是否按 sender 进一步收紧待产品确认。
- 合并转发读取有界:单条读取子消息超过 50 条时截断(summary 标注数量)、无翻页;工具输出渲染上限 50 行 / 12k 字符。
- patch 为全量替换:流式期间用户手动展开折叠面板会被下一次刷新收起;流式结束后不再刷新,可自由展开。
- 超长正文拆成多张卡片(分段投放):推理内容不跨卡片,始终留在首卡;超出 5 KB 的部分被截断(只保留开头),不追加投放。
开发
pnpm install --store-dir ./.pnpm-store # 受限环境;正常环境直接 pnpm install
pnpm typecheck # Host(tsconfig.json)+ Client(tsconfig.client.json)两套
pnpm test
pnpm build # → lib/index.js(Host)+ lib/client.js(浏览器半区)
pnpm verify:host # Host bundle 以纯 ESM 导入加载
pnpm verify:client # client loader 冒烟
pnpm check # 以上全部
测试以 stub 替换官方 SDK 与 DSH 宿主服务,无需真实凭据。附件管线相关测试:tests/native-attachments.test.ts(存储层与结构化失败)、tests/feishu-tools.test.ts(两个工具的白名单 / 上限 / 取消)、tests/manifest.test.ts(清单格式与转义)、tests/inbound-image-prefetch.test.ts(预取降级)、tests/message-read.test.ts(引用 / 历史读取);接入链路由 tests/service.test.ts / tests/inbound-attachments.test.ts 覆盖。tests/settings.test.ts 覆盖:覆盖层 schema/层级、面板视图模型、连接阶段(待配置/未连接/已连接/失败)、凭据保存按就绪度连接、HTTP 路由(含 DSH 登录鉴权与同源写保护)、apply() 的 webServer + connection 接线、模型选择器的宿主目录与降级行为;tests/client-catalog.test.ts 覆盖浏览器半区的目录折叠(含依赖项解禁回归);以及 FeishuService 的覆盖加载 / 热更新 / 重启 / 连接失败记录 / 监听器释放。