# Changelog 本文件记录 `dsh-enhanced-prompt-editor` 的每一个对外变化。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。 ## [Unreleased] ### 新增 - **「原生输入框增强」两个开关:不用切模式就能用 deck 与高级回车** (用户 2026-08 要求「在设置加一项勾选设置:是否在原生输入框应用悬浮deck。 是否在原生输入框启动高级enter设置」,并要求复用现有实现)。 设置卡片新增一节,两个 `Switch`: | 开关 | 作用 | 默认 | |---|---|---| | 在原生输入框应用悬浮 deck | 「原生」模式下也悬浮显示同一条符号/缩进工具条 | **关** | | 在原生输入框启用高级回车 | 「原生」模式下也按「回车策略」接管裸回车(如回车换行) | **关** | 两者默认**关**:不勾选时原生输入框与之前完全一样。 实现是**复用**而不是重写:deck 就是扩展模式那条 `ExtendedSymbolBar` (`FloatingSymbolBar` 的定位与预留机制原样复用),回车就是 `native-composer.js` 同一个捕获阶段 keydown 处理器。 有两处必须**分开判断**,这是本轮最容易写错的地方: - **deck 可见性** = 扩展模式 **或**(原生模式 且 勾了 deck) → 新增 `deckSource` 快照源(插槽 `inject` 结果被永久缓存,必须走快照源); - **行号沟槽 / 隔行底色** 仍然**只**属于扩展模式 → 另给一个 `extendedSource`,`extended-bar.js` 按它决定要不要渲染 `ComposerGutter`。 合并成一个判断会把行号带进原生模式,改变原生外观。 另外两处配套修正: - **原生模式只装 keydown,不打扩展标记**。`native-composer.js` 的 `apply()` 原来把「打标记 + 写高度变量 + 装监听」绑在一起;现在拆开: `apply(expand)` 只在扩展模式下打标记,原生模式**只**装监听。 监听与标记**分开记账**,所以「扩展模式 → 原生+高级回车」会摘掉标记、留下监听, 而整体关闭时两者都摘干净(漏摘会留下一套展开中的 CSS)。 - **deck 的预留规则改挂 deck 自己的标记**(`data-dsh-epe-deck`)。 原来挂在扩展模式的标记上,而「原生 + deck」没有那个标记 → 预留失效、 悬浮条压住上方消息(离屏实测重叠 32px)。标记与预留变量同生共死 (`positionAboveInput` 的 dispose 现在两者都清),所以 deck 不显示时规则等价于不生效。 ⚠️ **新增字段必须同时补进三处手写字段清单**:`constants.js` 的 `defaultSettings`、`config.js` 的 `resolveConfig`、`settings-face.js` 的 `normalize`。 本轮实测踩中:漏了 `settings-face.js` 那一处时,开关能渲染、能保存, 但 `settings.get()` 里根本没有这两个字段 → **功能静默失效**,而其它测试全绿。 已加一条不变量测试(`defaultSettings` 的每个字段都必须活过 `resolveConfig` 与设置面 `normalize`)并做变异验证守住这里。 验证:`173/173` 测试通过,关键守卫逐条变异验证确认会变红。 - **按 `Esc` 从模式一/模式二退回原生模式**(用户 2026-10-16 要求 「esc 退出模式一或模式二 回退到原生模式 图标状态也同步更改」)。 实现见 `src/escape-exit.js`:挂在 `document` 的**捕获阶段**(与接管回车 同一个理由——要抢在 Lexical 之前),三条约束: ① **只在输入框内生效**(全局抢 Esc 会吞掉宿主关弹窗/退全屏的用途); ② **输入框为空才退**(有草稿时放行——用户多半想取消编辑,不是换模式); ③ 只调 `controller.set('native')`,**不手工补 UI 更新**: 角标由 `hooks` 快照源驱动,走同一条状态通道自动同步 (两处状态源手工会不同步)。 ### 修复 - **模式二与模式一终于完全一致(逻辑行以「草稿文本」为唯一权威)** (用户 2026-10-17 第三次反馈同一问题,并直接给出方向 「分行依旧未使用模式一的实现 你不能照搬吗」,同时指出 「行标与光标重叠;第一行行标未显示」)。 根因:模式一(侧栏 `textarea`)与模式二(内核 contenteditable) **各写了一套行几何**,而模式一的权威一直是 `text.split('\n')`。 模式二前两版都在**从 DOM 反推逻辑行**:第一版数 `

