@modesto_ma/dsh-channels 0.3.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.
@@ -81,7 +81,7 @@
81
81
  |------|------|------|
82
82
  | 工作区(私聊) | `(bot, open_id)` → 目录 `<baseDir>/p2p/<open_id>/`(header cwd) | `ctx.workspaceRegistry` + 磁盘 |
83
83
  | 工作区(群聊) | `(bot, chat_id, open_id)` → 目录 `<baseDir>/groups/<chat_id>/<open_id>/` | 同上 |
84
- | 话题工作目录 | `<工作区>/topics/<topic>/`(实际 cwd,附件落 `<目录>/attachments/`) | Session 日志 `working-directory/change` + 路由记录 `sessionDir` |
84
+ | 话题工作目录 | `<工作区>/topics/<topic>/`(实际 cwd;入站附件 v2 默认进 DSH 原生附件库,仅 archive 降级落 `<目录>/attachments/`) | Session 日志 `working-directory/change` + 路由记录 `sessionDir` |
85
85
  | session | `thread_id` → DSH `sessionId`(每话题一个) | 插件 `storageDomain` 域 `channels_routes` |
86
86
  | 会话延续 | 事件 `thread_id` 命中映射 → `agent.followup()` | — |
87
87
 
@@ -1,6 +1,6 @@
1
1
  # dsh-channels · 真机联调手册
2
2
 
3
- 本文档指导你**手动安装** dsh-channels 插件到本机 dsh,配置飞书机器人,并逐项验证私聊/群聊/话题群三条通道的流式卡片体验。所有步骤均已在本机 dsh checkout(web profile)上实测通过。
3
+ 本文档指导你**手动安装** dsh-channels 插件到本机 dsh,配置飞书机器人,并逐项验证私聊/群聊/话题群三条通道的流式卡片体验。基础安装、连接与三通道流式体验均已在本机 dsh checkout(web profile)上实测通过;§5.4 中标注「待手测」的附件管线项尚需在真实环境确认(自动测试覆盖的项已单独注明)。
4
4
 
5
5
  > 约定:下文用 `$HARNESS` 表示你的 deepseek-harness 仓库目录,`$PLUGIN` 表示本仓库目录(dsh_channels)。
6
6
 
@@ -17,13 +17,19 @@
17
17
  3. **权限管理**,开通以下权限(发布版本时生效):
18
18
  | 权限 | 用途 |
19
19
  |------|------|
20
- | `im:message`(获取与发送单聊、群组消息) | 收发消息、回复话题 |
20
+ | `im:message`(获取与发送单聊、群组消息) | 收发消息、回复话题、下载消息内图片/文件资源(`messageResource.get`,具体 scope 以控制台该接口要求为准) |
21
+ | `im:message:readonly`(读取单聊、群组消息内容) | 引用摘录、话题历史、`feishu_topic_messages` 的消息读取(message.get / mget / list),以及附件资源定位与合并转发子资源读取 |
21
22
  | `im:message.p2p_msg:readonly`(读取用户发给机器人的单聊消息) | 私聊通道 |
23
+ | `im:message.group_msg`(读取群组中所有消息,**敏感权限**) | 读取群/话题中机器人入群前或未 @ 机器人的历史消息;控制台要求时申请并由企业管理员审批,未开通时按“机器人可见范围”降级 |
22
24
  | `im:message.reactions:write_only`(发送、删除消息表情回复) | Typing / DONE / No 表态 |
23
25
  | `im:message:update`(更新消息卡片) | 流式卡片刷新(patch) |
24
26
  | `im:chat:readonly` 一类(获取群信息) | 入群通知卡展示群名/成员数(具体 scope 以控制台要求为准) |
25
27
 
26
28
  > 卡片更新接口本身支持 `im:message` 一项即可,但建议把 `im:message:update` 也加上,避免组合差异。
29
+ >
30
+ > **入站附件(v2)的权限**:图片/文件下载与合并转发容器子资源读取走消息资源接口,随消息收发/读取 scope(`im:message`,必要时 `im:message:readonly`;以控制台该接口的要求为准)。缺下载权限时不会静默丢文件:`feishu_attachment_fetch` 逐资源返回可读失败(如 `DOWNLOAD_FAILED` / 403 文案),archive 后端则加 `No` 表情并回一条纯文字失败提示。**native 主路径(默认)不新增附件专用权限**——下载仍走上面的消息资源 scope,存入 DSH 原生附件库后由宿主管理。
31
+ >
32
+ > **引用与话题历史(P2)的权限降级**:`feishu_topic_messages`、引用摘录、新会话首轮话题历史都依赖消息读取 scope。若权限未随版本发布或 `im:message.group_msg` 审批未通过,普通对话**不受影响**:引用/历史块静默省略,模型仍会正常回答;显式调用 `feishu_topic_messages` 时返回可读的 `PERMISSION_DENIED`(缺失 `im:message:readonly` 一类权限),不会报死错。被动引用/历史读取使用 **默认 3 秒**的短预算(`promptContextTimeoutMs`,不复用工具超时),首次降级会输出一条 **warn** 级日志(含原因与 scope 提示,之后同类降级仅 debug 级;`/status` 不报告读取 scope 状态)。控制台若提示“读取群消息需要敏感权限”,先申请并审批 `im:message.group_msg` 后再验证入群前历史。
27
33
  4. **事件与回调** → 事件订阅方式选择 **使用长连接接收事件**(WebSocket,免公网 IP)。
28
34
  - 添加事件:**接收消息 im.message.receive_v1**(勾选“接收群聊中 @ 机器人消息”)。
29
35
  - 添加事件:**机器人进群 im.chat.member.bot.added_v1**(机器人被拉进群后私聊管理员发群信息卡;未订阅时退化为“群里第一次未授权 @ 时补发”)。
