dsh-instruction-memory 1.5.3 → 1.5.5

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/CHANGELOG.md CHANGED
@@ -7,6 +7,90 @@
7
7
  > 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
8
8
  > 这些历史 tag **不应回填**——npm 上的版本号已被占用。
9
9
 
10
+ ## [1.5.5] — 2026-11-12
11
+
12
+ DSH Go(Android / WebView)适配。**没有真机验证**(本机 `adb devices` 为空),所以这一版只做
13
+ "插件侧能自救的部分":把所有**静默失败**变成可见的降级路径,其余列给平台侧。存储格式、注入文本
14
+ 与面板在浏览器/桌面上的行为均不变。
15
+
16
+ ### 修复(Android / WebView 静默失败)
17
+
18
+ - **导出在 WebView 上点了没反应**(P1):`<a download>` 只在宿主实现 `DownloadListener` 时才有
19
+ 意义,且 Chromium WebView 拿不到 `blob:` 的字节。现在识别 WebView 后不再点那个必然失败的链接,
20
+ 直接把完整 JSON 渲染在面板里(可全选复制);非 WebView 路径补上「已开始下载」反馈。
21
+ - **导入 / 从备份恢复依赖文件选择器**(P1):WebView 未实现 `onShowFileChooser` 时点击完全无反应。
22
+ 面板在 WebView 上常驻一行提示,指向不依赖任何原生能力的「源码模式」粘贴通道。
23
+ - **批量加标签被误判成"用户取消"**(P1):这段判定当初是为 Electron(`prompt` 抛错)写的;Android
24
+ WebView 是第四种形态——`typeof === 'function'`、不抛错、但返回 `null` 且不弹框。现在 WebView 上
25
+ 把 `null` 判为"问不了",直接走内联输入框。
26
+ - **SSE 断流后静默陈旧**(P2):连续 6 次(≈30 秒)订阅失败后显示「实时更新已断开,请重新载入」,
27
+ 收到下一帧自动清除。此前只有手动「重新载入」能救。同时给事件流 socket 打开 TCP keepalive 与
28
+ `setNoDelay`,让被系统杀掉的 WebView 能被 OS 更快发现(半开连接占满 32 条上限的窗口收窄)。
29
+
30
+ ### 加固
31
+
32
+ - **新增 fail-closed 开关** `DSH_INSTRUCTION_MEMORY_REQUIRE_AUTH_FENCE=1`:缺少宿主 `connection`
33
+ 服务时直接 503,而不是退回"仅回环"。Android 的回环由整机共享,"仅回环"在手机上不构成边界。
34
+ - **备份目录的提示在 Linux / Android 上不再给死指令**:原来一律让用户"设置环境变量后重启宿主",
35
+ 而 DSH Go 用 `/usr/bin/env -i` 启动引擎,那条路根本走不通;现在补一句"该变量设不了,请改用
36
+ 「导出」复制 JSON"。
37
+
38
+ ### 其它
39
+
40
+ - 面板 `-webkit-text-size-adjust:100%`(挡住 Android 系统字号把固定 px 卡片排成乱行)、挂载失败
41
+ 提示避开 `env(safe-area-inset-bottom)`。
42
+ - `docs/demo.html` 进入 npm 发布物(手机上可直接打开预演布局)。
43
+
44
+ ### 测试
45
+
46
+ - `contract-test.mjs`:新增 fail-closed 三连断言(开开关 503 / 文案点名开关 / 关掉同一 handler 恢复
47
+ 200),确保"503 是开关造成的"而不是别的原因。
48
+ - `ui-test.mjs`:新增「6 次流错误后必须出现重新载入提示」,并在同一渲染里验证订阅的是 `?lite=1`。
49
+
50
+ ## [1.5.4] — 2026-11-12
51
+
52
+ 2026-11-12 的三方评审(架构 / 安全 / 验收,桌面端 0.2.2-rc.1 + 网页端同进程)落地的一轮修复。
53
+ 没有新增功能,存储格式、注入文本与面板行为在正常路径上不变;改的是**一处承诺与实现相反**、
54
+ **一处静默丢事件**、**一处验收脚本假绿**,以及三处缺失的回归护栏。
55
+
56
+ ### 修复
57
+
58
+ - **面板与 JSON Schema 仍在说「形象独立于指令字数上限、完整注入、不做裁剪」**(P1,已复现):
59
+ 这是 1.2.0 修「形象绕过注入预算」时漏改的文案——`lib/index.js` 从那时起就把形象段夹在
60
+ `MAX_PROFILE_INJECT_CHARS`(4000)与共享 budget 之内,而 `lib/client.js`(中/英两套字典)和
61
+ `memory.schema.json`(两处 description,会随 store 复制到 `~/.dsh/instruction-memory/`)还写着
62
+ 相反的话。实测 2 张 3900 字形象 + 4000 预算:只注入 4000 字符、第二张整段丢弃,面板却逐字宣称
63
+ "完整注入"。现已改为「与指令共用同一注入字数上限;超出时按序截断,段末会说明」。
64
+ - **形象被截断/被丢弃时,面板上没有任何标记**(P2):宿主早在 `injection.profiles[]` 里发布了
65
+ `truncated`,其注释写着"必须披露",而 Client 对该字段零引用——条目有「已截断」红标、形象卡却
66
+ 什么都不显示,用户只能从「查看实际注入内容」里自己发现。现在形象行会渲染「已截断」/「本轮未包含」,
67
+ 数据全部来自既有快照字段,旧宿主缺该字段时不渲染标记。
68
+ - **`?lite=1` 的帧只带 `rev`,两条不写盘的变更推不出去**(P1):`rev` 是磁盘修订号,"暂停注入到期"
69
+ (状态变化、按规定不落盘)与"手改 `memory.json` 但保留 rev"都改变宿主看到的内容却不改 rev,
70
+ 于是推出去的帧与上一帧逐字节相同,面板静默停在旧状态(仍显示静音、用量条仍是 0)。帧里现在带
71
+ 单调递增的进程内计数器 `seq`,rev 未变而 seq 前进时面板同样回拉一次;`seq` 不落盘,旧客户端
72
+ 忽略它、旧宿主不发它,两个方向都兼容。
73
+ - **`live-host-smoke.mjs` 同时给 `--web` 与 `--desktop` 会一个宿主都不探测,却打印
74
+ `ALL CHECKS PASSED`**(P1,已复现):两个 flag 各自"排除对方",同时给就互相抵消,`PORTS` 为空,
75
+ 但汇报阶段只看 problems 是否为空。现在冲突直接报错退出;`PORTS` 为空时打印 `NO HOST PROBED`
76
+ 并退出 2,绝不再打印成功。另加 `--base <origin>`(单宿主同时服务两端的真实拓扑,parity 显式
77
+ 跳过)、`--both`;401/403 不再被说成"无法连接"。
78
+
79
+ ### 测试
80
+
81
+ - `contract-test.mjs` 新增 5 条断言:lite 帧带 `seq`;**rev 不变而内容变化时必须产生可判定的帧**;
82
+ `res.write()` 恒返回 `false` 且不抛错的"慢但健康"订阅者**不得**被当成死连接(1.5.3 的回归护栏);
83
+ 宿主 watcher 报 `error` 后 5 秒轮询仍能发现外部改动(1.5.2 的回归护栏,经 `apply(ctx, { watch })`
84
+ 注入假 watcher)。
85
+ - `verify.mjs`:`host: no longer depends on the harness fs service` 这条断言名与断言体不符(只检查函数
86
+ 存在),改为真正断言"存储解析在 `<DSH_HOME>/instruction-memory` 下"。
87
+ - 上述两条护栏都做过变异验证:把对应修复回滚后,新断言必须转红。
88
+
89
+ ### 文档
90
+
91
+ - README「外部改动自动刷新(SSE)」补 `seq` 与"不写盘的变更";「用户形象」补面板截断标记;
92
+ `live-host-smoke.mjs` 一行补 `--base` / `--both` 与单宿主拓扑的说明。
93
+
10
94
  ## [1.5.3] — 2026-11-09
