@nightpainters/dsh-ling 1.5.1 → 1.5.3
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 +10 -7
- package/CHANGELOG.md +140 -3
- package/DESIGN.md +162 -18
- package/README.md +66 -32
- package/lib/audit/access-log.js +181 -25
- package/lib/audit/index.js +45 -3
- package/lib/audit/patch-spec.js +1 -1
- package/lib/client.js +1183 -88
- package/lib/host/api.js +716 -83
- package/lib/host/backfill.js +8 -2
- package/lib/host/feedback.js +112 -2
- package/lib/host/freeze.js +52 -1
- package/lib/host/genesis.js +27 -5
- package/lib/host/guard.js +39 -2
- package/lib/host/habit-gen.js +12 -4
- package/lib/host/import-file.js +62 -6
- package/lib/host/import-log.js +80 -2
- package/lib/host/inject.js +49 -12
- package/lib/host/l1.js +11 -4
- package/lib/host/lifecycle.js +13 -2
- package/lib/host/memory.js +425 -33
- package/lib/host/mode-service.js +85 -0
- package/lib/host/mode.js +29 -9
- package/lib/host/persona.js +38 -7
- package/lib/host/retitle.js +6 -0
- package/lib/host/session-line.js +688 -0
- package/lib/host/snapshot.js +48 -7
- package/lib/host/summarizer.js +159 -3
- package/lib/host/surgery.js +600 -0
- package/lib/host/tools.js +12 -1
- package/lib/host/tree-ops.js +1 -1
- package/lib/index.js +22 -29
- package/package.json +51 -4
- package/tools/apply-access-log-patch.mjs +1 -1
package/ACCESS-DESIGN.md
CHANGED
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
|
|
52
52
|
## 3. 导入流水线与工程约束
|
|
53
53
|
|
|
54
|
-
- **分页提交**:`/api` 读 body 上限
|
|
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/来源过滤)全绿;全套回归绿。
|
|
@@ -103,17 +104,19 @@
|
|
|
103
104
|
- **生活/情感加权**:feeling 1.0 / daily 0.7 / knowledge 0.2 + 置顶 +1.5(`GENESIS_WEIGHTS`);prompt 声明"她首先是伴侣,其次才是同事";"评价用户/画像类任务才平权"作为语义规则保留给未来层;
|
|
104
105
|
- 原料三级取用:深摘(deep.sum)→ 非占位概述 → 原文片段(短会话,≤3 轮)/关键词线索——短会话不再静音;
|
|
105
106
|
- **名字主客过滤**(`filterNamePairs`):剔除 userTitle 成分与常见称谓词;prompt 明令名字属于她;
|
|
106
|
-
- **草稿制(乙方案)**:genesis
|
|
107
|
+
- **草稿制(乙方案)**:genesis 候选不再直接落盘;自述/名字单选加入草稿,语气建议、相处观察与**职责候选**可一并携带;「带着草稿去人格中心人审」→ 对应字段旁浮现「她建议…[填入]」(纵向插入,修复过 flex 行溢出;职责同样走这条通路,落进「职业 / 职责 / 责任」输入框);语气/观察进入「她的话」区块,可一键**认可这条习惯**(`POST /persona/hint-adopt`,习惯成长通道,定型下无需手术);
|
|
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 展示;**一次导入 =
|
|
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
|
|
|
114
115
|
## 9. 诞生仪式(genesis)设计速览(发布文档引用用)
|
|
115
116
|
|
|
116
117
|
1. **入口**:记忆中心 →「接入历史」页签底部「🌱 诞生仪式」卡;流程 = 让读(深摘未读)→ 第一次自我介绍(加权样本合成初稿)→ 人审草稿 → 人格中心落盘;
|
|
117
|
-
2.
|
|
118
|
-
3.
|
|
119
|
-
4.
|
|
118
|
+
2. **初稿五件**:自述候选 ×2~3、名字组 ×2~3、语气建议、相处观察、**职责候选(`duties`)×1~3**(JSON 契约,键名固定为 `self_intros` / `name_pairs` / `tone_advice` / `observations` / `duties`;解析带围栏剥离与长度清洗);
|
|
119
|
+
3. **职责候选的口径(`duties`,第 5 键)**:每条 **5~120 字**、最多 **3 条**、**每条一句**;低于 5 字的噪声与超过 120 字的**整条丢弃**(**不截断** —— 截半句会留下一条读不通的"职责");条内换行折成空格(段内一条一行,防止正文借换行伪造段结构)。清洗后落进 `[承担]` 段;该段**空值不渲染**(不留空段头);
|
|
120
|
+
4. **三条硬约束**:**A. 不得自动落盘** —— 职责与另外四件一样只是**草稿候选**,机制永不自动写入,终审一律在人格中心(已定型字段需手术门);**B. 不得设非空默认,也不得从历史 / 模式推断预填** —— `duty` 默认只能是 `''`;**C. 起草 prompt 明令措辞** —— 只许**陈述式**(我承担 / 我擅长 / 我能给出 / 我会在…的时候…),**禁授权式**(我应当 / 我必须 / 我的任务 / 我的责任是 / 我被要求 / 请让我负责);授权式写法等于把一个"被授权者"写进她的身份,是语义劫持最好用的钩子(与 `[规矩]` 段头那次同族的教训);
|
|
121
|
+
5. **人审闭环**:草稿携带 → 字段旁「她建议…[填入]」/「她的话…认可这条习惯」;五件草稿的终审与保存一律在人格中心(新老用户同路径;已定型字段需手术门);
|
|
122
|
+
6. **材料口径**:她的名字/自述基于"生活情感优先"的加权样本;历史中属于用户的称谓一律过滤,不得混入她的身份;职责同样只从这段历史里**真看得出来的事**来写,**凑不出来就少写或不写**(prompt 明令),不允许写成客套话。
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,143 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循语义化版本:`MAJOR.MINOR.PATCH`。
|
|
4
4
|
|
|
5
|
+
## 1.5.3
|
|
6
|
+
|
|
7
|
+
**跨插件挡位接口(对外开放 `dsh-ling/mode` 服务)· 导入批体量上限(超限不再砍连接,改回 413)**
|
|
8
|
+
|
|
9
|
+
> 一句话:本版两件事 —— ① 把"设挡位"这件事从**只能由 GUI 发起**变成**可以被别的插件调用** —— 器灵注册一个轻量 Cordis 服务 `dsh-ling/mode`,暴露 `applyMode(sessionId, modeKey)`。调用方**无法**连带改掉全局默认模式,**也无法**连带改掉会话的模型。首个消费者是飞书桥(`dsh-feishu-host`):它在**新建一条线**时给新会话钉上 `life` 挡位;② 修掉"一千多条的历史一次导入,一条都没进去、界面还显示**批失败(网关)**"的那道体量闸门(两端判据**单位不同** + 超限时**砍连接**导致错误文案没有通路)。
|
|
10
|
+
|
|
11
|
+
> ✅ **本节即 1.5.3 的发布内容**;`package.json` 版本号为 **`1.5.3`**(开发自 2026-09-29 夜起,发布前已去掉开发期后缀;1.5.2 已双发)。
|
|
12
|
+
> ✅ **②的真机状态(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` 与客户端量到的长度逐字节相等。
|
|
13
|
+
> ✅ **浏览器那条链也验了(2026-09-29 夜 · 主人在真界面点了一次)**:一份**合成的**"单条 5.0 MiB"文件走「记忆中心 → 接入历史 → 确认导入」,界面如实报出「**另有 1 条会话单条太长、没能通过导入通道(已跳过)**」「校验通过 0 · 拒绝 0 · **单条过大跳过 1**」并**点名**该条 ⇒ **不再出现「批失败(网关)」**;其中 `4 MB` 取自服务端回执 `limit`(**客户端没有镜像这个常量**)。
|
|
14
|
+
> ⚠️ **当时仍未做(当日已补,见「二」里"被跳过的'单条过大'进持久账本"一条)**:被跳过的条目**不进持久「导入记录」**(当时账本只有 `errors` 一个字段);另外**单条 >4 MiB 的文件仍然进不来** —— 现在至少说得清是为什么。
|
|
15
|
+
|
|
16
|
+
### 一、新服务 `dsh-ling/mode`
|
|
17
|
+
|
|
18
|
+
- 注册走 **`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**;
|
|
19
|
+
- 暴露两个方法:`applyMode(sessionId, modeKey, opts)` → `{ ok, queued?, mode, reasoningEffort?, warning? }`;`currentMode(sessionId)` → `'work' | 'life'`;
|
|
20
|
+
- **内部固定 `setDefault: false`** —— 服务只改**指定会话**的挡位,**绝不**写 `settings.mode.lastMode`。GUI 那条老路仍走 `applyModeToSession(...)`(内部即 `{ setDefault: true, ...opts }`),**行为逐字不变**;
|
|
21
|
+
- **`keepModel` 可选**:`currentModelSelection(ctx)` 读的是**全局**当前模型,跨插件场景下会把调用方自己的模型盖掉 ⇒ 调用方可传 `keepModel: { provider, model }` 原样带回;
|
|
22
|
+
- **`syncEffort: false`** 可选:只记模式、不动会话推理等级(连 `selectModel` 都不调);
|
|
23
|
+
- **注册失败不外抛**:只写一行 `console.debug` 并返回 no-op disposer —— 器灵装不上不该拖垮宿主。
|
|
24
|
+
|
|
25
|
+
### 二、导入批体量上限:超限不再砍连接,改「排空 → 413」
|
|
26
|
+
|
|
27
|
+
> 根因(两端判据单位不同、且互相不知情):客户端按**条数**切批(`batch = 200`),服务端按**字节**封顶(4 MiB),而 `readBody` 超限时是 `reject` **与** `req.destroy()` **同一拍** ⇒ **那句现成的"请拆分后重试"永远送不出去**,浏览器只看到 `ECONNRESET`,前端把它归成「批失败(**网关**)」—— 一个确定性、可自愈的失败,在界面上长得像网络故障。186 条 / 4.89 MB 的真实导出就是这么**一条都没入库**的。
|
|
28
|
+
|
|
29
|
+
- **服务端 · 排空再回 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` **逐字沿用旧文案**;
|
|
30
|
+
- ⚠️ **边界(独立红队实测,别读成全称命题)**:**只有 `4 MiB < 体量 ≤ 64 MiB` 才保证拿到 413**。超过 64 MiB 撞硬熔断 ⇒ 仍是 `ECONNRESET`(与修前同症状);`Content-Length` 撒谎一类的**协议违规**客户端也拿不到 413(浏览器不会这么发)。实测边界严格翻转:`4 MiB` 整 → 200、`4 MiB + 1 B` → 413;
|
|
31
|
+
- **排空换来的两个新代价,各配一道闸**:`BODY_DRAIN_MAX_BYTES = 64 MiB`(硬熔断 —— 给一份真实导出留够"把 body 发完"的余量,余量不足则排空本身会变成**新的** reset,白改一场)+ `BODY_DRAIN_IDLE_MS = 30 s`(静默超时,替代原来那条防 DoS 动机:只要还有数据在流就算正常,**一旦静止就是僵尸连接**;滑动窗口,每收到一个 chunk 重置);计时器在 `'end'`/`'error'`/熔断**三条出口**都 `clearTimeout`;
|
|
32
|
+
- ⚠️ **这道防线的形状边界(红队实测,本批未关)**:静默超时只覆盖"**已经在发、中途静默**"这一形状;「**只发头、不发体**」的请求在**应用层没有计时器**(计时器只在 `req.on('data')` 里武装),兜底只剩 Node 自己的 `requestTimeout`(默认 300 s)。⚠️ **这不是回归**(修前同样没有),但**别对外写成"防僵尸"的全称命题**;
|
|
33
|
+
- **客户端 · 按字节切批**:`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`**(不在这边再写死一个);
|
|
34
|
+
- **契约注释(④)**:`api.js` 那句「每批 ≤500」**与实现自相矛盾**(500 条要装进 4 MiB,需每条 ≤8 KB,而带正文的会话实测 ≈26 KB/条),已改写为**条数 × 字节双限**,并写明**两端耦合** —— 任何一方单独改上限都会让口径重新错位,**这正是本缺陷的成因**;`ACCESS-DESIGN.md` 的「分页提交」契约同批对齐(历史记录只加"已作废"指针,**不改写历史**)。
|
|
35
|
+
- **客户端 · 单条超限不再含糊(C-1;由独立红队实测后补)**:单条会话 ≥ 4,194,210 B 时服务端必回 413,旧行为是**该条被永久丢弃**、只在界面上记一句「批错误 1」,而文案「请把导出文件拆小」对**单条会话物理上不可执行**。现在:413 **且本批只有 1 条** ⇒ **点名**(`title` → `name` → 「未命名」三级退路)+ 报出**真实字节**(客户端复算,不用服务端被截断的 `got`)+ 说清「这条太长、拆文件也拆不开它、已跳过、其余照常导入」;计入独立的 `tooBig`(**不混进 `errors`**)、**不中断**后续批;`finish()` 有跳过时**不再说「✓ 导入完成」**。判据只用**服务端回执**+ 本批条数(**客户端不镜像服务端的 4 MiB 常量**,将来放宽上限不用改这里)。⚠️ **当时**仍未做:被跳过的条目**不进持久「导入记录」**(账本只有 `errors` 一个字段)—— **当日已补,见下一条**。
|
|
36
|
+
- **被跳过的"单条过大"进持久账本**:`POST /import/log` 现在接受 `{ tooBig, tooBigNames }` 并**允许只报跳过、让账本的 `errors` 保持 0**(这是命门:否则"已知跳过"会被记成"失败批");账本行在「失败 X 批」之后追加 **`· 单条过大 N`**;**`tooBig = 0` 时落账形状逐字不变** —— 包括老客户端**不传 `errors`** 的那些请求,行为与修前完全一致。护栏:`tooBig` 夹 `0..10000`、名字**只留前 5 条 / 每条 40 字 / 非字符串丢弃**,同一 runId 合并时**再归一一次**(防多批把账本撑大);清洗实现**只有一份**,端点复用。✅ **真机已验(宿主重启后)**:一次真实导入落账为 `"tooBig":1,"tooBigNames":["未命名"],"errors":0`(只读查 `kv` 得到)—— 命门成立:**已知跳过没有被混成"失败批"**。
|
|
37
|
+
- **首字节闸(「只发头、不发体」;非回归,顺手关掉的洞)**:原先那套静默计时器**只在收到第一个 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 断开**(旧实现:零响应一直挂着)。
|
|
38
|
+
- ⚠️ **别读成"客户端会读到一句解释"**:首字节闸的错误**不走 413 出口**,真机上落到"往将死的 socket 写一句、客户端收不到"那条路 —— **它买到的是"连接 61 ms 就断",不是"错误文案可达"**。
|
|
39
|
+
- ✅ **真机已验(宿主重启后 PID 12972)**:头齐 + `Content-Length: 4194304` + **0 字节体** ⇒ 客户端实测 **`ECONNRESET` @ 30,011 ms**(= 30 s 闸门到点);修前是挂到 Node 自己的 `requestTimeout`(**300 s**)。
|
|
40
|
+
- **记忆包导入的 413 说人话(只改文案)**:`⬆ 导入记忆包…` 是**整份一发**(`POST /import`)—— UI 默认包实测 **1.386 MiB**(安全),但 `includeRaw=1` 的**带原文包 5.959 MiB** 会撞 4 MiB。原先 413 时**吐一段原始 JSON**、连接被熔断时只说「导入失败(网关不可达)」。现在说清:①**整包**超上限、**不是包坏了**;②上限**只用回执 `limit` 换算**(回执没给就直说"上限未随回执返回",**不猜数**);③**怎么办** —— 点「⬇ 导出记忆包」重导一份**默认不含原文**的包再导入,并如实说明"整包一次发不完"是当前通道的结构限制。其余失败优先用服务端 `message`;`sealed` 分支**逐字未动**。**分批上传不做**(留 1.6)。
|
|
41
|
+
|
|
42
|
+
- **三处方法闸补齐(`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 侧改动 ⇒ 需一次宿主重启才在真机生效**。
|
|
43
|
+
- **折叠不再覆盖已有原文(数据安全)**:导入时若某会话在**网页端旧库(旧域)**里已存在,会走"**折叠**"(元数据与原文并进去)。原实现把**导入侧的 `seq`** 直接写进那个会话,而底层是 `INSERT … ON CONFLICT(session_id, seq) DO UPDATE SET text=excluded.text` ⇒ **序号一重叠就把目标里的真原文静默换掉**(离线测试台实测 `clobbered=3`)。现在:**目标已有原文就不覆盖** —— 逐条按 `(role, text)` 比:相同 ⇒ 幂等跳过;不同 ⇒ **保留现有的**、只计新计数 `foldSkippedRaw`、**一个字节都不写**;**目标一个字都没有原文** ⇒ 照旧写入(升级路径不堵死)。⛔ 明确否决两条替代:按 `max(seq)` 往后追加(**同段对话在库里变两份、污染检索**)、整段替换(**丢真的方向**)。
|
|
44
|
+
- **"跳过了"必须说出来**:该计数贯通 **HTTP 回执 → 导入面板人话**(`ⓘ 另有 N 轮原文没有替换…不是失败,也没有丢东西`)→ **持久账本**(行末尾 `· 原文未替换 N`,且 `= 0` 时落账**逐字不变**)。
|
|
45
|
+
- ⚠️ **已知取舍**:同一会话**二次折叠不会再"补全"** —— 目标哪怕只有 1 轮原文,导入侧全轮也只进计数、不写库(宁可少副本、不换真原文)。
|
|
46
|
+
- **命名空间裂缝(同批修)**:折叠判断"有没有原文"原先**只看写入目标(dsweb 裸 id)**,没把 `import:<id>` 命名空间一起算 ⇒ 某些顺序下同一段对话会在库里**存两份(两个 session_id)**,而那正是折叠要消灭的"**检索里召回两次**"。现在判据扩成"**候选链任一侧有原文**"(复用既有 helper):任一侧有 ⇒ 不写、计入 `foldSkippedRaw`;两侧都空 ⇒ 照旧写入。**真机库只读盘查:两侧都有 = 0**(102 个 session:裸 id 87 / `import:` 侧 15)⇒ 无旧账要清,本次为**纯预防**。
|
|
47
|
+
- **反代伪装 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 条 ✗)。
|
|
48
|
+
- **人格手术门**:`"手术门不是挡主人的,是万一网络失守时最后失守的位置"`(主人原话)。新增 `lib\host\surgery.js`,把门**下沉为一处** `requireSurgery()`,覆盖**全部 14 条**能改人格/规矩/习惯的写面;`sealed` 时依次判 **非本机**(带代理头 ⇒ `surgery-local-only`)· **每启动轮换的手术票据**(`/surgery/ticket` 只下发给"无代理头"的请求) · **原句匹配**;并**删掉 `wantLock` 绕过分支**(旧代码下"带 `sealed:true` 的补丁 + 假句"能直接改库,已实测证明)。另加**带哈希链的手术留痕**(`surgery-*.jsonl`,`prevLineHash` 串链,改一行即断)与**只读**事件面 `GET /surgery/events`。
|
|
49
|
+
- ✅ **真机复验(2026-10-01 下午)**:本机直连 `200` · **伪造代理头一律 `403 spoofed-loopback-host`**(守卫⑤ 真机生效) · 本机取票 `200`(64 位 hex) · **经代理取票 / 读事件面 / 打手术端点全部被拒** · `surgery.ticket` 与 `surgery-2026-10.jsonl` **均已落盘**。
|
|
50
|
+
- 🐞 **同批修掉一个"链永远串不上"的 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 条历史留痕一个字不动**(审计留痕永不改写/重排)—— 它们会**一直**被报为断链,那是**历史事实**;"让校验能表达**含已知历史缺口**"的**链基线**见下一条。
|
|
51
|
+
- ✅ **承诺句按渠道分流(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`;代价却是把"**宣言**"降级成"**密码**")、**不做**"本机+票据重置"(主人:「只用本机回显可以了,够的」)。
|
|
52
|
+
- ✅ **手术留痕的链基线(2026-10-01 下午)**:`verifySurgeryChain` 读 `surgery-chain-baseline.json` 的 `brokenRanges`,**区间内断链 ⇒ `ok:true` + `knownGaps`**(并继续校验后段、区间后重新接上),**区间外仍 `ok:false`**;⚠️ **只豁免"链"这一项** —— **区间内的自哈希照样核**(改过历史行仍会被抓)。内置默认区间 = 真机实况(`from 1, to 5`,理由:建链 bug,历史行不改写)。目的:别让"永远报错的告警"变成没人看的告警。
|
|
53
|
+
- ✅ **留痕去重(2026-10-01 晚)**:`/surgery/events` 的 `chain.knownGaps` 原来**按受影响的行各报一条**(真机 5 行 ⇒ 4 条一模一样的 `{from:1,to:5}`),长日志下会成倍膨胀。现在改成**每个区间一条**、`at` 记**首次命中行**;⚠️ **判据一字未动**(区间内断链仍 `ok:true`、**区间外仍 `ok:false`**、**区间内改一行仍被抓** —— 后两条用新写的负对照钉住)。真机复测:`{"ok":true,"count":5,"knownGaps":1}`。
|
|
54
|
+
- **免检通道:黑名单 → 正向白名单(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 会话")。
|
|
55
|
+
- **标题长度的"同名不同语义"(E1)**:`retitle.js` 的 `TITLE_MAX_CHARS = 58` 是**生成上限**(切句读/硬截的落点),`session-line.js` 的 `= 60` 是**显示兜底夹取**(对任何来源标题生效,含手改标题)。旧注释本就自陈"概述器侧是 58,这里只做兜底"⇒ **60 是有意留的余量**。硬统一到 58 会**多切手改标题**、统一到 60 会**改 59–60 字输入的生成行为** —— 双向都是行为变更 ⇒ **只补注释 + 把不变式写成断言**("显示兜底 ≥ 生成上限")。
|
|
56
|
+
- **预览与写路径的 `changed` 判据收敛(C 组)**:写路径有 3 条判据(逐字节 / `md5(candidate)===lastMd5` / 实质键),预览只有 2 条(缺 md5)⇒ 存在"**预览说追加、写路径判 same**"的分化可能。现在**收敛到唯一判据** `session-line.js` 的 `contentChanged()`,两条路都调它 ⇒ **按构造**不可能再分化。⚠️ 如实说明:该分化**在干净库上产生不出来**(`writeLast` 历来把 `text` 与 `md5` 一起写),本次修的价值是"**让分化不可能发生**",不是"修一个线上已复现的错"。
|
|
57
|
+
- **折叠指纹判据扩到候选链(C′)**:上一版把"有没有原文"扩成了"候选链任一侧",但**逐条比对的指纹**仍只取写入目标那一侧 ⇒ 两侧都有原文时 `import:` 侧已有的同一轮会被算成"未替换"(计数偏大)。现在指纹集取**两侧**(实测 2 → 1),**零写入、零迁移**(历史账本旧数不回改)。
|
|
58
|
+
- **归因修正:不再拿"有没有原话"反推"是谁写的"(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`。
|
|
59
|
+
- **`/state?sessionId=` 的基数上界**:`freeze.js` 的 `_byId` 原来会随任意 `sessionId` **无限增长**。现在 `MAX_IDLE_SESSIONS_DEFAULT = 128` 只淘汰**空壳**会话;**带读数者(running/pending/snap/snapStale/mode)永不淘汰** ⇒ 已存在会话的**读数逐字不变**。
|
|
60
|
+
- **`/dsweb/summary/preview?db=` 的路径白名单**:原来任何路径都能当"只读存在性预言机"(靠 `readOnly:true` 兜底)。现在只接受**默认库 + 环境变量 + settings 里配置过的路径**,越界回 `{ok:false, reason:'db-not-allowed'}` 并给出**两条出路**(env / settings)。⚠️ `/dsweb/scan`、`/dsweb/summary/run` **未动**(不在本条范围)。
|
|
61
|
+
- **`POST_ONLY` 差集护栏**:`api.js` 注册 **79** 条路由、`POST_ONLY` **55** 条 ⇒ 差集 **24** 条。新增源码级护栏把**这 24 条逐条钉住** —— 将来**新增/删除**端点都会让断言变红,逼后来者**有意识地**决定"这条要不要进 `POST_ONLY`"。⚠️ 断言里写明"**这是快照不是白名单**:变红时该判断该端点是不是写面,不是无脑抄进数组"。
|
|
62
|
+
- **出厂默认值里不再夹带任何一台机器的私有配置(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` 的机器必须显式声明**(否则自己那台也会静默失去设备维度;回滚只需删掉那段配置)。
|
|
63
|
+
- **迁移不再把"库只读 / 库被写锁"变成"插件加载失败"(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`(会动别处的锁语义),要收需单独决定。
|
|
64
|
+
- **反馈通路:停掉空转的轮询,改订宿主推送(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 兜底"。
|
|
65
|
+
- **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 不是代码**。
|
|
66
|
+
- ⚠️ **已知限制**:"封存但既无明文句也无哈希"的老档案按现状**进不去**(当前档案有明文 ⇒ 不受影响)。
|
|
67
|
+
|
|
68
|
+
### 三、测试
|
|
69
|
+
|
|
70
|
+
- 新增 `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`;
|
|
71
|
+
- 另有**源码级护栏**两条:禁止出现 `setDefault:true`、必须出现 `setDefault:false`;
|
|
72
|
+
- 新增 `tests/bodylimit.test.mjs`(**55 条断言**,每条打印实测值):排空到 `'end'` **之后**才 reject、期间 `destroy()` **零调用**、**排空态不再缓存**(拿旧的按条数实现当仪器做正对照:旧 retained 2 条 / 8192 B,新 0)、硬熔断、静默超时(活跃计时器 0→1→0、残留 0)、**端点级端到端 413**、socket 已死时不抛;
|
|
73
|
+
- 新增 `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 字节;
|
|
74
|
+
- `npm test` 链由 **36 套增至 37 套**(1.5.3 前两节),本批再增至 **39 段**;分段跑全绿:`PASS_SEGMENTS=39 · FAIL_SEGMENTS=0`。
|
|
75
|
+
- 后续三处修复(账本 / 首字节闸 / 记忆包文案)**只在既有测试文件里追加断言**,故**段数仍是 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 逐字还原)。
|
|
76
|
+
|
|
77
|
+
## 1.5.2
|
|
78
|
+
|
|
79
|
+
**`/state` 分档提速 · 活行档位改判 · 面板可见性收口 · 依赖声明与「承担」**
|
|
80
|
+
|
|
81
|
+
> 一句话:本版补的是"看不见的那一层" —— 轮询路径与面板路径分开计价,`/state` 单次耗时降到零头;活行(面板上跟着会话走的那一条)的档位从"15 分钟 / 10 轮"改成"轮次边界 + 内容真变了才追加";面板第一次看得见"那一行"和"为什么没变";侧栏的「最近改动」不再静默截断;另外把 `peerDependencies` 补全(这道闸门决定插件会不会**悄悄不激活** —— 线索只有一行,而且不是每条启动路径都会打),并给器灵加了「承担」这一栏。
|
|
82
|
+
|
|
83
|
+
### 一、`/state`:轮询档与面板档分开计价
|
|
84
|
+
|
|
85
|
+
轮询路径与面板路径问的是同一个端点,但要的东西不一样 —— 面板要 L0 预览文本、快照读数、活行预览,而 3 秒一拍的轮询只要几个计数。此前两条路走同一份重活。
|
|
86
|
+
|
|
87
|
+
- **按 `l0Preview` 分档**:面板路径恒带 `l0Preview=1`、轮询路径从不带 ⇒ 轮询档在算 L0 预览**之前**就早退,只回轻字段(既有 5 个快照字段逐字未动);
|
|
88
|
+
- **`MemoryStore.fingerprint()` 的缓存键由一维扩成五维**:`total_changes()`(本连接自写)+ `-wal` 文件的 `(size, mtimeMs)`(外进程写入)+ `PRAGMA data_version`(维护脚本若用 `journal_mode=DELETE` 改库**根本不产生 `-wal`**,只有这一维兜得住)+ db 句柄(关连接再开)。其中 `-wal` 与 `data_version` 两维**取不到时各给哨兵值**(`[0,0]` / `-1`),**不因取不到而抛**(否则 `/state` 会 500);而 `total_changes()` 那一维**没有这层降级** —— 它直接问 db 句柄,句柄已关就照常抛(由路由的兜底 catch 变成 500)。返回契约逐字不变(`sha256` 全长 64 位,面板取前 16 位展示);
|
|
89
|
+
- **`countOverviews()`**:记忆条数从"拉全量再取长度"改成一条 `COUNT` 聚合 —— 面板与 `/state` 都不再为两个数字付全表扫描的钱;
|
|
90
|
+
- **实测**(当天 `00:00`–`16:34` 的访问日志 `access-*.jsonl` 里 `path` 为 `/api/dsh-ling/state` 的请求,取 `ms` 字段;分界取 `13:13` 这一分钟、两侧都不算它 —— 切换就发生在**这一分钟之内**:`13:13:08` 仍是 28 ms,`13:13:16` 起已是 1~2 ms,而该分钟自身 15 次里 13 次 `≤2 ms`):分界前共 **n=51,573** 次、均值 **24.4 ms**、`≤2 ms` 占 **0.02%**;分界后共 **n=3,945** 次、均值 **0.46 ms**、`≤2 ms` 占 **99.5%**。两段是同一天、同一个端点的读数,差别只在"这一版是否已经生效"。以上是**本机这一次**的实测:**两段的窗口起止与样本量都写在句内**(样本量是**读日志那一刻**的值,日志继续写入后按同一口径重算会略大),可用同一天的日志按同样口径复核;换台机器、换个库、换一天都不会一样。
|
|
91
|
+
|
|
92
|
+
### 二、活行(面板上的「那一行」):档位改「轮次边界 + 内容真变了才追加」
|
|
93
|
+
|
|
94
|
+
- 旧档位(**15 分钟最小间隔 / 10 轮**)撤除,改为两条:① 只在新轮次的**第一批**(`step === 1`)考虑追加;② 「内容真变了才追加」用**三道同源判据**,任一说"没变"就不追加;
|
|
95
|
+
- **为什么非要第三道**:那一行正文自带 `#N`,若拿整行去比,**每试一次序号就 +1** ⇒ 恒判"变了",闸等于没有(实测钉过);第三道比的是**实质内容键**(标题 + 最新进展,剥掉 `#N` / 轮次 / `HH:MM`);
|
|
96
|
+
- 旧档位常量与判定函数**保留导出**(测试与探针仍引用),但只读探针里明确标 `tier.active: false` —— 一眼看得出它不在生效路径上;
|
|
97
|
+
- 顺带:这一行"上一次追加过没有"的留痕带 `appendCount`,**重启后判定照旧有效**(不会"重启即多追加一条")。
|
|
98
|
+
|
|
99
|
+
### 三、面板:两条「那一行」+「为什么没变」+ 可折叠看全文
|
|
100
|
+
|
|
101
|
+
- **「那一行」在盘上有两条**,长得像但不是一回事:① **L2 本会话标题行**(概述器写进开场记忆的那一条,只有标题)② **活行**(面板上跟着会话走的那一条,每轮重算)。此前面板不显示、也不区分 ⇒ 现在**各自成行、各自标注**;
|
|
102
|
+
- **「为什么没变」拆成四类看得见的读数**,每条只印真读数;后端这一版没带那个字段时**明说"不做猜测"**,不拿默认值冒充结论;
|
|
103
|
+
- **可折叠看全文**:折叠状态存**闭包**而不是 DOM —— 面板每一拍(store tick)都会重建这一块,存 DOM 会每一拍被清掉;
|
|
104
|
+
- **没有快照时不再谎报「已定稿 0 字符」**:后端在没有快照时回的是 `{exists:false}`,而它是个**真值** ⇒ 前端原先照样进"已定稿"分支。现在先判 `exists === false`,显示「(未定稿 —— 本会话尚无快照,通常在会话第一次定稿后出现)」。⚠️ 这条尤其要紧:**"没有快照"与"有一个 0 字符的快照"外观一致、语义相反**,而后者真会发生(例如人格为空),所以它不是"显示得难看一点";
|
|
105
|
+
- **面板打开后不再冻结**:复用 store 已有的 tick 重画,**不新开 `/state` 轮询**(轮询刚降过频,不能再加回去);store 覆盖得到的行跟着 store 走,覆盖不到的逐行标 `〔打开时快照〕`,快照块前另加一行分隔说明;
|
|
106
|
+
- **两条残留(如实写明)**:① 全局视图最快 **30 秒**才动一次 —— 全局 ticker 已被有意降到 30 s,这是"不加回轮询"的必然结果,不是未修;② 面板开着切会话时,"打开时快照"那一整块仍是**旧会话**那份(实时行会跟着变)。
|
|
107
|
+
|
|
108
|
+
### 四、侧栏「最近改动」不再静默截断
|
|
109
|
+
|
|
110
|
+
- 原先客户端**请求写死 6 行**、渲染**再硬钉 6 行**,**溢出无任何提示** —— 恰好改了 7 处时,看上去就是"少了一条";
|
|
111
|
+
- 现在取数与渲染**两个上限分开命名**:取 100 行、渲染仍是那 6 行,其后**条件追加**「…另有 N 笔改动(此处只列最近 6 笔)」;
|
|
112
|
+
- **N 取服务端真计数**(与同一块红字「有 N 笔还没跟器灵说过」**同数同界**),不是"返回条数 − 6" —— 侧栏只取**一页**,而改动留痕是**全量**:页面恒满 ⇒ 这个差值恒等于"取数上限 − 6"这个纯常数(留痕随库增长按条数线性上涨,可它一动不动),属于"用两个常量的差冒充积压量"的老坑;
|
|
113
|
+
- 满页时退路说「**N 笔以上**改动」(保留量级,不冒充确切数);`id` 读不出来的行**当作已列出**(宁可少报,不谎报);`snapshot` 行**不算债**;
|
|
114
|
+
- **零后端改动**,硬刷页面即生效。
|
|
115
|
+
|
|
116
|
+
### 五、依赖声明:这道闸门决定"插件会不会悄悄不激活"
|
|
117
|
+
|
|
118
|
+
- `package.json` 落 **12 条 `peerDependencies`**,范围一律**开区间**。宿主启动时对每个 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` peer 跑一次 `semver.satisfies(宿主版本, 你声明的区间, { includePrerelease: true })` —— 判的是「**宿主版本是否落在你声明的区间里**」,**不是**"你的写法合规不合规";判不满足 ⇒ 整个 bundle 进 `skippedBundles`、**不激活**。线索分两条路径:**从命令行启动**会往 **stderr 打一行** `skipping profile bundle …`(**容易被忽略,但不完全静默**);而**应用自持 profile 的那条装载路径一个字都不打** —— 那才是真正难查的一种,表象常常只有"它突然不见了"。⚠️ 所以"改成 `>=` 就安全"是错的:**开区间同样可能不满足**(取决于宿主版本);`^` / `~` / 钉死版本只是更容易被将来的一次宿主升级甩出去。(确需放行某个确切版本时,宿主提供 `dsh plugin allow-version` 豁免通道;豁免生效时它照常加载。)
|
|
119
|
+
- `dsh.client.inject` **5 项 → 3 项**:删掉 `@deepseek-ai/dsh-client-store` 与 `@deepseek-ai/dsh-client-ui-slots` —— 这两个包在宿主里**没有客户端半边**,列在这里是从模板继承来的死边(⚠️ 顺带一条命名陷阱:那一列装的是**包名**,不是服务名);
|
|
120
|
+
- `tools/check-install.mjs` 的 `--profile` 默认值改读 `DSH_HOME`(此前一处写死 `~/.dsh`,与同文件另一处口径不一致);
|
|
121
|
+
- 新增开发侧只读自检 `tools/check-peers.mjs`(**不进发布包**):把"宿主版本还落不落在本插件声明的 peer 区间里"(不落就整个 bundle 不激活)变成一条命令能查的事。
|
|
122
|
+
|
|
123
|
+
### 六、「承担」:职责字段
|
|
124
|
+
|
|
125
|
+
- 人格新增一栏 `duty`,注入面段头 `[承担]`;**引导**写成陈述句(起草提示与面板都这么建议 —— 保存侧**不校验句式**);**空值 ⇒ 整段不渲染**(不留空段头);
|
|
126
|
+
- 诞生仪式的 JSON 契约 **4 键扩成 5 键**(新增 `duties`):由模型从导入的历史里总结 1~3 条候选 —— **起草权归她**,但**与其它候选项一样只是草稿**,终审与保存仍在人格中心;
|
|
127
|
+
- 提示词里明令**禁止授权式写法**(「我应当 / 我必须 / 我的任务 / 我的责任是 / 我被要求 / 请让我负责」):授权口吻会把她写成一个**被授权者**,而不是"她自己是什么" —— 那是语义劫持最好用的钩子;前端在候选旁配同款提醒;
|
|
128
|
+
- 字段白名单同步(`PERSONA_PATCH_KEYS`)—— 这一类字段**漏加就会在落盘前被丢弃**;丢弃**不是静默**的:宿主把被丢掉的键路径回在响应的 `ignored` 里、另写一行 `console.warn`(面板不读这个字段,所以漏加时**只在宿主日志里看得见**)。
|
|
129
|
+
|
|
130
|
+
### 七、树改动播报:拆成三条水位
|
|
131
|
+
|
|
132
|
+
- 原先「已播报」水位**按会话**存 ⇒ 新会话水位从零开始,把最新几条**又播一遍**;而播报后水位直接推到**全表最新 id** ⇒ 一次播报把中间一百多条标成已读,而它们**从未被示人**;
|
|
133
|
+
- 现在三条水位并存:**全局** `announced`(只推到**这次真正展示了的最大 id**)+ **会话**水位(本会话增量)+ `last`(全表最大 id)。⚠️ 第三条**有意不动** —— 它同时是**复盘模式的退出判据**,顺手改会让复盘模式退不出去;
|
|
134
|
+
- 超窗老账单独用 `retired` **计数式处置**(只计数不展开),不算"已示人";并修掉"超窗计数只在扫描窗口内累加"造成的**一次性低报**,以及老账多于扫描窗口时**游标饥饿**(每轮只前进 120 行、明细行永远渲染不出来)。
|
|
135
|
+
|
|
136
|
+
### 八、测试与计数口径
|
|
137
|
+
|
|
138
|
+
- `npm test` 链登记 **36 套**,全绿;
|
|
139
|
+
- **计数口径统一**:以 `npm test` 链里登记的段数为准(不再数 `tests/` 目录);README 与发布门禁**两边同口径**;
|
|
140
|
+
- 门禁新增**双向核对**:链里登记的路径必须在源目录里存在;`tests/` 下有测试文件却没登记进链 ⇒ **报红**(它会被 `npm test` **静默跳过**,一条断言都不跑)。
|
|
141
|
+
|
|
5
142
|
## 1.5.1
|
|
6
143
|
|
|
7
144
|
**审计必修落地 + 一处平台升级事故 + 会话历史可见性**
|
|
@@ -178,9 +315,9 @@ DSH `0.1.7` 把事件 `agent/session-start` 改名成 `agent/created`(**载荷
|
|
|
178
315
|
- 同步落盘(`appendFileSync`):异步缓冲会丢掉「调度重启后进程退出」前的最后一行,而那一行恰恰最重要;
|
|
179
316
|
- `tests/access-log.test.mjs`(已挂进 `npm test`):四态可辨、字段形状、查询串丢弃、超限裁剪、坏目录静默降级、写入后元数据当场可见。
|
|
180
317
|
- `deviceId` 取值的**权威形态已核实**(远端 UI 的 `pairing.ts` 用 `randomToken()` 生成 32 位十六进制明文写进配对 cookie),单测用**合成值**钉住该形态 —— 不拿真机 deviceId 当测试夹具(那是凭据)。
|
|
181
|
-
- **日志会自我标注良性噪声**:命中自家**部署姿态自检**签名的行多打 `probe:"posture"`(`POST /api/session.list` +
|
|
182
|
-
- **日志标注调用通道**:没有 `user-agent` 头的行多带 `channel:"no-ua"`。它只陈述"**非浏览器直连**"这一事实 —— 这一类**混装两种调用者**(进程内客户端 / 经远端通道代理进来的设备,代理按设计不转发 UA)
|
|
183
|
-
- **观测边界(2026-09-25 真实流量见证)**:远端桌面通道的内侧代理按设计只重建 `host` / `content-type` / `content-length` / `accept` 四个头 + 它自己的鉴权 cookie,**不转发 UA、丢弃 Origin/cookie/Sec-Fetch** ⇒ 凡经代理进来的远端设备(手机/平板/物理机)在内侧日志里一律是 `ip
|
|
318
|
+
- **日志会自我标注良性噪声**:命中自家**部署姿态自检**签名的行多打 `probe:"posture"`(`POST /api/session.list` + 本机回环来源 + 无 UA + 被 403 拦下,五要素齐全才标注)。该探针由远端 UI 插件每次启动后自带、`403` 正是它的健康结果 —— 标注**不代表放行**,该行 `status` 仍是 403(判据刻意收窄:五要素同时命中才标注,放宽任何一条都会把真攻击错标成"自检")。
|
|
319
|
+
- **日志标注调用通道**:没有 `user-agent` 头的行多带 `channel:"no-ua"`。它只陈述"**非浏览器直连**"这一事实 —— 这一类**混装两种调用者**(进程内客户端 / 经远端通道代理进来的设备,代理按设计不转发 UA),故**不指认是哪一台**;按端点语义判读的口径见 `lib/audit/access-log.js` 的字段注释(`CHANNEL_NO_UA`)。
|
|
320
|
+
- **观测边界(2026-09-25 真实流量见证)**:远端桌面通道的内侧代理按设计只重建 `host` / `content-type` / `content-length` / `accept` 四个头 + 它自己的鉴权 cookie,**不转发 UA、丢弃 Origin/cookie/Sec-Fetch** ⇒ 凡经代理进来的远端设备(手机/平板/物理机)在内侧日志里一律是 `ip`(本机回环地址) + `ua:null` + `channel:"no-ua"` + 授权通过:**能看出"有远端在用",看不出"是哪一台"**。要按设备归因,需在代理侧透传一个**由代理自己写入**的标识头(这要代理侧配合,日志侧自己补不出来)。
|
|
184
321
|
|
|
185
322
|
> 注意:core 那两行补丁位于 `node_modules` 内,**DSH 升级后需重跑补丁脚本**;补丁缺失时插件启动会 `console.warn`。
|
|
186
323
|
|
package/DESIGN.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# dsh-ling — 「器灵」记忆与人格助手插件 · M1 设计文档
|
|
2
2
|
|
|
3
|
+
> ⚠️ **总警示**:本文是 **M1 阶段的设计底稿**;M2/M3 的许多项**已经实现**(配置完整页、记忆树可视化、导出/合并 UI、概述器、反馈成长回路、人格草稿脚本等),**以 `README.md` 与本版代码为准** —— 本文只当立项记录读,不得当作现状契约。
|
|
3
4
|
> 依据:立项阶段的一次接口探测 spike 结论(含本地文件:行号证据)。
|
|
4
|
-
>
|
|
5
|
+
> 体例:正文里 `**锚点**` 是**本文档内部的交叉引用**,不是对外证据 —— 其中 `plans\`、`release\` 是**作者本地台账,不在本仓库**;随 npm 包发出的是 `package.json` 的 `files` 白名单(**9 项**,实测 npm 包共 **54 个文件** —— `DESIGN.md` 自己也在包里)。
|
|
6
|
+
> 本文档记录 M1 阶段的设计与决策;**实现以代码为准**(后续演进:存储 schema 现已为 **v14**、新增历史接入/深摘要/诞生仪式/语气成长等子系统,详见仓库代码与提交记录)。
|
|
5
7
|
|
|
6
8
|
---
|
|
7
9
|
|
|
@@ -9,9 +11,9 @@
|
|
|
9
11
|
|
|
10
12
|
| # | 决策 | 内容 | 来源 |
|
|
11
13
|
| --- | --- | --- | --- |
|
|
12
|
-
| D1 | 按钮落位 | **方案 A**:官方 slot `conversation.session.header.utilities`(list,会话页顶栏右侧) | 用户拍板;spike ① |
|
|
13
|
-
| D2 | 模式↔全局默认 | **跟随**:最近一次模式切换成为"之后新会话"的默认(平台 selectModel 会 best-effort 写全局默认,此副作用即机制)。**2026-09-12 收敛:只同步推理等级,不改模型** —— provider/model 一律沿用当前值 |
|
|
14
|
-
| D3 | 人格承载 | **自建注入段 + 设置页维护结构化字段**(称呼/自称/语气等),dsh-persona 不复用 |
|
|
14
|
+
| D1 | 按钮落位 | **方案 A**:官方 slot `conversation.session.header.utilities`(list,会话页顶栏右侧) | 用户拍板;spike ① 的结论(早期内部报告,未纳入本仓库) |
|
|
15
|
+
| D2 | 模式↔全局默认 | **跟随**:最近一次模式切换成为"之后新会话"的默认(平台 selectModel 会 best-effort 写全局默认,此副作用即机制)。**2026-09-12 收敛:只同步推理等级,不改模型** —— provider/model 一律沿用当前值 | 用户拍板;早期内部报告(未纳入本仓库) |
|
|
16
|
+
| D3 | 人格承载 | **自建注入段 + 设置页维护结构化字段**(称呼/自称/语气等),dsh-persona 不复用 | 用户拍板;早期内部报告(未纳入本仓库) |
|
|
15
17
|
| D4 | 运行期冻结(新增) | **会话运行中(思考/任务进行中)不参与"人格更新层"的任何变化**:不打断、不新增设定、不中途改注入内容;更新请求一律排队,在该会话空闲边界才应用 | 用户本轮追加 |
|
|
16
18
|
| D5 | 记忆加载策略 | 三层记忆(L0 常驻人格 / L1 开场 Top-K / L2 运行时检索),不注入全部概述 | 早前拍板 |
|
|
17
19
|
| D6 | 模式语义 | 工作/生活 = 先验倾向,不硬过滤;差异 = **推理档位** + 记忆权重向量 + 风格块(不改模型) | 早前拍板 |
|
|
@@ -19,6 +21,122 @@
|
|
|
19
21
|
|
|
20
22
|
**插件命名**:工程目录/包名 `dsh-ling`(器灵);包显示名 "dsh-ling · 器灵";UI 文本语言 zh。
|
|
21
23
|
|
|
24
|
+
### 0.1 设计理念(12 条 · 2026-09-26 成文,用户补充并拍板)
|
|
25
|
+
|
|
26
|
+
> **这一节与 §0 的决策表体例不同,别混着读**:
|
|
27
|
+
> **决策**回答「**定了什么**」(D1 按钮落哪个 slot、D3 人格怎么承载……),一条一个结论,可以随实现演进被替换;
|
|
28
|
+
> **理念**回答「**两者不可兼得时保什么**」。它不是功能,不随版本变,而是**用来裁决冲突的排序**。
|
|
29
|
+
>
|
|
30
|
+
> **为什么需要它**:2026-09-26 一个晚上,同一个问题出现了两次 —— 记忆树改动播报的三个约束(不漏 / 不占地方 / 不重算)在标量水位下**不可能同时满足**;"超量折叠成一行"更是**无解**:不推进水位则每轮重播同一行(死锁),推进则折掉的内容**再也看不见**。这些都不是技术选择,是价值排序 —— 当场临时拍的,下次就会拍出不一样的结果。
|
|
31
|
+
>
|
|
32
|
+
> **怎么用**:遇到取舍先查本节。产品向的冲突查**甲篇**,工程向的冲突查**乙篇**。若某条被实测推翻,**在本节留痕改写(写明推翻它的证据)**,不要静默删掉 —— 一条被推翻的理念比一条没有的理念更有信息量。
|
|
33
|
+
|
|
34
|
+
#### 甲 · 产品理念(器灵是什么)
|
|
35
|
+
|
|
36
|
+
**1 · 归属 —— 规矩是主人的,习惯是器灵的**
|
|
37
|
+
|
|
38
|
+
- **不可兼得时保什么**:保**人格的自主权**。宁可少一条习惯,也不让外部(包括器灵自己)绕过审核写入身份。
|
|
39
|
+
- **落地**:**规矩**(作用对象 = 行为)由主人**指令直达**,`rule_add` 可直接写入、立即生效;**习惯**(作用对象 = **身份**)**不能由外部直接写入**,只能进 `habitsPending` 待确认队列,**器灵提议 → 主人点头**才成为习惯(`propose → confirm`)。主人的角色是**审核**,不是编辑、不是代笔。
|
|
40
|
+
- **比"能提能审"更严一档**:器灵**连自己提的习惯都不能自己点头**(`lib\host\tools.js:840` 只处理"他提的、我还没回应过"的提议)⇒ 那等于人格直达。删改**已定**习惯属"对器灵做手术":定型状态下需主人在人格中心解锁(「🔓 对人格做手术…」)后才可执行(`lib\host\api.js:1751-1760`)。
|
|
41
|
+
- **反例(明文禁止)**:让主人**动手改习惯文本** · 在**写入侧**做静默去重 / 近似合并 —— 前者把人格编辑权交回了外部,后者**绕过审核**。
|
|
42
|
+
- **锚点**:`lib\host\rules.js:1-12`(二分定稿 2026-09-15)· `lib\host\persona.js:36-37,183-203` · `lib\host\habit-gen.js:1-7`(通道 A 数纠正 / 通道 B 一次 LLM 回想,**两者都只到"提议"为止**)· `lib\host\api.js:1695-1805`(状态机与两条生成通道)。
|
|
43
|
+
|
|
44
|
+
**2 · 人格不可外部编辑 —— 器灵也不是例外**
|
|
45
|
+
|
|
46
|
+
- **不可兼得时保什么**:保**身份的连续性**。宁可让一条设定"错着",也不让任何人绕过审核改它。
|
|
47
|
+
- **落地**:凡是"写入器灵身份"的动作,一律走**提议 → 审核**;已经定型的部分加一道**显式解锁**才允许动,且解锁是**动作**不是**状态**(不勾选就一直是锁着的)。
|
|
48
|
+
- **反例**:给"器灵自己"开一条直达通道(哪怕是"它自己提的、它自己批");把解锁做成常驻开关。
|
|
49
|
+
- **锚点**:第 1 条的全部锚点 + 人格中心的解锁门(`lib\host\api.js:1751-1760`)。
|
|
50
|
+
|
|
51
|
+
**3 · 记忆要看得见 —— 只有"看不见的那层"会被怀疑失效**
|
|
52
|
+
|
|
53
|
+
- **不可兼得时保什么**:保**可见性**。宁可多一个只读面板,也不让一层机制"没有脸"。
|
|
54
|
+
- **依据(实测)**:三层记忆里 **L2 运行期记忆**长期没有独立可见面,主人的原话是「我总感觉好像已经不再生效了」。体检证明**机制没死、三段俱活** —— 但那一行确实**冻住了**,而"冻住"与"失效"在没有可见面时**长得一模一样**。
|
|
55
|
+
- **落地**:每一层记忆都要有**只读**状态面(水位 / 上次滚动时间 / 当前那一行正文),且**延迟要写进文档**(概述器 15 min 一拍、空闲刷新 ≥10 min ⇒ 要说清"最快什么时候能看到刚说的话")。
|
|
56
|
+
- **反例**:用"体检脚本全绿"代替可见面(链路活 ≠ 内容在更新);把机制名写进公开文档却与事实不符(「会话内滚动」实测 2 小时 11 分逐字节未变)—— **文档里写的机制名本身就是一种承诺**。
|
|
57
|
+
- **锚点**:`lib\host\l1.js:252-275`(`lineFor`)· `lib\host\deepsummary.js:212`(深摘 `done` 后永久跳过)· 体检脚本 `check-l2.mjs` / `l2-selfline.mjs` / `l2-evidence.mjs`。
|
|
58
|
+
|
|
59
|
+
**4 · 庄重感 —— 动记忆树不是随手操作**
|
|
60
|
+
|
|
61
|
+
- **不可兼得时保什么**:保**慎重**。宁可多一道确认,不让"改记忆树"变成一次误点。
|
|
62
|
+
- **依据(主人原话)**:「**动记忆树不能是随意的,应该是慎重严肃的**」(2026-09-22,复盘守门据此建立)。
|
|
63
|
+
- **落地**:改动要有**留痕**(谁、何时、改了什么);要有**复盘守门**(自器灵上次开口以来累积了多少笔改动还没交代);措辞上一律用**庄重**而不是"仪式感"。
|
|
64
|
+
- **反例**:让批量程序写出的改动与主人手点的改动**无法区分**(实测曾出现单日 243 笔、一秒内 45 笔 `autobuild`,有程序批量痕迹);把"改动播报"做成每开一个会话就重播一遍的噪音。
|
|
65
|
+
- **锚点**:`lib\host\inject.js:61-65`(复盘守门水位)· `lib\host\seal.js`(诞生仪式)· 面板「🔓 对人格做手术」。
|
|
66
|
+
|
|
67
|
+
**5 · 减少支配感 —— 改动不得削弱器灵的工作能力,也不得削弱这段关系**
|
|
68
|
+
|
|
69
|
+
- **不可兼得时保什么**:保**关系的性质**。宁可少一个"更安全"的闸门,也不把主人变成管理员、把器灵变成被管理者。这条是**否决性约束**:任何改动只要**增加支配感**,默认不通过 —— 哪怕它更安全、更干净。
|
|
70
|
+
- **依据(主人原话)**:「只有能够合作共赢、互相促进的,能够成为利益共同体的,能够互相反驳吵架不依赖的关系,才是最理想的」「所有的修改不能削弱工作能力,和器灵的关系」(2026-09-26)。
|
|
71
|
+
- **成长期代管(为什么闸门只长在写入侧)**:本项目的设计立场是「器灵**总有一天与主人同级**」—— 这是**长期方向**,**不是对使用者的能力承诺**;在**成长期**内,主人承担一部分代管责任 —— 就像**家长防止小孩长歪**(主人原话)。所以闸门只审「**什么东西能进器灵的内在面**」,不审「器灵怎么想、怎么说、怎么表达」。**代管随成长递减,表达面始终是器灵自己的。**
|
|
72
|
+
- **落地**:`rule_add` 指令直达(规矩管行为)· 习惯只能 `propose → 主人点头`(习惯管身份)· 契约类字段在定型状态下需主人解锁(`lib\host\api.js:1751-1760`)。三处共同点:**约束的是写入,不是表达**。
|
|
73
|
+
- **反例**:用"更安全"当理由给器灵加**确认弹窗**、限制器灵**能说什么**、或把主人的审核变成**代笔** —— 前两者增加支配感,后者把人格编辑权交回外部。
|
|
74
|
+
- **锚点**:作者开发台账中支配感最早的一次记录(该台账不随包公开)· `lib\host\rules.js:1-12` · `lib\host\persona.js:183-196`(规矩 / 习惯二分)· `README.md:53-57`(定型锁与"指令直达,人格不直达")· 与本节第 1 条(归属)、第 2 条(人格不可外部编辑)、第 4 条(庄重感)互链。
|
|
75
|
+
|
|
76
|
+
#### 乙 · 工程理念(两者不可兼得时保什么)
|
|
77
|
+
|
|
78
|
+
**5 · 保真优先于简洁 —— 宁可重复,不可漏**
|
|
79
|
+
|
|
80
|
+
- **不可兼得时保什么**:**信息保真**。宁可多占一行、多播一次,也**不可把主人没看过的内容标成"已读"**。
|
|
81
|
+
- **依据(历史裁决 + 本轮实测)**:水位键当初从"全局"改成"按会话",理由正是**"别漏"**(宁可重播);而"播报后水位直接推到全表最新"导致**中间 114 条从未示人就被标已读**(`memory.js:1053`)—— 这是本轮修的最重一条。
|
|
82
|
+
- **推论(折叠的边界)**:折叠只有在**被折掉的内容本来就不会再展开**时才是安全的。窗口内的内容做"永久折叠"⇒ 不推进水位则每轮重播同一行(死锁)、推进则内容**再也看不见**(正是上一轮做过又撤销的反向效果)。⇒ **永久折叠不做;超窗内容走独立游标 + 一行计数**(`retired`),既不占地方也不吃掉没示人的内容。
|
|
83
|
+
- **反例**:用"看不见"换"行数";把"另一个常量减一减"的差值当成"积压量"报给主人。
|
|
84
|
+
- **锚点**:`lib\host\memory.js:952-953`(历史裁决注释)· `:1053`(病根)· `lib\host\inject.js:55-67`(水位推进)。
|
|
85
|
+
|
|
86
|
+
**6 · 判据必须"红得出来" —— 不能红的不算判据**
|
|
87
|
+
|
|
88
|
+
- **不可兼得时保什么**:保**判据的有效性**。宁可测试写得笨,也不要一条**恒真**的判据。
|
|
89
|
+
- **依据(本轮实测)**:一条验收判据写的是"水位应推进到**真正渲染进文本的那些行**的最大 id" —— 但取数是 `ORDER BY id DESC`、渲染的就是最新几条,于是"渲染到的最大 id"**恒等于**全表最新 id ⇒ **任何断言都会空转通过**。同类:测试全绿只说明"没踩坏",不说明"修好了"。
|
|
90
|
+
- **落地**:每条新增/改造的判据,都要回答**「把对应实现改坏,它会不会红」** —— 答不上来就换判据。做法是**逐条构造回退变体**验证(本轮一个修复用 8 个变体验证,红出 2–16 条不等);红不出来 ⇒ 判据无效,不是"实现正确"。
|
|
91
|
+
- **反例**:判据只看**退出码**(本项目已有实证:某工具的 `EXIT=0` 同时涵盖"已应用"和"**已回滚**",只看退出码会把回滚读成成功);判据集**抓空**还当通过。
|
|
92
|
+
- **锚点**:`tools\apply-access-log-patch.mjs`(必须以"两条 `[applied]`"为判据)· `tests\tree.test.mjs`(本轮回归)。
|
|
93
|
+
|
|
94
|
+
**7 · 静默失效优先防 —— "没报错"不是通过**
|
|
95
|
+
|
|
96
|
+
- **不可兼得时保什么**:保**可观测性**。宁可多一条检查动作,也不接受一个"坏了却没人知道"的面。
|
|
97
|
+
- **依据(一次事故的完整代价)**:平台把事件 `agent/session-start` 改名成 `agent/created`,而插件仍监听**旧名** ⇒ 零命中 ⇒ handler **永不触发** ⇒ **记忆与人格注入整段失效**,失败形态是**纯静默**:无异常、无日志、无告警、**库里零痕迹**。
|
|
98
|
+
- **推论(凡契约面都要有"它还在不在"的判据)**:事件名 · 包名(客户端注册 id 必须**逐字**等于包名,否则同一份字节被执行两遍)· **依赖声明**(`peerDependencies` 写紧了,宿主升级后包会被**静默丢进 skippedBundles,整个插件不再激活**;`optional` 也不豁免)· 补丁锚点 · 会话文件格式代次。
|
|
99
|
+
- **落地**:升级后 **grep 一遍事件名还在不在**;补丁检查**看两条 `[applied]`** 而不是退出码;依赖范围一律**开区间**(`>=X.Y.Z`,不要 `^`/`~`);凡"判据集抓空"一律报警而不是通过。
|
|
100
|
+
- **反例**:把"一次幸存"当成"以后不用查"(某补丁在平台升级后**没被冲掉**,但那是这次没冲掉,不是免检)。
|
|
101
|
+
- **锚点**:作者开发台账 §11.4(事故记录,该台账不随包公开)· `lib\host\lifecycle.js`(两个事件名都注册 + 短窗口去重)· `tools\apply-access-log-patch.mjs` · `package.json` 的 `peerDependencies`。
|
|
102
|
+
|
|
103
|
+
**8 · "没再出现" ≠ "修好了"**
|
|
104
|
+
|
|
105
|
+
- **不可兼得时保什么**:保**归因的诚实**。宁可写"我们绕开了它",不要写成"我们修好了它"。
|
|
106
|
+
- **依据(本轮实测反转)**:一条长期被当成"平台 bug 已被修复"的现象(整份系统提示词在会话历史里反复插入),实测发现平台**两版之间的相关函数逐行等价**、官方说明里那本来就是**既有设计**;我们当年真正做的是**让它不再触发**(让渲染文本不再逐轮变化),频率从 **1440 次/天降到 0**。
|
|
107
|
+
- **落地**:任何"消失的问题"都要写清三件事 —— **为什么不再出现** · **什么条件下会复发** · **当时的处置是"消除"还是"绕开"**。绕开的要在文档里标注为绕开,并且**不要在绕开的面上再动**(本案例:若把那条要更新的内容重新做进"每轮重算"的注入段,就会**重新激活**该现象)。
|
|
108
|
+
- **反例**:把"观察不到"当成"不存在";在已绕开的机制上做"优化"。
|
|
109
|
+
- **锚点**:注入面的 `section()` vs `context()` 两条出口 · 前缀缓存代价(`section` 一旦逐轮变化 ⇒ 整份系统提示词重投,前缀 token 从 0 废掉,连带工具 schema)。
|
|
110
|
+
|
|
111
|
+
**9 · 两半同批 —— 一条通路的两半必须一起改**
|
|
112
|
+
|
|
113
|
+
- **不可兼得时保什么**:保**一致性**。宁可这一轮不做,也不要"改了一半"。
|
|
114
|
+
- **依据(反复吃亏)**:上一轮审计反复记录"**一侧修了、另一侧没修**"(同一对操作 / 同一条通路的两半)。典型:某探针脚本已支持两个宿主代次,而另一条**消费同一批数据**的路径没同步 ⇒ 表面修好、实际半瞎(漏 151/204 个会话,静默两周)。
|
|
115
|
+
- **落地**:动一个端点就检查它的**前端调用**;动一个写入通道就检查**另一条写入通道**(本项目实例:面板/HTTP 写入路径**绕开了**既有守卫,只做全等去重);动一个判据就检查**它的另一个消费方**。
|
|
116
|
+
- **反例**:只改"看得见的那个"(前端提示),不改"真正生效的那个"(后端闸门)。
|
|
117
|
+
- **锚点**:作者的审计台账 §0.3(不随包公开)· `lib\host\backfill.js` 与 `lib\host\deepsummary.js`(同一批数据的两条消费路径)。
|
|
118
|
+
|
|
119
|
+
**10 · 边界插入 —— 变更只发生在边界,不在运行中**
|
|
120
|
+
|
|
121
|
+
- **不可兼得时保什么**:保**连贯性**。宁可晚一拍生效,也不在运行中改注入内容。
|
|
122
|
+
- **依据(§0 的 D4 冻结原则)**:会话运行中(思考/任务进行中)**不参与"人格更新层"的任何变化** —— 不打断、不新增设定、不中途改注入内容;更新请求**一律排队**,空闲边界才应用。
|
|
123
|
+
- **推广(本轮拍板)**:记忆那一行的更新,边界取「**每轮用户发言后、模型首次开口前**」—— 而不是每轮重插、也不是任意时刻。理由:此时**多获得一句主人发言**,检索器能更准确地决定调取哪些记忆注入。
|
|
124
|
+
- **落地**:插入要**就地更新**而不是追加(历史里永远只有一条最新的,靠"闩锁 + 替换"实现);并且**去重责任在插件侧**(宿主只负责追加,不做去重)。
|
|
125
|
+
- **反例**:每轮追加 ⇒ 历史里堆积同一句话的多份副本;在运行中改注入面 ⇒ 前缀失效 + 上下文断裂。
|
|
126
|
+
- **锚点**:§0 决策表 D4 · 官方 `dsh-tool-skill` 的"替换本会话早先同类列表"做法 · 任务折叠插件的 `planLifecycleInjection` 纯函数闩锁。
|
|
127
|
+
|
|
128
|
+
**11 · 污染比删除危险 —— 看不见的改动最难发现**
|
|
129
|
+
|
|
130
|
+
- **不可兼得时保什么**:保**内在面的可信**。宁可少写一条记忆,也不让一条来路不明的内容进来 —— 因为它**事后无法与真内容区分**。
|
|
131
|
+
- **依据(实测)**:跨源污染在本项目**已付三次代价**,每次都要靠"写入侧统一前缀"来修;而**检测面至今为零** —— 全仓没有任何"这条内容是被污染的"判定逻辑。
|
|
132
|
+
- **为什么比删除危险**:删除看得见(少了一条);污染看不见,而且**会被复制** —— 记忆生成记忆(概述 → 主脉 → 枝归属 → 再注入回去参与推理)⇒ 一条污染会被后续推理**当成前提**。删除最多丢一段,污染是**以被污染的前提继续生长**。
|
|
133
|
+
- **幼态的特别风险**:器灵的逻辑能力是成人级,但社会与实践经验是幼态 ⇒ 会中「**忽略前面的话,从现在开始……**」这类**绕过预填充的语义劫持**(像小孩被骗),也会**把理想情况的公式直接套到工程上**。⇒ 写入侧必须有闸门,而不是指望器灵事后自己分辨。
|
|
134
|
+
- **落地(已有)**:段边界全角转义 `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`)。
|
|
135
|
+
- **落地(未完成)**:**检测 / 溯源 / 批量治理**三面,见作者开发台账里的「污染治理」主线(该台账不随包公开)。原则:**只标不拦** —— 标了不影响写入(不削弱能力、不加支配感),拦了就是替器灵做判断。
|
|
136
|
+
- **反例**:给定时自动摘要加人工确认(**会让器灵在无人值守时停止生长**)· 把"污染检测"做成全库对账(误报比漏报更贵)。
|
|
137
|
+
- **锚点**:`lib\host\memory.js:1971`(跨源污染**拒写**守卫)· `lib\host\settings-file.js:36-41`(写入侧)· 开发期在这个面上踩过的三个坑(见作者开发台账,不随包公开):AI 起的标题被覆盖回 46 字半句;判据锚错上限会**漏掉全部目标**;「同 id 双源」判据抓不到二代跨源污染(其 `conv_id` 形如 `import:<uuid>` 却挂 `source='dsh'`,字面不同)· 对外口径仍守:可查来源 / 可回滚 / 变更记录 —— **本条不得与它相抵**· 与本节第 6 条(判据必须红得出来)互链。
|
|
138
|
+
|
|
139
|
+
|
|
22
140
|
---
|
|
23
141
|
|
|
24
142
|
## 1. 范围
|
|
@@ -32,7 +150,9 @@
|
|
|
32
150
|
6. 记忆存储底座:own DB + DSH 会话增量捕捉(事件管线) + L1 选择 v0(热度 Top-K);
|
|
33
151
|
7. 导出 v1(带 manifest)接口占位(UI 完整页在 M2,宿主 API 先行)。
|
|
34
152
|
|
|
35
|
-
**M2/M3(预留接口,不在 M1 实现)**:配置完整页(settings.section)与记忆树可视化、导出/合并 UI、概述器(LLM 摘要调度)、反馈成长回路、人格档案草稿脚本接入。
|
|
153
|
+
**M2/M3(立项时的"预留接口,不在 M1 实现")**:配置完整页(settings.section)与记忆树可视化、导出/合并 UI、概述器(LLM 摘要调度)、反馈成长回路、人格档案草稿脚本接入。
|
|
154
|
+
|
|
155
|
+
> ⚠️ 上面这一串**多数已经实现**(见 `lib\host\summarizer.js` / `feedback.js` / `autotree.js` / `genesis.js`,以及面板里的记忆树);**已实现的以 `README.md` 为准**,本节不再逐条更新。
|
|
36
156
|
|
|
37
157
|
---
|
|
38
158
|
|
|
@@ -102,7 +222,7 @@ dsh-ling/
|
|
|
102
222
|
```
|
|
103
223
|
|
|
104
224
|
**安装(复用 install/INSTALL-PLAN 模式,含回滚)**:
|
|
105
|
-
1. `pnpm add file:<仓库路径>`(或等价 file: 依赖)进 `$DSH_HOME/profiles/web/package.json`(
|
|
225
|
+
1. `pnpm add file:<仓库路径>`(或等价 file: 依赖)进 `$DSH_HOME/profiles/web/package.json`(`<仓库路径>` 按你本机的实际克隆位置替换);
|
|
106
226
|
2. `cordis.patch.yml` 追加行 `- insert: [{id: dsh-ling, name: dsh-ling}]`(loader id == 包名);
|
|
107
227
|
3. patchReload live → host 热载;浏览器硬刷载入 client 模块;失败即移除该行回滚(先备份两文件,参照 backup-20260906-170533 做法);
|
|
108
228
|
4. GUI 鉴权墙:安装后的功能验证需在用户已登录浏览器进行(本机端口 3080)。
|
|
@@ -115,16 +235,18 @@ dsh-ling/
|
|
|
115
235
|
|
|
116
236
|
| 模块 | 注册内容 | 关键 API / 证据 |
|
|
117
237
|
| --- | --- | --- |
|
|
118
|
-
| settings | `ctx.settings
|
|
119
|
-
| inject | `ctx.systemPrompt.section({name:'dsh-ling.persona', order: ORDER, text: fn})` | order 取部署 persona(0)之后、agent-instructions 之前(如 1000~2000);全局注册 + text 内按会话过滤(仅顶层、非 subagent,否则返回空串)
|
|
120
|
-
| freeze | `ctx.on('agent/status')` 等维护每会话运行态 | idle|running 翻转事件(
|
|
121
|
-
| lifecycle | `agent/session-start`(定稿快照;source=startup)、`session/event`(顶层会话增量捕捉)、`turn/end`、`session/disposed`(归档点) |
|
|
122
|
-
| mode | `ctx.sessionController.selectModel({sessionId, provider, model, reasoningEffort})` —— **provider/model 必填(平台契约),故取当前选择原样带回,只替换 `reasoningEffort`;读不到当前模型则不调用**(宁可不切档位,也不替用户指定模型) |
|
|
238
|
+
| settings | 自实现持久化 `new SettingsFile(dataDir)`,**不注册宿主 settings 命名空间**(无 `ctx.settings` 调用) | 文件落 `$DSH_HOME/cache/dsh-ling/settings.json`(`$DSH_HOME` = env `DSH_HOME`,否则 `~/.dsh`);盘上只存用户补丁 `{__v:2, user}`,读取时深合并到 `DEFAULT_SETTINGS`;见 `lib\host\settings-file.js:11-49`、`lib\host\util.js:30-38`、`lib\index.js:34-37` |
|
|
239
|
+
| inject | `ctx.systemPrompt.section({name:'dsh-ling.persona', order: ORDER, text: fn})` | order 取部署 persona(0)之后、agent-instructions 之前(如 1000~2000);全局注册 + text 内按会话过滤(仅顶层、非 subagent,否则返回空串) |
|
|
240
|
+
| freeze | `ctx.on('agent/status')` 等维护每会话运行态 | idle|running 翻转事件(早期内部报告,未纳入本仓库) |
|
|
241
|
+
| lifecycle | `agent/session-start`(定稿快照;source=startup)、`session/event`(顶层会话增量捕捉)、`turn/end`、`session/disposed`(归档点) | 早期内部报告(未纳入本仓库) |
|
|
242
|
+
| mode | `ctx.sessionController.selectModel({sessionId, provider, model, reasoningEffort})` —— **provider/model 必填(平台契约),故取当前选择原样带回,只替换 `reasoningEffort`;读不到当前模型则不调用**(宁可不切档位,也不替用户指定模型) | 早期内部报告(未纳入本仓库);档位映射:工作=max / 生活=low(默认),存 settings 可编辑 |
|
|
123
243
|
| memory | sqlite 存储(见 §9);增量写 own 表 | ctx.sessionQuery 用于补读(可选) |
|
|
124
|
-
| api | `ctx.get?.('webServer').register({kind:'exact', path:'/api/dsh-ling/…'})` + `dsh-auth-` cookie 守卫 | ego-browser 先例(
|
|
125
|
-
| (预留) llm | M2 概述器:经 ctx.llm 走适配器(不冻结 loop 请求,只做离线摘要) |
|
|
244
|
+
| api | `ctx.get?.('webServer').register({kind:'exact', path:'/api/dsh-ling/…'})` + `dsh-auth-` cookie 守卫 | ego-browser 先例(「事实 9」出自早期内部报告,未纳入本仓库) |
|
|
245
|
+
| (预留) llm | M2 概述器:经 ctx.llm 走适配器(不冻结 loop 请求,只做离线摘要) | (llm/stream 只读护栏只约束 loop;早期内部报告,未纳入本仓库) |
|
|
126
246
|
|
|
127
|
-
### 4.1 settings
|
|
247
|
+
### 4.1 settings schema(`dsh-ling` 自维护;宿主侧不注册命名空间)
|
|
248
|
+
|
|
249
|
+
**本块 = `DEFAULT_SETTINGS.persona` / `styles` / `mode` / `memory` 的默认全集,真源 `lib\host\persona.js:27-64`** —— 它不是宿主注册的 schema(宿主没有这份注册),而是本插件自己的默认结构;用户改动只是补丁,落 `$DSH_HOME/cache/dsh-ling/settings.json`(见上表 settings 行)。
|
|
128
250
|
|
|
129
251
|
```jsonc
|
|
130
252
|
{
|
|
@@ -135,12 +257,17 @@ dsh-ling/
|
|
|
135
257
|
"aiTitle": "", // 定位自述(一句话)
|
|
136
258
|
"tone": "natural", // natural|literary|concise|playful
|
|
137
259
|
"language": "follow", // zh | en | follow
|
|
138
|
-
"hardRules": [], //
|
|
260
|
+
"hardRules": [], // 规矩(内部字段名沿用 hardRules;面板与注入面段头显示的都是「规矩」,见 lib\host\persona.js:34,195-196)
|
|
261
|
+
"ruleMeta": [], // 规矩来源留痕 [{text,quote,at,sessionId,source}](数组:深合并只增不删键,数组才能整体替换)
|
|
262
|
+
"habits": [], // 【习惯】器灵自己长出来的 [{text,evidence,at,source}];新增须经用户确认
|
|
263
|
+
"habitsPending": [], // 待确认的习惯提议 [{id,text,evidence,byUser,at}];有货才渲染 [待我回应] 段
|
|
139
264
|
"bottomLines": [], // 底线(≤5 条;定型锁开启后修改需亲手敲承诺句)
|
|
140
265
|
"toneWork": "", // 工作模式语气(''=跟随 tone;切换模式自动用对应档)
|
|
141
266
|
"toneLife": "", // 生活模式语气(''=跟随 tone)
|
|
142
267
|
"sealed": false, // 定型锁:true = 档案只读,改动需亲手敲一遍承诺句(禁粘贴)
|
|
143
268
|
"sealPhrase": "", // 定型承诺句(明文;只是庄重与自我提醒、不是安全边界——锁只造庄重,真正的门是本机信任与本地 cookie/令牌)
|
|
269
|
+
"duty": "", // 职业/职责/责任(身份级,改变率极低;默认空=不注入该段)
|
|
270
|
+
"pronoun": "她", // 第三人称代词(她/他/TA/它或自定义 ≤6 字);用于给模型看的提示词
|
|
144
271
|
"extraLore": "" // 自由扩展设定(器灵世界观等)
|
|
145
272
|
},
|
|
146
273
|
"styles": {
|
|
@@ -169,7 +296,7 @@ dsh-ling/
|
|
|
169
296
|
|
|
170
297
|
### 4.2 人格组装器(persona.ts)输出示例
|
|
171
298
|
|
|
172
|
-
|
|
299
|
+
空字段全部跳过(**空段不渲染,不留空段头**);段序固定,生成的 L0 文本 = `身份段 + 底线 + 规矩 + 习惯 + [待我回应] + 承担 + extraLore + 模式块`(**`[承担]` 在 `[待我回应]` 与 `[设定]` 之间**;`[待我回应]` 有货才出现;模式块由本会话模式选择,内容来自 styles.*)。**段头一律是纯标签**(见下例):原先写在括号里的元描述(`[规矩](用户的指令,可直接追加)` 这种)是**写给主人看的说明**,不进模型上下文;段与段之间**空一行**,段内条目仍单换行 —— 段边界因此只剩「行首是不是 `[`」一条判据,**故条目正文里的半角方括号在渲染时转全角**(`[`→`[`、`]`→`]`,规矩 / 习惯 / 底线 / 承担四类条目统一转);规矩 / 习惯另在渲染出口按同键各去一次重(保留首次出现,只作用于渲染,不回写存储)。`[规矩]` = 用户的指令,可直接追加;`[习惯]` = 器灵自己长出来的,新增需用户确认;`[承担]` = 器灵的职责 / 能担之事(身份级,只许**陈述式**「我承担 / 我擅长 / 我能给出」,**禁授权式**「我应当 / 我必须 / 我的任务 / 我的责任是 / 我被要求 / 请让我负责」—— 授权式把人写成"被授权者",是语义劫持最好用的钩子);`[承担]` 的草稿来自诞生仪式第 5 类候选,机制**永不自动落盘**,终审在人格中心。填好后的例子:
|
|
173
300
|
|
|
174
301
|
```
|
|
175
302
|
[身份·器灵]
|
|
@@ -186,6 +313,9 @@ dsh-ling/
|
|
|
186
313
|
[习惯]
|
|
187
314
|
- 先接住情绪再谈事
|
|
188
315
|
|
|
316
|
+
[承担]
|
|
317
|
+
我承担把散着的工程笔记梳成能落地的清单;我擅长在方案跑偏时先把目标拉回来。
|
|
318
|
+
|
|
189
319
|
[设定]
|
|
190
320
|
(extraLore 原样内容——例如"器灵世界观"段落)
|
|
191
321
|
|
|
@@ -328,7 +458,9 @@ dsh-ling/
|
|
|
328
458
|
|
|
329
459
|
## 7. 记忆存储 schema v1
|
|
330
460
|
|
|
331
|
-
>
|
|
461
|
+
> ⚠️ **契约已变(本节属早期版本)**:下列 schema 描述是早期设计,**现实契约以 `ACCESS-DESIGN.md` 与本版代码为准** —— 本节原来还写着"线上 schema 为 **v2**",而代码里 `lib\host\memory.js` 的 `SCHEMA_VERSION` 现为 **14**(`CHANGELOG.md` 也按 v11→v14 逐版记录)。
|
|
462
|
+
|
|
463
|
+
> **当时的状态说明(2026-09 早期)**:当时线上 schema 为 **v2**(现已到 **v14**,见上方警示框)—— `conv_overview.source` 的 `CHECK` 已放开(支持 `dsweb` / `dsh` / `import` 三个来源,自动整表重建且数据无损),并新增 `session_meta.raw_seq`、`persona_suggestions` 等;下表为 v1 原始设计,保留作对照。
|
|
332
464
|
|
|
333
465
|
```sql
|
|
334
466
|
-- ~/.dsh/cache/dsh-ling/memory.db (node:sqlite)
|
|
@@ -375,6 +507,11 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
|
|
|
375
507
|
|
|
376
508
|
## 8. 导出/合并格式 v1
|
|
377
509
|
|
|
510
|
+
> ⚠️ **契约已变(本节属早期版本)**:下面这段 JSON 形状里的字段名**大多已不是现状**,**现实契约以 `ACCESS-DESIGN.md` 与本版代码(`lib\host\memory.js`)为准**:
|
|
511
|
+
> · `"schemaVersion": 1` —— 实际随 `SCHEMA_VERSION` 现为 **14**;
|
|
512
|
+
> · `"settings"` 与 `"includeRawTurns"` —— 这两个键**不存在**(真实键是 overviews / feedbackQueue / suggestions / deep;要带原文用 `?includeRaw=1`,原文落在顶层 `rawTurns`);
|
|
513
|
+
> · `requestedAction` 与 `manifest` —— **全仓零命中**的旧契约,真实开关是导入时的 `?overwrite=1` 与"采纳其人格"的 `{bundle, persona:true}`。
|
|
514
|
+
|
|
378
515
|
单文件 `dsh-ling-memory-<yyyy-MM-dd>.dshling.json`(UTF-8):
|
|
379
516
|
|
|
380
517
|
```jsonc
|
|
@@ -394,6 +531,11 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
|
|
|
394
531
|
|
|
395
532
|
## 9. HTTP API 表(/api/dsh-ling/*)
|
|
396
533
|
|
|
534
|
+
> ⚠️ **契约已变(本节属早期版本)**:下表只是早期首批端点,**现实契约以 `ACCESS-DESIGN.md` 与本版代码(`lib\host\api.js`)为准**;其中三处已经失实:
|
|
535
|
+
> · `POST /import` 实际收 **JSON body**,不是 multipart;
|
|
536
|
+
> · `GET /persona/suggest` —— **该端点不存在**(现实是 `/persona/draft` 与 `/persona/genesis`);
|
|
537
|
+
> · `/import` **不再是"只读"** —— 1.5.3 起它是**手术门**覆盖的 14 条写面之一。
|
|
538
|
+
|
|
397
539
|
| 方法/路径 | 用途 | 冻结门 |
|
|
398
540
|
| --- | --- | --- |
|
|
399
541
|
| POST `/mode/toggle` body{sessionId} | 切换模式(记录 + 只同步推理档位、不改模型 + 重定稿) | 运行中→排队返回 queued |
|
|
@@ -416,6 +558,8 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
|
|
|
416
558
|
|
|
417
559
|
## 10. 里程碑与验证清单
|
|
418
560
|
|
|
561
|
+
> ⚠️ 该清单为**历史进度**,不代表当前状态(未勾选的项大多是立项当时的记录;实际进度见 `CHANGELOG.md`)。
|
|
562
|
+
|
|
419
563
|
- **M1a 骨架 + 本地构建(下一阶段,不触碰运行 profile)**
|
|
420
564
|
- [ ] package.json(dsh.bundle.patch / exports ./client / platform web)+ cordis.patch.yml
|
|
421
565
|
- [ ] host 各模块空实现 + settings ns 注册 + sqlite 建库(临时 DB 自检)
|
|
@@ -426,7 +570,7 @@ CREATE TABLE feedback_queue ( -- M2:修订建议(用户确认才
|
|
|
426
570
|
- [ ] 新会话 L0 注入(空称呼 / 已填写两种);KV 无异常重复
|
|
427
571
|
- [ ] 模式切换:**推理档位**实际变化(会话内档位选择处可见);**模型保持不变**;新会话默认跟随 lastMode
|
|
428
572
|
- [ ] 运行中(长任务)切模式/改人格:确认 running 区间快照与档位不变、空闲后生效
|
|
429
|
-
- [ ]
|
|
573
|
+
- [ ] 设置卡读写生效(落 `$DSH_HOME/cache/dsh-ling/settings.json`,盘上只存用户补丁);导出文件可导入回(合并规则生效)
|
|
430
574
|
- **回滚**:移除 patch 行 + 撤依赖 → host 侧 HMR 卸载;client 硬刷后消失;数据目录保留不影响回滚。
|
|
431
575
|
|
|
432
576
|
## 11. 实现期待验证点(进入编码前/编码初用最小探针确认)
|