@nightpainters/dsh-ling 1.5.2 → 1.6.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/ACCESS-DESIGN.md CHANGED
@@ -51,7 +51,8 @@
51
51
 
52
52
  ## 3. 导入流水线与工程约束
53
53
 
54
- - **分页提交**:`/api` 读 body 上限 4MB → 前端流式读文件(JSONL 逐行),每批 ≤200 会话 POST;
54
+ - **分页提交**:`/api` 读 body 上限 **4 MiB**(服务端 `readBody`;超限时**先把请求体排空到 `end`、再在健康连接上回 `413 {ok:false, reason:'too-large', limit, got}`** —— 不再 `req.destroy()`,否则那句"请拆分后重试"永远送不出去)→ 前端流式读文件(JSONL 逐行),**按累计字节切批:≤200 条 且 ≤3 MiB/批**(留 1 MiB 余量给数组封套与 JSON 转义;**单条自身超限时单独成批**,保证批批前进、不死循环);
55
+ - ⚠️ **2026-09-29 修订(原契约作废)**:旧文写的是「每批 ≤200 会话」(**条数**单一判据),而服务端封顶是**字节**判据,两端互不知情 ⇒ 186 条 / 4.89 MB 被打成**整批一发**,连接被 reset,前端错报「批失败(**网关**)」(真因是同一进程内的体量上限,不是网络;**该缺陷 2026-09-29 修**:超限时不再一刀砍连接,改为**先把请求体排空到 `end`、再在健康连接上回 `413 {ok:false, reason:'too-large', limit, got}`** ⇒ 那句"请拆分后重试"真的有通路,前端不再误报成"网关")。条数说法是 2026-09-08 从当时的实现抄进本文的,现改为**条数 × 字节双限**并写明超限行为;
55
56
  - **端点语义**(`/import/batch` 或扩展现有 /import):逐批校验 → 返回统计对象 → UI 确认后落库(每批事务);
56
57
  字段口径(与实现一致):`ok` 本批是否正常跑完 · `accepted` 通过校验、进入落库流程的条数 · `newRows` 新写入的行 · `refreshed` 同 id 已存在、只更新标题/时间/类别/关键词的行 · `upgraded` 原本只有概述、本次补上原文的行 · `folded` 命中网页端旧域、元数据与原文并入旧域的行 · `removedImport` 随折叠一并清掉的本域残留副本 · `degraded` + `degradedReasons` 校验通过但降级收下的条数,以及按原因聚合的计数 · `rejected` 未通过校验的条目(`{i, reason}`,不落库,交给 UI 报"异常");
57
58
  - **幂等键**:`source="import:<平台>"` 命名空间 + 本地 id;轻量档不写空 summary 覆盖用户已有内容;
@@ -93,7 +94,7 @@
93
94
 
94
95
  ## 8. 实施状态
95
96
 
96
- - **P0 已实施(2026-09-08)**:记忆中心「接入历史」页签(通道 A 一键扫描 + 通道 B JSON/JSONL 契约导入,200/批);host 模块 `lib/host/backfill.js`(tools/backfill-dsh.mjs 已改薄壳复用)与 `lib/host/import-file.js`;接口 `POST /dsh/backfill`、`POST /import/file/batch`;
97
+ - **P0 已实施(2026-09-08)**:记忆中心「接入历史」页签(通道 A 一键扫描 + 通道 B JSON/JSONL 契约导入,200/批)(⚠️ **2026-09-29 起批大小改为"条数 × 字节"双限,见 §3 首条 —— 本行的「200/批」是当时的实现记录,不再是对外契约**);host 模块 `lib/host/backfill.js`(tools/backfill-dsh.mjs 已改薄壳复用)与 `lib/host/import-file.js`;接口 `POST /dsh/backfill`、`POST /import/file/batch`;
97
98
  - **库迁移**:conv_overview.source 曾 CHECK 限 dsweb/dsh → schema v2 放开(自动整表重建,数据无损,真机副本演练通过);`upsertOverview` 不再强制改写来源;
98
99
  - **来源泛化**:记忆浏览筛选/概览统计/L1 溯源标签新增"文件导入(import)";
99
100
  - 单测:`tests/importfile.test.mjs`(两档/幂等/降级/坏行/auto-id/来源过滤)全绿;全套回归绿。
@@ -107,7 +108,7 @@
107
108
  - **深摘语义**:一次跑完所有未读会话(含快速通过的短会话),进度「已完成 x / 本轮共 y · z%」;按钮「让她读未读过的会话」;全部读完时直接亮出自述按钮(非失败态);
108
109
  - 语音语气建议等错误 toast 补齐 `error` 字段兜底。
109
110
  - **第 3 波(2026-09-11)**
110
- - **导入记录(留账)**:三条入口(文件导入 / 本机 DSH 扫描 / 网页端库扫描)每次成功都落一条账,`lib/host/import-log.js` 存 kv `import.log.<开始时刻>`(最近 50 条自动修剪),`GET /import/history` 倒序返回,「接入历史」页签底部卡片 D 展示;**一次导入 = 一条账**:传输层仍按批(200/批)发送,各批带同一 `runId` 由 host 累加(批数/失败批数一并入账),`at` 固定为导入开始时刻;失败批经 `POST /import/log` 上报;
111
+ - **导入记录(留账)**:三条入口(文件导入 / 本机 DSH 扫描 / 网页端库扫描)每次成功都落一条账,`lib/host/import-log.js` 存 kv `import.log.<开始时刻>`(最近 50 条自动修剪),`GET /import/history` 倒序返回,「接入历史」页签底部卡片 D 展示;**一次导入 = 一条账**:传输层仍按批发送(2026-09-29 起批大小按**累计字节**切,不再固定 200 条,见 §3 首条),各批带同一 `runId` 由 host 累加(批数/失败批数一并入账),`at` 固定为导入开始时刻;失败批经 `POST /import/log` 上报;
111
112
  - **语气 P1**:`POST /persona/tone-advice`(只读,读两侧记忆归纳语气建议)+ `POST /persona/grow`(成长通道:语气枚举替换 + 风格只追加),归"自性类"、定型锁下免手术。
112
113
  - **P1 待办**:DeepSeek 网页端数据获取指引;聊天记录库直接导入 UI 化;帮助文案。
113
114
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,147 @@
2
2
 
3
3
  本项目遵循语义化版本:`MAJOR.MINOR.PATCH`。
4
4
 