@@ -107,6 +113,23 @@ chmod 600 $DSH_HOME/.credentials.yaml
107
113
  dmMode: 'open' # 私聊通道开关
108
114
  threadFallback: 'degrade' # 话题被拒时降级为会话级路由
109
115
  topicIsolation: 'auto' # 话题目录隔离:auto(Git 仓库用 worktree,否则子目录)/ subdir / worktree
116
+ acceptAttachments: true # 接收飞书附件:native 下图片预取、文件懒取、纯附件静默登记
117
+ attachmentStore: 'native' # 入站附件后端:native(DSH 原生附件库)/ archive(旧版工作区 attachments/)
118
+ prefetchImages: true # 触发消息图片预取为首轮 ImageBlock
119
+ prefetchImageMaxBytes: 20971520 # 单张预取图片上限(20 MiB)
120
+ prefetchTotalMaxBytes: 52428800 # 单次预取总字节预算(50 MiB)
121
+ prefetchTimeoutMs: 10000 # 图片预取整段超时(10 s)
122
+ historyBootstrapLimit: 10 # 新会话首轮话题历史条数(0 关闭,上限 20)
123
+ promptContextTimeoutMs: 3000 # 被动引用/历史读取预算(3 s)
124
+ pendingAttachmentTtlMs: 604800000 # 静默登记保留期(7 天;0 = 不按时间清理)
125
+ pendingAttachmentLimit: 10 # 每个 (会话,话题,发送者) scope 的静默登记容量
126
+ feishuTools: # 模型工具(Web 面板按子键合并,不写空未改子键)
127
+ attachmentFetch: true # feishu_attachment_fetch:按需懒取已知消息的图片/文件
128
+ attachmentFetchMaxResources: 10 # 单次最多取回资源数(1–50)
129
+ attachmentFetchMaxBytes: 104857600 # 单次总字节预算(100 MiB)
130
+ attachmentFetchTimeoutMs: 120000 # 单次调用整段超时(120 s)
131
+ topicMessages: true # feishu_topic_messages:读取引用/话题历史并授权附件
132
+ topicMessagesTimeoutMs: 20000 # 单次话题读取整段超时(20 s)
110
133
  approvalTimeoutMs: 300000 # HITL 审批决策卡等待时长(默认 5 分钟)
111
134
  accessMode: 'guarded' # 准入模式:guarded(默认)| open(旧行为,不安全)
112
135
  adminOpenId: 'ou_xxx' # 管理员 open_id(可留空,留空时 guarded 下拒绝一切对话)
@@ -132,8 +155,26 @@ chmod 600 $DSH_HOME/.credentials.yaml
132
155
  | `accessMode` | `guarded` | 准入模式:`guarded` 仅管理员+白名单;`open` 保留旧行为(任何可用范围用户可对话,不安全) |
133
156
  | `adminOpenId` | 未设置 | 唯一管理员 open_id(`ou_` 开头);留空 + guarded = 拒绝一切对话(`/userid` 引导仍可用)。面板可改 |
134
157
  | `authRequestTtlDays` | `7` | 入群/用户授权申请卡有效期(1–30 天) |
158
+ | `acceptAttachments` | `true` | 接收飞书图片/文件/合并转发记录。native 下触发消息图片预取、文件懒取、纯附件静默登记;`false` 时附件消息完全惰性(不保存、不触发、不登记) |
159
+ | `attachmentStore` | `native` | 入站附件后端:`native`(DSH 原生附件库,默认;不可变、内容寻址去重、随 Session 回放、不自动清理)/ `archive`(旧版下载到话题工作区 `attachments/`,会阻塞会话建立) |
160
+ | `prefetchImages` | `true` | native 下把触发消息图片预取为首轮 `ImageBlock`;失败/超限/超时降级为清单懒取项。关闭后图片也需 `feishu_attachment_fetch` |
161
+ | `prefetchImageMaxBytes` | `20971520` | 单张预取图片字节上限(20 MiB,1 B–200 MiB) |
162
+ | `prefetchTotalMaxBytes` | `52428800` | 一次触发消息预取的总字节预算(50 MiB,1 B–1 GiB);用尽后剩余图片降级清单 |
163
+ | `prefetchTimeoutMs` | `10000` | 图片预取整段超时(100–120000 ms);到期保留已取回图片、其余降级清单。注意配置键是 `prefetchTimeoutMs`(早期 Issue 草稿写作 `prefetchImageTimeoutMs`) |
164
+ | `pendingAttachmentTtlMs` | `604800000` | 静默登记元数据保留期(7 天,≥0;`0` = 不按时间清理,每 scope 容量上限仍生效) |
165
+ | `pendingAttachmentLimit` | `10` | 每个 `(会话, 话题, 发送者)` scope 的静默登记容量(1–100);超出淘汰该 scope 最旧记录 |
166
+ | `historyBootstrapLimit` | `10` | 新会话首轮自动附带本话题最近 N 条消息摘要(0 关闭,上限 20;失败静默降级);更早的消息用 `feishu_topic_messages` 翻页 |
167
+ | `promptContextTimeoutMs` | `3000` | 被动引用/首轮历史读取的整段预算(100–10000 ms);超时即省略对应块、回合照跑。模型主动调用工具的读取超时另由 `feishuTools.topicMessagesTimeoutMs` 控制 |
168
+ | `feishuTools.attachmentFetch` | `true` | 是否挂载 `feishu_attachment_fetch`;关闭后文件无法懒取,插件**整体回退 archive 后端** |
169
+ | `feishuTools.attachmentFetchMaxResources` | `10` | 单次工具调用最多取回的附件数(1–50);超出返回可读原因,可按 `file_key` 拆分重试 |
170
+ | `feishuTools.attachmentFetchMaxBytes` | `104857600` | 单次工具调用的总下载字节预算(100 MiB,1 B–1 GiB);文件流式写入、不整文件缓冲 |
171
+ | `feishuTools.attachmentFetchTimeoutMs` | `120000` | 单次 `feishu_attachment_fetch` 整段超时(1000–600000 ms);到期取消并逐项报告 |
172
+ | `feishuTools.topicMessages` | `true` | 是否挂载 `feishu_topic_messages`;只影响模型工具,被动引用/历史摘录仍生效 |
173
+ | `feishuTools.topicMessagesTimeoutMs` | `20000` | 单次 `feishu_topic_messages` 整段超时(1000–120000 ms) |
135
174
  | `loggerLevel` | `info` | 官方 SDK 日志级别 |