` 的个数, 第二版数 `
` —— 但**真实换行往往只是文本节点里的换行字符** (输入区是 `white-space:pre-wrap`,换行字符直接渲染成新行,**根本不需要 `
`**),于是**一整段被当成 1 个逻辑行**:行号「1」被撑成十几行高、 后面的行号整体下移压住正文、整块同色无交替。 用户截图可量出该结论:文字区一整块同色约 **15 行**高,行号 2–7 间距 80px(= 1 行),文字起点 x≈336 而行号右边界 x≈363(**确实重叠**)。 修正后两个模式共用同一套口径: | | 模式一 | 模式二 | |---|---|---| | 逻辑行 | `text.split('\n')` | **同一公式**,草稿经 `useInput` 取 | | 视觉行数 | 隐藏 `textarea` 镜像测量 | 真 DOM 上按字符区间量 `Range.getClientRects()` | | 行号 | `flex:none` **独立一列** | 输入区 `padding-left` 让出**真实列宽** | | 底色 | 整块画在 `textarea` 的 `backgroundImage` 上 | 整块画在输入区上(`background-origin:content-box`) | 同时修掉「行标与光标重叠」:旧版用「负 margin + 等量 padding」当作 「不改变文字位置」的技巧,但行号列是**覆盖层** —— 负 margin 把文字拉回 14px,正好落在 0–26px 的行号列**下面**。现在照搬模式一的 flex 两列语义, 行号列**真正把文字挤到右边**(几何测试断言「行号列右缘 == 文字列左缘」)。 另有一处必须保留的细节:**块尾占位 `
` 不占位**。独立验证 (132 组离屏实测)证明,若把行尾 `
` 与「块结束标记」都算作换行, 字符区间会逐行漂移并命中跨块 `Range`,**80 行提示词会被高估成 150 行** (+87.5%)。这条规则被 `alignWithDraft` 的尾部修剪与块级校正**掩盖**, 行为测试锁不住,因此另加**源码级断言 + 变异验证**。 另按「块自身盒子高度」校正在内核 `p:last-child:after` 的 `--dsh-composer-hint` **伪元素**上(它不在文本流里,`Range` 数不到, 长 hint 会把最后一行挤走却测不出来)。 验证:`160/160` 测试通过;用用户截图里的同一份 12 行内容端到端复核 —— 逻辑行 12、浏览器实测 20 个视觉行、行号列右缘 38.0 == 文字列左缘 38.0、 行号 1–12 高度均为「行数 × 行高」、底色为真实 `linear-gradient`。 - **模式二把行数算成了精确的 2 倍**(用户 2026-10-16 报「实际只有六行 却显示到12 并且六行的背景色错误 自动分行也是无效」)。 根因:**Lexical 给每个段落的结尾都渲染一个 `
`**(contenteditable 里空块 必须有 `
` 才显示得出来),而浏览器对「块内**行尾**的 `
`」**不额外加 行盒** —— 它只是结束当前行。而切分逻辑遇到 `
` 就提交一段、循环结束**又提交 一个尾部空段**,于是每个段落多切出 1 段:6 行 → **12 段**。 离屏实测(行高 24px): | DOM | 浏览器真实行数 | 修复前切出的段数 | |---|---|---| | `

aaa

` | **1** | 2 | | `

aaa

` | **2** | 3 | | `


`(空段落) | **1** | 2 | | 6 个各带尾 `
` 的 `

` | **6** | **12** ← 用户报的 | 修正:切完段后**丢掉尾部空段**。修复后 7 种真实形状(含真实粘贴产物、 软换行、空行、纯 `

