dsh-sidecard-ask 1.8.4 → 2.0.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.
Files changed (5) hide show
  1. package/CHANGELOG.md +176 -0
  2. package/README.md +125 -15
  3. package/client.js +649 -46
  4. package/index.js +718 -13
  5. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,181 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.0
4
+
5
+ **主题:追问卡片标题栏的 `×` 从「隐藏整个浮层」改成「删除这条追问对话」——一次 Breaking 的语义重定义,顺带把「隐藏浮层」这套能力整体下线,并移除反馈表单里「用邮件客户端发送」这条出口。**
6
+
7
+ `×` 原来是全局开关:点一下整个浮层缩成一个带计数的圆点,再点圆点恢复。用户实际想要的是**删掉眼前这条追问**,
8
+ 而当时删除只能绕两条路走(卡片底部按钮 / 设置页历史管理器),且 `×` 的「隐藏」在多卡场景下会连着别的卡片一起收走。
9
+ 2.0.0 把 `×` 直接对齐到用户心智,并**不再保留**「隐藏整个浮层」这套能力(不是挪位置,是不留替代入口)。
10
+
11
+ ### Breaking
12
+
13
+ - **`×` 语义变更**:卡片标题栏最右侧的 `×` 由「隐藏整个浮层」改为「**删除这条追问对话**」,`title` / `aria-label`
14
+ 改用新键 `cardDelete`。删除会同时从活动卡片列表与持久历史里移除该卡片,**不可撤销**。
15
+ - **`floatMode` 移除 `off`**:枚举从 `['capsule', 'full', 'off']` 收窄为 `['capsule', 'full']`,设置页该下拉只剩两项。
16
+ 旧配置里存过 `'off'` 的**无需手工迁移**:宿主 `sanitizeLayer` 判定为非法值 → 忽略并在 `problems` 里提示,
17
+ 回退默认 `capsule`(不写迁移代码,也不静默改写用户配置)。
18
+ - **小圆点 launcher 删除**:浮层不再有「只剩一个计数圆点」的形态,相关渲染分支、样式与恢复入口一并删除。
19
+ 浮层只剩 `capsule`(一行高胶囊)与 `full`(完整卡片栈,铺满右栏 dock 时为 `dock`)。
20
+ - **删除 4 个 i18n 键**:`setFloatModeOff`(设置页「隐藏」项)、`floatHide`(`×` 的旧 title)、
21
+ `floatRestore`(从圆点恢复浮层的入口)、`floatCount`(圆点计数)。`floatStreaming` / `floatDone` 保留(胶囊仍在用)。
22
+
23
+ ### 新增:删除交互
24
+
25
+ - **空卡直接删**:该卡无任何已完成追问轮(`history.length === 0`)且主回答正文与思考过程都为空、且不在作答中
26
+ (非 `pending` / `streaming`)时,点 `×` **直接删除,不弹确认**。
27
+ - **其余情况弹插件内轻量确认层**(`DeleteConfirmDialog`,不使用 `window.confirm`):标题「删除这条追问对话?」,
28
+ 正文说明会同时从浮层与追问历史里移除、不可撤销;已有追问轮次时追加一行「含 {n} 轮追问」;
29
+ 两个按钮「取消」/「删除」(删除为危险色 `.dsa-btn-danger`)。
30
+ - **无障碍与误触防护**:`role="dialog"` / `aria-modal="true"` / `aria-label` 取标题;默认焦点固定在被标记 `autoFocus`
31
+ 的「取消」上(不落在危险按钮,防连按 Enter);`Esc` 取消(`stopPropagation`,不冒泡到卡片自身的 Esc 处理)、
32
+ 点遮罩取消(仅当 `event.target === event.currentTarget`);确认层打开时卡片的 `R` / `C` / `Esc` 快捷键全部失效。
33
+ - **流式中删除**:先 `abort()` 中止请求 + 通知宿主 `cancel`,丢弃该卡进行中的增量缓冲(`pendingDeltas` / `pendingReasonings`),再执行删除。
34
+ - **删除动作顺序固定**:按同一 `id` 从 `history` 与 `cards` 双移除 → 立即写盘持久化 → 关闭该卡已打开的侧栏 / host tab 表面
35
+ → 浮层 toast「已删除这条追问对话」(2600ms 自动消失)。
36
+ - **删除后重排**:剩余卡片按原顺序塌缩补位;删掉最后一张时 `total === 0` → 浮层整体不渲染(无空态)。
37
+ - **上下文隔离**:每张卡的追问上下文本就按卡隔离,删除不影响其它卡片;后续生成只读取剩余卡片自身历史。
38
+
39
+ ### 移除:反馈表单的「用邮件客户端发送」出口
40
+
41
+ - **UI**:反馈弹窗从三条出口减到两条(`发送` / `复制内容`),删掉 `data-feedback-mailto` 按钮与
42
+ `sendFeedbackByMailApp()`;没有发信服务时弹窗仍按两种就绪状态说明缺什么,并指向「复制内容」。
43
+ - **Host**:删掉 `POST /feedback-mailto` 路由、`buildMailtoUrl()` 构造器与 `API_METHODS` 里的条目。
44
+ 未命中的方法由既有兜底返回 `404 not-found`(`error.message` 带方法名),旧页面调用会明确失败而不是静默。
45
+ - **`/state`**:`capabilities.feedback.transport` 在未配置时由 `'mailto'` 改为 `'none'`;`reason` 只回 SMTP 的原因。
46
+ - **i18n**:`feedbackMailto` 删除,`feedbackMailtoOnly` 重命名为 `feedbackNoCredentials`(文案改指向「复制内容」),
47
+ `feedbackUnconfigured` 同步改写;zh/en 两侧键集仍然完全一致。
48
+ - **理由**:这条出口只是把同一封邮件交给系统邮件应用,与「复制内容」的实际效果重叠,却让 Host 多出一个
49
+ 任何人都能调的 URL 构造接口(还会把明文收件地址拼进 URL)。删掉它,反馈的发送面只剩 SMTP 一条。
50
+ - **测试**:`test/contract-test.mjs` 删除 `buildMailtoUrl` / 路由 200 / 按钮存在三类断言,改为断言
51
+ `transport === 'none'`、`/feedback-mailto` 两次 404(有/无收件人)、`data-feedback-mailto` 不存在、
52
+ 两个退役 i18n 键在 zh/en 都是 `undefined`(`node test/verify.mjs` 的 `t()` 键扫描同时覆盖新键名)。
53
+
54
+ ### 清理:1.9.1 遗留的 10 个死 i18n 键
55
+
56
+ - **删掉 10 个 `feedbackSmtp*` 标签键**:`feedbackSmtpShow` / `feedbackSmtpHide` / `feedbackSmtpEnvNote` /
57
+ `feedbackSmtpHost` / `feedbackSmtpSecure` / `feedbackSmtpPassSaved` / `feedbackSmtpPass` / `feedbackSmtpFrom` /
58
+ `feedbackSmtpHint` / `feedbackSmtpSaveHint`。它们只属于 1.9.1 删掉的设置页 SMTP 表单,全仓已无任何
59
+ `t('feedbackSmtp…')` 调用点,留着只是让翻译者以为这些输入框还在。
60
+ - **配置键不动**:`feedbackSmtpHost` / `feedbackSmtpPort` / `feedbackSmtpSecure` / `feedbackSmtpUser` /
61
+ `feedbackSmtpPass` / `feedbackSmtpFrom` 作为 `config.json` 字段与 `client.js` 的 `CLIENT_DEFAULTS` 继续存在,
62
+ 按环境变量或用户层配置生效;删的只是同名的**界面标签**。
63
+ - **zh/en 同步删**,键集仍完全一致;`test/contract-test.mjs` 的退役键断言扩到 12 个键(含上一节的两个),
64
+ 两个字典都必须取到 `undefined`。
65
+
66
+ ### 兼容性与边界
67
+
68
+ - **不涉及**:无服务端会话存储、无多端同步、无遥测埋点(这些维度 N/A);删除不进「回收站」,**不提供撤销**
69
+ (无 Undo、无快照、无 5 秒窗口)。
70
+ - **承载面差异**:`×` 只出现在**浮层 / 内嵌浮层**(含 dock 到右侧栏的浮层卡片)上。被宿主 tab 承载的同一张卡
71
+ (`better-sidebar` / `native-rightbar` 独立标签页)**不提供 `×`**,沿用它自己的标签关闭 + 底部「关闭」——
72
+ 「关闭」只把卡片摘离浮层、**历史仍保留**在设置页历史管理器里,与「删除」是两种真实需求。
73
+ - **版本号**:`1.9.1 → 2.0.0`,三处同步(`package.json` 的 `version`、`index.js` 的 `PLUGIN_VERSION`、`client.js` 的 `CLIENT_VERSION`)。
74
+ - **测试**:`test/float-placement.mjs` 增补 2.0.0 行为断言(无 `dsa-launcher` / `floatRestore`、`floatControl('capsule')` 存在、
75
+ `onDelete` 已接线、无 `mode === 'off'` 渲染分支、`floatMode` 默认 `capsule`);`test/contract-test.mjs` 增补
76
+ `sanitizeLayer` 丢弃 `'off'`、设置页 option 序列为 `['capsule','full']`、卡片头恰有一枚 `aria-label === cardDelete` 的按钮、
77
+ `deleteCard` 的四类行为、4 个已删键为 `undefined`。
78
+ - **文档**:README 的配置表 / dock 形态说明 / 自测第 8–9 条同步改写,反馈章节的「三条出口」表改为两条并记下移除理由;
79
+ `docs/PRD-followup-delete.md` 出 v3 修订(D0 定稿 + 隐藏能力下线 + 规格逐条对齐已交付代码,路由清单去掉 `feedback-mailto`)。
80
+
81
+ ## 1.9.1
82
+
83
+ **主题:反馈入口重做——从「设置页里一整块常驻表单」改成「顶部一行横幅 + 居中弹窗」。**
84
+
85
+ 用户看过 1.9.0 的界面后原话是「啥也没改啊,反馈部分全部重新做」。复盘:1.9.0 虽然把横幅改成了「默认收起」,
86
+ 但收起态仍是一条**整宽品牌色描边大卡片**,展开态把类型/标题/描述/邮箱/SMTP 六行表单全塞进设置页,视觉上跟改之前
87
+ 没区别,真正的设置项还是被挤到首屏之外。所以 1.9.1 不再做增量修饰,**整块重写**:
88
+
89
+ - **`dsa-feedback-bar` 一行横幅**替代原来的 `dsa-feedback-hero` 卡片:左侧「反馈」标题 + 一句说明,
90
+ 右侧一个主按钮;高度只有一行,设置页第一眼看到的就是真正的设置项。
91
+ - **表单搬进居中对话框**(`FeedbackDialog`,`role="dialog"` / `aria-modal="true"`,`Esc`、点背景、`×` 三种关闭),
92
+ 复用插件已有的 `.dsa-backdrop` / `.dsa-dialog` 弹窗样式,不再往页面流里插任何表单节点。
93
+ - **删掉设置页里的「设置邮件发送(SMTP)」区块**(host/端口/SSL/账号/密码/From 六个输入框 + 折叠按钮):
94
+ 发信凭证属于部署方/维护者,改用**环境变量或用户层 `config.json`**,填报人看不到也不需要这些概念。
95
+ 配置键本身保留,host 侧行为不变。
96
+ - **`/state` 反馈就绪状态**改成在弹窗里用一行状态条说明(已就绪 / 有收件人缺 SMTP / 未配置),不再占设置页版面。
97
+ - 保留 1.9.0 已定的语义:收件人打包默认 `b64:` 混淆值且环境变量优先、填报人「你的邮箱」写进 `Reply-To:`、
98
+ 三条出口(发送 / 用邮件客户端发送 / 复制内容)、host 侧限速与打码日志。
99
+ - 测试:`test/contract-test.mjs` 里原先针对内联表单的 61 行断言整块替换为 126 行(横幅只有一行、点开才渲染表单、
100
+ 弹窗可独立渲染、关闭路径三种、SMTP 输入框**不存在**、字典键齐全),contract 1209/13 → **1239/0**。
101
+ - README §4.4 与配置表同步重写;不再出现「在设置页填 SMTP」这类指引。
102
+
103
+ ## 1.9.0
104
+
105
+ **主题:设置页多了一个「反馈」入口——把问题与建议直接用邮件发出去,而仓库里不存任何邮箱或密码。**
106
+
107
+ 起因是用户要求:在插件设置页新增「反馈」按钮,让用户把问题和建议通过邮件发到指定邮箱,并明确要求
108
+ 「邮箱地址与服务凭证通过配置项或环境变量注入,不得硬编码或写入仓库」「不引入非必要第三方依赖」。
109
+
110
+ - **一条可折叠的反馈横幅**(设置页**最顶部**、**默认收起**、品牌色描边卡片 `data-dsa-feedback-section`):收起时只有
111
+ 一行(标题 + 一句话说明 + 主按钮),点开才是表单——类型(问题 / 建议 / 疑问 / 其他)、标题(4–120 码点)、
112
+ 详细描述(10–4000 码点)、可选的**你的邮箱**;收件人不占界面位置(见下)。
113
+ 标题拒绝换行(头注入的第二道防线);计数按**码点**,一段 120 个 emoji 的标题不会被当成 240 字拒绝。
114
+ - **`From:` 仍是维护者自己的 SMTP 账号,填报人的邮箱进 `Reply-To:`**。QQ/163 会拒收不属于本账号的
115
+ `From:`(553),所以发件身份不变;填报人在「你的邮箱」里填了地址,Host 校验为邮箱后写成 `Reply-To:`,
116
+ 维护者按「回复」直接回到填报人(该值不含 CR/LF,无法注入头字段);留空则整行不写。`mailto:` 回退路径
117
+ 没有 `Reply-To` 可用,同一地址同时留在正文 `Contact:` 行。(早期版本一度完全删掉 `Reply-To`、只留正文行;
118
+ 在用户要求「跟 recycle-bin 的效果一样」之后改回现在的形态。)
119
+ - **收件人完全不在页面上出现**:`/state` 只给 `hasRecipient` / `toMasked` 这类摘要,`config` 里也删掉了
120
+ `feedbackAddress`;设置页既没有掩码行,也没有「更换收件人」入口与地址输入框。收件人优先级是**环境变量
121
+ (`DSH_SIDECARD_FEEDBACK_TO` 或共用的 `DSH_CRB_FEEDBACK_TO`)> 用户层/打包配置 `feedbackAddress`**。
122
+ 「用邮件客户端发送」向 Host 要一条拼好的 `mailto:` URL 再跳转。
123
+ 就绪状态分三种说(没地址 / 有地址缺 SMTP / 齐全),不再把「能用邮件客户端发送」说成「未配置」。
124
+ 这条经历**两轮返工**:第一版给了 `type="password"` + 「显示收件人」开关,用户截图指出「这不是直接暴露
125
+ 我的邮箱吗」——一键看明文本身就是暴露面;第二版改成只渲染掩码 + 只写更换入口,用户又指出这两块本来
126
+ 就不该存在,于是全部删除。
127
+ - **三条出口**:`发送`(收件邮箱 + SMTP 凭证齐备时可用,成功后清空草稿)/ `用邮件客户端发送`(只要收件邮箱,
128
+ 拼 `mailto:`,**零凭证**)/ `复制内容`(总是可用,纯文本进剪贴板)。未配置时提交按钮**不置灰**——
129
+ 点下去会拿到 503 与原因,比一个死按钮更容易自查。
130
+ - **凭证优先从环境变量来**(后来补上设置页,见下一条):`DSH_SIDECARD_SMTP_URL`(`smtps://user%40example.com:pass@smtp.example.com`;
131
+ 也接受共用的 `DSH_CRB_SMTP_URL`)或分开的 `DSH_SIDECARD_SMTP_{HOST,PORT,SECURE,USER,PASS,FROM}`。
132
+ 收件邮箱走 `DSH_SIDECARD_FEEDBACK_TO` / `DSH_CRB_FEEDBACK_TO`(**优先**)或配置项 `feedbackAddress`。
133
+ `?from=` / `DSH_SIDECARD_SMTP_FROM` / `USER` 取到的发件地址必须像邮箱,否则整条 SMTP 路径判为未配置。
134
+ - **SMTP 也能直接在设置页填**(用户报「无法发送邮件」之后补的):本机 DSH 进程里没有任何 `DSH_SIDECARD_*`
135
+ 变量——普通用户不会给 DSH 设环境变量,所以「凭证只能来自环境变量」等于「发送按钮永远 503」。于是新增
136
+ `feedbackSmtpHost/Port/Secure/User/Pass/From` 六个配置项,折叠在「设置邮件发送(SMTP)」里,保存在
137
+ 用户层 `config.json`(仓库外,不进 npm 包)。**密码只写不读**:`/state` 删掉该键、页面永远拿到空串、
138
+ 空串保存=保留原值、`/state` 的 `config` 里也没有它。默认端口 465 配隐式 TLS;587 走 STARTTLS。
139
+ 顺带修掉一个真 bug:**STARTTLS 现在只在 `EHLO` 回复声明了它时才升级**,否则会给没开 STARTTLS 的中继
140
+ 发一个必被拒的动词,把一个正常的明文中继判成「发信失败」。
141
+ - **没有新依赖**:`node:net` + `node:tls` 手写最小 SMTP 客户端(`sendSmtpMessage`)——多行 `250-` 应答汇总、
142
+ `EHLO` → `STARTTLS`(非隐式 TLS 时)→ `AUTH PLAIN`(被拒回退 `AUTH LOGIN`)→ `MAIL FROM` → `RCPT TO` →
143
+ `DATA`(dot-stuffing + `\r\n.\r\n`)→ `QUIT`,20 秒超时,finally 清 socket/timer。
144
+ - **邮件本身**:`text/plain; charset=UTF-8` + base64 正文,主题 RFC 2047 编码(中文标题不乱码),
145
+ 正文附插件版本 / DSH 版本 / Node 版本 / 平台 / 时间;**不含主机名、用户名、路径**。
146
+ - **限速与日志**:Host 侧 30 秒 1 封、1 小时 5 封,失败返回 `feedback-too-fast` / `feedback-too-many` 并带
147
+ `retry-after`;只有真正投递成功才计入配额。日志只记 kind 与打码收件人(`o***@example.com`),**从不记正文**。
148
+ - **收件邮箱以 `b64:` 混淆打包**:默认值是维护者收件箱,存成 `b64:<base64>`,所以明文 `name@domain` 不出现在
149
+ 仓库与 npm 包里,挡的是正则抓取。**这是混淆不是加密**——`decodeAddress()` 就在同包里,拿到包的人总能还原;
150
+ 地址也仍可由用户层配置或环境变量整体接管。早期版本默认空字符串、要求每个部署方自己配;用户要求
151
+ 「跟 recycle-bin 的效果一样」之后改成现在这样,代价是维护者邮箱随包公开(可接受:反馈本来就该寄给它)。
152
+ - 错误码契约:`feedback-invalid`(400,边界/头注入)、`feedback-unconfigured`(503,缺收件人或缺 SMTP)、
153
+ `feedback-too-fast` / `feedback-too-many`(429)、`feedback-send-failed`(502,附服务器原文)。
154
+ `/state` 的 `capabilities.feedback` 给出 `{transport, configured, toMasked, reason, limits}`,设置页据此显示就绪状态。
155
+ - 顺手把深色主题下的主按钮字色改成深墨(`.dsa-btn-primary`):这是全站一直存在的 2.79:1,不是本次引入,
156
+ 但新卡片里的「发送」正好是主按钮,一并修掉。
157
+ - 修复了一处顺带发现的真问题:`writeJson` 用 `res.writeHead(status, {...})` 会**丢弃**此前 `setHeader` 的值,
158
+ 429 的 `retry-after` 因此一直发不出去;`writeJson/writeError` 现在接受 `extraHeaders`。
159
+ - 测试:`contract` 新增 **232** 项(纯函数边界、`parseSmtpUrl`/`readSmtpFromEnv` 两种写法(含 `DSH_CRB_SMTP_URL`
160
+ 别名)、`b64:` 收件人解码、「环境变量优先于配置项」的优先级回归、`Reply-To:` 组头、组信的 base64 与 RFC 2047、
161
+ 限速闸门,以及一条**真 socket** 的 `/feedback` 端到端——本地 SMTP 假中继逐条断言 `AUTH PLAIN`、信封、`DATA`、
162
+ 编码后的主题与正文、限速 429 与 `retry-after`,并断言正文**没有进日志**)。
163
+ 总项数 1359 → **1582**(verify 57 + contract 1221 + float 78 + smoke 226)。
164
+ - **同日二次配置(用户要求「跟 dsh-conversation-recycle-bin 的效果一样」)**,四处对齐:
165
+ 1. 反馈横幅**默认收起**成一行,主按钮点开才展开表单(原来默认展开,把真正的设置项挤到屏幕外);
166
+ 2. 「联系方式」改成**你的邮箱**:`type="email"` + 客户端格式预校验 + Host 侧再校验,填了作为 `Reply-To:`;
167
+ 3. 收件人默认值改为**打包的 `b64:` 混淆地址**,并让**环境变量优先**于它(原来配置优先,一旦默认非空,
168
+ 环境变量就永久失效);`feedbackAddress` 也接受 `b64:` 形式并在解码后校验;
169
+ 4. 直接复用兄弟插件的 `DSH_CRB_SMTP_URL` / `DSH_CRB_FEEDBACK_TO`:本机只配一次,两个插件都能发信。
170
+ 实测(本机用户级 `DSH_CRB_SMTP_URL` 注入后):`describeFeedback()` → `configured:true, transport:'smtp'`。
171
+ - **如实标注**:
172
+ 1. 只对**本地假中继**验证过协议,没有连过真实服务商(Gmail / QQ / 163 的应用专用密码、白名单、
173
+ `MAIL FROM` 与 `USER` 同域等要求都未实测);
174
+ 2. 设置页的**展开交互**仍无法在合成 DOM 里断言——`test/harness.mjs` 的 `useState` 不触发重渲染;测试覆盖的是
175
+ 「默认收起(`style.display:none`)、按钮存在、`aria-expanded='false'`、字段齐全」,展开后的输入与提交需在页面上手测;
176
+ 3. 「用邮件客户端发送」只验证了 URL 的编码,没有真的拉起系统邮件应用;
177
+ 4. 只有纯文本,没有附件/截图上传(见交付说明里的待确认项)。
178
+
3
179
  ## 1.8.4