136
175
 
176
+ > **面板与配置位置**:v2 附件配置已加入 Web 面板「设置 → 飞书渠道 → 会话与行为」:`acceptAttachments` / `attachmentStore` / `prefetch*` / `pendingAttachment*` / `historyBootstrapLimit` 在「内容与附件」组,`feishuTools.*` 与 `promptContextTimeoutMs` 在「模型工具」组;保存写入同一 Web 覆盖文件,`feishuTools.*` 按子键合并(不会把未修改子键写空)。直接写 profile 补丁层同样生效,面板覆盖优先。其中 `feishuTools.topicMessages=false` 只关闭模型工具,被动的引用/历史 prompt 摘录仍生效;要完全关闭首轮历史请设 `historyBootstrapLimit: 0`。archive 后端下 `feishu_attachment_fetch` 不挂载,`feishu_topic_messages` 仍可读取历史文本,但工具描述会提示附件不可取。
177
+
137
178
  ### 3.3 初始化管理员与授权(默认准入)
138
179
 
139
180
  插件默认 `accessMode: 'guarded'`:**没有配置管理员时,所有对话都会被拒绝**(这是 fail-closed 的安全默认值)。初始化顺序:
@@ -179,6 +220,16 @@ DSH_HOME=$DSH_HOME pnpm dsh web # 默认 http://127.0.0.1:3080
179
220
 
180
221
  > 第二行只在宿主挂载了 `approval` 服务时出现;若显示 `主机未挂载 approval 服务,HITL 审批应答关闭`,说明该部署未启用 HITL(`approval/request` 事件永远不会派发),并非插件故障。
181
222
 
223
+ **附件管线启动告警**(只影响附件,不影响对话):
224
+
225
+ | 日志 | 含义 |
226
+ |------|------|
227
+ | `attachmentStore=native 但宿主未挂载 ctx.attachments,入站附件回退话题 attachments/ 工作区落盘` | 宿主没有 DSH 原生附件库:入站附件按旧 archive 行为处理;如需 native 请确认 web profile 启用 `dsh-attachment-local` |
228
+ | `feishu_attachment_fetch 未挂载:宿主 tools/attachments 服务不可用(会话继续)` | 缺 `tools` 或 `attachments` 服务:该工具不注册,会话照常建立 |
229
+ | `feishu_topic_messages 未挂载:宿主 tools 服务不可用(会话继续)` | 缺 `tools` 服务:话题读取工具不注册 |
230
+ | `pending attachment storage domain unavailable; using a process-memory TTL fallback…` | 宿主拒绝 `pending_attachments` 表:静默登记退化为进程内 TTL,重启丢失(仅防御场景) |
231
+ | `adopted N legacy staged attachment(s) into …; the one-time _staging migration is deprecated and will be removed after one release cycle` | 旧版 `_staging` 遗留文件被一次性迁入话题目录;按 D12,该兼容路径在 v2 发布后的下一个 minor 版本移除,可据此清理旧数据 |
232
+
182
233
  **未配置凭据(不是启动失败)**:日志会提示 `飞书应用凭据未配置完整(缺少 FEISHU_APP_…)`,插件照常加载但**不连接**;打开 Web **设置 → 飞书渠道 → 连接**,按三步引导填写 App ID / App Secret 并保存后自动连接。
183
234
 
184
235
  **常见连接失败**(插件保持加载,面板「连接」标签页显示原因,可重试):
@@ -241,6 +292,54 @@ DSH_HOME=$DSH_HOME pnpm dsh web # 默认 http://127.0.0.1:3080
241
292
 
242
293
  每个新话题在该工作区内创建独立的话题工作目录 `<工作区>/topics/<话题 id>/`,会话列表仍归属该项目;同一话题内 `/renew` 复用同一话题目录。也可在飞书里用 `/workspace` 查看当前会话的实际工作目录。
243
294
 
