dsh-composer-ux 0.3.0 → 0.4.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.4.0] — 2026-09-15
6
+
7
+ > 尚未发布:先在用户机器上实测,通过后再发 GitHub / npm。
8
+
9
+ ### 新增
10
+
11
+ - **跨分类移动条目**:条目列表最下边多了一行「+ 新建 / 移动」。点开分两个动作 ——「+ 新建一条」与「→ 把别的分类的条目移动到这里」;后者列出其它分类的条目(`分类名 · 条目名`),点它即移入当前分类、**追加到末尾**,`id` / 正文 / 插入模式都跟着走。设置页与面板共用同一个组件(`AddPromptRow.tsx`),行为不会分叉。
12
+ - 移走最后一条时源分类**留空但不删除**(空分类是合法状态);目标分类不存在、就是源分类、或已满时**原样返回**(不写盘)。
13
+ - **插入模式改为三选一:关 / 每次 / 仅首次**(原来是单个「默认插入」勾选框)。
14
+ - 「仅首次」只在**这个会话的第一条消息**上附加。判据是官方会话快照里的 `SessionSnapshot.blank`(这个会话还没有任何消息),**不是**插件自己记的状态——发完第一条它自动失效,刷新页面、切走再切回来都不会重复附加。
15
+ - 用三选一而不是两个勾选框:「每次」与「仅首次」互斥,两个独立勾选框能造出「又每次又仅首次」的矛盾状态。界面只发出一个 mode,由 `withInsertMode` **一次把两个标志写对**。
16
+ - 文件里「仅首次」写成扩展键 `autoSendFirst: true`(只在为真时出现),`autoSend` 语义不变——与参考实现的互操作性因此保持;代价是对方重新保存时会丢掉这个键(「仅首次」在那边退化成「关」)。手工把两个键都写成 `true` 时按「每次」处理(每次插入本来就包含第一次),下次写盘修正回互斥。
17
+ - 「+ 新建」出来的条目正文是占位「(待填写)」而不是空串:空正文会被净化丢掉,而面板里的新建是**立即写盘**的,给空串会当场消失、看起来像坏掉了。设置页保存时也会先剔掉正文为空的条目**并在状态行说明**「已跳过 N 条」。
18
+ - **「右键菜单」从布尔开关改为三选一:官方 / 浏览器 / 自定义**(原来是「使用系统原生菜单」)。
19
+ - **官方**(新默认档):本插件**完全不介入**——既不 `preventDefault` 也不 `stopImmediatePropagation`,DSH 与其它插件自己的右键处理原样生效(DSH 官方输入框本身没有右键菜单,所以通常看到的就是浏览器的菜单)。
20
+ - **浏览器**:固定用浏览器自带的菜单,并在本插件这一层**挡住其它插件的菜单**(这是它与「官方」的唯一差别);粘贴免授权、零配置。
21
+ - **自定义**:插件自己的固定样式菜单(7 项可逐项开关,只在这一档显示那些开关);`preventDefault` 掉浏览器菜单,并和「浏览器」档一样**挡住其它插件的菜单**(0.2.0~0.4.0 的自定义档只 `preventDefault + stopPropagation`,同一层里别的插件的捕获监听仍会收到事件,可能弹出两个菜单;本次补齐,由用户拍板)。
22
+ - **迁移**:旧布尔 `menuNative` 仍然认——`true → 浏览器`、`false`(明确关过那个开关)`→ 自定义`;两个键都没有(从没设过)→ 新默认档「官方不介入」。**这是一处行为变化**:以前「没设过」等于自定义菜单,现在没设过等于不介入。旧字段只作迁移线索,0.4.0 起不再写它;「恢复默认」会把两个键一起清掉。
23
+ - 为此把宿主 schema 里的旧布尔改成**可选**(schemastery 的 `.required(false)`,且不给默认值):否则「文档里没有这个键」与「明确设成了 false」在解析之后长得一模一样,迁移就分不出该落哪一档。
24
+ - 控件与「插入模式」共用同一个胶囊组件(`PillChoice.tsx`),样式与无障碍行为不会各写一遍而分叉。
25
+
26
+ ### 开发中发现并修掉的问题
27
+
28
+ 1. **「仅首次」在写回编辑器那一步被二次过滤掉了**(用户实测发现:新会话第一条只发「每次」的,不发「仅首次」的)。
29
+ - 成因:批次算得没错——`appendBatchForSend(book, blank)` 在空会话里正确交出了「每次 + 仅首次」;但紧接着写回的函数(当时叫 `withAlwaysPrompts`)内部**又按 `always === true` 过滤了一遍**,于是「仅首次」的条目全被丢掉;**若一条「每次」都没有,`picked.length === 0` 直接返回 null,第一条消息里什么都不附**。
30
+ - ✅ 正解:写回这一步只负责「把给它的这一批拼上去」,模式判断只留一处(`settings-contract.ts` 的 `insertModeOf`)。同时把这处过滤的载体 `alwaysPrompts()`(按原始标志过滤的辅助函数)**整个删掉**——留着它就会有人再拼一遍同样的错。函数更名成 `withPromptsAppended` / `applyPromptsForSend`,名字如实反映「批次已定,这里只写回」。
31
+ 2. **我原来那条「接缝护栏」是字符串匹配,所以这个 bug 全绿通过。** 它只断言 `client.tsx` 里 `promptsForSend` 接的是 `appendBatchForSend(book, currentBlankSession())` —— 也就是只验「批次算得对」,完全没验「写进去对不对」。
32
+ - ✅ 正解:改成**执行整条路径**的护栏——用 `publishInputBridge` 装一个假编辑器,再调**拦截器真正调用的** `applyPromptsForSend(appendBatchForSend(book, blank))`,断言**最终写回编辑器的整段文本**;另加一条语义护栏:写回路径里不得再出现 `always === true` / `firstOnly === true`。
33
+ 3. **新护栏被我自己写的说明注释弄红了。** 我在 `quick-commands.ts` 里解释这个 bug 时写了 `always === true` 的字面量,护栏匹配到注释里的散文就报红——和上一轮「护栏命中面板里过时的注释」是同一类错误。
34
+ - ✅ 正解:护栏匹配前先剥注释(`codeOnly()`)。注释里必须能自由记录踩过的坑。
35
+ 4. **设置页「快捷指令」卡片里三处 `**加粗**` 是 Markdown 字面量**,在 React 里就渲染成三个星号(`**发送时怎么附加**`)。这是 0.4.0 写文案时留下的,没人报过但一眼可见。
36
+ - ✅ 正解:改成 `<strong>`。**这类问题只有看渲染结果才发现**——文案改动要顺手在真界面里看一眼。
37
+ 5. **Firefox 的粘贴说明一直是错的**(用户照做之后实测发现)。我们教用户「把 `about:config` 里的 `permissions.default.clipboard-read` 设为 `1` 就不弹授权窗」——用户设完之后,「粘贴(P)」小窗照旧出现。
38
+ - 查证结论:那个小窗是 Firefox 的**固定安全机制**——网页读剪贴板时它弹一个只有「Paste」一项的临时菜单(约 1 秒后才可点),与那个首选项无关;能免掉它的只有 `dom.events.testing.asyncClipboard` 这类**测试用**开关,那等于允许任何网站静默读剪贴板([MDN](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#security_considerations)、[caniuse](https://caniuse.com/mdn-api_clipboard_readtext):只有带 `clipboardRead` 权限的扩展不受此限)。
39
+ - ✅ 正解:界面、README 与运行时提示统一改成准确说法——「Firefox 一定会弹那个小窗,插件关不掉;不想多这一步就用 Ctrl+V,或把「菜单来源」换成「浏览器 / 官方」档」。并加一条护栏:界面与运行时提示里**不许再出现**那个首选项名(变异测试:写回去立刻变红)。README 另留一句「早先写法是错的」的更正,避免有人从旧文档学到错的。
40
+ - 教训:**「用户照做之后没用」就是最硬的证据**。这条错误能存活好几个版本,是因为它只写在文案与提示里、没有任何测试盯着它是否成立。
41
+
42
+ ### 测试
43
+
44
+ - 全量 **304 条 0 失败**(`npm test`:35 + 106 + 125 + 38)。
45
+ - 新增覆盖:插入模式三态与「两个标志都为真 → 按每次」的优先级、切模式时互斥标志被清掉、`autoSendFirst` 的文件往返与旧文件(只有 `autoSend`)的向后兼容、发送批次在 `blank=false` 时**不带**「仅首次」、**端到端写回**(两批都在 / 第二条只剩「每次」/ 只有「仅首次」时第一条仍附加 / 原文为空时不写)、跨分类移动的语义与四种 no-op、「会话是否为空」取自槽位快照的接缝、两处共用同一控件。
46
+ - 右键三档新增两层测试:**纯函数**层钉住迁移的每种情形(新字段优先、旧 `true → 浏览器`、旧 `false → 自定义`、都没有 → 官方、脏数据落回旧布尔、元数据齐全);**行为**层扩展客户端测试桩(让 `addEventListener` 保留处理函数、补齐 `Element` 与 `getSelection`),真的派发一次 `contextmenu`,断言官方档三件事都不做、浏览器档只 `stopImmediatePropagation` 不 `preventDefault`、自定义档 `preventDefault` + 挡住同层监听。**这个行为测试当场抓出一处既有行为**:0.4.0 及以前的自定义档其实不挡同层其它捕获监听(可能弹出两个菜单),已按用户拍板补齐,README 同步改写。
47
+ - 变异测试:写回路径的 3 处变异(二次过滤 / `blank` 失效 / 永远不带「仅首次」)与设置页警告的 3 处变异(删掉 / 样式降级 / 改写措辞)全部被拦住并变红。
48
+
5
49
  ## [0.3.0] — 2026-09-15
6
50
 
7
51
  > 尚未发布:先在用户机器上实测,通过后再发 GitHub / npm。
package/README.md CHANGED
@@ -15,8 +15,8 @@ DeepSeek Harness Web 输入体验增强插件:
15
15
  1. **设置 → 输入体验**(设置页新增条目)
16
16
  - 版式对齐社区插件 `@linxin666/dsh-web-all` 的「Web 插件」页:顶部常显「中文名 + 内嵌英文包名 `dsh-composer-ux` 的一行描述 + 总开关」;其下五个栏目为**可折叠卡片**(标题行只放标题 + 一句动态概览,长说明与控件都在展开后的内容区),**默认全部折叠、不记忆展开状态**,可同时展开多个。
17
17
  - **键位**:分别配置「发送键」「换行键」——常用预设(Enter / Ctrl+Enter / Alt+Enter / Shift+Enter)+ 点击「自定义…」后直接按任意组合键录制(Esc 取消,Backspace 清除),支持清空为「无」;发送与换行不能设为相同按键;可一键恢复默认。
18
- - **右键菜单**:输入框右键菜单的 7 个条目(撤销 / 重做 / 剪切 / 复制 / 粘贴 / 删除 / 全选)可单独开关;可切换「使用系统原生菜单」(浏览器自带菜单,粘贴免授权)。
19
- - **快捷指令**:清单增删改 / 上下移 / 「默认插入」勾选 / 优化强度三档(详见下节)。
18
+ - **右键菜单**:三档「菜单来源」(官方不介入 / 浏览器菜单 / 自定义菜单,见下「右键菜单」一节);自定义档那 7 个条目(撤销 / 重做 / 剪切 / 复制 / 粘贴 / 删除 / 全选)可单独开关。
19
+ - **快捷指令**:分类增删改 / 条目增删改与上下移 / 跨分类移动 / 每条一个插入模式(关 · 每次 · 仅首次)/ 优化强度三档(详见下节)。
20
20
  - **设置面板**:导航可滚动开关;边缘拖拽调整面板大小(尺寸记忆持久化)与尺寸预设。
21
21
  - **OpenCode 请求头**:给 OpenCode 的模型请求自动附加 `x-opencode-session`(详见下节)。
22
22
  2. **快捷指令按钮**:输入框工具行里、「展开」按钮左侧的胶囊按钮,点开是常备提示词清单 + 「优化提示词」。
@@ -69,26 +69,49 @@ dsh plugin --profile web add <你克隆或解压出来的目录>
69
69
 
70
70
  ## 右键菜单
71
71
 
72
- - 样式与系统编辑器菜单一致(深色圆角、三分组、快捷键右对齐);未选中文本时「剪切 / 复制 / 删除」置灰;被关闭的条目不显示(分隔线自动合并)。
73
- - 撤销 / 重做 / 剪切 / 复制 / 删除 / 全选 走标准编辑命令;**粘贴 读取剪贴板后在光标处插入纯文本**——这是浏览器的安全限制:首次使用可能出现一次授权提示,被拒绝或无响应时会提示「请用 Ctrl+V 粘贴」。
72
+ 设置 → 输入体验 → 右键菜单 里是三选一的**菜单来源**(只影响右键,不影响键位与其它功能):
73
+
74
+ - **官方**(默认):本插件完全不介入,DSH 与其它插件自己的右键处理原样生效。DSH 官方输入框本身没有右键菜单,所以通常看到的就是浏览器的菜单。
75
+ - **浏览器**:固定使用浏览器自带的菜单(样子随浏览器而变),并在本插件这一层**挡住其它插件的菜单**;好处是**粘贴免授权、零配置**。
76
+ - **自定义**:使用本插件固定样式的菜单(未选中文本时「剪切 / 复制 / 删除」置灰)。这一档的「粘贴」要读剪贴板,浏览器会先要一次授权,三家处理不同:
77
+
78
+ | 浏览器 | 首次/之后 | 改或撤销授权 | 免掉弹窗的办法 |
79
+ | --- | --- | --- | --- |
80
+ | Chrome / Edge | 弹出后点「允许」即记住这个站点,之后不再问 | 地址栏最左的网站图标 → 网站设置(Edge 叫「此站点的权限」)→ 剪贴板;或 `chrome://settings/content/clipboard`(Edge 是 `edge://settings/content/clipboard`) | 不需要:允许一次即可 |
81
+ | Firefox | 每次点「粘贴」都会弹一个只有「粘贴(P)」一项的小窗(约 1 秒后才可点),点它才完成 | 无(没有站点授权面板) | **关不掉**:这是 Firefox 的安全机制,网页不允许静默读剪贴板。不想多这一步就按 Ctrl+V,或把「菜单来源」切成「浏览器 / 官方」档 |
82
+
83
+ > 0.4.0 实测更正:早先文档里教过「改 `about:config` 里某个剪贴板首选项就不弹窗」——**那是错的**,那个弹窗与任何首选项都无关(用户在 Firefox 上照做后弹窗照旧)。依据:[MDN Clipboard API 的安全说明](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#security_considerations)(读到不允许的内容时,浏览器弹的临时菜单里只有一个 Paste 项、约 1 秒后才可点)、[caniuse](https://caniuse.com/mdn-api_clipboard_readtext)(只有带 `clipboardRead` 权限的扩展不显示粘贴提示)。能免掉它的设置都属于「允许任何网站静默读剪贴板」那一类,本插件不教、也不建议改。
84
+ >
85
+ > `chrome://` / `edge://` 这些地址**不能做成网页里的链接**(浏览器禁止页面跳转到内部协议),只能手输或复制粘贴到地址栏。被拒绝授权或无响应时,菜单里会提示「请用 Ctrl+V 粘贴」。
86
+
87
+ 自定义档的细节:
88
+
89
+ - 样式与系统编辑器菜单一致(深色圆角、三分组、快捷键右对齐);被关闭的条目不显示(分隔线自动合并);只在这档下才显示那 7 个条目开关与「全部开启」。
90
+ - 撤销 / 重做 / 剪切 / 复制 / 删除 / 全选 走标准编辑命令;**粘贴 读取剪贴板后在光标处插入纯文本**——这是浏览器的安全限制。
74
91
  - 菜单打开时点击外部、Esc、滚动或窗口变化都会关闭。
92
+ - 该档 `preventDefault` 掉浏览器菜单,并和「浏览器」档一样**挡住同一层里其它插件的捕获监听**(否则两边会各弹一个菜单)。
75
93
 
76
94
  ## 快捷指令与提示词优化
77
95
 
78
96
  输入框工具行里、「展开」按钮的左侧有一个同款胶囊按钮「快捷指令」(槽位 `conversation.input.right`,order 89 < 官方「展开」的 90)。点开展开面板:
79
97
 
80
98
  - **快捷指令清单(分类两级结构)**:点条目把内容插入输入框(原有内容保留、另起一行)。面板顶部是分类标签(点它切换,`+` 新增一个分类);分类的改名 / 排序 / 删除,以及条目的增删改、上下移、恢复内置 9 条,都在 设置 → 输入体验 → 快捷指令清单 里。
81
- - **「默认插入」**:条目右侧的勾选框。勾上后,在你**点发送时**(Enter 或官方发送按钮)勾选的提示词会自动拼到消息**末尾**一起发出,输入框里不提前显示;多条按列表顺序拼接、条目间空一行。**跨分类生效**(勾选按条目 id 找,不限于当前分类)。原文为空时不附加(交还官方原语义)。
99
+ - **插入模式(三选一,每条一个)**:「关」只插入不附加;「每次」在你**点发送时**(Enter 或官方发送按钮)自动拼到消息**末尾**一起发出;「仅首次」**只在这个会话的第一条消息**上附加。输入框里都不提前显示;多条按列表顺序拼接、条目间空一行。判定与「每次」一样**跨分类生效**(按条目 id 找,不限于当前分类),原文为空时不附加(交还官方原语义)。
100
+ - 「仅首次」的判据是官方会话快照里的 `blank`(这个会话还没有任何消息):发完第一条它自己就为 false,所以**不需要插件自己记状态**;刷新页面、切走再切回来都不会重复附加。
101
+ - 两种模式**同时存在**时,新会话的第一条消息里**两批都附**:「每次」的那几条在前、「仅首次」的那几条在后,一起拼到末尾(第二条起就只剩「每次」的了)。
102
+ - 为什么是一个三选一控件、而不是两个勾选框:「每次」与「仅首次」互斥,两个独立勾选框能造出「同时又每次又仅首次」的矛盾状态;三选一只发出一个 mode,由 `withInsertMode` 一次把两个标志写对。
103
+ - 手工编辑文件时若把 `autoSend` 与 `autoSendFirst` 都写成 `true`,按「每次」处理(每次插入本来就包含第一次),并在下次写盘时修正回互斥状态。
82
104
  - **优化提示词**:把输入框里的话交给**另一个 AI** 整理成一条能直接发给工作 AI 的清晰指令,结果**直接写回输入框**(Ctrl+Z 可还原)。三档强度:普通 / 高级(默认)/ 极端。
83
105
 
84
106
  ### 数据存在哪
85
107
 
86
- 快捷指令(分类 + 条目 + 「默认插入」标记)存在 **`$DSH_HOME/quick-prompts.json`**(默认 `~/.dsh/quick-prompts.json`):
108
+ 快捷指令(分类 + 条目 + 每条的插入模式)存在 **`$DSH_HOME/quick-prompts.json`**(默认 `~/.dsh/quick-prompts.json`):
87
109
 
88
110
  - **与会话、项目无关**:换仓库、换会话都在;可以直接备份、搬迁、手工编辑(改完在设置页点「重新读取」)。
89
111
  - 宿主半用「临时文件 → `fsync` → `rename`」**原子替换**,并用 `<file>.lock` 串行化写入;写到一半断电不会留下半截 JSON,并发保存也不会互相截断。
90
112
  - 文件读不动(不是合法 JSON / 结构认不出 / 空文件)时,**先把坏文件改名隔离**成 `quick-prompts.json.bad-<时间戳>` 再如实报错,**绝不静默返回空列表**。这一条是刻意的:若回退成「读不懂就当空列表」,用户下一次保存就会把空列表写回去,**真数据被覆盖**。
91
113
  - 文件字段名与 [lnyuqian/dsh-quick-prompts](https://github.com/lnyuqian/dsh-quick-prompts) 对齐(`categories` / `name` / `title` / `text` / `autoSend` / `order`),同一个文件两边都读得懂。⚠️ 但**不要同时装两个插件**——同一个文件两个写者会互相覆盖(对方还是非原子写)。
114
+ - 「仅首次」是我们加的**扩展键** `autoSendFirst: true`(只在为真时写出来)。对方的插件会忽略它,并在它重新保存时丢掉这个键——也就是说「仅首次」在那边会退化成「关」。
92
115
  - 0.2.x 存在设置文档里的那份旧列表会在**首次读取时自动迁移**成「默认」分类;旧值**保留在设置文档里不清空**,万一回滚到 0.2.x 还能看到自己那几条。
93
116
  - 单条提示词上限从 4000 字提到 **20 万字**(只做防呆,不再截断正常提示词);送去「优化」的原文仍限 8000 字(那是给模型的输入,不能跟着涨)。
94
117
 
@@ -186,7 +209,10 @@ pnpm dsh web --patch <本仓库的绝对路径>/cordis.dev.patch.yml
186
209
 
187
210
  除快捷指令之外的设置存在 Host 用户设置文档(默认 `$DSH_HOME/settings.yaml`)的 `composer-ux` 分区,随工作区/机器生效;删除该分区即恢复这些设置的默认值。
188
211
 
189
- 快捷指令(分类 + 条目 + 「默认插入」)**不在**设置文档里,它存 `$DSH_HOME/quick-prompts.json`(见上「数据存在哪」)。设置文档里那份 `quickPrompts` 只是 0.2.x 的**迁移种子**,故意保留不清空。
212
+ 快捷指令(分类 + 条目 + 插入模式)**不在**设置文档里,它存 `$DSH_HOME/quick-prompts.json`(见上「数据存在哪」)。设置文档里那份 `quickPrompts` 只是 0.2.x 的**迁移种子**:只在 `quick-prompts.json` **不存在**时被读一次(用来把旧列表搬成「默认」分类),平时不参与任何行为,插件也不会再写它。
213
+
214
+ - 确认不需要这份回滚兜底后,**可以从设置文档里手动删掉它**(连同 `quickPrompts:` 这一项)——不影响插件运行;之后若 JSON 丢失,只会回落到内置 9 条。删掉后它不会自己长回来。
215
+ - 想要**真备份**请复制 `quick-prompts.json`,不要指望这份种子:它是旧快照,不会随你的后续修改更新。
190
216
 
191
217
  ## 发行包与「装前体检」
192
218