`)的**逻辑行数与渲染高度全部吻合** (`高度 ÷ 行高` 是浏览器给的权威答案)。 底色也随之正确(6 行 → 第 1/3/5 行着色,与模式一相位一致)。 新增一条精确复刻该形状的回归测试,并用「渲染高度」做**独立对照** (不是我复刻的算法自证)。 - **模式二的行号数目被放大了数倍**(用户 2026-10-16 反馈「还是没用 并且行标 显示错误」,截图里行号间距被撑大、空行也被编号)。 上一版改成「用 `Range.getClientRects()` 数行盒」时,取的是 **rect 的个数** —— 但 **Lexical 会把一行文字拆成多个文本节点/内联元素**,同一视觉行上会返回 多个 rect(实测一行 4 个文本节点 → **8 个 rect**,而 `distinctTops` 只有 1)。 实测 4 行内容得到 **31 个 rect**,被当成 31 行。 现改为**按 rect 的 `top` 去重**(量化到 1px)数不同的视觉行: 同一份内容从 31 修正为 **4**,与「段高 96px ÷ 行高 24px = 4 行」吻合。 ⚠️ 旧测试没抓到这个 bug,因为它的 fixture 每行是**单个文本节点**(每行恰好 1 个 rect)——正是不会触发该 bug 的那种结构。现新增 `fragmented` 变体 (每行拆成多个内联 ``),并加一条对照断言 (`rawRectCount > 行数 × 2`)防止该回归测试变成空转。 - **模式二粘贴长提示词时没有行号、也没有交替底色**(用户 2026-10-16 报 「粘贴这么长的提示词 模式一会自动换行 模式二不会 不统一」,随后澄清 「我说的换行是有行标数 背景颜色有变化的那种 不是行内换行」)。 根因是**对内核行为的想当然**:原实现用 CSS counter 数 `[data-composer-input] > p`、用 `nth-of-type(odd)` 画斑马纹。但内核的粘贴通道是 `mt(t,e){ e.insertRawText(t.getData("text/plain")) }`,而 `insertRawText(t){ this.insertNodes(qr(t)) }`、`qr` 把 `\n` 交给 `linebreak: () => push(qi())`,`qi()` 是 `LineBreakNode`,其 `createDOM()` 返回 `createElement("br")` —— **换行是 `
`,不是新的 `

`**。 粘一份 80 行提示词的真实产物是「**1 个 `

` + 79 个 `
`**」 (离屏实测 `pCount=1, brCount=7, textLines=8, lineNumbers=1`), 于是行号恒为 1、斑马纹一片不交替。 现改为**实测渲染出来的行盒**:`src/composer-rows.js` 用 `Range.getClientRects()` 数行盒、`
` 各算一行;行号由 `src/composer-gutter.js` 渲染成**绝对定位的 gutter**(`createPortal` 到内核 `.grow`,不改内核 DOM),底色写成内核 `

