@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/README.md CHANGED
@@ -41,21 +41,26 @@
41
41
 
42
42
  ---
43
43
 
44
- ## 器灵是什么
44
+ ## 器灵是什么(能力全景)
45
+
46
+ > 下表是**全部能力的一览**:`✅ 已实现` 是今天就能用的;`🚧 在做` 是**设计已定、代码正在落地**(写在这里是为了让设定走在实现前面,不是"已交付");`📋 设定` 是方向已定、尚未开工。**没有标的一律是 ✅ 已实现。**
45
47
 
46
48
  | | 能力 | 说明 |
47
49
  |---|---|---|
48
- | 🔒 | **人格定型锁** | **契约类**(名字、称呼、底线、世界观、代词、承担(职责))定型后修改需**亲手敲一遍承诺句**(禁粘贴),并进变更记录可回滚;**自性类**(语气、风格、自述、习惯)由器灵生长、你点头即成:**三条专用成长通道**(自我总结 / 建议采纳 / 生长)不问承诺句,但它们各自只动白名单内的字段;走**通用人格接口**改 persona / styles 时,即便只改语气也仍要承诺句 —— 免手术的只是那三条通道,不是"自性类"这三个字 |
50
+ | 🔒 | **人格定型锁** | **契约类**(名字、称呼、底线、世界观、代词、承担(职责))定型后修改需**亲手敲一遍承诺句**(禁粘贴),而且**只在本机做得成、每次启动还要取一次手术票据**,再进变更记录可回滚;**自性类**(语气、风格、自述、习惯)由器灵生长、你点头即成:**三条专用成长通道**(自我总结 / 建议采纳 / 生长)不问承诺句,但它们各自只动白名单内的字段;走**通用人格接口**改 persona / styles 时,即便只改语气也仍要承诺句 —— 免手术的只是那三条通道,不是"自性类"这三个字 |
49
51
  | 🌳 | **记忆树 · 复盘模式** | 记忆不是一叠平铺的行,而是**一条主干 + 分枝 + 主脉**的工作线结构,枝之间可连边;检索按**血缘档位**加权(同枝 1.0 / 祖先 0.7 / 旁系 0.4),让器灵分得清"这轮在做的"和"三个月前那次闲聊"。**任何树操作都不改写任何一条记忆的内容** —— 分枝、并脉、连边只增加关系。**例外一个**:主脉提炼**再次采纳**时会覆盖该主脉记忆的上一版(**覆盖前自动归档**,注入面也会说明"旧版已归档")。改树走**复盘**:平时只读,进入复盘才解锁,每步留痕(改了什么、为什么),改完必须让器灵看到才准退出(详见下节) |
50
52
  | 🧠 | **三层记忆** | **L0 人格档案**(自称/对你的称呼/底线/规矩·习惯/承担(职业·职责·责任)/语气/世界观;**顶层会话与会话分叉**恒定注入,**子代理会话不注入**;设置里关掉人格注入时,连它一起不推)→ **L1 开场记忆**(从历史里按相关度挑选的概述,有预算,**永不塞全文**;带摘要的行**加权**(按"摘要是否有信息量"给一笔加法加分,不是排序优先级),行内摘要只取首句、上限 110 字)→ **L2 运行期记忆**(本会话原文随对话累进;空闲时把本会话那一行重排回注入面。该行由「概述标题 + 摘要首句」构成(标题的来历见下文「三层记忆各自什么时候更新」,不是会话原文首句),**深摘要定稿后不再自动更新**。注意:L2 不是独立的第三块注入内容 —— 注入面的主体是 L0 与「记忆·开场」两块(树改动、日期锚是挂上去的追加行),**本会话自己就是「记忆·开场」里的一行**) |
51
53
  | 📥 | **把历史请进来** | 三条通道:网页端聊天记录库增量扫描 / 本机 DSH 会话扫描 / ChatGPT·通用 JSON·JSONL 导出文件导入;每次导入**留一笔账** |
52
54
  | 📖 | **让器灵读一遍** | 长会话逐条深摘要、短会话以原文片段参与,后台串行,进度可见 |
55
+ | 📄 | **找回原文**(1.6) | 器灵能读回你当年的**原话**,不只是概述 —— 四档下钻:**会话地图 → 按内容找会话 → 对话目录 → 原文全文**。它**不替你挑"最重要的那 1 万字"**,只给坐标系(详见下文「找回原文」一节) |
56
+ | 🧾 | **投递留痕**(1.6) | 🚧 **在做** —— 每一次记忆注入都落一条账:**注入了谁、为什么是它、多少字**;还记**落选者**(差一步就进的那几条,以及原因是"分数不够"还是"字数放不下")与**触发词**。目前**只写不读**(读面待定),不进注入面、不占工具预算 |
57
+ | 🌿 | **深层库 · 结论层**(1.6) | 🚧 **在做** —— 从会话里提炼**承诺 / 决定 / 事实 / 偏好 / 关系**,每条一句话、**点得回原文**。与记忆树**并立**,条目不被枝隶属(枝可反向引用它,像榕树的气根)。一条纪律:**"被注入"不算数,只有"真被采用"才加固** —— 防住"注入越多→分越高→越容易被注入"的自我强化 |
53
58
  | 🧾 | **给旧历史补摘要** | 网页端历史里"只有标题、没有摘要"的会话可以补上摘要:先看**候选清单**(标题 · 轮次 · 命中),再选引擎 —— **默认本机小助手(零成本)**,也可显式改选当前全局大模型;探测不到本机小助手就明确报错,**绝不静默换引擎**;后台串行、进度可见(首轮实测 466 条、零成本) |
54
59
  | 🌱 | **诞生仪式** | 从读过的记忆里按"生活与情感优先"加权抽样 → 合成**自述 + 名字候选 + 语气建议 + 相处观察 + 职责候选**(器灵承担 / 擅长 / 能给出什么,**只收陈述句**) → **全部是草稿**,你逐条人审后才落盘 |
55
60
  | ⏳ | **时间的重量** | 每次开口前器灵都知道今天是几号星期几(`[今天] 2026-09-11 周五`)、现在是什么时段(`[现在] 深夜`)与`距上次对话:3 天前`(间隔按**日历天**分档 —— 刚刚 / 今天早些时候 / 昨天 / N 天前,给不到分钟;称呼跟随你的设定);命中纪念日会先说一句专属问候。**要精确到分钟,走 `clock` skill 现查** |
56
61
  | 🎭 | **工作 / 生活双模式** | 同一份人格档案,两种关系:工作模式是同事(正式称呼、简洁、知识优先),生活模式是伴侣(昵称、活泼、情感优先) |
57
62
  | 🔁 | **成长回路** | 你点踩或纠正 → 器灵的成长通道把重复出现的模式提炼成**习惯候选** → **你点头才成为习惯**;"让她读记忆、归纳语气"一键成长 |
58
- | 📜 | **规矩 / 习惯** | **规矩**是你的指令:可直接加(面板,或会话里一句"记进规矩" —— **器灵替你写时必须带上你的原话作凭据**;你在面板上直接敲的那一下就是同意,不必再附原话),**建议上限** 12 条(超出只在写入后提醒合并,**不拦**);**习惯**是器灵自己长出来的:新增需你点头、**删改走手术门**,**建议上限** 8 条(超出同样只提醒、不拦) —— **指令直达,人格不直达** |
63
+ | 📜 | **规矩 / 习惯** | **规矩**是你的指令:可直接加(面板,或会话里一句"记进规矩" —— **器灵替你写时必须带上你的原话作凭据**;你在面板上直接敲的那一下就是同意,不必再附原话),**建议上限** 12 条(超出只在写入后提醒合并,**不拦**);**习惯**是器灵自己长出来的:新增需你点头、**删改走手术门**(本机 + 每次启动取一次票据 + 原句,详见下文那道门的说明),**建议上限** 8 条(超出同样只提醒、不拦) —— **指令直达,人格不直达** |
59
64
  | 🌾 | **习惯生成器** | 「扫描纠正记录(零模型)」:把器灵反复被你纠正的模式数出来(≥4 次且跨 ≥3 个会话);「✨ 让她回想最近的相处」:一次读 200 条概述 + 800 条你的原话、逐轮推进不重复 —— **只提议,不落盘** |