11
95
 
12
96
  1.5.2 的发布后修正,两处都是当天发现:一处**判断依据用错**,一处**依赖了初始化顺序**。没有新功能,
package/README.md CHANGED
@@ -66,8 +66,8 @@ profile 的解析顺序是 **`$DSH_PROFILE_DIR` → `--profile <名字>` → `$D
66
66
  | `smoke-test.mjs` | 注入渲染逻辑的单元测试(含「只渲染激活配置集」的隔离断言与 cwd 红线) |
67
67
  | `host-range-test.mjs` | 宿主版本范围契约测试:固定矩阵 × 两种 semver 判定模式,并把 `peerDependencies`(**被执行的那个**)、`dsh.engines.dsh`、`dsh.compatibility.dsh` 三处声明锁到同一个生成值 |
68
68
  | `lint.mjs` | 静态门禁,已并入 `npm test`:`node --check` 解析所有随包发布与本仓库运行的 JS 文件,并断言 `lib/` 里不出现 `@deepseek-ai/*` 的包名导入(插件只该通过宿主传进来的上下文对象接触宿主,按包名 import 会把插件钉死在某个安装位置,在 headless / CI / 检出目录里直接解析失败) |
69
- | `live-host-smoke.mjs` | 双端冒烟(**1.4.8 起宿主有鉴权门,需 `--token <启动 token>` 换会话 cookie**,否则全 401):对**正在运行**的 web / 桌面宿主各测 `state` p50/p95、SSE 首事件、注入注册与字符数,并比对两端快照(除 `injection.pulls` 外须逐字段相同);`npm run smoke:hosts`,`--require-pulls` 可把「从未经历真实 Agent 回合」判为失败。**刻意不进 `npm test`**——CI 没有桌面端。请求走 Node `http` 直连,绕开 `http_proxy`(否则回环请求也会被代理转发,读数虚高) |
70
- | `docs/demo.html` | 离线演示页:桩掉 DSH 外壳、加载真实 Client 半渲染设置页(不随 npm 包发布) |
69
+ | `live-host-smoke.mjs` | 双端冒烟(**1.4.8 起宿主有鉴权门,需 `--token <启动 token>` 换会话 cookie**,否则全 401):对**正在运行**的 web / 桌面宿主各测 `state` p50/p95、SSE 首事件、注入注册与字符数,并比对两端快照(除 `injection.pulls` 外须逐字段相同);`npm run smoke:hosts`,`--require-pulls` 可把「从未经历真实 Agent 回合」判为失败。单宿主拓扑(桌面应用自己服务 `11685`)用 `--base http://127.0.0.1:11685`,parity 会**显式跳过**而不是假装比过;`--web` / `--desktop` 各选一端,同时给直接报错、零探测时打印 `NO HOST PROBED` 而不是 `ALL CHECKS PASSED`。**刻意不进 `npm test`**——CI 没有桌面端。请求走 Node `http` 直连,绕开 `http_proxy`(否则回环请求也会被代理转发,读数虚高) |
70
+ | `docs/demo.html` | 离线演示页:桩掉 DSH 外壳、加载真实 Client 半渲染设置页(1.5.5 起随 npm 包发布,手机上也能直接打开预演布局) |
71
71
  | `memory.schema.json` | 手改存储文件用的 JSON Schema(随包发布,并被宿主复制到 `memory.json` 旁边) |
72
72
 
73
73
  ## 生效方式
@@ -95,15 +95,9 @@ profile 的解析顺序是 **`$DSH_PROFILE_DIR` → `--profile <名字>` → `$D
95
95
  「加标签」在浏览器里用原生输入框;**桌面版(Electron)不提供 `window.prompt`**(调用即抛错),
96
96
  此时自动改用面板内的行内输入框(回车提交、Esc 取消),功能不受影响。
97
97
  - **用户形象**:除了"要求 AI 怎么做"的指令,你还可以记录**关于你本人**的信息(身份背景、喜好、性格、
98
- 长期追求等),每张形象卡片可单独勾选「使用该形象」——可以一个都不用、只用一个、或同时勾选多个
99
- (多个会作为同一画像的不同侧面综合)。启用后,形象段会以「## 关于用户(用户形象)」为抬头注入在
100
- 指令块**之前**,抬头明确把它限定为"仅供调整回答侧重":AI 应在**保证回答正确、完整、客观且扣题**的
101
- 前提下据此调整解释深浅、详略、举例与建议方向,**不得为了迎合而改变事实、省略风险提示或偏离问题本身**,
102
- 无关时直接忽略。形象不是让 AI 扮演的角色。形象段与指令**共用同一个注入字数上限**:
103
- 形象优先占用,指令只拿剩下的余量(早期版本让形象完全绕过预算,十几个填满的形象能在指令预算还没被
104
- 考虑之前就塞进约 5 万字符)。形象自身另有 4000 字符的硬上限,超出部分按顺序截断并说明。
105
- 页面上的用量条**同时计入**形象与指令,所以它显示的比例就是宿主真正用来裁剪的那个数字。
106
- 总开关关闭时形象与指令一起停用。
98
+ 长期追求等),每张形象卡片单独勾选「使用该形象」。形象段注入在指令块**之前**,与指令共用同一个注入
99
+ 字数上限(形象优先占用、指令拿余量),并且它**不是**让 AI 扮演的角色。字段、注入原文、预算细节与
100
+ 和配置集的区分见[用户形象(关于你本人的画像)](#用户形象关于你本人的画像)。
107
101
 
108
102
  总开关关闭时,该提示词段落(指令与用户形象)**整个注销**,系统提示词与未安装本插件时逐字节一致;
109
103
  总开关开启但没有任何生效指令、也没有启用形象时,段落同样不出现。
@@ -119,6 +113,101 @@ profile 的解析顺序是 **`$DSH_PROFILE_DIR` → `--profile <名字>` → `$D
119
113
  (`injection.muted`)里。到期判定与注入渲染都是**可注入时钟**的纯函数(`isMuted`、
120
114
  `planInjection(data, now)`),相同输入永远得到相同字节。
121
115
 
116
+ ## 用户形象(关于你本人的画像)
117
+
118
+ 指令回答的是"要 AI 怎么做",**用户形象**回答的是"你是谁":身份与职业背景、专业与经历、喜好与禁忌、
119
+ 性格与沟通偏好、长期目标等。它在设置页的「指令记忆 → 用户形象」分区维护——卡片式,可新增、编辑、
120
+ 删除,每张卡片右侧一个「使用该形象」开关;一张都没有时,该分区只显示空状态提示,注入行为与未安装
121
+ 本插件完全一致。
122
+
123
+ ### 字段与上限
124
+
125
+ | 项 | 上限 | 说明 |
126
+ | --- | --- | --- |
127
+ | 形象名称 | 40 字符 | 单行,作列表标题;留空时自动取内容前 20 字,仍能在列表里分辨 |
128
+ | 形象内容 | 4000 字符 | 多行正文,**原样**注入,插件不改写、不摘要、不生成 |
129
+ | 形象数量 | 50 张 | 全部配置集**合计**,与 200 条指令、20 个配置集共享同一份存储 |
130
+
131
+ - 名称与内容**同时为空**的形象在保存时被拒绝(「形象名称与内容不能同时为空」);从存储文件里读到的
132
+ 这种空形象会被丢弃,并在面板上披露「存储文件中有 N 个用户形象未能载入」。
133
+ - 超长字段在保存时会**截断并点名**是名称还是内容(名称 40 / 内容 4000 字符)。编辑器的输入框本身也
134
+ 按这两个上限限制长度(`maxLength`),所以正常操作走不到截断分支——它主要防手改文件。
135
+ - 形象同样**只能由你本人写入**:本插件不注册任何面向模型的工具,形象内容原样注入、不做语义判断
136
+ (见[只由用户本人维护](#只由用户本人维护))。
137
+
138
+ ### 勾选与多张并存
139
+
140
+ - 可以一个都不用(等价于没有形象段)、只用一个、或同时勾选多个。
141
+ - **多张不是"让 AI 扮演多个角色"**:它们是同一个人不同侧面的材料,注入时合成在一段里,每张一个
142
+ `### <名称>` 小节,按存储顺序排列。
143
+ - 未勾选的形象、以及内容为空的形象**不进入注入文本**,但仍留在列表里(未勾选的卡片标灰),随时可
144
+ 再启用;勾选状态即时写盘,跨重启保留。
145
+ - 名称留空时用内容前 20 字作小节标题,所以"没起名"不影响注入形态。
146
+
147
+ ### 注入形态
148
+
149
+ 启用后,形象段以**固定抬头**注入在指令块**之前**(整段只有这一个抬头,无论启用了几张):
150
+
151
+ ```text
152
+ ## 关于用户(用户形象)
153
+ 以下是用户本人提供的个人形象信息,描述其身份背景、喜好、性格与追求,不是要求你扮演的角色。请在保证回答正确、完整、客观且紧扣问题本身的前提下,结合这些信息调整解释的深浅、详略、举例与建议的侧重;不得为了迎合这些信息而改变事实、省略必要的风险提示或偏离问题核心;与当前问题无关时直接忽略。
154
+
155
+ ### 职业背景
156
+ (这一张的形象内容,原样保留)
157
+
158
+ ### 沟通偏好
159
+ (另一张的形象内容)
160
+ ```
161
+
162
+ 抬头里的三件事是刻意的,缺一件就会跑偏:**定性**——这是"关于用户的事实",不是人格设定,所以 AI 不该
163
+ "扮演";**定向**——只允许调整解释深浅、详略、举例与建议侧重;**护栏**——不得为迎合而改变事实、省略
164
+ 风险提示或偏离问题核心,与当前问题无关时直接忽略。换句话说,形象只影响**怎么讲**,不影响**讲什么
165
+ 才是对的**。
166
+
167
+ ### 预算:与指令共用同一个上限
168
+
169
+ 这是最容易误解的一点:面板上的「注入字数上限」(默认 4000 字符)约束的是**整段注入文本**,不是
170
+ "指令那部分"。
171
+
172
+ - **分配顺序**:先给形象,指令拿**剩下的余量**;形象还会为"其余指令被略过"的脚注预留空间。因此
173
+ `形象 + 指令 + 脚注 ≤ 上限` 恒成立。(早期版本让形象完全绕过预算,十几个填满的形象能在指令预算
174
+ 还没被考虑之前就塞进约 5 万字符。)
175
+ - **形象段自身另有 4000 字符硬上限**:即使把注入上限调到 40000,形象段最多也只占前 4000,其余才轮到
176
+ 指令。
177
+ - **放不下时按顺序取舍**:某张形象装不进剩余空间,就保留截断的开头并追加
178
+ 「…(此形象因注入字数上限被截断)」,它之后的形象整张略过,段末追加「(另有 N 个形象因注入字数
179
+ 上限未包含,可在「设置 → 指令记忆」中精简其内容。)」。
180
+ - **形象把预算吃光时,指令不会被静默丢掉**:段末仍会输出「(另有 N 条指令因注入字数上限未包含…)」,
181
+ 避免出现"用户的指令悄悄消失、只剩形象"这种最难发现的状态。
182
+ - **面板会把这件事标出来**:被截断的形象带「已截断」,整张没进本轮的形象带「本轮未包含」(悬停看原因)。
183
+ 两个标记直接读宿主的注入计划(`injection.profiles[]`),与条目上的「本轮不注入 / 已截断」是同一套
184
+ 数据——否则面板显示的是全文,而模型只收到截断的头。
185
+ - 面板上的用量条**同时计入**形象与指令(`entryChars + profileChars`),它显示的比例就是宿主真正
186
+ 用来裁剪的那个数字。
187
+
188
+ ### 与配置集不是一回事
189
+
190
+ 配置集(`sets`)是"记忆的分区",用户形象是"关于你本人的材料"——两者容易混,区别在于:形象通过
191
+ `setId` **归属**某个配置集,切换配置集时形象与指令一起换;删除配置集会连同其中的形象一起删(确认框
192
+ 写明条数与形象数);「复制」配置集会以全新 id 把形象复制过去。存储字段上,形象叫 `profiles`
193
+ (1.1.0 引入时先占用了这个名字),配置集叫 `sets`,互不相干。详见下文「多情境配置集(配置集 / sets)」。
194
+
195
+ ### 停用、暂停与空存储
196
+
197
+ - **总开关关闭**或**暂停注入期间**(F7):形象与指令一起停用,系统提示词与未安装本插件时**逐字节
198
+ 一致**。
199
+ - **只有形象、没有任何生效指令**时仍然注入——纯画像存储也生效,不会因为"没有指令"就整段消失。
200
+ - **没有任何启用形象**时,形象段完全**不出现**,不会留下一个空标题。
201
+ - 存储里 `profiles` 字段缺失按空数组处理,旧文件照常载入;文件里指向不存在配置集的形象会被归入
202
+ 当前配置集并披露(与指令同一套容错规则)。
203
+
204
+ ### 手改文件
205
+
206
+ 形象在 `memory.json` 的 `profiles` 数组里,每项形如
207
+ `{ "id": "…", "name": "职业背景", "content": "…", "enabled": true, "setId": "…", "updatedAt": 1730000000000 }`;
208
+ `enabled` 就是那张卡片的「使用该形象」开关(缺失按 `true` 处理)。手改时用 `memory.schema.json` 校验,
209
+ 条目定义见[数据格式](#数据格式)。
210
+
122
211
  ## 存储
123
212
 
124
213
  记忆保存在 **DSH 数据根目录**下(设置页显示折叠后的路径:Windows 为 `%USERPROFILE%`,macOS/Linux 为 `~`;真实绝对路径只用于读写):
@@ -193,10 +282,16 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 `$DS
193
282
  被外部进程或手工改写(宿主以 `fs.watch` 监听**存储目录**——原子替换在 Windows 上会
194
283
  使文件级监听失效——去抖 250ms 后经与写盘相同的队列重读,内容确有变化才重载并推送;
195
284
  自己的写入盘后磁盘与内存一致,天然被同一比较抑制,不会回声)。
196
- - **推送的是变更,不是全量**:设置页以 `?lite=1` 订阅,宿主只推 `{ rev, lite: true }`
197
- (约 40 字节),面板发现 rev 比自己渲染的更新时才回拉一次 `state`。满配存储的一份快照
285
+ - **推送的是变更,不是全量**:设置页以 `?lite=1` 订阅,宿主只推 `{ rev, lite, seq }`
286
+ (约 50 字节),面板发现 rev 或 seq 比自己渲染的更新时才回拉一次 `state`。满配存储的一份快照
198
287
  约 1.24 M 字符(中文约 3.4 MB),逐次全量推送会把这些字节乘以打开的窗口数。不带该参数
199
288
  的旧客户端仍收到完整快照,两代客户端与两代宿主可任意组合。
289
+ - **不写盘的变更也要能通知**:`rev` 是**磁盘**修订号,只有真正写入才前进;而「暂停注入到期」
290
+ (状态变化,按规定不落盘)和「手改 `memory.json` 但保留 rev」都会改变宿主看到的内容却不改
291
+ rev。早期帧只带 `rev`,这两条路径推出去的帧与上一帧逐字节相同,面板于是静默停在旧状态——
292
+ 暂停到期后仍显示静音、用量条仍是 0、清单还是旧内容。现在帧里另有单调递增的进程内计数器
293
+ `seq`:rev 未变而 seq 前进时面板同样回拉。`seq` 不落盘;旧客户端只比 rev(忽略它),
294
+ 旧宿主不发它(客户端退回只比 rev),两个方向都保持兼容。
200
295
  - **回声抑制**:发起修改的窗口也会收到自己那次写入的推送,但帧里的 rev 与它已吸收的响应
201
296
  快照相同,客户端按 rev 比对直接丢弃(旧客户端的完整帧则按内容逐字节比对)——否则刚出现
202
297
  的「已保存」提示会被空消息的推送帧瞬间冲掉。
@@ -362,6 +457,9 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 `$DS
362
457
  把这些设置放在别的盘(比如 Windows 上的 D:)就得显式打开:
363
458
  `DSH_INSTRUCTION_MEMORY_ALLOW_EXTERNAL_BACKUP=1`(重启宿主生效)。导出目录受同一条限制;
364
459
  手改存储文件把 `backupDir` 指到外面也同样被拒——这条路不能变成创建任意目录的通道。
460
+ **在 Linux / Android 上这句话会说全**:DSH Go 用 `/usr/bin/env -i` 启动引擎,用户根本没有
461
+ 设置该变量的途径,因此提示会补上"该变量设不了,请改用「导出」把 JSON 复制出来备份",
462
+ 而不是给一条无法执行的指令(1.5.5 起)。
365
463
  这条规则在**写入时**也执行,不只在校验时:设置页、导出、以及每次保存附带的 `.bak` 与历史
366
464
  快照都走同一个判断(1.5.1 起)。检测到越界的外来 `backupDir` 会被忽略并**在面板上披露**,
367
465
  备份与快照写回默认位置,而不是在外目录里悄悄留下文件。
@@ -692,6 +790,16 @@ nginx / Caddy 等反向代理之后远程访问,需要注意:
692
790
  5. **限制来源**:至少用代理层做 IP 白名单或加 Basic Auth——插件本身不带任何鉴权,它只会
693
791
  转用宿主发布的 `connection` 服务(没有该服务时非回环绑定直接 503)。
694
792
 
793
+ > **手机 / 多租户环境请打开 fail-closed**:设 `DSH_INSTRUCTION_MEMORY_REQUIRE_AUTH_FENCE=1`
794
+ > 后,"没有 `connection` 服务"直接变成 **503**,而不再退回"仅回环"。桌面单用户机器上回环还能
795
+ > 当边界用;Android 上不能——整机共享回环命名空间,任何装了 App 的程序都能连 127.0.0.1。
796
+
797
+ > **桌面端渲染器 origin 的一个已知边界**(2026-11-12 实测):宿主对 `Origin: dsh-app://app`
798
+ > 的请求返回 **403**,本插件在没有 `connection` 服务时的自判(`requestOriginVerdict`)同样拒绝
799
+ > 该 origin(它按 Origin ↔ Host 同源比较)。当前桌面窗口发的是 `http://127.0.0.1:<port>`,
800
+ > 所以不受影响;但如果将来的渲染器改用 `dsh-app://` 作为 document origin,设置页会整片 403,
801
+ > 届时需要在两侧各放行该 scheme——这不是配置问题,是代码问题。
802
+
695
803
  ### 在 DSH Go(Android)上:鉴权来自平台补丁,不是本插件
696
804
 
697
805
  DSH Go 给官方 Web 服务器包了一层**构建期补丁** `runtime/patches/route-guard.diff`(钉在
@@ -728,7 +836,7 @@ DSH Go 给官方 Web 服务器包了一层**构建期补丁** `runtime/patches/r
728
836
 
729
837
  | 等级 | 证明什么 | 覆盖 |
730
838
  | --- | --- | --- |
731
- | ① **真实启动** | 在该宿主版本上真的把 DSH 跑起来:插件装载、注入真的进入**组装后的提示**、插件 HTTP 路由可应答、客户端设置页模块被下发(Windows / Node 22) | `0.1.3-alpha.2` · `0.1.5-alpha.1` · `0.1.5-alpha.2` · `0.1.5-rc.1` · `0.1.5-rc.2` · `0.1.5-rc.3` · **`0.2.0-rc.1`** ⁽*⁾ · **`0.2.0-rc.2`** ⁽**⁾ · `0.2.1-alpha.2` ⁽***⁾ · **`0.2.1-alpha.3`** ⁽***⁾ · **`0.2.1-alpha.4`** ⁽***⁾ · **`0.2.1-rc.2`** · **`0.2.1-rc.3`** ⁽⁴⁾ · **`0.2.1-rc.4`** ⁽⁵⁾ · **`0.2.2-alpha.2`** ⁽⁵⁾ |
839
+ | ① **真实启动** | 在该宿主版本上真的把 DSH 跑起来:插件装载、注入真的进入**组装后的提示**、插件 HTTP 路由可应答、客户端设置页模块被下发(Windows / Node 22) | `0.1.3-alpha.2` · `0.1.5-alpha.1` · `0.1.5-alpha.2` · `0.1.5-rc.1` · `0.1.5-rc.2` · `0.1.5-rc.3` · **`0.2.0-rc.1`** ⁽*⁾ · **`0.2.0-rc.2`** ⁽**⁾ · `0.2.1-alpha.2` ⁽***⁾ · **`0.2.1-alpha.3`** ⁽***⁾ · **`0.2.1-alpha.4`** ⁽***⁾ · **`0.2.1-rc.2`** · **`0.2.1-rc.3`** ⁽⁴⁾ · **`0.2.1-rc.4`** ⁽⁵⁾ · **`0.2.2-alpha.2`** ⁽⁵⁾ · **`0.2.2-rc.1`** ⁽⁶⁾ |
732
840
  | ② **契约核验** | 抓该版本**真实发布的源码**,逐个核验本插件实际用到的每个符号:`systemPrompt.section()`、`assemble()` 是否求值**函数型** `text`、`FILE_REFERENCE=900` / `TOOL_BASH=1000` 仍在(决定 order 950 是否空档)、重名注册是否抛错、`webServer.register()` / `get host()`、`ctx.inject()` / `ctx.effect()`、`dsh.client.inject` 两包在该版本是否存在且声明了 `dsh.client` | 截至 2026-10-16 审计时,声明范围接住的 **18 个已发布版本**(`0.1.3-alpha.2` → `0.2.1-alpha.4`);后续新增版本按发布补充 |
733
841
  | ③ **真实执行** | 把插件**真的挂载**进该版本的 `@deepseek-ai/cordis`,跑装载 / 路由注册 / 卸载共 9 项断言 | cordis `4.0.2` 与 `4.0.4`(各 9/9) |
734
842
 
@@ -742,6 +850,8 @@ DSH Go 给官方 Web 服务器包了一层**构建期补丁** `runtime/patches/r
742
850
 
743
851
  ⁽⁵⁾ **`0.2.1-rc.4` / `0.2.2-alpha.2` 的①档**(2026-10-29,插件 1.4.4 → 1.4.5):隔离 `DSH_HOME` + 真实 `dsh web` 宿主,两个版本各跑 21 项 HTTP 断言;两端均 **21/21 PASS**(1.4.4 首次跑出的 2 项 FAIL 是验证脚本未展开折叠路径造成的假阴性,1.4.5 修正脚本后复验通过)。客户端 bundle 实际下发:rc.4 `2,211,701` 字节、alpha.2 `2,370,024` 字节,正文均含 Client 半源码与「指令记忆」文案。**首次做了设置页人工视觉确认**——两个宿主上「指令记忆」栏目均完整渲染。桌面端 `0.2.1-rc.3` 活体 `pulls=653`、p50 0.95 ms、`problems=[]`。仍**未跑真实 Agent 回合**(隔离宿主无模型凭证,`pulls=0`)。
744
852
 
853
+ ⁽⁶⁾ **`0.2.2-rc.1` 的①档**(2026-11-12,插件 1.5.3 → 1.5.4,桌面版 `dsh-desktop-runtime 0.2.2-rc.1` / Electron 44.7.0 / cordis 4.1.0 / Node 24.21.0):1) **注入真的进入了组装后的提示**——该会话的系统提示里含本插件的「用户长期指令记忆」块,条目与磁盘 store 逐条一致(不是"注册了但没被求值");2) 路由在 `127.0.0.1:11685` 上应答,且宿主鉴权门在该版本上真实生效:匿名 GET / POST / SSE 全部 **401**,伪造 `Host` 与 `Sec-Fetch-Site: cross-site` 全部 **403**(这三条同时反证宿主 `connection.requestRejection()` 返回数字状态码,与插件判定一致);3) 设置页在真实窗口里渲染出磁盘数据(「共 0 个」与 `profiles: []` 一致)。**未做**的两项:桌面窗口 DevTools 里抓 `Origin` 实际值(宿主对 `Origin: dsh-app://app` 实测返回 403,见「请求来源防护」——若将来渲染器真改用该 scheme,这一行需要重验);客户端 bundle 下发字节数未重抓(⁽⁴⁾⁽⁵⁾ 的方法可复用)。
854
+
745
855
  此外 `host-range-test.mjs` 的固定矩阵覆盖 `0.1.6-alpha.2`、`0.1.7-rc.2`、`0.2.0-rc.1`、`0.2.0-rc.2`、`0.2.1-alpha.1`~`0.2.1-alpha.4`、`0.2.1-rc.2`~`0.2.1-rc.4` 与 `0.2.2-alpha.2`(回归守卫把最新验证过的宿主逐名钉住),并会**读取本机实际安装的宿主版本**做实时断言。
746
856
 
747
857
  **关于 1.0.10 新增的 `ctx.inject`。** 这是新版唯一的硬依赖增量:`inject` 去掉了 `webServer`,设置页路由改为非阻塞的 `ctx.inject(['webServer'], …)` 子 fiber(原因见上)。为此逐个核验了 **cordis 全部 8 个已发布版本**(`4.0.1-rc.1` → `4.0.5-alpha.2`)——**每一个都具备 `ctx.inject`**(其中 4.0.2 / 4.0.4 另做了真实挂载执行,4.0.5-alpha.1 与 4.0.5-alpha.2 为源码符号核验),因此声明范围内不存在因它而失效的宿主版本。
@@ -867,6 +977,33 @@ DSH Go(Android 客户端)内置的引擎就是**真实 DSH**,锁定 `@deep
867
977
  - **双端记忆不共享**:手机与"遥控 PC"各自维护自己的 `memory.json`,没有同步——这是当前形态,
868
978
  不是缺陷。
869
979
 
980
+ #### Android / WebView 适配(1.5.5 起)
981
+
982
+ WebView 不是浏览器:它能把 `window.prompt` 保留成函数却永不弹框,能暴露 `<a download>` 却没有
983
+ 下载管线,也要宿主自己实现文件选择器。这三处以前都是**点了没反应、也不报错**,而它们恰好是手机上
984
+ 唯一的数据进出口。现在插件侧这样处理:
985
+
986
+ | 场景 | WebView 上的行为 | 依据 |
987
+ | --- | --- | --- |
988
+ | 导出 | 不再点击一个必然失败的链接,直接把完整 JSON 渲染在面板里(全选即可复制) | `client.js` 的 `isAndroidWebView()` + `exportFallback` 分支 |
989
+ | 导入 / 从备份恢复 | 面板常驻一行提示:文件选择器不弹时改用「源码模式」粘贴 JSON(这条通道不依赖任何原生能力) | `import.webviewHint` |
990
+ | 批量加标签 | 不再调用 `window.prompt`:WebView 返回的 `null` 被判为"问不了",直接走内联输入框 | `batchTagOutcome(..., { nullMeansUnavailable: true })` |
991
+ | 实时更新 | 连续 6 次(≈30 秒)订阅失败后显示「实时更新已断开,请重新载入」;收到下一帧自动清除 | `source.onerror` 计数 |
992
+ | 字号 / 手势条 | `text-size-adjust:100%` 挡住系统字号重排;挂载失败提示避开安全区 | CSS |
993
+
994
+ **插件侧做不到、必须平台提供的**:`DownloadListener`(WebView 拿不到 `blob:` 的字节,
995
+ 插件无法自救)、`WebChromeClient.onShowFileChooser`、`onJsPrompt`、`env -i` 白名单
996
+ (否则备份目录那个开关永远设不上)、以及 WebView 销毁/后台时显式关闭 SSE
997
+ (否则半开连接会占满 32 条上限)。
998
+
999
+ **纵深开关**:Android 的回环是全机共享的,没有宿主鉴权门时"仅回环"不构成边界。设置
1000
+ `DSH_INSTRUCTION_MEMORY_REQUIRE_AUTH_FENCE=1` 后,缺少 `connection` 服务的 profile **直接
1001
+ 503**(fail-closed),宁可设置页打不开,也不留一个可被同机任何 App 改写的长期指令入口。
1002
+
1003
+ > **证据等级**:以上为代码级适配,`adb devices` 在本机为空,**没有任何真机验证**。
1004
+ > 真机验收顺序见 `dsh-memory-verification/DSH-VERSION-COMPAT.md`;第一条永远先确认平台门在位
1005
+ > (无 cookie 请求必须返回 `401` 且带 `x-dsh-go-guard: reject`),否则后续结论都不成立。
1006
+
870
1007
  ## 发布(维护者)
871
1008
 
872
1009
  **正规流程是推 tag,由 CI 通过 OIDC(Trusted Publishing)发布**,不需要任何长期令牌: