dsh-composer-ux 0.5.0 → 0.6.2
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 +229 -0
- package/README.md +31 -7
- package/lib/client.js +581 -291
- package/lib/client.js.map +4 -4
- package/lib/index.js +992 -204
- package/lib/index.js.map +4 -4
- package/package.json +3 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,235 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.6.2] — 2026-09-25(浮层磨砂修复 + 设置页「刷新」按钮 + 默认终端「接管前自检」)
|
|
6
|
+
|
|
7
|
+
> 三块内容合成一版:①用户截图报的「快捷指令面板透明看不清」;②桌面版里「重启 DSH」不可能生效,补一个轻量替代;③
|
|
8
|
+
> 「默认终端」在官方安装的桌面版里会把会话的 shell 打死,补一道**接管前自检**(并顺带定位到两条官方缺陷)。
|
|
9
|
+
> ①与②只动**客户端半**(刷新页面即可生效);③含**宿主半**(装完需**重启 DSH**)。
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- **浮层只画了「半透明」、漏了官方成对使用的「毛玻璃」**。DSH 的菜单面是**一对**令牌:
|
|
14
|
+
`--dsw-specific-menu`(半透明填充)+ `backdrop-filter: var(--dsw-menu-backdrop-filter)`
|
|
15
|
+
(实测 `blur(40px) saturate(150%)`)。官方每一个绘制前者的浮层都**两者齐用**,形如
|
|
16
|
+
`.panel:before{background:var(--dsw-specific-menu);backdrop-filter:var(--dsw-menu-backdrop-filter)}`;
|
|
17
|
+
本插件只写了 `background` ⇒ 半透明在、磨砂不在 ⇒ 浮层成了**一块没磨砂的玻璃**,
|
|
18
|
+
背后正文原样透出来、与面板里的文字叠在一起。
|
|
19
|
+
- 判定方法(下次照抄):在 `app.asar` 的平台 CSS 里搜令牌名,看官方组件是不是**成对**出现;
|
|
20
|
+
只有填充、没有 blur 的就是漏抄。
|
|
21
|
+
- 两处一起修:`menu`(自定义档右键菜单)与 `quickPanel`(快捷指令面板)。只修一处会出现
|
|
22
|
+
「一个浮层清楚、另一个还在透」。
|
|
23
|
+
|
|
24
|
+
### 变更(同批对齐官方高层级表面)
|
|
25
|
+
|
|
26
|
+
- 浮层描边改走 **elevation 发丝线**:`border: 0` + `box-shadow: var(--dsw-elevation-prominent)` +
|
|
27
|
+
`--dsw-elevation-stroke-color: var(--dsw-alias-border-l1)`。官方高层级表面一律 `border: 0`
|
|
28
|
+
(描边由 elevation 自带);本插件原先**既有实线边框、又有 elevation 那 0.5px 描边**,
|
|
29
|
+
等于同一个边缘画了两条。显式钉 `l1` 是为了保持升级前的观感
|
|
30
|
+
(`--dsw-elevation-stroke-color` 的全局默认是 `border-l4`)。
|
|
31
|
+
- 圆角改用官方令牌 `var(--dsw-radius-lg)`(= **16px**;原为写死的 10 / 12)。
|
|
32
|
+
- **刻意不跟官方的一处**:`z-index` 保留 `9999`(官方浮层是 1100)。本插件浮层挂在 `shell.overlay`
|
|
33
|
+
这个独立层叠上下文里,要与别的插件抢层,降到 1100 只会被别人的浮层盖住(见 `src/client/panel.ts` 文件头)。
|
|
34
|
+
|
|
35
|
+
### 新增
|
|
36
|
+
|
|
37
|
+
- **设置页抬头右端增加「刷新」按钮**(在「重启 DSH」左边;`SettingsSection.tsx` 的 `RefreshButton`)。
|
|
38
|
+
起因:用户在**官方安装的桌面版**里按「重启 DSH」没反应。查证后这不是偶发故障、而是**形态不匹配**(见下),
|
|
39
|
+
于是需要一个轻量替代。它只做 `location.reload()`:不发请求、不碰宿主、**不打断正在跑的会话**
|
|
40
|
+
(会话在宿主侧活着,刷新只重建界面)。而插件的**客户端半**(设置页 / 右键菜单 / 键位拦截 /
|
|
41
|
+
快捷指令面板)本来就随页面 bundle 走 —— 重新加载页面就能生效,这正是「让改动生效」在桌面版里
|
|
42
|
+
唯一还走得通的路。
|
|
43
|
+
- 排布:轻的在前(刷新 → 重启 → GitHub);重启处于「确认中 / 重启中」时刷新一并禁用 ——
|
|
44
|
+
否则一次刷新会把用户手上那条待确认的横幅冲掉。
|
|
45
|
+
- 护栏:`test/settings-render.mjs` 新增第 6 节 —— **真渲染**出页面,再比较两枚按钮的先后,
|
|
46
|
+
并按交接文档 §7.25 的教训不满足于"出现过"。该套件 24 → **28 项**。
|
|
47
|
+
- **接管「默认终端」之前先自检**(`src/terminal/tool.ts` 的 `probeBashExecution` + `src/terminal/host.ts` 的门控
|
|
48
|
+
+ `src/terminal/contracts.ts` 的 `probeFailed` 状态文案)。起因:官方安装的**桌面版**在**受限文件策略**下,
|
|
49
|
+
命令要过沙箱 runner,而 runner 是用 `process.execPath`(= `DeepSeek Harness.exe`,不是 node)起的,
|
|
50
|
+
实测每条命令 `0xC0000142`(DLL 初始化失败)**零输出**;照旧接管会得到
|
|
51
|
+
「`restrict(pwsh)` 已生效 + 自带的 bash 又跑不动」= 该会话**一个能用的 shell 都没有**(比不接管更糟)。
|
|
52
|
+
- 探针走的是**与真工具完全相同的执行路径**(借道 `createBashTool(...).execute(...)`:同一份 argv 构造、
|
|
53
|
+
同一个沙箱 `confine`、同一个 `subprocess.spawn`),且不带 `sandbox_permissions` —— 自检不该弹审批。
|
|
54
|
+
- 不通过就**不接管**(不 restrict、不注册、不加提示词段),保持 PowerShell,并把原因写进状态行
|
|
55
|
+
(「已启用,但自检未通过:… —— 为避免把会话的 shell 打死,暂不接管」)。
|
|
56
|
+
- `danger-full-access` **不做**探针:那条路会跳过 `confine()`,同一台桌面版上实测正常
|
|
57
|
+
(`uname -a` → `MINGW64_NT-10.0-26200 … Msys`)。结论按 `路径|模式` 缓存,换路径/换模式才重探。
|
|
58
|
+
- 护栏:`test/terminal-policy.mjs` 新增 7 组共 23 项(该套件 176 → **199 项**),
|
|
59
|
+
`test/mutation-guards.mjs` 新增 `AB`(拆掉门控)/`AC`(不看模式)/`AD`(门控退回部署默认)/
|
|
60
|
+
`AE`(不再补 `ELECTRON_RUN_AS_NODE`)——**四条都咬住**。
|
|
61
|
+
- **打包形态的 spawn 环境补 `ELECTRON_RUN_AS_NODE=1`**(`createBashTool` 的 `electron` 选项,默认看
|
|
62
|
+
`process.versions.electron`):Windows 上受限模式的沙箱 runner argv[0] 是 `process.execPath`
|
|
63
|
+
(`dsh-sandbox-local/lib/index.js:539`),桌面版里那是 Electron 的 exe;用 Electron 跑脚本必须带这个变量,
|
|
64
|
+
否则 runner 根本起不来(实测零输出 / `0xC0000142`)。补上之后,受限模式下探针拿到的是 **bash 自己的报错**
|
|
65
|
+
(`couldn't create signal pipe, Win32 error 5`),状态行不再是含糊的"零输出"。
|
|
66
|
+
对 `bash.exe` 本身无副作用(它不认这个变量)。⇒ 它**不会**让 Git Bash 在受限模式下可用,只是把诊断说准。
|
|
67
|
+
- 🔎 **同轮定位到两条官方缺陷**(与本插件无关,已写成可直接提交的报告:
|
|
68
|
+
工作区 `给官方的缺陷报告-桌面版受限沙箱.md`):① 打包版受限模式下沙箱 runner 用 `process.execPath`
|
|
69
|
+
启动却没人给 `ELECTRON_RUN_AS_NODE`(影响**任何**经沙箱 spawn 的命令);② MSYS2(Git Bash)在
|
|
70
|
+
Windows ACL 沙箱下启动即崩(`couldn't create signal pipe, Win32 error 5`),而同一沙箱下
|
|
71
|
+
`cmd` / `pwsh` 实测都正常。
|
|
72
|
+
- ⚠️ **真机首验就抓到我实现里的一个 bug**:门控最初调 `sandboxPolicy.resolve({})`,拿到的是
|
|
73
|
+
**部署默认**(base bundle:`process.env.DSH_PERMISSION_MODE ?? 'workspace-write'`),而工具执行时用的是
|
|
74
|
+
**该会话自己**的策略(`{ session: exec.agent.session }`)⇒ 一个 `danger-full-access` 的会话被误判成受限、
|
|
75
|
+
自检按预期失败、整栏被白白撤掉(真机上确实发生了一次:工具表里 `bash` 消失)。已改成逐会话解析
|
|
76
|
+
(`confinedModeOf(deps, agent)`),并加"门控把 session 交给了 sandboxPolicy.resolve"这条断言 + 变异 `AD` 钉住。
|
|
77
|
+
- ✅ **真机验证(同一轮,用户配合)**:会话切到受限策略后,门控按预期调到探针、探针拿到
|
|
78
|
+
`0xC0000142`(零输出)——**§7.32 的根因被独立复现**;而这次是**探针替会话死了一次**:插件不接管、
|
|
79
|
+
状态行写「已启用,但自检未通过…」、模型侧保住 PowerShell(`terminalEffective: pwsh`)。
|
|
80
|
+
这正是这道护栏要的效果:不必先把自己的 shell 打死才知道那条路是死的。
|
|
81
|
+
- 完整静态兼容性核对见工作区 `兼容性测试-composer-ux-0.6.1-vs-desktop-0.1.7-rc.2.md`;
|
|
82
|
+
机制背景见交接文档 §7.32 与 §7.34。
|
|
83
|
+
|
|
84
|
+
### 说明(⚠️ 发版前必须补做)
|
|
85
|
+
|
|
86
|
+
- **「重启 DSH」在官方安装的桌面版里不可能生效**(2026-09-25 查证,属机制性限制、不是 bug):
|
|
87
|
+
它重放的是「`node <bin.js>` 或裸 `dsh`」(`src/restart.ts` 的 `launchCommand`),而桌面版宿主是
|
|
88
|
+
**Electron 应用**(`DeepSeek Harness.exe`),两个条件都不成立;`~/.dsh/launcher/_boot.log` 里
|
|
89
|
+
反复出现「无法定位 dsh CLI(dsh 安装目录未找到)」正与此吻合,且重启助手写下的诊断日志
|
|
90
|
+
自 **2026-09-23** 起一条都没有。**本轮没有改它的行为**(用户只选了"加刷新")。
|
|
91
|
+
- **本机的活 profile 是 `profiles/desktop`,不是 `profiles/web`**(详见交接文档 §7.32):
|
|
92
|
+
用户这次「默认终端」开关的写入落在 `profiles/desktop/cordis.patch.yml`。所以本机交付要覆盖
|
|
93
|
+
**两个**已安装副本的 `lib/client.js`(`profiles/desktop/…` 才是活的那份,`profiles/web/…` 一并覆盖以免踩错)。
|
|
94
|
+
- **构建与测试本轮已真跑**(不再是"手工同步"):`node build.mjs` → `npm test` = **774 passed / 0 failed**;
|
|
95
|
+
`node test/settings-render.mjs` = **28 passed / 0 failed**。仓库与两个 profile 的 `lib/client.js`
|
|
96
|
+
md5 一致 = `9017EBC3EBAA2F02427C8BF415461389`(`lib/index.js` 未变,仍是 `955C23B7…`)。
|
|
97
|
+
⚠️ **上面这组数字与哈希是"加自检探针之前"那一轮的**。探针 + 环境变量两处改动(<sup>2026-09-25</sup>)之后重跑:
|
|
98
|
+
`npm test` = **797 passed / 0 failed**(41+17+294+133+**199**+87+26)、`test/settings-render.mjs` = 28;
|
|
99
|
+
`lib/client.js` md5 **仍是** `9017EBC3EBAA2F02427C8BF415461389`(客户端半一字未动),
|
|
100
|
+
`lib/index.js` md5 **已变为** `46EEFE792622BE82B9C34A8524BAB12D`(宿主半:探针 + 逐会话门控 + 打包版 env)
|
|
101
|
+
⇒ 交付时两个 profile 要按新的 `lib/index.js` 覆盖(**只换 client.js 不够了**)。
|
|
102
|
+
- ⚠️ **覆盖产物有个 pnpm 硬链接的坑**(2026-09-25 实测踩到):两个 profile 的
|
|
103
|
+
`node_modules/dsh-composer-ux/lib/index.js` 曾经**共用同一个 inode**(pnpm 硬链接,link count=4,
|
|
104
|
+
其中还包含 pnpm store 里的 blob)⇒ 直接 `cp -f` 会**穿透 inode 写入**,一次就改了全部链接、
|
|
105
|
+
连 store 都被污染(以后 `pnpm install` 该包会装到改过的内容)。
|
|
106
|
+
**正确做法**:先 `rm` 再 `cp`(断开硬链接,各持独立 inode),并在改动后核对
|
|
107
|
+
store blob 的 md5 仍是原始值(`955C23B76D69D90DA8484D5F17B19DA6`);
|
|
108
|
+
store 文件名是**内容 sha512**(`files/<前2位>/<后128位>`),内容被改过就对不上名。
|
|
109
|
+
|
|
110
|
+
## [0.6.1] — 2026-09-24
|
|
111
|
+
|
|
112
|
+
> 起因是一次真机故障:设置页的开关"点了没反应",而界面上**一句提示都没有**。查到最后是两层独立原因,
|
|
113
|
+
> 两层都在这一版里处理掉。**含宿主半改动**(启动时新增一次孤儿锁回收),装完需要**重启 DSH**。
|
|
114
|
+
|
|
115
|
+
### 修复
|
|
116
|
+
|
|
117
|
+
- **volatile 标记改成逐字段(不再标根),对齐官方形状**。这是"设置改完不生效"的根因:
|
|
118
|
+
schemastery 3.18.4 一旦看到**根**带 volatile,就把**整棵解析结果**包成**一个根引用**
|
|
119
|
+
(`Config({}).enabled === undefined`,值只在 `.get()` 里);而 loader 把配置变更提交回
|
|
120
|
+
**运行中引用**的那条路(`_commitVolatile`)是按**叶子路径**设计的。官方插件
|
|
121
|
+
(`ui-theme` / `locale` / `llm-deepseek` / `ui-conversation` …)**一律逐字段标,没有一个标根**。
|
|
122
|
+
改完的形状是"普通对象壳 + 叶子引用",真机验证:`Config({})` 不再是引用,`parsed.enabled` 是叶子引用。
|
|
123
|
+
- 顺带把两条护栏补成"字段级":`host-settings-generations` 新增「每个字段都带标记」「根节点不标」,
|
|
124
|
+
变异 Z 改为动**方法分支**(3.18.4 下标记走 `.volatile()`,动 meta 兜底那行已经不参与执行、
|
|
125
|
+
护栏会"咬不住"——这是升级后的新事实,之前那条变异因此失效)。
|
|
126
|
+
- **孤儿写入锁会让整个 profile 的设置写入静默失效**。DSH 用 `<profile>/package.json.lock` 串行化跨进程写入,
|
|
127
|
+
且规定争用方**永不删除**已存在的锁(孤儿锁由操作者处理);而 `restart-webui.bat` 的 `taskkill /T /F`
|
|
128
|
+
硬杀只要落在一次写入中间,锁就永久留下 —— 此后**每一次**设置写入都在 2 秒后超时,读得到、写不进,
|
|
129
|
+
界面只表现为"点了没反应"(2026-09-23 连撞两次,两次都是重启留下的)。
|
|
130
|
+
- 新增 `src/settings-lock.ts`:宿主半启动时读一次锁里的 PID,**只在确认该进程已不存在时**删除;
|
|
131
|
+
内容认不出 / PID 还活着 / 权限不足无法判定 → 一律不动(误删活锁会让两个写入者交错提交同一个 profile patch)。
|
|
132
|
+
判据是纯函数,逐条钉在测试第 12 节(19 条)。
|
|
133
|
+
- **设置页不再"静默装死"**:写入被宿主拒绝、或**写入被接受但运行时的值没变**时,把原因显示在设置卡顶部
|
|
134
|
+
(`writeNotice`,与「重启 DSH」横幅同一位置)。以前这两种情况都只往控制台写一行,用户能看到的只有"开关不动"。
|
|
135
|
+
|
|
136
|
+
### 文档
|
|
137
|
+
|
|
138
|
+
- README「设置持久化」补「⚠️ 设置写不进去 / 点了没反应:两种成因与恢复」(含手工回收锁的一行命令)。
|
|
139
|
+
- README 更正一处过期描述:0.1.7 起设置存在 **profile 的 `cordis.patch.yml`**,不再是 `$DSH_HOME/settings.yaml`。
|
|
140
|
+
|
|
141
|
+
### 测试
|
|
142
|
+
|
|
143
|
+
- `test/quick-commands.mjs` 新增第 12 节:孤儿锁判据 + 真实文件系统上的四条路径(无锁 / 认不出 / 持有者活着 / 持有者已死)。
|
|
144
|
+
- 适配 DSH 随包的 **schemastery 3.18.4**:它开始认 `meta.volatile` 并在解析时把节点包成**引用**
|
|
145
|
+
(3.18.2 没有这个分支)。于是"从 schema 解析结果直接读普通值"的断言(`quick-commands` 第 6 节、
|
|
146
|
+
`host-header-mirror` 第 15 节、`host-settings-generations` 第 1 节)改为先解引用;断言要分的那几件事没有变。
|
|
147
|
+
|
|
148
|
+
## [0.6.0] — 2026-09-22(**未单独发布**,内容并入 0.6.1)
|
|
149
|
+
|
|
150
|
+
> **适配 DSH 0.1.7**(对 `dsh-v0.1.7-alpha.1` 逐项核对)。同一个产物同时支持 0.1.6 与 0.1.7:
|
|
151
|
+
> 两代的设置服务形状不同,代码按**能力探测**分支,不维护两份产物。
|
|
152
|
+
> **本版含宿主半改动**(注册方式、字段表、事件名),装完必须**重启 DSH** 才生效。
|
|
153
|
+
>
|
|
154
|
+
> ⚠️ **0.1.7 自己的升级动作会搬走 `~/.dsh/settings.yaml`**:它把该文件改名成 `settings.yaml.imported`
|
|
155
|
+
> 并逐节导入 profile patch(**一次性、部分导入过的文件不会重跑**)。本插件这一段在 0.1.7 上
|
|
156
|
+
> **导不进去**(那时插件还没有 Config,服务报 `No configurable plugin entry`),所以旧值只会留在
|
|
157
|
+
> 那个 `.imported` 文件里;0.1.6 又会重新生成一份干净的 `settings.yaml`。
|
|
158
|
+
> 结果就是:**升级到 0.1.7 之前,请先备份 `settings.yaml`(或那个 `.imported`),旧值需要手动迁回。**
|
|
159
|
+
|
|
160
|
+
### 破坏性变更
|
|
161
|
+
|
|
162
|
+
- **删掉「设置面板 → 导航滚动」这一栏**:DSH 0.1.7 的官方设置页已给导航列自带 `overflow-y: auto`
|
|
163
|
+
(`ui-settings-general` 的 `.navList`),本插件再注入一套就是重复实现。整项删除:字段 `panelScroll`、
|
|
164
|
+
`html.dsh-ux-panel-scroll` 那组注入样式、标题行那个小开关、栏判据里的那一条。**「边缘缩放」照旧保留。**
|
|
165
|
+
- 迁移影响:只开过「导航滚动」、从没拖过尺寸的旧文档会被判成"这一栏从没碰过" ⇒ 该栏回到默认关。
|
|
166
|
+
这是刻意的:导航滚动已经不存在,那一栏剩下的唯一功能是缩放,而这些人从没用过它。
|
|
167
|
+
- 旧文档里残留的 `panelScroll` 不会让升级失败:schemastery 对**未声明键是原样放行**(已实测),
|
|
168
|
+
所以不需要为它留一个宽容字段。
|
|
169
|
+
|
|
170
|
+
### 适配(DSH 0.1.7)
|
|
171
|
+
|
|
172
|
+
- **两代设置服务都认领**(客户端半):0.1.6 及以前是 `ctx.settingsScope.bind({ namespace })`,
|
|
173
|
+
0.1.7 起是 `ctx.configForms.get(namespace)`。两者**不能同时写进静态 `inject`** —— 各自在对方那一代
|
|
174
|
+
永远不出现,插件会静默等依赖、**整块不挂载**(设置页、键位、右键菜单、快捷指令一起消失)。
|
|
175
|
+
所以静态 inject 只留 `slots`,两个名字各用 `ctx.inject` 等一次,再加一次"直接读一次"兜底。
|
|
176
|
+
- **宿主半按能力分支注册**:`settings.register` 存在才注册(0.1.6);0.1.7 的命名空间由导出的 `Config` 推导。
|
|
177
|
+
- **"自己那一行"换了读法**:0.1.7 没有 `settings.get()`。自己的行从 Config 读(volatile 引用要解引用),
|
|
178
|
+
别人的行(只读 llm-pi-ai 的 providers.headers)从 `describe()` 读;两代差异收口在 `makeReader()`。
|
|
179
|
+
- **事件名换代**:0.1.6 发 `settings/updated`,0.1.7 发 `settings/document-updated`(旧名在 0.1.7 里
|
|
180
|
+
**整个不存在**)。两个都听,任一世代只会有一个触发。
|
|
181
|
+
- **manifest 的 `dsh.client.inject` 改成包行名**:`["slots","settingsScope"]` → `["@deepseek-ai/dsh-client-ui-settings"]`
|
|
182
|
+
(0.1.7 起这个字段的语义是"本行物化前必须先到位的包行",写服务名没有意义)。
|
|
183
|
+
- **volatile 标记靠数据、不靠方法**:宿主半把 schemastery **内联**进产物(取自构建时的 DSH 检出),
|
|
184
|
+
所以"在 0.1.6 上构建出来的包"里没有 `.volatile()`(它是 schemastery 3.18.3 才加的)。
|
|
185
|
+
改为直接写 `schema.meta.volatile = true` —— 0.1.7 的设置服务读的正是这个标记,与构造它的库版本无关。
|
|
186
|
+
0.1.7 里**只有 volatile 字段会被服务、也只有 volatile 路径能被写入**,所以当时把整棵 Config 标了 volatile
|
|
187
|
+
(**这一做法在 0.6.1 里被推翻:改成逐字段标记** —— 根标记在 schemastery 3.18.4 下会让"写入不生效",见上方 0.6.1 段)。
|
|
188
|
+
|
|
189
|
+
### 提示词优化改成「条目 + 逐字依据」(机制取自对方 0.6 线)
|
|
190
|
+
|
|
191
|
+
- **背景(核对过的实话)**:0.5.x 那三档文本逐字取自对方 `lib/index.js` 的 **`LEGACY_V011`** ——
|
|
192
|
+
该常量自己的注释写着"逐字节取自 0.1.1 的 system 提示词",且只在**回退策略** `v011-ptc` 下使用;
|
|
193
|
+
对方默认策略是 `STRATEGY_DEFAULT = 'v6'`(`buildSystem()` 走 `LANG_ONLY_SYSTEM` /
|
|
194
|
+
`STRATEGY_V5_SYSTEM` + `CLOSING_SELFCHECK` + 收件人契约)。**即:抄到的是对方历史回退路径上的那一层。**
|
|
195
|
+
更根本的问题是:自由改写器下"模型替用户发明一条需求"在结构上无法被发现,
|
|
196
|
+
提示词里写多少遍"不许新增"都只是空话。
|
|
197
|
+
- **机制照对方 0.6 线重写**(`po06/lib/interpreter.js` 的 `validateProvenance`、
|
|
198
|
+
`po06/lib/compiler.js` 的固定节序 + 预算丢弃):
|
|
199
|
+
- 模型不再自由改写,只产出条目;`rewrite` / `requirement` / `quality` **必须**附一段在用户原话里
|
|
200
|
+
**逐字存在**的引文,宿主做字面子串比对,**对不上就只丢那一条**并记账(单点不得废整轮——
|
|
201
|
+
这条纪律是对方 2026-09-22 真机回归换来的);找不到逐字匹配时退一步做**空白归一**比对,
|
|
202
|
+
免得折行/多空格复述白白丢条目。
|
|
203
|
+
- `rewrite` 按引文在原话里的**位置**回填(右侧先动,下标不串位),没被覆盖的原话**原样保留**,
|
|
204
|
+
其余条目按固定节序追加在末尾、每条带自己的逐字依据 —— 于是"凭空加需求"拿不出依据。
|
|
205
|
+
- 篇幅预算按档位给(普通 1.4 倍 / 高级 2.6 倍 / 极端 4 倍,另有绝对上限),超预算按固定顺序丢
|
|
206
|
+
**可选的节**、必保节永不丢,并把"省略了几条"写进成品(降级要出声,不静默截断)。
|
|
207
|
+
- 空产出**重试一次**(话术取自对方 `retryEmpty`,它来自对方真机"思考完成却没有产出")。
|
|
208
|
+
- **两条兜底**:模型没按信封输出 → 按旧行为**整段照收**(保证这次改造不会让原本能用的优化变成失败);
|
|
209
|
+
输出像信封但半截坏掉 → **直接失败**,绝不把坏 JSON 写进输入框(这是真机上最伤人的一种"优化")。
|
|
210
|
+
- **新增**:设置页每一档的提示词**可编辑**(`optimizerPromptBasic/Advanced/Extreme`,留空 = 用内置),
|
|
211
|
+
带「载入内置版」「恢复内置」,并显示插件固定追加的那段输出契约;状态行如实回报
|
|
212
|
+
`N 条补全 · 丢弃 M 条 · 重试过一次 · 自定义提示词`。**JSON 输出契约不由自定义提示词控制**
|
|
213
|
+
(它是"逐字依据"能被校验的前提)。
|
|
214
|
+
- **未采纳(如实记录)**:对方的会话上下文读取(回合 0–10 / 全文)、只读工具(read/glob/grep)、
|
|
215
|
+
发送前拦截 + 原话不改 + 随行包注入、每会话隔离与 `po06.json` 状态文件 —— 这些都要新的服务接线或
|
|
216
|
+
会改掉本插件"点按钮 → 回填输入框"的既有交互,本次不动。
|
|
217
|
+
- 提示词仍是三档(普通 / 高级 / 极端)与原有温度(0.2 / 0.3 / 0.3),**对外接口未变**:
|
|
218
|
+
`POST /composer-ux/optimize` 的请求体与 `ok/text/error/provider/model/truncated` 字段照旧,
|
|
219
|
+
新增字段(`itemCount` / `dropped` / `warnings` / `fallback` / `retried` / `promptSource` / `budget` / `chars`)
|
|
220
|
+
都是**附加**的,老客户端不读也不会坏。
|
|
221
|
+
|
|
222
|
+
### 测试
|
|
223
|
+
|
|
224
|
+
- 新增 `test/settings-service-adopt.mjs`(23 项):用真产物跑三种服务组合(只有 `settingsScope` /
|
|
225
|
+
只有 `configForms` / 两个都没有),并钉住"静态 inject 只能写 `slots`"。
|
|
226
|
+
- 新增 `test/host-settings-generations.mjs`(14 项):两代服务形状各跑一遍宿主半,钉住
|
|
227
|
+
`Config.meta.volatile`、0.1.7 形状下"没有 register 也不抛"、"Config 读不出来要退回 describe"。
|
|
228
|
+
- 变异护栏 21 → 26 条:新增 W(静态 inject 绑回某一代)、X(0.1.7 的认领被删)、
|
|
229
|
+
Y(register 不按能力调用)、Z(volatile 标记丢失)、AA(Config 空值不退回 describe)。
|
|
230
|
+
- 提示词优化这一块:`test/quick-commands.mjs` 从 3 节扩到 4 节(新增「依据校验与宿主装配」),
|
|
231
|
+
逐条钉住"引文对不上 → 只丢那一条并记账"「原文没被改写覆盖的部分原样保留」「半截 JSON 绝不写回」
|
|
232
|
+
「没给信封 → 整段照收兜底」「空产出重试一次」「自定义提示词不顶掉输出契约」等 50+ 项。
|
|
233
|
+
|
|
5
234
|
## [0.5.0] — 2026-09-21
|
|
6
235
|
|
|
7
236
|
> **本版含宿主半新能力**(六个折叠卡的栏开关进了 settings schema),装完必须**重启 DSH** 才会作用到会话上。
|
package/README.md
CHANGED
|
@@ -20,16 +20,18 @@ DeepSeek Harness Web 输入体验增强插件:
|
|
|
20
20
|
> 上图即 0.5.0 的设置页(六栏都开着)。注意标题行**只有卡级开关与从属控件** —— 右键菜单那 7 个条目开关已按实测反馈搬进卡内的「自定义」档(见第 0 条)。
|
|
21
21
|
|
|
22
22
|
0. **两层开关:总开关 + 每栏开关**。设置页顶部「启用输入增强」是**总开关(默认开)**,它是一道总闸;六张折叠卡的标题行右端各有一个**卡级开关(默认关)**,决定「这一栏要不要生效」。两处都满足才生效(`enabled && 该栏开关`)。
|
|
23
|
+
- **兼容性**:同一个产物同时支持 **DSH 0.1.6 与 0.1.7**。两代的设置服务形状不同(`settingsScope.bind` → `configForms.get`、宿主 `settings.register` 被删除、事件名换代),代码按**能力探测**分支,不维护两份产物。
|
|
24
|
+
- ⚠️ **升级 DSH 到 0.1.7 之前先备份 `~/.dsh/settings.yaml`**:0.1.7 的官方升级会把它改名成 `settings.yaml.imported` 并逐节导入 profile patch(一次性、不可逆),而本插件这一段在旧版上**导不进去**(那时还没有 Config 字段表),旧值只会留在那个 `.imported` 文件里。
|
|
23
25
|
- **老用户不会被升级弄坏**:卡级开关的默认值不是硬编码的 `false`,而是按「设置文档里有没有『你在用』的痕迹」迁移 —— 碰过的栏保持开着、没碰过的才是关;全新安装(空白文档)才是六栏全关。「快捷指令」那一栏多一条文件判据(0.3.0 起条目存在 `quick-prompts.json`,设置文档里看不出来)。
|
|
24
26
|
- **升级前可以先看一眼**:`node test/check-sections.mjs` 拿你真实的 `settings.yaml` 跑一遍迁移,打印六栏会变成什么(**只读**,不写任何文件)。
|
|
25
|
-
- **关掉一栏 = 这一块完全不介入**,且栏内的值全部保留(打开即原样恢复):键位关 → 输入框按 DSH 原生键位;右键菜单关 → 本插件不介入;快捷指令关 → 输入框那枚按钮消失;设置面板关 →
|
|
27
|
+
- **关掉一栏 = 这一块完全不介入**,且栏内的值全部保留(打开即原样恢复):键位关 → 输入框按 DSH 原生键位;右键菜单关 → 本插件不介入;快捷指令关 → 输入框那枚按钮消失;设置面板关 → 不拖大小;OpenCode 请求头关 → 停止注入并撤销已写入的头;默认终端关 → 保持 PowerShell。
|
|
26
28
|
- 「OpenCode 请求头」那一栏**没有额外的开关**:原来的「附加请求头」本来就是「这一栏要不要生效」,直接搬到了标题行。
|
|
27
29
|
1. **设置 → 输入体验**(设置页新增条目)
|
|
28
|
-
- 版式对齐社区插件 `@linxin666/dsh-web-all` 的「Web 插件」页:顶部常显「中文名 + 内嵌英文包名 `dsh-composer-ux` 的一行描述 + 总开关」;其下六个栏目为**可折叠卡片** —— 标题行左侧是「标题 + 一句动态概览 +
|
|
30
|
+
- 版式对齐社区插件 `@linxin666/dsh-web-all` 的「Web 插件」页:顶部常显「中文名 + 内嵌英文包名 `dsh-composer-ux` 的一行描述 + 总开关」;其下六个栏目为**可折叠卡片** —— 标题行左侧是「标题 + 一句动态概览 + 展开箭头」,右侧是**这一栏的开关与从属控件**(设置面板的缩放开关、默认终端的三档都在这里),长说明与其余控件在展开后的内容区;**默认全部折叠、不记忆展开状态**,可同时展开多个。标题行**只放卡级开关与从属控件**:右键菜单那 7 个条目开关按用户后来的反馈**搬回了卡内**(它们只对「自定义」档有意义,见下一条)。
|
|
29
31
|
- **键位**:分别配置「发送键」「换行键」——常用预设(Enter / Ctrl+Enter / Alt+Enter / Shift+Enter)+ 点击「自定义…」后直接按任意组合键录制(Esc 取消,Backspace 清除),支持清空为「无」;发送与换行不能设为相同按键;可一键恢复默认。
|
|
30
32
|
- **右键菜单**:三档「菜单来源」(官方不介入 / 浏览器菜单 / 自定义菜单,见下「右键菜单」一节);卡内**只显示当前选中那一档的说明**(点官方看官方的、点浏览器看浏览器的、点自定义看自定义的;自定义档还带 Chrome / Edge 与 Firefox 的剪贴板授权说明);那 7 个条目(撤销 / 重做 / 剪切 / 复制 / 粘贴 / 删除 / 全选)**只在「自定义」档显示**、逐个开关(旁边一个「全部开启」)。这三件事有**真渲染测试**盯着(`node test/settings-render.mjs`:把设置页渲染成 HTML,断言三档正文互斥、那 7 行只在自定义档、标题行没有第二个入口)。
|
|
31
33
|
- **快捷指令**:分类增删改 / 条目增删改与上下移 / 跨分类移动 / 每条一个插入模式(关 · 每次 · 仅首次)/ 优化强度三档(详见下节)。
|
|
32
|
-
-
|
|
34
|
+
- **设置面板**:边缘调整大小一个开关(标题行);尺寸预设与拖拽说明在展开区。**导航列滚动由 DSH 官方的设置页自带**(0.1.7 起 `.navList` 就有 `overflow-y: auto`),所以 0.6.0 把「导航滚动」那个开关整项删掉了 —— 留着就是重复实现。
|
|
33
35
|
- **OpenCode 请求头**:给 OpenCode 的模型请求自动附加 `x-opencode-session`(详见下节)。
|
|
34
36
|
- **默认终端**:Windows 上把模型用的终端工具从 PowerShell 换成 Git Bash(三档在标题行;详见下节)。
|
|
35
37
|
- **重启 DSH**:卡片抬头右端(GitHub 链接左边)那枚按钮,两步确认后原地重启(详见「维护:重启 DSH」)。
|
|
@@ -116,6 +118,9 @@ dsh plugin --profile web add <你克隆或解压出来的目录>
|
|
|
116
118
|
- 为什么是一个三选一控件、而不是两个勾选框:「每次」与「仅首次」互斥,两个独立勾选框能造出「同时又每次又仅首次」的矛盾状态;三选一只发出一个 mode,由 `withInsertMode` 一次把两个标志写对。
|
|
117
119
|
- 手工编辑文件时若把 `autoSend` 与 `autoSendFirst` 都写成 `true`,按「每次」处理(每次插入本来就包含第一次),并在下次写盘时修正回互斥状态。
|
|
118
120
|
- **优化提示词**:把输入框里的话交给**另一个 AI** 整理成一条能直接发给工作 AI 的清晰指令,结果**直接写回输入框**(Ctrl+Z 可还原)。三档强度:普通 / 高级(默认)/ 极端。
|
|
121
|
+
- **0.6.0 起它不再"自由改写"**:模型只产出**条目**,每条 `rewrite`/`requirement`/`quality` 必须附一段**在你原话里逐字存在**的引文;宿主逐条做字面比对,对不上就**只丢那一条**并记账。`rewrite` 按引文位置回填,没被覆盖的原话原样保留,其余条目按节追加在末尾。于是"替你发明一条你没说过的需求"在结构上做不到。
|
|
122
|
+
- 三档的差别是**依据预算**:普通 = 只做语言层修复;高级 = 可以补"能指回原话某一句"的必要要求;极端 = 再加分阶段计划与预案。成品有篇幅预算(普通档 1.4 倍),超了就按固定顺序丢可选的节,并把"省略了几条"写进成品(绝不静默截断)。
|
|
123
|
+
- 状态行会如实交代这一轮的结果:`N 条补全 · 丢弃 M 条 · 重试过一次 · 自定义提示词`;模型没按条目契约输出时走**整段照收**的兜底并标注 `未校验依据`(保证改造不会让原本能用的优化变成失败),输出像条目信封但半截坏掉时**直接失败**、绝不把坏 JSON 写进输入框。
|
|
119
124
|
|
|
120
125
|
### 数据存在哪
|
|
121
126
|
|
|
@@ -131,7 +136,9 @@ dsh plugin --profile web add <你克隆或解压出来的目录>
|
|
|
131
136
|
|
|
132
137
|
### 为什么优化要走宿主的 HTTP 接口
|
|
133
138
|
|
|
134
|
-
出网请求由宿主的模型适配器发出,浏览器侧碰不到模型路由;宿主半与客户端半之间也没有别的受支持通道。所以「优化提示词」是一次往返:浏览器 `POST /composer-ux/optimize` → 宿主用 `ctx.get('llm').stream(...)` 独立跑一次模型调用 →
|
|
139
|
+
出网请求由宿主的模型适配器发出,浏览器侧碰不到模型路由;宿主半与客户端半之间也没有别的受支持通道。所以「优化提示词」是一次往返:浏览器 `POST /composer-ux/optimize` → 宿主用 `ctx.get('llm').stream(...)` 独立跑一次模型调用 → 把装配好的正文回给浏览器填进输入框。这条路径与 [WestFox-AwA/dsh-prompt-optimizer](https://github.com/WestFox-AwA/dsh-prompt-optimizer) 同构。
|
|
140
|
+
|
|
141
|
+
**提示词的来历(如实写)**:0.5.x 那三档是逐字提取自它 0.5 线的 `lib/index.js`(BSD-3-Clause,作者「啃轮胎的西狐」)。0.6.0 起机制与提示词都改按它 **0.6 线**(`po06/lib/interpreter.js` 的 `SYSTEM_PROMPT` + `validateProvenance`、`po06/lib/compiler.js` 的固定节序与预算丢弃)**重写**,不再是逐字提取 —— 那些文件里没有现成可抄的"三档改写提示词",能借的是机制本身:条目化产出、逐字引文、只丢单条、降级出声。落在 `src/optimizer-prompt.ts`(提示词)与 `src/optimizer-assemble.ts`(校验 + 装配)两个文件,署名与来源说明保留在各个文件头。
|
|
135
142
|
|
|
136
143
|
优化用的模型**跟随你当前的默认模型**(`agentDefaultModel.currentSelection()`),不额外配置;每次优化会花一次模型调用,但**不占对话轮次、不进会话历史**。
|
|
137
144
|
|
|
@@ -239,11 +246,13 @@ dsh-composer-ux/
|
|
|
239
246
|
├── docs/settings-panel-0.5.0.png # README 顶部那张设置页截图(用 tag 固定的 raw 链接引用,不进 npm 包)
|
|
240
247
|
├── test/
|
|
241
248
|
│ ├── host-header-mirror.mjs # 请求头镜像的行为测试(假 settings 驱动构建产物)
|
|
249
|
+
│ ├── host-settings-generations.mjs # 宿主半两代设置服务(0.1.6 的 get/register 与 0.1.7 的 describe)
|
|
242
250
|
│ ├── quick-commands.mjs # 快捷指令 / 优化接口 / 路由注册 / 重启机制
|
|
243
251
|
│ ├── quick-store.mjs # 快捷指令文件存储与写回路径
|
|
244
252
|
│ ├── terminal-policy.mjs # 默认终端:探测 / 渲染 / 升级审批 / bash 工具 / 宿主半接线
|
|
245
253
|
│ ├── client-registration.mjs # 客户端注册协议、右键行为、抬头按钮、六栏开关与标题行布局
|
|
246
|
-
│ ├──
|
|
254
|
+
│ ├── settings-service-adopt.mjs # 客户端两代设置服务认领(settingsScope / configForms / 都没有)
|
|
255
|
+
│ ├── mutation-guards.mjs # 变异测试(手动跑):把每条护栏拆掉,测试必须变红(26 条)
|
|
247
256
|
│ ├── settings-render.mjs # 真渲染测试(手动跑):借 profile 的 react 把设置页渲染成 HTML
|
|
248
257
|
│ ├── check-sections.mjs # 升级前自查(只读):拿真实 settings.yaml 跑一遍栏开关迁移
|
|
249
258
|
│ └── opencode-header-wire-probe.mjs # 线级探针:本地端点,用来看 DSH 出网请求带了什么头
|
|
@@ -254,11 +263,12 @@ dsh-composer-ux/
|
|
|
254
263
|
|
|
255
264
|
```sh
|
|
256
265
|
node build.mjs # 产出 lib/index.js + lib/client.js
|
|
257
|
-
npm test #
|
|
266
|
+
npm test # 七个套件;当前 669 passed, 0 failed
|
|
258
267
|
node test/mutation-guards.mjs # 手动跑:变异测试,证明那套护栏真的在咬人
|
|
259
268
|
node test/settings-render.mjs # 手动跑:把设置页真渲染成 HTML,断言版式与互斥显示(20 项)
|
|
260
269
|
node test/check-sections.mjs # 只读:升级前看六栏会变成什么
|
|
261
270
|
node test/host-header-mirror.mjs # 只跑宿主半的请求头测试(写入/撤销/改名/幂等/不误删)
|
|
271
|
+
node test/host-settings-generations.mjs # 只跑宿主半的「两代设置服务」兼容(0.1.6 / 0.1.7 形状)
|
|
262
272
|
node test/terminal-policy.mjs # 只跑「默认终端」(探测 / 渲染 / 审批 / 工具 / 接线)
|
|
263
273
|
# 线级探针(可选):起一个本地端点,再把某条路由临时指过来,即可看到真实出网请求头
|
|
264
274
|
node test/opencode-header-wire-probe.mjs 8799 600000
|
|
@@ -290,13 +300,27 @@ pnpm dsh web --patch <本仓库的绝对路径>/cordis.dev.patch.yml
|
|
|
290
300
|
|
|
291
301
|
## 设置持久化
|
|
292
302
|
|
|
293
|
-
除快捷指令之外的设置存在
|
|
303
|
+
除快捷指令之外的设置存在 **profile 的 `cordis.patch.yml`** 里本插件那一行(`$DSH_HOME/profiles/<profile>/cordis.patch.yml` 的 `- id: composer-ux` → `config`)——0.1.7 起上游已废弃 `$DSH_HOME/settings.yaml`(它被改名成 `settings.yaml.imported` 后一次性导入);0.1.6 及以前才是 `settings.yaml` 的 `composer-ux` 分区。删掉那一项即恢复这些设置的默认值。
|
|
294
304
|
|
|
295
305
|
快捷指令(分类 + 条目 + 插入模式)**不在**设置文档里,它存 `$DSH_HOME/quick-prompts.json`(见上「数据存在哪」)。设置文档里那份 `quickPrompts` 只是 0.2.x 的**迁移种子**:只在 `quick-prompts.json` **不存在**时被读一次(用来把旧列表搬成「默认」分类),平时不参与任何行为,插件也不会再写它。
|
|
296
306
|
|
|
297
307
|
- 确认不需要这份回滚兜底后,**可以从设置文档里手动删掉它**(连同 `quickPrompts:` 这一项)——不影响插件运行;之后若 JSON 丢失,只会回落到内置 9 条。删掉后它不会自己长回来。
|
|
298
308
|
- 想要**真备份**请复制 `quick-prompts.json`,不要指望这份种子:它是旧快照,不会随你的后续修改更新。
|
|
299
309
|
|
|
310
|
+
### ⚠️ 设置写不进去 / 点了没反应:两种成因与恢复
|
|
311
|
+
|
|
312
|
+
**第一层:写入锁成了孤儿锁。** DSH 用 `<profile>/package.json.lock`(`wx` 独占创建 + 内容为持有者 PID)串行化跨进程写入,对争用方的规定是「**绝不删除已存在的锁**——锁的年龄证明不了持有者已经停下;孤儿锁属于操作者动作」。而**硬杀**(`restart-webui.bat` 的 `taskkill /T /F`)只要正好落在一次设置写入中间,这把锁就永久留在磁盘上。后果不是"某次写入失败",而是**该 profile 此后每一次设置写入都在 2 秒后超时**:读得到、写不进,界面上只表现为"点了没反应",一句报错都没有(2026-09-23 真机连撞两次,两次都是重启留下的)。
|
|
313
|
+
|
|
314
|
+
- **0.6.1 起的自动回收**:宿主半在启动时读一次锁里的 PID,**只有确认该进程已不存在**才删(内容认不出、PID 还活着、权限不足无法判定 → 一律不动)。判据与理由见 `src/settings-lock.ts`,逐条钉在测试第 12 节。
|
|
315
|
+
- **手工恢复**(自动回收没赶上时):
|
|
316
|
+
|
|
317
|
+
```powershell
|
|
318
|
+
Get-Content "$env:USERPROFILE\.dsh\profiles\web\package.json.lock" # 先确认持有者 PID 是否还活着
|
|
319
|
+
Remove-Item "$env:USERPROFILE\.dsh\profiles\web\package.json.lock" -Force
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**第二层:写入落盘了,但界面还是旧值。** 这说明**运行中的进程没有把这次配置变更接进运行时**(文件里的值是对的)。设置页自 0.6.1 起会在写入被拒、或"写了但运行时没变"时把原因显示在卡片顶部;遇到第二种情况**重启一次 DSH** 即可(重启会按文件里的值启动)。
|
|
323
|
+
|
|
300
324
|
## 发行包与「装前体检」
|
|
301
325
|
|
|
302
326
|
插件市场的装前体检会扫描宿主代码里的混淆/动态执行特征(`eval` / `new Function` / 超长 base64 块)。本插件的 `lib/` 里这些特征**为零**:
|