dsh-instruction-memory 1.4.0 → 1.4.2

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,164 @@
7
7
  > 1.0.13 起由 CI 经 OIDC Trusted Publishing 发布(首个带 provenance 的版本)。
8
8
  > 这些历史 tag **不应回填**——npm 上的版本号已被占用。
9
9
 
10
+ ## [1.4.2] — 2026-10-24
11
+
12
+ 适配性复核(宿主 `0.2.1-rc.3`,桌面端 + 网页端)。**没有任何运行时代码改动**——这一版把复核证据
13
+ 固化进仓库,并补上了一条此前一直缺的**直接**证据。
14
+
15
+ ### 新增证据
16
+
17
+ - **网页端 rc.3 隔离实测**:独立 `DSH_HOME` + 真实 `dsh web` 宿主(`0.2.1-rc.3`)驱动 21 项 HTTP
18
+ 断言——**21/21 PASS**(CRUD、落盘持久化、SSE 连接帧、`section` 注册/注销、4 项请求门禁、
19
+ IPv6 同源等价与混源拒绝、30 连发稳定性、清理回基线)。冒烟 `state` p50 0.45 ms(空库)/
20
+ 0.77 ms(1 条生效)、p95 ≤ 0.93 ms、SSE 首事件 0.54–0.61 ms。
21
+ 证据:`dsh-memory-verification/out/compat-1016-live-latest-rc3.json`、`…-rc3b.json`。
22
+ - **桌面端 rc.3 活体实测**:`live-host-smoke.mjs --desktop 14582` → `registered=true`、
23
+ `chars=546`、`activeCount=3`、**`pulls=5052`**(真实 Agent 回合真的拉取过 5052 次)、
24
+ p50 0.72 ms / p95 1.11 ms、SSE 首事件 1.2232 ms、`problems=[]`。
25
+ - **首次拿到「客户端模块确实被下发」的直接证据**:`0.2.1-rc.3` 网页宿主的 boot HTML 预加载清单里
26
+ 出现 `dsh-instruction-memory/client.js`,抓取该合并 bundle 返回 `200 text/javascript`
27
+ (2,204,270 字节),正文含本插件 Client 半的源码头注释与「指令记忆」文案。此前 `0.2.0-rc.1`
28
+ 只能用"合成器硬校验 + 契约包逐字节未变"间接推断(见 README ⁽*⁾),现在这一项对 rc.3
29
+ 是直接观测。
30
+
31
+ ### 兼容面判定
32
+
33
+ - **`0.2.1-rc.2` → `0.2.1-rc.3` 只是重新构建**:与 npm 上 `rc.2` 发布物逐文件 SHA-256 比对——
34
+ `dsh-system-prompt` / `dsh-client-ui-renderer` / `dsh-client-ui-settings` / `dsh-host-webserver` /
35
+ `dsh-app-boot` **仅 `package.json` 变**(版本号与 peer 指向自身 rc.3);`dsh-client-modules` 的
36
+ 1001 行里只有 `const commit` 一行变;`dsh-agent-loop` 自身代码未变(仅把 `zod` 打进包内)。
37
+ ⇒ 对插件可见的契约面是重建,不是行为变化。
38
+
39
+ ### 仍未验证
40
+
41
+ - 设置页的**人工视觉确认**:桌面端与网页端都还没有"用眼睛看过一次"(打开 设置 → 指令记忆)。
42
+ - 隔离网页宿主的 `pulls` 为 0(没跑过真实 Agent 回合);真实回合的拉取计数只在桌面端有 5052 次观测。
43
+
44
+ ### 已知环境坑(不是缺陷)
45
+
46
+ `npm test` 第一步 `lint.mjs` 在 DSH 的 Windows 沙箱下会误报 `13 FAILED TO PARSE`:它用
47
+ `spawnSync(..., { encoding: 'utf8' })` 捕获子进程输出,而该沙箱不允许管道 stdio(EPERM),
48
+ `res.status` 变成 `null`。等价手工核验(逐个 `node --check`)13/13 全部通过,CI 亦不受影响。
49
+
50
+ ## [1.4.1] — 2026-10-22
51
+
52
+ 运行期缺陷(桌面端 / 网页版审计)与安全审查(对 1.4.0 源码)发现的问题修复,
53
+ 加上双宿主(网页端 :8080 / 桌面端 :19387,宿主均为 0.2.1-rc.2)适配性审计后的加固。
54
+
55
+ ### 修复
56
+
57
+ 桌面端 / 网页版审计(宿主 `0.2.1-rc.2`、Electron 44.7.0)发现的三条运行期缺陷:
58
+
59
+ - **指令里出现 `{{…}}` 会让整个 Agent 回合崩溃**:`systemPrompt.section()` 注册时没有关掉宿主的
60
+ 模板插值。宿主 `renderPrompt()` 默认对每个 section 做 `{{变量}}` 替换,遇到无法解析的引用直接抛
61
+ `unknown prompt variable "{{x}}" in section "instruction-memory"`;而 agent-loop 在 `step()` 里
62
+ 裸调 `renderPrompt()`(无 try/catch),于是一条写着 `proxy_pass {{upstream}};` 的指令
63
+ (Nginx 片段、Vue/Jinja/Handlebars 模板都算)就会终止当前回合。现在注册时显式传
64
+ `interpolate: false`,该 section 按字面量渲染。
65
+ - **桌面端「批量加标签」点击即抛异常**:Electron 44 把 `window.prompt` 替换成
66
+ `function () { throw new Error("prompt() is not supported.") }`,并经 contextBridge 推进主世界,
67
+ `typeof` 仍返回 `'function'`,原来的类型守卫形同虚设。现在调用点用 try/catch 判定
68
+ (`batchTagOutcome()`):prompt 缺失或抛错时改用面板内的行内输入框;用户主动取消(返回 `null`)
69
+ 仍按「取消」处理,不会被误判成"环境不支持"。
70
+ - **桌面 profile 缺依赖声明**:`profiles/desktop/package.json` 的 `dsh.profile.bundles` 列了
71
+ `dsh-instruction-memory`,`dependencies` 却没有它、`pnpm-lock.yaml` 也无记录,运行期靠手工
72
+ junction 维持。该 profile 下次 `pnpm install`(插件增删/更新都会触发)会清掉链接,随后 `dsh`
73
+ 只在 stderr 打一行 `skipping profile bundle` 就静默跳过,插件在桌面端凭空消失。现在补齐
74
+ `dependencies` 里的 `link:` 声明。
75
+
76
+ 安全审查(对 1.4.0 源码)发现的问题:
77
+
78
+ - **并发提交可绕过 30 秒冷却、重复投递**:冷却时间戳原本在 SMTP 会话结束后才写入,
79
+ 两个同时到达的请求都能通过检查、各发一封。现在冷却在发送开始前就占位,失败时立即释放。
80
+ - **面板 15 秒超时短于宿主的投递上限**:SMTP 慢时页面报"超时",而邮件仍在路上,
81
+ 用户重试就会收到重复邮件。面板改为最多等待 90 秒,宿主新增整封投递上限 `SMTP_TOTAL_TIMEOUT_MS`(60 秒);
82
+ `verify.mjs` 固定两者的大小关系。
83
+ - **配置文件里 `smtp.pass` 写成数字时被静默丢弃**:端口/`secure` 早已按 JSON 标量处理,密码却只接受字符串,
84
+ 导致"密码明明填了却认证失败"。现在数字会被字符串化,对象/数组则明确报 `smtp.pass 类型无效`。
85
+ - **`to` 与 `toEncrypted` 同时存在时明文静默优先**:会让"我已加密"变成假象,损坏的密文也不会被发现。
86
+ 现在直接拒绝并说明原因;环境变量 `..._FEEDBACK_TO` 仍可临时覆盖。
87
+ - **地址校验过于宽松**:`EMAIL_RE` 原本放行 NUL 等控制字节与非 ASCII 地址,
88
+ 而 `buildRawMail` / `sendSmtpMail` 依赖调用方先校验。现在校验排除控制字节,
89
+ 非 ASCII 地址明确报错(未实现 SMTPUTF8),且组信与发信入口各自再做一次换行/控制字符断言。
90
+ - **`\bLOGIN\b` 误判 AUTH 机制**:EHLO 中任何含独立单词 `LOGIN` 的扩展都会让客户端错误选择 `AUTH LOGIN`。
91
+ 现在只解析 `AUTH` 能力行(RFC 4954)。
92
+ - **响应行缓冲无上限**:行为异常或恶意的服务器可持续发送无 CRLF 数据撑大内存,
93
+ 现在上限 64 KiB,超出即断开并报 `SMTP 响应过长`。
94
+ - **超长主题只生成一个 RFC 2047 编码词**:最长约 240 字符,部分客户端无法解码。
95
+ 现在按 45 字节切分为多个、每个不超过 75 字符的编码词。
96
+ - **日志脱敏漏掉 SMTP 用户名**:服务器拒绝文本可能回显登录账号,现在用户名也一并隐藏。
97
+
98
+ ### 优化(源码审查第二轮)
99
+
100
+ - **相对 `$DSH_HOME` 会让存储路径保持相对**:`resolveStorePath()` 原样返回
101
+ `rel-home\instruction-memory\memory.json`,与 README「设置页显示完整绝对路径」矛盾,
102
+ `dirname()` 还会退化成 `.`,同一份存储在进程内的身份取决于路径写法。现在相对的
103
+ `$DSH_HOME` 在启动时按 cwd 解析为绝对路径(空白仍视为未设置);`install.mjs` 同步修正。
104
+ - **陈旧锁删不掉时保存会永久挂起**:`acquireStoreLock()` 的 `unlink` 失败被吞掉后直接
105
+ `continue`,绕过了下面的 deadline 判断;只要锁文件持续不可删(Windows 句柄未共享删除、
106
+ 只读卷),这次保存就永远不返回,后续所有磁盘操作都堵在同一队列里。deadline 现在在每次
107
+ 尝试的**顶部**判断,且每次失败都先等待 50 ms 再重试。
108
+ - **面板把每字段上限硬编码**:编辑器写死 120/6000/200 与标签 8/24,而 `snapshot.limits`
109
+ 只发布了部分上限——宿主改上限后面板会接受加载器随后截断的文本。现在
110
+ `maxTitle`/`maxContent`/`maxWhen`/`maxTagsPerEntry`/`maxTagLength` 全部随快照发布,
111
+ 编辑器按快照取值(对旧宿主仍保留原默认值)。
112
+ - **历史快照只按份数保留,磁盘占用无上限**:满配存储一份快照约 3.6 MB,20 份约 72 MB,
113
+ 自定义备份目录指向云盘时等于同量上传。新增按设备的 **32 MB 字节预算**,超出时从最旧
114
+ 逐出、最新一份永远保留;策略抽成纯函数 `planHistoryPrune()` 便于单测。
115
+ - **SSE 每帧都推全量快照**:满配时约 1.24 M 字符(中文约 3.4 MB)/帧,且每次保存推给
116
+ 每个打开的设置页。设置页现在以 `?lite=1` 订阅,宿主只推 `{ rev, lite: true }`(约 40
117
+ 字节),客户端发现 rev 落后再拉 `state`;不带该参数的旧客户端仍收到完整快照,双向兼容。
118
+ - **可访问性**:表单 `<label>` 全部补 `htmlFor`/`id`(此前 0 个关联,点标签不聚焦、读屏
119
+ 不关联);「展开全部/收起」从可点击 `div` 改为 `<button aria-expanded>`;错误与提示分别
120
+ 补 `role="alert"` / `role="status"`;标签删除按钮补可访问名「移除标签 X」。
121
+ - **编辑器错误提示不可见**:条目编辑器是提前返回的分支,不渲染面板级错误行——空标题保存、
122
+ 冲突或写盘失败时 `setError()` 设了消息却什么都不显示。现在编辑器内自带错误行(并带
123
+ `role="alert"`)。
124
+ - **发布包体积**:`docs/screenshot.png` 由 262 KB 压到 97 KB(同尺寸调色板量化),
125
+ 整个 tarball 从 435 KB 降到约 270 KB。
126
+ - **发布前跑测试**:`prepublishOnly` 现在执行 `check-metadata.mjs && npm test`,本地
127
+ `npm publish` 不再能绕过测试套件(CI 的 publish.yml 原本就会跑)。
128
+
129
+ ### 优化(双宿主审计跟进,宿主 0.2.1-rc.2 / Electron 44)
130
+
131
+ - **破坏性确认的降级策略前后矛盾**:批量删除、集合删除、形象删除、单条删除写的是
132
+ `typeof window.confirm === 'function' && !window.confirm(…)`——没有对话框时条件整体为
133
+ `false`,删除**照常执行**;而版本回滚与整体恢复是「没有对话框就拒绝」。同一类不可恢复
134
+ 操作在缺 `window.confirm` 的宿主里行为相反。现在统一走 `askConfirm(message, unavailable)`:
135
+ 持久数据类默认 `deny`(无对话框即拒绝,并在面板显示 `confirm.unavailable`,不再静默吞掉
136
+ 点击),只有「丢弃未保存草稿」类(退出编辑器、重载源码视图)用 `allow`——那里拒绝会把
137
+ 人困在死按钮上,且不涉及已落盘数据,`verify.mjs` 对此有既有守护。
138
+ 返回值改为三态 `yes`/`no`/`unavailable`,用户主动取消不会被误报成「环境不支持」;
139
+ `confirm` **存在但调用即抛**(Electron 44 给 `prompt()` 用的正是这种桩)按缺失处理,
140
+ 而原先的 `typeof` 判断对这种桩完全失效。
141
+ - **窄窗口双列表单挤成竖排断字**:`.im-grid2` 固定 `1fr 1fr` 且没有任何断点,桌面端把窗口
142
+ 拉窄后每列不足 250px,标签与占位文本逐词换行。新增 560px 断点:表单转单列,批量标签
143
+ 输入去掉 160px 下限并独占一行(此前会横向溢出)。
144
+ - **双端冒烟没有一条可复现的命令**:新增 `live-host-smoke.mjs`(`npm run smoke:hosts`),
145
+ 一次测完 `state` 的 p50/p95、SSE 首事件、注入注册与字符数、以及两端快照的逐字段一致性;
146
+ 任一项超阈值(p50 > 50 ms)或快照出现 `injection.pulls` 之外的差异即以退出码 1 失败。
147
+ 脚本内全部请求走 Node `http` 直连,**不继承 `http_proxy`**——本机代理指向 `127.0.0.1:9708`,
148
+ 连回环请求也转发,审计期间正是它让 SSE 读数从 0.7 ms 虚高到 6 ms。
149
+ `--require-pulls` 把「该宿主从未经历真实 Agent 回合」判为失败,用于桌面端注入链路的验收。
150
+ 该脚本**刻意不接入 `npm test`**:CI 没有桌面端,接进去必然常红。
151
+
152
+ ### 测试
153
+
154
+ - `verify.mjs` 新增运行期缺陷的回归用例:注册的 section 必须带 `interpolate: false`
155
+ (宿主渲染期崩溃的守卫)、`__batchTagOutcome()` 的四种输入(正常作答 / `null` 取消 /
156
+ 抛错 / 非字符串答案)。并在宿主 `0.2.1-rc.2` 的**真实** `renderPrompt()` 上端到端确认:
157
+ 带该标志时含 `{{upstream}}` 的指令按字面量渲染,去掉标志则复现原报错。
158
+ - `feedback-test.mjs` 新增上述每一条的回归用例:并发路由(只投递一封、且不额外建连)、
159
+ 数字/非法类型 `pass`、`to`+`toEncrypted` 冲突、控制字节与非 ASCII 地址、
160
+ 组信头部注入断言、长主题切分与还原、AUTH 机制解析、60 秒总上限、无 CRLF 洪泛的有界缓冲。
161
+ 假 SMTP 服务器支持自定义 EHLO 能力行与洪泛选项。
162
+ - 本轮新增:`smoke-test.mjs` 覆盖相对 `$DSH_HOME` 的绝对化与 `planHistoryPrune()` 的
163
+ 份数/字节/保底规则;`contract-test.mjs` 断言五个逐字段上限随快照发布且与代码一致,
164
+ 并验证 `?lite=1` 的连接帧与变更帧都是紧凑 rev 标记、普通流仍收完整快照;
165
+ `ui-test.mjs` 断言每个字段标签都指向真实控件、校验错误带 `role="alert"`,并以
166
+ EventSource 桩确认客户端确实以 `?lite=1` 订阅、收到更新的 rev 时会回拉 `state`。
167
+
10
168
  ## [1.4.0] — 2026-10-18