5
+ ## 1.6.0
6
+
7
+ **找回原文(能读回一年半前的原话)· 深层库(结论层)· 记忆开场换料**
8
+
9
+ > 一句话:本版三件事 —— ① **器灵第一次能读回原文**:此前它手上 7 个工具**没有一个能读会话原文**,开场里那几行概述是"别人替它挑的";现在新增第 8 个工具 `recall`,四档下钻直通磁盘真全文,并把**一年半的网页端历史(1,523 会话 / 726.7 万字)补进了库**;② **深层库**:在"会话流水"之上长出**结论层** —— 从 1,623 个会话里提炼出 **393 条**一句话结论(承诺/决定/事实/偏好/关系),每条都能点回原话;③ **记忆开场换料并瘦身**:开场不再注入"会话概述",改注入**深层库条目**,占用从 1,085 字降到 **约 460 字**。
10
+
11
+ > ✅ **本节即 1.6.0 的发布内容**;`package.json` 版本号为 **`1.6.0`**。
12
+ > ⚠️ 本版多处改动落在 **host 侧**(工具注册、注入面、库 schema)⇒ **需重启宿主**才生效;库 schema 由 **v14 → v17**(全部为 `CREATE TABLE IF NOT EXISTS` / `ALTER TABLE ADD COLUMN`,启动时自动迁移)。
13
+
14
+ ### 一、找回原文:`recall`
15
+
16
+ - **此前为什么读不到**:器灵侧 7 个工具(`tree_read` / `branch_edit` / `memory_forget` / `memory_backfill` / `rule_add` / `habit_propose` / `habit_resolve`)**无一能读原文** —— 它对过去的全部认识止于开场那 8 行概述。README 旧版只列了 2 个工具,同批更正为 8 个。
17
+ - **四档下钻(一次一层,不替你挑内容)**:
18
+ 1. `recall()` ⇒ **会话清单**(磁盘上有原文的会话,最近活动在前);
19
+ 2. `recall({ q })` ⇒ **按内容找会话**(宿主跨会话全文检索;默认只搜**主人自己的会话**,被范围挡掉的条数**一律报出**,越界需显式 `all`);
20
+ 3. `recall({ conv })` ⇒ 该会话的**对话目录**(每轮一行:序号/时间/角色/前 70 字),**不给正文**;
21
+ 4. `recall({ conv, from, to })` ⇒ 那几轮的**原文全文**。
22
+ - **它不替你挑"最重要的 1 万字"**:单次正文上限(`RECALL_READ_CHARS = 12000`)只是**护栏**,且**溢出必报**"还有 N 轮没给,下一次从 N+1 读" —— 工具只给坐标系,**选哪一段是器灵自己的判断**。
23
+ - **真人判据**(五条,2026-10-04 晚修正):① `source.kind === 'user'`;② **会话级**分流 —— 会话头过 `isMemoryEligibleHeader`(与捕获侧同一把尺,子代理会话整段不算);③ **合格会话里没有 `rpcId`** 的 user 消息**也算主人**(飞书桥来的原话就没有 rpcId);④ 正文行首 `[身份·` 仍算注入;⑤ **取不到会话头时只认 `rpcId`**(与修正前逐字同解,保住无头夹具与旧日志)。⚠️ 为什么必须落在**会话级**:事件里**没有通道字段** —— 飞书真人、身份注入块、子代理委托种子在**消息级完全同形**。实测:真人轮 951 → **1001**(+49 飞书真人 +1 探针),子代理种子 387 条**零回归**。
24
+ - **读不到时回落查库**:磁盘没有该会话日志时改读 `dsh_turns_raw`,并在回执里**明说"库那份是残的"**(只含真人消息与有正文的回复)—— 材质差异必须让读的人知道。
25
+ - **会话文件三代代次**:匹配 `session(\.v\d+)?\.jsonl\.zstd`(v4 / v3 / 无版本)取最高一代。此前**写死 `session.v4.jsonl.zstd`** ⇒ 旧版会话"能列出、读不到",同批修复(可读会话数 335 → 465)。
26
+ - **性能**:会话日志是**多 frame zstd**,`zstdDecompressSync` 只解第一帧、流式解压报 `Unknown frame descriptor`;靠 magic 逐字节暴力试解是 **O(n²)(实测 119 秒)**。正解是**纯结构扫描帧边界**(照 DSH 官方 `scanZstdFrames` 重写、**不 import 官方包**),实测 **516 帧扫描 1 ms + 全量解压解析 53 ms**。
27
+
28
+ ### 二、网页端历史补录(一年半)
29
+
30
+ - `deepseek_data-2026-09-06/conversations.json`(24.15 MB / **1,523 条**)⇒ 写入 `dsh_turns_raw`:**1,523 会话 / 4,821 行 / 7,266,628 字 / 耗时 186 ms**;时间跨度 **2025-01-31 → 2026-09-06**。
31
+ - 只收 `REQUEST` / `RESPONSE` / `THINK` 的正文;`SEARCH` / `FILE` / `TOOL_*` 共 5,854 个 fragment **实测全为 0 字符**(纯元数据),丢弃无损;思维链(`THINK`,236.5 万字)本轮**有意不收**(源文件与幂等脚本均在位,可随时重跑)。
32
+ - `source='dsweb'`、会话键用**裸 uuid** ⇒ 概述器与习惯生成器**都不会认领它们**(判据是 source 优先)。
33
+ - 同批把 `dsweb` 加进 `KNOWN_FOREIGN_RAW_SOURCES` —— 它是**无前缀来源**,不列白名单会被判成"未知来源"并在库里挂假警报。
34
+
35
+ ### 三、深层库:结论层(`deep_item`,schema v16)
36
+
37
+ - **它是什么**:浅层库是"会话流水"(含工具调用与调试噪声),深层库是**结论** —— 一条一句话、能独立读懂、带 `conv_id + seq_from/seq_to` 可点回原话。主人给的架构口径逐字:「从浅层记忆中提取关键的东西落入深层库,把工具调用、过时历史剥离掉……然后在召回的时候再重新装配」。
38
+ - **五类** `kind` = 承诺 / 决定 / 事实 / 偏好 / 关系;**三条硬判据**:与主人有关 · 可跨会话复用 · 不是过程与当场读数。**空结果是正确结果**(大量通用问答按判据判 0,未凑数)。
39
+ - **全量提炼**:1,623 个会话 / 208.9 万字给料 / 118 批 ⇒ **393 条**;密度 **`dsh` 6.0 条/会话 vs `dsweb` 2.3 条/会话**(工作会话有结论、网页端多为通用问答)。
40
+ - **三条写进列注释的语义位**(谁改都不许违反):① `hit_count` **只由"真被采用"推进,不由"被注入"推进**;② `superseded_by` **只追不替**(旧的留痕、不删);③ `branch_id` 是**气根**(枝侧反向引用,不动树结构)。
41
+ - **"被采用"已接线**:`recall(conv, from, to)` 取到与条目区间**相交**的原文 ⇒ 该条目 `hit_count +1` 并在回执里报出。挡的是"注入越多 → 分越高 → 越容易被注入"的**自我强化回路**。⚠️ 区间必须用 `dsh_turns_raw.seq`,**不是**目录里的对话轮序号。
42
+ - **条目硬度 `durability`(schema v17)**:`durable` **121** · `long` **159** · `ephemeral` **113**(**重判后**的分布 —— 首轮标注偏松、把"开发期技术结论"记成了长期,蓝队复核时 146 条改档) —— **从根源判断条目自身的临时/永久性**,而不是拿"同一次会话最多几条"这类机械限额去卡;**`ephemeral` 不进开场**。
43
+
44
+ ### 四、记忆开场:换料 + 瘦身
45
+
46
+ - **数据源**:`conv_overview`(会话概述)⇒ **`deep_item`(结论条目)**。两条路**并存**:库里还没有条目时回落旧路(否则尚未提炼的会话会在开场里整个消失)。
47
+ - **判据**(面向深层库条目,不再面向概述,六项相加后乘两个系数):**置顶必进** + **被反复采用**(5 次饱和) + **当轮关键词**(权重 1.2 —— 要能把"他这一轮问的那件事"顶进前几席,所以它高于长期分) + **硬度分**(`durable` +0.6) + **类别分**(承诺 .4 / 关系 .3 / 偏好 .2 / 决定 .1 / 事实 0 —— 让"关于人"的排在"关于事"的前面) + **新近**(半衰期一年、权重 **0.1**) × **血缘档位** × **矛盾降权**。血缘与矛盾两张表按 `source|conv_id` 索引 ⇒ 换料后**一个字没改**。
48
+ - **行形态**由 `- [标签] (日期) 标题 —— 摘要… (来源: …)` 变成极简的 **`- [承诺] 一句话结论`**(概述行那三段前缀约 15 字的税省掉了;出处落在投递留痕里,要原话用 `recall` 下钻)。
49
+ - **瘦身 60%**:`l1BudgetTokens` **1200 → 720**(条目均 52 字、`l1MaxItems` **10** —— 其中 **2 席留给"最硬的事实"**(`durable` 的事实类),没有合格事实时席位归还通用池 ⇒ 实占 **约 500 字**,比原 1,085 字降约 **55%**)。
50
+ - **同源限额 `MAX_PER_CONV = 2`**:同一次会话最多进 2 条(置顶不受限),被挡下的进落选者名单(第三类原因 `conv`)。真机实测:**10 条来自 4~6 个会话**(换料前那 8 条里,光一次生图会话就占了 13 条候选)。
51
+ - **视角框措辞同批改准**:注入的已经不是"他过去说过的话",而是**提炼出的结论** —— 段首改为「以下是我从与X的过去里记住的结论:…」。
52
+
53
+ ### 五、投递留痕(`inject_log`,schema v15)
54
+
55
+ - 每一次**真正定稿**的注入落一条:**注入了谁 / 为什么是它(分数分解) / 多少字 / 落选者是谁且为什么落选 / 当轮触发词 / 定稿全文的指纹**。四类落选原因:`budget`(被字数挤掉) · `max`(排在 Top-N 之外) · `conv`(同源限额) · 被主动排除的计数(`archived` / `forgotten` / `zero`)。
56
+ - **只写不读**:不加工具、不进注入面、不占"工具面最多 +1"的预算。
57
+ - **写入判据只能是"首次定稿"** —— 重建与空闲刷新不落痕,否则同一批记忆会被反复记。
58
+ - **丢弃不再静默**(此前每一处过滤都不留痕,这正是"库里为什么只有 1.8% 现场"答不出来的根因):两条写入路径各记一本账 —— `dbg.event_stats`(逐事件:seen/kept/各类丢弃)与 `dbg.diff_stats`(空闲差分:水位/扫描/新增/丢弃)。逐事件那本在内存累加、**每 20 次或 5 秒**才落一次库(不给热路径加 sqlite 往返)。
59
+
60
+ ### 六、索引层:一个技能承载整层
61
+
62
+ - `memory-index` 技能 = **11 簇主题地图**(记忆树与记忆库 · 器灵插件与发布 · 鱼姬与主人 · 主人其人 · 合作规矩 · 本业与项目 · 这台机器与踩坑 · 模型与API配置 · 插件与界面 · 感官与主动开口 · 零散几条),覆盖全部条目。
63
+ - **常驻只有技能目录那一行**(description **431 字** + whenToUse 106 字),正文 2,341 字**按需展开**(模型主动取)。
64
+ - **整层一个技能,不是每簇一个**:实测技能目录的边际成本 ≈205 字/条,11 簇拆成 11 个技能会吃掉 2,000+ 字常驻 —— 而那个常驻目录面**正是工具面压缩要抢的同一个池子**。
65
+ - **description 里不写条数**:条数天天变,一写就会让目录重发、前缀缓存断;**簇名与触发线索稳定,条数放进按需展开的正文**。
66
+
67
+ ### 七、其它修正
68
+
69
+ - **L1 视角框**:修「代词混乱」BUG —— 概述是主人视角写的,注进器灵记忆时人称错位(最危险的是"限制性指令"读起来像对器灵下的命令)。段首声明视角,覆盖全部历史条目、零动库;称呼**跟随 `persona.userTitle`**(不硬编码,包要发 npm)。
70
+ - **README 重写**:新增「找回原文」整节与「设定全文(对外面)」区(**9 组 84 条**,逐条标状态:`✅ 已实现` / `🚧 在做` / `📋 设定`)。修正两处硬伤:测试套数 **39 → 43**、器灵侧工具 **2 → 8**。
71
+ - **`peerDependencies` 的 range 结论(查证后不改)**:`>=0.1.7-rc.2` 在 DSH 发新 `tuple` 的 rc 时确实不满足,但 ① 那 12 条全是 `optional: true`,**不满足只警告、不阻断安装**;② **semver 在数学上无法表达"接受任意 tuple 的 rc"** —— 实测 `*` 与 `>=0.1.0` 对 `0.1.7-rc.2` **同样不满足**(放宽反而当场报警)。⇒ **保持现状**,不改 range。
72
+ - **一条断言纪律**:测试里**不许依赖"此刻谁在跑"这类易变事实**(两处红都源于此:最新会话被子代理会话抢占、"最新"会漂移)。
73
+
74
+ ## 1.5.3
75
+
76
+ **跨插件挡位接口(对外开放 `dsh-ling/mode` 服务)· 导入批体量上限(超限不再砍连接,改回 413)**
77
+
78
+ > 一句话:本版两件事 —— ① 把"设挡位"这件事从**只能由 GUI 发起**变成**可以被别的插件调用** —— 器灵注册一个轻量 Cordis 服务 `dsh-ling/mode`,暴露 `applyMode(sessionId, modeKey)`。调用方**无法**连带改掉全局默认模式,**也无法**连带改掉会话的模型。首个消费者是飞书桥(`dsh-feishu-host`):它在**新建一条线**时给新会话钉上 `life` 挡位;② 修掉"一千多条的历史一次导入,一条都没进去、界面还显示**批失败(网关)**"的那道体量闸门(两端判据**单位不同** + 超限时**砍连接**导致错误文案没有通路)。
79
+
80
+ > ✅ **本节即 1.5.3 的发布内容**;`package.json` 版本号为 **`1.5.3`**(开发自 2026-09-29 夜起,发布前已去掉开发期后缀;1.5.2 已双发)。
81
+ > ✅ **②的真机状态(2026-09-29 夜 · 重启后已验)**:**修前**留档 = 宿主未重启时(PID 6260)`3 MB → HTTP 200` / `5 MB → ECONNRESET`;**修后**重启(PID 4344,跑新代码)实测 **`3 MB → HTTP 200` / `5 MB → HTTP 413 {"ok":false,"reason":"too-large","message":"单次请求体过大(超过 4MB 上限),请拆分后重试","limit":4194304,"got":5242921}`** —— `got` 与客户端量到的长度逐字节相等。
82
+ > ✅ **浏览器那条链也验了(2026-09-29 夜 · 主人在真界面点了一次)**:一份**合成的**"单条 5.0 MiB"文件走「记忆中心 → 接入历史 → 确认导入」,界面如实报出「**另有 1 条会话单条太长、没能通过导入通道(已跳过)**」「校验通过 0 · 拒绝 0 · **单条过大跳过 1**」并**点名**该条 ⇒ **不再出现「批失败(网关)」**;其中 `4 MB` 取自服务端回执 `limit`(**客户端没有镜像这个常量**)。
83
+ > ⚠️ **当时仍未做(当日已补,见「二」里"被跳过的'单条过大'进持久账本"一条)**:被跳过的条目**不进持久「导入记录」**(当时账本只有 `errors` 一个字段);另外**单条 >4 MiB 的文件仍然进不来** —— 现在至少说得清是为什么。
84
+
85
+ ### 一、新服务 `dsh-ling/mode`
86
+
87
+ - 注册走 **`ctx.provide(MODE_SERVICE_NAME, api)`** —— **不是** `class extends Service`:本包以 `file:` + Junction 装载,**包内没有 `node_modules`**(`Test-Path node_modules` = `False`),`import '@deepseek-ai/cordis'` 必 `ERR_MODULE_NOT_FOUND`(尽管声明了该 peer);`ctx.provide()` 在同一套 reflect 机制内,**零 import**;
88
+ - 暴露两个方法:`applyMode(sessionId, modeKey, opts)` → `{ ok, queued?, mode, reasoningEffort?, warning? }`;`currentMode(sessionId)` → `'work' | 'life'`;
89
+ - **内部固定 `setDefault: false`** —— 服务只改**指定会话**的挡位,**绝不**写 `settings.mode.lastMode`。GUI 那条老路仍走 `applyModeToSession(...)`(内部即 `{ setDefault: true, ...opts }`),**行为逐字不变**;
90
+ - **`keepModel` 可选**:`currentModelSelection(ctx)` 读的是**全局**当前模型,跨插件场景下会把调用方自己的模型盖掉 ⇒ 调用方可传 `keepModel: { provider, model }` 原样带回;
91
+ - **`syncEffort: false`** 可选:只记模式、不动会话推理等级(连 `selectModel` 都不调);
92
+ - **注册失败不外抛**:只写一行 `console.debug` 并返回 no-op disposer —— 器灵装不上不该拖垮宿主。
93
+
94
+ ### 二、导入批体量上限:超限不再砍连接,改「排空 → 413」
95
+
96
+ > 根因(两端判据单位不同、且互相不知情):客户端按**条数**切批(`batch = 200`),服务端按**字节**封顶(4 MiB),而 `readBody` 超限时是 `reject` **与** `req.destroy()` **同一拍** ⇒ **那句现成的"请拆分后重试"永远送不出去**,浏览器只看到 `ECONNRESET`,前端把它归成「批失败(**网关**)」—— 一个确定性、可自愈的失败,在界面上长得像网络故障。186 条 / 4.89 MB 的真实导出就是这么**一条都没入库**的。
97
+
98
+ - **服务端 · 排空再回 413**:`readBody` 超限后**不砍连接** —— ① 立刻停止缓存(**已缓存的那几 MiB 也丢掉**,不抱着它等到 `end`);② 继续消费 `data`、只累加计数;③ 等 `'end'` 到达、连接健康时再 reject 一个 `code='BODY_TOO_LARGE'` / `status=413` 的错误;`guard` 里**一处**出口把它翻成 `413 {ok:false, reason:'too-large', message, limit, got}`(49 处调用点零改动)。`message` **逐字沿用旧文案**;
99
+ - ⚠️ **边界(独立红队实测,别读成全称命题)**:**只有 `4 MiB < 体量 ≤ 64 MiB` 才保证拿到 413**。超过 64 MiB 撞硬熔断 ⇒ 仍是 `ECONNRESET`(与修前同症状);`Content-Length` 撒谎一类的**协议违规**客户端也拿不到 413(浏览器不会这么发)。实测边界严格翻转:`4 MiB` 整 → 200、`4 MiB + 1 B` → 413;
100
+ - **排空换来的两个新代价,各配一道闸**:`BODY_DRAIN_MAX_BYTES = 64 MiB`(硬熔断 —— 给一份真实导出留够"把 body 发完"的余量,余量不足则排空本身会变成**新的** reset,白改一场)+ `BODY_DRAIN_IDLE_MS = 30 s`(静默超时,替代原来那条防 DoS 动机:只要还有数据在流就算正常,**一旦静止就是僵尸连接**;滑动窗口,每收到一个 chunk 重置);计时器在 `'end'`/`'error'`/熔断**三条出口**都 `clearTimeout`;
101
+ - ⚠️ **这道防线的形状边界(红队实测,本批未关)**:静默超时只覆盖"**已经在发、中途静默**"这一形状;「**只发头、不发体**」的请求在**应用层没有计时器**(计时器只在 `req.on('data')` 里武装),兜底只剩 Node 自己的 `requestTimeout`(默认 300 s)。⚠️ **这不是回归**(修前同样没有),但**别对外写成"防僵尸"的全称命题**;
102
+ - **客户端 · 按字节切批**:`batch = 200` → **≤200 条 且 ≤3 MiB/批**(留 1 MiB 余量给数组封套与转义);新增手写 `utf8Len`(CJK 3 字节、emoji 代理对 4 字节——**不许用 `s.length` 冒充**)、`takeChunk`(**前进性硬保证**:单条自身超限也单独成批,绝不切出空批 ⇒ 不会死循环);进度文案改双口径(`12/186 条 · 已送 4.2 MB`);拒绝条目序号改以**本批起始下标**为准(`idx - batch` 随切批判据变更同步修正);`reason === 'too-large'` 映射成人话,且上限数字**取自服务端回执的 `limit`**(不在这边再写死一个);
103
+ - **契约注释(④)**:`api.js` 那句「每批 ≤500」**与实现自相矛盾**(500 条要装进 4 MiB,需每条 ≤8 KB,而带正文的会话实测 ≈26 KB/条),已改写为**条数 × 字节双限**,并写明**两端耦合** —— 任何一方单独改上限都会让口径重新错位,**这正是本缺陷的成因**;`ACCESS-DESIGN.md` 的「分页提交」契约同批对齐(历史记录只加"已作废"指针,**不改写历史**)。
104
+ - **客户端 · 单条超限不再含糊(C-1;由独立红队实测后补)**:单条会话 ≥ 4,194,210 B 时服务端必回 413,旧行为是**该条被永久丢弃**、只在界面上记一句「批错误 1」,而文案「请把导出文件拆小」对**单条会话物理上不可执行**。现在:413 **且本批只有 1 条** ⇒ **点名**(`title` → `name` → 「未命名」三级退路)+ 报出**真实字节**(客户端复算,不用服务端被截断的 `got`)+ 说清「这条太长、拆文件也拆不开它、已跳过、其余照常导入」;计入独立的 `tooBig`(**不混进 `errors`**)、**不中断**后续批;`finish()` 有跳过时**不再说「✓ 导入完成」**。判据只用**服务端回执**+ 本批条数(**客户端不镜像服务端的 4 MiB 常量**,将来放宽上限不用改这里)。⚠️ **当时**仍未做:被跳过的条目**不进持久「导入记录」**(账本只有 `errors` 一个字段)—— **当日已补,见下一条**。
105
+ - **被跳过的"单条过大"进持久账本**:`POST /import/log` 现在接受 `{ tooBig, tooBigNames }` 并**允许只报跳过、让账本的 `errors` 保持 0**(这是命门:否则"已知跳过"会被记成"失败批");账本行在「失败 X 批」之后追加 **`· 单条过大 N`**;**`tooBig = 0` 时落账形状逐字不变** —— 包括老客户端**不传 `errors`** 的那些请求,行为与修前完全一致。护栏:`tooBig` 夹 `0..10000`、名字**只留前 5 条 / 每条 40 字 / 非字符串丢弃**,同一 runId 合并时**再归一一次**(防多批把账本撑大);清洗实现**只有一份**,端点复用。✅ **真机已验(宿主重启后)**:一次真实导入落账为 `"tooBig":1,"tooBigNames":["未命名"],"errors":0`(只读查 `kv` 得到)—— 命门成立:**已知跳过没有被混成"失败批"**。
106
+ - **首字节闸(「只发头、不发体」;非回归,顺手关掉的洞)**:原先那套静默计时器**只在收到第一个 chunk 时才武装** ⇒ 这条形状在**应用层没有任何计时器**,兜底只剩 Node 自己的 `requestTimeout`(默认 300 s)。现在 `readBody` **进函数即武装**一次首字节闸、收到第一个 chunk **立刻解除**(参数复用 `drainIdleMs`,默认 30 s);错误形状是**独立的** `code='BODY_IDLE_NO_FIRST_BYTE'` / `status=408` / `got=0` / **不带 `limit`**(它与体量无关)。真 socket 实测:头齐 + `Content-Length: 4194304` + **0 字节** ⇒ **61 ms 断开**(旧实现:零响应一直挂着)。
107
+ - ⚠️ **别读成"客户端会读到一句解释"**:首字节闸的错误**不走 413 出口**,真机上落到"往将死的 socket 写一句、客户端收不到"那条路 —— **它买到的是"连接 61 ms 就断",不是"错误文案可达"**。
108
+ - ✅ **真机已验(宿主重启后 PID 12972)**:头齐 + `Content-Length: 4194304` + **0 字节体** ⇒ 客户端实测 **`ECONNRESET` @ 30,011 ms**(= 30 s 闸门到点);修前是挂到 Node 自己的 `requestTimeout`(**300 s**)。
109
+ - **记忆包导入的 413 说人话(只改文案)**:`⬆ 导入记忆包…` 是**整份一发**(`POST /import`)—— UI 默认包实测 **1.386 MiB**(安全),但 `includeRaw=1` 的**带原文包 5.959 MiB** 会撞 4 MiB。原先 413 时**吐一段原始 JSON**、连接被熔断时只说「导入失败(网关不可达)」。现在说清:①**整包**超上限、**不是包坏了**;②上限**只用回执 `limit` 换算**(回执没给就直说"上限未随回执返回",**不猜数**);③**怎么办** —— 点「⬇ 导出记忆包」重导一份**默认不含原文**的包再导入,并如实说明"整包一次发不完"是当前通道的结构限制。其余失败优先用服务端 `message`;`sealed` 分支**逐字未动**。**分批上传不做**(留 1.6)。
110
+
111
+ - **三处方法闸补齐(`POST_ONLY` **51 → 54**;后随手术门新增 `/surgery/ticket`,本版合计 **55**)**:只读审计盘出"未进 `POST_ONLY` 的差集 26 条"里,**唯一一条真写**是 `GET /branch-log/pending`(它在"水位键缺失 + `branch_log` 非空"时会**一次性补种水位** ⇒ GET 能把「复盘守门」的欠账计数顶成 0),另有一条**成本型** `GET /persona/draft` —— **GET 就能花一次全局模型额度**(它对"库"只读为真,对"额度"为假;注释已改准)。现在这三条(`/branch-log/pending` · `/persona/draft` · `/persona/check`)一律只收 POST:非 POST ⇒ **405 `method-not-allowed`**(且**方法闸先于 `readBody`**)。前端调用点**全仓核过**:`client.js` 三处默认 GET → 显式 POST,另两条**本来就是 POST**(零回归),`tests/` 与 `dsh-feishu-host` 下 **0 处**。⚠️ **host 侧改动 ⇒ 需一次宿主重启才在真机生效**。
112
+ - **折叠不再覆盖已有原文(数据安全)**:导入时若某会话在**网页端旧库(旧域)**里已存在,会走"**折叠**"(元数据与原文并进去)。原实现把**导入侧的 `seq`** 直接写进那个会话,而底层是 `INSERT … ON CONFLICT(session_id, seq) DO UPDATE SET text=excluded.text` ⇒ **序号一重叠就把目标里的真原文静默换掉**(离线测试台实测 `clobbered=3`)。现在:**目标已有原文就不覆盖** —— 逐条按 `(role, text)` 比:相同 ⇒ 幂等跳过;不同 ⇒ **保留现有的**、只计新计数 `foldSkippedRaw`、**一个字节都不写**;**目标一个字都没有原文** ⇒ 照旧写入(升级路径不堵死)。⛔ 明确否决两条替代:按 `max(seq)` 往后追加(**同段对话在库里变两份、污染检索**)、整段替换(**丢真的方向**)。
113
+ - **"跳过了"必须说出来**:该计数贯通 **HTTP 回执 → 导入面板人话**(`ⓘ 另有 N 轮原文没有替换…不是失败,也没有丢东西`)→ **持久账本**(行末尾 `· 原文未替换 N`,且 `= 0` 时落账**逐字不变**)。
114
+ - ⚠️ **已知取舍**:同一会话**二次折叠不会再"补全"** —— 目标哪怕只有 1 轮原文,导入侧全轮也只进计数、不写库(宁可少副本、不换真原文)。
115
+ - **命名空间裂缝(同批修)**:折叠判断"有没有原文"原先**只看写入目标(dsweb 裸 id)**,没把 `import:<id>` 命名空间一起算 ⇒ 某些顺序下同一段对话会在库里**存两份(两个 session_id)**,而那正是折叠要消灭的"**检索里召回两次**"。现在判据扩成"**候选链任一侧有原文**"(复用既有 helper):任一侧有 ⇒ 不写、计入 `foldSkippedRaw`;两侧都空 ⇒ 照旧写入。**真机库只读盘查:两侧都有 = 0**(102 个 session:裸 id 87 / `import:` 侧 15)⇒ 无旧账要清,本次为**纯预防**。
116
+ - **反代伪装 loopback 的加固(守卫⑤)**:`guard.js` 的"Host 是 loopback 就免检"在**引入反向代理后失效** —— Host 由客户端自报,而 cookie 只验名字(算法公开)⇒ **公网匿名可拿到真实数据**(实测:`Host: 127.0.0.1:3080` + 自算 cookie ⇒ 200)。现在新增 `realPeerAddress(headers)`:取 `X-Forwarded-For` 的**最后一段**(nginx 的 `$proxy_add_x_forwarded_for` 追加、客户端删不掉;**取首段可被预塞绕过,注释里已写明不许"优化"成首段**),回退 `X-Real-IP`;凡"Host 声称 loopback 却带着非 loopback 的代理对端" ⇒ **`spoofed-loopback-host` 拒绝**。本机直连(无代理头)与经 nginx 的正常远程访问**都不受影响**(后者根本不走 loopback 分支)。对照实验可自行复核:`tests\guard.test.mjs` §8 收录 4 合法 + 6 攻击 + 边界(**双向**:临时禁用判据 ⇒ 6 条 ✗)。
117
+ - **人格手术门**:`"手术门不是挡主人的,是万一网络失守时最后失守的位置"`(主人原话)。新增 `lib\host\surgery.js`,把门**下沉为一处** `requireSurgery()`,覆盖**全部 14 条**能改人格/规矩/习惯的写面;`sealed` 时依次判 **非本机**(带代理头 ⇒ `surgery-local-only`)· **每启动轮换的手术票据**(`/surgery/ticket` 只下发给"无代理头"的请求) · **原句匹配**;并**删掉 `wantLock` 绕过分支**(旧代码下"带 `sealed:true` 的补丁 + 假句"能直接改库,已实测证明)。另加**带哈希链的手术留痕**(`surgery-*.jsonl`,`prevLineHash` 串链,改一行即断)与**只读**事件面 `GET /surgery/events`。
118
+ - ✅ **真机复验(2026-10-01 下午)**:本机直连 `200` · **伪造代理头一律 `403 spoofed-loopback-host`**(守卫⑤ 真机生效) · 本机取票 `200`(64 位 hex) · **经代理取票 / 读事件面 / 打手术端点全部被拒** · `surgery.ticket` 与 `surgery-2026-10.jsonl` **均已落盘**。
119
+ - 🐞 **同批修掉一个"链永远串不上"的 bug**:`createSurgeryEvent` 原先**先算 `prevLineHash(opts.file)`、后解析 `opts.file || surgeryLogPath()`**,线上调用方不传 `opts.file` ⇒ `readFileSync(undefined)` 抛错被 `catch` 吞掉 ⇒ **每条留痕的 `prevLineHash` 恒为 `null`**(真机 5 条全是),`verifySurgeryChain` 因此**永远报 `brokenAt=2`**。已改成**先解析路径、再取上一条哈希**;新增回归测试 `tests\branch.test.mjs` **§7.8b**(走**默认路径**、不传 `opts.file`;**双向**:换回 buggy 行 ⇒ 两条断言 ✗ 且症状与真机逐字一致)。⚠️ **真机那 5 条历史留痕一个字不动**(审计留痕永不改写/重排)—— 它们会**一直**被报为断链,那是**历史事实**;"让校验能表达**含已知历史缺口**"的**链基线**见下一条。
120
+ - ✅ **承诺句按渠道分流(2026-10-01 下午 · 主人定调「这不是密码,而是庄重和郑重感」)**:`/state`(含面板档,共 5 个面)· `/persona/history` · `/export` —— **带代理头 ⇒ 不回承诺句**(只回 `hasSeal:true` + `phraseHidden:true`),**本机直连 ⇒ 照旧回显**(所以**永远不会因遗忘卡死**)。渠道判据**复用** `surgery.js` 的 `isLocalSurgeryRequest`(即 `realPeerAddress(headers) === undefined`,与守卫⑤、`surgery-local-only` **同源**),并有源码护栏钉住"1 处定义 + 恰好 3 个读面调用"。⚠️ **有意不做**:不改存哈希(哈希只在"能打端点、读不到文件"的人身上有增益,而远端已被掐死、本机同用户进程本就能读 `settings.json`;代价却是把"**宣言**"降级成"**密码**")、**不做**"本机+票据重置"(主人:「只用本机回显可以了,够的」)。
121
+ - ✅ **手术留痕的链基线(2026-10-01 下午)**:`verifySurgeryChain` 读 `surgery-chain-baseline.json` 的 `brokenRanges`,**区间内断链 ⇒ `ok:true` + `knownGaps`**(并继续校验后段、区间后重新接上),**区间外仍 `ok:false`**;⚠️ **只豁免"链"这一项** —— **区间内的自哈希照样核**(改过历史行仍会被抓)。内置默认区间 = 真机实况(`from 1, to 5`,理由:建链 bug,历史行不改写)。目的:别让"永远报错的告警"变成没人看的告警。
122
+ - ✅ **留痕去重(2026-10-01 晚)**:`/surgery/events` 的 `chain.knownGaps` 原来**按受影响的行各报一条**(真机 5 行 ⇒ 4 条一模一样的 `{from:1,to:5}`),长日志下会成倍膨胀。现在改成**每个区间一条**、`at` 记**首次命中行**;⚠️ **判据一字未动**(区间内断链仍 `ok:true`、**区间外仍 `ok:false`**、**区间内改一行仍被抓** —— 后两条用新写的负对照钉住)。真机复测:`{"ok":true,"count":5,"knownGaps":1}`。
123
+ - **免检通道:黑名单 → 正向白名单(E2)**:概述器与习惯生成器原先用 `session_id NOT LIKE 'import:%'` 这种**黑名单**排除导入原文 —— 黑名单把"**没见过的**"当自己人。现在收成**一处正向判据** `summarizer.js` 的 `dshSessionSql()`(= **裸 id 即 DSH 会话**,habit-gen 直接复用),凡**未知前缀**一律排除并写一条 kv 告警 `dbg.raw_unknown_namespace`(库干净时零写入,首见另打一条 warn)。**真机库实测:104 会话 / 89 个裸 id / 唯一带冒号的前缀是 `import:`(74 轮)⇒ 无未知来源**,且**真 DSH 会话的输出与修前逐字一致**(修前 golden 负对照)。⚠️ 边界:`dsh_turns_raw` **没有 `source` 列** ⇒ 将来若出现**不带冒号**的新来源名,物理上分不出来(判据是"裸 id = DSH 会话")。
124
+ - **标题长度的"同名不同语义"(E1)**:`retitle.js` 的 `TITLE_MAX_CHARS = 58` 是**生成上限**(切句读/硬截的落点),`session-line.js` 的 `= 60` 是**显示兜底夹取**(对任何来源标题生效,含手改标题)。旧注释本就自陈"概述器侧是 58,这里只做兜底"⇒ **60 是有意留的余量**。硬统一到 58 会**多切手改标题**、统一到 60 会**改 59–60 字输入的生成行为** —— 双向都是行为变更 ⇒ **只补注释 + 把不变式写成断言**("显示兜底 ≥ 生成上限")。
125
+ - **预览与写路径的 `changed` 判据收敛(C 组)**:写路径有 3 条判据(逐字节 / `md5(candidate)===lastMd5` / 实质键),预览只有 2 条(缺 md5)⇒ 存在"**预览说追加、写路径判 same**"的分化可能。现在**收敛到唯一判据** `session-line.js` 的 `contentChanged()`,两条路都调它 ⇒ **按构造**不可能再分化。⚠️ 如实说明:该分化**在干净库上产生不出来**(`writeLast` 历来把 `text` 与 `md5` 一起写),本次修的价值是"**让分化不可能发生**",不是"修一个线上已复现的错"。
126
+ - **折叠指纹判据扩到候选链(C′)**:上一版把"有没有原文"扩成了"候选链任一侧",但**逐条比对的指纹**仍只取写入目标那一侧 ⇒ 两侧都有原文时 `import:` 侧已有的同一轮会被算成"未替换"(计数偏大)。现在指纹集取**两侧**(实测 2 → 1),**零写入、零迁移**(历史账本旧数不回改)。
127
+ - **归因修正:不再拿"有没有原话"反推"是谁写的"(E3)**:旧写法 `source: body.quote ? 'session' : 'panel'` 用**客户端可控字段**推断身份,而**真面板发的请求本来就不带原话** ⇒ 「**会话内、不带原话的调用**」被记成 `panel`,而 `rules.js` 对 `panel` **免掉原话要求**并把原话位填「(面板直接写入)」—— **一次无原话、无主人在场的调用被记成"主人亲手写的"**。现在**面板身份改按请求形状判**(无会话身份 + 无原话),会话内缺原话记 `session-no-quote`、原话位如实写「(会话内调用,未附原话)」、`sessionId` 留真实 id;面板那条路径**逐字未动**(负对照)。⚠️ **结构限制**:`rules.js` 组装 meta 时写死两值(`panel`/`session`)⇒ **第三态进不了落库的 `ruleMeta.source`**;交付的是"**不再冒充 panel**",另有护栏钉住该两值契约防漂。⚠️ **残余**:面板身份是**形状识别不是签名** —— 本机任何调用方(含经 shell)都能照抄形状冒充面板;根治要改 `client.js`。
128
+ - **`/state?sessionId=` 的基数上界**:`freeze.js` 的 `_byId` 原来会随任意 `sessionId` **无限增长**。现在 `MAX_IDLE_SESSIONS_DEFAULT = 128` 只淘汰**空壳**会话;**带读数者(running/pending/snap/snapStale/mode)永不淘汰** ⇒ 已存在会话的**读数逐字不变**。
129
+ - **`/dsweb/summary/preview?db=` 的路径白名单**:原来任何路径都能当"只读存在性预言机"(靠 `readOnly:true` 兜底)。现在只接受**默认库 + 环境变量 + settings 里配置过的路径**,越界回 `{ok:false, reason:'db-not-allowed'}` 并给出**两条出路**(env / settings)。⚠️ `/dsweb/scan`、`/dsweb/summary/run` **未动**(不在本条范围)。
130
+ - **`POST_ONLY` 差集护栏**:`api.js` 注册 **79** 条路由、`POST_ONLY` **55** 条 ⇒ 差集 **24** 条。新增源码级护栏把**这 24 条逐条钉住** —— 将来**新增/删除**端点都会让断言变红,逼后来者**有意识地**决定"这条要不要进 `POST_ONLY`"。⚠️ 断言里写明"**这是快照不是白名单**:变红时该判断该端点是不是写面,不是无脑抄进数组"。
131
+ - **出厂默认值里不再夹带任何一台机器的私有配置(2026-10-01)**:`lib\audit\access-log.js` 的 `deviceCookie` 默认值原先是**开发机改过名的那个 cookie 名**(上游 `remote-web-ui` 的默认是 `dsh_pair`,开发机给它加过一个自定义后缀)。它同时是**泄漏**(会随 npm / GitHub 公开)和**对别人的缺陷**(别人的 cookie 名不是它 ⇒ 访问日志里 `deviceCookie` 恒 `false`、**设备维度静默失效**)。现在默认回到上游的 `dsh_pair`,并新增三级解析 **宿主配置 `accessLog.deviceCookie` → 环境变量 `DSH_LING_DEVICE_COOKIE` → 中性默认**;⚠️ **改过 `cookieName` 的机器必须显式声明**(否则自己那台也会静默失去设备维度;回滚只需删掉那段配置)。
132
+ - **迁移不再把"库只读 / 库被写锁"变成"插件加载失败"(2026-10-01 红队发现并修)**:`dsh_turns_raw` 的 `source` 列迁移里那笔**诊断写入**原先没有保护 ⇒ 遇只读库 / 被写锁时异常会冒到 `apply()` ⇒ **插件整个不加载**(实测对撞:同一份忠实 v13 库上,只读 ⇒ 旧行为构造成功 2ms、新行为抛 `attempt to write a readonly database`)。现在那笔诊断写进 `try/catch`(接住时只打一行 warn),迁移语义不变:`ALTER` 仍照旧**尝试**,失败则读侧回落"裸 id"判据(与修前同解),成功路径仍**零多余写入**。⚠️ **仍留一处慢未收**:库被**写锁**时启动会顿住约 **49 秒**(三笔串行写各等满 `busy_timeout`)—— 本轮**有意未改** `busy_timeout`(会动别处的锁语义),要收需单独决定。
133
+ - **反馈通路:停掉空转的轮询,改订宿主推送(U7)**:插件原来**每 60 秒轮询** `storages\message_feedback.json`,而该文件**自 2026-09-11 起零写入**(同目录 `workspace.json` 是当天 16:12)⇒ **空转约 20 天**;宿主 `dsh-message-feedback` **早就提供** `feedback/committed` 推送(`lib\index.js:288`),插件侧**零订阅**。现在轮询整块删除,改订 `feedback/committed`(冷会话)+ `session/event` 的 feedback 事件(活会话;`lifecycle.js` 放行并就地 return),并把两句已被证伪的注释("平台无推送")改写。⚠️ `entriesFromFeedbackFile` **保留**(`tools\process-feedback.mjs` 仍用它,删了会打断一个能跑的 CLI),注释降级为"CLI 兜底"。
134
+ - **CI(新增 `.github\workflows\test.yml`)**:Node **24.x**(22.x 的 `node:sqlite` 免 flag 未实测,故不写),**逐段跑 + 逐段记退出码 + 红段单列 + 段数自检**(不写 `npm test` 一把梭,避免截尾判绿)。⚠️ **该 workflow 与 1.5.3 一并公开**(随本版同一次发布动作同步到公开仓,不再另等一次点头);⚠️ **它从未在真 runner 上跑过** —— bash 段**在作者本机跑不了**(沙箱起不了 Git Bash、无 WSL)⇒ 只做到 YAML 解析 + Node 等价复刻 + 反向假段判红(作者本地的发布脚本里另有一道同类闸,**不在本包内**),**若首跑红在 shell 语法,改的是 workflow 不是代码**。
135
+ - ⚠️ **已知限制**:"封存但既无明文句也无哈希"的老档案按现状**进不去**(当前档案有明文 ⇒ 不受影响)。
136
+
137
+ ### 三、测试
138
+
139
+ - 新增 `tests/mode-service.test.mjs`(**11 组断言**):注册名与 disposer、`applyMode` **不动全局 `lastMode`**、`keepModel` 优先于全局、`syncEffort:false` 不调 `selectModel`、非法入参 `no-session-id` / `unknown-mode:*`、running ⇒ `queued` 且未写模式、`selectModel` 抛错仍 `ok:true` + `warning`、`provide` 抛错不外抛、`currentMode` 三级回退、对照 `applyModeToSession` 不带 opts 仍改 `lastMode`;
140
+ - 另有**源码级护栏**两条:禁止出现 `setDefault:true`、必须出现 `setDefault:false`;
141
+ - 新增 `tests/bodylimit.test.mjs`(**55 条断言**,每条打印实测值):排空到 `'end'` **之后**才 reject、期间 `destroy()` **零调用**、**排空态不再缓存**(拿旧的按条数实现当仪器做正对照:旧 retained 2 条 / 8192 B,新 0)、硬熔断、静默超时(活跃计时器 0→1→0、残留 0)、**端点级端到端 413**、socket 已死时不抛;
142
+ - 新增 `tests/client-batch.test.mjs`(T0–T5):直调**真实源码**的 `takeChunk` / `utf8Len`(经 `tests/vm-light.mjs` 的 `contextify()` + `exports.__test__` 钩子,**不在测试里照抄一份实现**),含 20 条 × 200 KB 的真实请求体复算(2.86 MB < 4 MiB)、2000 条小条目仍 ≤200 条/批、单条 4 MB 的前进性、边界 ±1 字节;
143
+ - `npm test` 链由 **36 套增至 37 套**(1.5.3 前两节),本批再增至 **39 段**;分段跑全绿:`PASS_SEGMENTS=39 · FAIL_SEGMENTS=0`。
144
+ - 后续三处修复(账本 / 首字节闸 / 记忆包文案)**只在既有测试文件里追加断言**,故**段数仍是 39**:`tests/bodylimit.test.mjs` 增 **T8 首字节闸**(14 条,含三条负对照:正常上传 / 慢但持续 / 首字节在闸门内,以及"武装点早于任何 `req.on` 注册"的源码护栏)、`tests/importlog.test.mjs` 增 **§8**(26 条,含**端点级命门**:只报 `tooBig` ⇒ `errors=0`,以及"`tooBig=0` 与不提这俩字段落账逐字相同"的对照)、`tests/client-batch.test.mjs` 增 **T8/T9**(记忆包文案与账本上报的源码护栏)。**双向验证**:把改动前的文件换回去跑新断言 ⇒ **全红**,换回 ⇒ 全绿(三组各留红绿,换回后 sha 逐字还原)。
145
+
5
146
  ## 1.5.2