60
65
  | 🗣 | **代词** | 默认「她」;可选 她 / 他 / TA / 它,或自定义(≤6 字)。**祂 不在预设里**(中文里专指神祇,设为默认等于替你宣称神性);属契约类,改动走手术门 |
61
66
  | ♻️ | **记忆自己追平** | 长会话空闲时自动把开场记忆刷新到最新(先跑本会话增量概述 → 比对记忆版本 → 同一条至少间隔 10 分钟),**运行中绝不打断**;刷新次数与最近一次时间在 `/state` 接口的 `idleRefresh` 字段里(`count` 与 `lastAt`)(面板本身只显示"已定稿 N 字符 · mode · 待刷新",不显示次数) |
@@ -63,6 +68,14 @@
63
68
 
64
69
  > **关于那道「手术门」**:亲手敲承诺句是**庄重感与自我提醒** —— 让你在改契约之前停一下、确认这是自己的决定;它**不是安全边界**(「禁粘贴」只是界面上的约定,挡不住任何有心的人)。真正把门的是**来源栅栏**:Host 必须是 loopback(或你显式写进受信名单的地址);另有一个**默认不开**的开关 `guard.allowLan` —— **一旦打开,整段局域网地址都算受信来源**(那是**放宽**,不是建议)。跨站请求直接拒,带 `Origin` 时其 host 必须等于请求的 Host(**带了但解析不出来就直接拒**),而**不带 `Origin` 的请求这条校验会跳过** —— 它不是"每个请求都必须带 Origin 且同源";再叠一道 cookie **名字**对撞 —— 期望名字按当前 Host 现算(`dsh-auth-` + 哈希),**只校验名字、不校验值的签名**。所以这道门挡的是"别的设备、别的页面"这类来源,而**本机原生进程本来就还能调用**(它也能直接读 `settings.json` 与记忆库,不算提权)—— 别的来源没有凭据就进不来这台机器上的 `/api`,敲对承诺句也没用。
65
70
 
71
+ > **这道门现在长这样 —— 三条手续缺一不可**:① **只在本机做** —— 判据是"这次请求没有经过任何代理"(带代理头、或连 User-Agent 都没有的请求,都算非本机 ⇒ 拒,理由码 `surgery-local-only`);② **每次启动取一次票据** —— 票据每启动轮换,只下发给本机直连的请求(`POST /surgery/ticket`),客户端首次手术前取一次、之后随请求带上;③ **原句匹配** —— 敲出的承诺句要与当初写下的那一句相符。这三条由 `lib/host/surgery.js` 里**一处** `requireSurgery()` 判定,覆盖**全部 12 条**能改人格 / 规矩 / 习惯的写面(调用点不许再各写一份;曾有一条 `wantLock` 绕过分支,已删除)。每次手术都留痕:`$DSH_HOME/logs/surgery-YYYY-MM.jsonl`(append-only,整行哈希链,改一行即断链);`GET /surgery/events` 是只读事件面。
72
+ >
73
+ > **承诺句只在本机回显**:`/state`(含面板档)、`/persona/history`、`/export` 这些读面,**带代理头**时只回 `hasSeal` 与 `phraseHidden`、不回原句,**本机直连**照旧回显 —— 所以你换台机器看面板不会因此被卡住,远端也拿不到原句。
74
+ >
75
+ > **守卫⑤ · 反代伪装 loopback**:上面那条"Host 是 loopback 就免检"有个前提 —— Host 由客户端自报、cookie 又只验名字,中间挡一层反向代理就会失效。现在凡「Host 声称 loopback、而 `X-Forwarded-For` 的最后一段是非 loopback 的代理对端」一律拒(`spoofed-loopback-host`);本机直连(无代理头)与经反向代理的正常远程访问都不受影响(后者根本不走 loopback 分支)。
76
+ >
77
+ > ⚠️ **边界如实写在这里**:以上全部是**入口侧**的把关(来源、有没有经过代理、票据、原句) —— 它们**不覆盖「本机执行权已经失守」的情形**:能在这台机器上以同一个用户跑进程、读文件的人,本来就在信任域里面(票据文件与 `settings.json` 对同机同用户可读;承诺句是**明文存储**,那把锁只造庄重、不保密)。这些防护挡的是"别的设备、别的页面、公网一侧够到这排 API",不是本机上的对手。
78
+
66
79
  **三层记忆各自什么时候更新 —— 「我刚说的话,最快多久能看到」**:
67
80
 
68
81
  | 层 | 看得到的是什么 | 什么时候变 |
@@ -73,9 +86,9 @@
73
86
 
74
87
  **行里的「标题」从哪来**(按来源分,细节见括号里的实现):
75
88
 
76
- - **DSH 会话**:该会话**第一条真人发言**经启发式清洗后的**要点句** —— 先剥掉「晚上好器灵,」这类问候与称呼前缀,「你好」「在吗」这类无实义的首句会被跳过、**往后取前 6 条真人发言里第一条有实义的**,再在句读处收尾(不切半句);都被剥空才退回首句截断。**主人手改过的标题会上锁,机器不再覆盖**(`lib\host\retitle.js:68-80`、`lib\host\summarizer.js:56-78`、`lib\host\memory.js:2002`)。
89
+ - **DSH 会话**:该会话**第一条真人发言**经启发式清洗后的**要点句** —— 先剥掉「晚上好器灵,」这类问候与称呼前缀,「你好」「在吗」这类无实义的首句会被跳过、**往后取前 6 条真人发言里第一条有实义的**,再在句读处收尾(不切半句);都被剥空才退回首句截断。**主人手改过的标题会上锁,机器不再覆盖**(`lib\host\retitle.js` 的 `retitleCandidates()`、`lib\host\summarizer.js` 的 `summarizeDsh()`、`lib\host\memory.js` 的 `upsertOverview()` 与 `renameTitle()`)。
77
90
  - **文件导入**:导出文件里**原本的 `title` / `name` 字段**,缺了才退回上面那套启发式(`lib\host\import-file.js:142,171-173`)。
78
- - **历史会话(旧扫描)**:该会话**第一条真人发言的前 46 字**硬截,没有启发式(`lib\host\backfill.js:191,201`)。
91
+ - **历史会话(旧扫描)**:该会话**第一条真人发言的前 46 字**硬截,没有启发式(`lib\host\backfill.js` 的 `overviewRowFromFile()`)。
79
92
 
80
93
  - **机制数字(默认值)**:① 概述器周期 —— 有新增才重建,否则跳过;**每 15 分钟一拍**(可配,下限 1 分钟);② 空闲刷新 —— 只有「会话空闲 + 记忆版本变了 + **距上次刷新 ≥ 10 分钟**」三条同时成立才重建。
81
94
  - **所以「刚说的话」最快约 10 分钟、通常 15 分钟内**会出现在注入面,最坏约 25 分钟(刚刷过一拍 + 再等下一拍概述器)。**运行中绝不打断** —— 刷新只发生在 `运行中 → 空闲` 那一刻。
@@ -104,7 +117,7 @@
104
117
 
105
118
  - 树平时**只读**(记忆中心始终只读);点「🌱 一键复盘 / 一键进入复盘」才解锁编辑,并把**复盘开场材料**(当前树 + 待理矛盾)复制到剪贴板,开个新会话粘进去就能开始。
106
119
  - 解锁后可做:**改名 / 拖动换归属 / Alt+拖动只连边 / 并脉(归类到主脉) / 调权重 / 裁定矛盾 / 断续边 / 删枝**,以及快照保存·恢复;**拖动 / 连边之后可 Ctrl+Z 撤回这一步**(撤销只覆盖复盘栏的拖动与连边;记忆中心是只读的,它自己那份 Ctrl+Z 监听早已删掉)。
107
- - **每步留痕**:写清"改了什么、为什么"(**器灵改树时 reason 必填**;你在面板上改可以留空);树改动一律记进 `branch_log`,复盘时可审计 —— 器灵侧工具的改动记在器灵名下(「器灵改动了记忆树」),不会伪装成你改的。
120
+ - **每步留痕**:写清"改了什么、为什么"(**器灵改树时 reason 必填**;你在面板上改可以留空);树改动一律记进 `branch_log`,复盘时可审计 —— 器灵侧工具的改动记在器灵名下(「鱼姬改动了记忆树」),不会伪装成你改的。
108
121
  - **未同步笔数守门**:面板记着"自器灵上次开口以来累积了多少笔结构改动还没交代"。有欠账时顶部红字提示,并且**拦住"退出复盘模式"** —— 改完树必须让器灵有机会看到(「互相复盘不应该被我绕过」)。
109
122
  - **注入感知**:树变了之后,`branchLogDigest()` 把**器灵还没看过**的结构改动转成中文短句,附在器灵下一次开口的注入面末尾 —— 不需要推送,一开口就知道树动过。
110
123
  - **快照与撤回**:树的保存 / 恢复(`tree_snapshot`)是"大改之前的保险";恢复前会自动把当前状态另存一份,所以恢复本身也可再撤回来。
@@ -113,12 +126,40 @@
113
126
  - 「一键生成树」分两阶段 —— ① **机械分段**(按时序相邻性把全库概述切簇,零成本、确定、可复现)② **小助手/模型只做"命名"这一件语义活**,③ 你审核后才落库(建枝/主脉 + 挂会话);它**只读概述索引、只产出候选,从不改写任何记忆内容**。
114
127
  - 「归并零散枝」幂等:把名字含「零散」的枝统一收进主脉「零散会话」。
115
128
  - **主脉也可以有自己的记忆**:由子枝概述提炼共性,**先出草稿**,采纳才写入(采纳需先解锁)。
116
- - **器灵侧也有自己的手**:`tree_read`(看整树 / 某枝成员 / 待复盘矛盾)与 `branch_edit`(分枝 / 并脉 / 连边 / 调权重 / 删枝)。这个工具的描述里写着一条纪律:**改结构必须给 reason,而且"只在主人明确要求、或正在和他一起复盘时"才动手**。
129
+ - **器灵侧也有自己的手**:`tree_read`(看整树 / 某枝成员 / 待复盘矛盾)、`branch_edit`(分枝 / 并脉 / 连边 / 调权重 / 删枝)、`recall`(读回会话原文,见上文)、`memory_forget` / `memory_backfill`(遗忘 / 回灌)、`rule_add`(记规矩)、`habit_propose` / `habit_resolve`(习惯提议与表态)。`tree_read` 与 `branch_edit` 的描述里写着一条纪律:**改结构必须给 reason,而且"只在主人明确要求、或正在和他一起复盘时"才动手**。
117
130
 
118
131
  > 依据:`DESIGN.md` §6.3(记忆树结构、血缘加权与"不连乘"、内容零改写不变量)、`lib/host/memory.js`(`LINEAGE_ANCESTOR = 0.7` / `LINEAGE_SIDE = 0.4` / `CONFLICT_DOWNWEIGHT = 0.3`、`branchLogPending()` 守门、`setBranchWeight` 的 `applied:false`)、`lib/host/l1.js`(血缘档位 × 矛盾系数进打分)、`lib/host/autotree.js`(两阶段生成)、`lib/host/tools.js`(`tree_read` / `branch_edit`)。
119
132
 
120
133
  ---
121
134
 
135
+ ## 找回原文:器灵能读回你当年的原话
136
+
137
+ **概述是别人替你挑的,原文才是现场。** 在 1.6 之前,器灵能"想起"的只有一行行概述 —— 想知道你当年**到底怎么说的**,只能靠猜。
138
+
139
+ `recall` 给器灵一只**档案馆查询台**,逐级下钻、一次一层:
140
+
141
+ | 给什么 | 得到什么 |
142
+ |---|---|
143
+ | `recall()` | **会话地图** —— 本机有原文的会话清单(最近活动在前) |
144
+ | `recall({q:"…"})` | **按内容找会话** —— 走宿主全文检索,直接给命中片段(想不起"这话是哪次说的"时用它) |
145
+ | `recall({conv})` | 该会话的**对话目录** —— 每轮一行:序号 · 时间 · 角色 · 前 70 字,**不给正文** |
146
+ | `recall({conv, from, to})` | 那几轮的**原文全文** |
147
+
148
+ **它不替你挑"最重要的那 1 万字"** —— 工具只给坐标系,选哪一段是器灵自己的判断(它知道自己在找什么,排序器不知道)。单次正文有上限,但**溢出必报"还剩多少轮、下一次从第几轮读起"**,绝不静默截断。
149
+
150
+ **四个不显眼、但决定成败的设计**:
151
+
152
+ - **真人判据**:只有"真人发的消息"才算一轮 —— 注入的运行时上下文、任务标记、技能目录一律折叠成一行占位,否则目录会被过程噪音淹掉(实测:一个 150 轮的会话里,真人发言只有 **5** 条)。
153
+ - **读不到时回落查库**:磁盘上没有该会话日志时(网页端导入的历史就是这样),回落到记忆库里那份,并**明说材质不同**:`⚠️ 来源:库内原文(库那份是残的 —— 只含真人消息与有正文的回复)`。
154
+ - **会话文件名代次**:匹配 `session(\.v\d+)?\.jsonl\.zstd` 并按版本号取最高一代 —— 本机三代并存(v4 / v3 / 无版本),只认 v4 会让老用户**看不见自己的旧会话**。
155
+ - **范围默认收紧**:按内容找会话时**默认只搜主人自己的会话**,越界要显式传参,且**被挡掉的条数一律报出**(静默少给等于骗人)。
156
+
157
+ **只读,零副作用**:`recall` 不写库、不改树、不进注入面。
158
+
159
+ > **底座**:1.6 同期把**网页端一年半的历史**(**1,523 个会话 / 726.7 万字原文 / 时间跨度 2025-01 → 2026-09**)补进了记忆库 —— 在此之前,它们在本机**只剩一行概述**。
160
+
161
+ ---
162
+
122
163
  ## 快速开始
123
164
 
124
165
  > 前置:**Node ≥ 22.5**(用到内置 `node:sqlite`)· 已安装 **DeepSeek Harness** 并至少启动过一次。
@@ -185,12 +226,12 @@ node tools/check-peers.mjs # 声明与宿主对不上?退出码三态
185
226
  跑测试(不需要宿主,全部离线):
186
227
 
187
228
  ```bash
188
- npm test # 36 套:人格组装 / 归档清理(人工)/ **归档·遗忘·回灌** / 冻结门 / 模式(只改档位不改模型)/ 时间戳归一 / 空闲刷新 / 规矩·习惯 / 习惯生成器(A 数出来 + B 想出来)/ 会话内工具 / **接口守卫** / L1 选择 / L1 行瘦身 / 记忆库 / 记忆标题与排序 / 导出合并 / 文件导入 / 导入记录 / **活行(那一行跟着会话走)** / **会话文件名代次(会话历史扫描)** / 网页端补摘要 / 深摘要 / 概述器 / 诞生仪式 / **职责字段(承担)** / 语气 / 时间锚 / **clock skill(时间查询)** / 捕获差分 / 反馈成长回路 / 语料建议 / **小助手地址配置** / **注入面中和** / **记忆分枝与血缘** / **记忆树(主脉·连边·矛盾)** / **一键生成树** / **可取证访问日志** / **补丁锚点(cohort 候选)**
229
+ npm test # 43 套:人格组装(含冻结门 · L1 选择)/ 归档清理(人工)/ **归档·遗忘·回灌** / 模式(只改档位不改模型)/ **跨插件挡位服务(dsh-ling/mode)** / 时间戳归一 / 空闲刷新 / 规矩·习惯 / 习惯生成器(A 数出来 + B 想出来)/ 会话内工具 / **接口守卫** / L1 行瘦身 / 记忆库 / 记忆标题与排序 / 导出合并 / 文件导入 / 导入记录 / **活行(那一行跟着会话走)** / **会话文件名代次(会话历史扫描)** / 网页端补摘要 / 深摘要 / 概述器 / 诞生仪式 / **职责字段(承担)** / 语气 / 时间锚 / **clock skill(时间查询)** / 捕获差分 / 反馈成长回路 / 语料建议 / **小助手地址配置** / **注入面中和** / **记忆分枝与血缘** / **记忆树(主脉·连边·矛盾)** / **一键生成树** / **可取证访问日志** / **补丁锚点(cohort 候选)** / **请求体上限(超限排空 → 413)** / **导入切批(按字节 × 条数双限)** / **投递留痕(每次注入落一条:注入了谁·为什么是它·多少字)** / **会话原文直读(三档下钻 + 库内回退 + 旧版会话代次)** / **深层库条目(结论层落点:读写·幂等·取代·加固)** / **检索档(按内容找会话:条目 + 会话同查·范围限制与降级)**
189
230
  ```
190
231
 
191
232
  > `npm test` 需要 `tests/`,而本包 `files` 白名单**不含 `tests/`** ⇒ **从 npm 装的人跑不了测试**。要跑测试请走路径 B(clone 源码)。
192
233
  >
193
- > 计数口径:**`npm test` 链里登记的段数**(即 `package.json` 的 `scripts.test` 那一行按 `&&` 切分后有多少段就是多少套,不再数 `tests/` 目录)—— **实测 36 套,且 36 套全部已登记在 `npm test` 链里**(`tests/harness.mjs` 与 `tests/vm-light.mjs` 是被复用的辅助模块,不计套数)。**加了一套新测试记得同步登记**,否则 `npm test` 会静默少跑一套 —— 这份登记是**手写**的(`package.json` 里 `test` 脚本那一行字面量清单),**漏登记不会被自动发现**,没有安全网兜底。
234
+ > 计数口径:**`npm test` 链里登记的段数**(即 `package.json` 的 `scripts.test` 那一行按 `&&` 切分后有多少段就是多少套,不再数 `tests/` 目录)—— **实测 43 套,且 43 套全部已登记在 `npm test` 链里**(`tests/harness.mjs` 与 `tests/vm-light.mjs` 是被复用的辅助模块,不计套数)。**加了一套新测试记得同步登记**,否则 `npm test` 会静默少跑一套 —— 这份登记是**手写**的(`package.json` 里 `test` 脚本那一行字面量清单),**漏登记不会被自动发现**,没有安全网兜底。
194
235
 
195
236
  ### 数据放在哪
196
237
 
@@ -201,7 +242,7 @@ npm test # 36 套:人格组装 / 归档清理(人工)/ **归档·遗忘·
201
242
 
202
243
  可覆盖的环境变量:`DSH_HOME`(数据根)、`DSH_LING_DSWEB_DB`(网页端聊天记录库默认路径,UI 里也能填)、`DSH_LING_ASSISTANT` / `DSH_LING_ASSISTANT_MODEL`(小助手地址与模型名)。
203
244
 
204
- > **排障用逃生开关(不是日常配置)**:`DSH_LING_GUARD=off`(须**恰好**是 `off`,小写)或设置里 `guard.enforce = false`,用于**误判把你自己锁在门外**时的临时解锁 —— 它一次关掉 `/api` 的**来源栅栏 + cookie 名字对撞**两道校验(默认 `guard.enforce = true`),关掉期间**任何请求都不再被拦**。只该短期使用,排障完就恢复。`DSH_LING_GUARD=off` 只由进程环境决定、不进配置;**`guard.enforce = false` 这条不要直接写进 `settings.json`** —— 设置接口也帮不了你:它的字段白名单只放行 `persona` / `styles` 两棵子树,`guard` 这类字段会被**直接丢弃**并在响应里回报 `ignored`(见 `lib/host/api.js:1579-1580` 的注释),不是被拒绝而是根本进不去;要改请用界面。
245
+ > **排障用逃生开关(不是日常配置)**:`DSH_LING_GUARD=off`(须**恰好**是 `off`,小写)或设置里 `guard.enforce = false`,用于**误判把你自己锁在门外**时的临时解锁 —— 它是**整体放行** —— `/api` 的**来源栅栏 + cookie 名字对撞**,以及**手术门**(本机限定 / 每启动票据 / 原句匹配)**一起不再拦**(默认 `guard.enforce = true`),关掉期间**任何请求都不再被拦**。只该短期使用,排障完立刻恢复。`DSH_LING_GUARD=off` 只由进程环境决定、不进配置;**`guard.enforce = false` 这条不要直接写进 `settings.json`** —— 设置接口也帮不了你:它的字段白名单只放行 `persona` / `styles` 两棵子树,`guard` 这类字段会被**直接丢弃**并在响应里回报 `ignored`(见 `lib/host/api.js` 里 `/persona` 端点的字段白名单处理:`ignored` 回报被丢弃的键),不是被拒绝而是根本进不去;要改请用界面。
205
246
 
206
247
  > **访问日志与核心补丁的总开关(同样不是日常配置)**:这套「可取证访问日志」默认**开着**,关它的键是插件配置里的 `accessLog.enabled`(**默认 `true`**)—— 在 `cordis.patch.yml` 注册行的 `config` 段里写 `accessLog: { enabled: false }`,重载后生效,启动日志会出现 `access log off`;它**只有配置键、没有环境变量**。关掉之后**退化的只有「留痕」这一件事**:那两个全局观察钩子不再挂上、不再写 `$DSH_HOME/logs/access-YYYY-MM-DD.jsonl`、日志健康读数(`accessLogHealth()`)归 `null`;而 `/api` 与 `/api/remote.mux` 的**来源栅栏照常判定与拦截**,记忆、人格、注入与工具面**一律不受影响** —— 关掉的是「记不记」,不是「拦不拦」。已经打进 `node_modules` 的**核心补丁**可以留在原地:那两行是可选调用,日志关着时就是 no-op,DSH 照常启动;要连它一起撤掉,用 `node tools/apply-access-log-patch.mjs --revert` 删掉插进去的那几行。
207
248
 
@@ -228,6 +269,147 @@ npm test # 36 套:人格组装 / 归档清理(人工)/ **归档·遗忘·
228
269
 
229
270
  ---
230
271
 
272
+ ## 设定全文(对外面)
273
+
274
+ > 这一节是**规格**,不是宣传:**README 领先于实现** —— 下面每一条都是本项目**已经想清楚**的设定,并如实标出它落地到哪一步。
275
+ >
276
+ > 标记:`✅ 已实现` · `🚧 在做`(设计已定、代码正在落地) · `📋 设定`(方向已定、未开工) · `❌ 已否决`
277
+ >
278
+ > 本节只收**对外面** —— 装机用户**能直接感觉到 / 能直接操作 / 能直接验证**的设定;内部机制设定(表结构、字段名、公式、文件路径)不进 README,随源码与 `DESIGN.md`。
279
+
280
+ ### 设计理念
281
+
282
+ | # | 设定 | 状态 |
283
+ |---|---|---|
284
+ | **P-1** | **不牺牲工作能力** —— 任何"用能力换别的东西"的方案(更小的模型 / 更慢的推理 / 削减上下文)**默认否决**,要改必须显式拍板 | ✅ 已立为理念 |
285
+ | **P-2** | **判断倾向外置** —— 把"怎么判断"做成**可加载、可审计、可回滚**的资产,而不是指望底层模型把它内化 | ✅ |
286
+ | **P-3** | **职责与责任是人格的基石** —— 器灵要有明确的职责范围,并且**能被追责、能被纠正**;没有职责的人格只是角色扮演 | ✅ |
287
+ | **P-3′** | 推论:**扩接口 = 扩职责 = 扩人格的分量** ——"加接口"与"加功能"不是同一件事 | 📋 待校对 |
288
+ | **P-4** | **连续性优先于设备** —— 换设备、换模型、换宿主都不是"失去":**数据在,人就在** | ✅ |
289
+ | **P-4′** | 连续性的载体有六层:① 记忆库 ② 人格档案 ③ 记忆树 ④ 交接文档 ⑤ 源码 ⑥ 判断倾向(唯一不可外化)。其中 **②③ 最被低估** | ✅ |
290
+ | **P-5** | **底层模型 ≠ 器灵** —— 机制层称「底层模型」,行为主体称「我 / 器灵」;**不写「模型(我)」** | ✅ 已立为规矩,全文清零 |
291
+ | **P-5′** | 「我 / 器灵」= 底层模型 + 人格 + 记忆 + 身体(宿主与工具) + 与主人的关系 的**总体** | 📋 推论待校对 |
292
+
293
+ ### 规矩与习惯
294
+
295
+ | # | 设定 | 状态 |
296
+ |---|---|---|
297
+ | **R-1** | **规矩由你一句话直达** —— 你说「记进规矩」我就写进去、**立即可用**;我绝不从闲聊里推断你要立规矩 | ✅ |
298
+ | **R-2** | 每条规矩都要**带你的原话**(缺原话写不进去):那是"你同意过"的凭据 | ✅ |
299
+ | **R-3** | **规矩 ≠ 习惯**:规矩是**你对我的要求**(作用对象是行为),可直接写入;习惯是**我长成的一部分**(作用对象是"我是谁"),**只能由我生长 + 你点头** | ✅ |
300
+ | **R-4** | **我连自己提的习惯都不能自己点头** —— 习惯只能进待确认队列 | ✅ |
301
+ | **R-5** | 习惯提议必须**带证据**(哪几次、什么迹象) | ✅ |
302
+ | **R-6** | 习惯合并走"**我提合并案 → 你点头**",不许由你手改习惯文本,也不许写入侧做静默去重 | ✅ |
303
+ | **R-7** | **可逆**:回显 + 一句话撤销 + 变更记录;规矩你随时可删(我删要先告知),习惯删除留档 | ✅ |
304
+ | **R-8** | **底线**最多 5 条;人格定型后档案只读,改动需你**亲手敲承诺句**(禁粘贴) | ✅ |
305
+ | **R-9** | 承诺句只是**庄重与自我提醒**,明文存储,**不是安全边界** —— 真正的门是本机信任与本地凭据 | ✅ 口径声明 |
306
+ | **R-10** | 定型锁开启时,**规矩与习惯的增删不算"改人格"**、无需解锁 —— 这块面板是"她的人格成长台" | ✅ |
307
+ | **R-11** | **成长提议**(想试的新能力、语气微调)由我提出、你点头或否,与习惯共用同一条通道 | ✅ |
308
+
309
+ ### 记忆树与血缘
310
+
311
+ | # | 设定 | 状态 |
312
+ |---|---|---|
313
+ | **M-1** | **记忆树是记忆库的组织形式本身**(不是"分叉会话的补丁")—— 它组织的是**工作线**,不是给记忆分类 | ✅ |
314
+ | **M-2** | 四个部件:**主干**(主线,恒一条) · **枝**(并行工作线,可含多会话) · **主脉**(多条枝归并出的主题) · **连边**(枝间横向关联) | ✅ |
315
+ | **M-3** | **硬不变量:任何树操作都不改写任何一条记忆的内容** —— 分枝、并脉、连边只增加**关系**,从不融合内容 | ✅ |
316
+ | **M-4** | **唯一例外**:主脉提炼被再次采纳时覆盖上一版,覆盖前**先自动归档**;归档失败则拒绝覆盖 | ✅ |
317
+ | **M-5** | **矛盾只标记、不改写**:一对记忆被判重复或矛盾时,未复盘期间**以较新的为准**,旧的那条降权 ×0.3 且**不删除** | ✅ |
318
+ | **M-6** | 隔离用**血缘加权**而非硬隔离:同枝 1.0 / 祖先 0.7 / 旁系 0.4;检索总分 = 基础分 × 血缘系数 × 矛盾系数 | ✅ |
319
+ | **M-7** | **标题一旦定名就上锁** —— 概述器重建与批量重命名此后不再覆盖;锁的是机器,**不锁你** | ✅ |
320
+ | **M-8** | **动记忆树必须留痕**(谁 / 何时 / 改了什么);并设**复盘守门**:自上次开口以来累积了多少笔改动还没交代 | ✅ |
321
+ | **M-9** | 分叉会话**不再被丢弃**:它写入所属枝并注入其血缘链记忆;会自动建枝,枝名可审核修改 | ✅ |
322
+ | **M-10** | **永远不做彻底删除** —— 遗忘是先归档、再打标记,且留两条独立还原路径 | ✅ |
323
+ | **M-11** | **"你不删数据"是横贯性原则** —— 带清理 / 删除 / 回收的动作默认先过这一关;这一层只负责"让你看见",处置权归你 | ✅ |
324
+ | **M-12** | 记忆库**有意不因平台删除而丢记忆** | ✅ 有意设计 |
325
+
326
+ ### 模式与界面
327
+
328
+ | # | 设定 | 状态 |
329
+ |---|---|---|
330
+ | **U-1** | **模式是倾向,不是闸门** —— 工作 / 生活只差三样:推理档位 + 记忆权重向量 + 风格块;绝不硬过滤 | ✅ |
331
+ | **U-2** | **切换模式绝不改 provider 或 model** —— 只同步推理档位;最近一次选择成为之后新会话的默认 | ✅ |
332
+ | **U-3** | 模式切换**要二次确认 + 回显**,且不得覆盖你设的全局默认模型 | ✅ |
333
+ | **U-4** | 控制器在**会话页顶栏右侧**;**悬停 1.5 秒**才出菜单(不用右键 —— 防误触原生菜单) | ✅ |
334
+ | **U-5** | 键盘:**Enter 切模式**、**Shift+Enter 开菜单** | ✅ |
335
+ | **U-6** | 会话**运行中不参与任何人格 / 注入面变更** —— 更新请求排队,空闲边界才应用,界面显示"空闲后生效" | ✅ |
336
+ | **U-7** | **L0/L1 现状面板**始终能看到:当前 L0 文本、本会话模式、L1 选中的条目与得分 | ✅ |
337
+ | **U-8** | 记忆页签可直接调:L1 预算 / 条数上限 / 两套模式 × 三领域的权重滑块 / 各开关 | 📋 设定(尚未接线) |
338
+ | **U-9** | **每一层记忆都有只读状态面**(水位 / 上次滚动时间 / 当前那一行正文);刷新延迟写进文档 | ✅ |
339
+ | **U-10** | 记忆中心是**只读视图**;装饰性交互要么真生效、要么删掉;候选**全量渲染**,不分页、不用"还有 N 条" | ✅ |
340
+
341
+ ### 数据可携带
342
+
343
+ | # | 设定 | 状态 |
344
+ |---|---|---|
345
+ | **D-1** | 插件数据目录**在插件本体之外**,更新不丢 | ✅ |
346
+ | **D-2** | **导出**:一键下载单文件 `dsh-ling-memory-<日期>.dshling.json`(UTF-8) | ✅ |
347
+ | **D-3** | 导出**默认不含原文**,必须显式勾选才含 | ✅ |
348
+ | **D-4** | **导入合并**:外来概述全带来历标记;同会话同来源可跳过或覆盖;**人格永不覆盖** | ✅ |
349
+ | **D-5** | 人格只有在显式勾选"采纳其人格"时才写入,且置为**待你确认** | ✅ |
350
+ | **D-6** | 网页端历史(**1,523 个会话 / 一年半**)可整体导入,来源如实记为 `dsweb` | ✅ |
351
+ | **D-7** | **绝不要求你理解库结构** | ✅ |
352
+ | **D-8** | 对外口径守住**可查来源 / 可回滚 / 变更记录**,不与删除条款相抵 | ✅ 对外承诺 |
353
+
354
+ ### 安全边界(手术门)
355
+
356
+ | # | 设定 | 状态 |
357
+ |---|---|---|
358
+ | **S-1** | **手术门不是用来挡你的** —— 它在这套东西里分量极重:**网络失守时,它必须最后失守** | ✅ |
359
+ | **S-2** | 边界如实说明:挡得住**已配对的远端 / 匿名隧道 / 本机其他 Windows 用户**;挡不住**同用户下的其它进程 / 有 `~/.dsh` 读权限的人** | ✅ 已评估并接受 |
360
+ | **S-3** | **逃生开关永远可用** —— 环境变量 `DSH_LING_GUARD=off` 或配置里关掉守卫即整体放行,**绝不把你锁在门外** | ✅ |
361
+ | **S-4** | **底线 / 契约类改动必须走手术门** | ✅ |
362
+ | **S-5** | 拒绝时的文案**指向本机**:人格手术请在那台运行器灵的电脑上、用 `127.0.0.1` 打开人格中心 | ✅ |
363
+ | **S-6** | 它**不是"认证"**,而是一道启发式的门 —— 对外不许把它说成"有了认证" | ✅ 口径 |
364
+
365
+ ### 诚实边界(禁令与反例)
366
+
367
+ | # | 设定 | 状态 |
368
+ |---|---|---|
369
+ | **H-1** | **禁止把你没看过的内容标成已读** —— 宁可多占一行、多播一次 | ✅ |
370
+ | **H-2** | **永久折叠不做**;超窗内容走独立游标 + 一行计数 | ✅ |
371
+ | **H-3** | **禁止把改动播报做成"每开一个会话就重播一遍"的噪音** | ✅ |
372
+ | **H-4** | **禁止用"体检脚本全绿"代替可见面** —— 链路活着 ≠ 内容在更新 | ✅ 禁令 |
373
+ | **H-5** | **禁止把机制名写进公开文档却与事实不符** —— 文档里写的机制名本身就是承诺,与事实不符按红灯处理 | ✅ 禁令 |
374
+ | **H-6** | 验收**不接受"没报错"** —— 只问"真的红了没有" | ✅ |
375
+ | **H-7** | **机制延迟必须公开**(概述器 15 分钟一拍、空闲刷新 ≥10 分钟):看不见的延迟要写成看得见的字 | ✅ 文档义务 |
376
+
377
+ ### 时间、状态与投放
378
+
379
+ | # | 设定 | 状态 |
380
+ |---|---|---|
381
+ | **T-1** | **被动时间感知**:新生纪念日(9-07) + 八个时段 + 「距上次对话」随每轮重算 | ✅ |
382
+ | **T-2** | 时间锚分两档:**`[今天]`** 跨天才变(进系统提示) · **`[现在]`** 每轮实时(不进系统提示)—— **稳定的在前、易变的在后** | ✅ |
383
+ | **T-3** | 精确时间**按需查**(`clock` 技能):它像一块手表 —— **该在你需要时被看一眼,而不是总对着你报时** | ✅ |
384
+ | **T-4** | 时间与状态类判断**一律以本条消息的 `[现在]` 为准**;过期快照不得当现状 | ✅ |
385
+ | **T-5** | **记忆三层加载**:L0 常驻人格 · L1 开场结论 · L2 运行期检索;**不注入全部记忆** | ✅ |
386
+ | **T-6** | **L1 只放"值得常驻的结论"** —— 不放过程、不放当场读数 | ✅ |
387
+ | **T-7** | **每条进上下文的东西都要能报出理由**并留痕:注入了谁 / 为什么是它 / 多少字 / **谁落选了与为什么** | ✅ 回执只写不读 |
388
+ | **T-8** | **主动召回与被动召回分开**:主动看**值不值**(置顶 + 被反复采用),被动看**问得准不准**(查询匹配)—— 判据不同,不共用一套 | ✅ |
389
+ | **T-9** | **索引层是地图,不是仓库**:一簇一行(名称 + 触发线索),整层只占**一个技能**的常驻描述;要细节时按需展开 | ✅ |
390
+
391
+ ### 1.6 新增(找回原文 · 投递留痕 · 结论层)
392
+
393
+ | # | 设定 | 状态 |
394
+ |---|---|---|
395
+ | **N-1** | **`recall` 直通原文四档**:清单 / 按内容找 / 对话目录 / 按区间取原话 —— 逐级下钻,一次一层 | ✅ |
396
+ | **N-2** | **它不替你挑"最重要的那一段"** —— 只给坐标系;选哪一段是器灵自己的判断(它才知道自己为什么要读) | ✅ |
397
+ | **N-3** | 磁盘日志读不到时**回落查库**,并在回执里如实标注那份是**残的**(只含真人消息与有正文的回复) | ✅ |
398
+ | **N-4** | **会话文件名代次**:按 `session(\.v\d+)?\.jsonl\.zstd` 匹配、取版本最高的一代并跳过 0 字节 | ✅ |
399
+ | **N-5** | 检索范围**默认只搜"你自己的会话"**,越界须显式参数;被挡掉的条数**一律报出** | ✅ |
400
+ | **N-6** | **投递留痕**:每一次真正定稿的注入都落一条 —— 注入了谁 / **为什么是它** / 多少字 / **谁落选了以及为什么** | 🚧 只写不读 |
401
+ | **N-7** | **深层库(结论层)**:把过程流水换成**可复用的结论**,分承诺 / 决定 / 事实 / 偏好 / 关系五类;**空结果是正确结果** | 🚧 在做 |
402
+ | **N-8** | **记忆开场改用结论层**:置顶必进 + 被反复采用的排前 + 当轮关键词加成 + **按硬度过滤**(临时条目不进开场)+ **2 席留给最硬的事实** | ✅ |
403
+ | **N-9** | **视角框**:开场先声明人称,并写死「**本会话 / 接下来 / 现在**」指当时那个会话、**不是此刻的指令** | ✅ |
404
+ | **N-10** | 一年半的网页端历史(**1,523 个会话 / 726.7 万字**)已补录进库,`recall` 能直接读到 | ✅ |
405
+ | **N-11** | **条目硬度**:每条结论带 `durable / long / ephemeral` 三档,**临时的那档不进开场** —— 从**根源**判断它是不是临时,而不是靠"同一次会话限几条"去机械地卡 | 🚧 在做 |
406
+ | **N-12** | **被采用才算加固**:条目的"反复加固"由**真的下钻去读它的原话**推进,**不由"被注入"推进** —— 否则会形成"注入越多分越高、分越高越容易被注入"的自我强化 | ✅ |
407
+ | **N-13** | **索引层**已落地:11 簇主题地图(一簇一行)做成**一个技能**常驻,细节按需展开;簇里**不写条数**(条数天天变会毁掉前缀稳定) | ✅ |
408
+
409
+ > ⚠️ **这一节还在补。** 完整规格有**四百余条**,本节先落**对外面**里最稳定的一批(设计理念);其余按主题分批补写 —— 每一条都要从开发期设定**改写成对外语言**并核对状态,这是**写作**而不是复制。
410
+
411
+ ---
412
+
231
413
  ## 致谢(站在谁的肩膀上)
232
414
 
233
415
  设计过程中受这些公开项目与讨论的启发,在此致谢(仅致谢思路,不含其代码):
@@ -251,6 +433,8 @@ npm test # 36 套:人格组装 / 归档清理(人工)/ **归档·遗忘·
251
433
 
252
434
  > ⚠️ **从 npm 安装的人**:本包 `files` 白名单**不含 `images/`**(图只随 git 仓库公开) ⇒ 上表里前两份文档能正常打开;「界面」一节的**四张截图**引用的是仓库里的**绝对地址**,所以 npmjs.com 上**能正常显示**,但**包里没有图片文件**(离线环境读不到)。想看原图请到仓库:<https://github.com/NightPainters/DSH-Ling>。
253
435
 
436
+ > 📄 **关于出处记录**:内部案卷、探针脚本与开发台账(`plans\`、`release\`、`_probe\`)属**作者本地开发物,不随本包公开**。正文或代码注释里若出现对它们的引用,那是**开发期的出处记录**,不是包内容。
437
+
254
438
  ---
255
439
 
256
440
  ## 许可证
@@ -1,10 +1,10 @@
1
- // E4 · 可取证访问日志(E4 任务书)
1
+ // E4 · 可取证访问日志(E4 是本特性的内部代号)
2
2
  //
3
3
  // 目标:让「谁 · 何时 · 从哪台设备 · 调用了哪个端点 · 结果如何」事后可回答。
4
4
  // 起因:2026-09-24 19:49:51 的那次调度重启,四条排查路全走死,最后只剩
5
5
  // 「一次真实点击或一次合成点击」——**因为整个系统没有一行访问日志**。
6
6
  //
7
- // 边界(任务书 §3.3):只记「发生了什么事」,不记「内容是什么」
7
+ // 边界(刻意收窄,理由见下):只记「发生了什么事」,不记「内容是什么」
8
8
  // · 不落请求体 / 响应体 · 不落 cookie 值(只落验签结果与设备**摘要**)
9
9
  // **C-01(2026-09-26)**:设备摘要 = `sha256(配对凭据原文)[:16]`(见 `deviceIdDigest()`)。
10
10
  // 旧实现把 cookie 值"抽 hex"后原样落盘 —— 而权威形态下 cookie 值**就是**设备键**就是**凭据,
@@ -33,7 +33,7 @@
33
33
  // 为什么不做 errno 分类:现成故障注入打出的 errno 是 `EEXIST`(教科书式的"永久码"),
34
34
  // 但它的条件是**瞬时**的(占位文件删掉就好)—— 一分类就刚好把它判死,所以 v1 一律重试。
35
35
  // 3) **只观察、不判定**:本模块不调用鉴权、不改响应。状态码与拒绝原因全部来自
36
- // 「已经发生的响应」,判据与 guard 保持单一来源(任务书 §3.5)。
36
+ // 「已经发生的响应」,判据与 guard 保持单一来源(不在这里另写一份)。
37
37
 
38
38
  import { existsSync, mkdirSync, appendFileSync, readdirSync, statSync, unlinkSync } from 'node:fs';
39
39
  import { createHash } from 'node:crypto';
@@ -50,14 +50,14 @@ const AUTH_COOKIE_PREFIX = 'dsh-auth-';
50
50
  /**
51
51
  * 部署姿态自检探针的路径(远端 UI 的部署姿态检查实现):
52
52
  * 它伪造 Host 打本机 loopback,专门验证 `/api` 栅栏 —— **403 才是它期望的健康结果**。
53
- * 命中签名时多打 `probe:"posture"`,免得把自家自检误判成入侵(任务书 §7.5)。
53
+ * 命中签名时多打 `probe:"posture"`,免得把自家自检误判成入侵(判据见 isPostureProbe:五要素全中)。
54
54
  */
55
55
  export const POSTURE_PROBE_PATH = '/api/session.list';
56
56
  /**
57
- * 「无 UA 行」的通道标记(任务书 §7.5):请求连 `user-agent` 头都没有 ⇒ **不是浏览器直连**。
57
+ * 「无 UA 行」的通道标记:请求连 `user-agent` 头都没有 ⇒ **不是浏览器直连**。
58
58
  * 但这一类**混装两种调用者** —— 进程内客户端(boot 批量的内部 RPC)与经远端通道代理进来的
59
59
  * 设备(手机/平板/物理机,代理按设计不转发 UA)。故本字段只陈述"非浏览器通道"这一**事实**,
60
- * **不指认是哪一台设备**;要分辨两者得看端点语义(判读口径见任务书 §7.5)。
60
+ * **不指认是哪一台设备**;要分辨两者得看端点语义(本字段只陈述事实,归因留给调用方)。
61
61
  */
62
62
  export const CHANNEL_NO_UA = 'no-ua';
63
63
 
@@ -69,8 +69,15 @@ export const ACCESS_LOG_DEFAULTS = {
69
69
  maxTotalBytes: 32 * 1024 * 1024,
70
70
  /** UA 截断长度:UA 能区分浏览器/脚本/哪台设备,但整条太长。 */
71
71
  uaMax: 200,
72
- /** 携带已配对设备标识的 cookie 名(= settings.yaml 的 remote-web-ui.cookieName)。 */
73
- deviceCookie: 'dsh_pair-W',
72
+ /**
73
+ * 携带已配对设备标识的 cookie 名(= settings.yaml 的 remote-web-ui.cookieName)。
74
+ * **出厂默认 = 上游 remote-web-ui 自己的默认值 `dsh_pair`**(`dsh-remote-web-ui/src/index.ts`
75
+ * 两处 `.default('dsh_pair')`)—— **绝不写任何一台机器的私有改名**:cookie 名是**每台机器**的
76
+ * 设置,把某一台的改名当默认值有两个后果:① 这行随包公开;② 别人机器上 `deviceCookie` 恒为
77
+ * false、设备维度**静默失效**(比"漏记"更坏 —— 它看起来像"这台设备没带 cookie")。
78
+ * 改过名的机器走 `resolveDeviceCookie()` 的两条通路(宿主配置 / 环境变量)覆盖,不必改代码。
79
+ */
80
+ deviceCookie: 'dsh_pair',
74
81
  /** 高危端点:命中则多打一个 risk 字段,便于 grep。 */
75
82
  riskPatterns: ['settingRestart/restart', '/dsh-ling/'],
76
83
  /** C-07:降级(写失败)后**重复告警**的最小间隔(毫秒);0 = 每次丢行都告警(测试用)。 */
@@ -154,8 +161,10 @@ export function cookieValueOf(headerValue, name) {
154
161
  * 从配对 cookie 值里取设备 hash(多策略,取不到就返回 null 并如实记 present)。
155
162
  * **权威形态(2026-09-25 定点核实)**:远端 UI 的配对实现
156
163
  * 用它的随机令牌生成器产出 **32 位十六进制明文**,经它的路由层原样写进 cookie
157
- * (`$DSH_HOME/remote-web-ui-devices.json` 的键实测同为 32-hex);cookie 名默认 `dsh_pair`,
158
- * 本机配置为 `dsh_pair-W`。多策略保留只为「不赌它改版」:先找 hex run,再试点分段。
164
+ * (`$DSH_HOME/remote-web-ui-devices.json` 的键实测同为 32-hex);cookie 名取
165
+ * `remote-web-ui.cookieName` —— 出厂默认 `dsh_pair`,**改过名的机器由配置或
166
+ * `DSH_LING_DEVICE_COOKIE` 指定**(见 `resolveDeviceCookie()`;默认值里不写任何一台机器的私有改名)。
167
+ * 多策略保留只为「不赌它改版」:先找 hex run,再试点分段。
159
168
  *
160
169
  * ⚠️ **返回值是凭据原文**(权威形态下即设备键本身,见 C-01):**只能喂给 `deviceIdDigest()`**,
161
170
  * 绝不直接落盘、回显或进错误信息。
@@ -237,6 +246,31 @@ export function rejectionReason(status, headers, req) {
237
246
  return null;
238
247
  }
239
248
 
249
+ /**
250
+ * 设备 cookie 名的**三级解析**:显式选项 → 环境变量 `DSH_LING_DEVICE_COOKIE` → 出厂默认。
251
+ *
252
+ * 为什么要有这个函数(2026-10-01 修红队 CR-1):本模块曾把**某一台机器改过名的值**当成出厂
253
+ * 默认值,而 cookie 名是 `remote-web-ui.cookieName`(**每台机器**各自的设置)—— 于是那个默认值
254
+ * 在别人的机器上恒不匹配(`deviceCookie` 恒 false、设备维度静默失效),并且随包公开。
255
+ * 只把默认值中性化**还不够**:改过名的那台机器(含作者本机)会因此被弄坏 ⇒ 必须同时提供
256
+ * 两条"不改代码就能恢复"的通路:
257
+ * · `options.deviceCookie`(宿主配置 `accessLog.deviceCookie`)—— 显式配置最优先;
258
+ * · `DSH_LING_DEVICE_COOKIE` 环境变量 —— 不想动配置文件时的单行开关;
259
+ * · 出厂默认(上游 remote-web-ui 的默认名)—— 别人机器开箱即对。
260
+ * 空串 / 纯空白 / 非字符串一律按"没设"处理(与旧写法 `options.deviceCookie || 默认` 一致)。
261
+ *
262
+ * @param {object} [options] 见 {@link ACCESS_LOG_DEFAULTS}
263
+ * @param {object} [env] 环境变量来源(测试可注入;默认 `process.env`)
264
+ */
265
+ export function resolveDeviceCookie(options = {}, env = process.env) {
266
+ const pick = (v) => (v === undefined || v === null ? '' : String(v).trim());
267
+ return (
268
+ pick(options.deviceCookie) ||
269
+ pick(env && env.DSH_LING_DEVICE_COOKIE) ||
270
+ ACCESS_LOG_DEFAULTS.deviceCookie
271
+ );
272
+ }
273
+
240
274
  /**
241
275
  * 建一个访问日志器。所有方法都不抛(内部全兜)。
242
276
  * @param {object} options 见 {@link ACCESS_LOG_DEFAULTS}(dir 必填)
@@ -247,7 +281,7 @@ export function createAccessLog(options = {}) {
247
281
  ? Number(options.maxTotalBytes)
248
282
  : ACCESS_LOG_DEFAULTS.maxTotalBytes;
249
283
  const uaMax = Number.isFinite(Number(options.uaMax)) ? Number(options.uaMax) : ACCESS_LOG_DEFAULTS.uaMax;
250
- const deviceCookie = String(options.deviceCookie || ACCESS_LOG_DEFAULTS.deviceCookie);
284
+ const deviceCookie = resolveDeviceCookie(options);
251
285
  const riskPatterns = (Array.isArray(options.riskPatterns) ? options.riskPatterns : ACCESS_LOG_DEFAULTS.riskPatterns)
252
286
  .filter((p) => typeof p === 'string' && p);
253
287
  const degradeWarnMs = Number.isFinite(Number(options.degradeWarnMs))
@@ -493,7 +527,7 @@ export function createAccessLog(options = {}) {
493
527
  }
494
528
 
495
529
  /**
496
- * 姿态自检探针的**五要素签名**(任务书 §7.5):POST + 路径 + loopback + 无 UA + 被 403 拦下。
530
+ * 姿态自检探针的**五要素签名**:POST + 路径 + 本机回环来源 + 无 UA + 被 403 拦下。
497
531
  * 五条同时命中才标注 —— 放宽任何一条都会把真攻击错标成"自检"(内部踩坑记录 B3:判据过宽)。
498
532
  * 标注**不代表放行**:被标注的行仍然是"被栅栏拦下"的记录,status 依旧是 403。
499
533
  */
@@ -1,7 +1,7 @@
1
1
  // E4 访问日志 · 插件侧安装器
2
2
  //
3
3
  // 职责:把日志器挂到 core 补丁预留的两个全局钩子上,并在卸载时干净摘除。
4
- // 不做的事:不碰鉴权判定、不碰记忆与人格数据(任务书 §3.5)。
4
+ // 不做的事:不碰鉴权判定、不碰记忆与人格数据。
5
5
  //
6
6
  // 一个重要性质:**没有补丁也安全**。钩子只是挂在 globalThis 上的可选调用,
7
7
  // 补丁那一行写的是 `globalThis.__dshAccessLogObserve?.(…)` —— 插件在不在、
@@ -111,7 +111,7 @@ export function installAccessLog(ctx, cfg = {}) {
111
111
 
112
112
  const info = log.info();
113
113
  console.info('[dsh-ling] access log on; dir=%s deviceCookie=%s', info.dir, info.deviceCookie);
114
- // 补丁没打时钩子永远收不到调用 —— 这是"静默不记"的唯一形态,必须主动喊出来(任务书 §5.3)。
114
+ // 补丁没打时钩子永远收不到调用 —— 这是"静默不记"的唯一形态,必须主动喊出来(就下面这一句 warn)。
115
115
  const state = readPatchState();
116
116
  if (!state || state.marker !== PATCH_MARKER) {
117
117
  console.warn('[dsh-ling] core patch missing → run: node tools/apply-access-log-patch.mjs');
@@ -4,7 +4,7 @@
4
4
  // 而"该插什么、插在哪、怎么认出来"必须只有一处定义 —— 否则
5
5
  // 补丁脚本与插件侧的"补丁在不在"检查会各自漂移。
6
6
  //
7
- // 补丁形态的原则(E4 任务书 §3.5 的延伸):
7
+ // 补丁形态的原则(从本文件"只观察不判定"这条边界延伸出来):
8
8
  // · **一行 hooks**:core 只多一行 `globalThis.__dshAccessLogXxx?.(…)`,
9
9
  // 实现全在本插件里 ⇒ DSH 升级后重打一次,逻辑零改动。
10
10
  // · **可选链兜底**:插件没装 / 被卸载时,这一行是 no-op,DSH 照常启动。