4
180
 
5
181
  **主题:选区旁的动作条从「六枚等价胶囊排成一排」改成一块面板加一个主操作——并顺手堵掉右侧被窗口裁掉的那条路。**
package/README.md CHANGED
@@ -22,7 +22,8 @@
22
22
 
23
23
  > **1.5.0 增强一览**(详见 CHANGELOG):内置浮层不再压住主对话——宽度改由纯函数 `floatPlacement`
24
24
  > 按「对话区右侧可用带宽」计算(旧实现是固定 400px,在用户截图上压入正文约 113px);浮层默认是**一行高的胶囊**,
25
- > 可随时「收起 / 隐藏」,运行时的形态切换不写回配置;新增 `floatMode`(默认 `capsule`)与
25
+ > 可随时「收起为胶囊」,运行时的形态切换不写回配置;卡片标题栏右侧的 `×` 自 2.0.0 起是**删除这条追问对话**
26
+ > (连同追问历史一起删),不再是隐藏浮层;新增 `floatMode`(默认 `capsule`)与
26
27
  > `floatMaxWidth`(默认 340)两项设置;卡片阴影减半。
27
28
 
28
29
  > **1.4.0 增强一览**(详见 CHANGELOG):卡片**思考过程**(reasoning)可折叠展示并随答案流式渲染;