295
+ **附件落点(v2)**:默认 `attachmentStore: 'native'` 下入站图片/文件存入 **DSH 原生附件库**(`$DSH_HOME/attachments/v1` 一类宿主目录,由宿主管理),**话题目录下不会出现 `attachments/`**;只有显式 archive 后端或宿主缺原生附件库时才会看到 `<工作区>/topics/<话题>/attachments/`。v2 主路径也不再写 `<baseDir>/_staging/`:该目录只保留旧版遗留文件的一次性迁入(成功时一条 warn),按 D12 在 v2 发布后的下一个 minor 版本移除;`_staging` 被显式排除在「目录清理」之外,不会被保留期清理误删。
296
+
297
+ ### 5.3 引用与话题历史(P2)验证点
298
+
299
+ 需要真机操作;自动测试已覆盖 prompt 组装与工具行为,但飞书侧权限/历史范围仍需人工确认。
300
+
301
+ 1. **引用文字**:在私聊或已绑定话题内,长按一条更早的机器人/用户消息 → 回复(飞书会自动 @ 被回复消息的发送方)→ 发送“解释一下上面这条”。预期:卡片正常开始;模型能复述被引用消息的内容(引用摘录已进 prompt),而不是回答“看不到引用”。
302
+ 2. **引用文件/图片**:先发一条带文件(或图片)的消息,再引用它问“这个文件里是什么”。预期:引用消息的附件会出现在 `<feishu_context>` 清单(`message_id` / `file_key` 为被引用消息),模型用 `feishu_attachment_fetch` 取回后回答;若模型未调用工具,可要求它“用 feishu_attachment_fetch 取一下清单里被引用那条消息的附件”。
303
+ 3. **引用合并转发**:把几条消息合并转发到聊天里,然后引用该合并转发问“总结一下转发内容”。预期:prompt 中出现被引用的转发文本,且容器 `message_id` 同时出现在引用块与清单;模型需要子资源时用**顶层容器 message_id** 调用 `feishu_attachment_fetch` 成功(子消息 id 会 400,属预期)。
304
+ 4. **话题历史 / @ 之前**:在已有讨论的话题群里,先让其他成员(或自己)在话题内发几条消息,再 @ 机器人提问。预期:新会话首轮 prompt 含最近的 `<feishu_history>` 摘要(默认最近 10 条,每行带 `message_id`,附件可按该 id 懒取);更早的消息用 `feishu_topic_messages` 翻页查看。若缺 `im:message.group_msg` 等权限,历史块会静默省略但普通对话不受影响——首次降级有一条 warn 日志(含义见 §1 权限说明),按 §1 申请权限后重新发布版本。
305
+ 5. **入群前历史**:机器人入群前该群已有消息时,重复第 4 步;若能看到入群前的消息,说明当前应用权限覆盖了机器人可见范围,否则按“机器人可见范围”降级(文档已注明,不保证入群前历史)。
306
+ 6. **关闭首轮历史**:把 `historyBootstrapLimit` 设为 `0` 并重载插件,新建会话确认首轮不再附带 `<feishu_history>`;`feishu_topic_messages` 仍可显式读取。
307
+
308
+ ### 5.4 附件管线 v2(P3)真机验证清单
309
+
310
+ > 自动测试已覆盖以下行为(见 [docs/attachment-pipeline-v2.md](attachment-pipeline-v2.md) §12.1),但**插件链路与飞书权限/历史范围仍需真机确认**。表中「状态」一列区分:**已真机** = 对应 Issue 已记录的真实设备结果;**待手测** = 尚无插件链路真机记录(自动测试覆盖);**API 已验** = 同应用 bot 身份只读 API 探针已验,但插件端到端待手测。执行后把结论写回 `.agents/roadmaps/attachment_pipeline_v2/acceptance.md`,未通过项如实记录,不要用 mock 结果冒充。
311
+
312
+ **准备**:`pnpm build` 后用新构建启动(或 HMR 重载插件);准备好 p2p、普通群、话题群各一个,聊天里发一个 ≥100 MB 的大文件附件(飞书允许范围内);确认 `attachmentStore: 'native'`、`feishuTools.attachmentFetch/topicMessages` 为默认开启。
313
+
314
+ | # | 场景 | 操作 | 预期 | 状态 |
315
+ | --- | --- | --- | --- | --- |
316
+ | A1 | 私聊·文本 | 私聊发送一条普通文字 | Typing → 卡片**立即**出现并流式回复;会话/话题正常建立 | ✅ 已真机(P1/P2) |
317
+ | A2 | 私聊·图片 | 发一张截图 + “这是什么” | 卡片先出现;首轮含 `ImageBlock`,模型不调用 fetch 即可描述(可能另用 `read_image`);首轮清单不列该已预取图片;原生库对象数 +1、话题目录无 `attachments/` | ✅ 已真机(P1 步骤 2) |
318
+ | A3 | 私聊·文件懒取 | 发 “看下这个文件” + 一个文件(如 48 MB zip) | 卡片先出现;首轮只有清单行(`message_id/file_key/name`),**下载发生在回合开始之后**;模型调 `feishu_attachment_fetch` 后结果为 `[text, file]` + 只读路径;文件字节进原生库、话题目录无 `attachments/`、无 `_staging` | ✅ 已真机(P1 步骤 3) |
319
+ | A4 | 私聊·纯附件静默 | 只发一张图片(不带文字),然后再发一条文件(不带文字) | 无回复、无表情、无新话题/会话、无下载;`pending_attachments` 各新增 1 行(scope=chat,kind=image/file),原生库对象数与工作区文件不变 | ✅ 已真机(P1 步骤 1 图片;P2 步骤 3a 文件) |
320
+ | A5 | 私聊·静默引用 | 在 A4 之后发普通文字:“刚发的文件叫什么/内容是什么” | 首轮清单列出静默登记的文件;模型 fetch 取回后正确回答;对应 pending 行变 `consumed=true` | ✅ 已真机(P1 步骤 4;P2 步骤 3b) |
321
+ | A6 | 私聊·引用文件 | 引用一条带文件的消息,问文件内容 | prompt 出现 `<feishu_quote>`(含 message_id),清单出现 quote 行;模型 fetch 成功 | ✅ 已真机(P2 步骤 3c) |
322
+ | A7 | 私聊·引用合并转发 | 合并转发两条消息(含一个文件),引用它问“总结/取其中文件” | quote 块展开子消息并标“合并转发,共 N 条”;清单为容器行;fetch 用**顶层容器 message_id** 成功(子消息 id 会 400,属预期) | ✅ 已真机(P2 步骤 4) |
323
+ | A8 | 私聊·大文件 | 发 ≥100 MB(如有 1 GB 级更好)文件 + “看下这个文件” | 卡片立即出现、不等待;清单先列文件;模型调 fetch 后流式入库,超过 `attachmentFetchTimeoutMs` 时返回可读超时,不阻塞后续对话 | ⏳ 待手测(P0 已验 980 MB 下载/取消,admission 链路未验;自动测试覆盖超时/取消) |
324
+ | A9 | 私聊·下载失败 | 引用/登记一条已被撤回或过期(资源不可用)的附件消息,让模型 fetch | 工具逐资源返回可读失败(如 `DOWNLOAD_FAILED` / 资源过期说明),不出现 `[object Object]`,回合正常结束 | ⏳ 待手测(自动测试覆盖失败路径) |
325
+ | B1 | 对话群·@ 触发 | 普通群 @ 机器人发图片 + 文字 | 新话题 + 卡片;图片预取进首轮;话题内后续无 @ 消息续聊同一 Session | ✅ 已真机(P1 步骤 5) |
326
+ | B2 | 对话群·未 @ 附件 | 同一群内不 @ 只发一张图片 / 只发一个文件,随后 @ 机器人问“我刚才发的文件” | 未 @ 时完全静默(无回复/表情/会话/下载),群 scope pending 新增行;随后 @ 建话题的首轮清单可列出并懒取该文件 | ✅ 已真机(P1 步骤 6 群未 @ 图片静默);⏳ 文件与跨 scope 引用部分待手测(自动测试覆盖) |
327
+ | B3 | 对话群·引用文件 | @ 机器人并引用群里的一条文件消息 | quote 摘录 + 清单 quote 行 + fetch 成功(权限范围内) | ⏳ 待手测(私聊同源已真机;自动测试覆盖) |
328
+ | C1 | 话题群·@ 图片/文件 | 在话题群的话题内 @ 机器人发图片 + 文件 | 在该话题内回复卡片(不新建话题);图片进首轮/文件进清单 | ⏳ 待手测(普通群 @ 建话题与话题群历史已真机;话题群图片/文件组合自动测试覆盖) |
329
+ | C2 | 话题群·纯附件静默 | 已绑定话题内不 @ 只发文件 | 完全静默;thread scope 登记;该话题后续触发消息的清单可列出 | ⏳ 待手测(群未 @ 静默已真机;话题内纯附件由自动测试覆盖,未单独真机) |
330
+ | C3 | 话题群·历史 | 话题内先发 A/B/C(不 @),再 @ 机器人提问 | 新 Session 首轮含 `<feishu_history>` A/B/C(带 message_id);`feishu_topic_messages` 可翻页,续聊轮不重复注入历史;工具渲染含 `- 下一页 cursor:` | ✅ 已真机(P2 步骤 5/6R) |
331
+ | C4 | 话题群·入群前历史 | 选择一个入群前就有消息的群,重复 C3 | 能看到机器人可见范围内早于入群的消息;看不到时属权限/可见范围限制,历史块静默省略(不报错) | ⏳ 待手测(API 探针显示可见早约 1 个月历史,但“入群时间”不可查询、不同租户可能不同) |
332
+ | D1 | archive 降级 | 面板把存储后端改为 `archive`(或临时把 `feishuTools.attachmentFetch` 设为 `false`),发图片/文件 | 旧行为:图片/文件下载到话题 `attachments/`(或 `_staging/` 后迁入),纯附件消息加 `DONE` 表情、不触发模型;不挂 fetch 工具;清单不出现 | ✅ 已真机(P1 步骤 7 显式 archive);⏳ `attachmentFetch=false` 组合待手测(自动测试覆盖) |
333
+ | D2 | 缺原生附件库 | 在无 `ctx.attachments` 的部署(如 headless/旧宿主)启动并发附件 | 自动回退 archive 旧行为;日志一条 warn:`attachmentStore=native 但宿主未挂载 ctx.attachments…`;不崩溃 | ⏳ 待手测(自动测试覆盖) |
334
+ | D3 | 关闭预取 | 面板关闭 `prefetchImages` 后重启/重载,新会话发图片 + 文字 | 快速出现卡片,首轮**无** ImageBlock;图片出现在清单,模型用 `feishu_attachment_fetch` 取回后回答 | ⏳ 待手测(自动测试覆盖) |
335
+ | D4 | 关闭附件接收 | 面板关闭 `acceptAttachments` 后发纯附件 / 文件+文字 | 纯附件:无回复/无登记/无下载;文件+文字:只以文字触发,附件不保存不登记 | ⏳ 待手测(自动测试覆盖) |
336
+ | E1 | 权限缺失·读取 | 临时移除 `im:message:readonly`(或让 `im:message.group_msg` 未审批)后发布版本,发引用消息/在群内 @ | 普通对话正常;引用/历史块静默省略;首次降级一条 warn;显式 `feishu_topic_messages` 返回可读 `PERMISSION_DENIED`;恢复权限并重发版本后恢复 | ⏳ 待手测(用户已决定跳过;自动测试覆盖 throw/resolved 两种降级) |
337
+ | E2 | 权限缺失·下载 | 临时移除消息资源读取相关 scope,让模型 fetch 一个文件 | 工具逐资源返回可读失败(403/权限说明),回合不崩;archive 下则加 `No` 并回纯文字失败提示 | ⏳ 待手测(自动测试覆盖) |
338
+ | F1 | 面板配置 | 在 Web 「会话与行为」依次修改存储后端、预取开关/上限、登记 TTL/容量、两个工具开关与上限并保存,新建会话验证生效 | 面板读写与帮助文案正确;嵌套 `feishuTools.*` 只改被改子键;旧值不丢;保存后入站行为即时按新配置判定、工具挂载对新会话生效;非法值被拒 | ⏳ 待手测 + 自动测试(settings 覆盖) |
339
+ | F2 | 清理语义 | 在 native 后端执行「目录清理」,再回到旧话题发消息 | 只删过期话题/旧版目录;用户工作区与会话日志保留;native 附件不受影响(Session 历史仍可读);archive 后端下话题目录内 `attachments/` 随目录删除 | ⏳ 待手测(cleanup/settings 自动测试覆盖) |
340
+
341
+ **记录要求**:每个「待手测」项执行后,在 `acceptance.md` 对应 AC 下写「操作 / 实际结果 / 结论 / 环境(构建 SHA、profile、日期)」;失败的项保留原文与日志摘录,修复后复测再更新。`historyBootstrapLimit: 0`、非本 route 负向、`acceptAttachments=false` 等用户已决定跳过真机的项,标注「自动测试覆盖,用户决定跳过真机」,不要写成已验证。
342
+
244
343
  ## 6. 常见问题排查
245
344
 
246
345
  | 现象 | 排查 |
@@ -251,6 +350,13 @@ DSH_HOME=$DSH_HOME pnpm dsh web # 默认 http://127.0.0.1:3080
251
350
  | 群内 @ 机器人无响应(登录台无 access 报错) | 管理员不受白名单限制,可直接对话;**非管理员**需要该群已授权且自己在用户白名单里。可用 `/auth status`(管理员)或 Web 面板「访问授权」标签页确认 |
252
351
  | `/auth` 无反应 | `/auth` 仅管理员可用;非管理员在群里会被静默忽略、私聊会收到固定提示 |
253
352
  | 群聊 @ 无响应 | 事件订阅是否勾选“接收群聊中 @ 机器人消息”;机器人是否在群内且未被群主禁言 |
353
+ | 模型说看不到刚发的文件 / 文件没有自动出现 | 属 v2 设计:文件默认不加载,只在 `<feishu_context>` 清单里声明;模型需调用 `feishu_attachment_fetch`。可明确要求“用 feishu_attachment_fetch 取清单里 message_id=… 的附件”;若工具没注册,检查启动日志 `feishu_attachment_fetch 未挂载` 与宿主 `tools`/`attachments` 服务 |
354
+ | 只发附件(不带文字)机器人毫无反应 | native 后端下的**预期行为**:纯附件消息静默登记,不回复、不建会话、不下载;随后发一条文字即可引用清单里的附件。archive 后端下会看到 `DONE` 表情且附件落到话题目录 |
355
+ | 模型没有调用 feishu_attachment_fetch | 检查 `<feishu_context>` 清单是否出现在 prompt(有未加载附件时才有);确认工具开关 `feishuTools.attachmentFetch` 与 native 后端;必要时在消息里点明“用附件工具取一下”。漏调风险由清单措辞 / 工具描述 / guidance 收敛,没有预下载兜底(决定 D4) |
356
+ | 引用或话题历史内容缺失 | 缺消息读取 scope(`im:message:readonly` / 敏感的 `im:message.group_msg`):引用/历史块静默省略、普通对话不受影响;启动后首次降级有一条 warn,之后 debug 级。按 §1 申请权限并重新发布版本;显式调用 `feishu_topic_messages` 会返回可读 `PERMISSION_DENIED` |
357
+ | `feishu_attachment_fetch` 超时 / 报字节超限 | 大文件受 `feishuTools.attachmentFetchTimeoutMs`(默认 120 s)与 `attachmentFetchMaxBytes`(默认 100 MiB)限制:可调大上限/超时,或用 `file_key` 分批取;资源过期/被撤回时逐资源返回可读失败,可直接重试或让用户重发 |
358
+ | 升级到 v2 后在旧话题里“找不到以前的附件” | 旧 archive 部署的附件仍在旧话题 `attachments/`,但新会话不再枚举该目录;Session 历史里的只读路径仍可读。需要长期保留请勿启用目录清理,或从历史消息重新发送/引用 |
359
+ | 话题目录里出现 `attachments/` | 当前生效的是 archive 后端:检查 `attachmentStore`(是否显式设为 `archive`)、宿主是否挂载 `ctx.attachments`、`feishuTools.attachmentFetch` 是否为 `false`。native 后端不会写话题 `attachments/` |
254
360
  | 有 Typing 但无卡片 | 权限 `im:message` / `im:message:update`;卡片 2.0 需要较新的飞书客户端(升级客户端后重试) |