11
169
 
12
170
  ### 新增
package/README.md CHANGED
@@ -44,6 +44,7 @@ dsh plugin --profile web remove dsh-instruction-memory
44
44
  | `smoke-test.mjs` | 注入渲染逻辑的单元测试(含「只渲染激活配置集」的隔离断言与 cwd 红线) |
45
45
  | `host-range-test.mjs` | 宿主版本范围契约测试:固定矩阵 × 两种 semver 判定模式,并把 `peerDependencies`(**被执行的那个**)、`dsh.engines.dsh`、`dsh.compatibility.dsh` 三处声明锁到同一个生成值 |
46
46
  | `lint.mjs` | 语法门禁:`node --check` 解析所有随包发布与本仓库运行的 JS 文件,已并入 `npm test` |
47
+ | `live-host-smoke.mjs` | 双端冒烟:对**正在运行**的 web / 桌面宿主各测 `state` p50/p95、SSE 首事件、注入注册与字符数,并比对两端快照(除 `injection.pulls` 外须逐字段相同);`npm run smoke:hosts`,`--require-pulls` 可把「从未经历真实 Agent 回合」判为失败。**刻意不进 `npm test`**——CI 没有桌面端。请求走 Node `http` 直连,绕开 `http_proxy`(否则回环请求也会被代理转发,读数虚高) |
47
48
  | `docs/demo.html` | 离线演示页:桩掉 DSH 外壳、加载真实 Client 半渲染设置页(不随 npm 包发布) |