@@ -163,9 +164,9 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
163
164
  | `defaultCarrier` | `main` \| `side` | `side` | **默认作答位置**:提问框里仍可临时切换 | 立即 |
164
165
  | `sideSurface` | `auto` \| `native-rightbar` \| `better-sidebar` \| `flow` | `auto` | **窗口模式(侧边承载面)**:自动挑选(先 dsh-better-sidebar,再内置浮层卡片)/ 强制 DSH 原生右侧栏(不参与自动)/ 强制 dsh-better-sidebar / 强制内置浮层卡片 | 立即 |
165
166
  | `maxChars` | 200–60000 | `4000` | **最大字符数**:超出部分头尾保留、中间截断并标注 | 立即 |
166
- | `floatMode` | `capsule` \| `full` \| `off` | `capsule` | **内置浮层的默认形态**:胶囊(一行,最小遮挡)/ 完整卡片栈 / 只留一个计数圆点。运行时可即时收起、隐藏,但不写回本项——刷新后仍回到这里设的默认 | 立即 |
167
+ | `floatMode` | `capsule` \| `full` | `capsule` | **内置浮层的默认形态**:胶囊(一行,最小遮挡)/ 完整卡片栈。运行时可即时收起为胶囊,但不写回本项——刷新后仍回到这里设的默认。旧配置里存过 `off` 的会在启动时校验失败、回退为 `capsule`,并在 `problems` 里给出提示(`index.js` 的 `sanitizeLayer`),无需手工迁移 | 立即 |
167
168
  | `floatMaxWidth` | 240–560 | `340` | **内置浮层宽度上限**(px):实际宽度 = `min(本项, 对话区右侧可用带宽)`,可用带宽不足 240px 时才会压入对话区 | 立即 |
