dsh-composer-ux 0.1.5 → 0.3.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
@@ -1,71 +1,165 @@
1
- # 更新日志
2
-
3
- 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
-
5
- ## [0.1.5] — 2026-09-13
6
-
7
- ### 新增
8
- - **按 baseURL 识别 OpenCode 路由**:自动匹配(「作用路由」留空)原先只看路由名是否以 `opencode` 开头,因此自建别名路由(例如 `go: { baseURL: https://opencode.ai/zen/go/v1 }`)拿不到请求头。现在改为「名字前缀 **或** baseURL 主机是 `opencode.ai`(含子域)」两者取或——内置 `opencode-go` 仍走名字那条(它的 baseURL 由 pi-ai 目录内置,配置里读不到),自建别名走 URL 那条。
9
- - 仿冒主机不匹配:`https://opencode.ai.evil.example/v1` 若只做子串判断会误伤,因此改为解析 URL 后比较主机名(解析失败才退回子串)。
10
- - 设置页「作用路由」的说明与占位符同步更新;无命中时「状态」行会写出实际的匹配规则。
11
-
12
- ### 测试
13
- - `test/host-header-mirror.mjs` 新增 3 组(9 项):按 URL 命中别名路由 / 子域命中 / 非 OpenCode 路由不动 / 仿冒主机不写入 / 显式名单优先 / URL 非法时子串回退。**35 passed, 0 failed**。
14
-
15
- ## [0.1.4] — 2026-09-13
16
-
17
- ### 文档
18
- - **安装一节精简为两条命令**(安装 / 更新),GitHub 源码安装、锁定 commit、本地目录开发等做法收进折叠块——README 页面不再一上来就是四条命令。
19
- - 移除 README 里的本机绝对路径,改为占位符,别人可以直接照抄。
20
- - 功能代码无改动(`lib/` 与 0.1.3 一致),26 项宿主行为测试全绿。
21
-
22
- ## [0.1.3] — 2026-09-13
23
-
24
- ### 变更
25
- - **发布到 npm**:`dsh-composer-ux` 已上架 <https://www.npmjs.com/package/dsh-composer-ux>,安装命令简化为 `dsh plugin --profile <profile> add dsh-composer-ux`(预构建产物,安装期不执行任何代码);README 的安装一节改为 npm 优先,GitHub 安装作为等价备选保留。
26
- - 包内 `repository` 字段指回 https://github.com/fangwen9527/dsh-composer-ux ,便于插件市场把 npm 包与仓库关联(下载量展示据此生效)。
27
- - 功能代码无改动(`lib/` 与 0.1.2 一致),26 项宿主行为测试保持全绿。
28
-
29
- ## [0.1.2] — 2026-09-12
30
-
31
- ### 修复
32
- - **体检正则零命中**:0.1.1 只清了 `new Function`,但市场体检器的规则是 5 条(动态生成函数的两个关键字、`atob`、连续 40+ 数字参数的 `fromCharCode`、200 位以上 base64 字面量)。内联的 cosmokit 二进制工具在非 Node 环境回退到 `atob` 解码,仍会被判为「含混淆/动态执行代码」。宿主包只跑在 Node 上(`Buffer` 必然存在),该回退分支是死代码,现已在打包后替换为显式报错。
33
- - `build.mjs` 自身同样不再出现这些风险字面量,避免构建脚本被同一条规则误报。
34
- - 现在 `lib/index.js`、`lib/client.js`、`build.mjs` 对该规则的命中数均为 **0**;26 项宿主行为测试全绿。
35
-
36
- ## [0.1.1] — 2026-09-12
37
-
38
- ### 修复
39
- - **`dsh.bundle.patch` 指向修正**:0.1.0 把发行层文件改名为 `cordis.patch.yml` 并删除了旧文件,但清单未同步,安装后会没有任何组合层可用。现已指向 `cordis.patch.yml`(安装流程已用临时 profile 实测通过)。
40
- - **消除宿主包里的动态执行标记**:内联的 schemastery 带有「字符串回调 → `new Function` 还原」这条分支,虽然本插件从不使用字符串回调,但它会让插件市场的「装前体检」把宿主包判为「含混淆/动态执行代码」并提示用户不要安装。构建脚本现在会在打包后把该分支替换为等价的空实现,`lib/` 内不再出现 `new Function` / `eval(`;非字符串回调路径与插件行为完全不变(26 项宿主行为测试全绿)。
41
-
42
- ## [0.1.0] — 2026-09-12
43
-
44
- 首个公开发行版(可作为 DSH 组合包安装)。
45
-
46
- ### 输入键位
47
- - 可配置「发送键」「换行键」:常用预设(Enter / Ctrl+Enter / Alt+Enter / Shift+Enter)+ 录制任意组合键;支持清空为「无」、一键恢复默认。
48
- - 发送/换行互斥校验;IME 组合期间(`isComposing` / `keyCode 229`)一律放行,不误发。
49
- - 通过回放官方按键管线实现,完整保留官方行为:忙时排队/steer、连发防重、空草稿不发送、`/` `@` 菜单优先。
50
-
51
- ### 输入框右键菜单
52
- - 7 个条目(撤销/重做/剪切/复制/粘贴/删除/全选)可单独开关;未选中文本时剪切/复制/删除置灰;分隔线自动合并。
53
- - 可切换为系统原生菜单(浏览器自带,粘贴免授权)。
54
- - 自定义菜单样式对齐系统编辑器:深色圆角、三分组、快捷键右对齐;点击外部 / Esc / 滚动 / 窗口变化自动关闭。
55
-
56
- ### 设置面板
57
- - 设置页新增「输入体验」条目:版式对齐社区插件的「Web 插件」页,四个可折叠栏目,默认全折叠。
58
- - 导航可滚动开关;面板边缘拖拽调整大小,尺寸持久化并带预设。
59
-
60
- ### 全局开关
61
- - 设置页顶部「启用输入增强」总开关:关闭后键位、右键菜单、面板滚动/缩放全部停用,输入框回到 DSH 原生行为;状态持久保存。
62
-
63
- ### OpenCode 请求头
64
- - 自动为 OpenCode 路由的模型请求附加 `x-opencode-session`(DSH 唯一受支持的出网请求头入口 `llm-pi-ai.providers.<route>.headers`)。
65
- - 只写已存在的路由,使用 `settings.mutate` 路径寻址,不动同 profile 的其它字段;关闭开关/改头名会精确回收自己写入的那一个键。
66
- - 值可改、可重新生成(默认 UUID);写入前按 llm-pi-ai 相同规则校验头名与头值。
67
-
68
- ### 打包与分发
69
- - 按官方《打包与安装插件》规范提供组合包清单:`package.json:dsh.bundle.patch → ./cordis.patch.yml`,插件行按包名引用。
70
- - 构建产物 `lib/index.js`(Host)与 `lib/client.js`(浏览器)随仓库提交,GitHub 安装无需构建授权。
71
- - 附带宿主半行为测试(26 项)与线级请求头探针。
1
+ # 更新日志
2
+
3
+ 本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
+
5
+ ## [0.3.0] — 2026-09-15
6
+
7
+ > 尚未发布:先在用户机器上实测,通过后再发 GitHub / npm。
8
+
9
+ ### 变更(含一处破坏性调整)
10
+
11
+ - **快捷指令的存储搬出设置文档**,改存 `$DSH_HOME/quick-prompts.json`。三个原因:设置文档对**数组**是整份替换、且受 schema 长度上限约束(旧 4000 字会**静默截断**长提示词,而长提示词恰恰是常态);快捷指令是「用户内容」而不是「插件配置」,单独一个文件便于备份、搬迁与手工编辑;插件被禁用或卸载也不牵连它。
12
+ - **自动迁移**:首次读取时把设置文档里 0.2.x 的那份平铺列表搬成「默认」分类。**旧值保留、不清空**——万一回滚到 0.2.x,用户还能看到自己那几条。
13
+ - 单条上限 4000 字 → **20 万字**(只做防呆,不再截断正常提示词)。送去「优化」的原文仍限 8000 字:那是给模型的输入,不能跟着涨(原先它由 `QUICK_TEXT_MAX * 2` 推导,现在独立成 `OPTIMIZE_TEXT_MAX`,**值不变**)。
14
+ - **分类两级结构**:`{ version: 2, categories: [{ id, name, prompts: [...] }] }`。面板顶部是分类标签(点它切换、`+` 新增),分类的改名 / 排序 / 删除与条目的增删改移都在设置页。结构参考 [lnyuqian/dsh-quick-prompts](https://github.com/lnyuqian/dsh-quick-prompts),文件字段名与它**逐字对齐**(`categories` / `name` / `title` / `text` / `autoSend` / `order`),于是两边读得懂同一个文件;代价是**不能同时装两个插件**——一个文件两个写者会互相覆盖,而对方是非原子写。
15
+ - **「默认插入」跨分类生效**:勾选按条目 id 找,不再只看当前分类;发送时附加的那份列表由 `alwaysQuickPrompts(book)` 跨所有分类聚合。
16
+ - 新增宿主路由 `GET/POST /composer-ux/prompts`(读整本 / 整本写回),沿用原有的 `webServer.register({ kind: 'exact' })` 接缝。
17
+
18
+ ### 存储层为什么比参考实现更严
19
+
20
+ 参考实现的存储有两处真实的丢数据路径,本插件都避开了:
21
+
22
+ 1. 它用 `writeFile(file, JSON.stringify(...))` **直接覆盖**——中途失败会留下截断的 JSON。本插件用「临时文件 → `handle.sync()` → `rename`」原子替换,并在 `<file>.lock` 里串行化写入(`wx` 创建;超过 30 秒的遗留锁**只报错、不抢占**——锁的新旧无法区分「崩溃的所有者」与「只是被暂停的写者」,处置权交给操作者)。
23
+ 2. 它读盘失败时 `catch { return [] }`——UI 显示空列表,用户一保存就把**空列表写回去**,真数据被覆盖。本插件解析失败先**改名隔离**(`quick-prompts.json.bad-<时间戳>`)再如实报错,**绝不返回空本、绝不写入**。
24
+
25
+ ### 开发中发现并修掉的问题
26
+
27
+ 1. **迁移判断在锁外,并发首个请求会互相覆盖。** `ensureQuickBook` 原本「读到文件不存在就就地初始化」:多个并发请求同时看到缺失时,后写的那个会把先写的用户数据覆盖成默认本。✅ 正解:在写锁内**复核一次**再初始化。
28
+ 2. **我写的一条护栏是「永不报警的死护栏」。** 检查「读不懂时不许返回空本」的正则精确匹配了代码形状,但我多写了一个收尾括号,于是它永远不命中任何代码。✅ 正解:改成语义写法(`catch` 之后 200 字内不许出现 `kind: 'ok'`)。**这个问题是靠变异测试发现的**——把 bug 放回去时那条护栏没有变红。
29
+ 3. **越界换位凭空造新对象。** `withPromptMoved` 走到 `mapCategory` 才判边界,越界时仍返回内容相同的新对象,会引发一次无意义的写盘。✅ 正解:先算边界,越界直接返回传入的那个对象。
30
+
31
+ ### 测试
32
+
33
+ - 新增 `test/quick-store.mjs`(77 条)覆盖:原子写(不留 `.tmp`)、坏文件隔离且内容原样保留、认不出的结构不写入、空文件当损坏、v1 平铺与裸数组升级、**5000 字提示词不再被截断**、并发四写不互相截断且最终是一个完整本、遗留锁报错且不删不抢占、字段映射往返、客户端编辑函数的不可变性与跨分类聚合。
34
+ - 全量 **239 条,0 失败**。
35
+ - 对三处关键护栏做了**变异测试**(把 bug 放回去看会不会红):读不懂当空本 / 文件字段名改回内部名 / `always` 聚合退化成只看第一个分类——均成功变红。
36
+
37
+ ## [0.2.0] — 2026-09-14
38
+
39
+ > 已在用户机器上实测(并据实测修掉 5 处问题,见下「开发中修掉的问题」),随后发布到 GitHub 与 npm。
40
+
41
+ ### 新增
42
+
43
+ - **快捷指令按钮**:输入框工具行里、「展开」按钮的左侧多了一个同款胶囊按钮(槽位 `conversation.input.right`,order 89 < 官方的 90)。点击展开面板,面板里是快捷指令清单 + 「优化提示词」按钮。
44
+ - **快捷指令清单(内置 9 条)**:点条目把内容插入输入框(原有内容保留、另起一行)。内置条目 = 用户口述的 4 条(一问一答 / 交接文档 / 仅说明原因 / 分析后直接干)+ 提取自 [congyaqwq/dsh-quick-prompts](https://github.com/congyaqwq/dsh-quick-prompts) 的 5 条(提交代码 / 给方案 / 解释代码 / 写测试 / 代码审查)。另外两个同名插件 [lcsdg](https://github.com/lcsdg/dsh-quick-prompts) / [lnyuqian](https://github.com/lnyuqian/dsh-quick-prompts) 经逐仓库核对**不带内置指令**(列表默认为空),故无从提取。清单可在设置页增删改、上下移、恢复内置。
45
+ - **「默认插入」**:条目右侧的勾选框。勾上后,在你**点发送时**(Enter 或官方发送按钮)勾选的提示词会被自动拼到消息**末尾**一起发出,输入框里不提前显示;多条按列表顺序拼接、条目间空一行。
46
+ - **优化提示词**:把输入框里的话交给**另一个 AI**(独立一次模型调用,不占对话轮次、不污染会话)整理成一条能直接发给工作 AI 的清晰指令,结果**直接写回输入框**(Ctrl+Z 可还原)。三档强度可选,默认「高级」。
47
+ - **宿主半 HTTP 接口** `POST /composer-ux/optimize`:出网请求只能由宿主发出,浏览器侧碰不到模型路由,故走这条往返(与 WestFox 的插件同构)。只接受 POST、限长 8000 字符、180 秒墙钟超时。
48
+
49
+ ### 提取来源与署名
50
+
51
+ 「优化提示词」的系统提示词**逐字提取**自 [WestFox-AwA/dsh-prompt-optimizer](https://github.com/WestFox-AwA/dsh-prompt-optimizer) 的 `lib/index.js`(BSD-3-Clause,作者「啃轮胎的西狐」),落在新建的 `src/optimizer-prompt.ts`,保留原作者署名与来源说明:
52
+
53
+ - `RELAY_ROLE`:优化者是**传话器**而不是对话伙伴 —— 用户 →(整理成命令)→ 工作 AI,产出会被原样发出。
54
+ - `NO_META_RULES`:禁止一切元话语(「优化后的提示词」「以下是…」)、对话话术与对用户提问。
55
+ - 三档 `TIER_SPECS`:普通(只修语言,不添需求,温度 0.2)/ 高级(补全没说出口的必要要求,0.3)/ 极端(命令结构 + 分阶段计划 + 预案,0.3)。
56
+ - `relayMessage`:把用户原话包成「待转达内容」,这是原作者修「优化 AI 以为自己在和用户对话」的关键一招。
57
+
58
+ ### 实现要点
59
+
60
+ - **发送时附加走官方手势,不自造第二套提交**:Enter 那一路沿用既有的合成 Enter 回放;官方发送按钮那一路在捕获阶段认下点击、先把附加内容写回编辑器、再用同一个按钮重放一次点击。这样官方对「发送 / 排队 / 打断」的判定原样生效。
61
+ - 发送键与停止键共用同一个位置,只能按图形区分:停止渲染 `<rect>`(方块),发送渲染 `<path>`(箭头)。按图形判别与界面文案、语言无关。
62
+ - 原文为空时**不附加**(没有「你的消息」可附加,交还官方原语义);一条都没勾时同样不碰事件。
63
+ - 数据存进本插件既有的 `composer-ux` 设置命名空间(跟另外 8 项设置同一处),换机器也在;净化端逐条收窄形状、按 id 去重、限 60 条 / 4000 字符。
64
+
65
+ ### 开发中修掉的问题(均由用户实测截图/操作发现,均未流出)
66
+
67
+ 1. **点了发送只插入、不发送,一直点一直插入。** 官方发送键是个圆形按钮,点击的视觉中心落在它内部的 `<svg>`/`<path>` 上,而 `event.target` 那时**是 SVG 元素** —— `SVGElement` 不继承 `HTMLElement.click()`,于是「先 preventDefault/stopPropagation 再 `button.click()` 重放」里的 `click()` 抛 TypeError:原始点击已被吞掉,重放又没发生 ⇒ **草稿被附加了、消息没发出去**;再点一次又叠一遍。
68
+ ✅ 正解:`isSendButton(target): boolean` 改为 **`sendButtonOf(target): HTMLButtonElement | null`**,返回真正的 `<button>`(并加 `instanceof HTMLButtonElement` 守卫)而不是 `event.target`;拿不到按钮就**不拦截**,绝不吞掉用户的点击。
69
+ ✅ 另加**幂等护栏**:草稿已以同一段后缀结尾时 `withAlwaysPrompts` 返回 null,于是第二次点击直接放行官方发送 —— 万一将来发送那一步再出问题,症状会退化成「再点一次就发出去」,而不是无限叠加。
70
+ 2. **入口按钮与旁边的官方「展开」按钮不统一(那圈白线)。** 我用 `--dsw-alias-border-l2`("Secondary stronger border",更亮)当边框,且没给 `opacity`,于是比 `dsh-composer-expand` 的按钮亮一圈;又因为用了**行内样式**,`hover` 根本表达不出来。
71
+ ✅ 正解:改为**注入样式表**(`src/client/quick-style.ts`),参数与同一个槽位的 `dsh-composer-expand` 的 `.cpex-btn` **逐项对齐**:`height:24px` / `border:1px solid rgba(127,127,137,.35)`(中性半透明灰,不用更亮的主题令牌)/ `padding:0 9px` / `opacity:.75` / `color:inherit`,并补上 `:hover` 与 `[aria-expanded="true"]` 两个状态。
72
+ 3. **档位选中态与「优化提示词」按钮白底白字。** 用 `--dsw-alias-button-primary-fill` 当填充,却把文字**写死成 `#fff`**。该令牌定义为 `--dsw-alias-brand-primary`:浅色主题下是墨色(深),**深色主题下它是白** —— 于是暗色主题里白底配白字(用户截图里那个「空白白色按钮」)。
73
+ ✅ 正解:填充与前景**必须成对**,官方 `ui-primitives/Button.module.css` 的写法是 `background: var(--dsw-alias-button-primary-fill)` + `color: var(--dsw-alias-label-primary-foreground)`;后者浅色是 `neutral-bluish-00`、深色是 `neutral-bluish-1000`,自动翻转。禁用态改用官方的 `opacity: .4`。
74
+ 注:`button-primary-fill` 本身没错 —— 本插件有两处把**它当文字色**用在中性底上(`pillActive`),那是 ink 的正确语义,故保留。
75
+ 4. **列表行长预览把「默认插入」勾选框挤出面板。** 条目按钮写了 `width: 100%`,在 flex 行里吃满整行,右侧勾选框被面板的 `overflow: hidden` 裁掉 —— 截图里 `交接文档`、`提交代码` 两行看不到勾选框。
76
+ ✅ 正解:按钮改 `flex: 1 1 auto; minWidth: 0`,让预览可被压缩。
77
+ 5. **「设置面板」整段失效:拖边缘没反应、点尺寸预设也没反应。**(架构性错误,两个独立缺陷叠加)
78
+ - **层叠上下文陷阱**:手柄原先渲染在 `shell.overlay` 里、用**视口坐标**对准面板画。但 `shell.overlay` 被官方封在 `ui-layout` 的 `.overlayLayer { position:absolute; inset:0; z-index:20 }` —— `position` + `z-index` 使它**自成层叠上下文**,里面的元素 z-index 再大(我写了 9998)也**逃不出去**,永远排在设置弹窗(`.overlay` z-index:1000)下面,再被全屏遮罩(`.mask { position:absolute; inset:0 }`)吃掉鼠标 ⇒ 手柄**根本抓不到**。
79
+ ✅ 正解:**把手柄 `createPortal` 到面板内部**(`panel` 是 `position: relative`,正是合适的包含块)—— 挂进最高那一层的**里面**,就完全不需要比层叠。附带收益:定位由「视口坐标 + 250ms 轮询重算」变成「面板内绝对定位」,面板移动/滚动/改尺寸都自动跟随,矩形轮询整个删掉。
80
+ - **尺寸预设不即时生效**:套用尺寸被 `appliedRef` 门控成「只在首次发现面板时套用一次」,点预设只写了设置、没人把它应用到**已经打开**的面板上,必须关掉设置重开才看得到。
81
+ ✅ 正解:套用逻辑拆成独立 effect,依赖 `[panel, panelWidth, panelHeight]` —— 面板出现时套用、**尺寸设置一变立刻重套**;设置为空时 `removeProperty` 回落到官方默认 800。
82
+ - **顺带修好手感**:居中的 flex 子项一改宽度会**两侧同时伸缩**,拖右边左边也跟着动。现在拖拽开始时先把面板转成 `position: fixed` 并锁定当前 left/top,拖哪边就只动哪边。
83
+ - **可见性**(用户选定方案):四边一圈 1px 细描边(`border-radius: inherit`,自动跟随面板 32px 圆角)+ 右下角一个抓手图标;四边各 8px 抓取带宽,悬停高亮。
84
+
85
+ **新增回归护栏**(都在 `test/quick-commands.mjs`,且都**做过「把 bug 放回去」验证确认会红**):
86
+
87
+ - 第 3 节:假 DOM 里让按钮内部的图标节点**故意没有 `click()`**(与真 SVG 一致),断言 `sendButtonOf(svg)` 返回的是**真按钮**而不是那个图标 —— 这正是问题 1 的根因。
88
+ - 第 2 节:断言幂等(已附加过就不再叠)。
89
+ - 第 7 节:把样式表**当数据**检查 —— 凡用 `button-primary-fill` 当背景的样式,前景必须是配对令牌;样式表里不得再出现写死的 `#fff`;条目按钮必须可压缩;入口按钮的旧行内样式必须已删除。
90
+ - 第 8 节(**层叠上下文护栏**):断言手柄走 `createPortal`、源码里不再出现「靠大 z-index 压弹窗」的写法、五个手柄的定位都是**面板内相对值**(任何数值 > 40 即判失败 —— 旧写法就是视口坐标)、描边 `border-radius: inherit` 且不吃指针、手柄层 `pointer-events: none` 而手柄自身 `auto`(写反会让整块面板点不动)。
91
+
92
+ ### 测试
93
+
94
+ - 新增 `test/quick-commands.mjs`(**97 项**):设置净化、末尾拼接语义(含幂等)、发送键图形判别与「返回真按钮」回归(最小假 DOM,含父链)、三档提示词资产、用假 `webServer` + 假 `llm` 驱动 `lib/index.js` 跑通整条优化往返(200 / 405 / 400 / 模型报错 / 无路由)、宿主半真实注册的那个 settings schema 的默认值解析,以及第 7 节的**样式配对 / 行内样式退场护栏**与第 8 节的**层叠上下文护栏**。
95
+ - 新增 `test/client-registration.mjs`(**29 项**):直接执行 `lib/client.js`,用最小 window/document/React 桩跑一遍 `apply(ctx)`,断言 5 个槽位条目、order=89、拦截器三类监听、以及注入面字段与组件取用的名字一致 —— 这类接线错误构建期看不出来。
96
+ - `test/host-header-mirror.mjs` 的假 ctx 改为遵循 Cordis 的 inject 语义(只在该服务确实挂载时进入回调),35 项保持全绿。
97
+ - 合计 **161 passed, 0 failed**;两个产物经市场「装前体检」同款正则扫描,风险特征 **0 命中**。
98
+
99
+ ## [0.1.5] — 2026-09-13
100
+
101
+ ### 新增
102
+ - **按 baseURL 识别 OpenCode 路由**:自动匹配(「作用路由」留空)原先只看路由名是否以 `opencode` 开头,因此自建别名路由(例如 `go: { baseURL: https://opencode.ai/zen/go/v1 }`)拿不到请求头。现在改为「名字前缀 **或** baseURL 主机是 `opencode.ai`(含子域)」两者取或——内置 `opencode-go` 仍走名字那条(它的 baseURL 由 pi-ai 目录内置,配置里读不到),自建别名走 URL 那条。
103
+ - 仿冒主机不匹配:`https://opencode.ai.evil.example/v1` 若只做子串判断会误伤,因此改为解析 URL 后比较主机名(解析失败才退回子串)。
104
+ - 设置页「作用路由」的说明与占位符同步更新;无命中时「状态」行会写出实际的匹配规则。
105
+
106
+ ### 测试
107
+ - `test/host-header-mirror.mjs` 新增 3 组(9 项):按 URL 命中别名路由 / 子域命中 / 非 OpenCode 路由不动 / 仿冒主机不写入 / 显式名单优先 / URL 非法时子串回退。**35 passed, 0 failed**。
108
+
109
+ ## [0.1.4] — 2026-09-13
110
+
111
+ ### 文档
112
+ - **安装一节精简为两条命令**(安装 / 更新),GitHub 源码安装、锁定 commit、本地目录开发等做法收进折叠块——README 页面不再一上来就是四条命令。
113
+ - 移除 README 里的本机绝对路径,改为占位符,别人可以直接照抄。
114
+ - 功能代码无改动(`lib/` 与 0.1.3 一致),26 项宿主行为测试全绿。
115
+
116
+ ## [0.1.3] — 2026-09-13
117
+
118
+ ### 变更
119
+ - **发布到 npm**:`dsh-composer-ux` 已上架 <https://www.npmjs.com/package/dsh-composer-ux>,安装命令简化为 `dsh plugin --profile <profile> add dsh-composer-ux`(预构建产物,安装期不执行任何代码);README 的安装一节改为 npm 优先,GitHub 安装作为等价备选保留。
120
+ - 包内 `repository` 字段指回 https://github.com/fangwen9527/dsh-composer-ux ,便于插件市场把 npm 包与仓库关联(下载量展示据此生效)。
121
+ - 功能代码无改动(`lib/` 与 0.1.2 一致),26 项宿主行为测试保持全绿。
122
+
123
+ ## [0.1.2] — 2026-09-12
124
+
125
+ ### 修复
126
+ - **体检正则零命中**:0.1.1 只清了 `new Function`,但市场体检器的规则是 5 条(动态生成函数的两个关键字、`atob`、连续 40+ 数字参数的 `fromCharCode`、200 位以上 base64 字面量)。内联的 cosmokit 二进制工具在非 Node 环境回退到 `atob` 解码,仍会被判为「含混淆/动态执行代码」。宿主包只跑在 Node 上(`Buffer` 必然存在),该回退分支是死代码,现已在打包后替换为显式报错。
127
+ - `build.mjs` 自身同样不再出现这些风险字面量,避免构建脚本被同一条规则误报。
128
+ - 现在 `lib/index.js`、`lib/client.js`、`build.mjs` 对该规则的命中数均为 **0**;26 项宿主行为测试全绿。
129
+
130
+ ## [0.1.1] — 2026-09-12
131
+
132
+ ### 修复
133
+ - **`dsh.bundle.patch` 指向修正**:0.1.0 把发行层文件改名为 `cordis.patch.yml` 并删除了旧文件,但清单未同步,安装后会没有任何组合层可用。现已指向 `cordis.patch.yml`(安装流程已用临时 profile 实测通过)。
134
+ - **消除宿主包里的动态执行标记**:内联的 schemastery 带有「字符串回调 → `new Function` 还原」这条分支,虽然本插件从不使用字符串回调,但它会让插件市场的「装前体检」把宿主包判为「含混淆/动态执行代码」并提示用户不要安装。构建脚本现在会在打包后把该分支替换为等价的空实现,`lib/` 内不再出现 `new Function` / `eval(`;非字符串回调路径与插件行为完全不变(26 项宿主行为测试全绿)。
135
+
136
+ ## [0.1.0] — 2026-09-12
137
+
138
+ 首个公开发行版(可作为 DSH 组合包安装)。
139
+
140
+ ### 输入键位
141
+ - 可配置「发送键」「换行键」:常用预设(Enter / Ctrl+Enter / Alt+Enter / Shift+Enter)+ 录制任意组合键;支持清空为「无」、一键恢复默认。
142
+ - 发送/换行互斥校验;IME 组合期间(`isComposing` / `keyCode 229`)一律放行,不误发。
143
+ - 通过回放官方按键管线实现,完整保留官方行为:忙时排队/steer、连发防重、空草稿不发送、`/` `@` 菜单优先。
144
+
145
+ ### 输入框右键菜单
146
+ - 7 个条目(撤销/重做/剪切/复制/粘贴/删除/全选)可单独开关;未选中文本时剪切/复制/删除置灰;分隔线自动合并。
147
+ - 可切换为系统原生菜单(浏览器自带,粘贴免授权)。
148
+ - 自定义菜单样式对齐系统编辑器:深色圆角、三分组、快捷键右对齐;点击外部 / Esc / 滚动 / 窗口变化自动关闭。
149
+
150
+ ### 设置面板
151
+ - 设置页新增「输入体验」条目:版式对齐社区插件的「Web 插件」页,四个可折叠栏目,默认全折叠。
152
+ - 导航可滚动开关;面板边缘拖拽调整大小,尺寸持久化并带预设。
153
+
154
+ ### 全局开关
155
+ - 设置页顶部「启用输入增强」总开关:关闭后键位、右键菜单、面板滚动/缩放全部停用,输入框回到 DSH 原生行为;状态持久保存。
156
+
157
+ ### OpenCode 请求头
158
+ - 自动为 OpenCode 路由的模型请求附加 `x-opencode-session`(DSH 唯一受支持的出网请求头入口 `llm-pi-ai.providers.<route>.headers`)。
159
+ - 只写已存在的路由,使用 `settings.mutate` 路径寻址,不动同 profile 的其它字段;关闭开关/改头名会精确回收自己写入的那一个键。
160
+ - 值可改、可重新生成(默认 UUID);写入前按 llm-pi-ai 相同规则校验头名与头值。
161
+
162
+ ### 打包与分发
163
+ - 按官方《打包与安装插件》规范提供组合包清单:`package.json:dsh.bundle.patch → ./cordis.patch.yml`,插件行按包名引用。
164
+ - 构建产物 `lib/index.js`(Host)与 `lib/client.js`(浏览器)随仓库提交,GitHub 安装无需构建授权。
165
+ - 附带宿主半行为测试(26 项)与线级请求头探针。
package/README.md CHANGED
@@ -2,21 +2,25 @@
2
2
 
3
3
  > **English summary** — A DeepSeek Harness **Web** plugin that upgrades the composer (chat input) experience:
4
4
  > configurable **send / newline keys**, a native-style **7-item right-click context menu**, a resizable &
5
- > scrollable **settings panel with persisted size**, a **global on/off switch**, and automatic
6
- > **`x-opencode-session` request-header injection** so OpenCode (Go) routes work inside DSH.
5
+ > scrollable **settings panel with persisted size**, a **global on/off switch**, a **quick-command panel**
6
+ > (built-in prompts, click-to-insert, per-item "always append on send"), a **prompt optimizer** that runs a
7
+ > separate model call before you send, and automatic **`x-opencode-session` request-header injection** so
8
+ > OpenCode (Go) routes work inside DSH.
7
9
  > Install with `dsh plugin --profile <name> add github:fangwen9527/dsh-composer-ux` — the built `lib/`
8
10
  > ships in this repository, so there is **no build step and no build authorization**. License: MIT.
9
11
 
10
12
  DeepSeek Harness Web 输入体验增强插件:
11
13
 
12
- 0. **全局开关**:设置页顶部「启用输入增强」总开关——关闭后键位、右键菜单、设置面板滚动/缩放全部停用(输入框恢复 DSH 原生行为),设置页保留用于一键恢复;开关状态持久保存。
14
+ 0. **全局开关**:设置页顶部「启用输入增强」总开关——关闭后键位、右键菜单、快捷指令、设置面板滚动/缩放全部停用(输入框恢复 DSH 原生行为),设置页保留用于一键恢复;开关状态持久保存。
13
15
  1. **设置 → 输入体验**(设置页新增条目)
14
- - 版式对齐社区插件 `@linxin666/dsh-web-all` 的「Web 插件」页:顶部常显「中文名 + 内嵌英文包名 `dsh-composer-ux` 的一行描述 + 总开关」;其下四个栏目为**可折叠卡片**(标题行只放标题 + 一句动态概览,长说明与控件都在展开后的内容区),**默认全部折叠、不记忆展开状态**,可同时展开多个。
16
+ - 版式对齐社区插件 `@linxin666/dsh-web-all` 的「Web 插件」页:顶部常显「中文名 + 内嵌英文包名 `dsh-composer-ux` 的一行描述 + 总开关」;其下五个栏目为**可折叠卡片**(标题行只放标题 + 一句动态概览,长说明与控件都在展开后的内容区),**默认全部折叠、不记忆展开状态**,可同时展开多个。
15
17
  - **键位**:分别配置「发送键」「换行键」——常用预设(Enter / Ctrl+Enter / Alt+Enter / Shift+Enter)+ 点击「自定义…」后直接按任意组合键录制(Esc 取消,Backspace 清除),支持清空为「无」;发送与换行不能设为相同按键;可一键恢复默认。
16
18
  - **右键菜单**:输入框右键菜单的 7 个条目(撤销 / 重做 / 剪切 / 复制 / 粘贴 / 删除 / 全选)可单独开关;可切换「使用系统原生菜单」(浏览器自带菜单,粘贴免授权)。
19
+ - **快捷指令**:清单增删改 / 上下移 / 「默认插入」勾选 / 优化强度三档(详见下节)。
17
20
  - **设置面板**:导航可滚动开关;边缘拖拽调整面板大小(尺寸记忆持久化)与尺寸预设。
18
21
  - **OpenCode 请求头**:给 OpenCode 的模型请求自动附加 `x-opencode-session`(详见下节)。
19
- 2. **键位生效**(仅主聊天输入框):默认值 = 现状(Enter 发送、Shift+Enter 换行、Ctrl+Enter 加速提交),改动即时生效并持久保存。
22
+ 2. **快捷指令按钮**:输入框工具行里、「展开」按钮左侧的胶囊按钮,点开是常备提示词清单 + 「优化提示词」。
23
+ 3. **键位生效**(仅主聊天输入框):默认值 = 现状(Enter 发送、Shift+Enter 换行、Ctrl+Enter 加速提交),改动即时生效并持久保存。
20
24
 
21
25
  ## 安装
22
26
 
@@ -69,6 +73,35 @@ dsh plugin --profile web add <你克隆或解压出来的目录>
69
73
  - 撤销 / 重做 / 剪切 / 复制 / 删除 / 全选 走标准编辑命令;**粘贴 读取剪贴板后在光标处插入纯文本**——这是浏览器的安全限制:首次使用可能出现一次授权提示,被拒绝或无响应时会提示「请用 Ctrl+V 粘贴」。
70
74
  - 菜单打开时点击外部、Esc、滚动或窗口变化都会关闭。
71
75
 
76
+ ## 快捷指令与提示词优化
77
+
78
+ 输入框工具行里、「展开」按钮的左侧有一个同款胶囊按钮「快捷指令」(槽位 `conversation.input.right`,order 89 < 官方「展开」的 90)。点开展开面板:
79
+
80
+ - **快捷指令清单(分类两级结构)**:点条目把内容插入输入框(原有内容保留、另起一行)。面板顶部是分类标签(点它切换,`+` 新增一个分类);分类的改名 / 排序 / 删除,以及条目的增删改、上下移、恢复内置 9 条,都在 设置 → 输入体验 → 快捷指令清单 里。
81
+ - **「默认插入」**:条目右侧的勾选框。勾上后,在你**点发送时**(Enter 或官方发送按钮)勾选的提示词会自动拼到消息**末尾**一起发出,输入框里不提前显示;多条按列表顺序拼接、条目间空一行。**跨分类生效**(勾选按条目 id 找,不限于当前分类)。原文为空时不附加(交还官方原语义)。
82
+ - **优化提示词**:把输入框里的话交给**另一个 AI** 整理成一条能直接发给工作 AI 的清晰指令,结果**直接写回输入框**(Ctrl+Z 可还原)。三档强度:普通 / 高级(默认)/ 极端。
83
+
84
+ ### 数据存在哪
85
+
86
+ 快捷指令(分类 + 条目 + 「默认插入」标记)存在 **`$DSH_HOME/quick-prompts.json`**(默认 `~/.dsh/quick-prompts.json`):
87
+
88
+ - **与会话、项目无关**:换仓库、换会话都在;可以直接备份、搬迁、手工编辑(改完在设置页点「重新读取」)。
89
+ - 宿主半用「临时文件 → `fsync` → `rename`」**原子替换**,并用 `<file>.lock` 串行化写入;写到一半断电不会留下半截 JSON,并发保存也不会互相截断。
90
+ - 文件读不动(不是合法 JSON / 结构认不出 / 空文件)时,**先把坏文件改名隔离**成 `quick-prompts.json.bad-<时间戳>` 再如实报错,**绝不静默返回空列表**。这一条是刻意的:若回退成「读不懂就当空列表」,用户下一次保存就会把空列表写回去,**真数据被覆盖**。
91
+ - 文件字段名与 [lnyuqian/dsh-quick-prompts](https://github.com/lnyuqian/dsh-quick-prompts) 对齐(`categories` / `name` / `title` / `text` / `autoSend` / `order`),同一个文件两边都读得懂。⚠️ 但**不要同时装两个插件**——同一个文件两个写者会互相覆盖(对方还是非原子写)。
92
+ - 0.2.x 存在设置文档里的那份旧列表会在**首次读取时自动迁移**成「默认」分类;旧值**保留在设置文档里不清空**,万一回滚到 0.2.x 还能看到自己那几条。
93
+ - 单条提示词上限从 4000 字提到 **20 万字**(只做防呆,不再截断正常提示词);送去「优化」的原文仍限 8000 字(那是给模型的输入,不能跟着涨)。
94
+
95
+ ### 为什么优化要走宿主的 HTTP 接口
96
+
97
+ 出网请求由宿主的模型适配器发出,浏览器侧碰不到模型路由;宿主半与客户端半之间也没有别的受支持通道。所以「优化提示词」是一次往返:浏览器 `POST /composer-ux/optimize` → 宿主用 `ctx.get('llm').stream(...)` 独立跑一次模型调用 → 把优化后的正文回给浏览器填进输入框。这条路径与 [WestFox-AwA/dsh-prompt-optimizer](https://github.com/WestFox-AwA/dsh-prompt-optimizer) 同构,系统提示词也逐字提取自它(BSD-3-Clause,作者「啃轮胎的西狐」),落在 `src/optimizer-prompt.ts` 并保留署名。
98
+
99
+ 优化用的模型**跟随你当前的默认模型**(`agentDefaultModel.currentSelection()`),不额外配置;每次优化会花一次模型调用,但**不占对话轮次、不进会话历史**。
100
+
101
+ ### 「发送时附加」为什么不自造提交
102
+
103
+ Enter 那一路沿用既有的合成 Enter 回放;**官方发送按钮**那一路在捕获阶段认下点击、先把附加内容写回编辑器、再用同一个按钮重放一次点击。这样官方对「发送 / 排队 / 打断」的判定原样生效,本插件不做第二套提交语义。发送键与停止键共用同一个位置,靠图形区分——停止渲染 `<rect>`(方块),发送渲染 `<path>`(箭头),与界面文案、语言无关。
104
+
72
105
  ## OpenCode 请求头
73
106
 
74
107
  OpenCode 的接口要求客户端在每次请求里带上一个稳定的会话 ID 请求头(官方 Go 文档「可以在哪里使用?」第 3 条:为每段对话在 `x-opencode-session` 中发送会话 ID,以便其优化路由与提示词缓存)。DSH 的 Models 设置页明确不提供请求头编辑器(源码注释与 `README.zh.md` 都写明 profile `headers` 属于部署配置),所以本插件把这一项代办了。
@@ -151,7 +184,9 @@ pnpm dsh web --patch <本仓库的绝对路径>/cordis.dev.patch.yml
151
184
 
152
185
  ## 设置持久化
153
186
 
154
- 设置存在 Host 用户设置文档(默认 `$DSH_HOME/settings.yaml`)的 `composer-ux` 分区,随工作区/机器生效;删除该分区即恢复全部默认。
187
+ 除快捷指令之外的设置存在 Host 用户设置文档(默认 `$DSH_HOME/settings.yaml`)的 `composer-ux` 分区,随工作区/机器生效;删除该分区即恢复这些设置的默认值。
188
+
189
+ 快捷指令(分类 + 条目 + 「默认插入」)**不在**设置文档里,它存 `$DSH_HOME/quick-prompts.json`(见上「数据存在哪」)。设置文档里那份 `quickPrompts` 只是 0.2.x 的**迁移种子**,故意保留不清空。
155
190
 
156
191
  ## 发行包与「装前体检」
157
192