48
49
  | `memory.schema.json` | 手改存储文件用的 JSON Schema(随包发布,并被宿主复制到 `memory.json` 旁边) |
49
50
 
@@ -69,6 +70,8 @@ dsh plugin --profile web remove dsh-instruction-memory
69
70
  多选与上方的筛选/搜索叠加(「全选」只作用于当前可见条目)。整批修改在宿主侧是**单次写入**
70
71
  (只产生一次落盘、一次修订号递增与一次同步推送),全部条目已是目标状态时不写盘;批量删除需确认。
71
72
  批量的启停/优先级/标签调整不重盖更新时间,因此不会改变同优先级下的注入先后。
73
+ 「加标签」在浏览器里用原生输入框;**桌面版(Electron)不提供 `window.prompt`**(调用即抛错),
74
+ 此时自动改用面板内的行内输入框(回车提交、Esc 取消),功能不受影响。
72
75
  - **用户形象**:除了"要求 AI 怎么做"的指令,你还可以记录**关于你本人**的信息(身份背景、喜好、性格、
73
76
  长期追求等),每张形象卡片可单独勾选「使用该形象」——可以一个都不用、只用一个、或同时勾选多个
74
77
  (多个会作为同一画像的不同侧面综合)。启用后,形象段会以「## 关于用户(用户形象)」为抬头注入在