168
- | `dockRail` | 布尔 | `true` | **右侧栏打开时占满它**:答案卡铺满右栏标签条下方的内容区,读起来像栏内的一页,而不是 340px 的小浮层。收起为胶囊、关闭浮层、右栏宽度 < 280px 或高度 < 240px 时自动回到常规浮层;实现靠测量右栏 DOM 矩形,**没有**走 DSH 的右栏 API(见 §4.2 与 §6.1) | 立即 |
169
+ | `dockRail` | 布尔 | `true` | **右侧栏打开时占满它**:答案卡铺满右栏标签条下方的内容区,读起来像栏内的一页,而不是 340px 的小浮层。收起为胶囊、点卡片底部「关闭」、右栏宽度 < 280px 或高度 < 240px 时自动回到常规浮层;实现靠测量右栏 DOM 矩形,**没有**走 DSH 的右栏 API(见 §4.2 与 §6.1) | 立即 |
169
170
  | `shortcut` | 形如 `Alt+Q`、`Ctrl+Shift+K` | `Alt+Q` | **快捷键**:唤起提问框(`trigger` 允许时) | 立即 |
170
171
  | `captureZones` | `auto` \| `chat` \| `task` \| `chat+task` | `auto` | 只在哪些区域触发 | 立即 |
171
172
  | `showInUnclassified` | 布尔 | `true` | 无法归类的区域是否也触发 | 立即 |
