dsh-composer-ux 0.20.0 → 0.22.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 +124 -0
- package/README.md +28 -7
- package/bin/dsh-composer-ux.mjs +320 -0
- package/dsh-plugin.json +55 -0
- package/icon.png +0 -0
- package/lib/client.js +929 -776
- package/lib/client.js.map +4 -4
- package/lib/index.js +678 -190
- package/lib/index.js.map +4 -4
- package/package.json +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,130 @@
|
|
|
2
2
|
|
|
3
3
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## [0.22.0] — 2026-10-15(借鉴 dsh-ops:安全门控 / 运行期覆盖 / 自检 / 市场清单 / CLI + 适配 dsh 0.2.1-alpha.4)
|
|
6
|
+
|
|
7
|
+
### 适配 dsh 0.2.1-alpha.4(用户 2026-10-15:「更新网页版,把插件适配,测试没问题就发布」)
|
|
8
|
+
|
|
9
|
+
宿主从 0.2.1-alpha.2 升到 **0.2.1-alpha.4**(独立分支 `upgrade-0.2.1-alpha.4`,检出 + `pnpm install`
|
|
10
|
+
+ 全量 `npm run build`,构建 9 项全过)。形状核对 36 项对着 alpha.4 源码**全部通过**:
|
|
11
|
+
settings 三入口与 `document-updated` 事件、`volatile` 处理、`webServer.register`、沙箱
|
|
12
|
+
`confine(argv, policy, signal)`、目录流程槽位、tool-terminal 都在。alpha.4 对插件作者的两处新增:
|
|
13
|
+
`engines.dsh`(声明后**单独决定**兼容性、忽略 peerDependencies)与官方可选 bundle
|
|
14
|
+
`@deepseek-ai/dsh-experimental-terminal-bundle`。
|
|
15
|
+
|
|
16
|
+
**真机抓到并修掉的问题(这次适配的实质内容)**:`@deepseek-ai/dsh-tool-terminal` 声明的是
|
|
17
|
+
**精确版本 peer**。我插件自带的依赖范围被 pnpm 解析成 `0.2.1-alpha.3`(本地元数据缓存旧),
|
|
18
|
+
而运行时是 `alpha.4` ⇒ 宿主判定不兼容后**只是把那一行禁用**(启动日志一句 warning),
|
|
19
|
+
模型工具表里少 6 个 `terminal_*` 工具,界面上完全看不出来。修法两条:
|
|
20
|
+
|
|
21
|
+
1. **profile 层按运行时版本钉**(与桌面 profile 一直以来的做法一致):
|
|
22
|
+
`bin/dsh-composer-ux.mjs` 的 `install` 在装完插件后自动 `add @deepseek-ai/dsh-tool-terminal@<运行时版本>`;
|
|
23
|
+
2. **把这个静默失败变成可见**:设置页「自检」新增一行「持久终端工具」,
|
|
24
|
+
拿 tool-terminal 的 peer 要求与**运行时版本**(从进程入口往上找最近的 package.json —— 本仓锁步版本)
|
|
25
|
+
比对,不一致就红并给出修法。CLI 的 `status` 也打印同一份结论。
|
|
26
|
+
|
|
27
|
+
真机验证(alpha.4 web profile `a4web`):启动日志干净、自检 9 行全对(含「持久终端工具 — 已挂上;
|
|
28
|
+
tool-terminal 0.2.1-alpha.4(要求 0.2.1-alpha.4)」)、运行期请求头覆盖已挂上、旧写入清理已完成。
|
|
29
|
+
|
|
30
|
+
**刻意没做的**:不声明 `engines.dsh`。alpha.4 起它一旦声明就单独决定兼容性,而本插件**刻意**支持
|
|
31
|
+
0.1.6/0.1.7 的老设置形状(见下面「两代设置服务」),声明下限会把那些用户一刀切掉;
|
|
32
|
+
上限又会把"其实还能跑"的新运行时挡在门外。所以继续用"能力探测 + 失败方向安全"的老办法。
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
用户 2026-10-13:「第一梯队和第二梯队 除了英文化 其他都做,做完之后先测试,不要发布」。
|
|
37
|
+
来源是 [dsh-ops@0.2.9](https://github.com/T-Auto/dsh-ops) 的对比分析(见 `../dsh-ops-对比分析.md`)。
|
|
38
|
+
|
|
39
|
+
### 1. 安全:解释层「内置 Bash」按会话权限门控(这条其实是我这边的真问题)
|
|
40
|
+
|
|
41
|
+
它以前是 `node:child_process.spawn` **直接执行**、只锁 cwd —— 也就是**绕过会话策略**:
|
|
42
|
+
一个 `read-only` 会话里,只要用户开过这一栏,解释层就能跑任意命令。
|
|
43
|
+
现在(用户选定"接沙箱为主、门控兜底"):
|
|
44
|
+
|
|
45
|
+
- 会话是 `danger-full-access` ⇒ 直接跑(与宿主同判据);
|
|
46
|
+
- `read-only` / `workspace-write` ⇒ **必须**先过 `ctx.sandbox.confine(argv, policy)`,
|
|
47
|
+
且后端自报 `enforcement !== 'full'` 时**拒绝执行**("部分约束"不算边界);
|
|
48
|
+
- 拿不到权限模式、或没有沙箱服务 ⇒ **不发这个工具**(证明不了安全就不给),并在设置页/自检里说明。
|
|
49
|
+
|
|
50
|
+
### 2. 安全:请求头改成**运行期可逆覆盖**,不再写用户配置
|
|
51
|
+
|
|
52
|
+
以前「OpenCode 请求头」把 `providers.<route>.headers.<name>` **写进用户的模型路由配置**,
|
|
53
|
+
再靠记账 + 撤销回滚 —— 那是本插件唯一会动用户配置的功能。
|
|
54
|
+
现在照 dsh-ops 的路子:挂官方 loader 的 `internal/config` 事件,在**配置被解析时**补上字段,
|
|
55
|
+
从不落盘(官方 `llm-pi-ai` 自己也是这么挂的)。旧版本写进去的残留由迁移表**一次性**清掉,
|
|
56
|
+
且只删"值等于我们记账值"的那些(用户自己写的同名头绝不动)。
|
|
57
|
+
|
|
58
|
+
### 3. 新功能:自检(设置页一张卡 + `/composer-ux/doctor` 路由)
|
|
59
|
+
|
|
60
|
+
一行一句人话,把散在五处的状态摊平:设置字段是否都在宿主 schema(**不在 ⇒ 开关拨不动**)、
|
|
61
|
+
八栏开关、终端服务与 bash 探测、请求头覆盖与迁移、快捷指令本、通知凭据、解释层档位、运行环境。
|
|
62
|
+
真机首跑就抓出两条:覆盖状态读的是**陈旧值**(已修)、以及测试 profile 的「通知开着但缺凭据」。
|
|
63
|
+
|
|
64
|
+
### 4. 新功能:`command` XOR `script_path`(长脚本走文件)
|
|
65
|
+
|
|
66
|
+
长脚本、多行、含大量引号/反斜杠/中文的命令,先落文件再传路径 —— 避免在工具调用 JSON 里做多层转义
|
|
67
|
+
(一处漏了就整轮 `MALFORMED_RESPONSE` 作废,这是 dsh-ops 明确记下来的一条)。
|
|
68
|
+
|
|
69
|
+
### 5. 工程:版本化迁移表(`src/migrations.ts`)
|
|
70
|
+
|
|
71
|
+
以前迁移靠"文档里有没有这个键"这类判据,写在业务代码里。现在每条迁移一行、带版本号字段,
|
|
72
|
+
成功才写标记、失败下次重来、绝不覆盖用户后续的显式选择。
|
|
73
|
+
|
|
74
|
+
### 6. 展示:市场清单 `dsh-plugin.json` + 图标
|
|
75
|
+
|
|
76
|
+
`package.json` 加 `icon`(**宿主真的会读它**:`@deepseek-ai/dsh-package-manifest`),
|
|
77
|
+
并补一份带**权限及理由**披露的 `dsh-plugin.json`(当前宿主不消费它,属市场/生态展示口径)。
|
|
78
|
+
|
|
79
|
+
### 7. 新功能:CLI `dsh-composer-ux`
|
|
80
|
+
|
|
81
|
+
`status` / `doctor` / `install` / `uninstall`;**安装卸载一律委托官方 CLI**(探测桌面自带的 dsh.cmd,
|
|
82
|
+
不在 profile 上另搞一套锁与回滚)。运行时自检仍在设置页那张卡里(CLI 拿不到宿主服务)。
|
|
83
|
+
|
|
84
|
+
### 8. 体验:错误文案标准化
|
|
85
|
+
|
|
86
|
+
未知参数在执行前点名拒绝(同时列出可接受的键)、缺执行器给 `ENOENT` 说人话而不是裸 spawn 异常、
|
|
87
|
+
长命令被拒时直接指向 `script_path`。
|
|
88
|
+
|
|
89
|
+
### 顺手修掉的启动期问题
|
|
90
|
+
|
|
91
|
+
真机重启时又撞到一次 profile 写锁(这次是「终端状态写入」):把所有**启动期/迁移期**的写
|
|
92
|
+
统一走 `retryWrite`(抽成 `src/retry-write.ts`,终端子系统也用它)。
|
|
93
|
+
|
|
94
|
+
**验证**:`npm test` —— **31 个套件 2668 passed / 0 failed**(新增 `test/doctor.mjs` 32 条;
|
|
95
|
+
`host-header-mirror` 重写为"绝不写用户配置 + 运行期注入 + 一次性清理" 57 条;解释层 bash 的
|
|
96
|
+
权限组合四条;自检卡渲染 5 条)。真机(0.2.1-alpha.2 实例)验过:覆盖挂上、旧写入清理完成、
|
|
97
|
+
自检报告正确、启动日志干净。**未发布**(用户要求只测不发)。
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## [0.21.0] — 2026-10-10(「优化提示词」默认收缩:面板打开回到快捷指令那半 + 优化选项加折叠)
|
|
101
|
+
|
|
102
|
+
用户 2026-10-10(截图):「让优化提示词默认收缩起来」。确认后两处都改:
|
|
103
|
+
|
|
104
|
+
### 1. 打开面板回到默认那一半(不再被"上次的结果"顶过去)
|
|
105
|
+
|
|
106
|
+
- 旧规则:结果框"从无到有"就自动切到优化那半 —— 但**重开面板时上一轮的结果也算"从无到有"**,
|
|
107
|
+
于是每次打开都直接落在优化那半、外加一整组选项,把「优化完成」顶到看不见(截图里只剩一行)。
|
|
108
|
+
- 现在:每次**打开**面板 ⇒ 回到「快捷指令」那半,哪怕上一轮结果还摆着(打开那一刻就有的结果框算
|
|
109
|
+
**基线**,不抢视图;切换按钮写「上次的结果在这儿」)。
|
|
110
|
+
- 保留的:**面板已经开着**时结果框从无到有照旧自动切过去(点 ✨ 必须看得到结果,不能"点了没反应");
|
|
111
|
+
「**正在跑**」是打开时唯一的例外 —— 那时要能看见进度与「取消」。
|
|
112
|
+
- 点 ✨ 另外**明确**切一次那半:上一轮结果还在时它不是"从无到有",只靠那条规则会看起来没反应。
|
|
113
|
+
|
|
114
|
+
### 2. 「优化选项」加一层折叠,默认收起
|
|
115
|
+
|
|
116
|
+
- 收起时标题行右侧给一行当前值摘要(权限 · 上下文 · 只读工具 · Bash;指定过模型时才带模型),
|
|
117
|
+
不点开也看得见关键值;展开后与 0.14.0 的形态完全一致(七个控件 + 详情)。
|
|
118
|
+
- 卡片改成**受控**(`open` / `onToggle` 由面板给):折叠要在每次打开面板时复位,卡片只负责画。
|
|
119
|
+
|
|
120
|
+
### 验证
|
|
121
|
+
|
|
122
|
+
- `npm test`:30 套件 **2591 passed / 0 failed**。`panel-split-render` 40→55:
|
|
123
|
+
新增 §0(把"打开=默认""从无到有才切"这两条跨渲染规则抽成纯函数钉住 —— SSR 一次渲染测不到转换)、
|
|
124
|
+
§2 改成"有结果框也不抢视图"、§2c 直接渲卡片验收起/展开两态、§3 直接渲结果框验布局;
|
|
125
|
+
`client-registration` 271→276(旧的那条源码断言改写 + 5 条新契约)。
|
|
126
|
+
- `npm run test:mutations`:**195 条全部咬住**(EB 的 expect 跟着改,新增 HP–HU 5 条:
|
|
127
|
+
打开不回到默认、不复位折叠、折叠初值改展开、收起照样渲七个控件、摘要丢了、✨ 不明确切换)。
|
|
128
|
+
|
|
5
129
|
## [0.20.0] — 2026-10-10(适配 DSH 0.2.1-alpha.2:真机测出并修掉三处真问题)
|
|
6
130
|
|
|
7
131
|
用户 2026-10-10:把 `D:\DeepSeek Harness`(未封装版检出)升到最新(`dsh-v0.2.1-alpha.2`)并用它测插件。
|
package/README.md
CHANGED
|
@@ -50,6 +50,20 @@ DeepSeek Harness Web 输入体验增强插件:
|
|
|
50
50
|
2. **快捷指令按钮**:输入框工具行里、「展开」按钮左侧的胶囊按钮,点开是常备提示词清单 + 「优化提示词」。
|
|
51
51
|
3. **键位生效**(仅主聊天输入框):默认值 = 现状(Enter 发送、Shift+Enter 换行、Ctrl+Enter 加速提交),改动即时生效并持久保存。
|
|
52
52
|
|
|
53
|
+
## 0.22.0 新增(安全 / 自检 / CLI)
|
|
54
|
+
|
|
55
|
+
- **解释层「内置 Bash」按会话权限门控**:完全权限直跑;其它模式**必须**过 `ctx.sandbox.confine`,
|
|
56
|
+
约束不是 `full` 就拒绝执行;拿不到权限模式或没有沙箱服务 ⇒ **不发这个工具**。
|
|
57
|
+
- **「OpenCode 请求头」不再写你的配置**:改成挂官方 `internal/config` 事件做**运行期可逆覆盖**,
|
|
58
|
+
0.21.x 及以前写进去的残留由迁移表一次性清掉(只删值与记账一致的,你自己写的同名头不动)。
|
|
59
|
+
- **自检**:设置页多了「自检」卡(也可 `GET /composer-ux/doctor`),一行一句人话摊平
|
|
60
|
+
设置字段是否都在宿主 schema、八栏开关、终端服务与 bash 探测、请求头覆盖、快捷指令本、
|
|
61
|
+
通知凭据、解释层档位、运行环境。
|
|
62
|
+
- **CLI**:`npx dsh-composer-ux status|doctor|install|uninstall`(安装卸载一律委托官方 CLI)。
|
|
63
|
+
- **长命令走文件**:解释层的 bash 工具支持 `command` 与 `script_path` 二选一,
|
|
64
|
+
多行/长脚本落文件再传路径,免得在工具调用 JSON 里做多层转义。
|
|
65
|
+
- **市场清单**:`package.json` 的 `icon`(宿主会读)+ `dsh-plugin.json`(权限及理由披露)。
|
|
66
|
+
|
|
53
67
|
## 安装
|
|
54
68
|
|
|
55
69
|
```sh
|
|
@@ -143,10 +157,17 @@ dsh plugin --profile web add <你克隆或解压出来的目录>
|
|
|
143
157
|
- **0.12.0 起有记忆链**:你把成品插进输入框、改了几处、又点一次优化时,宿主会把**上一版成品**
|
|
144
158
|
一起给模型(截断 1500 字),它只需围绕变化点调整、不必从零重写(你已确认过的地方不会被改回去)。
|
|
145
159
|
不带的三种情况:同一段原文重跑(同文重试)、草稿就是上一轮成品(对成品重跑)、上一轮失败/取消;“重新优化”只在你手改过框里成品时才带。
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
160
|
+
- **面板分两半(0.13.1)**:中部现在是「**优化提示词**」(结果框 + 优化选项)与「**快捷指令**」(分类 + 条目列表)**二选一**,
|
|
161
|
+
中间一个切换按钮(▸ / ▾)换着看;标题行的**三档档位与 ✨ 按钮始终露在外面**
|
|
162
|
+
—— 所以「跑一次优化」永远是一步,不必先展开。**点 ✨ 会自己切到优化那半**(否则结果被藏在收起的一半里,
|
|
163
|
+
看起来像没反应);手动切回快捷指令不会被抢回去;点结果框的「✕」会顺手切回快捷指令。
|
|
164
|
+
- **默认收缩(0.21.0,用户 2026-10-10 的截图)**:每次**打开面板**都回到「快捷指令」那半,
|
|
165
|
+
哪怕上一轮的结果还摆着 —— 旧规则是"结果框从无到有就自动切过去",而重开面板时旧结果也算"从无到有",
|
|
166
|
+
于是每次打开都直接落在优化那半、外加一整组选项,把「优化完成」顶到看不见。
|
|
167
|
+
现在:打开那一刻就有的结果框算**基线**(不抢视图,切换按钮会写「上次的结果在这儿」);
|
|
168
|
+
**面板已经开着**时结果框从无到有照旧自动切过去;「**正在跑**」是打开时唯一的例外(要能看见进度与「取消」)。
|
|
169
|
+
同一张面板里的「**优化选项**」也加了一层折叠、**默认收起** —— 收起时标题行右侧给一行当前值摘要
|
|
170
|
+
(权限 / 上下文 / 只读工具 / Bash,指定过模型时才带模型),不点开也看得见关键值。
|
|
150
171
|
- **修掉「展开后看不到按钮」(0.13.1)**:以前结果框和快捷指令列表**挤同一块固定高度**,结果框一长
|
|
151
172
|
(条目多 + 成品多行)就把列表挤没、再把自己底部的**「插入输入框 / 重新优化 / 复制」顶出面板边界被裁掉**。
|
|
152
173
|
现在两半互斥、各自内部滚,而且结果框拆成「可滚内容区 + **钉在底部的按钮行**」—— 面板再矮也不裁那排按钮。
|
|
@@ -553,7 +574,7 @@ Windows 上 DSH 给模型的终端工具是 **PowerShell**(工具名 `pwsh`)
|
|
|
553
574
|
| 工具循环的消息形状 / 轮次封顶 / 异常必须降级 | 同上第 6 节(假 llm 驱动整条循环,逐条检查 `role:'tool'` 与 `toolCallId`) |
|
|
554
575
|
| 工具路径:默认关、开了才派、**查完吐散文必须回落**、拿不到工作目录就不派 | `test/quick-commands.mjs` 5g 节(13 条,真路由 + 假 llm) |
|
|
555
576
|
| 工具读到的内容不进台账 | 同 5g 节:断言台账里只有工具名与次数、没有读到的文件内容 |
|
|
556
|
-
| 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations
|
|
577
|
+
| 变异守卫真的会咬人,且**必须是指定的那条测试变红** | `npm run test:mutations`:**(205 条全部咬住)**(判据是 `red.some(line => line.includes(item.expect))`,不是"有红就行") |
|
|
557
578
|
| **桌面应用里弹系统文件夹框**(0.18.0 起;0.19.0 起默认关、设置页可开) | ✅ **有真机证据**:2026-10-09 用户在桌面应用点「选择工作区目录」,实测「成功替换为系统文件管理器」(先用 profile 补丁验过一次,之后的插件内 shadow 版走的是同一条 preload 桥);`node test/directory-flow.mjs` 49 条 + 变异 FS–FY(判定/状态机)与 GZ/HA(开关幂等装卸)罩着 |
|
|
558
579
|
| **设置字段全部进了宿主 schema**(0.19.2 起) | `test/client-registration.mjs` 第 16 节:把设置契约里 49 个 `*_FIELD` 常量逐个核对 `ownSchema()`,再点名 6 个(5 个通知 + 目录选择)+ 变异 HG/HH。**为什么值得单列一行**:写设置走 `settings.mutate`,字段没进 schema 会被直接抛 `Config field "…" is not volatile` —— 0.17.0 的 5 个通知字段就是这么漏的(症状是"开关拨不动 + 写入失败提示"),2026-10-09 才发现并修掉 |
|
|
559
580
|
| 发布门禁三条判据 | `npm run gates` 全过;`check:tag` 实测 `v0.12.0` 的 tag 名与所指提交里的版本一致 |
|
|
@@ -690,9 +711,9 @@ node build.mjs # 产出 lib/index.js + lib/client.js
|
|
|
690
711
|
# ⚠️ 宿主半会**内联** schemastery / cosmokit:
|
|
691
712
|
# 优先用 DSH 检出里的 vendor 副本,检出不在时退到 node_modules
|
|
692
713
|
# 里同版本的 npm 包(两者逐字节相同)——CI 上走的就是退路
|
|
693
|
-
npm test #
|
|
714
|
+
npm test # 31 个套件;当前 2676 passed, 0 failed(2026-10-09 实测;CI 三平台同样全绿)
|
|
694
715
|
# 走 scripts/run-tests.mjs:顺带把"多少套件/多少条"记进 test/.last-run.json
|
|
695
|
-
npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(
|
|
716
|
+
npm run test:mutations # 手动跑:变异测试,证明那套护栏真的在咬人(206 条,须单独跑)
|
|
696
717
|
# 同样记录结果,供下面的文档门禁核对
|
|
697
718
|
npm run gates # 发版门禁三条一起跑:tag 指向 / 包内容 / 文档数字
|
|
698
719
|
npm run check:tag # ① tag 名里的版本 == 该 tag 所指提交里的 package.json 版本
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `dsh-composer-ux` 命令行(0.22.0)。
|
|
4
|
+
*
|
|
5
|
+
* 为什么要有它(借鉴 dsh-ops 的 install/status/uninstall):
|
|
6
|
+
* · 安装:官方 CLI 的用法与"保留 profile"的坑(桌面端得用应用自带的那个 CLI)对新手不友好,
|
|
7
|
+
* 这里把探测与提示做掉,**但绝不自己实现一套 profile 锁与回滚** —— 一律委托官方 CLI;
|
|
8
|
+
* · 排障:`status` / `doctor` 把"装到哪了、版本对不对、依赖在不在"直接打出来。
|
|
9
|
+
* 更完整的运行时报告(设置字段、终端替换、请求头覆盖…)在设置页那张「自检」卡里 ——
|
|
10
|
+
* 那需要跑在宿主进程里读服务,CLI 拿不到。
|
|
11
|
+
*
|
|
12
|
+
* 用法:
|
|
13
|
+
* npx dsh-composer-ux status [--profile desktop]
|
|
14
|
+
* npx dsh-composer-ux doctor [--profile desktop] [--dsh-cli <path>]
|
|
15
|
+
* npx dsh-composer-ux install [--profile desktop] [--dsh-cli <path>] [--version 0.22.0]
|
|
16
|
+
* npx dsh-composer-ux uninstall [--profile desktop] [--dsh-cli <path>]
|
|
17
|
+
*/
|
|
18
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { dirname, join, resolve } from 'node:path'
|
|
21
|
+
import { fileURLToPath } from 'node:url'
|
|
22
|
+
import { spawnSync } from 'node:child_process'
|
|
23
|
+
|
|
24
|
+
const PACKAGE = 'dsh-composer-ux'
|
|
25
|
+
const SELF = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
26
|
+
const own = JSON.parse(readFileSync(join(SELF, 'package.json'), 'utf8'))
|
|
27
|
+
|
|
28
|
+
/** 解析 `--flag value` 风格的参数(不做花哨的短选项)。 */
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const out = { _: [] }
|
|
31
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
32
|
+
const token = argv[i]
|
|
33
|
+
if (token.startsWith('--')) {
|
|
34
|
+
const key = token.slice(2)
|
|
35
|
+
const next = argv[i + 1]
|
|
36
|
+
if (next === undefined || next.startsWith('--')) out[key] = true
|
|
37
|
+
else { out[key] = next; i += 1 }
|
|
38
|
+
} else out._.push(token)
|
|
39
|
+
}
|
|
40
|
+
return out
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const dshHome = () => (typeof process.env.DSH_HOME === 'string' && process.env.DSH_HOME !== ''
|
|
44
|
+
? process.env.DSH_HOME
|
|
45
|
+
: join(homedir(), '.dsh'))
|
|
46
|
+
|
|
47
|
+
/** 列出所有 profile 及其是否装了本插件。 */
|
|
48
|
+
function listProfiles() {
|
|
49
|
+
const root = join(dshHome(), 'profiles')
|
|
50
|
+
const rows = []
|
|
51
|
+
let names = []
|
|
52
|
+
try {
|
|
53
|
+
names = readdirSync(root, { withFileTypes: true }).filter(entry => entry.isDirectory()).map(entry => entry.name)
|
|
54
|
+
} catch {
|
|
55
|
+
return rows
|
|
56
|
+
}
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
const dir = join(root, name)
|
|
59
|
+
const manifest = join(dir, 'package.json')
|
|
60
|
+
let declared = false
|
|
61
|
+
let installed = ''
|
|
62
|
+
try {
|
|
63
|
+
const parsed = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
64
|
+
declared = parsed?.dependencies?.[PACKAGE] !== undefined
|
|
65
|
+
|| (parsed?.dsh?.profile?.bundles ?? []).includes(PACKAGE)
|
|
66
|
+
} catch { /* 没有 package.json 就当作没装 */ }
|
|
67
|
+
const versionFile = join(dir, 'node_modules', PACKAGE, 'package.json')
|
|
68
|
+
if (existsSync(versionFile)) {
|
|
69
|
+
try { installed = JSON.parse(readFileSync(versionFile, 'utf8')).version ?? '' } catch { installed = '' }
|
|
70
|
+
}
|
|
71
|
+
if (declared || installed !== '') rows.push({ name, dir, declared, installed })
|
|
72
|
+
}
|
|
73
|
+
return rows
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 找官方 CLI。
|
|
78
|
+
*
|
|
79
|
+
* 桌面端必须用它自带的那个(普通 CLI 管不了保留的 `desktop` profile)——这与 dsh-ops 的说明一致。
|
|
80
|
+
*/
|
|
81
|
+
function findDshCli(explicit) {
|
|
82
|
+
const candidates = []
|
|
83
|
+
if (typeof explicit === 'string') candidates.push(explicit)
|
|
84
|
+
if (typeof process.env.DSH_CLI === 'string' && process.env.DSH_CLI !== '') candidates.push(process.env.DSH_CLI)
|
|
85
|
+
const localAppData = process.env.LOCALAPPDATA ?? ''
|
|
86
|
+
if (localAppData !== '') {
|
|
87
|
+
candidates.push(join(localAppData, 'Programs', 'DeepSeek Harness', 'resources', 'runtime', 'cli', 'bin', 'dsh.cmd'))
|
|
88
|
+
}
|
|
89
|
+
for (const base of ['C:/Program Files/DeepSeek Harness', 'D:/deepseekharness', 'D:/DeepSeek Harness']) {
|
|
90
|
+
candidates.push(join(base, 'resources', 'runtime', 'cli', 'bin', 'dsh.cmd'))
|
|
91
|
+
candidates.push(join(base, 'apps', 'cli', 'lib', 'bin.js'))
|
|
92
|
+
}
|
|
93
|
+
candidates.push('dsh')
|
|
94
|
+
for (const candidate of candidates) {
|
|
95
|
+
if (candidate === 'dsh') return { cli: candidate, how: 'PATH 上的 dsh' }
|
|
96
|
+
if (existsSync(candidate)) return { cli: candidate, how: candidate }
|
|
97
|
+
}
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** 跑官方 CLI(一律委托,不自己动 profile)。 */
|
|
102
|
+
function runOfficial(cli, args, options) {
|
|
103
|
+
const isNodeScript = cli.endsWith('.js')
|
|
104
|
+
const command = isNodeScript ? process.execPath : cli
|
|
105
|
+
const argv = isNodeScript ? [cli, ...args] : args
|
|
106
|
+
console.log(`> ${command} ${argv.join(' ')}`)
|
|
107
|
+
// Windows 上的 .cmd/.bat 包装器必须经 shell 才能起(否则 spawnSync 直接 ENOENT)。
|
|
108
|
+
const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(command)
|
|
109
|
+
const result = spawnSync(command, argv, {
|
|
110
|
+
stdio: 'inherit',
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
...(needsShell ? { shell: true } : {}),
|
|
113
|
+
...options,
|
|
114
|
+
})
|
|
115
|
+
return result.status ?? 1
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function cmdStatus(args) {
|
|
119
|
+
console.log(`${PACKAGE} ${own.version}(本机 CLI)`)
|
|
120
|
+
console.log(`DSH_HOME: ${dshHome()}`)
|
|
121
|
+
const profiles = listProfiles()
|
|
122
|
+
const only = typeof args.profile === 'string' ? args.profile : undefined
|
|
123
|
+
const rows = only === undefined ? profiles : profiles.filter(row => row.name === only)
|
|
124
|
+
const cliForStatus = findDshCli(args['dsh-cli'])
|
|
125
|
+
const runtimeForStatus = cliForStatus === undefined ? '' : dshRuntimeVersion(cliForStatus.cli)
|
|
126
|
+
if (rows.length === 0) {
|
|
127
|
+
console.log(only === undefined ? '没有找到任何装过本插件的 profile。' : `profile ${only} 里没装本插件。`)
|
|
128
|
+
}
|
|
129
|
+
for (const row of rows) {
|
|
130
|
+
console.log(`· profile ${row.name}:${row.declared ? '已在 package.json 里声明' : '只在 node_modules 里'};装上的是 ${row.installed === '' ? '(读不到版本)' : row.installed}`)
|
|
131
|
+
const term = toolTerminalState(row.dir, runtimeForStatus)
|
|
132
|
+
if (term.toolVersion === '') {
|
|
133
|
+
console.log(' · 持久终端:profile 里没有 @deepseek-ai/dsh-tool-terminal(模型那 6 个 terminal_* 工具会缺)')
|
|
134
|
+
} else if (term.mismatch) {
|
|
135
|
+
console.log(` · ❗持久终端:tool-terminal ${term.toolVersion} 要求 ${term.peerRequired},profile 里实际 ${term.peerInstalled} ⇒ 宿主会禁用那一行`)
|
|
136
|
+
console.log(` 修法:dsh plugin --profile ${row.name} add @deepseek-ai/dsh-tool-terminal@<运行时版本>`)
|
|
137
|
+
} else if (runtimeForStatus !== '' && term.toolVersion !== runtimeForStatus) {
|
|
138
|
+
// 只是提示:这个 profile 可能属于**另一个安装**(例如桌面应用的 profile),那就正常。
|
|
139
|
+
console.log(` · 持久终端:tool-terminal ${term.toolVersion};与本次所用 CLI 的运行时 ${runtimeForStatus} 不同`)
|
|
140
|
+
console.log(' (若该 profile 属于另一个安装,这是正常的;要确认就换成那个安装的 CLI 再跑一次 —— 宿主自检那张卡会直接说)')
|
|
141
|
+
} else {
|
|
142
|
+
console.log(` · 持久终端:tool-terminal ${term.toolVersion}(要求 ${term.peerRequired || '无'},profile 里 ${term.peerInstalled || '读不到'})`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
console.log(`官方 CLI:${cliForStatus === undefined ? '没找到(安装/卸载时用 --dsh-cli 指定)' : cliForStatus.how}`)
|
|
146
|
+
console.log(`运行时版本:${runtimeForStatus || '(读不到)'}`)
|
|
147
|
+
return 0
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 读一个 profile 里「持久终端三件套」的实际状态。
|
|
152
|
+
*
|
|
153
|
+
* 为什么 CLI 也要管这件事(2026-10-15 真机踩到):`@deepseek-ai/dsh-tool-terminal` 声明的是
|
|
154
|
+
* **精确版本 peer**。若 profile 里装的版本与运行时对不上,宿主只会把那一行禁用 + 启动日志一句
|
|
155
|
+
* warning,模型工具表里少 6 个 terminal_* 工具,界面上完全看不出来。所以安装时按运行时版本钉住它,
|
|
156
|
+
* 并在 status/doctor 里核对(判据与宿主半自检那张卡一致:拿 peer 要求比 profile 里实际装的版本)。
|
|
157
|
+
*/
|
|
158
|
+
function toolTerminalState(profileDir, runtimeVersion) {
|
|
159
|
+
const read = name => {
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(readFileSync(join(profileDir, 'node_modules', ...name.split('/'), 'package.json'), 'utf8'))
|
|
162
|
+
} catch {
|
|
163
|
+
return undefined
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const tool = read('@deepseek-ai/dsh-tool-terminal')
|
|
167
|
+
const peers = tool?.peerDependencies ?? {}
|
|
168
|
+
const peerName = Object.keys(peers).find(key => key.startsWith('@deepseek-ai/dsh-')) ?? ''
|
|
169
|
+
const required = peerName === '' ? '' : String(peers[peerName])
|
|
170
|
+
/**
|
|
171
|
+
* ⚠️ 只用 **profile 自己**能证明的东西判"不匹配"。
|
|
172
|
+
*
|
|
173
|
+
* 为什么不直接和运行时版本比:一个 profile 属于哪个安装(桌面应用 / 源码检出 / 另一个版本)
|
|
174
|
+
* 从 profile 目录本身看不出来 —— 2026-10-15 试过,拿检出的 alpha.4 去比桌面 profile 立刻是假阳性
|
|
175
|
+
* (桌面 profile 的 tool-terminal 是 0.2.0-rc.2,跟它自己的运行时才是对的)。
|
|
176
|
+
* 所以:profile 里能读到 peer 实际版本时才算"证实的越界";否则只输出提示(见调用方)。
|
|
177
|
+
*/
|
|
178
|
+
const installed = required === '' ? '' : String(read(peerName)?.version ?? '')
|
|
179
|
+
void runtimeVersion
|
|
180
|
+
return {
|
|
181
|
+
toolVersion: String(tool?.version ?? ''),
|
|
182
|
+
peerRequired: required,
|
|
183
|
+
peerInstalled: installed,
|
|
184
|
+
mismatch: required !== '' && installed !== '' && required !== installed,
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 问官方 CLI 要运行时版本(用于把 tool-terminal 钉到同一版)。 */
|
|
189
|
+
function dshRuntimeVersion(cli) {
|
|
190
|
+
const isNodeScript = cli.endsWith('.js')
|
|
191
|
+
const command = isNodeScript ? process.execPath : cli
|
|
192
|
+
const argv = isNodeScript ? [cli, '--version'] : ['--version']
|
|
193
|
+
try {
|
|
194
|
+
const needsShell = process.platform === 'win32' && /\.(cmd|bat)$/i.test(command)
|
|
195
|
+
const out = spawnSync(command, argv, {
|
|
196
|
+
encoding: 'utf8',
|
|
197
|
+
windowsHide: true,
|
|
198
|
+
...(needsShell ? { shell: true } : {}),
|
|
199
|
+
})
|
|
200
|
+
const match = /\d+\.\d+\.\d+[-\w.]*/.exec(String(out.stdout ?? '').trim())
|
|
201
|
+
return match === null ? '' : match[0]
|
|
202
|
+
} catch {
|
|
203
|
+
return ''
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cmdDoctor(args) {
|
|
208
|
+
let bad = 0
|
|
209
|
+
const report = (ok, label, detail) => {
|
|
210
|
+
console.log(`${ok === true ? '✓' : ok === 'warn' ? '!' : '✗'} ${label}${detail === undefined ? '' : ` — ${detail}`}`)
|
|
211
|
+
if (ok === false) bad += 1
|
|
212
|
+
}
|
|
213
|
+
report(true, '本机 CLI 版本', own.version)
|
|
214
|
+
const manifestPath = join(SELF, 'dsh-plugin.json')
|
|
215
|
+
if (existsSync(manifestPath)) {
|
|
216
|
+
try {
|
|
217
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
218
|
+
report(manifest.version === own.version, '市场清单版本与包版本一致',
|
|
219
|
+
`${String(manifest.version)} vs ${own.version}`)
|
|
220
|
+
} catch (error) {
|
|
221
|
+
report(false, '市场清单可解析', String(error))
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
report('warn', '市场清单', '没有 dsh-plugin.json(不影响运行)')
|
|
225
|
+
}
|
|
226
|
+
report(existsSync(join(SELF, 'lib', 'index.js')), '宿主半产物在', 'lib/index.js')
|
|
227
|
+
report(existsSync(join(SELF, 'lib', 'client.js')), '客户端半产物在', 'lib/client.js')
|
|
228
|
+
report(existsSync(join(SELF, 'cordis.patch.yml')), '组合补丁在', 'cordis.patch.yml')
|
|
229
|
+
const cli = findDshCli(args['dsh-cli'])
|
|
230
|
+
report(cli !== undefined, '找得到官方 CLI', cli === undefined ? '用 --dsh-cli 指定' : cli.how)
|
|
231
|
+
const profiles = listProfiles()
|
|
232
|
+
report(profiles.length > 0 ? true : 'warn', '装到哪个 profile',
|
|
233
|
+
profiles.length === 0 ? '没有任何 profile 装了它' : profiles.map(row => `${row.name}@${row.installed === '' ? '?' : row.installed}`).join('、'))
|
|
234
|
+
report('warn', '更完整的运行时自检', '在 DSH 的「设置 → 输入体验 → 自检」里点「运行自检」(需要宿主进程)')
|
|
235
|
+
console.log(bad === 0 ? '\n结论:本机安装看起来是好的。' : `\n结论:有 ${String(bad)} 项需要处理。`)
|
|
236
|
+
return bad === 0 ? 0 : 1
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function cmdInstall(args) {
|
|
240
|
+
const profile = typeof args.profile === 'string' ? args.profile : ''
|
|
241
|
+
if (profile === '') {
|
|
242
|
+
console.error('必须显式指定 profile:--profile desktop(或 web / 其它已初始化的 profile)')
|
|
243
|
+
console.error('桌面端要用桌面应用自带的 CLI;本机若探测不到,用 --dsh-cli <path> 指定。')
|
|
244
|
+
return 2
|
|
245
|
+
}
|
|
246
|
+
const cli = findDshCli(args['dsh-cli'])
|
|
247
|
+
if (cli === undefined) {
|
|
248
|
+
console.error('找不到官方 DSH CLI。用 --dsh-cli "<安装目录>/resources/runtime/cli/bin/dsh.cmd" 指定。')
|
|
249
|
+
return 2
|
|
250
|
+
}
|
|
251
|
+
if (profile === 'desktop') {
|
|
252
|
+
console.log('提示:`desktop` 是保留 profile,普通 CLI 管不了它;这里用的是探测到的官方 CLI(桌面自带那个)。')
|
|
253
|
+
}
|
|
254
|
+
const spec = typeof args.version === 'string' ? `${PACKAGE}@${args.version}` : `${PACKAGE}@${own.version}`
|
|
255
|
+
const first = runOfficial(cli.cli, ['plugin', '--profile', profile, 'add', spec])
|
|
256
|
+
if (first !== 0) return first
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 顺手把持久终端那件「必须与运行时同版」的包按运行时版本钉进 profile。
|
|
260
|
+
*
|
|
261
|
+
* 为什么不用插件自己的依赖范围拉(2026-10-15 真机踩到):范围会被 pnpm 解析成"当前发布的最新
|
|
262
|
+
* alpha",一旦比运行时新(或本地元数据缓存旧而选了旧版),宿主就判定不兼容并把那一行禁掉 ——
|
|
263
|
+
* 模型少 6 个工具,界面无感。profile 层显式钉版本才是确定的(桌面 profile 一直是这么做的)。
|
|
264
|
+
*/
|
|
265
|
+
const runtime = typeof args['dsh-version'] === 'string' ? args['dsh-version'] : dshRuntimeVersion(cli.cli)
|
|
266
|
+
if (runtime === '') {
|
|
267
|
+
console.log('提示:读不到运行时版本,跳过持久终端的版本对齐(可用 --dsh-version <版本> 指定)。')
|
|
268
|
+
return 0
|
|
269
|
+
}
|
|
270
|
+
const pinned = runOfficial(cli.cli, [
|
|
271
|
+
'plugin', '--profile', profile, 'add', `@deepseek-ai/dsh-tool-terminal@${runtime}`,
|
|
272
|
+
])
|
|
273
|
+
if (pinned !== 0) {
|
|
274
|
+
console.log(`提示:@deepseek-ai/dsh-tool-terminal@${runtime} 没装上;模型那 6 个 terminal_* 工具会缺(设置页「自检」会显示)。`)
|
|
275
|
+
}
|
|
276
|
+
return 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function cmdUninstall(args) {
|
|
280
|
+
const profile = typeof args.profile === 'string' ? args.profile : ''
|
|
281
|
+
if (profile === '') {
|
|
282
|
+
console.error('必须显式指定 profile:--profile desktop')
|
|
283
|
+
return 2
|
|
284
|
+
}
|
|
285
|
+
const cli = findDshCli(args['dsh-cli'])
|
|
286
|
+
if (cli === undefined) {
|
|
287
|
+
console.error('找不到官方 DSH CLI。用 --dsh-cli <path> 指定。')
|
|
288
|
+
return 2
|
|
289
|
+
}
|
|
290
|
+
return runOfficial(cli.cli, ['plugin', '--profile', profile, 'remove', PACKAGE])
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function usage() {
|
|
294
|
+
console.log(`dsh-composer-ux ${own.version}
|
|
295
|
+
|
|
296
|
+
status [--profile <name>] [--dsh-cli <path>] 看装在哪、装的什么版本
|
|
297
|
+
doctor [--profile <name>] [--dsh-cli <path>] 本机自检(产物、清单、官方 CLI、profile)
|
|
298
|
+
install --profile <name> [--dsh-cli <path>] [--version <v>]
|
|
299
|
+
uninstall --profile <name> [--dsh-cli <path>]
|
|
300
|
+
help
|
|
301
|
+
|
|
302
|
+
安装与卸载一律**委托官方 DSH CLI**(不自己动 profile);运行时自检在
|
|
303
|
+
DSH 的「设置 → 输入体验 → 自检」里那张卡(它需要跑在宿主进程里读服务)。`)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const argv = process.argv.slice(2)
|
|
307
|
+
const args = parseArgs(argv)
|
|
308
|
+
const command = args._[0] ?? 'help'
|
|
309
|
+
let code = 0
|
|
310
|
+
if (command === 'status') code = cmdStatus(args)
|
|
311
|
+
else if (command === 'doctor') code = cmdDoctor(args)
|
|
312
|
+
else if (command === 'install') code = cmdInstall(args)
|
|
313
|
+
else if (command === 'uninstall') code = cmdUninstall(args)
|
|
314
|
+
else if (command === 'help' || command === '--help' || command === '-h') usage()
|
|
315
|
+
else {
|
|
316
|
+
console.error(`不认识的子命令:${command}`)
|
|
317
|
+
usage()
|
|
318
|
+
code = 2
|
|
319
|
+
}
|
|
320
|
+
process.exit(code)
|
package/dsh-plugin.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/T-Auto/dsh-std/8a15541117cb3c13ddb8f2468308691b6051d162/packages/manifest/schema/dsh-plugin-0.15.schema.json",
|
|
3
|
+
"manifestVersion": "0.15",
|
|
4
|
+
"id": "dsh-composer-ux.input-experience",
|
|
5
|
+
"name": "dsh-composer-ux",
|
|
6
|
+
"version": "0.22.0",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"facets": {
|
|
9
|
+
"host": {
|
|
10
|
+
"entry": "lib/index.js",
|
|
11
|
+
"apiVersion": "v1alpha1"
|
|
12
|
+
},
|
|
13
|
+
"client": {
|
|
14
|
+
"entry": "lib/client.js",
|
|
15
|
+
"platform": "web"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"requires": {
|
|
19
|
+
"contracts": []
|
|
20
|
+
},
|
|
21
|
+
"permissions": [
|
|
22
|
+
{
|
|
23
|
+
"name": "dsh-composer-ux.fs.read",
|
|
24
|
+
"scope": "dsh-home",
|
|
25
|
+
"reason": "Read the plugin's own files under <DSH_HOME>: quick-prompts.json, optimizer state, and the profile's settings document to resolve this plugin's config."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "dsh-composer-ux.fs.write",
|
|
29
|
+
"scope": "dsh-home",
|
|
30
|
+
"reason": "Write only the plugin's own files under <DSH_HOME>: quick-prompts.json, optimizer dock state, and this plugin's own settings namespace. It never writes another plugin's config (the 0.21 request-header write path was replaced by a runtime overlay in 0.22.0)."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "dsh-composer-ux.process.spawn",
|
|
34
|
+
"scope": "subprocess",
|
|
35
|
+
"reason": "Start the DSH restart helper when the user asks to restart, probe the local Git Bash path for the terminal swap, and (only when the user enables it) run the optimizer's fact-checking shell tool confined by ctx.sandbox.confine or limited to danger-full-access sessions."
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"contributes": {
|
|
39
|
+
"x-display": [
|
|
40
|
+
{
|
|
41
|
+
"icon": "./icon.png",
|
|
42
|
+
"title": "dsh-composer-ux",
|
|
43
|
+
"description": "输入体验增强:键位、右键菜单、快捷指令、提示词优化、通知、统计与成本、目录选择、终端策略(八栏,逐栏开关)"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"x-prompt-sections": [
|
|
47
|
+
{
|
|
48
|
+
"name": "dsh-composer-ux:optimizer",
|
|
49
|
+
"order": 4200,
|
|
50
|
+
"summary": "提示词优化器只在用户点「✨ 优化提示词」时组装的一段调用说明;不写 AGENTS.md,设置里关掉即不注入。"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"subscriptions": []
|
|
55
|
+
}
|
package/icon.png
ADDED
|
Binary file
|