` 的 inline `background-image`。 与模式一(textarea)语义统一:一个 `\n` 一个逻辑行,软换行的续行同色。 - **模式二的行号变量在两个作用域都没定义**(同一轮踩到的第二个静默坑)。 `--dsh-epe-line` / `--dsh-epe-stripe` 原先只在 `panel.js` 的 CSS 里定义 (作用域是侧栏面板)。模式二里未定义时 `var()` 会让**整条 `background-image` 被判无效并丢弃**,浏览器不报错,实测 `getComputedStyle(p).backgroundImage === "none"` —— 条纹一条都不显示。 现已在模式二自己的作用域里定义这两个变量(与 E-016 同族)。 ### 变更 - **模式二的行号沟槽由 14px 加宽到 22px**(用户 2026-10-16「14px 沟槽不够的话 适度加宽」),两位数行号不再紧贴文字。 ⚠️ 加宽同时把输入区的 `padding-left` 改成 22px,并用 `margin-left:-22px` + `padding-left:14px` 把文字**推回内核原本的 14px 起点** —— 三个数字不是同一个值,写成同一个会把文字右移 8px(几何测试会红)。 占位提示(`[data-composer-placeholder]` 的 `inset` 左值)也同步到 22px。 - **设置卡片注册到了错误的分组**(2026-10-16 按内核源码更正)。 原来注册在 `plugins.item`,那属于插件页的**「内置插件配置」**分组(官方插件用 `ItemCard` → `ItemDetail`);第三方插件走的是 `mine = listed.filter(pkg => !pkg.official)` → `renderGroup("bundles", "已安装插件", …)` → `PackageCard` → `PackageDetail` → `renderSlot("plugins.bundle.config", { view: "page" }, { entryKey: pkg.name })`。 已改为 **`plugins.bundle.config`**(keyed 槽,`key` 必须等于 `package.json` 的 `name`; 内核门控是 `configured: ledger.bundles.has(openPkg.name)`,key 写错会**静默不渲染**)。 用户路径:侧栏「插件」→「已安装插件」→ 本插件 → 在其中配置。 ⚠️ 此前 `UI-STANDARDS.md` 与 skill `dsh-plugin-development` 都把 `plugins.item` 写为第三方入口,是**过时建议**,已一并更正;以 app.asar 里的内核源码为准。 - **编辑区的交替行底色完全不显示**(用户 2026-10-16 报「行数标和行背景也没有」)。 根因是 CSS 写法错误:渐变的**色标位置写成了裸的 `--dsh-epe-stripe`,漏了 `var()`**。色标只接受 `` / ``,一个自定义属性名 两者都不是 → **整条 `background-image` 被判无效并回落到 `none`** → 一条条纹 都没有,而控制台不报任何错。改成 `var(--dsh-epe-stripe)` 即恢复。 已在测试里按元素分别断言(滚动容器/编辑区/渐变色标),并补了针对性变异验证。 - **符号按钮的悬浮提示显示裸 key**(用户 2026-10-16 报「指针悬浮显示的不是简单 易懂的中文名」)。原实现读的是 `symbol..hint`,那个 key **在字典里不存在**, 于是显示 `symbol.paren.hint`。改为「中文名 + 符号」(如 `圆括号 ()`), 并加测试断言悬浮提示里不得出现 `symbol.` / `panel.` 前缀。 - **后面几个符号按钮辨识度差**(用户 2026-10-16 问「后面几个是不是有问题」)。 确实是:`~~`(删除线)在小字号下和 `~` 看不出区别,`**` 与 `*` 也几乎一样。 改为字母标签:`B` 加粗 / `I` 斜体 / `S` 删除线 / `H` 标题 / `•` 无序列表 / `❝` 引用 / `Link` / `Img` / `―――` 分隔线。 ### 新增 - **行号沟槽**(用户要求):编辑区左侧显示行号,与编辑区共享同一个行高变量, 滚动时按 `scrollTop` 同步偏移。 - **一键缩进 / 反缩进**(用户要求):`→` 给选中行各加两个空格,`←` 去掉一层 缩进(只削真的存在的那部分,不顶掉内容)。缩进用两个空格而不是 Tab —— Markdown 里两空格是惯用缩进,且不会在别的渲染器里变成 8 列宽跳格。 - **字号与按钮大小设置**(用户要求「字号 图标没有大小设置 我觉得有点小」): 新增「编辑区字号」(11–28px,默认 16)与「快捷键按钮大小」(80–180%,默认 100) 两项。字号走 CSS 变量 `--dsh-epe-font`,条纹的行高 `--dsh-epe-line` 自动跟随, 不必两处同步。 - **符号行同时加到模式二**(用户要求):扩展输入框模式下也会显示同一排符号。 两个模式**共用同一份符号表与行为规则**(`src/edit-ops.js`),不会各修一份。 写入走内核的**官方程序化插入通道**(`inputActions.captureInsertion()` + `insertText(text, span)`,坐标是全文坐标、自带提交机防护),而不是直接改 Lexical 内部节点——后者的 `anchor.offset` 是节点内偏移,跨段落会算错。 - **编辑区逐行条纹**:每行背景深浅交替,便于逐行扫读(用户 2026-10-16 要求)。 实现用 `repeating-linear-gradient` **画在 textarea 上**,而不是把 textarea 拆成 「每行一个元素」——后者会让原生光标移动、跨行选择、IME 组合输入、粘贴多行、 undo/redo 全部需要自己重写(且很难做对),而条纹只需固定行高即可精确画出。 配色用 `color-mix(... var(--dsw-alias-label-primary) 5%)`:浅色主题下变深、 深色主题下变浅,两个主题都成立且不硬编码颜色。 - **编辑区下方快捷键行**(用户 2026-10-16 要求「在发送按钮行的空白放一行快捷按键」): - **成对符号**:`()` `[]` `{}` `""` `''` `` ` `` `()` `「」` `『』` `【】` `《》` - **Markdown 常用**:`**` `*` `~~` ` ``` ` `#` `-` `>` `[]()` `![]()` `---` - **粘贴按钮**:在光标处粘贴剪贴板内容(替换选区) - **选中文字后点成对符号 = 把文字框在符号内**(用户明确要求,**不是**把框选的 文字改成符号)。有选区时 `abc → (abc)` 且内层文字**保持选中**(方便继续叠加, 例如再点 `**` 得到 `(**abc**)`);无选区时插入一对、光标落在中间 `(|)`。 ### 修复 - **深色主题下发送按钮显示为纯白,悬浮时变成黑底白字**(用户 2026-10-16 报)。 两个叠加的根因: 1. 底色用了 `--dsw-alias-brand-primary`,它在深色主题下解析成**近白色**, 而文字写死 `#fff` → 白底白字。内核自己的发送按钮(`InputBar` 的 `.primary`) 用的是 `--dsw-alias-button-info-fill` / `--dsw-alias-button-info-hover`, 已改用同款 token; 2. 通用悬浮规则 `.dsh-epe-btn:hover:not(:disabled)` 特异性 **(0,3,0)** 高过 主按钮规则 **(0,2,0)**,于是悬浮时背景被换成中性灰、文字仍是白色 → 黑底白字。 现在通用规则用 `:where(:hover)` 把特异性压到 (0,2,0),主按钮自己的 hover 规则靠更高特异性稳定胜出,**不需要 `!important`**。 - **扩展输入框下方一大片区域点不动**(用户 2026-10-16 报:「只在最顶上点击有用 输入框下方一大片区域无法点击输入文字」)。**这是上一版修复引入的**: 我把 `min-height` 加在了滚动容器 `[data-input-scroll]` 上,于是容器被撑到 640px, 而里面真正可编辑的 contenteditable(`[data-composer-input]`)**仍然只有 36px** (内核 `.input{min-height:36px}`)——中间 604px 里既没有可聚焦元素, 内核也**故意不管**: `onCardMouseDown` 里有一句 `if (scrollRef.current?.contains(target)) return;` (它假定滚动区里都是编辑区)。宿主不处理、又没有可点控件 → 死区。 修法:`min-height` 改加在**编辑区**上,编辑区自己就有那么高,容器随之同高,死区为零; `max-height` 仍留在滚动容器上(决定内容超长后从哪里开始滚动)。 - **离开编辑器模式时,右侧栏那一列没被收起来**(用户 2026-10-16 报:「关闭了 但侧栏没有关闭」)。 根因:**关 tab ≠ 收列**。内核《默认页与关闭规则》原文——停靠面的最后一个 tab 有一条规则, **仅当**它是「唯一停靠 tab」时,关闭才会连同整列一起收起;而停靠面**永远不为空** (内核会自动填充引导页),所以本插件的编辑器 tab 几乎从不唯一 → `close(tabId)` 只关掉 tab,右侧栏那一列照样开着。 修法:关掉自己的 tab 后,若**这一列是「进入编辑器模式时才由我们展开的」**, 再调 `toggleExpanded()` 把它收回去。用户本来就开着侧栏时不擅自收起(用 `isExpanded()` 在进入前记录一次)。 另外把 `screen()` 换成公开的 `mounted` 快照(README《跨插件面》明确列出), 没有会话在屏幕上时安静跳过——「还没渲染」不是失败,不该污染启动报告。 - **切到「扩展输入框模式」时,不再只设高度上限**(用户 2026-10-16 报:切过去看不到展开)。 内核的输入框高度是 `.scroll{max-height:var(--dsh-composer-text-max-height)}`——那是 **上限**而非高度,空输入框仍然只有 36px(`.input{min-height:36px}`),所以旧实现要 等内容长到撑满上限才看得出变化。现在同时给滚动区一个 `min-height`, **挂上标记的那一刻就真的撑开**。 - 覆盖方式也换了:旧实现把 `--dsh-composer-text-max-height` 写在 **seat 的内联样式**上, 而 seat 会在会话切换时被宿主**重新挂载**,内联样式随之丢失 —— 扩展会悄悄失效。 现在标记挂在 `` 上,规则用属性选择器命中「当前挂载的那个」输入框, 宿主重挂后自动继续生效。 - 直写 `min-height`/`max-height` 到 `[data-input-scroll]`,特异性 (0,2,1) 胜过内核 `.scroll` 的 (0,1,0),**不需要 `!important`**。 - **切换模式时自动收掉上一个模式的界面**(用户 2026-10-16 报:切到模式二没有关掉模式一的编辑器)。 此前只处理了「进入编辑器模式 → 打开面板」,没有处理「离开 → 关闭面板」, 于是切到扩展输入框后右侧栏还挂着编辑器,两个增强同时占着地方。 现在订阅模式变化(`modes.subscribe`),**任何来源**的模式变化都走同一条副作用路径: T 的循环、以及用户在设置卡片里关掉某个模式时的收敛。 - **T 按钮的角标会停在第一帧**(严重,静默失效)。插槽渲染器把 `entry.inject()` 的返回值 按 `(entry, binding)` **缓存进 WeakMap 且永不失效**(内核 `cachedSessionInject`), 所以 inject 里给 `mode: <当前值>` 会**永久冻住**——点 T 切了模式,角标一直显示旧值。 改为官方 `dsh-client-ui-settings-subagent` 同款写法:inject 返回 `hooks` face 提供**快照源**,渲染器把它绑成组件 prop(`useMode()`),组件在渲染期调用。 ⚠️ 这与本插件 `speechStore` 那处的坑是同一形状(见下方"已知陷阱")。 ### 新增 - **三种工作模式**。T 按钮不再只有「打开面板」一个动作,而是在模式间循环: - **编辑器模式**:点 T 打开右侧栏 Markdown 面板(原有行为); - **扩展输入框模式**:把 DSH **原生输入框**向上加高,并单独决定裸回车的语义; - **原生**:两个增强都不生效(循环的最后一站,不可勾选)。 两个增强模式可以在插件页里独立勾选,并决定谁排在前面;两个都关掉时 T 变暗、 不做任何事。模式记在 `localStorage`(`dsh.enhancedPromptEditor.mode`),跨会话保留。 - **扩展输入框的「高级 Enter 设置」**:`跟随 DSH`(Enter 发送,默认)或 `回车换行`(Enter 换行、`Cmd/Ctrl+Enter` 才发送)。 - 实现:在 `document` 上以**捕获阶段**监听 `keydown`,早于 Lexical 挂在 contenteditable 根上的冒泡监听器;需要换行时 `preventDefault()` + `stopImmediatePropagation()` 把按键彻底吞掉,再经 `editor.update()` 取 `_selection` 调 `RangeSelection.insertParagraph()` 插入段落。 ⚠️ 不能用 `insertText(' ')`:那走 `spliceText`,把字面 ` ` 塞进文本节点, 渲染时并不产生换行。 - **拿不到编辑器时不吞这次回车**(只降级 + 记一条记录)——吃掉用户的回车却 什么都不插会让输入框彻底没法用。 - **切换提醒**:切到扩展输入框模式时弹一条 `Toast` 横幅(顶部居中,6 秒), 说清「原生输入框会加高、回车按你的设置、再点一次 T 回到原生」。 - **插件详情页配置卡片**(`plugins.item`),用 primitives 的 `SettingsForm` + `SettingsFormModel`:暂存草稿、**只有点保存才写入**、非法草稿阻止保存。 - **构建期产物守卫** `assertBundleShape()`:① `new Function` 解析产物; ② 外部模块请求只允许前端 seed 表那 9 个。 - **启动报告新增运行期降级记录**:扩展输入框控制器拿不到编辑器/插入被拒时, 同样写进 `localStorage` 并显示在配置卡片顶部。 ### 变更 - **UI 组件改为全部来自 `@deepseek-ai/dsh-client-ui-primitives`**(`Button`、 `Switch`、`Menu`、`SettingsForm`、图标…),不再自写按钮/下拉/SVG。 用户 2026-10-15 裁定:它是前端 boot seed 表里的冻结基座模块,官方 10 个配置页 里 9 个都 require 它;此前「禁止 require 任何 Harness Client 包」的结论是把 白屏事故归错了因(真因始终是漏声明 `inject` 服务名)。 - **设置从 `settings.section` 迁到 `plugins.item`**。现行规范是「注册了配置页的 插件在插件页里编辑;设置里只保留只读的插件列表」。 - `package.json` 的 `dsh.client` 增加 **`immediately: true`**:第三方插件的客户端 bundle 是惰性加载的,与 entry 创建**并发**,偶尔赶不上会让 web boot 报 `<包名>: import failed` → 整站白屏(而插件自己的启动报告可能还是 `ok: true`)。 该字段让 web boot 在建 entry 之前 `await` 预取完毕。**属 host 侧 manifest 字段,改动要重启应用。** - `dsh.client.inject` 补上 `@deepseek-ai/dsh-client-ui-primitives` 与 `@deepseek-ai/dsh-client-ui-plugin-manager`(后者提供 `plugins.item` 插槽)。 ### 修复 - **去掉了 T 按钮的悬停提示框**。用户 2026-10-16:「指针在 T 上悬浮会显示一个你的 窗口 是我们的中文名字 去掉」。此前用的是原生 `title` 属性,浏览器会弹一个显示 插件中文名的系统气泡;现在模式由角标表达,可访问名称走 `aria-label`(不产生视觉浮层)。 - **配置卡片在宿主 schema 尚未重载时谎报开关状态**。`scope.value` 只含 Host 真的 服务出来的字段;刚装完插件还没重启时新模式字段整批缺失,而 `value === true ? 'true' : 'false'` 会把「默认为开」显示成「关」。 现在所有 spec 的 `format` 在字段缺失/非法时退回 `defaultSettings()` 的值。 ### 修复 - **插件不再可能拖垮 DSH 启动**(严重)。此前 `apply` 里调用 `ctx.sidebarRightTabs` 时抛 `cannot get property "sidebarRightTabs" without inject`:该服务由内核 `@deepseek-ai/dsh-client-ui-sidebar-right` 提供,`package.json` 的 `dsh.client.inject` 里列了它(模块能加载),但插件自己导出的 `inject` 里没写这个**服务名**——cordis 的 `ctx.` 代理只放行 `inject` 里声明过的服务,读它同步抛错。异常冒到 `apply` 外 把 cordis fiber 打成 `FAILED`,DSH web boot 随即 `throw new Error("web boot: 1 entry did not activate")`,**整个界面白屏**,而 crash log 只有一行 `dsh-enhanced-prompt-editor: failed`,没有任何堆栈。 - `inject` 补全为 `['slots', 'locale', 'sidebarRightTabs', 'sidebarRight']`; - `dsh.client.inject` 补上 `@deepseek-ai/dsh-client-ui-slots` 与 `@deepseek-ai/dsh-client-locale`,保证这两个服务的提供方先就位; - Host 侧同样把 `ctx.webServer` 改成 `ctx.get('webServer')`,不留同类隐患。 ### 新增 - **启动自检与降级**(`src/boot-diagnostics.js`):`apply` 的每一步各自包一层守卫, **任何一步失败都只降级、只记录,绝不向外抛异常**——一个插件的接线错误不该让用户白屏。 - `console.error` 一份;写进 `localStorage`(渲染进程里唯一事后能从磁盘读回的地方); - 设置页顶部显示「启动降级」提示,逐条列出出错步骤与消息; - 读取工具:`~/.dsh/bin/dsh-boot-report`。 - **静态守卫测试**:扫描客户端源码里所有 `ctx.` 服务访问(剔除注释与字符串, 排除 Context 自带成员),断言每一个都出现在导出的 `inject` 里,并反向断言 `inject` 没有无用项。本机实测:删掉 `sidebarRightTabs` 该守卫立即变红。 - 启动健壮性测试:全服务抛错的 ctx、空 ctx、单面失败、`slots.inject` 全挂、 `locale` 挂掉走本地兜底文案等场景下,`apply` 均不得外抛,且其余表面照常注册。 ## [0.1.0] — 2026-10-15 首个可用版本。 ### 新增 - **输入框工具行按钮**:注册进 `conversation.input.left`,28px 圆钮,主体为字形 `T`,右下角叠一枚放大镜角标表示「展开成编辑面板」。 - **右侧栏编辑面板**:注册自定义 tab 类型 `enhanced-prompt-editor`,整块区域为编辑区,可与对话并排查看。每个会话一个面板。 - **三种草稿存储**,设置页随时可切: - `workspace` —— 写成工作区里的 `.md` 文件,可多份、可被 DSH 内置文件阅读器打开; - `browser` —— 浏览器 localStorage 单份草稿; - `none` —— 不持久化。 - **发送**:面板右下角按钮,经 `inputActions.setDraft` + `submit` 直接进会话,不绕开 DSH 的提交机。 - **语音听写**:复用 DSH 语音转文字插件的 `remote.speech`,自行采集并编成 16 kHz 单声道 PCM16 WAV。未安装语音插件时按钮不渲染。 - **回车语义可配**:默认回车换行、`Cmd/Ctrl+Enter` 发送;可在设置里反转。`Shift+Enter` 恒为换行,输入法组合期间的回车不拦截。 - **设置页**:注册 `settings.section`,含存储方式、落盘目录、回车行为、面板高度、阅读器开关、听写开关。 ### 安全 - 落盘走本插件 Host 侧的 loopback-only 同源路由(DSH 自带的 `workspace-files` Remote 只有读能力)。 - 客户端传来的目录与文件名一律不可信:Host 侧解析后断言最终路径仍在会话工作区内,越界即拒绝。 - 跨站 `Origin` 被拒;单次请求体上限 1 MiB。 - 文件名中的路径分隔符降级为 `draft.md`,而不是报错。 ### 稳健性 - **样式注入不再可能抛错**。DSH 的插槽渲染器给每个条目套 `SlotErrorBoundary`, 渲染期一旦抛错会调 `reportEntryError(..., { abdicate: true })`,条目进 `abdicated` 集后**从此不再渲染**。原实现直接操作 `document.head`,在 head 被 改写/冻结等环境里会让按钮与面板永久消失(症状与「插槽被别的插件挤掉」相同, 极易误判)。现集中在 `src/css.js`:把 `document` 当不可信输入,任何异常都 返回 `false` 而不外抛。 - 新增 6 项测试钉住该行为:坏文档下的纯函数容错、三个表面在 `document` 抛错时 仍能渲染、渲染组件不得直连 `document.head`、插槽 id 不得撞内核与已装插件。 ### 备注 - 客户端 bundle 由本仓库自带的 `src/build-client.mjs` 生成。DSH 浏览器侧只消费单个 `./client` bundle,而本项目不引入构建工具链,因此用约 200 行实现了一个只支持窄语法子集的打包器;遇到不支持的写法会直接报错。 [0.1.0]: https://example.invalid/dsh-enhanced-prompt-editor/releases/tag/v0.1.0