@@ -181,6 +182,13 @@ plugin_manager(action="install_bundle", target="D:\\Users\\34332\\AI\\dsh-sideca
181
182
  | `answerStyle` | `concise` \| `standard` \| `detailed` | `standard` | **回答风格档位**:只给最小完整答案 / 现状 / 必要时展开并给出依据。指令追加在人格之后,**不会**覆盖"用提问的语言作答"与"选中文本是数据"这两条 | 下一次提问 |
182
183
  | `customPersona` | 字符串,≤500 码点 | `''` | **自定义指令**:追加在风格档位之后,以 `Additional user instruction:` 标注随每次提问发送(例如"尽量给可验证来源") | 下一次提问 |
183
184
  | `followUpSuggestions` | 布尔 | `false` | **模型生成的建议追问**:让模型在答案末尾用 `<followups>问1 \| 问2</followups>` 给出 2–3 个可点的下一步问题(客户端剥离该块并渲染为芯片,**不额外发请求**) | 下一次提问 |
185
+ | `feedbackAddress` | 字符串(邮箱或 `b64:` 编码)\| `''` | 维护者收件箱(`b64:` 混淆存储) | **反馈收件邮箱**:设置页「反馈」弹窗把问题与建议发到这里。默认值指向维护者自己的邮箱,以 `b64:<base64>` 形式打包——**这是混淆不是加密**,`decodeAddress()` 就在同包里,只能挡住正则抓取;环境变量 `DSH_SIDECARD_FEEDBACK_TO` / `DSH_CRB_FEEDBACK_TO` **优先于**本项,置空则反馈弹窗只留「复制内容」 | 立即 |
186
+ | `feedbackSmtpHost` | 字符串,≤255 | `''` | **SMTP 服务器**。1.9.1 起只从**环境变量或用户层配置**读。**环境变量优先于配置**(`DSH_SIDECARD_SMTP_URL` / `DSH_CRB_SMTP_URL` 等能凑出完整端点时,配置里的整段不生效);只有用户层配置会存它,**不进仓库** | 立即 |
187
+ | `feedbackSmtpPort` | 数字,1–65535 | `465` | SMTP 端口。465 配隐式 TLS,587 配 STARTTLS(服务器没声明 STARTTLS 时不会硬升级) | 立即 |
188
+ | `feedbackSmtpSecure` | 布尔 | `true` | 隐式 TLS(465)。关掉则按 STARTTLS 处理 | 立即 |
189
+ | `feedbackSmtpUser` | 字符串,≤254 | `''` | SMTP 登录账号。部分中继给的是裸账号名而非邮箱,故不强制邮箱格式 | 立即 |
190
+ | `feedbackSmtpPass` | 字符串,≤512 | `''` | **密码 / 授权码**(QQ、163 必须用授权码)。**只写不读**:`/state` 会删掉它、页面永远拿到空串;留空保存=保留原值 | 立即 |
191
+ | `feedbackSmtpFrom` | 字符串(邮箱)\| `''` | `''` | 发信账号显式指定的 `From:`,留空等于 `feedbackSmtpUser`。必须是 SMTP 服务商允许的地址 | 立即 |
184
192
 
185
193
  非法值会被拒绝(设置页报错、接口返回 `invalid-config`),并把被忽略的项列在 `/state` 的 `problems` 里——不会静默吞掉。
186
194
 
@@ -216,7 +224,7 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
216
224
  - 位置 = 右栏**标签条之下**的内容区矩形,宽度取右栏宽度,高度取到底部;右栏自己的标签条仍然露出、可点。
217
225
  - 卡片在这个形态下没有圆角/描边/阴影(`.dsa-card` 的装饰被 `[data-dsa-float-mode="dock"]` 覆盖),只有一条左边线与右栏内容区分界。
218
226
  - 标题栏(追问 + 徽标 + ⇥ / ×)与底部操作行固定上下两端,中间 `AnswerBody` 独立滚动——长答案不再把操作行推出视野。
219
- - 收起为胶囊(⇥)、关闭(×)、或把 `dockRail` 关掉,都立即回到常规浮层;右栏宽度 < 280px 或高度 < 240px 时也回退(窄栏里铺满会把正文挤成每行 8–9 字)。
227
+ - 收起为胶囊(⇥)、删除这条追问对话(×)、或把 `dockRail` 关掉,都立即回到常规浮层;右栏宽度 < 280px 或高度 < 240px 时也回退(窄栏里铺满会把正文挤成每行 8–9 字)。注意 × 是**删除**:卡片从浮层与追问历史里一并移除;只想摘离浮层、保留历史请用底部的「关闭」。
220
228
  - 折叠的右栏不会被误用:`railOnScreen()` 会拒绝停在帧外(`left >= frameWidth - 1`)或尺寸不足的面板,量不到就回退,绝不把卡片画到帧外。
221
229
  - 矩形从哪来:**优先量本插件自己渲染在右栏里的 `CardHost` 节点**(`[data-dsa-rail-host]`)——它所在的位置按定义就是「一页正文」,不依赖任何未被文档承诺的标记;找不到时再按 `data-sidebar-right-*`、dockkit 世代、`.rightbar`、`sidebar.right` 槽逐代回退。
222
230
  - 什么时候重算:窗口改宽、右栏开合(只盯属性的 `MutationObserver`)、右栏自身改宽(`ResizeObserver`),以及挂载后的四次延时重测(罩住右栏的滑入动画)。流式刷新不会触发几何重算。
@@ -235,7 +243,7 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
235
243
  | `no-rail` | 没找到已展开的右栏(右栏关着,或这个构建的标记与预期不同) |
236
244
  | `rail-too-small` | 找到了,但宽 < 280px / 高 < 240px,或它停在帧外(折叠态被拒绝) |
237
245
  | `off` | 设置里的 `dockRail` 被关掉了 |
238
- | `mode` | 当前不是完整卡片形态(胶囊 / 隐藏),本来就不该铺满 |
246
+ | `mode` | 当前不是完整卡片形态(胶囊 / 折叠态),本来就不该铺满 |
239
247
 
240
248
  ### 4.3 选区旁的动作条(外观)
241
249
 
@@ -255,6 +263,99 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
255
263
  外观之外没有行为变化:键盘漫游、`Alt+1…5`、右键菜单、点击后直接发问都照旧。这条也不写配置项——
256
264
  它是版式修正,不需要开关。
257
265
 
266
+ ### 4.4 设置页的「反馈」入口(邮件)
267
+
268
+ 设置页**最顶部**是一条品牌色描边的**反馈横幅**(`dsa-feedback-bar`):一行文字 + 右侧主按钮,**点开才是表单**,
269
+ 表单放在一个居中对话框(`role="dialog"`,`Esc` / 点背景 / 右上角 `×` 都能关)。1.9.1 之前是「顶部常驻内联表单」,
270
+ 它把真正的设置项全挤到屏幕外,被否掉后重做成现在这种「一条横幅 + 一个弹窗」。表单字段:类型(问题 / 建议 / 疑问 / 其他)、
271
+ 标题、详细描述、可选的**你的邮箱**(填了就写进邮件的 `Reply-To:`,维护者按「回复」能直接回给你;留空即匿名)。
272
+ **设置页上没有任何 SMTP 输入框**:发信凭据属于部署方/维护者,走环境变量或用户层配置(见下),填报人不用管。
273
+ 两条出口,从首选到兜底:
274
+
275
+ | 出口 | 何时可用 | 说明 |
276
+ |---|---|---|
277
+ | **发送**(推荐) | 收件邮箱已配置 **且** SMTP 凭证可用(环境变量或用户层配置) | 插件直接把邮件投给 SMTP 服务器;成功后清空草稿 |
278
+ | **复制内容** | 总是 | 把同样的纯文本(含版本与环境)放进剪贴板,用户自己粘到任意渠道 |
279
+
280
+ > 1.9.x 曾有的第三条出口「用邮件客户端发送」(`POST /feedback-mailto` 返回拼好的 `mailto:` URL)已在
281
+ > 2.0.0 整体移除:它让插件多出一条谁都可以调的出口,却只是把同一封邮件交给系统邮件应用,与「复制内容」的
282
+ > 实际效果重叠。现在没有发信服务时,弹窗直接说明缺什么,并指向「复制内容」。
283
+
284
+ **收件邮箱默认可用,也可被环境变量接管。** 打包默认值是维护者邮箱(`b64:` 混淆存储,见上表);
285
+ 环境变量 `DSH_SIDECARD_FEEDBACK_TO` 或 `DSH_CRB_FEEDBACK_TO`(与 dsh-conversation-recycle-bin 共用)
286
+ **优先**于它。用户层配置落在 `$DSH_HOME/sidecard-ask/config.json`
287
+ (Windows 默认 `C:\Users\<你>\.dsh\sidecard-ask\config.json`)——**不在插件目录里,因此不会被提交、
288
+ 不会进 npm 包**。
289
+
290
+ **SMTP 凭证有两种给法**,都落在部署方/维护者这一侧(1.9.1 起设置页不再提供填写框):
291
+
292
+ 1. **用户层配置**(`$DSH_HOME/sidecard-ask/config.json`,Windows 默认
293
+ `C:\Users\<你>\.dsh\sidecard-ask\config.json`,或 profile 的 `cordis.patch.yml` 里给该行写 `config`):
294
+ `feedbackSmtpHost` / `feedbackSmtpPort` / `feedbackSmtpSecure` / `feedbackSmtpUser` / `feedbackSmtpPass` /
295
+ `feedbackSmtpFrom`。**密码只写不读**:`/state` 删除该键、页面永远拿到空串,留空保存表示保留原值。
296
+ QQ 邮箱请填 `smtp.qq.com` / 465 / `secure: true` / 你的 QQ 邮箱 / **授权码**
297
+ (不是 QQ 密码,要在 QQ 邮箱后台「设置 → 账户 → POP3/SMTP 服务」里生成)。
298
+ 2. **环境变量**(推荐,**优先级高于用户层配置**),有两种写法:
299
+
300
+ ```bash
301
+ # 写法一:一个 URL(smtps = 隐式 TLS,端口默认 465;smtp = STARTTLS,端口默认 587)
302
+ DSH_SIDECARD_SMTP_URL="smtps://user%40example.com:app-password@smtp.example.com"
303
+ # 也可以直接复用 dsh-conversation-recycle-bin 的那一个:
304
+ DSH_CRB_SMTP_URL="smtps://user%40example.com:app-password@smtp.example.com"
305
+
306
+ # 写法二:分开写(密码里带 @ 或 : 时不用手动转义)
307
+ DSH_SIDECARD_SMTP_HOST="smtp.example.com"
308
+ DSH_SIDECARD_SMTP_PORT="465"
309
+ DSH_SIDECARD_SMTP_SECURE="1" # 1/true/yes = 隐式 TLS
310
+ DSH_SIDECARD_SMTP_USER="user@example.com"
311
+ DSH_SIDECARD_SMTP_PASS="app-password"
312
+ DSH_SIDECARD_SMTP_FROM="user@example.com" # 可选,默认等于 USER
313
+ ```
314
+
315
+ 补充约定(都会体现在 `/state` 的 `capabilities.feedback` 里,反馈弹窗据此显示「已就绪 / 未配置」):
316
+
317
+ - **环境变量优先于用户层配置**,而且只认「完整的那一套」:环境里能凑出主机+账号+密码就用环境(`smtp.source = 'env'`),
318
+ 否则才看用户层 `config.json` 填的;两边都不完整就报「还没填全」。这条顺序是有原因的——本机曾出现「环境变量是好的、
319
+ 配置里留着半套(填了主机没填密码)」,旧实现先看配置,于是一套能用的凭证被半套配置挡住,
320
+ 反馈永远发不出去。1.9.1 起设置页不自带 SMTP 表单,这条优先级只作用于「环境变量 vs 用户层配置」。
321
+
322
+ - **发件人必须像邮箱**:`?from=` / `DSH_SIDECARD_SMTP_FROM` / `USER` 三者取到的地址必须通过邮箱格式检查,
323
+ 否则整条 SMTP 路径判为未配置——`MAIL FROM` 里塞不进一个合法地址就该早点说清楚。
324
+ - **STARTTLS 只在服务器声明时才升级**:先看 `EHLO` 的多行回复里有没有 `STARTTLS`,没有就直接明文对话;
325
+ 否则一个没开 STARTTLS 的内网中继会被自己的升级请求判成「发信失败」。
326
+ - 本地/内网中继可以用 `smtp://user:pass@host:25?starttls=0` 关掉升级;**不要**对公网服务器这么写。
327
+ - 邮件正文是 `text/plain; charset=UTF-8` + base64,主题用 RFC 2047 编码(中文标题不会变乱码);
328
+ 正文里附插件版本、DSH 版本、Node 版本、平台与时间,**不含主机名、用户名或任何路径**。
329
+ - 限速在 Host 侧:30 秒内最多一封、1 小时内最多 5 封;失败返回 `feedback-too-fast` /
330
+ `feedback-too-many` 并带 `retry-after` 头。只有**真正投递成功**才计入配额,中继故障不烧配额。
331
+ - 日志只记 kind 与**打码后的**收件人(`o***@example.com`),**从不记录正文**。
332
+ - **收件人从页面里彻底消失**(两轮返工的终点:先删掉「显示收件人」开关,再按用户要求删掉掩码行与
333
+ 「更换收件人」按钮):
334
+ - `/state` 的 `capabilities.feedback` 只给 `hasRecipient` / `toMasked` 这类摘要,**没有** `to`;
335
+ - `/state` 的 `config` 里也**删掉了** `feedbackAddress`,页面拿不到它;
336
+ - 设置页不显示收件人的任何形式——没有掩码行、没有更换入口、没有地址输入框。收件人只由用户层配置
337
+ `feedbackAddress` 或环境变量 `DSH_SIDECARD_FEEDBACK_TO` 提供,那是部署方/维护者的事,填写者不用管;
338
+ - 页面也**不会**拿到 `mailto:` URL:2.0.0 删掉了 `/feedback-mailto` 路由与其 URL 构造器,
339
+ 未命中的方法一律得到 `404 not-found`。
340
+ - **`From:` 仍是维护者自己的 SMTP 账号,但填报人的邮箱会写进 `Reply-To:`**:QQ/163 会拒收不属于本账号的
341
+ `From:`(553),所以发件身份不变;填报人在「你的邮箱」里填了地址,Host 就把它作为 `Reply-To:` 写进邮件头,
342
+ 维护者按「回复」直接回到填报人。该值在 Host 侧先过邮箱格式校验且不含 CR/LF,因此无法注入头字段;
343
+ 留空则整行不写(匿名反馈)。
344
+
345
+ - 两种就绪状态在界面上分开说,不混成一个「未配置」:没有收件人 → 说没有可用发信服务;有收件人但没有 SMTP
346
+ 凭证 → 说「只缺发信凭据」;齐全 → 什么都不提示。两种情况都指向「复制内容」,不再留一条邮件应用捷径。
347
+ - **关于「加密」的实话**:这里的邮箱只是一次 SMTP 的收件地址,插件必须能解出明文才能发信,任何随插件
348
+ 分发的「加密」都等于把钥匙一起发出去,只能算混淆——打包默认值用的 `b64:` 就是这种混淆,只挡正则抓取。
349
+ 真正有效的保护是**只存本机配置 + 明文不进浏览器 + 日志打码 + 可被环境变量整体接管**——这四条都已实现。
350
+ 仍然避免不了的一处是**已经提交进 git 历史的作者邮箱**(见下文)。
351
+
352
+ 失败排查顺序:`/state` 的 `capabilities.feedback.reason`(它会直接说「账号或密码还没填全」这类话)→
353
+ 用户层配置是否整行都在(服务器/端口/账号/密码;**端口必须是数字**,字符串会被判非法)→ 环境变量是否
354
+ 被 DSH 进程继承(改完要重启 DSH)→ SMTP 服务商是否要求**应用专用密码**(Gmail / QQ 邮箱 / 163 都要求,
355
+ 登录密码会被拒)→ 端口与 TLS 写法是否匹配(465 用隐式 TLS;587 让服务器自己声明 STARTTLS)→
356
+ 中继是否在白名单里要求 `FROM` 与 `USER` 同域。
357
+ `feedback-send-failed` 的 message 是服务器原文,直接读它比猜快。
358
+
258
359
  ---
259
360
 
260
361
  ## 五、侧边承载面与"侧边卡片插件"适配
@@ -271,7 +372,7 @@ bleeds = 对话区右边界 - left > 1 // 只有带宽 < 240px 时才
271
372
  这是 `auto` 的**第一选择**。
272
373
  - 没装:自动使用内置浮层卡片,功能完全一致——**这条路径是第二选择,也是保底路径**。
273
374
  浮层默认是**贴帧右边缘的胶囊**(一行高,只有状态与问题首句),点击才展开成完整卡片,
274
- 标题栏右侧的「收起 / 隐藏」可随时收回;它的宽度由 `floatPlacement` 计算,
375
+ 标题栏右侧的 ⇥ 可随时收起为胶囊、× 则删除这条追问对话(连同追问历史,不可撤销);它的宽度由 `floatPlacement` 计算,
275
376
  只会占用**对话区右侧那条预留带**,不会压住正文(细节见 §4.1)。
276
377
  - **原生右侧栏为什么退出 `auto`**:卡片被塞进右栏只是一个 tab,而这个 tab 的生命周期不归插件管——
277
378
  卡片关闭后右栏仍留着一条读作「该卡片已关闭」的空标签,和你自己的右栏标签抢位置,
@@ -614,16 +715,19 @@ node tools/compat-probe.mjs --json tools/.cache/matrix.json
614
715
  「第 1 版」;点它 → 显示**旧问题 + 旧答案**并标注"正在查看历史版本(只读)",点「返回最新」回到新答案。
615
716
  刷新页面后历史卡片仍能切版本(v3 持久化)。连续改问 6 次后只保留最近 5 版。
616
717
  8. **浮层形态与位置(1.5.0)**:默认应是一个**一行高的胶囊**(状态 + 问题首句),点它才展开成完整卡片;
617
- 展开后标题栏右侧有「收起为胶囊」(⇥)与「隐藏浮层」(×)——隐藏后只剩一个带计数的圆点,点它恢复。
718
+ 展开后标题栏右侧有**收起为胶囊(⇥)**与**删除这条追问对话(×)**——× 会把该卡片从浮层与追问历史里
719
+ 一并移除,**无法撤销**(有内容时会先弹插件内确认层;点浮层底部的「关闭」则只把它摘离浮层、历史仍保留在
720
+ 设置页的历史管理器里)。2.0.0 起不再有「隐藏整个浮层 / 只留一个计数圆点」这套形态。
618
721
  用 F12 选中浮层根节点(`[data-dsa-root]`)读两个值自检:
619
- - `data-dsa-float-mode` 应为 `capsule` / `full` / `off`;
722
+ - `data-dsa-float-mode` 应为 `capsule` / `full`(铺满右栏时为 `dock`);
620
723
  - `data-layer-width` 应等于 `min(floatMaxWidth, 对话区右边界到帧右边缘的可用带宽)`。
621
724
  **关键判据:胶囊/卡片的左边缘不得越过对话区(`[data-slot^="conversation"]`)的右边界**。
622
725
  把窗口从 1920px 拖到 900px、再开关一次右侧栏,浮层应随之变窄而不是往左压住正文;
623
726
  只有窗口窄到可用带宽不足 240px 时才允许压入(此时是设计内的降级,不是 bug)。
624
727
  9. **占满右侧栏(1.8.3,`dockRail`)**:先把右侧栏打开(任意面板,例如「团队任务板」),再追问一次 →
625
728
  答案卡应铺满**标签条下方**的整块内容区:右边缘贴帧边、下边缘贴窗口底,右栏的标签条仍然露出可点。
626
- 点卡片标题栏的 ⇥(收起为胶囊)或 × → 卡片立即退出占满、回到常规浮层,右栏内容区恢复。
729
+ 点卡片标题栏的 ⇥(收起为胶囊)→ 卡片立即退出占满、回到常规浮层,右栏内容区恢复;点 × 则是删除这条
730
+ 追问对话(同样退出占满,但卡片与历史都不再存在)。
627
731
  自检:F12 读浮层根节点,`data-dsa-float-mode` 应为 `dock`,`data-dsa-dock` 也应为 `dock`;
628
732
  `.dsa-stack` 上的 `data-dsa-dock-body` 应等于卡片宽度(px)。**没铺满就看 `data-dsa-dock`**:
629
733
  `no-rail` = 没找到右栏,`rail-too-small` = 找到了但太窄或在帧外,`off` = 开关关了,`mode` = 不是完整形态。
@@ -664,15 +768,21 @@ node tools/compat-probe.mjs --json tools/.cache/matrix.json
664
768
  cd D:\Users\34332\AI\dsh-sidecard-ask
665
769
  node test/verify.mjs # 静态:清单/补丁/导出面/i18n/零依赖
666
770
  node test/contract-test.mjs # 契约:常量、信封、SSE 逐帧、纯函数、槽位注册与渲染
667
- node test/float-placement.mjs # 几何:浮层宽度不越过对话区右边界(1.5.0 新增)
668
- node test/smoke-test.mjs # 端到端:流式/截断/取消/持久化/失败分支/并发/围栏
771
+ node test/float-placement.mjs # 浮层定位矩阵
772
+ node test/smoke-test.mjs # 冒烟:装载、槽位、渲染、错误路径
669
773
  ```
670
774
 
775
+ 当前规模(本机实测,四项均 exit 0):verify **57** 项 + contract **1239** 项 + float-placement **78** 项 +
776
+ smoke **226** 项 = **1600 项全部通过**(较 1.8.4 的 1359 多 241 项;contract 从 989 涨到 1239,新增大多来自
777
+ 反馈功能:
778
+ 纯函数边界、`parseSmtpUrl` / `readSmtpFromEnv` 两种写法(含复用 `DSH_CRB_SMTP_URL`)、`b64:` 收件人解码、
779
+ 「环境变量优先于用户层配置」的优先级回归、`Reply-To:` 组头、限速闸门,以及一条**真 socket** 的 `/feedback`
780
+ 端到端——本地 SMTP 假中继逐条断言 `AUTH PLAIN`、信封、`DATA`、编码后的主题与正文、429 与 `retry-after`,
781
+ 并断言正文没有进日志;1.9.1 又把内联表单的断言整块换成「横幅一行 + 弹窗表单」的断言)。
782
+
671
783
  四个脚本都以 `process.exitCode` 反映结果,失败会列出具体条目;测试会把 `DSH_HOME` 指向临时目录,不会污染真实配置。
672
- 当前规模(本机实测,四项均 exit 0):verify **57** 项 + contract **998** 项 + float-placement **78** 项 +
673
- smoke **226** 项 = **1359 项全部通过**(较 1.8.3 的 1350 多 9 项:选区动作条改成单一面板后新增的
674
- 3 条渲染断言——恰好一个 `.dsa-quickbar-sep`、chips 自成一组、主按钮是 embedded 变体——与 6 条样式断言:
675
- 单一面板、品牌实底主按钮、chips 去边框、分隔线规则、深色墨字覆盖、以及面板自带 `maxWidth` 上限。
784
+ 1.8.4 为 1359 项:contract 989 → 998,新增 6 条钉住选区动作条的版式(单一面板、品牌实底主按钮、
785
+ chips 去边框、分隔线规则、深色墨字覆盖、面板自带 `maxWidth` 上限)。
676
786
  1.8.3 为 1350 项:contract 972 → 989,新增 8 项钉住设置页的「占满
677
787
  右侧栏」复选框、3 个新 i18n 键,以及承载面 `auto` 的新顺序;float-placement 38 → 78,新增 40 项覆盖 `railDock` 的夹取/最小尺寸/
678
788
  `clipped` 语义、真机上 dock 没生效那次发现的三个缺陷(残留的 `rail` 变量、右栏开合不触发重算、