@@ -104,7 +107,8 @@ dsh plugin --profile web remove dsh-instruction-memory
104
107
 
105
108
  路径优先级与官方 `@deepseek-ai/dsh-home-paths` 一致:显式 `$DSH_HOME` → `~/.dsh`。
106
109
  **空白的 `$DSH_HOME` 视为未设置**,绝不会退回当前工作目录——否则用户换个目录启动
107
- DSH 就会得到第二份空记忆,看起来像"记忆消失"。
110
+ DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 `$DSH_HOME`(如 `./data`)会在
111
+ 启动时按当前目录解析为绝对路径,因此设置页显示的始终是完整路径。
108
112
 
109
113
  > 1.0.0 之前的构建曾把数据写在宿主进程 cwd 下(`.dsh-instruction-memory.json`)。
110
114
  > 首次启动新版本时会自动迁移到上述位置,并把原文件改名为 `*.migrated` 保留。
@@ -147,13 +151,17 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。
147
151
  `Accept: text/event-stream`),存储一旦在面板之外发生变化,已打开的面板**自动更新**,
148
152
  无需手动「重新载入」:
149
153
 
150
- - **触发来源**:其他窗口经同一宿主保存(写盘成功即向所有订阅者推送快照);`memory.json`
154
+ - **触发来源**:其他窗口经同一宿主保存(写盘成功即向所有订阅者推送变更);`memory.json`
151
155
  被外部进程或手工改写(宿主以 `fs.watch` 监听**存储目录**——原子替换在 Windows 上会
152
156
  使文件级监听失效——去抖 250ms 后经与写盘相同的队列重读,内容确有变化才重载并推送;
153
157
  自己的写入盘后磁盘与内存一致,天然被同一比较抑制,不会回声)。
154
- - **回声抑制**:发起修改的窗口也会收到自己那次写入的推送帧,但帧内容与它已吸收的响应
155
- 快照逐字节相同,客户端按内容比对直接丢弃——否则刚出现的「已保存」提示会被空消息的
156
- 推送帧瞬间冲掉。
158
+ - **推送的是变更,不是全量**:设置页以 `?lite=1` 订阅,宿主只推 `{ rev, lite: true }`
159
+ (约 40 字节),面板发现 rev 比自己渲染的更新时才回拉一次 `state`。满配存储的一份快照
160
+ 约 1.24 M 字符(中文约 3.4 MB),逐次全量推送会把这些字节乘以打开的窗口数。不带该参数
161
+ 的旧客户端仍收到完整快照,两代客户端与两代宿主可任意组合。
162
+ - **回声抑制**:发起修改的窗口也会收到自己那次写入的推送,但帧里的 rev 与它已吸收的响应
163
+ 快照相同,客户端按 rev 比对直接丢弃(旧客户端的完整帧则按内容逐字节比对)——否则刚出现
164
+ 的「已保存」提示会被空消息的推送帧瞬间冲掉。
157
165
  - **降级而非报错**:宿主不支持或连接中断时 `EventSource` 按 `retry: 5000` 自动重连,
158
166
  重连失败期间面板照常可用(「重新载入」仍是兜底),不会弹出错误。
159
167
  - 编辑器中未保存的草稿**不受推送影响**;保存时按当前 `rev` 提交,外部若已抢先修改
@@ -242,6 +250,9 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。
242
250
  - 每台设备**各保留最近 20 份**(按文件名里的设备标签分组),逐出最旧的。两台机器共用一个
243
251
  备份目录时,谁也不会把对方的历史剪掉。不符合该命名的文件(手工放的、别的工具写的、
244
252
  云盘的冲突副本)**一律不碰**。
253
+ - 份数之外还有**32 MB 字节预算**(同样按设备计算):满配存储(200 条 × 6000 字符)的一份
254
+ 快照约 3.6 MB,只按份数保留会到约 72 MB;超出预算时从最旧的开始逐出,但**最新一份永远
255
+ 保留**(哪怕它自己就超过预算)。
245
256
  - **为什么放在 `history/` 子目录**:`memory.json` 所在目录被 `fs.watch` 监听以做外部改动
246
257
  自动刷新,快照绝不能混进去触发重载;子目录不在非递归监听的范围内,文件名过滤也会兜底。
247
258
  - 设置页「版本历史」里可展开列表(时间、rev、条目/形象数、体积、是否来自其他设备),
@@ -399,7 +410,7 @@ DSH 就会得到第二份空记忆,看起来像"记忆消失"。
399
410
 
400
411
  | 变量 | 必填 | 说明 |
401
412
  | --- | --- | --- |
402
- | `DSH_INSTRUCTION_MEMORY_FEEDBACK_TO` | 是 | 收件邮箱(单个地址) |
413
+ | `DSH_INSTRUCTION_MEMORY_FEEDBACK_TO` | 是 | 收件邮箱(单个地址,仅 ASCII;非 ASCII 地址会明确报错而不是发出乱码头) |
403
414
  | `DSH_INSTRUCTION_MEMORY_FEEDBACK_SMTP_HOST` | 是 | SMTP 服务器主机名 |
404
415
  | `DSH_INSTRUCTION_MEMORY_FEEDBACK_FROM` | 否 | 发件地址,默认取 SMTP 用户名;部分服务商要求与登录账号同域 |
405
416
  | `DSH_INSTRUCTION_MEMORY_FEEDBACK_SMTP_PORT` | 否 | 1–65535,默认按 `_SMTP_SECURE` 取 465 / 587 |
@@ -467,11 +478,13 @@ smtp://用户:密码@smtp.example.com:587?secure=0&from=bot@example.com
467
478
  `DSH_INSTRUCTION_MEMORY_FEEDBACK_SECRET=……`
468
479
  2. 用插件自带的小工具生成密文(密钥从环境变量读取,不会打印到终端):
469
480
  `node lib/mailer.js encrypt-to you@example.com`
470
- 3. 把输出的 `im1:…` 粘贴为 `toEncrypted`,删除 `"to"`,然后重启 DSH(环境变量只在启动时读取)。
481
+ 3. 把输出的 `im1:…` 粘贴为 `toEncrypted`,删除 `"to"`(同时保留两者会被拒绝),然后重启 DSH
482
+ (环境变量只在启动时读取)。
471
483
 
472
484
  实现是 AES-256-GCM + scrypt 派生密钥,密文带认证标签:改错一个字符或密钥不匹配都会明确报
473
- `无法解密反馈收件邮箱`,不会静默换收件人。若同时存在 `to`(环境变量或文件),明文优先,
474
- 方便临时覆盖。
485
+ `无法解密反馈收件邮箱`,不会静默换收件人。配置文件里同时留下 `to` 和 `toEncrypted` 会被**直接拒绝**
486
+ (否则明文一直有效,加密只是摆设);环境变量 `DSH_INSTRUCTION_MEMORY_FEEDBACK_TO` 仍优先于
487
+ 文件里的 `toEncrypted`,用于临时改收件人。
475
488
 
476
489
  **能力边界(请勿误解)**:SMTP 信封必须把收件人明文交给服务器,TLS 只保护传输链路;
477
490
  本加密保护的是配置文件**静态副本**,不防"能以同一 Windows 用户身份读取环境变量和文件"的攻击者,
@@ -488,11 +501,17 @@ smtp://用户:密码@smtp.example.com:587?secure=0&from=bot@example.com
488
501
  - **拒绝在明文连接上发送密码**:服务器不提供 STARTTLS 时会直接失败,而不是降级。
489
502
  - 主题与正文均为 UTF-8(RFC 2047 编码主题、base64 正文),中文不会乱码。
490
503
  - 成功后清空草稿并提示"反馈已发送";失败时保留草稿,页面显示原因,宿主日志记一行脱敏错误。
491
- - 默认 30 秒冷却:同一进程内连续提交会被拒绝(防误触重复发信),可用上面的变量调整。
504
+ - 面板为一次发送最多等待 90 秒,宿主自身的整封投递上限是 60 秒:面板总比宿主晚放弃,
505
+ 因此"超时"提示出现时邮件不会再迟到地发出去。
506
+ - 默认 30 秒冷却:同一进程内连续提交会被拒绝(防误触重复发信),可用上面的变量调整;
507
+ 冷却在发送**开始**前就占位,所以两个同时到达的请求也只有一个会被投递。
492
508
 
493
509
  常见失败:`尚未配置反馈收件邮箱` → 环境变量/文件未生效(环境变量改完要重启 DSH;
494
510
  配置文件每次发送都会重新读取,无需重启);`反馈收件邮箱已加密,但未设置 …SECRET` 或
495
511
  `无法解密反馈收件邮箱` → 密钥缺失/不匹配,重新执行 `encrypt-to` 并同步更新环境变量;