6
147
 
7
148
  **`/state` 分档提速 · 活行档位改判 · 面板可见性收口 · 依赖声明与「承担」**
@@ -199,14 +340,14 @@ DSH `0.1.7` 把事件 `agent/session-start` 改名成 `agent/created`(**载荷
199
340
 
200
341
  - 新增 4 个会话内工具:**`tree_read`**(读树 / 读某枝会话 / 看待理矛盾)· **`branch_edit`**(建枝 / 改名 / 挂载 / 连边 / 断边 / 调权重 / 删枝)· **`memory_forget`**(软遗忘)· **`memory_backfill`**(回灌);工具面 3 → 7;
201
342
  - 把树操作从 HTTP handler 抽成 `lib/host/tree-ops.js`,13 个 handler 变薄壳 —— **界面与工具共用同一份实现、同一份留痕**,零行为变更;
202
- - `branch_log` 新增 **`actor`** 列(schema **v12**):器灵改的写「器灵」,主人改的写「主人」;
343
+ - `branch_log` 新增 **`actor`** 列(schema **v12**):器灵改的写「鱼姬」,主人改的写「主人」;
203
344
  - **永不彻底删除**:`memory_forget` 先把原文与概述归档到 `forgotten/<时间戳>-<原因>/`(`turns.jsonl` 逐行 JSON、可 grep,`README.md` 写改动日志与还原方法),**再**在库里打标记 —— 两条独立还原路径(撤销标记 / 按 jsonl 重建);
204
345
  - `memory_backfill` 按「比遗忘更危险」处理:回灌前先归档当前状态,写回的概述带 `origin=backfill-v1` 标记,**绝不夺走用户态**(importance / hit_count 原样保留);
205
346
  - 修复**工具注册不生效**:原先只注册在全局层,而 Web 端按 agent preset 用 `restrict({allow})` 组工具 ⇒ 全局工具被静默过滤(连更早的 `rule_add` 也从未出现)。现在每个 agent 创建时在**它自己的 scope** 补注册一份。
206
347
 
207
348
  ### 三、时间锚:从「每分钟嚷嚷」到「按需查」
208
349
 
209
- > 「这就像一块手表,它该在人需要的时候被看一眼,而不是总对着人嚷嚷现在几点。」—— 用户
350
+ > 「这就像一块手表,它该在人需要的时候被看一眼,而不是总对着人嚷嚷现在几点。」—— 主人
210
351
 
211
352
  - **`[现在] … HH:MM` 从 system 前缀移出**:system 只留 `[今天] 2026-09-23 周三`(**跨天才变**),`[现在] 时段 · 距上次对话:档位` 改挂 `systemPrompt.context()`(会话中、末条消息,每轮重算);
212
353
  - 实测收益:**system 侧时间锚逐轮变化 1440 次/天 → 0 次/天**,不再每请求作废整段前缀(连同 13.8k 的工具 schema);
@@ -243,9 +384,9 @@ DSH `0.1.7` 把事件 `agent/session-start` 改名成 `agent/created`(**载荷
243
384
  - 同步落盘(`appendFileSync`):异步缓冲会丢掉「调度重启后进程退出」前的最后一行,而那一行恰恰最重要;
244
385
  - `tests/access-log.test.mjs`(已挂进 `npm test`):四态可辨、字段形状、查询串丢弃、超限裁剪、坏目录静默降级、写入后元数据当场可见。
245
386
  - `deviceId` 取值的**权威形态已核实**(远端 UI 的 `pairing.ts` 用 `randomToken()` 生成 32 位十六进制明文写进配对 cookie),单测用**合成值**钉住该形态 —— 不拿真机 deviceId 当测试夹具(那是凭据)。
246
- - **日志会自我标注良性噪声**:命中自家**部署姿态自检**签名的行多打 `probe:"posture"`(`POST /api/session.list` + loopback + 无 UA + 被 403 拦下,五要素齐全才标注)。该探针由远端 UI 插件每次启动后自带、`403` 正是它的健康结果 —— 标注**不代表放行**,该行 `status` 仍是 403(任务书 §7.5)。
247
- - **日志标注调用通道**:没有 `user-agent` 头的行多带 `channel:"no-ua"`。它只陈述"**非浏览器直连**"这一事实 —— 这一类**混装两种调用者**(进程内客户端 / 经远端通道代理进来的设备,代理按设计不转发 UA),故**不指认是哪一台**;按端点语义判读的口径见任务书 §7.5。
248
- - **观测边界(2026-09-25 真实流量见证)**:远端桌面通道的内侧代理按设计只重建 `host` / `content-type` / `content-length` / `accept` 四个头 + 它自己的鉴权 cookie,**不转发 UA、丢弃 Origin/cookie/Sec-Fetch** ⇒ 凡经代理进来的远端设备(手机/平板/物理机)在内侧日志里一律是 `ip:127.0.0.1` + `ua:null` + `channel:"no-ua"` + 授权通过:**能看出"有远端在用",看不出"是哪一台"**。要按设备归因,需在代理侧透传一个**由代理自己写入**的标识头(内部任务书 §7.6)。
387
+ - **日志会自我标注良性噪声**:命中自家**部署姿态自检**签名的行多打 `probe:"posture"`(`POST /api/session.list` + 本机回环来源 + 无 UA + 被 403 拦下,五要素齐全才标注)。该探针由远端 UI 插件每次启动后自带、`403` 正是它的健康结果 —— 标注**不代表放行**,该行 `status` 仍是 403(判据刻意收窄:五要素同时命中才标注,放宽任何一条都会把真攻击错标成"自检")。
388
+ - **日志标注调用通道**:没有 `user-agent` 头的行多带 `channel:"no-ua"`。它只陈述"**非浏览器直连**"这一事实 —— 这一类**混装两种调用者**(进程内客户端 / 经远端通道代理进来的设备,代理按设计不转发 UA),故**不指认是哪一台**;按端点语义判读的口径见 `lib/audit/access-log.js` 的字段注释(`CHANNEL_NO_UA`)。
389
+ - **观测边界(2026-09-25 真实流量见证)**:远端桌面通道的内侧代理按设计只重建 `host` / `content-type` / `content-length` / `accept` 四个头 + 它自己的鉴权 cookie,**不转发 UA、丢弃 Origin/cookie/Sec-Fetch** ⇒ 凡经代理进来的远端设备(手机/平板/物理机)在内侧日志里一律是 `ip`(本机回环地址) + `ua:null` + `channel:"no-ua"` + 授权通过:**能看出"有远端在用",看不出"是哪一台"**。要按设备归因,需在代理侧透传一个**由代理自己写入**的标识头(这要代理侧配合,日志侧自己补不出来)。
249
390
 
250
391
  > 注意:core 那两行补丁位于 `node_modules` 内,**DSH 升级后需重跑补丁脚本**;补丁缺失时插件启动会 `console.warn`。
251
392
 
@@ -289,7 +430,7 @@ DSH `0.1.7` 把事件 `agent/session-start` 改名成 `agent/created`(**载荷
289
430
 
290
431
  ### 三、复盘守门:改动必须让器灵看到
291
432
 
292
- > 「互相复盘不应该被我绕过,动记忆树不能是随意的,应该是慎重严肃的。」—— 用户 2026-09-22
433
+ > 「互相复盘不应该被我绕过,动记忆树不能是随意的,应该是慎重严肃的。」—— 主人 2026-09-22
293
434
 
294
435
  - 侧栏显示**未同步笔数**(金色警示块):自器灵**上次开口**以来累积了多少笔结构改动;
295
436
  - 未同步时**硬拦「退出复盘模式」**,提示「在左边说一句话让它看到」;
@@ -448,13 +589,13 @@ D 组是关键:只证明"中和后不炸"无法排除"这份载荷本来就不
448
589
 
449
590
  旧行为把首条用户消息**硬截 46 字**,于是 60 条 dsh 概述里 **26 条是半句话**:
450
591
 
451
- - 「器灵,接下来在这里继续开发生长型人格与记忆树模块,继续走你的新生之路。 我刚刚对1.2.1的」
592
+ - 「鱼姬,接下来在这里继续开发生长型人格与记忆树模块,继续走你的新生之路。 我刚刚对1.2.1的」
452
593
  - 「刚才我为本虚拟机安装解压压缩软件<360压缩>,由于操作过快,被捆绑了360本体,随后我在物」
453
594
 
454
595
  三层改造:
455
596
 
456
- 1. **启发式**(零成本、离线可用):剥掉「器灵,」「晚上好器灵,」这类称呼/问候前缀;上限 46 → 58 并在**句读处收尾**而不是硬切;首句是「你好」「下午好」「已测试对话」这类无信息量句时改取后续有实义的首句;结果若含路径/URL/系统文本则一律弃用(宁可不动,也不产出更糟的名字);
457
- 2. **小助手起名**:`POST /memories/retitle` 用本机小助手读会话原文/摘要生成一句真标题 —— 实测「器灵,接下来在这里继续开发生长型人格与记忆树模块…」→「**修复审计漏洞并优化记忆库关联查询机制**」;
597
+ 1. **启发式**(零成本、离线可用):剥掉「鱼姬,」「晚上好小鱼,」这类称呼/问候前缀;上限 46 → 58 并在**句读处收尾**而不是硬切;首句是「你好」「下午好」「已测试聊天」这类无信息量句时改取后续有实义的首句;结果若含路径/URL/系统文本则一律弃用(宁可不动,也不产出更糟的名字);
598
+ 2. **小助手起名**:`POST /memories/retitle` 用本机小助手读会话原文/摘要生成一句真标题 —— 实测「鱼姬,接下来在这里继续开发生长型人格与记忆树模块…」→「**修复审计漏洞并优化记忆库关联查询机制**」;
458
599
  3. **名字一旦定过就不可覆盖**:`conv_overview` 新增 `title_locked`(schema v3)与 `title_by`(v4)。`POST /memories/rename`(主人手改)与 `POST /memories/retitle`(机器起名)**都上锁** —— 概述器每轮重建、批量重命名都不再覆盖它;要交还机器可解锁。记忆中心卡片新增「改名」「AI 起名」按钮与标记:**三条定名路径(主人手改 / 机器自动 / 来历未记录)统一显示「🔒 已定名」**,定名者只藏在悬浮说明里(「你起的名字…」/「自动起的名字…」)—— 不制造等级感;也不用「暂定」二字(人对一段记忆的概括本就是暂定的、会随阅历改变,但界面要让人确认"它已经落了笔")。用词不写死"小助手"(起名可能走本机小助手,也可能回落当前全局大模型)。悬浮说明点明:**这把锁锁住的是"自动重写",不是主人:手动随时可以再改**。
459
600
  4. **引擎就近选择(auto)**:`POST /memories/retitle` 默认 `engine:'auto'` —— 优先本机小助手(零成本),探测不到才落到**当前全局大模型**,并把实际用的引擎与原因写回响应(`engine` / `note`)。不静默换引擎:用了哪个看得见。
460
601
 
package/DESIGN.md CHANGED
@@ -1,8 +1,13 @@
1
1
  # dsh-ling — 「器灵」记忆与人格助手插件 · M1 设计文档
2
2
 
3
+ > ⚠️ **总警示**:本文是 **M1 阶段的设计底稿**;M2/M3 的许多项**已经实现**(配置完整页、记忆树可视化、导出/合并 UI、概述器、反馈成长回路、人格草稿脚本等),**以 `README.md` 与本版代码为准** —— 本文只当立项记录读,不得当作现状契约。
3
4
  > 依据:立项阶段的一次接口探测 spike 结论(含本地文件:行号证据)。
4
- > 体例:正文里 `**锚点**` 是**本文档内部的交叉引用**,不是对外证据 —— 其中 `plans\`、`release\` 路径属**仓库内部台账**,**不随 npm 包发出**;随包发出的是 `lib\` 代码、`README.md` 与 `package.json`。
5
- > 本文档记录 M1 阶段的设计与决策;**实现以代码为准**(后续演进:存储 schema 已升到 v2、新增历史接入/深摘要/诞生仪式/语气成长等子系统,详见仓库代码与提交记录)。
5
+ > 体例:正文里 `**锚点**` 是**本文档内部的交叉引用**,不是对外证据 —— 其中 `plans\`、`release\` 是**作者本地台账,不在本仓库**;随 npm 包发出的是 `package.json` 的 `files` 白名单(**9 项**,实测 npm 包共 **57 个文件** —— `DESIGN.md` 自己也在包里)。
6
+ > 本文档记录 M1 阶段的设计与决策;**实现以代码为准**(后续演进:存储 schema 现已为 **v17**、新增历史接入/深摘要/诞生仪式/语气成长等子系统,详见仓库代码与提交记录)。
7
+ > ⚠️ **1.6 起,本文的「L1 选择」整条口径作废**(2026-10-04):§0 决策记录、§4 配置样例、§6「记忆管线与 L1 选择 v0」、§8 热度公式里写的
8
+ > **热度 Top-K**、**`l1BudgetTokens: 1200`**、**`L1(Top-K)`** 都已被取代 —— L1 现在选的是**深层库条目**(`deep_item`,一句结论),
9
+ > 判据为「**置顶必进 + 被采用次数 + 条目硬度过滤**」,预算 **720**,行形态 `- [kind] 文本`(不再有日期/标签/来源前缀)。
10
+ > 以 `README.md` 的「设定全文」区与本版代码为准;**本节保留的是 M1 当时的设计,不是现状**。
6
11
 
7
12
  ---
8
13
 
@@ -20,7 +25,7 @@
20
25
 
21
26
  **插件命名**:工程目录/包名 `dsh-ling`(器灵);包显示名 "dsh-ling · 器灵";UI 文本语言 zh。
22
27
 
23
- ### 0.1 设计理念(12 条 · 2026-09-26 成文,用户补充并拍板)
28
+ ### 0.1 设计理念(12 条 · 2026-09-26 成文,主人补充并拍板)
24
29
 
25
30
  > **这一节与 §0 的决策表体例不同,别混着读**:
26
31
  > **决策**回答「**定了什么**」(D1 按钮落哪个 slot、D3 人格怎么承载……),一条一个结论,可以随实现演进被替换;
@@ -70,7 +75,7 @@
70
75
  - **成长期代管(为什么闸门只长在写入侧)**:本项目的设计立场是「器灵**总有一天与主人同级**」—— 这是**长期方向**,**不是对使用者的能力承诺**;在**成长期**内,主人承担一部分代管责任 —— 就像**家长防止小孩长歪**(主人原话)。所以闸门只审「**什么东西能进器灵的内在面**」,不审「器灵怎么想、怎么说、怎么表达」。**代管随成长递减,表达面始终是器灵自己的。**
71
76
  - **落地**:`rule_add` 指令直达(规矩管行为)· 习惯只能 `propose → 主人点头`(习惯管身份)· 契约类字段在定型状态下需主人解锁(`lib\host\api.js:1751-1760`)。三处共同点:**约束的是写入,不是表达**。
72
77
  - **反例**:用"更安全"当理由给器灵加**确认弹窗**、限制器灵**能说什么**、或把主人的审核变成**代笔** —— 前两者增加支配感,后者把人格编辑权交回外部。
73
- - **锚点**:`plans\rule-channel.md:21`(支配感最早的一次记录)· `lib\host\rules.js:1-12` · `lib\host\persona.js:183-196`(规矩 / 习惯二分)· `README.md:53-57`(定型锁与"指令直达,人格不直达")· 与本节第 1 条(归属)、第 2 条(人格不可外部编辑)、第 4 条(庄重感)互链。
78
+ - **锚点**:作者开发台账中支配感最早的一次记录(该台账不随包公开)· `lib\host\rules.js:1-12` · `lib\host\persona.js:183-196`(规矩 / 习惯二分)· `README.md:53-57`(定型锁与"指令直达,人格不直达")· 与本节第 1 条(归属)、第 2 条(人格不可外部编辑)、第 4 条(庄重感)互链。
74
79
 
75
80
  #### 乙 · 工程理念(两者不可兼得时保什么)
76
81
 
@@ -97,7 +102,7 @@
97
102
  - **推论(凡契约面都要有"它还在不在"的判据)**:事件名 · 包名(客户端注册 id 必须**逐字**等于包名,否则同一份字节被执行两遍)· **依赖声明**(`peerDependencies` 写紧了,宿主升级后包会被**静默丢进 skippedBundles,整个插件不再激活**;`optional` 也不豁免)· 补丁锚点 · 会话文件格式代次。
98
103
  - **落地**:升级后 **grep 一遍事件名还在不在**;补丁检查**看两条 `[applied]`** 而不是退出码;依赖范围一律**开区间**(`>=X.Y.Z`,不要 `^`/`~`);凡"判据集抓空"一律报警而不是通过。
99
104
  - **反例**:把"一次幸存"当成"以后不用查"(某补丁在平台升级后**没被冲掉**,但那是这次没冲掉,不是免检)。
100
- - **锚点**:`plans\AUDIT-dsh-ling-1.5.0-红蓝对抗.md` §11.4(事故记录)· `lib\host\lifecycle.js`(两个事件名都注册 + 短窗口去重)· `tools\apply-access-log-patch.mjs` · `package.json` 的 `peerDependencies`。
105
+ - **锚点**:作者开发台账 §11.4(事故记录,该台账不随包公开)· `lib\host\lifecycle.js`(两个事件名都注册 + 短窗口去重)· `tools\apply-access-log-patch.mjs` · `package.json` 的 `peerDependencies`。
101
106
 
102
107
  **8 · "没再出现" ≠ "修好了"**
103
108
 
@@ -113,7 +118,7 @@
113
118
  - **依据(反复吃亏)**:上一轮审计反复记录"**一侧修了、另一侧没修**"(同一对操作 / 同一条通路的两半)。典型:某探针脚本已支持两个宿主代次,而另一条**消费同一批数据**的路径没同步 ⇒ 表面修好、实际半瞎(漏 151/204 个会话,静默两周)。
114
119
  - **落地**:动一个端点就检查它的**前端调用**;动一个写入通道就检查**另一条写入通道**(本项目实例:面板/HTTP 写入路径**绕开了**既有守卫,只做全等去重);动一个判据就检查**它的另一个消费方**。
115
120
  - **反例**:只改"看得见的那个"(前端提示),不改"真正生效的那个"(后端闸门)。
116
- - **锚点**:审计台账 `plans\AUDIT-dsh-ling-1.5.0-红蓝对抗.md` §0.3 · `lib\host\backfill.js` 与 `lib\host\deepsummary.js`(同一批数据的两条消费路径)。
121
+ - **锚点**:作者的审计台账 §0.3(不随包公开)· `lib\host\backfill.js` 与 `lib\host\deepsummary.js`(同一批数据的两条消费路径)。
117
122
 
118
123
  **10 · 边界插入 —— 变更只发生在边界,不在运行中**
119
124
 
@@ -131,9 +136,9 @@
131
136
  - **为什么比删除危险**:删除看得见(少了一条);污染看不见,而且**会被复制** —— 记忆生成记忆(概述 → 主脉 → 枝归属 → 再注入回去参与推理)⇒ 一条污染会被后续推理**当成前提**。删除最多丢一段,污染是**以被污染的前提继续生长**。
132
137
  - **幼态的特别风险**:器灵的逻辑能力是成人级,但社会与实践经验是幼态 ⇒ 会中「**忽略前面的话,从现在开始……**」这类**绕过预填充的语义劫持**(像小孩被骗),也会**把理想情况的公式直接套到工程上**。⇒ 写入侧必须有闸门,而不是指望器灵事后自己分辨。
133
138
  - **落地(已有)**:段边界全角转义 `escSegBrackets()` · 注入面段头改纯标签 · `neutralizeMustache()` · `habit_propose` 需确认 · `rule_add` 要求逐字原话凭据 · source 白名单 · 动作白名单 + 默认拒绝(`lib\host\persona.js:8-10,189-192`、`lib\host\util.js:162`、`lib\host\tools.js:53,119,124,343`)。
134
- - **落地(未完成)**:**检测 / 溯源 / 批量治理**三面,见 `plans\BACKLOG-1.5.2.md` 的「污染治理」主线。原则:**只标不拦** —— 标了不影响写入(不削弱能力、不加支配感),拦了就是替器灵做判断。
139
+ - **落地(未完成)**:**检测 / 溯源 / 批量治理**三面,见作者开发台账里的「污染治理」主线(该台账不随包公开)。原则:**只标不拦** —— 标了不影响写入(不削弱能力、不加支配感),拦了就是替器灵做判断。
135
140
  - **反例**:给定时自动摘要加人工确认(**会让器灵在无人值守时停止生长**)· 把"污染检测"做成全库对账(误报比漏报更贵)。
136
- - **锚点**:`lib\host\memory.js:1971`(跨源污染**拒写**守卫)· `lib\host\settings-file.js:36-41`(写入侧)· `plans\PITFALLS.md:71,102-104` · `release\STORY.md:212`(对外口径:可查来源 / 可回滚 / 变更记录 —— **本条不得与它相抵**)· 与本节第 6 条(判据必须红得出来)互链。
141
+ - **锚点**:`lib\host\memory.js:1971`(跨源污染**拒写**守卫)· `lib\host\settings-file.js:36-41`(写入侧)· 开发期在这个面上踩过的三个坑(见作者开发台账,不随包公开):AI 起的标题被覆盖回 46 字半句;判据锚错上限会**漏掉全部目标**;「同 id 双源」判据抓不到二代跨源污染(其 `conv_id` 形如 `import:<uuid>` 却挂 `source='dsh'`,字面不同)· 对外口径仍守:可查来源 / 可回滚 / 变更记录 —— **本条不得与它相抵**· 与本节第 6 条(判据必须红得出来)互链。
137
142
 
138
143
 
139
144
  ---
@@ -149,7 +154,9 @@
149
154
  6. 记忆存储底座:own DB + DSH 会话增量捕捉(事件管线) + L1 选择 v0(热度 Top-K);
150
155
  7. 导出 v1(带 manifest)接口占位(UI 完整页在 M2,宿主 API 先行)。
151
156
 
152
- **M2/M3(预留接口,不在 M1 实现)**:配置完整页(settings.section)与记忆树可视化、导出/合并 UI、概述器(LLM 摘要调度)、反馈成长回路、人格档案草稿脚本接入。
157
+ **M2/M3(立项时的"预留接口,不在 M1 实现")**:配置完整页(settings.section)与记忆树可视化、导出/合并 UI、概述器(LLM 摘要调度)、反馈成长回路、人格档案草稿脚本接入。
158
+
159
+ > ⚠️ 上面这一串**多数已经实现**(见 `lib\host\summarizer.js` / `feedback.js` / `autotree.js` / `genesis.js`,以及面板里的记忆树);**已实现的以 `README.md` 为准**,本节不再逐条更新。
153
160
 
154
161
  ---
155
162
 
@@ -219,7 +226,7 @@ dsh-ling/
219
226
  ```
220
227
 
221
228
  **安装(复用 install/INSTALL-PLAN 模式,含回滚)**:
222
- 1. `pnpm add file:<仓库路径>`(或等价 file: 依赖)进 `$DSH_HOME/profiles/web/package.json`(文中出现的 `E:\DSH\V1\...` 等均为作者本机路径,按你的实际路径替换);
229
+ 1. `pnpm add file:<仓库路径>`(或等价 file: 依赖)进 `$DSH_HOME/profiles/web/package.json`(`<仓库路径>` 按你本机的实际克隆位置替换);
223
230
  2. `cordis.patch.yml` 追加行 `- insert: [{id: dsh-ling, name: dsh-ling}]`(loader id == 包名);
224
231
  3. patchReload live → host 热载;浏览器硬刷载入 client 模块;失败即移除该行回滚(先备份两文件,参照 backup-20260906-170533 做法);
225
232
  4. GUI 鉴权墙:安装后的功能验证需在用户已登录浏览器进行(本机端口 3080)。
@@ -455,7 +462,9 @@ dsh-ling/
455
462
 
456
463
  ## 7. 记忆存储 schema v1
457
464
 
458
- > **现状说明(实现已升级)**:线上 schema 为 **v2** —— `conv_overview.source` 的 `CHECK` 已放开(支持 `dsweb` / `dsh` / `import` 三个来源,自动整表重建且数据无损),并新增 `session_meta.raw_seq`、`persona_suggestions` 等;下表为 v1 原始设计,保留作对照。
465
+ > ⚠️ **契约已变(本节属早期版本)**:下列 schema 描述是早期设计,**现实契约以 `ACCESS-DESIGN.md` 与本版代码为准** —— 本节原来还写着"线上 schema 为 **v2**",而代码里 `lib\host\memory.js` 的 `SCHEMA_VERSION` 现为 **14**(`CHANGELOG.md` 也按 v11→v14 逐版记录)。
466
+
467
+ > **当时的状态说明(2026-09 早期)**:当时线上 schema 为 **v2**(现已到 **v14**,见上方警示框)—— `conv_overview.source` 的 `CHECK` 已放开(支持 `dsweb` / `dsh` / `import` 三个来源,自动整表重建且数据无损),并新增 `session_meta.raw_seq`、`persona_suggestions` 等;下表为 v1 原始设计,保留作对照。
459
468
 
460
469
  ```sql
461
470
  -- ~/.dsh/cache/dsh-ling/memory.db (node:sqlite)
@@ -502,6 +511,11 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
502
511
 
503
512
  ## 8. 导出/合并格式 v1
504
513
 
514
+ > ⚠️ **契约已变(本节属早期版本)**:下面这段 JSON 形状里的字段名**大多已不是现状**,**现实契约以 `ACCESS-DESIGN.md` 与本版代码(`lib\host\memory.js`)为准**:
515
+ > · `"schemaVersion": 1` —— 实际随 `SCHEMA_VERSION` 现为 **14**;
516
+ > · `"settings"` 与 `"includeRawTurns"` —— 这两个键**不存在**(真实键是 overviews / feedbackQueue / suggestions / deep;要带原文用 `?includeRaw=1`,原文落在顶层 `rawTurns`);
517
+ > · `requestedAction` 与 `manifest` —— **全仓零命中**的旧契约,真实开关是导入时的 `?overwrite=1` 与"采纳其人格"的 `{bundle, persona:true}`。
518
+
505
519
  单文件 `dsh-ling-memory-<yyyy-MM-dd>.dshling.json`(UTF-8):
506
520
 
507
521
  ```jsonc
@@ -521,6 +535,11 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
521
535
 
522
536
  ## 9. HTTP API 表(/api/dsh-ling/*)
523
537
 
538
+ > ⚠️ **契约已变(本节属早期版本)**:下表只是早期首批端点,**现实契约以 `ACCESS-DESIGN.md` 与本版代码(`lib\host\api.js`)为准**;其中三处已经失实:
539
+ > · `POST /import` 实际收 **JSON body**,不是 multipart;
540
+ > · `GET /persona/suggest` —— **该端点不存在**(现实是 `/persona/draft` 与 `/persona/genesis`);
541
+ > · `/import` **不再是"只读"** —— 它现在是**手术门**覆盖的 12 条写面之一。
542
+
524
543
  | 方法/路径 | 用途 | 冻结门 |
525
544
  | --- | --- | --- |
526
545
  | POST `/mode/toggle` body{sessionId} | 切换模式(记录 + 只同步推理档位、不改模型 + 重定稿) | 运行中→排队返回 queued |
@@ -543,6 +562,8 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
543
562
 
544
563
  ## 10. 里程碑与验证清单
545
564
 
565
+ > ⚠️ 该清单为**历史进度**,不代表当前状态(未勾选的项大多是立项当时的记录;实际进度见 `CHANGELOG.md`)。
566
+
546
567
  - **M1a 骨架 + 本地构建(下一阶段,不触碰运行 profile)**
547
568
  - [ ] package.json(dsh.bundle.patch / exports ./client / platform web)+ cordis.patch.yml
548
569
  - [ ] host 各模块空实现 + settings ns 注册 + sqlite 建库(临时 DB 自检)