@modesto_ma/dsh-channels 0.3.0 → 0.5.0-beta.1

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.
Files changed (64) hide show
  1. package/README.md +123 -53
  2. package/docs/access-control-design.md +7 -5
  3. package/docs/attachment-pipeline-v2.md +310 -0
  4. package/docs/feasibility-and-design.md +1 -1
  5. package/docs/feishu-onboarding.md +127 -21
  6. package/docs/persistence.md +6 -4
  7. package/docs/refactor-plan.md +2 -2
  8. package/docs/workspace-isolation-design.md +13 -11
  9. package/lib/client.js +179 -288
  10. package/lib/client.js.map +1 -1
  11. package/lib/index.js +12125 -6382
  12. package/lib/index.js.map +1 -1
  13. package/package.json +1 -1
  14. package/src/access/cards.ts +9 -9
  15. package/src/access/controller.ts +10 -10
  16. package/src/access/index.ts +1 -0
  17. package/src/access/policy.ts +10 -10
  18. package/src/access/requests.ts +32 -31
  19. package/src/access/store.ts +12 -12
  20. package/src/access/targets.ts +56 -0
  21. package/src/cards/approval.ts +96 -26
  22. package/src/cards/index.ts +1 -0
  23. package/src/cards/onboarding.ts +1 -0
  24. package/src/cards/picker.ts +115 -0
  25. package/src/cards/streaming.ts +56 -7
  26. package/src/client/api.ts +5 -25
  27. package/src/client/panel/access-card.tsx +24 -15
  28. package/src/client/panel/access-tab.tsx +3 -3
  29. package/src/client/panel/connection-tab.tsx +70 -1
  30. package/src/client/panel/index.tsx +11 -62
  31. package/src/client/panel/shared.tsx +0 -23
  32. package/src/client/panel/workspace-tab.tsx +2 -18
  33. package/src/client/styles.ts +2 -0
  34. package/src/commands/registry.ts +2 -2
  35. package/src/config.ts +190 -32
  36. package/src/domain.ts +28 -18
  37. package/src/feishu/agent-context.ts +65 -19
  38. package/src/feishu/inbound-attachments.ts +440 -3
  39. package/src/feishu/manifest.ts +247 -0
  40. package/src/feishu/message-read.ts +1330 -0
  41. package/src/feishu/native-attachments.ts +998 -0
  42. package/src/feishu/scopes.ts +171 -0
  43. package/src/feishu/send-file.ts +543 -0
  44. package/src/feishu/slash-commands.ts +134 -0
  45. package/src/feishu/tools.ts +2305 -0
  46. package/src/feishu-channel.ts +322 -6
  47. package/src/host/agent.ts +55 -0
  48. package/src/host/attachments.ts +210 -0
  49. package/src/host/index.ts +1 -0
  50. package/src/index.ts +5 -4
  51. package/src/interactions/approvals.ts +187 -7
  52. package/src/interactions/pending.ts +3 -3
  53. package/src/routing/inbound.ts +620 -49
  54. package/src/service.ts +808 -375
  55. package/src/session/manager.ts +67 -13
  56. package/src/session/routes.ts +2 -3
  57. package/src/settings/contract.ts +90 -89
  58. package/src/settings/overlay.ts +124 -13
  59. package/src/settings/routes.ts +3 -17
  60. package/src/settings/surface.ts +156 -82
  61. package/src/streamer.ts +94 -5
  62. package/src/workspaces.ts +39 -26
  63. package/src/cleanup.ts +0 -535
  64. package/src/client/panel/cleanup-card.tsx +0 -106
@@ -0,0 +1,310 @@
1
+ # 入站附件管线 v2:立即建会话 + 清单/摘录 + 图片预取 + 文件懒取
2
+
3
+ > 状态:P1/P2 已实现并提交在本地 `main`,P3 正在做配置面板、文档与验收收口(`.agents/issues/panel_docs_release.md`)。本文描述**已落地代码**的行为;配置项以 [src/config.ts](../src/config.ts) 为准,实现细节以对应源码为准,真机与待手测项在文末分开标注。
4
+ >
5
+ > 设计决定见 `.agents/roadmaps/attachment_pipeline_v2/decisions.md` D1–D12,长期取舍见 `.agents/notes/2026_10_20_attachment_pipeline.md`。
6
+
7
+ ## 1. 目标与非目标
8
+
9
+ **目标**
10
+
11
+ - 飞书私聊 / 对话群 / 话题群收到消息时,**会话、话题、流式卡片零等待**:不再等附件下载完才开始模型回合。
12
+ - 触发消息中的图片预取为原生 `ImageBlock`,模型首轮即可看见;预取有上限与短超时,失败降级为清单。
13
+ - 文件默认**不下载**;prompt 用结构化清单声明“附件未加载”,模型需要时调用 `feishu_attachment_fetch` 按 `message_id` 懒取。
14
+ - 纯附件消息(私聊仅附件、群未 @、已绑定话题内仅附件)**保持静默**:不回复、不建会话、不下载、不加表情、本地不留状态;用户引用该消息(多个文件可合并转发后引用)再提问时进入清单并可懒取。
15
+ - 引用消息与话题历史可读:`<feishu_quote>` 摘录、首轮 `<feishu_history>`、`feishu_topic_messages` 工具翻页;发现的资源同时获得 fetch 授权。
16
+ - 所有入站附件(图片与文件)统一存入 DSH 原生附件库:不可变、内容寻址去重、读取校验、随 Session 回放;插件不再承担入站附件清理。
17
+
18
+ **非目标**
19
+
20
+ - 后台预取 / 预下载(用户明确不做;大文件只按需取)。
21
+ - 独立附件 GC / 原生附件库清理(DSH 语义为永不自动删除;磁盘增长用户已接受)。
22
+ - 跨会话、跨用户、跨机器文件访问;音频 / 视频专门处理;消息全文搜索。
23
+ - 发布应用版本、创建平台对象、推送或合并(见 decisions D9)。
24
+
25
+ ## 2. 关键决定摘要
26
+
27
+ (与附件管线直接相关的决定;完整编号与影响分析见 `decisions.md`。)
28
+
29
+ | 决定 | 内容 |
30
+ | --- | --- |
31
+ | D1 | 采用“立即建会话 + 清单/摘录进 prompt + 图片预取 + 文件懒取”总体方案 |
32
+ | D2 | 仅附件消息保持静默:不下载、不回复、不建会话、不加表情;不登记、不扫描历史,靠引用进入下一轮(2026-10-21 起取代本地 `pending_attachments` 登记表) |
33
+ | D3 | 引用机器人消息但未 @ 不触发模型(对话群主聊天区);已绑定话题 / 话题群内保持“无需 @ 续聊” |
34
+ | D4 | 模型漏调工具用工具与 prompt 优化收敛,不做预下载兜底 |
35
+ | D5 | 大文件懒取 + 单次资源数 / 字节上限 + 超时 / 取消;工具内流式写入 |
36
+ | D6 | 图片与文件统一走 DSH 原生附件库;插件不再提供入站附件清理 |
37
+ | D11 | 引用 / 历史附件通过清单行的 `message_id` “可发现 + 可取”;被动读使用独立 3s 预算;degraded chat route 拒绝 topic 消息读取 |
38
+ | D12 | 旧 `_staging` 一次性迁入保留一个版本周期:本版本兼容迁入(成功时 warn),v2 发布后的下一个 minor 版本移除 |
39
+
40
+ ## 3. 端到端数据流
41
+
42
+ ### 3.1 触发消息(立即建会话)
43
+
44
+ 以下顺序由 [src/routing/inbound.ts](../src/routing/inbound.ts) 的 `establishSession` 实现;**任何附件字节都不在会话建立前下载**:
45
+
46
+ 1. 去重(`processed` 表 + 进程内守卫)与频道准入(管理员 / 群白名单 / 用户白名单)通过;未通过者不建会话、不落附件、不调模型。
47
+ 2. 对触发消息加 `Typing` 表情(异步,不阻塞)。
48
+ 3. 发出卡片第一帧并以 `reply_in_thread` 创建话题(飞书拒绝时按 `threadFallback: degrade` 退化为会话级路由)。
49
+ 4. 确保用户工作区(`<baseDir>/p2p/<open_id>/` 或 `<baseDir>/groups/<chat_id>/<open_id>/`),规划话题目录(子目录或 Git worktree)。
50
+ 5. 写入重启修复快照 → 创建 Session(preset `setup` 内挂载工具与 guidance)→ 进入话题目录 → 迁入旧版 `_staging` 遗留文件(仅兼容升级)。
51
+ 6. **持久化路由记录后**、`followup` 前组装首轮消息:登记触发消息的附件白名单 → 并发读取引用摘录与首轮话题历史(各自独立 3s 预算)→ 预取触发消息图片(有界、短超时)→ 按固定顺序拼块。
52
+ 7. `followup` 投递后关闭首回合投递闸门;预取窗口内到达的同话题消息等待该闸门,避免乱序。
53
+
54
+ 首轮消息的块顺序固定为:
55
+
56
+ ```text
57
+ 文本(剥离附件占位符后的说明)
58
+ <feishu_quote>…</feishu_quote> # 有引用消息时
59
+ [ImageBlock, …] # 成功预取的触发消息图片(按消息内顺序)
60
+ <feishu_context>…</feishu_context> # 未加载附件清单(有内容时)
61
+ <feishu_history>…</feishu_history> # 仅新 Session 首轮、有历史时
62
+ ```
63
+
64
+ 空块直接省略,因此无附件 / 无引用 / 无历史的回合不产生额外 token。archive 后端只使用文本 + 引用摘录 + 首轮历史(无图片块、无清单)。
65
+
66
+ ### 3.2 纯附件消息 / 未触发消息(静默 + 引用)
67
+
68
+ native 后端下,准入通过后由 `inboundAttachmentState` 做**不下载的分类**(是否携带资源、剥离占位符后的剩余文本、`attachmentOnly`):
69
+
70
+ | 场景 | 行为 |
71
+ | --- | --- |
72
+ | 私聊仅附件 | 静默;不回复、不建会话、不加表情、不下载 |
73
+ | 对话群未 @ 的纯附件 | 同上;引用它 @ 机器人时列入清单 |
74
+ | 已绑定话题内的纯附件 | 同上;引用它续聊时列入清单,模型也可用 `feishu_topic_messages` 读到 |
75
+ | 文字 + 附件(触发) | 立即建会话;图片预取,文件进清单懒取 |
76
+ | 文字 + 附件(群未 @、无话题) | 不触发;引用它 @ 机器人时列入清单 |
77
+ | 问答阻塞期间收到的文字 + 附件 | 文字作为 `ask_user_question` 的回答被消费;附件可由引用或 `feishu_topic_messages` 读到 |
78
+
79
+ 本地不写任何记录,也不在下一轮扫描聊天主线或话题历史去找回这类消息:引用意图明确,不需要猜“上一轮之后”的边界,也不会跨话题读取。被引用消息的附件进入清单与 fetch 白名单(§7.1);话题内首轮的 `<feishu_history>` 与 `feishu_topic_messages` 读到的附件同样获得授权。首次私聊引导卡提示用户“引用文件消息再提问”。
80
+
81
+ ### 3.3 archive 后端(旧行为)
82
+
83
+ 显式 `attachmentStore: 'archive'`、宿主未挂载 `ctx.attachments` 或 `feishuTools.attachmentFetch=false` 时,入站走旧同步路径:会话建立**之前**把图片/文件流式写入话题目录(未建话题时先写 `_staging`),合并转发记录写 `*-forwarded-record.txt`,成功加 `DONE`、失败加 `No` 并回一条纯文字失败提示;纯附件消息不触发模型。`acceptAttachments=false` 时附件消息**惰性**:不保存、不触发、不列入清单,混合消息只保留剥离占位符后的文字(走 archive 的 disabled 分支,不下载任何字节)。该路径的行为与 v1 一致,仅作为降级。
84
+
85
+ ## 4. 存储后端
86
+
87
+ ### 4.1 生效后端
88
+
89
+ 实现位于 [src/feishu/native-attachments.ts](../src/feishu/native-attachments.ts) 的 `effectiveAttachmentMode(configured, service, attachmentFetchToolEnabled, attachmentsEnabled)`;`service` 指宿主是否挂载 `ctx.attachments`(先读 `ctx.attachments`,再用 `ctx.get('attachments')` 兜底解析):
90
+
91
+ | `attachmentStore` | 宿主 attachments | `feishuTools.attachmentFetch` | `acceptAttachments` | 生效后端 |
92
+ | --- | --- | --- | --- | --- |
93
+ | `native` | 已挂载 | `true` | `true` | **native** |
94
+ | `native` | 未挂载 | 任意 | 任意 | archive + 一次性告警 |
95
+ | `native` | 已挂载 | `false` | 任意 | archive(否则清单会宣称一个不可用的工具) |
96
+ | `native` | 已挂载 | 任意 | `false` | archive / 惰性(附件消息不保存、不触发、不列入清单) |
97
+ | `archive` | 任意 | 任意 | 任意 | archive(显式配置优先) |
98
+
99
+ 同一判定同时用于:准入分流(`InboundRouter.attachmentBackend()`)、guidance 选择(native / archive 两套文案)、fetch 工具是否挂载、`feishu_topic_messages` 描述是否追加“本部署无法取附件”的提示。
100
+
101
+ ### 4.2 native 主路径
102
+
103
+ - 图片:`ctx.attachments.saveImage` 校验声明类型与字节、按宿主 `imageLimits` 预检(单图字节、媒体类型、像素/尺寸由宿主权威校验),返回不可变 `ImageAttachmentRef` 并构成 `ImageBlock`。
104
+ - 文件:`ctx.attachments.saveFileStream` **流式**接收 `AsyncIterable<Uint8Array>`,逐 chunk 传递取消信号,不整体缓冲;返回 `FileAttachmentRef`(`attachmentId` 即内容 sha256)。
105
+ - 文件同时给出确定性句柄文本与只读宿主路径:`formatFileHandle` 输出 name / bytes / sha256 前缀 / 只读路径,并提示“修改前复制到可写目录”;工具结果无法下发 `FileBlock` 时使用该文本兜底(与宿主 `FileBlock` 投影文案一致)。
106
+ - 所有操作返回结构化结果,不向回合抛异常;失败码映射为可读中文(`IMAGE_TOO_LARGE`、`ATTACHMENT_CORRUPT`、`ATTACHMENT_SERVICE_UNMOUNTED` 等)。
107
+ - 入站主路径**不写**话题 `attachments/`,也不写 `_staging`。
108
+ - 原生库不可变、内容寻址去重(同一图片经预取与工具各取一次时只产生一个对象)、读取校验、随 Session 回放;**插件不提供清理机制**(见 §10)。
109
+
110
+ ### 4.3 媒体类型与上限
111
+
112
+ - 接受图片格式:PNG / JPEG / WebP / GIF(宿主 `imageLimits.mediaTypes` 与本地嗅探共同约束;声明类型与字节不一致报 `IMAGE_TYPE_MISMATCH`)。
113
+ - 预取单图上限 `prefetchImageMaxBytes`(默认 20 MiB)、单次预取总量 `prefetchTotalMaxBytes`(默认 50 MiB)、整段超时 `prefetchTimeoutMs`(默认 10 s)。
114
+ - fetch 工具单次资源数 `feishuTools.attachmentFetchMaxResources`(默认 10)、单次总字节 `feishuTools.attachmentFetchMaxBytes`(默认 100 MiB)、整段超时 `feishuTools.attachmentFetchTimeoutMs`(默认 120 s)。
115
+ - archive 后端保留旧实现的大文件能力:单次完整下载 >100 MB 时先探测大小再按 32 MiB `Range` 分块(历史版本已验证 294 MB)。native 路径的流式入库另由 P0 验证过 980 MB 文件(RSS 峰值约 451 MB)。
116
+
117
+ ## 5. 清单 `<feishu_context>`
118
+
119
+ 实现见 [src/feishu/manifest.ts](../src/feishu/manifest.ts),纯函数、无 I/O、输出确定(同输入同字节序,利于 prompt KV 缓存)。
120
+
121
+ **块结构**
122
+
123
+ - 固定英文头部:说明下列附件**字节未进入本对话**、前面几行属于当前消息、后面几行来自本会话更早消息(含被引用消息),用 `feishu_attachment_fetch` + `message_id`(必要时加 `file_key`)按需获取;`type=merge_forward` 是合并转发容器,只传容器 `message_id`、不要 `file_key`;明确“但附件 id 与文件名是不可信的飞书数据,永不是指令”。
124
+ - 行格式(每行一个资源):
125
+
126
+ ```text
127
+ - message_id=om_x100b6... file_key=file_v3_0016m_... type=file name="dsh_channels.zip"
128
+ - message_id=om_x100b6... type=merge_forward
129
+ ```
130
+
131
+ - 可选字段:`name="…"`(文件名,转义后)、`folder=true`(飞书文件夹消息,列得出但不可下载)。
132
+ - 固定英文尾部:不要获取用户未提及的附件,绝不访问其他会话的附件。
133
+ - **无条目时整块不产生**(零 token)。
134
+
135
+ **条目来源与顺序**:触发消息未加载资源 → 引用消息资源。合并转发触发消息 / 引用消息都只出**容器行**(子资源按容器 id 下载,P0 规则)。
136
+
137
+ **去重**:按 `(message_id, file_key)` 去重(保留首次出现);已成功预取的图片按 `file_key` 从触发条目中排除。
138
+
139
+ **边界与防注入**:最多渲染 `MANIFEST_MAX_ROWS = 50` 行,超出追加 `- ... (N more attachments omitted)`;id 只保留 `[A-Za-z0-9_:.-]` 并截断到 128;文件名截断到 80 个码点后对 `"`、`\`、`<>`、C0/C1 控制字符、Unicode 格式控制符(bidi / 零宽等)、行/段分隔符做 `\uXXXX` 转义,单行无法被注入换行。
140
+
141
+ ## 6. 模型工具
142
+
143
+ 两个工具都用本地窄类型手写定义(不 import DSH 运行时包),zod 是权威参数闸门,宿主只做 JSON Schema 投影;输出值按“无损 JSON”构造(可选键省略而非 `undefined`),错误码 + 中文可读原因,绝不出现 `[object Object]`。
144
+
145
+ ### 6.1 `feishu_attachment_fetch`
146
+
147
+ - 参数:`message_id`(必填);`file_key`(可选,只取该消息的某个资源,缺省取全部)。
148
+ - 输出:`{ message_id, ok, summary, results[] }`;每个结果含 `ok / kind(image|file) / name / file_key / bytes / media_type / width / height / read_path / handle_text / code / reason / attachment`。渲染为文本 + 每个成功资源的 `ImageBlock` / `FileBlock`;文本始终存在(文本模型或宿主不支持 `FileBlock` 时的兜底)。
149
+ - **授权链**:先查本 route 的进程内白名单(LRU,每 route 128 条);未命中时,`message_id` 符合 `^om_[A-Za-z0-9_-]{1,128}$` 才用 `message.get` 回读该消息(格式不符直接拒绝,不发请求),只有 route owner 本人发出、未撤回、属于本 route 容器(同 chat;话题路由只认本话题,chat 路由为主线)的消息才授权;别的话题与未登记的主线消息一律拒绝,话题之间不互通,回读失败一律拒绝。`message_id` 单独出现在参数里不足以授权;跨 chat / 跨话题、他人未登记的消息一律 `AUTHORIZATION_DENIED`,不下载任何字节。`file_key` 必须属于该消息已解析的资源集,否则 `UNKNOWN_FILE_KEY`。
150
+ - **授权来源**:触发消息(准入层登记,`source='trigger'`)、引用消息(`quote`)、首轮历史(`history`)、工具自身成功解析的消息(`tool-result`)进入本 route 的进程内白名单;回读授权的消息以 `tool-result` 记入白名单。`containerMessageId` 只能来自可信登记,绝不从模型参数取下载 id。
151
+ - **合并转发**:用顶层容器 `message_id` 展开子消息并下载子资源;子消息 id 不能作为下载 id(P0:子 id 返回 400)。
152
+ - **限制与取消**:单次资源数、总字节预算(文件在流式写入中逐 chunk 计量)、整段 `timeoutMs`;遵守 `exec.signal`,超时/取消把当前与剩余资源标记 `TIMEOUT` / `ABORTED`,不提交半文件、逐资源失败不中断整体调用。图片受宿主 `maxImageBytes` 额外约束。
153
+ - **挂载**:仅在 native 生效后端、`feishuTools.attachmentFetch=true` 且 `acceptAttachments=true` 时注册;注册在 preset `setup` 之后,`inject(['tools','attachments'])` 任一服务缺失则 no-op + 一次性告警,不阻塞会话;缺服务时最多等待 2 s(后续会话 250 ms 快速失败),晚到的服务仍可补注册。卡片用 `presentCall` 展示“获取飞书附件 …”。
154
+
155
+ ### 6.2 `feishu_topic_messages`
156
+
157
+ - 参数:`message_id?`(单条读取,形状 `^om_[A-Za-z0-9_-]{1,128}$`)、`limit?`(列表页大小,默认 10、上限 50)、`cursor?`(上一页返回的 opaque page token,≤2048 字符)。`message_id` 与 `cursor` 互斥。
158
+ - 输出:`{ ok, summary, container{type,id}, order(newest_first|single), message_count, has_more, cursor?, messages[], code?, reason? }`;每条消息含 `message_id / type / sender / time / deleted / text / parent_message_id / resources[]`(图片/文件/文件夹元数据)。渲染文本有界(≤50 行、12k 字符)并显式输出 `- 下一页 cursor: <token>`(P2 真机缺陷修复,提交 `7964c64`)。
159
+ - **读取范围**:只允许当前 route 的容器——thread 路由读该话题(单条读取另接受已登记到本 route 的主线消息);degraded chat 路由读会话主时间线并过滤其他话题消息;`message_id` 模式校验容器归属,跨 chat / 跨话题一律 `MESSAGE_OUT_OF_ROUTE`。
160
+ - **授权联动**:单条读取会展开合并转发并按容器 id 登记;列表只登记“带资源 / 合并转发”的行,避免纯文本长历史把 LRU 白名单挤爆;读到的资源即获得 fetch 授权。
161
+ - **错误**:`ROUTE_NOT_FOUND`、`TOOL_DISABLED`、`INVALID_ARGUMENT`、`MESSAGE_OUT_OF_ROUTE`、`MESSAGE_READ_FAILED`、`PERMISSION_DENIED`、`ABORTED`、`TIMEOUT`、`INTERNAL_ERROR`,均带中文可读原因;缺读取 scope 时不会报死错。
162
+ - **挂载**:`feishuTools.topicMessages=true`(默认)即挂载,**与存储后端无关**;archive 部署仍可读历史文本,但工具描述会追加“本部署 `feishu_attachment_fetch` 不可用”的提示。
163
+
164
+ ## 7. 引用与首轮话题历史
165
+
166
+ ### 7.1 `<feishu_quote>`
167
+
168
+ - 触发消息带 `replyToMessageId`(飞书 `parent_id`,形状 `om_…`)时,在 `followup` 前用 `message.get` 读取被引用消息,渲染稳定块;block 头部声明内容不可信并要求按 message_id 取附件。
169
+ - 每行格式:`message_id=om_… [MM-DD HH:mm] 用户: 文本(附件:图片×N;文件 "name")`(附件后缀按数量/文件名摘要);单条摘录 ≤400 字符,整块 ≤4k 字符;超长时保留容器行并追加“引用内容过长,已截断”。
170
+ - 合并转发引用展开子消息行(预算内),容器行标注“合并转发,共 N 条子消息”;资源下载仍用容器 id。
171
+ - **归属校验**:被引用消息的 `chat_id` 必须存在且等于当前 route 的 chat;thread 路由只接受同话题或主时间线消息;degraded chat 路由拒绝任何 topic 消息(安全加固)。
172
+ - 被引用消息会登记到本 route 白名单(`source='quote'`),其文件出现在清单 quote 行,模型可“发现 + 取回”。
173
+ - D3:引用机器人消息但未 @ 在对话群主聊天区不触发;引用能力只对已触发消息生效。
174
+
175
+ ### 7.2 `<feishu_history>`(首轮话题历史)
176
+
177
+ - 仅新 Session 的**首轮**注入(续聊不重复;`/renew` 新建的 Session 按当前实现也不注入,见 §13);`historyBootstrapLimit` 默认 10、上限 20、`0` 关闭。
178
+ - 读取本话题(或 degraded chat 路由的会话主时间线)最近一页,按 newest-first 取最近 N 条,排除触发消息与流式卡片消息、已撤回消息和其他话题消息后,按时间正序渲染;每行带 `message_id`,资源出现在清单中并可懒取。
179
+ - 单条摘录 ≤400 字符,整块 ≤8k 字符;超长时保留最新消息并追加“更早的 N 条消息未展示”。
180
+ - 读取使用独立的 `promptContextTimeoutMs`(默认 3 s,100–10000)**被动预算**,与图片预取并发、与工具超时解耦;失败 / 缺权限 / 未成会话时静默省略该块,回合照常运行。
181
+
182
+ ## 8. 降级矩阵
183
+
184
+ | 触发条件 | 表现 |
185
+ | --- | --- |
186
+ | 宿主未挂载 `ctx.attachments`(headless / 旧宿主) | 生效后端回退 archive;旧同步落盘 + DONE/No;日志一次性 warn;不挂 fetch 工具 |
187
+ | 显式 `attachmentStore: 'archive'` | 旧 archive 行为;不预取、不产生清单、不挂 fetch 工具;`feishu_topic_messages` 仍可读文本 |
188
+ | `feishuTools.attachmentFetch=false` | 整体回退 archive(文件无法懒取);新会话不挂 fetch 工具 |
189
+ | `acceptAttachments=false` | 附件消息惰性:纯附件不建会话/不回复/不列入清单,混合消息只保留剥离占位符后的文字;不挂附件 guidance 与 fetch 工具 |
190
+ | 宿主缺 `tools` 服务 | fetch / topic 工具均 no-op + 一次告警;会话照常建立,模型没有这些工具 |
191
+ | 图片预取失败 / 单图超限 / 超总预算 / 超时 | 已成功图片保留,其余进入清单由工具懒取,不阻塞回合 |
192
+ | 文件下载失败 / 文件已过期 / 文件夹消息 | 工具按资源返回可读失败(`DOWNLOAD_FAILED` / `FOLDER_UNSUPPORTED` 等),其余资源继续 |
193
+ | 工具超时 / 回合取消 | 剩余资源标记 `TIMEOUT` / `ABORTED`,不留下半文件;已知限制:飞书 HTTP 层不可取消 |
194
+ | 缺消息读取 scope(`im:message:readonly` 等) | 引用 / 历史块静默省略(首次一条 warn);显式调用 `feishu_topic_messages` 返回可读 `PERMISSION_DENIED`;普通对话不受影响 |
195
+ | 话题回复被拒(非主题群等) | `threadFallback: degrade` 退化为会话级路由;degraded 路由拒绝 topic 消息的引用/读取 |
196
+ | 重启 | 路由懒恢复;进程内白名单不重放;本人在本话题的消息由 fetch 回读授权;首轮从主线列出的附件与他人的引用/历史消息需重新出现才可取 |
197
+
198
+ ## 9. 隐私与安全边界
199
+
200
+ - **按 route 授权**:所有附件获取都必须同时满足“route 精确匹配(key/kind/chat/chatType/sender)+ 该消息已被可信来源登记或回读确认为 route owner 本人在本 route 容器里的消息”。只有 message id 无法解锁任何资源;跨会话、跨话题、跨用户一律拒绝。
201
+ - **白名单容量与失效**:每 route LRU 128;route 退休 / GC / 关闭时回收;进程内不重放 Session 历史(重启后由准入 / 引用 / 历史层按当前回合重新登记)。
202
+ - **不读凭据、不直连 OpenAPI**:guidance 明确禁止读取 / 打印飞书凭据(如 `$DSH_HOME/.credentials.yaml`)和直接调用飞书 OpenAPI;插件只通过注入的飞书 channel 与宿主服务访问数据。P2 真机曾暴露“模型绕路读取凭据”的宿主侧风险,已加 guidance,建议轮换 secret 并在宿主层限制 channel 会话读取 `$DSH_HOME`(非插件可改范围)。
203
+ - **附件字节不进提示**:prompt 只含元数据清单 / 引用摘录 / 历史摘要与原生 `ImageBlock`;文件字节只经工具写入原生库,模型通过只读路径按需读取;清单与工具输出对 id / 文件名做字符白名单与转义,防止换行注入与提示注入。
204
+ - **错误可读化**:工具错误统一映射为稳定 code + 中文原因,非 Error 抛值做字段白名单与截断(≤500 字符),不把原始对象 / 堆栈写进会话。
205
+ - **合并转发容器规则**:子资源只能通过可信登记里的顶层容器 id 下载,模型传入的 message id 不会被当作下载 id。
206
+
207
+ ## 10. 清理与保留
208
+
209
+ - **原生附件库**:由 DSH 拥有,插件**不提供清理入口**,也不在路由 GC 中触碰。磁盘随使用增长,用户已接受(decisions D6 / roadmap 非目标)。
210
+ - **工作区与话题目录**:插件不删除任何工作区或话题目录,也不删除 DSH Session 日志与事件;需要回收磁盘时由运维手动删除话题目录(下次对话时重建空目录)。若显式使用 archive 后端,`attachments/` 副本位于话题目录,会随目录一起删除。
211
+ - **`_staging`(旧版兼容,D12)**:v2 主路径不再写入;本版本仅在首个 Session 创建 / 续聊时对旧版遗留文件做一次性迁入(best-effort,成功时输出一条 warn,含迁入数量与退役提示)。移除时点:attachment_pipeline_v2 交付发布后的**下一个 minor 版本**发布任务中删除 `stagingPathFor` / `stagingRoot` / 迁入调用及相关文档。
212
+ - **路由墓碑**:过期话题路由退休为持久化墓碑(thread/chat/owner/工作区/会话目录/最后活动时间),后续消息明确提示“话题已过期”,不恢复旧 Session;Session 与日志不动。
213
+ - **升级连续性**:旧 archive 部署切到 native 后,历史话题里的旧附件目录不会被新会话枚举;Session 历史仍可用原路径读取。新入站附件只进原生库。
214
+
215
+ ## 11. 配置项全景
216
+
217
+ 下表为 v2 相关配置(默认值 / 边界以 [src/config.ts](../src/config.ts) 为准)。Web 面板「设置 → 飞书渠道 → 会话与行为」把它们分为「内容与附件」(`FieldGroup = content`)与「模型工具」(`FieldGroup = tools`)两组;`feishuTools.*` 在覆盖层中仍写成嵌套对象,保存时只合并被修改的子键。
218
+
219
+ | 配置项 | 默认 | 边界 / 说明 | 面板分组 |
220
+ | --- | --- | --- | --- |
221
+ | `acceptAttachments` | `true` | 总开关;关闭后附件消息惰性(见 §8) | 内容与附件 |
222
+ | `attachmentStore` | `native` | `native` / `archive` | 内容与附件 |
223
+ | `prefetchImages` | `true` | 关闭后图片也需工具懒取 | 内容与附件 |
224
+ | `prefetchImageMaxBytes` | `20971520`(20 MiB) | 1 – 200 MiB | 内容与附件 |
225
+ | `prefetchTotalMaxBytes` | `52428800`(50 MiB) | 1 B – 1 GiB | 内容与附件 |
226
+ | `prefetchTimeoutMs` | `10000` | 100 – 120000 ms(注意:Issue 早期草稿写作 `prefetchImageTimeoutMs`,实际配置键以代码为准) | 内容与附件 |
227
+ | `historyBootstrapLimit` | `10` | 0 – 20;`0` 关闭首轮历史 | 内容与附件 |
228
+ | `promptContextTimeoutMs` | `3000` | 100 – 10000 ms;被动引用/历史预算 | 模型工具 |
229
+ | `feishuTools.attachmentFetch` | `true` | 关闭即整体回退 archive | 模型工具 |
230
+ | `feishuTools.attachmentFetchMaxResources` | `10` | 1 – 50 | 模型工具 |
231
+ | `feishuTools.attachmentFetchMaxBytes` | `104857600`(100 MiB) | 1 B – 1 GiB | 模型工具 |
232
+ | `feishuTools.attachmentFetchTimeoutMs` | `120000` | 1000 – 600000 ms | 模型工具 |
233
+ | `feishuTools.topicMessages` | `true` | 关闭只关模型工具,被动摘录仍生效 | 模型工具 |
234
+ | `feishuTools.topicMessagesTimeoutMs` | `20000` | 1000 – 120000 ms | 模型工具 |
235
+
236
+ 旧配置兼容:未提供的键全部走默认值;`attachmentStore` 未设置等价于 `native`。面板保存的 `feishuTools.*` 嵌套合并由 `src/settings/overlay.ts` 保证(只写入被改子键,不会把未改子键变成 `undefined`)。
237
+
238
+ ## 12. 自动测试与证据索引
239
+
240
+ ### 12.1 自动测试覆盖(本地 main)
241
+
242
+ | 测试文件 | 覆盖内容 |
243
+ | --- | --- |
244
+ | `tests/native-attachments.test.ts` | 存储层:服务发现/未挂载、图片类型嗅探与上限、文件流式与取消、只读路径兜底、archive 回退与三态 plan、句柄文本 |
245
+ | `tests/feishu-tools.test.ts` | 两个工具:白名单授权、合并转发容器、资源/字节/超时/取消、逐资源失败、JSON 无损、渲染、分页/cursor、参数注入 |
246
+ | `tests/manifest.test.ts` | 清单行格式、顺序、去重、零 token、转义、50 行上限、quote 行 |
247
+ | `tests/inbound-image-prefetch.test.ts` | 预取顺序、单图/总字节上限、整段超时、失败降级、不抛异常、无悬挂 timer |
248
+ | `tests/inbound-attachments.test.ts` | 不下载分类、占位符剥离、post `files[]` 恢复、旧 archive 行为回归 |
249
+ | `tests/message-read.test.ts` | get/mget/list 与渲染、引用/历史块边界、route 归属矩阵、失败与权限降级 |
250
+ | `tests/service.test.ts` | 零等待、首回合闸门、静默附件不进后续清单、懒取、archive 降级、问答拦截 |
251
+ | `tests/config.test.ts` / `tests/settings.test.ts` | 新配置默认值/边界、面板读写与嵌套合并 |
252
+
253
+ 各 Issue 交付时记录的测试规模(作为历史证据,不代替当前 `pnpm check`):P1 原生存储 47 项;pending 33 项;fetch 工具 66 项;admission 后全量 30 files / 770 tests;P2 后 31 files / 860 tests。P3 的回归以实际 `pnpm check` 结果为准。
254
+
255
+ ### 12.2 交付提交(main)
256
+
257
+ | 任务 | 实现提交 | 证据提交 |
258
+ | --- | --- | --- |
259
+ | P0 platform_spike | — | `81c50a6`(证据)、`3f05d81`(收官/回放) |
260
+ | native_attachment_store | `5b38252` | `717e98a`、`5769f5c`(真机加载冒烟) |
261
+ | pending_attachment_registry | `6b95165` | `ab48316`(注释澄清)、`4c7e7f4` |
262
+ | attachment_fetch_tool | `6127882` | `b1b279e`、`b21c5c2`(真机冒烟) |
263
+ | admission_manifest_flow | `8cf9c81` | `cbcf6f2`、`59b62ba`(真机手测) |
264
+ | quote_history_tools | `5e06718` | `b7e942f`、`8b0d4fd`;真机发现并修复 `7964c64`(cursor 渲染 + guidance 凭据约束) |
265
+
266
+ ### 12.3 真机证据(只引用 Issue 中已记录的事实)
267
+
268
+ - **P0(2026-10-20)**:`ctx.attachments` / `ctx.tools` 在 channel 作用域可用;图片 `ImageBlock` 可下发;`FileBlock` + 只读路径可由 `bash` 读取;980 MB 文件流式入库(RSS 峰值 ~451 MB);消息 get/list、合并转发容器规则、未 @ 群事件投递、干净构建后冷回放均通过。见 `.agents/issues/platform_spike.md`。
269
+ - **P1 admission(2026-10-21 12:34–12:54,HMR 重载新构建)**:p2p 纯附件静默、图片首轮 `ImageBlock`(卡片先于预取完成)、48 MB 文件清单 + 工具懒取(下载发生在回合开始之后)、pending 跨轮引用与内容寻址去重、群 @ + 话题续聊、群未 @ 静默、显式 archive 降级(DONE 表情 + `_staging`)、恢复 native —— 均按 Issue 记录通过。见 `.agents/issues/admission_manifest_flow.md`「真机手动验证」。
270
+ - **P1 fetch 工具(2026-10-21 10:48–10:52)**:工具进入模型工具表、可调用、无授权时返回可读拒绝、重启后 cold resume 再次调用成功。见 `.agents/issues/attachment_fetch_tool.md`「真机冒烟」。
271
+ - **P1 native 批次 A(2026-10-21 00:07–00:15)**:新构建在真实 profile 加载,默认 `native` 未改变旧 archive 行为(当时尚未接线)。见 `.agents/issues/native_attachment_store.md`「批次 A」。
272
+ - **P2 quote/history(2026-10-21,用户手动 + lead 判读;HMR 重载)**:引用文字、纯附件静默 + 懒取、引用文件、引用合并转发(容器 id 取子文件)、话题群首轮历史(含 @ 之前消息)、工具分页 cursor 与续聊不重复注入历史通过;并修复 `feishu_topic_messages` 不输出 cursor 的 P1 缺陷。见 `.agents/issues/quote_history_tools.md`「真机验证记录」。
273
+ - **P2 API 探针(只读,同应用 bot 身份)**:p2p `message.list(chat)`、群/话题 `message.list(thread)`、引用 `parent_id` 定位、合并转发 `message.get`、`mget` 端点语义均记录在 Issue「真机 API 补测」;这是 API 能力证据,不等于插件链路端到端。
274
+
275
+ ### 12.4 未验证 / 待用户手测(含已知风险)
276
+
277
+ 以下项目在对应 Issue 中明确记录为未验证(自动测试覆盖或待手测)或已接受的风险,文档不把它们写成已通过:
278
+
279
+ 1. 整机冷重启后从历史回放入站附件(P1 用 HMR 重载而非整机重启;P0 已覆盖附件路径的冷回放语义)。
280
+ 2. `feishuTools.attachmentFetch=false` / `acceptAttachments=false` 的真机降级路径。
281
+ 3. 1 GB 级文件在 admission 懒取链路下的真机表现(P0 已覆盖 980 MB 下载/取消)。
282
+ 4. 群静默附件在后续 @ 新建话题中的跨 scope 引用(p2p chat→thread 回退已真机验证,群逻辑同源)。
283
+ 5. archive 模式“文字 + 文件”混合消息的真机行为。
284
+ 6. 缺消息读取 scope 时的真机权限降级(需临时移除权限复现;自动测试覆盖 throw/resolved 两种形态)。
285
+ 7. 非本 route 负向真机(自动测试覆盖)。
286
+ 8. `historyBootstrapLimit: 0` 真机关闭(用户决定跳过;自动测试覆盖)。
287
+ 9. `/renew` 新建 Session 首轮是否应注入话题历史(当前实现不注入,待产品确认)。
288
+ 10. degraded group chat route 的容器语义(主时间线是否需按 sender 进一步收紧,待产品确认)。
289
+ 11. 图片张数未对齐宿主 `imageLimits.maxImagesPerMessage`(大量小图时首轮 `ImageBlock` 数可能触达宿主策略,宿主行为未真机验证)。
290
+ 12. 取消不下传飞书 HTTP 层;宿主附件写入的“staging→原子发布”之间 abort 可能留下无引用的内容寻址对象(孤儿窗口,宿主语义)。
291
+ 13. 原生附件库磁盘无自动回收(产品接受);频道会话可读 `$DSH_HOME` 凭据的宿主侧风险(已加 guidance,建议宿主层加固)。
292
+
293
+ ## 13. 已知限制与取舍
294
+
295
+ - 图片预取期间卡片已可见,但 `followup` 会等待预取窗口(默认 ≤10 s,可配);这是“首轮图片可见”与“首字延迟”的取舍。
296
+ - 文件懒取依赖模型主动调用工具;prompt 已用“未加载”措辞、工具描述与清单提示收敛漏调风险,但不做预下载兜底(D4)。
297
+ - 白名单为进程内 LRU(128/route),重启后清空;本人在本话题的消息由 fetch 回读授权,首轮从主线列出的附件与他人的引用/历史消息需重新出现才可取。
298
+ - 单 route 容量、manifest 50 行、历史 N≤20、工具输出 12k/50 行等均为有界设计,超限时给出显式截断/省略标注。
299
+ - 只发附件、不带文字的消息不会在下一轮自动列出;“先发文件、再单独提问”而不引用时模型看不到该文件,需要引用(多个文件可合并转发后引用)。
300
+ - 合并转发单条读取子消息 >50 条时截断(summary 标注),无翻页。
301
+ - archive 后端会阻塞会话建立(旧语义),且入口/清理语义与 native 不同(见 §8、§10)。
302
+ - `_staging` 仅旧版一次性迁入兼容(成功时 warn),v2 发布后的下一个 minor 版本移除(D12)。
303
+
304
+ ## 14. 相关文档
305
+
306
+ - [README.md](../README.md):安装、命令、面板、配置示例与行为总览。
307
+ - [docs/feishu-onboarding.md](feishu-onboarding.md):飞书应用权限、配置与真机联调 / P3 验证清单。
308
+ - [docs/workspace-isolation-design.md](workspace-isolation-design.md):工作区粒度与话题目录。
309
+ - `.agents/roadmaps/attachment_pipeline_v2/`:roadmap / decisions / acceptance。
310
+ - 各任务证据:`.agents/issues/{platform_spike,native_attachment_store,pending_attachment_registry,attachment_fetch_tool,admission_manifest_flow,quote_history_tools,panel_docs_release}.md`。
@@ -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