512
+ `配置文件同时包含 to 与 toEncrypted` → 删掉明文 `to`;`反馈收件邮箱含非 ASCII 字符`
513
+ → 换成 ASCII 地址(未实现 SMTPUTF8);`配置文件 smtp.pass 类型无效` → 密码写成 JSON 字符串;
514
+ `SMTP 总耗时超过 60000 ms` → 服务端长时间无响应,检查主机/端口/网络;
496
515
  `SMTP 连接失败` → 主机/端口/防火墙;`STARTTLS 失败` → 服务商要求 465 隐式 TLS;
497
516
  `认证失败` → 用户名/密码或应用专用密码,部分邮箱需先开启 SMTP 服务;
498
517
  `发件地址被拒绝` → 显式设置 `FROM` 为与账号同域的地址。
@@ -567,7 +586,7 @@ GUI 与路由本就同主机不同端口),`none`(用户直接导航)放
567
586
 
568
587
  | 等级 | 证明什么 | 覆盖 |
569
588
  | --- | --- | --- |
570
- | ① **真实启动** | 在该宿主版本上真的把 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`** ⁽***⁾ |
589
+ | ① **真实启动** | 在该宿主版本上真的把 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`** ⁽⁴⁾ |
571
590
  | ② **契约核验** | 抓该版本**真实发布的源码**,逐个核验本插件实际用到的每个符号:`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` | 声明范围接住的**全部 18 个已发布版本**(`0.1.3-alpha.2` → `0.2.1-alpha.4`) |
572
591
  | ③ **真实执行** | 把插件**真的挂载**进该版本的 `@deepseek-ai/cordis`,跑装载 / 路由注册 / 卸载共 9 项断言 | cordis `4.0.2` 与 `4.0.4`(各 9/9) |
573
592
 
@@ -577,6 +596,8 @@ GUI 与路由本就同主机不同端口),`none`(用户直接导航)放
577
596
 
578
597
  ⁽***⁾ **`0.2.1-alpha.2`(2026-10-10)与 `0.2.1-alpha.3` / `0.2.1-alpha.4`(2026-10-16)的①档**:隔离安装该版本宿主树 + 独立 `DSH_HOME` 装入 npm 正式发布物,真实启动 web 宿主后以 **21 项 HTTP 断言**驱动(CRUD、落盘持久化、SSE 连接帧、`section` 注册/注销、4 项请求门禁、IPv6 同源等价与混源拒绝、30 连发稳定性、清理回基线)——三个版本各 **21/21 PASS**。证据:`dsh-memory-verification/out/compat-1016-live-live-alpha2.json`、`dsh-memory-verification/out/compat-133-live-alpha{3,4}.json`。同样**未做设置页人工视觉确认**。
579
598
 
599
+ ⁽⁴⁾ **`0.2.1-rc.2` / `0.2.1-rc.3` 的①档**(2026-10-24,插件 1.4.2):`rc.3` 上隔离 `DSH_HOME` + 真实 `dsh web` 宿主驱动 **21 项 HTTP 断言 21/21 PASS**,桌面端活体 `pulls=5052`、p50 0.72 ms;并且**首次直接抓到了客户端模块的下发证据**——boot HTML 的预加载清单含 `dsh-instruction-memory/client.js`,该合并 bundle 返回 `200 text/javascript`(2,204,270 字节),正文含 Client 半源码与「指令记忆」文案。因此上一条 ⁽*⁾ 里"无法直接抓到 `window.__DSH_BOOT__`"的限制**已对 rc.3 解除**。`rc.2` 未单独起宿主,靠它与 `rc.3` 的逐文件字节比对(仅 `package.json` 与一行构建号不同)承接结论。证据:`dsh-memory-verification/out/compat-1016-live-latest-rc3.json`、`…-rc3b.json`。仍**未做设置页人工视觉确认**。
600
+
580
601
  此外 `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`(回归守卫把最新验证过的宿主逐名钉住),并会**读取本机实际安装的宿主版本**做实时断言。
581
602
 
582
603
  **关于 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 为源码符号核验),因此声明范围内不存在因它而失效的宿主版本。
Binary file