255
361
  | 卡片正文只到某长度 | 旧版行为;新版单卡目标 28 KB(`CARD_TARGET_BYTES`,飞书上限 30 KB 留 2 KB 冗余),超长正文自动分段为多张续卡(「📄 回复分段 n」,每卡 ≤ 5 个表格,断点避开代码围栏与表格),续卡无面板/页脚、可用满 28 KB,全文保留;推理面板独立 5 KB 上限,超出的推理只保留开头并追加「已截断」提示 |
256
362
  | 报错 230099 / `ErrCode: 11310 card table number over limit` | 飞书单卡表格数量超限(真机验证上限 5 个);新版已按“每卡最多 5 个表格”自动切分,不再触发;历史版本请重装插件并重启 dsh |
@@ -45,8 +45,8 @@ src/
45
45
  | 首触引导卡 | ✅ | 私聊首次触达一张,`welcomedChats` 持久化去重,`welcomeOnboarding` 可关 |
46
46
  | 问答卡自由文本回答 | ✅ | 无选项问题直接开放答题;阻塞期间普通文本回复即答案(`custom`) |
47
47
  | `/model` 查看与切换 | ✅ | `session/request-policy.ts` 统一 provider/model/effort;`ctx.llm.listModels` 校验;经 `command/run` 持久化 |
