# Changelog All notable changes to `dsh-conversation-recycle-bin` are documented here. The format follows Keep a Changelog; the project uses Semantic Versioning. ## [0.3.3] — 2026-10-19 本次修复对应 `AUDIT-2026-10-19-review.md` 的 S1–S3、P1–P5、R1–R4 与 M 项;每条均带回归测试。 ### Security - **S1:`Host` 头不带端口时同源端口比较被恒真绕过**:`requestPort === ''` 时旧表达式退化为 `originPort === originPort`,任意端口的 Origin 都通过。现在缺失端口按 Origin 自身 scheme 的默认端口归一(http→80、https→443),`Host: 127.0.0.1` + `Origin: http://127.0.0.1:9999` 被拒为 `403 E_FORBIDDEN_ORIGIN`,而 `http://127.0.0.1`(无端口)仍放行。 ### Fixed - **S2:未知 GET 路由误报 405**:路径不存在时 GET/POST 统一返回 `404 E_NOT_FOUND`,405 只保留给「路径存在但方法错误」。 - **S3:损坏/缺失 manifest 的回收站目录永久隐身**:新增 `trash.scanUnreadable()`(missing / corrupt / unreadable 三类),`/trash` 把它们列为 `status: 'unreadable'` 的 incomplete 项;`discard` 对目录存在但清单不可读的条目走 `performDiscard` 的磁盘检查——空目录可强制清理,仍持有 `session/` 日志或记忆文件则拒绝(`409 E_TRASH_CONFLICT`),不再只能手动删盘。 - **P1:live drain 轮询每轮全量重建会话列表**:`inspectRunning` 先走新增的 `gateway.isRunning(sessionId)` 内存存活表探针(`running/active/busy` 布尔),探针答不出才回退全量 `inspect()`,批量删除多个 live 会话时不再线性放大 `controller.list()` 与工作区扫描。 - **P2:`/trash` 每次全量读盘解析 manifest**:加 1.5s TTL 缓存,delete/restore/purge/discard 成功后主动失效。 - **P3:`/audit` 全量读 5MB 再解析**:`tail()` 只读尾部 256KB 块、丢弃首条残行,malformed 只统计窗口内。 - **P4:跨进程锁等待 30s 短于批量最坏耗时**:`/delete` 锁预算按 `min(900s, 15s + 16s×批量数 + 每 live 会话的 drain 窗口)` 估算,大批量不再被第二个 Host 误判 `E_LOCK_BUSY`。 - **P5:全部 blocked 时仍做全量记忆计数**:`blocked.length > 0` 时跳过 `countForSessions`,返回 `{total:0, skipped:true}` 形状。 - **R1:请求体超限后继续读完整个 body**:超限立即 `finish(413)`,慢速超大请求不再占着连接。 - **R2:反馈投递失败也扣每小时额度**:`takeSlot` 返回 `release()`,传输失败 catch 里先退还槽位再抛 `E_FEEDBACK_TRANSPORT`;成功投递才计数,SMTP/HTTP 临时故障不会把用户锁一小时。 - **R3:批量列表按 soft 固定裁决,与 `defaultMode` 脱节**:`/sessions` 每行裁决改用 `config.defaultMode`;`stopLiveSessions:false` + hard 默认时,不可删的 live 行直接标 `deletable:false`。 - **R4:失败的删除项也占每分钟额度**:`limiter.take(1)` 改为带 `release()` 的预留,单条失败在 catch 里退还槽位,只有真正成功的删除计入窗口;首个 item 预算不足直接抛 429 的既有契约不变。 ### Changed / Maintenance - **M2:会话 ID 正则去重**:抽出 `lib/core/ids.js` 的 `SESSION_ID_PATTERN`,`api.js` / `gateway.js` 共享,删除两处本地副本。 - **M4:`/state` 不再外泄记忆适配器绝对路径**:只输出 `{id,label,available,reason}`,`storePath` 不离开 Host。 - **M1:删除无引用的死 CSS `.dc-mode-preferred`**。 - **M3:`npm run check` 改为 `scripts/check-syntax.mjs` 遍历 `lib/**/*.js` + `client.js` 逐个 `node --check`**(原链式清单只覆盖 3 个文件,新增/改名模块不会再漏检)。 - 回归测试:`feedback` 17 → 18、`core` 13 → 14、`api` 34 → 38;新增覆盖 S1 无端口 Host、S2 未知路由 404、S3 损坏清单列出/清理/冲突、R2 失败退额度、R3 默认模式裁决、R4 失败退额度。 ## [0.3.2] — 2026-10-18 ### Fixed - **`feedback.visitorAsFrom` 在 HTTP 通道静默失效**:HTTP 通道此前固定发送配置的 `from`(未配置则省略字段),`visitorAsFrom: true` 只在 SMTP 通道生效。现在 HTTP 通道同样在显式开启且访客填写了邮箱时把访客邮箱作为 `From`;默认配置下发送的请求体逐字节不变。 - **邮件正文把访客邮箱标注为发件人**:正文上下文里的 `from / 发件人` 行改为如实的 `visitor / 访客邮箱`。此前无论 `visitorAsFrom` 是否开启,该行都把访客邮箱标成发件人,与真实 `From`(发信账号或访客邮箱)不符;`reply-to` 行保留不变。 ## [0.3.1] — 2026-10-18 ### Added - **表单改为让用户填写自己的邮箱**:反馈对话框里原来的「联系方式」换成「你的邮箱(选填)」`type="email"` 输入框,客户端与 Host 双重校验(必须是 `a@b.cc` 形状、≤200 字)。格式不合法直接以 `400 E_FEEDBACK_INVALID` + `details.field === 'email'` 拒绝,而不是默默发一封无法回复的邮件。 - 新增配置项 **`feedback.visitorAsFrom`**(默认 `false`):访客填写的邮箱默认写入 `Reply-To`,并在正文的 `from / 发件人` 行展示;设为 `true` 时同时作为 `From`——只有允许任意发件人的中继才行,QQ/163/Gmail 会拒收(要求 From 与登录账号一致)。`/state.feedback.visitorAsFrom` 会同步给前端,用于切换输入框下方的提示文案。 ### Changed - 邮件正文上下文字段由 `contact:` 改为 `from / 发件人:` + `reply-to:` 两行;HTTP 通道的 `reply_to` 取自访客邮箱。 - `test/feedback.test.mjs` 16 → 17 个用例(新增 `resolveSender` 发件人策略单测、非法邮箱拒绝);`test/client-module.test.mjs` 20 → 19(删除一条逐字节重复的用例外壳,行为覆盖不变),全量仍为 122。`README.md` §9.2 增加 `visitorAsFrom` 行与「发件人不是访客邮箱」排查项;`config.schema.json` / `cordis.patch.yml` 同步新键。 ### Fixed - **删除当前会话后的结果对话框会被兜底刷新抹掉**:Host 无法原地关闭该会话时会 arm 一次兜底刷新,原来是固定 900ms 后 `location.reload()`,弹窗里的「部分失败明细」根本来不及看。现在结果对话框会等用户关闭(按钮 / Escape / 点遮罩)后再刷新,5s 只是对话框被晾着的安全网;关闭时还会 `clearTimeout` 掉安全网计时器,避免关掉之后又被刷一次。对应文案 `dialog.reloadHint` 由「会话列表将自动刷新」改为「关闭本对话框后会刷新会话列表」。 - **反馈表单的邮箱校验顺序与 Host 相反**:客户端先判超长再判格式,300 字的地址会提示「不是邮箱」,而 Host 对同一次提交回的是「太长」。现在两边统一为「先形状、后长度」(对齐 `lib/host/feedback.js:241-250`)。 - **「记忆不可用」的原因混进英文硬编码**:无可用存储时 `not detected` 直接拼进中文文案。改用词条 `state.notDetected`(中英各一条)。 - **反馈表单的校验提示只在「输入过」时出现**:只用 Tab 扫过表单、一个字符没改的用户会看到一个禁用的提交按钮却看不到原因。现在「改过」或「离开过任一字段」(`dirty || touched`)即显示提示。 - **`noValidate` 挂在 `div` 上**:这是 form 的属性,放在对话框的 `div` 上无效,属误导性死属性,已删除(本组件不含 `