dsh-custom-mode 1.11.0 → 1.11.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/README.i18n.yaml +2 -2
- package/README.md +13 -3
- package/README.zh.md +10 -3
- package/client.js +37 -30
- package/index.mjs +71 -6
- package/locales.mjs +19 -17
- package/package.json +1 -1
- package/preset/preset.yml +4 -1
- package/seed.mjs +52 -0
package/README.i18n.yaml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# 双语配对一致性记录:两侧在「上次确认一致」时的 git blob 哈希。
|
|
2
2
|
# 两份文档权威相同——改完任意一侧,请把另一侧也改掉,然后重新记录:
|
|
3
3
|
# node tools/verify-translation-pairing.mjs --write
|
|
4
|
-
README.md:
|
|
5
|
-
README.zh.md:
|
|
4
|
+
README.md: 31ff076c2ce3710b0e4597e8091d6edc74e5ecb8
|
|
5
|
+
README.zh.md: 23c1e72f1c796320950b5eead938967c8ec6f062
|
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ One command installs everything — the settings-page plugin, and the preset it
|
|
|
59
59
|
activation:
|
|
60
60
|
|
|
61
61
|
```sh
|
|
62
|
-
dsh plugin --profile web add dsh-custom-mode@1.11.
|
|
62
|
+
dsh plugin --profile web add dsh-custom-mode@1.11.2 # pin the version to get this one for sure
|
|
63
63
|
# A bare `add dsh-custom-mode` is subject to pnpm's release cooldown (`minimumReleaseAge`, 1 day by
|
|
64
64
|
# default): for hours after a release it can silently install an OLDER version — measured: a bare
|
|
65
65
|
# install 38 minutes after 1.3.0 shipped landed on 1.0.3. Check what you got with `npm ls
|
|
@@ -107,7 +107,8 @@ Electron shell**, so this plugin's page renders there unchanged. One install pat
|
|
|
107
107
|
|
|
108
108
|
- **Install from inside the app**: sidebar → **Plugins** → add plugin → search `dsh-custom-mode`.
|
|
109
109
|
The app runs the shared plugin manager with its own bundled pnpm.
|
|
110
|
-
- **Do not try to use
|
|
110
|
+
- **Do not try to use a standalone CLI for it — dsh itself refuses** (measured on 0.1.7-rc.2, and again on
|
|
111
|
+
0.2.0-rc.2):
|
|
111
112
|
```
|
|
112
113
|
$ dsh plugin --profile desktop add .
|
|
113
114
|
error: profile "desktop" is managed exclusively by the Electron application
|
|
@@ -116,8 +117,17 @@ Electron shell**, so this plugin's page renders there unchanged. One install pat
|
|
|
116
117
|
and startup recovery renames `cordis.patch.yml`. `install.sh` therefore **refuses** `--profile desktop`
|
|
117
118
|
too and points you at the app, so the two agree instead of one silently working around the other.
|
|
118
119
|
(A lab simulation of the profile shape can override it with `DSH_ALLOW_DESKTOP_PROFILE=1`.)
|
|
120
|
+
**The app's own bundled CLI is the exception** — `<install>\resources\runtime\cli\bin\dsh.cmd` is allowed
|
|
121
|
+
to operate on that profile (measured on 1.11.1: `add dsh-custom-mode@1.11.2` → `+ dsh-custom-mode 1.11.1`).
|
|
122
|
+
It is a launcher, not a dsh feature: it starts Electron with `ELECTRON_RUN_AS_NODE=1` and runs the
|
|
123
|
+
desktop-host CLI, so it is the app talking to its own profile rather than something driving from outside.
|
|
119
124
|
- **Nothing else to do on the preset side**: `$DSH_HOME/.agent-presets/` is product data shared by the
|
|
120
125
|
desktop app and the CLI, and the preset is seeded by the plugin on first activation.
|
|
126
|
+
- **If the mode does not show up in the picker, open Settings → Custom mode and read the warning.** A preset
|
|
127
|
+
whose rows cannot start on that machine is marked *broken* by the platform and dropped from every picker;
|
|
128
|
+
the settings page names the rows and offers **Fix for this line**. On the desktop line that can take one
|
|
129
|
+
click and then a **restart of the app** — the picker re-reads the registry only at boot (measured on
|
|
130
|
+
1.11.1, see `docs/MEASUREMENTS.md` section 34).
|
|
121
131
|
- If a third-party bundle ever keeps the app from starting, the native recovery dialog offers
|
|
122
132
|
**Disable third-party plugins**; installed packages and the plugin's own data stay on disk.
|
|
123
133
|
|
|
@@ -420,7 +430,7 @@ installing again — your data is not touched:
|
|
|
420
430
|
|
|
421
431
|
```sh
|
|
422
432
|
# the pinned form: what you ask for is what you get
|
|
423
|
-
dsh plugin --profile web add dsh-custom-mode@1.11.
|
|
433
|
+
dsh plugin --profile web add dsh-custom-mode@1.11.2
|
|
424
434
|
# then restart the DSH process that serves the web profile
|
|
425
435
|
```
|
|
426
436
|
|
package/README.zh.md
CHANGED
|
@@ -48,7 +48,7 @@ base mode / plugin switches / multi-assistant / multi-persona。
|
|
|
48
48
|
一条命令装完——设置页插件,以及它在首次激活时自动播种的 preset:
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
|
-
dsh plugin --profile web add dsh-custom-mode@1.11.
|
|
51
|
+
dsh plugin --profile web add dsh-custom-mode@1.11.2 # 钉版本才能确定拿到这一版
|
|
52
52
|
# 不带版本号会受 pnpm 的发布冷却期影响(`minimumReleaseAge`,默认一天):发布后数小时内按名安装
|
|
53
53
|
# 可能**静默装到旧版** —— 实测 1.3.0 发布 38 分钟后按名安装装到了 1.0.3。用 profile 里的
|
|
54
54
|
# `npm ls dsh-custom-mode` 核对实际装到的版本,或像上面那样钉版本。
|
|
@@ -89,7 +89,7 @@ dsh `0.1.6-alpha.2` 起有插件管理页:**侧边栏 → 插件 → 添加插
|
|
|
89
89
|
安装只有一条路,就是上面那条:
|
|
90
90
|
|
|
91
91
|
- **在应用里装**:侧栏 → **插件** → 添加插件 → 搜 `dsh-custom-mode`。应用自带 pnpm,走的是同一套插件管理器。
|
|
92
|
-
-
|
|
92
|
+
- **不要试着用独立的 CLI 装 —— dsh 自己就会拒绝**(0.1.7-rc.2 实测,0.2.0-rc.2 复测同样):
|
|
93
93
|
```
|
|
94
94
|
$ dsh plugin --profile desktop add .
|
|
95
95
|
error: profile "desktop" is managed exclusively by the Electron application
|
|
@@ -97,8 +97,15 @@ dsh `0.1.6-alpha.2` 起有插件管理页:**侧边栏 → 插件 → 添加插
|
|
|
97
97
|
桌面端独占 `$DSH_HOME/profiles/desktop`:那里的包操作持有 profile 事务锁,启动恢复还会重命名
|
|
98
98
|
`cordis.patch.yml`。所以 `install.sh` 也**拒绝** `--profile desktop` 并指向应用内安装 —— 两边一致,
|
|
99
99
|
而不是某一方偷偷绕过去。(只想在本机模拟 profile 形状时可设 `DSH_ALLOW_DESKTOP_PROFILE=1`。)
|
|
100
|
+
**应用自带的捆绑 CLI 是例外**:`<安装目录>\resources\runtime\cli\bin\dsh.cmd` 可以操作那个 profile
|
|
101
|
+
(1.11.1 实测:`add dsh-custom-mode@1.11.2` → `+ dsh-custom-mode 1.11.1`)。它是启动器而不是 dsh 的
|
|
102
|
+
新功能 —— 它用 `ELECTRON_RUN_AS_NODE=1` 起 Electron 去跑 desktop-host 的 CLI,所以那是**应用在跟自己的
|
|
103
|
+
profile 说话**,不是从外面驱动它。
|
|
100
104
|
- **预设那一侧不用做任何事**:`$DSH_HOME/.agent-presets/` 是桌面端与 CLI 共享的产品数据,
|
|
101
105
|
预设由插件首次激活时落盘。
|
|
106
|
+
- **模式没出现在选择器里,就去「设置 → 自定义模式」读那条告警。** 行的启动条件在本机不满足时,平台会把整个
|
|
107
|
+
预设判为 broken 并**从所有选择器里丢掉**;设置页会点名那几行,并给出**「按本线修复」**。在桌面端那条线上,
|
|
108
|
+
这通常是一次点击 + **重启应用** —— 选择器只在应用启动时重读注册表(1.11.1 实测,见 `docs/MEASUREMENTS.md` §34)。
|
|
102
109
|
- 万一某个第三方 bundle 让应用起不来:原生恢复对话框提供**「禁用第三方插件」**,已安装的包与插件数据都留在磁盘上。
|
|
103
110
|
|
|
104
111
|
**兼容性是声明出来的,不是猜的。** dsh 会拿每个插件的 `peerDependencies["@deepseek-ai/dsh"]` 与运行时
|
|
@@ -323,7 +330,7 @@ MIT
|
|
|
323
330
|
|
|
324
331
|
```sh
|
|
325
332
|
# 钉版本的写法:要哪版就是哪版
|
|
326
|
-
dsh plugin --profile web add dsh-custom-mode@1.11.
|
|
333
|
+
dsh plugin --profile web add dsh-custom-mode@1.11.2
|
|
327
334
|
# 然后重启为该 profile 提供服务的 DSH 进程
|
|
328
335
|
```
|
|
329
336
|
|
package/client.js
CHANGED
|
@@ -61,14 +61,14 @@ try {
|
|
|
61
61
|
const ZH = {
|
|
62
62
|
"nav": "自定义模式",
|
|
63
63
|
"assistant.heading": "助手",
|
|
64
|
-
"assistant.hint": "
|
|
64
|
+
"assistant.hint": "每个助手即一个独立模式:独立的系统提示词、基础模式与插件开关。新建会话时在模式选择器中选择。",
|
|
65
65
|
"assistant.empty": "还没有助手,用下面的输入框新建一个。",
|
|
66
66
|
"assistant.loadingList": "正在读取助手列表…",
|
|
67
67
|
"assistant.newPlaceholder": "新助手的名字(例如:写作助手)",
|
|
68
68
|
"assistant.meta": "标识",
|
|
69
69
|
"assistant.copyName": "「{name}」副本",
|
|
70
70
|
"assistant.broken": "组成文件有问题",
|
|
71
|
-
"assistant.switchHint": "
|
|
71
|
+
"assistant.switchHint": "切换助手保留各自的未保存草稿;列表中的「未保存」标记即为此。",
|
|
72
72
|
"btn.create": "新增助手",
|
|
73
73
|
"btn.creating": "创建中…",
|
|
74
74
|
"btn.duplicate": "复制一份",
|
|
@@ -86,7 +86,7 @@ try {
|
|
|
86
86
|
"msg.createFailed": "创建失败",
|
|
87
87
|
"msg.deleteFailed": "删除失败",
|
|
88
88
|
"msg.nameRequired": "请先给新助手起个名字。",
|
|
89
|
-
"msg.readOnlyHint": "
|
|
89
|
+
"msg.readOnlyHint": "仅管理本插件创建的助手;手写 preset 不计入,也不会被改写。",
|
|
90
90
|
"msg.reordered": "顺序已保存:新建会话时的模式选择器按这个顺序排列。",
|
|
91
91
|
"msg.reorderFailed": "调整顺序失败",
|
|
92
92
|
"msg.imported": "已导入到编辑器(还没有保存):检查后点「保存」。",
|
|
@@ -142,8 +142,8 @@ try {
|
|
|
142
142
|
"api.selfCheckFailed": "生成结果自检失败,已放弃写入:{detail}",
|
|
143
143
|
"api.internalError": "宿主内部错误:{detail}",
|
|
144
144
|
"api.bodyTooLarge": "请求体过大(上限 {max} 字节)",
|
|
145
|
-
"api.repairNotNeeded": "
|
|
146
|
-
"api.repaired": "已按本机这条 dsh 线关闭 {count}
|
|
145
|
+
"api.repairNotNeeded": "这个助手在本机没有起不来的行,无需修复。",
|
|
146
|
+
"api.repaired": "已按本机这条 dsh 线关闭 {count} 个起不来的行({ids})。现在这个模式能重新出现在选择器里。",
|
|
147
147
|
"api.badAssistantId": "助手标识不合法:{id}(只能用 a-z、0-9 与连字符)",
|
|
148
148
|
"api.seedFailed": "复制模式模板失败:{detail}",
|
|
149
149
|
"api.metaWriteFailed": "写入 preset.yml 失败:{detail}",
|
|
@@ -157,6 +157,7 @@ try {
|
|
|
157
157
|
"api.baseCompositionUnavailable": "本机取不到基础模式「{mode}」的出厂组成,无法改动插件开关或基础模式(系统提示词可以单独保存)。",
|
|
158
158
|
"warn.approvalGateMissing": "审批闸门未启用:本机这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词不会弹审批。见「详情」。",
|
|
159
159
|
"warn.unresolvableRows": "有行在本机这条 DSH 线上无法解析:平台会把整个模式判为 broken,并从新会话的选择器里**静默丢弃**。点右侧的「按本线修复」即可(只关掉那几行,其它选择不动)。",
|
|
160
|
+
"warn.presetBroken": "平台把这个模式判为 broken,它会**从新会话的选择器里消失** —— 而上面的「无法解析」检查解释不了为什么(多半是某一行依赖的服务/引擎没起来)。点右侧的「按本线修复」把那几行关掉即可。宿主给的原因见「详情」。",
|
|
160
161
|
"warn.exclusiveRowsActive": "两条互斥的壳同时开着(它们注册同名工具):平台会把整个模式判为 broken,并从新会话的选择器里**静默丢弃**。把其中一条关掉即可 —— 在设置页保存一次也会自动避让。",
|
|
161
162
|
"warn.baseCompositionUnavailable": "取不到本机这条 DSH 线的出厂组成:基础模式与插件开关暂时不能改(系统提示词不受影响,照常可保存)。已尝试的途径写在宿主日志里。",
|
|
162
163
|
"warn.approvalGateMissing.label": "审批闸门未启用",
|
|
@@ -168,7 +169,7 @@ try {
|
|
|
168
169
|
"history.label": "改动历史",
|
|
169
170
|
"history.pick": "选择要载入的版本…",
|
|
170
171
|
"history.load": "载入这一版",
|
|
171
|
-
"history.hint": "
|
|
172
|
+
"history.hint": "每次保存、会话内工具改动或手工编辑都会在此留一版;载入只改草稿,保存前不落盘。",
|
|
172
173
|
"history.by.settings": "设置页保存",
|
|
173
174
|
"history.by.external": "会话内/手工改动",
|
|
174
175
|
"msg.loadFailed": "载入这一版失败",
|
|
@@ -183,15 +184,15 @@ try {
|
|
|
183
184
|
"delete.close": "关闭",
|
|
184
185
|
"delete.plainConfirm": "确定永久删除「{name}」吗?该操作无法撤销。",
|
|
185
186
|
"name.heading": "模式名称",
|
|
186
|
-
"name.hint": "
|
|
187
|
+
"name.hint": "仅影响显示名(模式选择器与助手列表);内部标识与已有会话不变,新建会话后生效。",
|
|
187
188
|
"name.placeholder": "自定义模式",
|
|
188
189
|
"name.descriptionPlaceholder": "模式描述(显示在模式选择器里,可留空)",
|
|
189
190
|
"mode.heading": "基础模式",
|
|
190
|
-
"mode.hint": "
|
|
191
|
+
"mode.hint": "底子只决定「行集合」与工具能力:本模式的身份行始终替换底子那一行(提示词由你编辑,complete: false),底子的提示语语义不继承。保存后新建会话即生效,无需重启。",
|
|
191
192
|
"rows.heading": "插件开关",
|
|
192
|
-
"rows.hint": "
|
|
193
|
+
"rows.hint": "逐行控制本模式挂载的插件。未拨动的行保持官方默认(含平台判断);手动拨动后以你的为准。",
|
|
193
194
|
"prompt.heading": "系统提示词",
|
|
194
|
-
"prompt.hint": "
|
|
195
|
+
"prompt.hint": "每次模型调用前重新读取:保存后下一步生效,仅影响使用本助手的会话。「导入」读文件进编辑器(保存前不写入);「导出」存为 .md;「恢复出厂提示词」填回出厂模板(同样需保存)。",
|
|
195
196
|
"status.enabled": "已启用",
|
|
196
197
|
"status.disabled": "已停用",
|
|
197
198
|
"status.changed": "已改",
|
|
@@ -279,14 +280,14 @@ try {
|
|
|
279
280
|
const EN = {
|
|
280
281
|
"nav": "Custom mode",
|
|
281
282
|
"assistant.heading": "Assistants",
|
|
282
|
-
"assistant.hint": "Each assistant is an independent mode: its own system prompt, base mode and plugin switches
|
|
283
|
+
"assistant.hint": "Each assistant is an independent mode: its own system prompt, base mode and plugin switches. Pick one in the mode picker when you start a new session.",
|
|
283
284
|
"assistant.empty": "No assistants yet — create one with the field below.",
|
|
284
285
|
"assistant.loadingList": "Loading assistants…",
|
|
285
286
|
"assistant.newPlaceholder": "Name of the new assistant (e.g. Writing assistant)",
|
|
286
287
|
"assistant.meta": "ID",
|
|
287
288
|
"assistant.copyName": "{name} (copy)",
|
|
288
289
|
"assistant.broken": "Composition problem",
|
|
289
|
-
"assistant.switchHint": "Switching assistants
|
|
290
|
+
"assistant.switchHint": "Switching assistants keeps each draft; the \"Unsaved\" marker in the list is exactly that.",
|
|
290
291
|
"btn.create": "New assistant",
|
|
291
292
|
"btn.creating": "Creating…",
|
|
292
293
|
"btn.duplicate": "Duplicate",
|
|
@@ -304,7 +305,7 @@ try {
|
|
|
304
305
|
"msg.createFailed": "Could not create",
|
|
305
306
|
"msg.deleteFailed": "Could not delete",
|
|
306
307
|
"msg.nameRequired": "Give the new assistant a name first.",
|
|
307
|
-
"msg.readOnlyHint": "
|
|
308
|
+
"msg.readOnlyHint": "Manages only the assistants this plugin created; a hand-written preset is not listed and is never rewritten.",
|
|
308
309
|
"msg.reordered": "Order saved: the mode picker for new sessions follows it.",
|
|
309
310
|
"msg.reorderFailed": "Could not reorder",
|
|
310
311
|
"msg.imported": "Imported into the editor (not saved yet) — review it, then click Save.",
|
|
@@ -360,8 +361,8 @@ try {
|
|
|
360
361
|
"api.selfCheckFailed": "The generated composition failed its self-check, so nothing was written: {detail}",
|
|
361
362
|
"api.internalError": "Unexpected host error: {detail}",
|
|
362
363
|
"api.bodyTooLarge": "Request body too large (limit {max} bytes)",
|
|
363
|
-
"api.repairNotNeeded": "Nothing to repair —
|
|
364
|
-
"api.repaired": "Disabled {count} row(s) this dsh line cannot
|
|
364
|
+
"api.repairNotNeeded": "Nothing to repair — every row of this assistant can start on this machine.",
|
|
365
|
+
"api.repaired": "Disabled {count} row(s) this dsh line cannot start ({ids}). The mode can appear in the pickers again.",
|
|
365
366
|
"api.badAssistantId": "Invalid assistant id: {id} (a-z, 0-9 and hyphens only)",
|
|
366
367
|
"api.seedFailed": "Copying the mode template failed: {detail}",
|
|
367
368
|
"api.metaWriteFailed": "Writing preset.yml failed: {detail}",
|
|
@@ -378,6 +379,7 @@ try {
|
|
|
378
379
|
"warn.noDescription": "No description: the new-session mode picker will show it as \"no description yet\".",
|
|
379
380
|
"warn.approvalGateMissing": "The approval gate is off: this DSH build has no tools/pre-execute event, so in-session prompt rewrites do not ask for approval. See the details.",
|
|
380
381
|
"warn.unresolvableRows": "Some rows cannot be resolved on this DSH line: the platform marks the whole mode broken and silently drops it from the new-session picker. Click Fix for this line — it only turns those rows off and leaves your other choices alone.",
|
|
382
|
+
"warn.presetBroken": "The platform marks this mode broken, so it **disappears from the new-session picker** — and the \"unresolvable\" check above cannot explain why (usually a row whose service or engine never started). Click Fix for this line to turn those rows off. The host's own reason is in Details.",
|
|
381
383
|
"warn.exclusiveRowsActive": "Both shells are on, and they register the same tool names: the platform marks the whole mode broken and silently drops it from the new-session picker. Turn one of them off — saving from the settings page also resolves it automatically.",
|
|
382
384
|
"warn.approvalGateMissing.label": "Approval gate is off",
|
|
383
385
|
"warn.approvalGateMissing.hint": "This DSH build has no tools/pre-execute event, so in-session prompt rewrites do NOT ask for approval. The settings page is unaffected; upgrade DSH or turn the custom_prompt tool row off to restore the gate.",
|
|
@@ -401,11 +403,11 @@ try {
|
|
|
401
403
|
"delete.close": "Close",
|
|
402
404
|
"delete.plainConfirm": "Permanently delete \"{name}\"? This cannot be undone.",
|
|
403
405
|
"name.heading": "Mode name",
|
|
404
|
-
"name.hint": "
|
|
406
|
+
"name.hint": "Changes the display name only (mode picker and assistant list); the internal id and existing sessions are unaffected. Applies to the next new session.",
|
|
405
407
|
"name.placeholder": "Custom mode",
|
|
406
408
|
"name.descriptionPlaceholder": "Mode description (shown in the mode picker, optional)",
|
|
407
409
|
"mode.heading": "Base mode",
|
|
408
|
-
"mode.hint": "
|
|
410
|
+
"mode.hint": "The base decides the row set and the tool abilities: this mode always replaces the base's persona row with its own reader (complete: false), so the base's prompt semantics are not inherited. A save applies to the next new session; no restart needed.",
|
|
409
411
|
"rows.heading": "Plugin switches",
|
|
410
412
|
"rows.hint": "Control row by row which plugins this mode mounts, laid out like the official plugin list. Untouched rows keep the official default (platform conditions included); your manual choice wins.",
|
|
411
413
|
"prompt.heading": "System prompt",
|
|
@@ -697,14 +699,14 @@ try {
|
|
|
697
699
|
".cpfe-h{margin:0 0 2px;font-size:14px;line-height:22px;font-weight:500;color:var(--dsw-alias-label-primary)}",
|
|
698
700
|
".cpfe-sub{margin:0 0 8px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
699
701
|
// 区块引言:官方是标题下的一行 12px 灰字 —— 没有折叠按钮,也没有那个蓝色圆点。
|
|
700
|
-
".cpfe-hint{display:flex;flex-direction:column;gap:2px;margin:0 0
|
|
702
|
+
".cpfe-hint{display:flex;flex-direction:column;gap:2px;margin:0 0 6px}",
|
|
701
703
|
".cpfe-hint-line{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
702
704
|
".cpfe-hint-detail{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
703
705
|
// 描述:多行、自适应高度(没有多行输入组件,所以用 textarea + 同一批语义变量)
|
|
704
706
|
".cpfe-desc{box-sizing:border-box;min-height:56px;max-height:200px;resize:vertical;padding:8px 12px;border-radius:10px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;margin-bottom:8px}",
|
|
705
707
|
".cpfe-base-pending{color:var(--dsw-alias-state-warn-primary)}",
|
|
706
|
-
".cpfe-note{display:block;font-size:
|
|
707
|
-
".cpfe-mono{display:block;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:
|
|
708
|
+
".cpfe-note{display:block;margin:8px 0 0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
709
|
+
".cpfe-mono{display:block;margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
708
710
|
".cpfe-pills{display:flex;flex-wrap:wrap;gap:6px;align-items:center}",
|
|
709
711
|
".cpfe-picker{display:flex;align-items:center;gap:8px;margin:2px 0 4px}",
|
|
710
712
|
// 壳的菜单浮层靠一个独立的 backing 元素上色(实测在 0.1.7-rc.2 + headless 下那块是透明的,
|
|
@@ -717,11 +719,11 @@ try {
|
|
|
717
719
|
".cpfe-actions{display:flex;flex-wrap:wrap;gap:var(--g);align-items:center;margin-top:4px}",
|
|
718
720
|
".cpfe-newrow{display:flex;gap:var(--g);align-items:center;flex-wrap:wrap;margin-top:10px}",
|
|
719
721
|
".cpfe-field{display:flex;width:100%;margin-bottom:8px}",
|
|
720
|
-
".cpfe-row-head{font-size:14px;line-height:
|
|
722
|
+
".cpfe-row-head{font-size:14px;line-height:20px;color:var(--dsw-alias-label-primary)}",
|
|
721
723
|
".cpfe-row-switch{flex:0 0 auto}",
|
|
722
724
|
".cpfe-row-line{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}",
|
|
723
|
-
".cpfe-row-note{min-height:18px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis}",
|
|
724
|
-
".cpfe-row-toggle{flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:
|
|
725
|
+
".cpfe-row-note{min-height:18px;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}",
|
|
726
|
+
".cpfe-row-toggle{flex:0 0 auto;width:22px;height:22px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;line-height:1}",
|
|
725
727
|
".cpfe-row-toggle:hover{background:var(--dsw-alias-bg-layer-2)}",
|
|
726
728
|
".cpfe-row-open{border-color:var(--dsw-alias-border-l2)}",
|
|
727
729
|
".cpfe-row-detail{display:flex;flex-direction:column;gap:4px;margin:0 0 12px;padding:0 0 0 16px;border:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}",
|
|
@@ -738,9 +740,9 @@ try {
|
|
|
738
740
|
".cpfe-line:last-child{border-bottom:0}",
|
|
739
741
|
".cpfe-group{display:flex;flex-direction:column;min-width:0}",
|
|
740
742
|
".cpfe-kids{display:flex;flex-direction:column;margin-left:0;padding-left:16px}",
|
|
741
|
-
".cpfe-row{display:flex;gap:12px;align-items:center;box-sizing:border-box;min-height:0;padding:
|
|
743
|
+
".cpfe-row{display:flex;gap:12px;align-items:center;box-sizing:border-box;min-height:0;padding:12px 0;border:0;border-radius:0;background:none}",
|
|
742
744
|
".cpfe-row-meta{display:flex;flex-direction:column;gap:4px;min-width:0;flex:1}",
|
|
743
|
-
".cpfe-row-badges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
|
|
745
|
+
".cpfe-row-badges{display:flex;align-items:center;gap:6px;flex-wrap:wrap;height:18px}",
|
|
744
746
|
".cpfe-editor{box-sizing:border-box;width:100%;min-height:240px;resize:vertical;padding:12px;border-radius:10px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;line-height:20px}",
|
|
745
747
|
".cpfe-bar{display:flex;align-items:center;gap:var(--g);flex-wrap:wrap}",
|
|
746
748
|
".cpfe-status{font-size:13px;line-height:20px}",
|
|
@@ -749,8 +751,8 @@ try {
|
|
|
749
751
|
".cpfe-dirty{color:var(--dsw-alias-state-warn-primary)}",
|
|
750
752
|
".cpfe-danger{color:var(--dsw-alias-state-error-primary)}",
|
|
751
753
|
".cpfe-meta-line{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;min-width:0}",
|
|
752
|
-
".cpfe-version{font-size:
|
|
753
|
-
".cpfe-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:
|
|
754
|
+
".cpfe-version{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
|
|
755
|
+
".cpfe-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}",
|
|
754
756
|
// ── fallback-path controls (unused when the shell provides the atoms) ──
|
|
755
757
|
".cpfe-btn{appearance:none;cursor:pointer;padding:0 14px;height:32px;border-radius:8px;border:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}",
|
|
756
758
|
".cpfe-btn:disabled{opacity:.5;cursor:default}",
|
|
@@ -766,7 +768,7 @@ try {
|
|
|
766
768
|
// 视觉上也给一个明确的"此刻不能动"。
|
|
767
769
|
".cpfe-busy .cpfe-switch,.cpfe-busy .cpfe-row-toggle,.cpfe-busy .cpfe-input,.cpfe-busy .cpfe-field,.cpfe-busy .cpfe-editor{pointer-events:none;opacity:.65}",
|
|
768
770
|
".cpfe-busy .cpfe-editor{cursor:progress}",
|
|
769
|
-
".cpfe-tag{font-size:
|
|
771
|
+
".cpfe-tag{font-size:12px;line-height:18px;padding:0 5px;border-radius:4px;border:.5px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}",
|
|
770
772
|
".cpfe-tag-success,.cpfe-tag-info{color:var(--dsw-alias-state-success-primary)}",
|
|
771
773
|
".cpfe-tag-warning{color:var(--dsw-alias-state-warn-primary)}",
|
|
772
774
|
".cpfe-tag-danger{color:var(--dsw-alias-state-error-primary)}",
|
|
@@ -2031,8 +2033,13 @@ try {
|
|
|
2031
2033
|
"div",
|
|
2032
2034
|
{ key: code, className: "cpfe-warn-row" },
|
|
2033
2035
|
react.createElement("p", { className: "cpfe-warn" }, "⚠ " + t("warn." + code)),
|
|
2034
|
-
//
|
|
2035
|
-
|
|
2036
|
+
// 本线起不来的行可以一键修(只关掉那几行;用户创建它的那个版本可能早于播种改为派生的版本)。
|
|
2037
|
+
//
|
|
2038
|
+
// ★ 两个 code 都要给按钮:`presetBroken` 说的是"平台判它 broken,而『无法解析』解释不了"
|
|
2039
|
+
// —— 那正是**修复按钮唯一还帮得上的时候**。原先只认 `unresolvableRows`,于是
|
|
2040
|
+
// 第一轮修复之后 `unresolvable` 变空、按钮消失,而告警文案还在叫用户"点右侧的
|
|
2041
|
+
// 按本线修复"。实测(2026-10-01,桌面端)就是卡在这一步:能看见病,没有药。
|
|
2042
|
+
code === "unresolvableRows" || code === "presetBroken"
|
|
2036
2043
|
? react.createElement(
|
|
2037
2044
|
A.Button,
|
|
2038
2045
|
{ disabled: busy, onClick: repairRowsNow },
|
package/index.mjs
CHANGED
|
@@ -281,7 +281,9 @@ export function configWarnings(text, prompt, meta) {
|
|
|
281
281
|
warnings.push('personaOffWithPrompt')
|
|
282
282
|
}
|
|
283
283
|
if (find('custom-prompt-tool')?.disabled === true) warnings.push('toolOff')
|
|
284
|
-
|
|
284
|
+
// **不报「没有描述」**:1.11.2 起模板故意不再播种描述(它是产品数据,壳没法按界面语言本地化 ——
|
|
285
|
+
// 写一种语言在另一种界面里就是外语,两种都写就成了"中英拼接")。默认留空却弹一条 ⚠ 是自相矛盾的,
|
|
286
|
+
// 而且描述框的占位符本来就写着「可留空」。填不填是用户的自由,不该被一直念。
|
|
285
287
|
if (typeof meta?.name !== 'string' || meta.name.trim() === '') warnings.push('noName')
|
|
286
288
|
// 两条互斥的路同时开着 ⇒ 平台会把整个模式判 broken 并**从所有选择器里静默丢弃**。
|
|
287
289
|
// 保存路径已经会自动避让(见 applyRowExclusivity),所以走到这里只可能是手工编辑、直接调 HTTP API,
|
|
@@ -290,6 +292,32 @@ export function configWarnings(text, prompt, meta) {
|
|
|
290
292
|
return warnings
|
|
291
293
|
}
|
|
292
294
|
|
|
295
|
+
/**
|
|
296
|
+
* 从宿主给的那条 `broken` 里抠出**行 id**。
|
|
297
|
+
*
|
|
298
|
+
* 平台把"这个预设为什么起不来"写成一行行 `<rowId> (<module>): <reason>`,例如
|
|
299
|
+
*
|
|
300
|
+
* workflow-worker-thread (@deepseek-ai/dsh-workflow-worker-thread): never started
|
|
301
|
+
* tool-workflow (@deepseek-ai/dsh-tool-workflow): waiting for workflowEngine
|
|
302
|
+
*
|
|
303
|
+
* 这是**唯一**能知道"是哪一行害的"的来源:`unresolvableRows()` 只答"模块装没装",答不了
|
|
304
|
+
* "上游服务等不到"。实测(2026-10-01,官方桌面端 0.2.0-rc.2):关掉那个装不上的引擎行之后
|
|
305
|
+
* `unresolvable` 变空、页面**全绿**,而预设**仍然 broken**(依赖引擎的两个工具在等它)——
|
|
306
|
+
* 于是用户再也走不到可用状态,模式一直不出现。见 docs/MEASUREMENTS.md §34。
|
|
307
|
+
*
|
|
308
|
+
* @param {unknown} message - the roster row's `broken` string.
|
|
309
|
+
* @returns {string[]} row ids, de-duplicated, in the order they appear.
|
|
310
|
+
*/
|
|
311
|
+
export function brokenRowIds(message) {
|
|
312
|
+
if (typeof message !== 'string') return []
|
|
313
|
+
const ids = []
|
|
314
|
+
for (const line of message.split('\n')) {
|
|
315
|
+
const match = /^\s*([A-Za-z0-9][A-Za-z0-9_-]*)\s+\(/.exec(line)
|
|
316
|
+
if (match !== null) ids.push(match[1])
|
|
317
|
+
}
|
|
318
|
+
return [...new Set(ids)]
|
|
319
|
+
}
|
|
320
|
+
|
|
293
321
|
export function readState(rows, id, options = {}) {
|
|
294
322
|
const directory = assistantDir(rows, id)
|
|
295
323
|
if (directory === undefined) return unknownAssistant(id)
|
|
@@ -326,6 +354,15 @@ export function readState(rows, id, options = {}) {
|
|
|
326
354
|
if (!isBaseCompositionUnavailable(error)) throw error
|
|
327
355
|
baseUnavailable = { code: error.code, params: { mode, detail: describe(error) } }
|
|
328
356
|
}
|
|
357
|
+
// 宿主给的那条 broken(若这条线上有)。**必须在 return 之前算**:warnings 要用它。
|
|
358
|
+
const selfRow = Array.isArray(rows) ? rows.find((item) => item !== null && typeof item === 'object' && item.id === id) : undefined
|
|
359
|
+
const hostBroken = typeof selfRow?.broken === 'string' ? selfRow.broken.trim() : ''
|
|
360
|
+
const unresolvable = unresolvableRows(text)
|
|
361
|
+
const knownUnresolvable = new Set(unresolvable.map((row) => row.id))
|
|
362
|
+
const hostBrokenIds = brokenRowIds(hostBroken)
|
|
363
|
+
// 宿主说坏了,而我们**解释不了**(不是"模块装不上")—— 那就要单独点名,否则页面全绿、
|
|
364
|
+
// 模式却在选择器里不出现。抠不出行 id 时(宿主换了措辞)也照样点名:宁可笼统,不可沉默。
|
|
365
|
+
const unexplainedBroken = hostBroken !== '' && (hostBrokenIds.length === 0 || hostBrokenIds.some((rowId) => knownUnresolvable.has(rowId) === false))
|
|
329
366
|
return {
|
|
330
367
|
ok: true,
|
|
331
368
|
id,
|
|
@@ -349,7 +386,10 @@ export function readState(rows, id, options = {}) {
|
|
|
349
386
|
history: listHistory(directory),
|
|
350
387
|
// 本插件版本 + 本机装的那条 dsh 线上无法解析的行(页面据此显示版本与"按本线修复")。
|
|
351
388
|
version: PLUGIN_VERSION,
|
|
352
|
-
unresolvable
|
|
389
|
+
unresolvable,
|
|
390
|
+
// 宿主自己给的「这个预设为什么起不来」原文。它比 `unresolvable` 宽:除了"模块装不上",
|
|
391
|
+
// 还包括"上游服务等不到"这类**装配期**失败。页面据此点名,而不是在坏着的时候显示全绿。
|
|
392
|
+
broken: hostBroken === '' ? null : hostBroken,
|
|
353
393
|
// 「配置了却不生效」的告警码(文案在页面侧按语言渲染)。
|
|
354
394
|
warnings: [
|
|
355
395
|
...configWarnings(text, prompt.ok === true ? prompt.text : '', meta),
|
|
@@ -357,7 +397,11 @@ export function readState(rows, id, options = {}) {
|
|
|
357
397
|
// 诚实地把它变成页面上的告警,而不是只留在 console.error 里。
|
|
358
398
|
...(existsSync(join(directory, 'approval-gate-missing')) ? ['approvalGateMissing'] : []),
|
|
359
399
|
// 本线无法解析的启用行 = 平台会把整个预设判为 broken 并从选择器里丢掉(曾经的 P0)。
|
|
360
|
-
...(
|
|
400
|
+
...(unresolvable.length > 0 ? ['unresolvableRows'] : []),
|
|
401
|
+
// ★ 宿主报了 broken,而**我们自己的检查解释不了**(不是"模块装不上",而是"服务等不到"之类)。
|
|
402
|
+
// 不点名的话,页面上 `unresolvable` 空、告警空 —— 一切看起来正常,而模式**根本没出现在
|
|
403
|
+
// 选择器里**。这正是本仓记录过两次的那种"静默失败",只是换了个触发面。
|
|
404
|
+
...(unexplainedBroken ? ['presetBroken'] : []),
|
|
361
405
|
// 出厂组成取不到:提示词能改,基础模式与插件开关不能。
|
|
362
406
|
...(baseUnavailable === null ? [] : ['baseCompositionUnavailable']),
|
|
363
407
|
],
|
|
@@ -762,9 +806,30 @@ export function repairComposition(rows, input) {
|
|
|
762
806
|
const file = compositionFile(directory)
|
|
763
807
|
if (!existsSync(file)) return { ok: false, code: 'compositionMissing', params: { path: file }, error: '找不到组成文件:' + file }
|
|
764
808
|
const text = readFileSync(file, 'utf8')
|
|
765
|
-
|
|
809
|
+
// 两类"这一行在本机起不来",必须**都**关掉:
|
|
810
|
+
// ① 模块装不上 —— 我们自己在 node_modules 里查得出来(`unresolvableRows`);
|
|
811
|
+
// ② 装配期失败("服务等不到"之类)—— 只有宿主知道,写在它给的 `broken` 里。
|
|
812
|
+
// 只修 ① 会留下一个**页面全绿、预设却仍然 broken**的状态(桌面端实测,见 brokenRowIds 的注释):
|
|
813
|
+
// `unresolvable` 变空 ⇒ 告警消失 ⇒ 用户以为修好了,而模式始终没出现在选择器里。
|
|
814
|
+
const selfRow = Array.isArray(rows) ? rows.find((item) => item !== null && typeof item === 'object' && item.id === id) : undefined
|
|
815
|
+
// 只关**这份文本里真的有**的行:宿主报的可能是别处的行,而 `disableRowsInPlace` 对不存在的 id
|
|
816
|
+
// 是无操作 —— 计数会因此说谎(说关了 3 个其实只关了 1 个)。
|
|
817
|
+
const present = new Set()
|
|
818
|
+
const collectIds = (list) => {
|
|
819
|
+
for (const row of list) {
|
|
820
|
+
present.add(row.id)
|
|
821
|
+
if (Array.isArray(row.children)) collectIds(row.children)
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
try {
|
|
825
|
+
collectIds(collectRows(text))
|
|
826
|
+
} catch {
|
|
827
|
+
/* 文本解析不了就只按 unresolvableRows 走(它自己会兜底) */
|
|
828
|
+
}
|
|
829
|
+
const wanted = [...new Set([...unresolvableRows(text).map((row) => row.id), ...brokenRowIds(selfRow?.broken)])]
|
|
830
|
+
const bad = wanted.map((rowId) => ({ id: rowId })).filter((row) => present.has(row.id))
|
|
766
831
|
if (bad.length === 0) {
|
|
767
|
-
return { ok: true, id, code: 'repairNotNeeded', note: '
|
|
832
|
+
return { ok: true, id, code: 'repairNotNeeded', note: '这个助手在本机没有起不来的行,无需修复。' }
|
|
768
833
|
}
|
|
769
834
|
// **就地**关闭那几行,不重渲染:重渲染会顺手丢掉 base 之外的自有行,而用户的数据不该被这样动。
|
|
770
835
|
const rendered = disableRowsInPlace(text, bad.map((row) => row.id))
|
|
@@ -782,7 +847,7 @@ export function repairComposition(rows, input) {
|
|
|
782
847
|
// 草稿,否则下一次保存会按草稿重渲染,把刚才的修复原样撤销(issue #8 实测:
|
|
783
848
|
// 修复写进了磁盘,但草稿里那几行仍是启用的,于是保存后 ghost 行又回来了)。
|
|
784
849
|
params: { count: bad.length, ids, repairedIds: bad.map((row) => row.id) },
|
|
785
|
-
note: '已按本机这条 dsh 线关闭 ' + String(bad.length) + '
|
|
850
|
+
note: '已按本机这条 dsh 线关闭 ' + String(bad.length) + ' 个起不来的行(' + ids + ')。现在这个模式能重新出现在选择器里。',
|
|
786
851
|
}
|
|
787
852
|
}
|
|
788
853
|
|
package/locales.mjs
CHANGED
|
@@ -21,14 +21,14 @@ export const zh = {
|
|
|
21
21
|
// ── 助手管理 ──────────────────────────────────────────────────────────────
|
|
22
22
|
'assistant.heading': '助手',
|
|
23
23
|
'assistant.hint':
|
|
24
|
-
'
|
|
24
|
+
'每个助手即一个独立模式:独立的系统提示词、基础模式与插件开关。新建会话时在模式选择器中选择。',
|
|
25
25
|
'assistant.empty': '还没有助手,用下面的输入框新建一个。',
|
|
26
26
|
'assistant.loadingList': '正在读取助手列表…',
|
|
27
27
|
'assistant.newPlaceholder': '新助手的名字(例如:写作助手)',
|
|
28
28
|
'assistant.meta': '标识',
|
|
29
29
|
'assistant.copyName': '「{name}」副本',
|
|
30
30
|
'assistant.broken': '组成文件有问题',
|
|
31
|
-
'assistant.switchHint': '
|
|
31
|
+
'assistant.switchHint': '切换助手保留各自的未保存草稿;列表中的「未保存」标记即为此。',
|
|
32
32
|
'btn.create': '新增助手',
|
|
33
33
|
'btn.creating': '创建中…',
|
|
34
34
|
'btn.duplicate': '复制一份',
|
|
@@ -46,7 +46,7 @@ export const zh = {
|
|
|
46
46
|
'msg.createFailed': '创建失败',
|
|
47
47
|
'msg.deleteFailed': '删除失败',
|
|
48
48
|
'msg.nameRequired': '请先给新助手起个名字。',
|
|
49
|
-
'msg.readOnlyHint': '
|
|
49
|
+
'msg.readOnlyHint': '仅管理本插件创建的助手;手写 preset 不计入,也不会被改写。',
|
|
50
50
|
'msg.reordered': '顺序已保存:新建会话时的模式选择器按这个顺序排列。',
|
|
51
51
|
'msg.reorderFailed': '调整顺序失败',
|
|
52
52
|
'msg.imported': '已导入到编辑器(还没有保存):检查后点「保存」。',
|
|
@@ -102,8 +102,8 @@ export const zh = {
|
|
|
102
102
|
'api.selfCheckFailed': '生成结果自检失败,已放弃写入:{detail}',
|
|
103
103
|
'api.internalError': '宿主内部错误:{detail}',
|
|
104
104
|
'api.bodyTooLarge': '请求体过大(上限 {max} 字节)',
|
|
105
|
-
'api.repairNotNeeded': '
|
|
106
|
-
'api.repaired': '已按本机这条 dsh 线关闭 {count}
|
|
105
|
+
'api.repairNotNeeded': '这个助手在本机没有起不来的行,无需修复。',
|
|
106
|
+
'api.repaired': '已按本机这条 dsh 线关闭 {count} 个起不来的行({ids})。现在这个模式能重新出现在选择器里。',
|
|
107
107
|
'api.badAssistantId': '助手标识不合法:{id}(只能用 a-z、0-9 与连字符)',
|
|
108
108
|
'api.seedFailed': '复制模式模板失败:{detail}',
|
|
109
109
|
'api.metaWriteFailed': '写入 preset.yml 失败:{detail}',
|
|
@@ -117,6 +117,7 @@ export const zh = {
|
|
|
117
117
|
'api.baseCompositionUnavailable': '本机取不到基础模式「{mode}」的出厂组成,无法改动插件开关或基础模式(系统提示词可以单独保存)。',
|
|
118
118
|
'warn.approvalGateMissing': '审批闸门未启用:本机这个 DSH 版本没有 tools/pre-execute 事件,会话内改写系统提示词不会弹审批。见「详情」。',
|
|
119
119
|
'warn.unresolvableRows': '有行在本机这条 DSH 线上无法解析:平台会把整个模式判为 broken,并从新会话的选择器里**静默丢弃**。点右侧的「按本线修复」即可(只关掉那几行,其它选择不动)。',
|
|
120
|
+
'warn.presetBroken': '平台把这个模式判为 broken,它会**从新会话的选择器里消失** —— 而上面的「无法解析」检查解释不了为什么(多半是某一行依赖的服务/引擎没起来)。点右侧的「按本线修复」把那几行关掉即可。宿主给的原因见「详情」。',
|
|
120
121
|
'warn.exclusiveRowsActive': '两条互斥的壳同时开着(它们注册同名工具):平台会把整个模式判为 broken,并从新会话的选择器里**静默丢弃**。把其中一条关掉即可 —— 在设置页保存一次也会自动避让。',
|
|
121
122
|
'warn.baseCompositionUnavailable': '取不到本机这条 DSH 线的出厂组成:基础模式与插件开关暂时不能改(系统提示词不受影响,照常可保存)。已尝试的途径写在宿主日志里。',
|
|
122
123
|
'warn.approvalGateMissing.label': '审批闸门未启用',
|
|
@@ -128,7 +129,7 @@ export const zh = {
|
|
|
128
129
|
'history.label': '改动历史',
|
|
129
130
|
'history.pick': '选择要载入的版本…',
|
|
130
131
|
'history.load': '载入这一版',
|
|
131
|
-
'history.hint': '
|
|
132
|
+
'history.hint': '每次保存、会话内工具改动或手工编辑都会在此留一版;载入只改草稿,保存前不落盘。',
|
|
132
133
|
'history.by.settings': '设置页保存',
|
|
133
134
|
'history.by.external': '会话内/手工改动',
|
|
134
135
|
'msg.loadFailed': '载入这一版失败',
|
|
@@ -144,19 +145,19 @@ export const zh = {
|
|
|
144
145
|
'delete.plainConfirm': '确定永久删除「{name}」吗?该操作无法撤销。',
|
|
145
146
|
|
|
146
147
|
'name.heading': '模式名称',
|
|
147
|
-
'name.hint': '
|
|
148
|
+
'name.hint': '仅影响显示名(模式选择器与助手列表);内部标识与已有会话不变,新建会话后生效。',
|
|
148
149
|
'name.placeholder': '自定义模式',
|
|
149
150
|
'name.descriptionPlaceholder': '模式描述(显示在模式选择器里,可留空)',
|
|
150
151
|
|
|
151
152
|
'mode.heading': '基础模式',
|
|
152
|
-
'mode.hint': '
|
|
153
|
+
'mode.hint': '底子只决定「行集合」与工具能力:本模式的身份行始终替换底子那一行(提示词由你编辑,complete: false),底子的提示语语义不继承。保存后新建会话即生效,无需重启。',
|
|
153
154
|
|
|
154
155
|
'rows.heading': '插件开关',
|
|
155
|
-
'rows.hint': '
|
|
156
|
+
'rows.hint': '逐行控制本模式挂载的插件。未拨动的行保持官方默认(含平台判断);手动拨动后以你的为准。',
|
|
156
157
|
|
|
157
158
|
'prompt.heading': '系统提示词',
|
|
158
159
|
'prompt.hint':
|
|
159
|
-
'
|
|
160
|
+
'每次模型调用前重新读取:保存后下一步生效,仅影响使用本助手的会话。「导入」读文件进编辑器(保存前不写入);「导出」存为 .md;「恢复出厂提示词」填回出厂模板(同样需保存)。',
|
|
160
161
|
|
|
161
162
|
'status.enabled': '已启用',
|
|
162
163
|
'status.disabled': '已停用',
|
|
@@ -257,14 +258,14 @@ export const en = {
|
|
|
257
258
|
// ── assistant management ──────────────────────────────────────────────────
|
|
258
259
|
'assistant.heading': 'Assistants',
|
|
259
260
|
'assistant.hint':
|
|
260
|
-
'Each assistant is an independent mode: its own system prompt, base mode and plugin switches
|
|
261
|
+
'Each assistant is an independent mode: its own system prompt, base mode and plugin switches. Pick one in the mode picker when you start a new session.',
|
|
261
262
|
'assistant.empty': 'No assistants yet — create one with the field below.',
|
|
262
263
|
'assistant.loadingList': 'Loading assistants…',
|
|
263
264
|
'assistant.newPlaceholder': 'Name of the new assistant (e.g. Writing assistant)',
|
|
264
265
|
'assistant.meta': 'ID',
|
|
265
266
|
'assistant.copyName': '{name} (copy)',
|
|
266
267
|
'assistant.broken': 'Composition problem',
|
|
267
|
-
'assistant.switchHint': 'Switching assistants
|
|
268
|
+
'assistant.switchHint': 'Switching assistants keeps each draft; the "Unsaved" marker in the list is exactly that.',
|
|
268
269
|
'btn.create': 'New assistant',
|
|
269
270
|
'btn.creating': 'Creating…',
|
|
270
271
|
'btn.duplicate': 'Duplicate',
|
|
@@ -282,7 +283,7 @@ export const en = {
|
|
|
282
283
|
'msg.createFailed': 'Could not create',
|
|
283
284
|
'msg.deleteFailed': 'Could not delete',
|
|
284
285
|
'msg.nameRequired': 'Give the new assistant a name first.',
|
|
285
|
-
'msg.readOnlyHint': '
|
|
286
|
+
'msg.readOnlyHint': 'Manages only the assistants this plugin created; a hand-written preset is not listed and is never rewritten.',
|
|
286
287
|
'msg.reordered': 'Order saved: the mode picker for new sessions follows it.',
|
|
287
288
|
'msg.reorderFailed': 'Could not reorder',
|
|
288
289
|
'msg.imported': 'Imported into the editor (not saved yet) — review it, then click Save.',
|
|
@@ -338,8 +339,8 @@ export const en = {
|
|
|
338
339
|
'api.selfCheckFailed': 'The generated composition failed its self-check, so nothing was written: {detail}',
|
|
339
340
|
'api.internalError': 'Unexpected host error: {detail}',
|
|
340
341
|
'api.bodyTooLarge': 'Request body too large (limit {max} bytes)',
|
|
341
|
-
'api.repairNotNeeded': 'Nothing to repair —
|
|
342
|
-
'api.repaired': 'Disabled {count} row(s) this dsh line cannot
|
|
342
|
+
'api.repairNotNeeded': 'Nothing to repair — every row of this assistant can start on this machine.',
|
|
343
|
+
'api.repaired': 'Disabled {count} row(s) this dsh line cannot start ({ids}). The mode can appear in the pickers again.',
|
|
343
344
|
'api.badAssistantId': 'Invalid assistant id: {id} (a-z, 0-9 and hyphens only)',
|
|
344
345
|
'api.seedFailed': 'Copying the mode template failed: {detail}',
|
|
345
346
|
'api.metaWriteFailed': 'Writing preset.yml failed: {detail}',
|
|
@@ -356,6 +357,7 @@ export const en = {
|
|
|
356
357
|
'warn.noDescription': 'No description: the new-session mode picker will show it as "no description yet".',
|
|
357
358
|
'warn.approvalGateMissing': 'The approval gate is off: this DSH build has no tools/pre-execute event, so in-session prompt rewrites do not ask for approval. See the details.',
|
|
358
359
|
'warn.unresolvableRows': 'Some rows cannot be resolved on this DSH line: the platform marks the whole mode broken and silently drops it from the new-session picker. Click Fix for this line — it only turns those rows off and leaves your other choices alone.',
|
|
360
|
+
'warn.presetBroken': 'The platform marks this mode broken, so it **disappears from the new-session picker** — and the "unresolvable" check above cannot explain why (usually a row whose service or engine never started). Click Fix for this line to turn those rows off. The host\'s own reason is in Details.',
|
|
359
361
|
'warn.exclusiveRowsActive': 'Both shells are on, and they register the same tool names: the platform marks the whole mode broken and silently drops it from the new-session picker. Turn one of them off — saving from the settings page also resolves it automatically.',
|
|
360
362
|
'warn.approvalGateMissing.label': 'Approval gate is off',
|
|
361
363
|
'warn.approvalGateMissing.hint': 'This DSH build has no tools/pre-execute event, so in-session prompt rewrites do NOT ask for approval. The settings page is unaffected; upgrade DSH or turn the custom_prompt tool row off to restore the gate.',
|
|
@@ -381,13 +383,13 @@ export const en = {
|
|
|
381
383
|
|
|
382
384
|
'name.heading': 'Mode name',
|
|
383
385
|
'name.hint':
|
|
384
|
-
'
|
|
386
|
+
'Changes the display name only (mode picker and assistant list); the internal id and existing sessions are unaffected. Applies to the next new session.',
|
|
385
387
|
'name.placeholder': 'Custom mode',
|
|
386
388
|
'name.descriptionPlaceholder': 'Mode description (shown in the mode picker, optional)',
|
|
387
389
|
|
|
388
390
|
'mode.heading': 'Base mode',
|
|
389
391
|
'mode.hint':
|
|
390
|
-
'
|
|
392
|
+
'The base decides the row set and the tool abilities: this mode always replaces the base\'s persona row with its own reader (complete: false), so the base\'s prompt semantics are not inherited. A save applies to the next new session; no restart needed.',
|
|
391
393
|
|
|
392
394
|
'rows.heading': 'Plugin switches',
|
|
393
395
|
'rows.hint':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-custom-mode",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.2",
|
|
4
4
|
"description": "Custom modes and custom prompts for DeepSeek Harness (dsh): edit a mode's system prompt on the settings page (it takes effect on the next model step), pick its base mode, switch plugin rows one by one, and keep several assistants side by side — coding, chat or roleplay (RP) personas (multi-mode / multi-agent).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/preset/preset.yml
CHANGED
package/seed.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
25
25
|
import { renderComposition } from './composition.mjs'
|
|
26
|
+
import { readPresetMeta, writePresetMeta } from './meta.mjs'
|
|
26
27
|
import { writeAtomic } from './atomic.mjs'
|
|
27
28
|
import { dirname, join } from 'node:path'
|
|
28
29
|
import { fileURLToPath } from 'node:url'
|
|
@@ -172,6 +173,49 @@ export function seedPreset(presetDir, sourceDir = packagedPresetDir(), options =
|
|
|
172
173
|
return { created, refreshed, kept, errors }
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
/**
|
|
177
|
+
* We shipped a **bilingual** description (`中文 / English`) for three years' worth of installs.
|
|
178
|
+
*
|
|
179
|
+
* It was a workaround with a real cause: the description is **product data** in `preset.yml`, and the shell
|
|
180
|
+
* renders it in whatever language the UI happens to be in — it localises its own shipped presets, but it
|
|
181
|
+
* cannot localise ours. A bilingual line then read as one language inside the other, which is exactly the
|
|
182
|
+
* complaint that came back ("英文是英文中文是中文,不要混杂"): in the mode picker the entry showed
|
|
183
|
+
* `完整编码能力… / Full coding ability…` as a single run-on string, and it was long enough to make that
|
|
184
|
+
* card taller than the shipped ones next to it.
|
|
185
|
+
*
|
|
186
|
+
* So the template no longer ships a description at all: an absent description is a state the shell already
|
|
187
|
+
* supports (it prints its own "no description" placeholder), it cannot be wrong in either language, and the
|
|
188
|
+
* field stays user-editable on the settings page.
|
|
189
|
+
*
|
|
190
|
+
* This migrates installs that still carry **one of our own shipped strings verbatim**. Anything else — a
|
|
191
|
+
* description the user typed, however similar — is left alone: that is the whole contract of this file.
|
|
192
|
+
*/
|
|
193
|
+
export const SHIPPED_DESCRIPTIONS = [
|
|
194
|
+
"在设置页编辑本模式的系统提示词 / Edit this mode's system prompt in Settings.",
|
|
195
|
+
'完整编码能力,系统提示词来自 prompt.md,可在设置页随时修改、下一步即生效。 / Full coding ability; the system prompt lives in prompt.md and can be edited in the settings page — it takes effect on the next step.',
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Blank the description **only** when it is one of {@link SHIPPED_DESCRIPTIONS}.
|
|
200
|
+
*
|
|
201
|
+
* @param {string} presetDir - the installed preset directory.
|
|
202
|
+
* @returns {{migrated: boolean, reason?: string}} what happened.
|
|
203
|
+
*/
|
|
204
|
+
export function dropShippedDescription(presetDir) {
|
|
205
|
+
const file = join(presetDir, 'preset.yml')
|
|
206
|
+
if (!existsSync(file)) return { migrated: false, reason: 'no preset.yml' }
|
|
207
|
+
let meta
|
|
208
|
+
try {
|
|
209
|
+
meta = readPresetMeta(presetDir)
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return { migrated: false, reason: describe(error) }
|
|
212
|
+
}
|
|
213
|
+
const current = typeof meta.description === 'string' ? meta.description.trim() : ''
|
|
214
|
+
if (current === '' || SHIPPED_DESCRIPTIONS.includes(current) === false) return { migrated: false, reason: 'user text' }
|
|
215
|
+
const written = writePresetMeta(typeof meta.name === 'string' && meta.name.trim() !== '' ? meta.name : '自定义模式', '', presetDir)
|
|
216
|
+
return written.ok === true ? { migrated: true } : { migrated: false, reason: String(written.code ?? 'writeFailed') }
|
|
217
|
+
}
|
|
218
|
+
|
|
175
219
|
/**
|
|
176
220
|
* Seed and report, in the form the host half logs.
|
|
177
221
|
*
|
|
@@ -200,6 +244,14 @@ export function seedPresetWithLog(presetDir, log = console.error, info = console
|
|
|
200
244
|
if (result.created.length > 0) {
|
|
201
245
|
info(`custom-mode: 已播种 preset 到 ${presetDir}(新建 ${result.created.length} 个文件: ${result.created.join(', ')})`)
|
|
202
246
|
}
|
|
247
|
+
// 只订正**我们自己写进去的**那条描述(见 dropShippedDescription)。
|
|
248
|
+
try {
|
|
249
|
+
if (dropShippedDescription(presetDir).migrated === true) {
|
|
250
|
+
info(`custom-mode: 已清空描述 —— 它还是我们早年播种的那条中英拼接,壳无法按界面语言本地化产品数据`)
|
|
251
|
+
}
|
|
252
|
+
} catch (error) {
|
|
253
|
+
log(`custom-mode: 描述迁移失败(已忽略): ${describe(error)}`)
|
|
254
|
+
}
|
|
203
255
|
for (const error of result.errors) {
|
|
204
256
|
log(`custom-mode: preset 播种未完成 —— ${error}`)
|
|
205
257
|
}
|