48
- | 附件输入 | ✅ | `feishu/inbound-attachments.ts`:图片/文件流式落盘到工作区 `attachments/`、合并转发记录转存 `.txt`;不触发模型,Agent 用工具按需读取;`acceptAttachments` 可配 |
49
- | 目录清理 | ✅ | `cleanup.ts` 纯策略 + 设置面板卡片:`workspaceRetention`(off/7/30/90/180 天)+ 手动/每日清理,删除过期话题目录/旧版话题工作区并把路由退休为墓碑,用户工作区与 Session 日志保留 |
48
+ | 附件输入 | ✅ | v2:入站图片/文件默认进 DSH 原生附件库;触发消息图片预取、文件由 `feishu_attachment_fetch` 懒取、纯附件静默登记;`archive` 降级保留旧行为(`feishu/inbound-attachments.ts` 落工作区 `attachments/`,合并转发记录转存 `.txt`);详见 [attachment-pipeline-v2.md](attachment-pipeline-v2.md) |
49
+ | 目录清理 | ✅ | `cleanup.ts` 纯策略 + 设置面板卡片:`workspaceRetention`(off/7/30/90/180 天)+ 手动/每日清理,只清理工作区与 Agent 产物(过期话题目录/旧版话题工作区)并把路由退休为墓碑,用户工作区、Session 日志与原生附件库保留 |
50
50
 
51
51
  ## 验证基线
52
52
 
@@ -1,6 +1,8 @@
1
1
  # 工作区粒度与会话隔离设计
2
2
 
3
3
  > 状态:已确认并实现(v1)。目标:工作区数量随「人」增长而不是随「话题」增长,同时话题之间不互相踩文件。
4
+ >
5
+ > 附件说明(v2 更新):入站图片/文件默认存入 **DSH 原生附件库**(不写入话题目录),仅 `attachmentStore: 'archive'` 降级路径才写 `<话题目录>/attachments/` 与旧版 `_staging/`;详见 [attachment-pipeline-v2.md](attachment-pipeline-v2.md)。下文目录布局中的 `attachments/` 与 `_staging/` 均为 archive / 旧版兼容语义。
4
6
 
5
7
  ## 1. 问题
6
8
 
@@ -23,17 +25,18 @@ DSH 本身允许这两个维度解耦,见下。
23
25
  <baseDir>/
24
26
  p2p/<open_id>/ # 私聊用户工作区(注册 workspace,header.cwd)
25
27
  topics/<topic-token>/ # 话题目录(会话实际工作目录)
26
- attachments/ # 该话题的入站附件
28
+ attachments/ # 仅 archive 后端:该话题的入站附件(v2 native 默认不写)
27
29
  groups/<chat_id>/<open_id>/ # 群聊中某成员的工作区(注册 workspace)
28
30
  topics/<topic-token>/
29
- attachments/
31
+ attachments/ # 同上
30
32
  topics/... # 旧版「话题=工作区」目录,原样保留,不自动迁移
31
- _staging/<chatType>/<chatId>/<openId>/ # 话题建立前的附件暂存
33
+ _staging/<chatType>/<chatId>/<openId>/ # 仅旧版:话题建立前的附件暂存(v2 不再写入)
32
34
  ```
33
35
 
34
36
  - 私聊一个用户一个工作区;群聊**一个群 × 一个用户**一个工作区(不能做成「用户全局一个」:否则同一个人在不同群之间会串文件,A 群会话可能把 B 群的文件内容贴给 A 群成员)。
35
37
  - `<baseDir>/p2p/...`、`<baseDir>/groups/...` 与旧版「用户 × 聊天」共享工作区路径一致,升级后旧会话仍归属同一个项目。
36
38
  - 话题 token:真实话题用 `thread_id`;降级 chat-scoped 会话用其 session id。目录名经过安全字符校验。
39
+ - 入站附件(v2 native):存 DSH 原生附件库(宿主管理、不可变、内容寻址去重),不占话题目录;archive 降级才落 `attachments/`。
37
40
 
38
41
  ## 4. 隔离档位(`topicIsolation`)
39
42
 
@@ -61,9 +64,9 @@ DSH 本身允许这两个维度解耦,见下。
61
64
 
62
65
  - **隔离强度**:话题子目录/worktree 只保证「工具默认在话题目录工作」。同一用户工作区内的绝对路径访问仍然可行;沙箱写边界仍是用户工作区。
63
66
  - **硬隔离**(不采用):把 header.cwd 指向话题目录,会话在 GUI 中不再归属工作区。只有当话题来源不可信、必须防互相读取时才值得。
64
- - **附件**:仍落在当前话题目录的 `attachments/`;话题建立前收到的附件先暂存到 `_staging/`,首个 Session 创建时迁入。
67
+ - **附件**:v2 默认(`attachmentStore: 'native'`)入站图片/文件存 DSH 原生附件库,**不写话题目录**,因此目录清理不会影响 Session 附件;显式 archive 后端(或宿主缺原生附件库)才落当前话题目录的 `attachments/`,话题建立前收到的附件先暂存 `_staging/`,首个 Session 创建时一次性迁入。v2 主路径不再写 staging;`_staging` 仅作旧版兼容(成功迁入时 warn),v2 发布后的下一个 minor 版本移除(D12),且被显式排除在保留期清理之外。
65
68
  - **`/renew`**:同一话题内新建 Session,复用同一话题目录(worktree 也复用)。
66
- - **清理**:保留期只删除过期的话题目录 / 旧版话题工作区,**永不删除用户工作区**;活跃路由先退休为墓碑(旧话题消息收到「话题已过期」提示),Session 日志不删除。worktree 模式下只删除 checkout 目录,Git 分支与 worktree 管理记录按 DSH 语义保留(`git worktree prune` / `branch -D` 可回收)。
69
+ - **清理**:保留期只删除过期的话题目录 / 旧版话题工作区,**永不删除用户工作区**;活跃路由先退休为墓碑(旧话题消息收到「话题已过期」提示),Session 日志不删除;native 原生附件库不参与清理(插件不提供附件清理),archive 降级时话题目录内的 `attachments/` 副本会随目录删除,旧版 `_staging/` 显式排除在清理之外(D12)。worktree 模式下只删除 checkout 目录,Git 分支与 worktree 管理记录按 DSH 语义保留(`git worktree prune` / `branch -D` 可回收)。
67
70
  - **迁移**:不做自动迁移。旧版 `topics/...` 工作区继续服务旧路由,可由清理卡片按保留期回收;旧版「用户 × 聊天」共享工作区即新的用户工作区,直接复用。
68
71
 
69
72
  ## 7. 实现位置
@@ -72,7 +75,7 @@ DSH 本身允许这两个维度解耦,见下。
72
75
  |---|---|
73
76
  | `src/workspaces.ts` | 用户工作区 / 话题目录 / 标题 / 暂存目录解析 |
74
77
  | `src/session/isolation.ts` | 隔离档位判定、工作目录进入、worktree 创建与降级恢复 |
75
- | `src/routing/inbound.ts` | 建会话与续聊时的目录编排、附件迁移、路由写入 |
78
+ | `src/routing/inbound.ts` | 建会话与续聊时的目录编排、旧版 `_staging` 一次性迁入、路由写入 |
76
79
  | `src/session/manager.ts` | 会话创建/恢复/`/renew` 的目录挂载与附件 guidance |
77
80
  | `src/cleanup.ts` + `src/service.ts` | 按话题目录/旧版工作区回收的保留期策略与面板接线 |
78
81
  | `src/domain.ts` | 路由记录 v1 兼容扩展(`sessionDir` / `worktreeBranch`) |
package/lib/client.js CHANGED
@@ -78,7 +78,11 @@ window.__ModuleLoader__.load({
78
78
  /** Which editable field groups each tab renders (the tabs own one or two). */
79
79
  const TAB_FIELD_GROUPS = {
80
80
  connection: [],
81
- sessions: ["behavior", "content"],
81
+ sessions: [
82
+ "behavior",
83
+ "content",
84
+ "tools"
85
+ ],
82
86
  model: ["model"],
83
87
  workspace: ["workspace"],
84
88
  access: ["access"],
@@ -89,6 +93,7 @@ window.__ModuleLoader__.load({
89
93
  workspace: "工作区与文件",
90
94
  behavior: "行为",
91
95
  content: "内容与附件",
96
+ tools: "模型工具",
92
97
  model: "模型与推理",
93
98
  streaming: "流式卡片与日志",
94
99
  access: "准入与授权"
@@ -156,9 +161,9 @@ window.__ModuleLoader__.load({
156
161
  appSecretPlaceholder: "输入 App Secret",
157
162
  credentialsPendingHint: "填写后点「保存凭据」;两项齐全时插件会自动连接飞书。",
158
163
  connectionTabHelp: "连接状态与飞书应用凭据。凭据不完整时不会连接飞书。",
159
- sessionsTabHelp: "私聊、群聊触发方式与入站内容;「需重连」项在下次连接时读取。",
164
+ sessionsTabHelp: "私聊、群聊触发方式与入站内容:附件存储/预取、静默登记保留、模型工具开关与读取预算;「需重连」项在下次连接时读取。",
160
165
  modelTabHelp: "飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。",
161
- workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。",
166
+ workspaceTabHelp: "每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录,也不会被这里的目录清理删除。",
162
167
  accessTabHelp: "准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。",
163
168
  advancedTabHelp: "流式刷新等低层参数、Web 覆盖文件与只读信息。",
164
169
  save: "保存更改",
@@ -205,7 +210,7 @@ window.__ModuleLoader__.load({
205
210
  advancedHelp: "这些值来自 profile 配置,不能在面板里编辑。",
206
211
  readOnlyCard: "部署级参数",
207
212
  cleanupCard: "目录清理",
208
- cleanupCardHelp: "删除超过保留期且不再活跃的飞书话题目录(以及旧版本按话题建立的工作区目录);用户工作区本身、DSH 会话日志都不会删除。删除不可恢复:相关旧话题会失效并提示已过期。",
213
+ cleanupCardHelp: "只清理工作区与 Agent 产物:删除超过保留期且不再活跃的飞书话题目录、旧版本按话题建立的工作区目录,以及这些目录内 Agent 产生的文件;不影响 Session 附件——入站图片/文件默认存于 DSH 原生附件库,不随目录清理删除。用户工作区本身与 DSH 会话日志也不会删除。删除不可恢复:相关旧话题会失效并提示已过期。",
209
214
  cleanupRetention: "保留期",
210
215
  cleanupAutoLabel: "自动清理",
211
216
  cleanupCandidates: "可清理目录",
@@ -217,7 +222,7 @@ window.__ModuleLoader__.load({
217
222
  cleanupAutoInactive: "未执行(保留期为关闭)",
218
223
  cleanupNow: "立即清理",
219
224
  cleanupRunning: "清理中…",
220
- cleanupConfirm: "确定清理过期目录吗?该操作不可恢复,会删除这些目录内的全部文件(包括 attachments/ 附件)。",
225
+ cleanupConfirm: "确定清理过期目录吗?只清理工作区与 Agent 产物,不影响 Session 附件;但这些目录内未提交的文件会被删除,且操作不可恢复。",
221
226
  cleanupConfirmHint: "删除前会再次确认;保留期为「关闭」或没有过期目录时按钮不可用。",
222
227
  cleanupDisabledOff: "保留策略为「关闭」:请先在上方「过期目录保留期」中选择保留天数。",
223
228
  cleanupDisabledEmpty: "没有超过保留期的过期目录。",