@modesto_ma/dsh-channels 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -39
- package/docs/access-control-design.md +1 -1
- package/docs/attachment-pipeline-v2.md +318 -0
- package/docs/feasibility-and-design.md +54 -26
- package/docs/feishu-onboarding.md +123 -15
- package/docs/persistence.md +4 -4
- package/docs/refactor-plan.md +7 -6
- package/docs/workspace-isolation-design.md +81 -0
- package/lib/client.js +30 -15
- package/lib/client.js.map +1 -1
- package/lib/index.js +11008 -5132
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/cards/index.ts +1 -0
- package/src/cards/onboarding.ts +23 -0
- package/src/cards/primitives.ts +20 -0
- package/src/cards/streaming.ts +310 -291
- package/src/cleanup.ts +267 -119
- package/src/client/panel/cleanup-card.tsx +4 -2
- package/src/client/panel/workspace-tab.tsx +1 -1
- package/src/config.ts +221 -13
- package/src/domain.ts +141 -3
- package/src/feishu/agent-context.ts +66 -16
- package/src/feishu/inbound-attachments.ts +440 -3
- package/src/feishu/manifest.ts +270 -0
- package/src/feishu/message-read.ts +1333 -0
- package/src/feishu/native-attachments.ts +998 -0
- package/src/feishu/pending-attachments.ts +562 -0
- package/src/feishu/tools.ts +2345 -0
- package/src/feishu-channel.ts +55 -0
- package/src/host/agent.ts +55 -0
- package/src/host/attachments.ts +210 -0
- package/src/host/index.ts +1 -0
- package/src/index.ts +8 -6
- package/src/routing/inbound.ts +730 -81
- package/src/service.ts +536 -50
- package/src/session/isolation.ts +277 -0
- package/src/session/manager.ts +120 -23
- package/src/session/repair.ts +2 -3
- package/src/session/routes.ts +2 -0
- package/src/session-commands.ts +41 -14
- package/src/settings/contract.ts +43 -21
- package/src/settings/overlay.ts +124 -13
- package/src/settings/surface.ts +147 -14
- package/src/streamer.ts +70 -30
- package/src/workspaces.ts +74 -31
package/README.md
CHANGED
|
@@ -5,15 +5,15 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
5
5
|
- 对话入口:私聊、群聊 @ 机器人、话题群 @ 机器人
|
|
6
6
|
- 收到触发消息:立刻加 **“Typing”** 表情 → 以话题形式回复(**创建话题**)→ 绑定 DSH session → 卡片 2.0 **流式回复** → 成功时更新为 **“DONE”**,失败时更新为 **“No”**
|
|
7
7
|
- 话题内后续消息**无需再 @**,自动延续该 session
|
|
8
|
-
-
|
|
9
|
-
-
|
|
8
|
+
- 工作区归属:**每用户一个工作区**(私聊 = 用户;群聊 = 群 × 用户,同群不同用户互不影响);每个话题在工作区内拥有独立工作目录(默认 `topics/<话题 id>/`,Git 仓库工作区可自动改用 worktree),因此话题之间的文件互不干扰,而工作区数量只随用户增长
|
|
9
|
+
- **入站附件(v2,零等待)**:收到消息时**会话、话题、流式卡片立即建立,不等附件下载**。触发消息中的图片自动预取为首轮原生 `ImageBlock`,模型直接可见;文件默认不下载,prompt 用 `<feishu_context>` 清单声明“附件未加载”,模型需要时调用 `feishu_attachment_fetch` 按 `message_id` 懒取;引用消息与话题历史以 `<feishu_quote>` / `<feishu_history>` 摘录,更早的消息用 `feishu_topic_messages` 翻页(发现的资源同时获得 fetch 授权)。**只发附件、不带说明的消息完全静默**:不回复、不建会话、不下载、不加表情,只登记元数据,后续触发消息可列出并懒取。所有入站图片/文件统一存入 **DSH 原生附件库**(不可变、内容寻址去重、读取校验、随 Session 回放;插件不承担入站附件清理)。显式 `attachmentStore: 'archive'`、宿主未挂载原生附件服务或关闭 fetch 工具时退回旧版工作区 `attachments/` 落盘。完整设计见 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md)。
|
|
10
10
|
- **HITL 审批**:当模型请求需要授权的动作(如 bash 沙箱升级 `sandbox_permissions`)时,插件监听主机 `approval/request` 事件,在同一话题里**新发一张独立审批决策卡**(✅ 允许 / ❌ 拒绝 按钮),用户点击后回填主机审批结果;超时(`approvalTimeoutMs`,默认 5 分钟)自动拒绝(fail-closed),非会话发起人点击无效
|
|
11
11
|
- **结构化提问**:模型调用 `ask_user_question` 时,插件监听主机 `user-questions/request` 瀑布并发一张问答卡(每个问题一组选项按钮,多选先勾选再「提交本题选择」),答案回填给模型;主机自带的应答者只有 Web 客户端,没有本插件时纯飞书会话会以 `NO_PROVIDER` 失败。超时(`questionTimeoutMs`,默认 5 分钟)或回合取消时本轮提问失败(fail-closed);没有可选项的问题直接提示用文字作答,**阻塞期间普通文本回复即作为该问题的自由文本答案**(同时支持多选后的自定义文本)
|
|
12
12
|
- **准入与授权(默认开启)**:**管理员不受白名单限制**(私聊、任意群聊——包括尚未授权的群——都能直接发起会话);其他人私聊只收到一条固定提示(`/userid` 例外),群聊需要「群已授权 + 用户在全局白名单」两层同时命中。机器人被拉进群后私聊管理员发**群信息卡**(未授权群附授权按钮,用于放行白名单用户);已授权群里未授权用户 @ 会触发用户授权卡。授权入口:飞书 `/auth` 命令、授权卡、Web 设置面板;管理员 open_id 在面板配置(私聊 `/userid` 获取)
|
|
13
13
|
- 全部基于飞书官方 SDK [`@larksuiteoapi/node-sdk`](https://www.npmjs.com/package/@larksuiteoapi/node-sdk)(WebSocket 长连接,免公网)
|
|
14
14
|
- **Web 设置面板**:Web/桌面端 **设置 → 飞书渠道**,顶部状态头 + 6 个标签页(连接 / 会话与行为 / 模型 / 工作区 / 访问授权 / 高级);凭据不完整时插件不连接、面板显示三步引导,填写保存后自动连接(保存写入本机覆盖文件,部分字段需重连生效);详见下文「Web 设置面板」章节
|
|
15
15
|
|
|
16
|
-
设计与可行性论证见 [docs/feasibility-and-design.md](docs/feasibility-and-design.md)。
|
|
16
|
+
设计与可行性论证见 [docs/feasibility-and-design.md](docs/feasibility-and-design.md);工作区粒度与会话隔离的取舍见 [docs/workspace-isolation-design.md](docs/workspace-isolation-design.md);入站附件管线(存储 / 清单 / 工具 / 引用与历史 / 降级 / 隐私边界)见 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md)。
|
|
17
17
|
|
|
18
18
|
## 斜杠命令
|
|
19
19
|
|
|
@@ -37,17 +37,19 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
37
37
|
|
|
38
38
|
## 流式卡片
|
|
39
39
|
|
|
40
|
-
卡片 2.0
|
|
40
|
+
卡片 2.0,按**过程 → 正文 → 页脚**三层组织,默认 500ms 刷新(`refreshMs` 可调):
|
|
41
41
|
|
|
42
|
-
1.
|
|
43
|
-
|
|
44
|
-
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
-
|
|
42
|
+
1. **过程**(推理折叠面板 + 活动状态两行):
|
|
43
|
+
- **推理折叠面板**(`collapsible_panel`):带边框、默认折叠,右侧箭头随折叠状态翻转(折叠向下、展开向上,`icon_expanded_angle: 180`)。有推理内容时,面板标题沿成功链路 **推理中 → 回复中 → 回复结束** 切换,失败终态为 **回复失败**;**推理中阶段始终渲染**(兼作阶段指示灯与占位),**回复中 / 回复结束 / 回复失败阶段仅当存在推理内容时渲染**——无推理的完成/失败回合不会留下空面板
|
|
44
|
+
- **活动状态两行**(小字,**只在当前正在流式输出的那张卡上渲染**,已冻结的续卡/主卡不再重复;**互相独立、互不顶替**),让卡片在“模型不吐字”的阶段(工具执行、subagent、workflow)**不再静默**——按 `statusRefreshMs`(默认 5s)慢节奏刷新计时:
|
|
45
|
+
- **状态行**(委托进度):`tool-workflow/run-start / agent-start / agent-end / run-end`(工作流进度:完成数/总数、阶段、活动成员)→ run 结束转为 `🧵 工作流 xxx 结束(完成)` 并**保留到最终卡片**(后续工具调用、正文输出都不会顶掉它);**子代理进度**(跨会话映射):子代理是独立 session(`header.parentSession` 指向主会话),其 `subagent/descriptor`(label)、`tool/call`(当前工具)被折叠为 `🤖 子代理执行中:检索实现方案 (read) · 已运行 45s`,并行子代理显示计数,完成后在最终卡片汇总为 `🤖 子代理完成:a、b`。**只显示状态,不显示输出**——子代理正文是父模型的中间产物,绝不进卡片正文
|
|
46
|
+
- **工具行**(父会话当前工具调用):`tool/call` → `🔧 调用工具:bash · 已运行 Ns`(subagent 工具显示其 description、workflow 工具显示 meta.name),`tool/result` 返回即清除,**回合结束后收起**(完成态只保留结算状态行:工作流结论 / 子代理完成摘要)
|
|
47
|
+
- 两者可同屏:如 `🧵 工作流 xxx 结束(完成)` + `🔧 调用工具:bash`
|
|
48
|
+
2. **正文**:富文本 markdown;超长正文**自动分段投放**——单卡片预算用满后按干净断点(段落 / 标题 / 代码块边界)切分,且**每张卡最多 5 个表格**(飞书硬限制,错误 11310);断点会避开代码围栏与表格,**单个表格/代码块小于单卡预算时不会被从中间切断**(单块本身超过单卡预算时只能硬切,内容仍不丢失)。续片以「📄 回复分段 n」卡片跟进同一话题,**全文无丢失**;续卡被飞书拒收时自动降级为纯文本消息(内容仍送达)。**续卡只留正文**:`📄 回复分段 n` → 可选活动行(仅当它是当前活动卡)→ 正文,无面板、无页脚、无分割线
|
|
49
|
+
3. **结算页脚**(分割线 + 两行小字):**只在主卡、且只在回合结束(`done` / `error`)后渲染一次**——首帧、流式中、续卡都没有,因此不会出现 0/— 的空统计。
|
|
50
|
+
- 第一行:`🧠 模型 {model}({effort}) · 🔑 会话 {sessionId} · ⏱ 推理 {推理时长}`。推理时长为**本轮所有推理阶段的累计墙钟时间**(每个 step 从首个推理增量开始,到该 step 输出结束——首个正文增量或 step 结束——为止;工具执行时间不计入;回合在推理中结束时计到 turn 结束),无推理时为 `—`
|
|
51
|
+
- 第二行:`📊 上下文 {tokens}/{window} ({占比}) · 📩 输入 {n} · 📨 输出 {n} · ⚡ 缓存命中 {pct}`。占比 = 上下文 tokens / 窗口(<10% 保留一位小数,≥10% 取整);没有窗口时只显示 tokens
|
|
52
|
+
- 输入/输出为 session 累计口径;模型、推理强度、缓存命中按**当前轮次**,本轮在请求头产生前失败时模型/缓存命中显示 `—`、推理强度省略,不会沿用上一轮
|
|
51
53
|
|
|
52
54
|
## Web 设置面板(设置 → 飞书渠道)
|
|
53
55
|
|
|
@@ -64,12 +66,19 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
64
66
|
| 标签页 | 内容 |
|
|
65
67
|
| --- | --- |
|
|
66
68
|
| 连接 | 连接状态与连接信息(长连接状态、Bot open_id、运行时长、话题路由 / 活动会话 / 在途回复)、未配置时的三步引导、飞书应用凭据、最近一次连接失败原因、需重连的待生效提醒 |
|
|
67
|
-
| 会话与行为 |
|
|
69
|
+
| 会话与行为 | 「行为」:私聊模式、普通群 @ 要求、话题兜底、首次私聊引导、权限切换;「内容与附件」:附件接收、存储后端、图片预取与上限、首轮历史、静默登记保留期/容量;「模型工具」:`feishu_attachment_fetch` / `feishu_topic_messages` 开关与上限、被动读取预算 |
|
|
68
70
|
| 模型 | 模型服务商、模型 id、推理档位、Agent 预设(宿主目录驱动的级联下拉) |
|
|
69
|
-
| 工作区 |
|
|
71
|
+
| 工作区 | 工作区根目录(可点「选择目录…」调用系统目录选择框)、过期目录保留期、自动清理、「目录清理」预览与手动执行;话题目录隔离方式(`topicIsolation`)在 profile 配置中设置 |
|
|
70
72
|
| 访问授权 | 管理员 open_id、准入模式、申请有效期;已授权群 / 用户白名单与待处理申请 |
|
|
71
73
|
| 高级 | 流式刷新 / 状态行 / SDK 日志级别、部署级只读参数、Web 覆盖文件路径与「清除 Web 覆盖」 |
|
|
72
74
|
|
|
75
|
+
**入站附件与模型工具(v2)**:「会话与行为」标签页新增两组可编辑字段,与 profile 配置同名、写入同一 Web 覆盖文件(`$DSH_HOME/dsh-channels/settings.json`):
|
|
76
|
+
|
|
77
|
+
- **内容与附件**:附件接收(`acceptAttachments`)、入站附件存储后端(`attachmentStore`:DSH 原生附件库 / 工作区 `attachments/`)、触发消息图片预取(`prefetchImages`)及单图 / 总量 / 整段超时上限(`prefetchImageMaxBytes` / `prefetchTotalMaxBytes` / `prefetchTimeoutMs`)、首轮话题历史条数(`historyBootstrapLimit`)、静默附件登记有效期与每 scope 容量(`pendingAttachmentTtlMs` / `pendingAttachmentLimit`)。
|
|
78
|
+
- **模型工具**:`feishu_attachment_fetch` 开关与单次资源数 / 字节预算 / 超时(`feishuTools.attachmentFetch*`)、`feishu_topic_messages` 开关与超时(`feishuTools.topicMessages*`)、被动引用/历史读取预算(`promptContextTimeoutMs`)。
|
|
79
|
+
|
|
80
|
+
`feishuTools.*` 是嵌套配置:面板只把改动过的子键写进覆盖层,其余子键保持原值(Web 覆盖或 profile 配置),不会因部分保存变成未定义。字段默认值、边界与降级规则见下文配置示例和 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md)。
|
|
81
|
+
|
|
73
82
|
**连接状态**:阶段徽标(待配置 / 未连接 / 连接中 / 已连接 / 连接失败)、飞书长连接状态、Bot open_id 尾号、运行时长、话题路由数 / 活动会话 / 在途回复、最近一次连接失败原因;每 5 秒自动刷新,也可手动刷新或**重启飞书连接**(会断开当前长连接并中断进行中的回复)。徽标不是健康时会额外显示「健康判定」原因——健康等级由宿主**所有 Cordis fiber** 折叠而来,因此「降级」也可能来自其它插件入口仍在等待依赖/加载(与飞书通道无关),原因行会列出对应插件名;「连接失败」只针对飞书连接本身,插件入口保持加载。
|
|
74
83
|
|
|
75
84
|
**飞书应用凭据**:在「连接 → 飞书应用凭据」卡片里填写 **App ID / App Secret** 并保存,值经宿主 `ctx.credentials` 写入 DSH 凭据库(默认 `$DSH_HOME/.credentials.yaml` 的 `refs:` 段,仍遵循凭据层的解析优先级),面板只显示「已配置 / 未配置」与来源,**永不回显已存值**。两项齐全且通道未运行时保存后自动建立连接;已连接时保存只提示重连;只填了一项时保存成功但**不会连接**(状态保持「待配置」),补齐另一项后自动连接。已存值可以单独「移除已存值」;卡片下方显示的是凭据**引用名**(profile 配置里的 `appId` / `appSecret`),换引用名仍需改 profile 配置。
|
|
@@ -78,11 +87,11 @@ DeepSeek Harness(DSH)飞书渠道插件:**一个 dsh 实例对应一个飞
|
|
|
78
87
|
|
|
79
88
|
**模型选择器**:模型与推理在面板里是**独立的「模型」标签页**:宿主 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 /dsh-channels/catalog?provider=…&model=…`(Host 侧短 TTL 缓存,单次查询 4 秒上限)。`provider` 与 `model` 在宿主侧是一条完整的请求路由,因此面板保存时要求**两者同时设置或同时留空**,不允许只选一半。
|
|
80
89
|
|
|
81
|
-
`baseDir`
|
|
90
|
+
`baseDir` 是**模型/工具产生文件的落点**(入站附件默认进 DSH 原生附件库、不落这里;仅 archive 后端才写话题目录):**每个用户拥有一个独立工作区**(私聊 `<baseDir>/p2p/<open_id>/`,群聊 `<baseDir>/groups/<chat_id>/<open_id>/`,同一群里不同用户互不影响;旧版本的「用户 × 聊天」共享工作区正好复用为这个新工作区)。每个话题在工作区内拥有独立工作目录 `<工作区>/topics/<话题 id>/`(无话题的降级会话用 session id),所以话题之间的文件互不干扰,而工作区数量只随用户增长。面板里只接受**绝对路径**(`~` 开头按家目录展开,拒绝文件系统根目录),保存时会创建并校验该目录,不可用时当场报错,不会等到飞书里提示「会话创建失败」。已有 Session / 目录不会被迁移,因此切到新目录后旧话题继续用旧目录(话题内 `/renew` 也一样),只有之后新开的会话使用新目录。
|
|
82
91
|
|
|
83
92
|
**可视化选择工作区根目录**:输入框旁的「选择目录…」按钮会调用**宿主机的系统目录选择框**(macOS 的 NSOpenPanel / Windows 的文件夹对话框),选中后把绝对路径填进草稿,仍需点「保存更改」才落盘。该能力转发自宿主可选的 `ctx.directoryPicker` 服务:桌面端/本机 GUI 会话(native 后端)提供按钮;headless、远程或 SSH 会话没有可服务的显示会话(browse 后端),面板不显示按钮、保留手写输入,插件在缺少该服务的旧宿主上照常加载。选择框打开期间请求保持挂起;取消是正常结果,不会报错。
|
|
84
93
|
|
|
85
|
-
|
|
94
|
+
**目录清理**:「工作区」标签页内提供保留期与「目录清理」卡片,用于回收长期不用的话题目录。保留期可选 `off / 7 天 / 30 天 / 90 天 / 180 天`(默认 `off`);卡片实时预览「插件工作区数量 / 可清理目录 / 预计释放空间」,点「立即清理」需二次确认;也可开启 `workspaceRetentionAuto` 在启动后与每日自动执行一次。清理动作针对 `<baseDir>` 下的话题工作目录(当前布局)以及旧版本按话题建立、标题以 `飞书私聊 ·` / `飞书群聊 ·` 开头的工作区:删除目录,旧版工作区额外注销 workspace registry;并把仍指向它们的活跃路由退休为墓碑(该旧话题收到消息会明确提示「话题已过期」,不会恢复到一个已删除的目录)。**清理只删工作区与 Agent 产物,不影响 Session 附件与会话日志**:默认 native 后端下入站附件存放在 DSH 原生附件库,不在这些目录中,且原生库不提供插件侧清理(DSH 语义为不自动删除,磁盘增长由用户接受);**用户工作区本身永不删除,Harness Session 日志与事件记录也不删除**。若显式使用 archive 后端,旧版 `attachments/` 副本位于话题目录内,会随目录清理一起删除。静默登记的附件元数据另由周期 GC 按 `pendingAttachmentTtlMs` / `pendingAttachmentLimit` 清理,与目录清理无关。删除不可恢复,`off` 时按钮禁用。未建立飞书连接时(存储域未打开)预览不可用,卡片会提示先连接。
|
|
86
95
|
|
|
87
96
|
**访问授权**:「访问授权」标签页集中管理准入:管理员 open_id(`ou_` 开头,私聊 `/userid` 获取)、准入模式、授权申请有效期;「已授权群」「已授权用户」列表支持增删(群 ID 可从入群群信息卡或 `/groupid` 获得,用户 open_id 可用 `/userid` 查看);「待处理申请」可一键授权/忽略。白名单只约束非管理员:管理员在任何私聊/群聊都直接放行。`guarded` 且管理员未配置时标签页顶部会红字提示“当前会拒绝所有对话”。授权白名单与申请记录保存在插件 `storageDomain`(`channels_routes` 域),重启后保留;未建立飞书连接时白名单尚未加载,标签页会提示「连接建立后加载授权列表」并禁用写操作。
|
|
88
97
|
|
|
@@ -112,13 +121,15 @@ src/
|
|
|
112
121
|
├── host/ # DSH 表面(纯类型,无运行时依赖)
|
|
113
122
|
│ ├── core.ts # 身份 / LLM 消息与流 / Session 日志与事件
|
|
114
123
|
│ ├── agent.ts # Agent、注册表、agent preset
|
|
124
|
+
│ ├── attachments.ts # ctx.attachments 窄类型:Image/File 引用、内容块、错误码
|
|
115
125
|
│ ├── commands.ts # ctx.commands 表面
|
|
116
126
|
│ ├── interaction.ts # approval/request + user-questions/request
|
|
117
127
|
│ └── services.ts # 工作区 / 存储域 / 凭据 / token / llm / 投影 / HostContext
|
|
118
128
|
├── cards.ts # 卡片 barrel → src/cards/
|
|
119
129
|
├── cards/ # 卡片 2.0 JSON 渲染
|
|
120
|
-
│ ├── primitives.ts # 信封、markdown
|
|
121
|
-
│ ├── streaming.ts #
|
|
130
|
+
│ ├── primitives.ts # 信封、markdown、折叠面板、按钮、分栏等原语
|
|
131
|
+
│ ├── streaming.ts # 流式回复卡构成(面板/活动行/正文/结算页脚)、分段、字节/表格预算
|
|
132
|
+
│ ├── onboarding.ts # 私聊首次触达引导卡
|
|
122
133
|
│ ├── approval.ts # 审批决策卡
|
|
123
134
|
│ └── question.ts # 结构化问答卡
|
|
124
135
|
├── approvals.ts # 审批 barrel → src/interactions/
|
|
@@ -131,11 +142,17 @@ src/
|
|
|
131
142
|
│ ├── routes.ts # 路由存储 + TTL 墓碑
|
|
132
143
|
│ ├── repair.ts # inflight 快照 + 重启修复
|
|
133
144
|
│ ├── manager.ts # live Agent、冷恢复、preset、/renew 事务
|
|
145
|
+
│ ├── isolation.ts # 话题工作目录编排:workingDirectory 切换 / Git worktree + 降级
|
|
134
146
|
│ └── request-policy.ts # 会话级 provider/model/reasoningEffort 覆盖(/model、/effort)
|
|
135
147
|
├── feishu/
|
|
136
148
|
│ ├── delivery.ts # 出站投递:首帧话题卡、结果卡、分段降级、表情生命周期
|
|
137
|
-
│ ├──
|
|
138
|
-
│
|
|
149
|
+
│ ├── native-attachments.ts # 原生附件存储/降级:saveImage/saveFileStream、生效后端选择
|
|
150
|
+
│ ├── inbound-attachments.ts # 入站附件:不下载分类、图片预取、旧版工作区存档(archive)
|
|
151
|
+
│ ├── pending-attachments.ts # 静默登记:metadata-only 表、scope 隔离、TTL/容量
|
|
152
|
+
│ ├── manifest.ts # <feishu_context> 清单:只列未加载附件、去重、转义、行上限
|
|
153
|
+
│ ├── message-read.ts # 消息读取:get/mget/list、引用摘录、首轮历史、统一渲染
|
|
154
|
+
│ ├── tools.ts # feishu_attachment_fetch / feishu_topic_messages 与每 route 白名单
|
|
155
|
+
│ └── agent-context.ts # agent 作用域 system prompt:native/archive 双模式附件指引
|
|
139
156
|
├── routing/
|
|
140
157
|
│ └── inbound.ts # 入站路由:去重、准入、路由决策树、建/续会话、首触引导
|
|
141
158
|
├── commands.ts # 斜杠命令解析、agent 作用域注册与结果卡
|
|
@@ -144,7 +161,7 @@ src/
|
|
|
144
161
|
├── stats.ts # 宿主投影优先的 token / 缓存 / 上下文折叠
|
|
145
162
|
├── domain.ts # storageDomain 域声明与路由记录 schema
|
|
146
163
|
├── health.ts # 健康快照与卡片
|
|
147
|
-
├── cleanup.ts #
|
|
164
|
+
├── cleanup.ts # 过期话题目录 / 旧版工作区清理策略(纯逻辑,无 I/O)
|
|
148
165
|
├── session-export.ts # 逻辑日志导出
|
|
149
166
|
├── feishu-channel.ts # 官方 SDK Channel 适配(可注入 stub)
|
|
150
167
|
├── client/ # 浏览器半区:设置 →「飞书渠道」页面(构建到 lib/client.js)
|
|
@@ -166,7 +183,7 @@ src/
|
|
|
166
183
|
│ ├── directory-picker.ts # 系统目录选择框适配(ctx.directoryPicker native 后端)
|
|
167
184
|
│ ├── surface.ts # 状态 / 面板视图模型 + 保存 / 清除 / 重连 / 选目录
|
|
168
185
|
│ └── routes.ts # HTTP 路由 + 同源写保护
|
|
169
|
-
└── workspaces.ts #
|
|
186
|
+
└── workspaces.ts # 用户工作区 / 话题目录 / 旧版 _staging 路径解析
|
|
170
187
|
```
|
|
171
188
|
|
|
172
189
|
`lib/index.js` 与 `lib/client.js` 是构建产物(`pnpm build`):前者是安装后 profile 直接加载的 Host 入口(普通 ESM,不依赖 TypeScript 运行时转换),后者是设置面板的浏览器 bundle。`scripts/verify-host-loader.mjs` / `scripts/verify-client-loader.mjs` 分别做冒烟。
|
|
@@ -200,7 +217,7 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
|
|
|
200
217
|
config:
|
|
201
218
|
appId: 'FEISHU_APP_ID' # 凭据引用名(非密钥本身),经 ctx.credentials 解析
|
|
202
219
|
appSecret: 'FEISHU_APP_SECRET'
|
|
203
|
-
baseDir: './channels-data' # 工作区根目录(按
|
|
220
|
+
baseDir: './channels-data' # 工作区根目录(按 私聊用户 / 群×用户 组织;可在 Web 面板覆盖,面板要求绝对路径)
|
|
204
221
|
provider: 'deepseek-official' # 可选:省略则用部署默认
|
|
205
222
|
model: 'deepseek-v4-flash' # 可选
|
|
206
223
|
reasoningEffort: 'high' # 可选:模型声明的精确档位 id;省略则用适配器/provider 默认
|
|
@@ -210,12 +227,29 @@ npm 发行包已包含构建产物:Host 入口是 `lib/index.js`(profile 直
|
|
|
210
227
|
requireMention: true # 普通群需 @ 机器人
|
|
211
228
|
dmMode: 'open'
|
|
212
229
|
threadFallback: 'degrade' # 话题被拒时降级为会话级路由
|
|
230
|
+
topicIsolation: 'auto' # 话题目录隔离:auto(Git 仓库用 worktree,否则子目录)/ subdir / worktree
|
|
213
231
|
routeTtlMs: 2592000000 # 话题路由 30 天无活动即过期;后续消息明确拒绝,不新建 Session(0 = 关闭)
|
|
214
232
|
approvalTimeoutMs: 300000 # HITL 审批决策卡等待时长(默认 5 分钟;超时自动拒绝)
|
|
215
233
|
questionTimeoutMs: 300000 # ask_user_question 问答卡等待时长(默认 5 分钟;超时本轮提问失败)
|
|
216
|
-
acceptAttachments: true #
|
|
217
|
-
|
|
218
|
-
|
|
234
|
+
acceptAttachments: true # 接收飞书图片/文件/合并转发记录(native 下图片预取、文件懒取、纯附件静默登记;false 时附件消息完全惰性)
|
|
235
|
+
attachmentStore: 'native' # 入站附件存储后端:native(DSH 原生附件库,默认)/ archive(旧版工作区 attachments/)
|
|
236
|
+
prefetchImages: true # 触发消息图片预取为首轮 ImageBlock(native 生效;关闭后图片也需工具懒取)
|
|
237
|
+
prefetchImageMaxBytes: 20971520 # 单张预取图片上限(20 MiB)
|
|
238
|
+
prefetchTotalMaxBytes: 52428800 # 单次触发消息预取总字节预算(50 MiB)
|
|
239
|
+
prefetchTimeoutMs: 10000 # 图片预取整段超时(10 s;到期保留已取回图片,其余降级清单)
|
|
240
|
+
historyBootstrapLimit: 10 # 新 Session 首轮附带本话题最近 N 条消息摘要(0 关闭,上限 20)
|
|
241
|
+
promptContextTimeoutMs: 3000 # 被动引用摘录 / 首轮历史的整段读取预算(3 s;超时静默省略)
|
|
242
|
+
pendingAttachmentTtlMs: 604800000 # 静默登记元数据保留期(7 天;0 = 不按时间清理)
|
|
243
|
+
pendingAttachmentLimit: 10 # 静默登记每 (会话,话题,发送者) scope 容量上限
|
|
244
|
+
feishuTools: # 模型工具(Web 面板按子键合并,不会把未改子键写空)
|
|
245
|
+
attachmentFetch: true # feishu_attachment_fetch:按需懒取已知消息的图片/文件(关闭即整体回退 archive)
|
|
246
|
+
attachmentFetchMaxResources: 10 # 单次最多取回资源数(1–50)
|
|
247
|
+
attachmentFetchMaxBytes: 104857600 # 单次总字节预算(100 MiB;文件流式计量)
|
|
248
|
+
attachmentFetchTimeoutMs: 120000 # 单次工具调用整段超时(120 s)
|
|
249
|
+
topicMessages: true # feishu_topic_messages:读取引用消息 / 话题历史并授权其附件
|
|
250
|
+
topicMessagesTimeoutMs: 20000 # 单次话题读取整段超时(20 s;被动摘录用独立 3 s 预算)
|
|
251
|
+
workspaceRetention: 'off' # 过期话题目录清理阈值:off / 7d / 30d / 90d / 180d(只清理工作区与 Agent 产物)
|
|
252
|
+
workspaceRetentionAuto: false # 每日自动执行一次清理(默认关;删除不可恢复;用户工作区永不删除)
|
|
219
253
|
welcomeOnboarding: true # 私聊首次触达发一张引导卡(按 chat 持久化去重)
|
|
220
254
|
allowPermissionSwitch: false # 是否允许 /permissions <name> 从飞书侧切换权限预设(安全敏感)
|
|
221
255
|
accessMode: 'guarded' # 准入模式:guarded(默认,仅管理员+白名单)| open(旧行为,不安全)
|
|
@@ -246,28 +280,60 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
246
280
|
|
|
247
281
|
启动成功标志(控制台):`[dsh-channels] feishu long connection established, bot open_id: ou_xxx`;凭据不完整时不会连接,日志会提示缺少的凭据引用(如 `飞书应用凭据未配置完整(缺少 FEISHU_APP_SECRET…)`),打开 Web **设置 → 飞书渠道 → 连接** 补齐即可。
|
|
248
282
|
|
|
249
|
-
宿主 profile 已内置所需服务(agent-loop、llm-deepseek、session 持久化、storage-domain、workspace
|
|
283
|
+
宿主 profile 已内置所需服务(agent-loop、llm-deepseek、session 持久化、storage-domain、workspace、working-directory);web profile 默认组合还包含 DSH 原生附件库 `dsh-attachment-local` 与 `dsh-tools`(`ctx.attachments` / `ctx.tools`)。缺少原生附件库时入站附件自动回退 archive 后端(见「行为与降级」)。推荐确认 `dsh-token-meter` 已启用(卡片上下文占用显示)。Git worktree 隔离模式额外依赖可选的 `@deepseek-ai/dsh-experimental-worktree` 实验插件,缺失时自动回退为普通话题子目录。
|
|
250
284
|
|
|
251
|
-
**工具与 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
|
|
285
|
+
**工具与 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` 仅 native 后端挂载(`inject(['tools','attachments'])`),`feishu_topic_messages` 两个后端都挂载;宿主缺 `tools` / `attachments` 服务时工具 no-op + 一次性告警,不阻塞会话(详见 [docs/attachment-pipeline-v2.md](docs/attachment-pipeline-v2.md))。若机器人回复把 `<tool_calls>…` 当纯文本吐出,即该会话没挂上 preset;修好后再看会话的 `request/header` 应带 `tools` 字段、日志出现 `tool/call` 事件。无 preset roster 的部署(如 headless)不做此步骤,工具本就位于宿主层。
|
|
252
286
|
|
|
253
287
|
## 飞书侧准备
|
|
254
288
|
|
|
255
289
|
1. [飞书开放平台](https://open.feishu.cn/app) 创建**企业自建应用**,开启**机器人**能力。
|
|
256
290
|
2. 权限(至少):
|
|
257
|
-
- `im:message
|
|
291
|
+
- `im:message`(获取与发送单聊、群组消息;附件资源下载 `messageResource.get` 也走消息资源接口,具体 scope 以控制台该接口要求为准)
|
|
258
292
|
- `im:message.p2p_msg:readonly`(读取用户发给机器人的单聊消息)
|
|
293
|
+
- `im:message:readonly`(读取消息内容:引用摘录、话题历史、`feishu_topic_messages`)
|
|
294
|
+
- `im:message.group_msg`(读取群组消息,**敏感权限**,需企业管理员审批):读取群/话题历史(含机器人入群前的可见历史)。未开通时引用/历史块静默省略、普通对话不受影响,按“机器人可见范围”降级
|
|
259
295
|
- 事件:**接收消息 im.message.receive_v1**(含“接收群聊中 @ 机器人消息”)
|
|
260
296
|
- 事件:**机器人进群 im.chat.member.bot.added_v1**(入群后私聊管理员发群信息卡);对应的群信息读取权限(如 `im:chat:readonly`,以控制台要求为准)
|
|
261
297
|
- 私聊需要能主动给管理员发消息(`im:message` 已覆盖;管理员需在应用可用范围内)
|
|
262
298
|
- 回调:**卡片回传交互 card.action.trigger**(HITL 审批 / `ask_user_question` 问答按钮的回传事件,同样走长连接)
|
|
263
299
|
- `im:message.reactions:write_only`(发送、删除消息表情回复)
|
|
264
300
|
- `im:message:update`(更新卡片)
|
|
301
|
+
|
|
302
|
+
> 消息读取权限缺失时不会报死错:引用摘录与首轮话题历史静默省略(插件生命周期内首次降级输出一条 warn,之后 debug 级),显式调用 `feishu_topic_messages` 返回可读的 `PERMISSION_DENIED`。附件下载仍依赖 `im:message` 一类的消息资源权限;下载被拒时工具与 archive 路径都会返回可读原因。
|
|
265
303
|
3. 事件订阅方式选**长连接**(WebSocket,免公网)。
|
|
266
304
|
4. 发布版本并把用户加入可用范围;将机器人拉入测试群。
|
|
267
305
|
|
|
306
|
+
## 工作区与会话隔离
|
|
307
|
+
|
|
308
|
+
工作区(DSH workspace registry 中的「项目」)按**人**建立,只在首次需要时创建;每个话题在工作区内拥有独立的**实际工作目录**:
|
|
309
|
+
|
|
310
|
+
| 场景 | 工作区目录(header cwd) | 每个话题的工作目录(实际 cwd) |
|
|
311
|
+
|---|---|---|
|
|
312
|
+
| 私聊 | `<baseDir>/p2p/<open_id>/` | `<工作区>/topics/<话题 id>/` |
|
|
313
|
+
| 群聊 | `<baseDir>/groups/<chat_id>/<open_id>/` | 同上 |
|
|
314
|
+
|
|
315
|
+
- 群聊必须是「群 × 用户」:同一个人在不同群之间不共享文件,避免 A 群会话把 B 群内容带出来。
|
|
316
|
+
- 会话创建时以工作区为 header cwd(保证 GUI 里归属该项目、沙箱边界稳定),随后通过 `ctx.workingDirectory` 把实际工作目录切到话题目录;冷恢复会从 Session 日志还原该目录。因此话题之间的文件互不干扰,而工作区数量只随用户增长。
|
|
317
|
+
- `topicIsolation` 控制隔离形式:`auto`(默认)在用户工作区是 Git 仓库(含 `.git`)时给每个话题建一个 `feishu-<话题>` 分支 + worktree(适合多个话题并发改同一个代码仓库),否则用普通子目录;`subdir` 一律子目录;`worktree` 一律尝试 worktree,失败告警并回退子目录。
|
|
318
|
+
- 隔离强度:这是**默认工作目录层面**的隔离(bash / fs / 终端都从话题目录出发),不是防恶意的硬沙箱——沙箱写边界仍是用户工作区。若需要硬边界,可把话题目录直接作为 Session cwd,但会话会失去工作区归属(GUI 显示为未分组)。
|
|
319
|
+
- **入站附件(v2)**:native 后端下图片/文件存入 DSH 原生附件库,**不写入话题目录**,因此工作区与话题目录只承载 Agent 产物;仅显式 `archive` 后端(或宿主缺原生附件服务)才写当前话题目录的 `attachments/`,话题建立前先暂存 `<baseDir>/_staging/...`。v2 主路径不再写 `_staging`;旧版遗留的暂存文件仅保留一次性迁入,按 D12 在 v2 发布后的下一个 minor 版本移除,该目录不受保留期清理。`/renew` 复用同一话题目录。
|
|
320
|
+
- worktree 模式下清理只删除 checkout 目录;Git 分支与管理记录按 DSH 语义保留(需要时可在仓库里 `git worktree prune` / `git branch -D` 回收)。
|
|
321
|
+
- 旧版本「一个话题一个工作区」(`<baseDir>/topics/...`)不会自动迁移:旧话题继续用原目录,保留期清理可回收;旧版「用户 × 聊天」共享工作区正好作为新的用户工作区复用。
|
|
322
|
+
- 设计取舍与约束详见 [docs/workspace-isolation-design.md](docs/workspace-isolation-design.md)。
|
|
323
|
+
|
|
268
324
|
## 行为与降级
|
|
269
325
|
|
|
270
|
-
-
|
|
326
|
+
- **入站附件(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 归一化会丢的文件也会被恢复并进入清单/预取。**只发附件、不带说明的消息完全静默**:不回复、不建会话、不下载、不加表情,只把一个 metadata 记录写入 `pending_attachments`(按 `chat[:thread]:sender` 隔离,默认保留 7 天、每 scope 10 条),后续触发消息在清单中列出并可懒取。
|
|
327
|
+
- **引用与话题历史**:触发消息的 `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)。
|
|
328
|
+
- **入站附件存储后端与降级矩阵**:`attachmentStore: 'native'`(默认)且宿主挂载 `ctx.attachments`、`feishuTools.attachmentFetch=true`、`acceptAttachments=true` 时走 native 懒取;以下任一条件成立即整体回退旧版 archive(同步下载到工作区 `attachments/`、纯文本 prompt、无清单、不挂 fetch 工具):显式 `attachmentStore: 'archive'`;宿主未挂载原生附件库(一次性 warn);`feishuTools.attachmentFetch=false`(文件无法懒取)。`acceptAttachments=false` 时走 archive 的惰性分支:附件消息完全不保存、不触发、不登记,混合消息只保留剥离占位符后的文字。`feishu_topic_messages` 两个后端都挂载(archive 下描述会标注附件不可取)。archive 保留旧实现细节:文件流式落盘且文件名前缀 `YYYYMMDD-HHmmss-`,合并转发记录写 `*-forwarded-record.txt`,成功加 `DONE`、失败加 `No` 并回一条纯文字失败提示,>100 MB 文件先探测大小再按 32 MiB `Range` 分块下载;该路径会在会话建立前下载,属于降级而非默认体验。
|
|
329
|
+
- **附件清理与保留**:DSH 原生附件库**永不自动清理,插件也不提供清理入口**(D6;磁盘增长用户已接受)。「目录清理」只删插件创建的过期话题工作目录与旧版话题工作区,**用户工作区永不删除、Session 日志与事件永不删除**;native 附件在原生库中,不受目录清理影响。若显式使用 archive,`attachments/` 副本位于话题目录,会随目录清理一起删除。静默登记元数据由启动时与每 6 小时一次的 record GC 按 `pendingAttachmentTtlMs` / `pendingAttachmentLimit` 清扫。旧 `_staging` 目录**显式排除在目录清理之外**(避免误删待迁入的遗留附件);v2 主路径不再写 `_staging`,仅对旧版遗留文件做一次性迁入(成功时一条 warn),按 D12 在 v2 发布后的下一个 minor 版本移除。切换到 native 后,历史话题里的旧 archive 附件目录不会被新会话枚举,但 Session 历史仍可按原路径读取。
|
|
330
|
+
- **三类会话行为矩阵(v2)**:
|
|
331
|
+
|
|
332
|
+
| 场景 | 私聊 | 对话群(未 @) | 对话群(@) | 已绑定话题(无 @) |
|
|
333
|
+
| --- | --- | --- | --- | --- |
|
|
334
|
+
| 纯附件消息 | 静默登记(不建会话/不回复/不下载) | 静默登记(后续 @ 建话题可引用) | 静默登记(纯附件不触发) | 静默登记(thread scope) |
|
|
335
|
+
| 文字 + 附件 | 建会话;图片预取 / 文件清单 | 不触发;静默登记 | 建会话;图片预取 / 文件清单 | 续聊;图片预取 / 文件清单 |
|
|
336
|
+
| 纯文字 | 建会话 | 不触发 | 建会话 | 续聊同一 Session |
|
|
271
337
|
- **DSH 0.2.0 兼容**:会话日志读取改用 `Session.snapshotEvents()`(整段 `Session.events` getter 已被 DSH 删除);正文/推理增量改用瞬时事件 `agent/assistant-stream`(持久化 `assistant/chunk` 已不再产生,改为把流内嵌进 `assistant/message.stream`),并处理 `abandoned` 尝试的文本回滚;卡片 token / 缓存命中 / 上下文窗口优先读宿主 `sessionProjections`(`sessionStats` / `tokenUsage` / `contextPressure`),未挂载时回退到本地增量折叠 + `llm.resolveModelInfo`。完整评估、证据与实施状态见 [docs/review-dsh-0.2.0.md](docs/review-dsh-0.2.0.md)。
|
|
272
338
|
- **话题创建**:通过回复消息接口 `reply_in_thread: true`(官方文档明示的“以话题方式回复(即创建话题)”)。响应后经 `im.v1.message.get` 取回 `thread_id` 写入路由表。
|
|
273
339
|
- **话题延续**:`im.message.receive_v1` 事件自带 `thread_id`,命中路由表即 `agent.followup()`,无需 @。同一 (chat, user) 的**并发首消息会被串行化**:后到的消息等待在途的会话建立完成,再按串行语义续聊同一 session 或另起新话题——不会重复建 session、覆盖路由,也不会落入“无路由、无 @”的丢弃窗口。
|
|
@@ -275,20 +341,30 @@ FEISHU_APP_SECRET: xxxxxxxx
|
|
|
275
341
|
- **重启恢复**:路由表持久化在 `storageDomain`;消息到达时 `ctx.agents.resume()` 懒恢复 session。若会话已被其他 owner 恢复为 live(如 Web UI 打开过该会话、并发冷恢复竞态),插件**借用**该 live agent 直接 `followup()`,不再报 "cannot prepare session … while it is live"。借用的 agent 不归插件 dispose;shutdown 与在途 resume 竞态时,刚发布的 agent 会被立即 dispose 以避免泄漏进下一实例的 live 注册表。
|
|
276
342
|
- **重启修复(流式中断)**:每条正在流式的 turn 会把自己的修复凭据(trigger 消息、Typing reaction id、全部段卡 id)写入 `channels_routes` 域的 `inflight` 表(键 = 主卡 message id,turn 结束即删)。重启后长连接一建立,插件就把上一实例留下的中途卡片 patch 成「⚠️ 服务已重启,本次回复中断,请重新发送消息。」终态并补 DONE 表情;patch 失败重试一次(上限 2 次)后放弃。持久化盘点与缺口清单见 [docs/persistence.md](docs/persistence.md)。
|
|
277
343
|
- **消息去重(WS 重投幂等)**:飞书长连接重连会重投未 ack 的事件;每条消息在任何副作用之前先落一条 `processed` 记录(键 = message id,飞书全局唯一),重投消息被直接丢弃——同进程靠内存守卫、跨重启靠持久化记录,每条消息至多处理一次。记录保留 24 小时后由 GC 清扫。
|
|
278
|
-
- **路由表 GC 与过期话题拒绝**:超过 `routeTtlMs`(默认 30 天)无活动的真实话题路由会先写入最小持久化墓碑,再删除活动路由;此后该话题中的普通消息和 Session 命令会收到「话题已过期」提示,插件不会恢复旧 Session,也不会在可见历史仍属于旧 Session 的话题里新建 Session(`/help`、`/userid`、`/groupid`、`/health` 仍可用)。墓碑跨重启保留,仅含 thread/chat/owner
|
|
279
|
-
- **准入与授权**:`accessMode: 'guarded'`(默认)下,入站先过频道准入:**管理员不受任何白名单限制**,私聊/任意群聊(含未授权群)都可直接开会话;**非管理员**私聊回固定提示(`/userid` 例外),群聊要求 **群在白名单** 且 **open_id 在全局用户白名单**,两者缺一不可。未授权消息不建 Session
|
|
344
|
+
- **路由表 GC 与过期话题拒绝**:超过 `routeTtlMs`(默认 30 天)无活动的真实话题路由会先写入最小持久化墓碑,再删除活动路由;此后该话题中的普通消息和 Session 命令会收到「话题已过期」提示,插件不会恢复旧 Session,也不会在可见历史仍属于旧 Session 的话题里新建 Session(`/help`、`/userid`、`/groupid`、`/health` 仍可用)。墓碑跨重启保留,仅含 thread/chat/owner/工作区路径/会话目录/最后活动时间;旧 DSH Session 与日志不删除,话题目录仅在「目录清理」显式启用时按保留期删除(用户工作区永不删除)。降级的 chat-scoped 路由没有独立话题边界,仍直接清理。陈年 in-flight 快照同时在启动时及每 6 小时清扫。`routeTtlMs: 0` 关闭后续路由过期处理,已产生的墓碑仍保持拒绝语义。
|
|
345
|
+
- **准入与授权**:`accessMode: 'guarded'`(默认)下,入站先过频道准入:**管理员不受任何白名单限制**,私聊/任意群聊(含未授权群)都可直接开会话;**非管理员**私聊回固定提示(`/userid` 例外),群聊要求 **群在白名单** 且 **open_id 在全局用户白名单**,两者缺一不可。未授权消息不建 Session、不建话题目录、不落附件、不调模型;群里静默、私聊回固定文案。机器人被拉进任意群时(订阅 `im.chat.member.bot.added_v1`),插件私聊管理员发群信息卡(群名/`chat_id`/成员数/邀请人),未授权群附「授权本群 / 忽略」按钮;已授权群里未授权用户 @ 机器人时,管理员会收到用户授权申请卡(含昵称/open_id)。授权状态持久化在 `channels_routes` 域的 `access_groups` / `access_users` / `access_requests` 表,撤销立即生效。管理员 open_id 在 Web 面板配置;`/auth` 命令、授权卡、面板三种入口等价。
|
|
280
346
|
- **HITL 审批**:插件以 `prepend: true` 注册为宿主 `approval/request` 瀑布的应答者——本频道路由的 session 的审批请求**优先于** Web 网关的通用应答(其面板对飞书用户不可见),非本频道的请求原样 `next()` 交给其他应答者。审批卡是**独立消息**(锚定到该 session 最新流式卡、同话题),绝不嵌入流式卡(其全量替换 patch 会冲掉按钮)。待审批时允许/拒绝按钮以两列等宽方式排列在同一行;决策仅在会话发起人点击时生效:允许 → `allowed-once`、拒绝 → `rejected`、超时 → `rejected`(fail-closed,卡面显示「已超时」)、请求被撤回(abort)→ `cancelled`、卡片发送失败 → `unavailable`(fail-closed);每次决策后卡片更新为终态并移除两个按钮。审批记录(`approval/asked`/`decided`)由宿主会话日志负责,插件不做持久化——重启后遗留问题随 turn 消亡,与 Web 网关一致。需要飞书应用订阅 **card.action.trigger** 回调(见「飞书侧准备」);启动日志 `HITL 审批应答已就绪` 表示生效,`主机未挂载 approval 服务` 表示该部署未启用 HITL。
|
|
281
|
-
-
|
|
347
|
+
- **终态与表情**:成功轮的面板标题变为「回复结束」(无推理内容时不渲染面板),并把 `Typing` 换成 `DONE`;`turn/end.kind = error` 时面板标题为「回复失败」(同样仅当有推理内容),错误只在正文出现一次(`❌ **本轮运行失败**` + 结构化错误消息与错误码),并把 `Typing` 换成 `No`。模型/推理强度/本轮缓存样本不会继承上一轮。
|
|
282
348
|
- **限频**:卡片更新单条消息 5 QPS、应用级 1000 次/分钟;500ms(2 QPS)默认安全,`refreshMs` 下限 200ms;活动状态行的保活心跳默认 5s(`statusRefreshMs` 下限 1s),远低于限频。
|
|
283
|
-
-
|
|
349
|
+
- **卡片体积**:预算常量收敛为三个——`CARD_TARGET_BYTES`(28 KB,分段目标)、`CARD_MAX_BYTES`(30 KB,飞书硬上限,仅作 `buildCard` 的绝对兜底)、`REASONING_MAX_BYTES`(5 KB,推理面板内容窗口);**不再有面板预留**(原 5.5 KB 的 `CARD_PANEL_RESERVE_BYTES` 已删除),分段改为按整卡真实渲染测量:主卡探测面板最大窗口 + 完成态页脚,续卡无面板/页脚、可用满 28 KB(约 27.8 K ASCII 字符,100 KB ASCII 正文约 4 张卡)。正文超长自动分段为多张续卡,断点统一避开代码围栏与表格(段落/标题/行断点;单个代码块/表格小于单卡预算时整体换卡,超过单卡预算时只能硬切、内容不丢;每卡 ≤ 5 个表格,全文保留)。推理面板超出 5 KB 时保留头部并追加「…(内容过长,已截断)」(可见窗口约 4.96 K ASCII 字 / 1.65 K 中文字 / 1.24 K emoji),面板内表格省略;窗口长度与正文长短无关,不占用也不被正文挤占。
|
|
284
350
|
- **续卡降级**:分段卡片发送被飞书拒绝(如表格数量超限 230099/11310)时,自动改发纯文本消息;卡片更新失败按指数退避重试,不刷屏报错。
|
|
285
351
|
|
|
286
|
-
##
|
|
287
|
-
|
|
352
|
+
## 已知取舍
|
|
353
|
+
|
|
354
|
+
- **v2 附件(图片预取的等待窗口)**:卡片、话题与会话先建立,但首轮 `followup` 会等图片预取(默认整段 ≤10 s,`prefetchTimeoutMs` 可调);这是“首轮图片直接可见”与首字延迟之间的取舍,慢网下可调小或关闭 `prefetchImages`。不做后台预取/预下载。
|
|
355
|
+
- **v2 附件(文件懒取依赖模型调用)**:文件默认不在 prompt 里,模型必须主动调用 `feishu_attachment_fetch`;插件用清单“未加载”措辞、工具描述与 guidance 收敛漏调风险,但不做预下载兜底(决定 D4)。
|
|
356
|
+
- **v2 附件(授权白名单进程内)**:每 route 的已知消息 LRU 上限 128,进程重启后触发/引用即时授权丢失(durable 静默登记在 TTL 内仍可授权;历史里的清单文本仍可能提示模型,但 fetch 需要重新登记)。附件密集翻页仍可能挤掉更早条目。
|
|
357
|
+
- **v2 附件(清理与保留)**:DSH 原生附件库不可自动清理、插件不提供清理入口,磁盘随使用增长;「目录清理」只回收工作区与 Agent 产物(archive 副本位于话题目录会随目录删除)。静默登记元数据按 TTL/容量由周期 GC 清理;旧 `_staging` 仅一次性迁入兼容(成功时 warn)、不受目录清理,按 D12 在 v2 发布后的下一个 minor 版本移除。
|
|
358
|
+
- **v2 附件(权限降级)**:缺 `im:message:readonly` / `im:message.group_msg` 时引用摘录与首轮历史静默省略(首次一条 warn),显式调用 `feishu_topic_messages` 返回可读 `PERMISSION_DENIED`;普通对话不受影响。入群前历史只保证“机器人可见范围”。
|
|
359
|
+
- **v2 附件(升级连续性)**:旧 archive 部署切到 native 后,历史话题里已有的 `attachments/` 不会被新会话枚举;Session 历史仍可按原路径读取,旧目录由保留期清理回收。
|
|
360
|
+
- **`/renew` 的会话语义**:同一话题内新建 Session 沿用话题目录、复用附件;但新 Session 首轮不注入 `<feishu_history>`(仅 `startSession` 路径注入首轮历史,当前语义保留,若产品期望“新会话即带话题历史”需另行确认)。
|
|
361
|
+
- **degraded 群会话的读取容器**:话题创建失败降级为 chat 路由后,其容器是整群主时间线——同群他人主时间线消息可被读取/登记(topic 消息已明确拒绝);是否按 sender 进一步收紧待产品确认。
|
|
362
|
+
- **合并转发读取有界**:单条读取子消息超过 50 条时截断(summary 标注数量)、无翻页;工具输出渲染上限 50 行 / 12k 字符。
|
|
288
363
|
- patch 为全量替换:流式期间用户手动展开折叠面板会被下一次刷新收起;流式结束后不再刷新,可自由展开。
|
|
289
364
|
- 超长正文拆成多张卡片(分段投放):推理内容不跨卡片,始终留在首卡;超出 5 KB 的部分被截断(只保留开头),不追加投放。
|
|
365
|
+
- 活动状态行只在当前正在流式输出的那张卡上渲染:若最后一张续卡被飞书拒收、降级为纯文本(无法再 patch),结算状态行(工作流结论 / 子代理完成摘要)不会显示;主卡结算页脚不受影响。
|
|
290
366
|
- 子代理只显示状态(label / 当前工具 / 计时 / 并行计数),不显示其输出正文;后台子代理(`run_in_background`)在父 turn 结束后不再继续显示进度(卡片随父 turn 收尾)。
|
|
291
|
-
- 私聊 `reply_in_thread`
|
|
367
|
+
- 私聊 `reply_in_thread` 已随 P1 真机验证可用(私聊话题正常建立与续聊);若个别租户拒绝,按 `threadFallback: degrade` 退化为会话级路由。
|
|
292
368
|
- 会话的 agent 常驻内存,不做空闲回收(重启后按需懒恢复)。
|
|
293
369
|
- 重启修复会**全量替换**中断的卡片:旧进程里已流出的部分正文随之消失(本地没有副本,patch 是全量语义);修复是一次性 + 一次重试,不再无限重试。
|
|
294
370
|
- 路由 GC 是 TTL 兜底:未监听飞书“群解散/话题删除”事件做主动清理;真实话题过期后写入持久化墓碑,用户回来会收到明确拒绝,不会在旧话题中误建新 Session。
|
|
@@ -310,4 +386,4 @@ pnpm verify:client # client loader 冒烟
|
|
|
310
386
|
pnpm check # 以上全部
|
|
311
387
|
```
|
|
312
388
|
|
|
313
|
-
测试以 stub 替换官方 SDK 与 DSH
|
|
389
|
+
测试以 stub 替换官方 SDK 与 DSH 宿主服务,无需真实凭据。附件管线相关测试:`tests/native-attachments.test.ts`(存储层与 archive 降级)、`tests/pending-attachments.test.ts`(静默登记隔离 / TTL / 容量)、`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 路由(含同源写保护)、`apply()` 的 webServer 接线、模型选择器的宿主目录与降级行为;`tests/client-catalog.test.ts` 覆盖浏览器半区的目录折叠(含依赖项解禁回归);以及 `FeishuService` 的覆盖加载 / 热更新 / 重启 / 连接失败记录 / 监听器释放。
|
|
@@ -84,7 +84,7 @@ SDK 支持 `groupAllowlist` / `dmMode: 'allowlist'` + `dmAllowlist`,但插件*
|
|
|
84
84
|
独立审计还补充了三个同源缺口,都应在 P1/P2 顺手处理:
|
|
85
85
|
|
|
86
86
|
- **route-free / 未知 slash 在 owner 门之前就有副作用**:`/help` 会建话题发卡、`/health` 会泄露路由数、live Agent 数、bot open_id 尾号;非法命令名也会回输入错误卡(`src/service.ts:868-882`、`:942-994`)。
|
|
87
|
-
- **HITL 审批卡在群话题内对群成员可见**:审批卡携带工具名、reason、sessionId(`src/cards/approval.ts:62-90
|
|
87
|
+
- **HITL 审批卡在群话题内对群成员可见**:审批卡携带工具名、reason、sessionId(`src/cards/approval.ts:62-90`),流式卡结算后的页脚第一行仍带完整 session id、在群里可见(`buildModelLine`,`src/cards/streaming.ts:291-295`)。新模型下群内只有管理员和已授权用户,属“已授权成员可见”的可接受范围,后续可改私聊投递。
|
|
88
88
|
- **错误回显未脱敏**:入站异常会原样回 `dsh-channels 处理失败:<error>`(`src/service.ts:754-761`),可能带本地路径/Session 信息。
|
|
89
89
|
|
|
90
90
|
### 2.6 身份发现能力已经有了
|