dsh-loadout 0.12.3 → 0.12.6
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 +21 -1
- package/README.en.md +4 -2
- package/README.md +5 -3
- package/client.js +72 -26
- package/index.js +3 -2
- package/package.json +1 -1
- package/scripts/client-smoke.mjs +21 -0
- package/scripts/agent-lookup-probe.mjs +0 -24
- package/scripts/agent-probe.mjs +0 -25
- package/scripts/attribution-probe.mjs +0 -91
- package/scripts/cmd-probe.mjs +0 -30
- package/scripts/dock-probe.mjs +0 -53
- package/scripts/slots-probe.mjs +0 -59
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
「装备栏」是一个 DeepSeek Harness 插件:给每个对话选一套工具(套装),只把用得上的工具投给模型。
|
|
4
4
|
|
|
5
|
+
## 0.12.6
|
|
6
|
+
|
|
7
|
+
- **发布流水线失败的那次补上**:0.12.5 的 `npm publish --provenance` 在 sigstore 里报了 `TLOG_CREATE_ENTRY_ERROR (409) an equivalent entry already exists in the transparency log`——包本身没进 registry(`latest` 仍是 0.12.4),是可信日志去重把这一版挡了下来。`publish.yml` 现在多两层兜底:先看这个版本是不是其实已经到 registry 了(到了就算成功),否则去掉 `--provenance` 再发一次,并留下 warning;正常路径仍然带 provenance。
|
|
8
|
+
- 这一版内容与 0.12.5 相同(只是版本号 + 上面这条流水线兜底)。
|
|
9
|
+
|
|
10
|
+
## 0.12.5
|
|
11
|
+
|
|
12
|
+
- **把开发期探针从发布包里清掉**:`scripts/*-probe.mjs` 六个脚本(agent-probe / agent-lookup-probe / attribution-probe / cmd-probe / dock-probe / slots-probe)都是当时读 `app.asar` 用的一次性工具,里面硬编码了本机绝对路径,却因为 `files: scripts/*.mjs` 一直被塞进 npm 包。现在它们从仓库和包里一起消失,包里只剩三套自检。
|
|
13
|
+
- 市场条目的注释指向 `docs/RELEASING.md`(原来指向已删除的 `RELEASE.md`)。
|
|
14
|
+
- `publish.yml` 的说明改成事实:npm 已不发经典 token,需要 granular token + Bypass 2FA。
|
|
15
|
+
- `index.js` 里那个用来让 `restrict()` 报错的哨兵工具名从 `__dsh_plugin_kits_probe__` 改成 `__dsh_loadout_probe__`(旧包名残留,纯内部字符串,无行为变化)。
|
|
16
|
+
- README 补一句 `scripts/install.ps1` 的定位:没有 `dsh` CLI 时的链接安装,等价于 `install_bundle`。
|
|
17
|
+
## 0.12.4
|
|
18
|
+
|
|
19
|
+
- **重名当场说清楚**:新建/编辑套装时,输入框下方直接给出原因——「「仅聊天」是预置套装(标识 chat),不能用这个名字」,或是「已经有叫「我的套」的套装了」。占用时保存按钮置灰,不用等提交后才失败。
|
|
20
|
+
- 说明重名判定:名字先 trim + NFKC + 转小写,然后同时比对**自建套装的 name 与 title**、**预置套装的 name(稳定标识)与 title(界面显示)**、以及保留字(全部 / 仅内置 / all / default…)。所以预置显示「仅聊天」时,chat 依然被占用——chat 是它的标识。
|
|
21
|
+
- 宿主错误文案同步:「仅聊天」是预置套装(标识 chat),不能占用这个名字。
|
|
22
|
+
- **「+ 新建套装」改成动作色**:蓝色文字 + 浅蓝底 + 悬停加深,右侧补一句「从零配一套」,跟下面那些「点一下就切换」的套装行区分开。
|
|
23
|
+
- **「我的套装」折叠时也留着「+ 新建套装」**:网格收起只显示当前在用的那一套,创建入口始终在最后一行,不用先点开分组。
|
|
24
|
+
|
|
5
25
|
## 0.12.3
|
|
6
26
|
|
|
7
|
-
- **标题栏按钮不再「点一下才对」**:芯片只在菜单被打开时才去读状态,所以进对话先看到「装备栏」,点开一次才换成套装名。现在挂载就读一次,并监听 dsh-loadout:changed 与窗口
|
|
27
|
+
- **标题栏按钮不再「点一下才对」**:芯片只在菜单被打开时才去读状态,所以进对话先看到「装备栏」,点开一次才换成套装名。现在挂载就读一次,并监听 dsh-loadout:changed 与窗口 focus,别处切了套装也会立刻跟上。
|
|
8
28
|
- **设置页「预设套装」默认折叠**:与「我的套装」一致——折叠时只显示当前被设为默认的那一套,展开才列出五套。
|
|
9
29
|
- **同名撞车**:0.12 之前手建的套装如果叫 chat / plan 这类预置名,
|
|
10
30
|
- 回归断言: user kit shadowing a preset is renamed、 he renamed kit keeps its default and its bindings、presets start collapsed、 collapsed preset group keeps the default visible。
|
package/README.en.md
CHANGED
|
@@ -43,6 +43,8 @@ Letting Harness install it is the same as the third form:
|
|
|
43
43
|
plugin_manager action=install_bundle target=<absolute path to this directory>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
Without the dsh CLI you can use `scripts/install.ps1`: it creates the junction link and writes the profile manifest, the same thing `install_bundle` does. Its `-Copy` switch goes back to a copied install — avoid it unless you know why you want one.
|
|
47
|
+
|
|
46
48
|
Restart the app afterwards: host plugins have no hot reload, and the UI needs at least a refresh.
|
|
47
49
|
|
|
48
50
|
### Why not copy the files
|
|
@@ -63,7 +65,7 @@ node scripts/client-smoke.mjs # browser bundle: menu and settings panel render
|
|
|
63
65
|
- **Settings → Loadout**: manage kits, edit notes, choose the new-conversation default.
|
|
64
66
|
- `/kit <name>`: switch from the message box.
|
|
65
67
|
|
|
66
|
-
Settings has no conversation of its own, so it only decides *where new conversations start* — use the header button to change the current one. In the default list, `All` / `Built-in only` and kits are the same kind of card: each can copy its switch command or be set as default. **My kits** starts collapsed and shows only the current default; open it to see the rest. A saved kit can be **edited** on its card: the form opens under that card, and you either overwrite it (conversations already using it switch immediately) or save as a new kit.
|
|
68
|
+
Settings has no conversation of its own, so it only decides *where new conversations start* — use the header button to change the current one. In the default list, `All` / `Built-in only` and kits are the same kind of card: each can copy its switch command or be set as default. **My kits** starts collapsed and shows only the current default, with the same + New kit action right under it; open it to see the rest. A saved kit can be **edited** on its card: the form opens under that card, and you either overwrite it (conversations already using it switch immediately) or save as a new kit.
|
|
67
69
|
|
|
68
70
|
### Shipped presets (five, read-only)
|
|
69
71
|
Five read-only presets. Editing one forks it into one of your kits.
|
|
@@ -85,7 +87,7 @@ Five read-only presets. Editing one forks it into one of your kits.
|
|
|
85
87
|
## Upgrading
|
|
86
88
|
|
|
87
89
|
- 0.11.x → 0.12.x: same package name; install over it. The store migrates to `schema: 2` once, with a `loadout.json.v1.bak-<ts>` backup, and every existing conversation is pinned to 全部 so a default change cannot reach back into it.
|
|
88
|
-
- 0.4.x → 0.
|
|
90
|
+
- 0.4.x → 0.12.x: the package was renamed from `dsh-plugin-kits`; remove the old one first:
|
|
89
91
|
|
|
90
92
|
```text
|
|
91
93
|
plugin_manager action=remove_bundle target=dsh-plugin-kits
|
package/README.md
CHANGED
|
@@ -44,6 +44,8 @@ dsh plugin --profile desktop add link:<本目录绝对路径>
|
|
|
44
44
|
plugin_manager action=install_bundle target=<本目录绝对路径>
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
没有 dsh CLI 的机器上,可以用 `scripts/install.ps1`:它建 junction 链接并写 profile manifest,等价于 `install_bundle`;`-Copy` 会退回拷贝安装,除非你清楚后果,否则别用。
|
|
48
|
+
|
|
47
49
|
装完重启应用:宿主插件没有热重载,界面至少刷新一次。
|
|
48
50
|
|
|
49
51
|
### 为什么不能复制文件
|
|
@@ -64,7 +66,7 @@ node scripts/client-smoke.mjs # 浏览器 bundle:菜单与设置页渲染
|
|
|
64
66
|
- 设置 → 装备栏:管理套装、改备注、选新对话默认套装。
|
|
65
67
|
- `/kit <名字>`:在对话里直接切换。
|
|
66
68
|
|
|
67
|
-
设置页没有自己的对话,所以它只管「新对话从哪套开始」,不切换当前对话——当前对话请在标题栏里切。默认列表里「全部 /
|
|
69
|
+
设置页没有自己的对话,所以它只管「新对话从哪套开始」,不切换当前对话——当前对话请在标题栏里切。默认列表里「全部 / 仅内置」和套装是同一种卡片:都能「复制切换命令」或「设为默认」;「我的套装」默认折叠,只显示当前默认的那一个(这一行下面始终留着「+ 新建套装」),点开才列出全部。已经保存的套装可以在卡片上「编辑」:改动就在那张卡片下面展开,「覆盖保存」(正在用它的对话立刻按新工具集走)或「另存为新套装」。
|
|
68
70
|
|
|
69
71
|
### 预置套装(五套,只读)
|
|
70
72
|
|
|
@@ -101,8 +103,8 @@ node scripts/client-smoke.mjs # 浏览器 bundle:菜单与设置页渲染
|
|
|
101
103
|
|
|
102
104
|
## 升级
|
|
103
105
|
|
|
104
|
-
- 0.5.x → 0.
|
|
105
|
-
- 0.4.x → 0.
|
|
106
|
+
- 0.5.x → 0.12.x:包名没变(`dsh-loadout`),直接 `plugin_manager install_bundle` 覆盖即可;套装数据在 `~/.dsh/loadout.json`,第一次启动会迁移到 `schema: 2`(旧文件备份为 `.v1.bak-<时间戳>`)。
|
|
107
|
+
- 0.4.x → 0.12.x:包名从 `dsh-plugin-kits` 改成 `dsh-loadout`,先卸旧的再装新的:
|
|
106
108
|
|
|
107
109
|
```text
|
|
108
110
|
plugin_manager action=remove_bundle target=dsh-plugin-kits
|
package/client.js
CHANGED
|
@@ -46,6 +46,28 @@ window.__ModuleLoader__.load({
|
|
|
46
46
|
return kit.note ? `${kit.title}(${kit.note})` : kit.title
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Why a name cannot be saved, in the user's terms. The host compares a kit
|
|
51
|
+
* against preset *ids* as well as titles (a preset's `name` is a stable id —
|
|
52
|
+
* `chat` — while the UI shows its title, 仅聊天), so "chat" is refused even
|
|
53
|
+
* though it "looks different". Return '' when the name is free.
|
|
54
|
+
*/
|
|
55
|
+
function nameClash(raw, presets, kits, ignore) {
|
|
56
|
+
const value = String(raw ?? '').trim().toLowerCase()
|
|
57
|
+
if (value === '') return ''
|
|
58
|
+
const preset = (presets ?? []).find((item) =>
|
|
59
|
+
String(item.name ?? '').toLowerCase() === value || String(item.title ?? '').toLowerCase() === value)
|
|
60
|
+
if (preset !== undefined) {
|
|
61
|
+
return `「${preset.title}」是预置套装(标识 ${preset.name}),不能用这个名字,换一个吧`
|
|
62
|
+
}
|
|
63
|
+
const kit = (kits ?? []).find((item) =>
|
|
64
|
+
String(item.name ?? '').toLowerCase() === value || String(item.title ?? '').toLowerCase() === value)
|
|
65
|
+
if (kit !== undefined && kit.name !== ignore) {
|
|
66
|
+
return `已经有叫「${kit.title ?? kit.name}」的套装了,换一个名字,或者进去编辑它`
|
|
67
|
+
}
|
|
68
|
+
return ''
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
/**
|
|
50
72
|
* Presets (仅聊天 / 编程用 / 查找资料 / 项目梳理 / 写作) are shown again from
|
|
51
73
|
* 0.12: they are the fastest way to start a conversation of a known kind,
|
|
@@ -133,6 +155,10 @@ window.__ModuleLoader__.load({
|
|
|
133
155
|
},
|
|
134
156
|
rowHover: { background: 'rgba(59,130,246,.08)' },
|
|
135
157
|
rowActive: { background: 'rgba(59,130,246,.10)' },
|
|
158
|
+
// The one row that creates something, not selects something: keep it
|
|
159
|
+
// blue and tinted so it reads as an action at a glance.
|
|
160
|
+
rowPrimary: { color: '#2563eb', background: 'rgba(37,99,235,.07)', fontWeight: 600 },
|
|
161
|
+
rowPrimaryHover: { background: 'rgba(37,99,235,.14)' },
|
|
136
162
|
rowDisabled: { opacity: 0.45, cursor: 'default' },
|
|
137
163
|
indent: { paddingLeft: 26 },
|
|
138
164
|
mark: { width: 12, flex: 'none', color: '#2563eb', fontSize: 12, textAlign: 'center' },
|
|
@@ -273,7 +299,8 @@ window.__ModuleLoader__.load({
|
|
|
273
299
|
...styles.row,
|
|
274
300
|
...(props.indent === true ? styles.indent : null),
|
|
275
301
|
...(props.active === true ? styles.rowActive : null),
|
|
276
|
-
...(
|
|
302
|
+
...(props.tone === 'primary' ? styles.rowPrimary : null),
|
|
303
|
+
...(hover && !disabled ? (props.tone === 'primary' ? styles.rowPrimaryHover : styles.rowHover) : null),
|
|
277
304
|
...(disabled ? styles.rowDisabled : null),
|
|
278
305
|
}
|
|
279
306
|
const activate = () => {
|
|
@@ -295,7 +322,7 @@ window.__ModuleLoader__.load({
|
|
|
295
322
|
},
|
|
296
323
|
},
|
|
297
324
|
h('span', { style: styles.mark }, props.active === true ? '✓' : ''),
|
|
298
|
-
h('span', { style: styles.rowTitle },
|
|
325
|
+
h('span', { style: { ...styles.rowTitle, ...(props.tone === 'primary' ? { color: '#2563eb' } : null) } },
|
|
299
326
|
props.tip === undefined
|
|
300
327
|
? h('span', null, props.label)
|
|
301
328
|
: h(Tip, { text: props.tip }, h('span', null, props.label))),
|
|
@@ -337,7 +364,7 @@ window.__ModuleLoader__.load({
|
|
|
337
364
|
function Builder(props) {
|
|
338
365
|
const {
|
|
339
366
|
catalog, draft, onChange, busy, onCancel, onSave, saveLabel,
|
|
340
|
-
title, onSaveAs, saveAsLabel, nameField, namePlaceholder,
|
|
367
|
+
title, onSaveAs, saveAsLabel, nameField, namePlaceholder, clash,
|
|
341
368
|
} = props
|
|
342
369
|
// Both lists start collapsed: the builder opens with a name field and a
|
|
343
370
|
// summary, not with forty checkboxes.
|
|
@@ -368,6 +395,12 @@ window.__ModuleLoader__.load({
|
|
|
368
395
|
value: draft[nameField ?? 'name'] ?? '',
|
|
369
396
|
onChange: (event) => onChange({ ...draft, [nameField ?? 'name']: event.target.value }),
|
|
370
397
|
}),
|
|
398
|
+
// The host rejects a name that a preset or another kit already uses;
|
|
399
|
+
// saying so *here* keeps the user from staring at a save button that
|
|
400
|
+
// silently does nothing.
|
|
401
|
+
typeof clash === 'string' && clash !== ''
|
|
402
|
+
? h('div', { style: { ...styles.error, marginTop: 4 } }, clash)
|
|
403
|
+
: null,
|
|
371
404
|
h('input', {
|
|
372
405
|
style: { ...styles.input, marginTop: 6 },
|
|
373
406
|
placeholder: `备注(可选,最多 ${NOTE_MAX} 字),例如 用于视频制作`,
|
|
@@ -413,7 +446,12 @@ window.__ModuleLoader__.load({
|
|
|
413
446
|
onSaveAs === undefined
|
|
414
447
|
? null
|
|
415
448
|
: h('button', { type: 'button', style: styles.ghost, disabled: busy, onClick: onSaveAs }, saveAsLabel ?? '另存为新套装'),
|
|
416
|
-
h('button', {
|
|
449
|
+
h('button', {
|
|
450
|
+
type: 'button',
|
|
451
|
+
style: { ...styles.primary, ...(typeof clash === 'string' && clash !== '' ? { opacity: 0.5 } : null) },
|
|
452
|
+
disabled: busy || (typeof clash === 'string' && clash !== ''),
|
|
453
|
+
onClick: onSave,
|
|
454
|
+
}, saveLabel ?? '保存')))
|
|
417
455
|
}
|
|
418
456
|
|
|
419
457
|
// ---- the header control -------------------------------------------------
|
|
@@ -429,7 +467,7 @@ window.__ModuleLoader__.load({
|
|
|
429
467
|
// collapsed group still shows the one active row, and clicking the head is
|
|
430
468
|
// how you go browsing.
|
|
431
469
|
const [groups, setGroups] = useState(preview?.groups ?? { kits: false, presets: false, extras: false, applied: false })
|
|
432
|
-
const [builder, setBuilder] = useState(null)
|
|
470
|
+
const [builder, setBuilder] = useState(preview?.builder ?? null)
|
|
433
471
|
const [saveAs, setSaveAs] = useState(false)
|
|
434
472
|
const [saveName, setSaveName] = useState('')
|
|
435
473
|
const [saveNote, setSaveNote] = useState('')
|
|
@@ -696,9 +734,10 @@ window.__ModuleLoader__.load({
|
|
|
696
734
|
tip: '你自己保存的套装。点一下即可切换。',
|
|
697
735
|
onToggle: () => setGroups({ ...groups, kits: !groups.kits }),
|
|
698
736
|
}),
|
|
699
|
-
// A collapsed group shows
|
|
700
|
-
//
|
|
737
|
+
// A collapsed group shows the set that is on, plus the one action that
|
|
738
|
+
// creates a set — so "new kit" never needs a click on the head first.
|
|
701
739
|
activeUserKit === undefined ? null : h(MenuRow, {
|
|
740
|
+
key: 'active-kit',
|
|
702
741
|
label: kitLabel(activeUserKit),
|
|
703
742
|
hint: activeUserKit.isDefault ? '默认' : '',
|
|
704
743
|
active: true,
|
|
@@ -706,27 +745,31 @@ window.__ModuleLoader__.load({
|
|
|
706
745
|
onClick: () => run({ action: 'applyBase', base: 'kit', kit: activeUserKit.name }),
|
|
707
746
|
}),
|
|
708
747
|
groups.kits === true
|
|
709
|
-
?
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
hint: kit.isDefault ? '默认' : '',
|
|
716
|
-
indent: true,
|
|
717
|
-
onClick: () => {
|
|
718
|
-
run({ action: 'applyBase', base: 'kit', kit: kit.name })
|
|
719
|
-
// Picking one puts the list away again; the row that stays is the
|
|
720
|
-
// one just chosen, right under the group head.
|
|
721
|
-
setGroups({ ...groups, kits: false })
|
|
722
|
-
},
|
|
723
|
-
})),
|
|
724
|
-
h(MenuRow, {
|
|
725
|
-
label: '+ 新建套装',
|
|
748
|
+
? kits
|
|
749
|
+
.filter((kit) => activeUserKit === undefined || kit.name !== activeUserKit.name)
|
|
750
|
+
.map((kit) => h(MenuRow, {
|
|
751
|
+
key: kit.name,
|
|
752
|
+
label: kitLabel(kit),
|
|
753
|
+
hint: kit.isDefault ? '默认' : '',
|
|
726
754
|
indent: true,
|
|
727
|
-
onClick: () =>
|
|
755
|
+
onClick: () => {
|
|
756
|
+
run({ action: 'applyBase', base: 'kit', kit: kit.name })
|
|
757
|
+
// Picking one puts the list away again; the row that stays is the
|
|
758
|
+
// one just chosen, right under the group head.
|
|
759
|
+
setGroups({ ...groups, kits: false })
|
|
760
|
+
},
|
|
728
761
|
}))
|
|
729
762
|
: null,
|
|
763
|
+
// Always last inside 我的套装, open or collapsed: the create action sits
|
|
764
|
+
// where the eye ends up after scanning the kits.
|
|
765
|
+
h(MenuRow, {
|
|
766
|
+
key: 'new-kit',
|
|
767
|
+
label: '+ 新建套装',
|
|
768
|
+
tone: 'primary',
|
|
769
|
+
hint: '从零配一套',
|
|
770
|
+
indent: true,
|
|
771
|
+
onClick: () => openBuilder({ builtins: allBuiltins, plugins: [] }),
|
|
772
|
+
}),
|
|
730
773
|
SHOW_PRESETS === true
|
|
731
774
|
? h(GroupHead, {
|
|
732
775
|
title: '预设套装',
|
|
@@ -850,6 +893,7 @@ window.__ModuleLoader__.load({
|
|
|
850
893
|
onCancel: () => setBuilder(null),
|
|
851
894
|
onSave: saveBuilder,
|
|
852
895
|
saveLabel: '保存并启用',
|
|
896
|
+
clash: nameClash(builder.name, presets, kits, null),
|
|
853
897
|
})
|
|
854
898
|
|
|
855
899
|
return h('div', { style: styles.wrap, ref: box },
|
|
@@ -954,7 +998,7 @@ window.__ModuleLoader__.load({
|
|
|
954
998
|
const [data, setData] = useState(preview?.data ?? null)
|
|
955
999
|
const [busy, setBusy] = useState(false)
|
|
956
1000
|
const [message, setMessage] = useState('')
|
|
957
|
-
const [draft, setDraft] = useState(null)
|
|
1001
|
+
const [draft, setDraft] = useState(preview?.draft ?? null)
|
|
958
1002
|
// Editing a saved kit reuses the same builder as creating one; only the
|
|
959
1003
|
// action buttons differ (覆盖保存 keeps the name, 另存为新套装 mints a new one).
|
|
960
1004
|
const [editingKit, setEditingKit] = useState(null)
|
|
@@ -1146,6 +1190,7 @@ window.__ModuleLoader__.load({
|
|
|
1146
1190
|
onCancel: () => setDraft(null),
|
|
1147
1191
|
onSave: saveDraft,
|
|
1148
1192
|
saveLabel: '保存',
|
|
1193
|
+
clash: nameClash(draft.name, presets, kits, null),
|
|
1149
1194
|
})),
|
|
1150
1195
|
message === '' ? null : h('div', { style: styles.error }, message),
|
|
1151
1196
|
h('div', { style: styles.ledger },
|
|
@@ -1296,6 +1341,7 @@ window.__ModuleLoader__.load({
|
|
|
1296
1341
|
},
|
|
1297
1342
|
onSave: saveOverwrite,
|
|
1298
1343
|
saveLabel: '覆盖保存',
|
|
1344
|
+
clash: nameClash(draft.title, presets, kits, editingKit),
|
|
1299
1345
|
onSaveAs: saveEditingAsNew,
|
|
1300
1346
|
saveAsLabel: '另存为新套装',
|
|
1301
1347
|
}))
|
package/index.js
CHANGED
|
@@ -98,7 +98,8 @@ function assertKitName(raw, presets) {
|
|
|
98
98
|
if (NAME_FORBIDDEN.test(value)) throw new Error('名字里不能有 / \\ : * ? " < > | 或控制字符')
|
|
99
99
|
const lower = value.toLowerCase()
|
|
100
100
|
if (RESERVED_NAMES.includes(lower)) throw new Error(`"${value}" 是保留名字,请换一个名字`)
|
|
101
|
-
|
|
101
|
+
const preset = presets === undefined ? undefined : findKit(presets, value)
|
|
102
|
+
if (preset !== undefined) throw new Error(`「${preset.title}」是预置套装(标识 ${preset.name}),不能占用这个名字`)
|
|
102
103
|
return value
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -1237,7 +1238,7 @@ export function apply(ctx, config = {}) {
|
|
|
1237
1238
|
}
|
|
1238
1239
|
try {
|
|
1239
1240
|
// A rejected restriction names every global tool it knows about.
|
|
1240
|
-
tools.restrict({ deny: ['
|
|
1241
|
+
tools.restrict({ deny: ['__dsh_loadout_probe__'] })
|
|
1241
1242
|
} catch (error) {
|
|
1242
1243
|
const message = String(error instanceof Error ? error.message : error)
|
|
1243
1244
|
const match = /known global tools: ([^;]+)/u.exec(message)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loadout",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
4
4
|
"description": "Loadout for DeepSeek Harness: group tools into kits, keep only the current kit's tools visible in each conversation, and manage everything from a settings panel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
package/scripts/client-smoke.mjs
CHANGED
|
@@ -233,6 +233,8 @@ if (menuDefault !== undefined) {
|
|
|
233
233
|
check('presets start collapsed', !flat.includes('写作'), flat.slice(0, 400))
|
|
234
234
|
check('user kits start collapsed', !flat.includes('另一套'), flat.slice(0, 400))
|
|
235
235
|
check('the active kit is the only kit row while collapsed', flat.includes('我的套(用于视频制作)'), flat.slice(0, 400))
|
|
236
|
+
// Creating a kit must not need a click on the group head first.
|
|
237
|
+
check('the new-kit row is visible while my kits are collapsed', flat.includes('+ 新建套装'), flat.slice(0, 400))
|
|
236
238
|
check('applied plugins start collapsed', !flat.includes('alpha 插件'), flat.slice(0, 400))
|
|
237
239
|
check('applied plugins collapse into one summary row', flat.includes('已应用 1 个插件'), flat.slice(0, 400))
|
|
238
240
|
check('menu opens with a status line', flat.includes('当前:我的套') && flat.includes('加挂 1'), flat.slice(0, 200))
|
|
@@ -359,9 +361,28 @@ try {
|
|
|
359
361
|
const presetExpandedText = textOf(presetExpanded)
|
|
360
362
|
check('only the default preset carries the default tag',
|
|
361
363
|
presetExpandedText.includes('仅聊天') && presetExpandedText.includes('默认'), presetExpandedText.slice(0, 400))
|
|
364
|
+
// 「+ 新建套装」 is the only row that creates something; it must read as an
|
|
365
|
+
// action (blue, tinted), not as another selectable kit.
|
|
366
|
+
const menuWithKits = expand(header.component({ sessionId: 'session-1', preview: { data: fixture, open: true, groups: { kits: true, presets: false, extras: false } } }))
|
|
367
|
+
const newKit = nodesIn(menuWithKits, (node) => node.props?.role === 'menuitem' && node.props?.style?.color === '#2563eb' && textOf(node).includes('+ 新建套装'))
|
|
368
|
+
check('the new-kit row is highlighted as an action', newKit.length === 1, String(newKit.length))
|
|
362
369
|
const renameButtons = nodesIn(panel, (node) => node.type === 'button' && textOf(node) === '重命名')
|
|
363
370
|
// Renaming moved into 编辑: one editor for name, note and tools on the card.
|
|
364
371
|
check('renaming lives inside 编辑 rather than its own button', renameButtons.length === 0 && editButtons.length === 2, `${renameButtons.length} · 重命名 / ${editButtons.length} · 编辑`)
|
|
372
|
+
|
|
373
|
+
// A name the host would refuse is explained where the user typed it: a preset's
|
|
374
|
+
// `name` is a private id (`chat`), so "chat" looks free and is not.
|
|
375
|
+
const clashPanel = expand(registration.component({
|
|
376
|
+
preview: { data: fixture, draft: { name: 'chat', note: '', builtins: ['read'], plugins: [] } },
|
|
377
|
+
}))
|
|
378
|
+
const clashText = textOf(clashPanel)
|
|
379
|
+
check('a name taken by a preset is explained inline',
|
|
380
|
+
clashText.includes('是预置套装') && clashText.includes('仅聊天'), clashText.slice(0, 600))
|
|
381
|
+
const clashKitPanel = expand(registration.component({
|
|
382
|
+
preview: { data: fixture, draft: { name: '我的套', note: '', builtins: ['read'], plugins: [] } },
|
|
383
|
+
}))
|
|
384
|
+
check('a name taken by my own kit is explained inline',
|
|
385
|
+
textOf(clashKitPanel).includes('已经有叫「我的套」的套装了'), textOf(clashKitPanel).slice(0, 600))
|
|
365
386
|
} catch (error) {
|
|
366
387
|
failures.push(`panel render threw: ${error instanceof Error ? error.message : String(error)}`)
|
|
367
388
|
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Is there a host service that maps a session id to its live agent?
|
|
3
|
-
*/
|
|
4
|
-
import { readFileSync, readdirSync } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
const BASE = 'C:/Users/JungleYXu/AppData/Local/Programs/DeepSeek Harness/resources/app.asar/dsh/node_modules/@deepseek-ai'
|
|
7
|
-
const packages = readdirSync(BASE)
|
|
8
|
-
const pattern = /agentFor|bySession|sessionAgent|agentOf|resolveAgent/g
|
|
9
|
-
|
|
10
|
-
for (const name of packages) {
|
|
11
|
-
if (!name.startsWith('dsh-')) continue
|
|
12
|
-
let text
|
|
13
|
-
try {
|
|
14
|
-
text = readFileSync(`${BASE}/${name}/lib/index.js`, 'utf8')
|
|
15
|
-
} catch {
|
|
16
|
-
continue
|
|
17
|
-
}
|
|
18
|
-
const hits = [...new Set([...text.matchAll(pattern)].map((m) => m[0]))]
|
|
19
|
-
if (hits.length === 0) continue
|
|
20
|
-
const index = text.search(pattern)
|
|
21
|
-
console.log(`\n### ${name} :: ${hits.join(', ')}`)
|
|
22
|
-
console.log(text.slice(Math.max(0, index - 200), index + 260).replace(/\s+/g, ' '))
|
|
23
|
-
}
|
|
24
|
-
|
package/scripts/agent-probe.mjs
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How does host code get a session id from an agent object?
|
|
3
|
-
*/
|
|
4
|
-
import { readFileSync } from 'node:fs'
|
|
5
|
-
|
|
6
|
-
const BASE = 'C:/Users/JungleYXu/AppData/Local/Programs/DeepSeek Harness/resources/app.asar/dsh/node_modules/@deepseek-ai'
|
|
7
|
-
const files = [
|
|
8
|
-
'dsh-tools/lib/index.js',
|
|
9
|
-
'dsh-agent/lib/index.js',
|
|
10
|
-
'dsh-session/lib/index.js',
|
|
11
|
-
'dsh-subagent/lib/index.js',
|
|
12
|
-
]
|
|
13
|
-
|
|
14
|
-
for (const rel of files) {
|
|
15
|
-
let text
|
|
16
|
-
try {
|
|
17
|
-
text = readFileSync(`${BASE}/${rel}`, 'utf8')
|
|
18
|
-
} catch (error) {
|
|
19
|
-
console.log(`skip ${rel}: ${error.code}`)
|
|
20
|
-
continue
|
|
21
|
-
}
|
|
22
|
-
const hits = [...text.matchAll(/agent\.session[A-Za-z]*/g)].map((m) => m[0])
|
|
23
|
-
const unique = [...new Set(hits)]
|
|
24
|
-
console.log(`${rel} -> ${unique.join(', ') || '(none)'}`)
|
|
25
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Offline probe: does the package-name heuristic actually recover the plugins
|
|
3
|
-
* that own the host-registered tools? Prints the derived index and the mapping
|
|
4
|
-
* for a list of real tool names, so the heuristic can be checked before the app
|
|
5
|
-
* is restarted.
|
|
6
|
-
*
|
|
7
|
-
* ELECTRON_RUN_AS_NODE=1 "DeepSeek Harness.exe" scripts/attribution-probe.mjs
|
|
8
|
-
*/
|
|
9
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
10
|
-
import { homedir } from 'node:os'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
|
-
|
|
13
|
-
const BUILTIN_PREFIXES = new Set([
|
|
14
|
-
'todo', 'web', 'kit', 'agent', 'work', 'read', 'write', 'edit', 'glob', 'grep',
|
|
15
|
-
'ask', 'present', 'skill', 'workflow', 'subagent', 'send', 'list', 'interrupt',
|
|
16
|
-
'job', 'schedule', 'create', 'get', 'update', 'exit', 'compress', 'decompress',
|
|
17
|
-
'search', 'load', 'working',
|
|
18
|
-
])
|
|
19
|
-
|
|
20
|
-
const NAME_NOISE = new Set([
|
|
21
|
-
'core', 'tools', 'tool', 'client', 'server', 'shared', 'common', 'plugin', 'plugins',
|
|
22
|
-
'dsh', 'ui', 'web', 'harness', 'extension', 'preset', 'settings', 'sidebar', 'runtime',
|
|
23
|
-
])
|
|
24
|
-
|
|
25
|
-
const SELF = 'dsh-loadout'
|
|
26
|
-
|
|
27
|
-
const REAL_GLOBAL_TOOLS = [
|
|
28
|
-
'kit_list', 'kit_apply', 'kit_create', 'kit_set_tools', 'kit_set_default', 'kit_delete',
|
|
29
|
-
'modlens_read_image',
|
|
30
|
-
'weknora_list_knowledge_bases', 'weknora_search', 'weknora_read_document', 'weknora_ask',
|
|
31
|
-
'hindsight_sync_status', 'hindsight_diagnose', 'hindsight_search_knowledge_pages',
|
|
32
|
-
'hindsight_list_knowledge_pages', 'hindsight_read_knowledge_page', 'hindsight_reflect',
|
|
33
|
-
'hindsight_capture_initiative', 'hindsight_ingest_document',
|
|
34
|
-
'working_directory',
|
|
35
|
-
'vision_describe', 'vision_bootstrap', 'vision_materialize', 'vision_ground', 'vision_detect',
|
|
36
|
-
'vision_crop', 'vision_present', 'vision_pixel_diff', 'vision_colors', 'vision_ocr',
|
|
37
|
-
'vision_long_screenshot_ocr', 'vision_trace', 'vision_extract_foreground', 'vision_html_screenshot',
|
|
38
|
-
'list_mcp_resources', 'list_mcp_resource_templates', 'read_mcp_resource',
|
|
39
|
-
'compress', 'decompress', 'search_context',
|
|
40
|
-
'acp_status', 'acp_cache', 'acp_read_original_image',
|
|
41
|
-
'load_workspace_dependencies',
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
function profilePackages() {
|
|
45
|
-
const root = join(homedir(), '.dsh', 'profiles')
|
|
46
|
-
const names = new Set()
|
|
47
|
-
for (const entry of readdirSync(root)) {
|
|
48
|
-
const file = join(root, entry, 'package.json')
|
|
49
|
-
if (!existsSync(file)) continue
|
|
50
|
-
const parsed = JSON.parse(readFileSync(file, 'utf8'))
|
|
51
|
-
for (const name of Object.keys(parsed.dependencies ?? {})) names.add(name)
|
|
52
|
-
for (const bundle of parsed?.dsh?.profile?.bundles ?? []) if (typeof bundle === 'string') names.add(bundle)
|
|
53
|
-
}
|
|
54
|
-
return [...names]
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function buildIndex(packages) {
|
|
58
|
-
const index = new Map()
|
|
59
|
-
for (const packageName of packages) {
|
|
60
|
-
const base = packageName.replace(/^@[^/]+\//, '')
|
|
61
|
-
if (base === SELF) continue
|
|
62
|
-
const stripped = base.replace(/^dsh-/, '').replace(/^plugin-/, '')
|
|
63
|
-
const tokens = [stripped, ...stripped.split('-')]
|
|
64
|
-
.map((token) => token.toLowerCase())
|
|
65
|
-
.filter((token) => token.length >= 3 && !NAME_NOISE.has(token) && !BUILTIN_PREFIXES.has(token))
|
|
66
|
-
for (const token of tokens) if (!index.has(token)) index.set(token, { id: token, title: stripped, packageName: base })
|
|
67
|
-
}
|
|
68
|
-
return index
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const packages = profilePackages()
|
|
72
|
-
const index = buildIndex(packages)
|
|
73
|
-
console.log(`profile packages: ${packages.length}`)
|
|
74
|
-
console.log(`tokens: ${[...index.keys()].sort().join(', ')}`)
|
|
75
|
-
console.log('')
|
|
76
|
-
const grouped = new Map()
|
|
77
|
-
const orphan = []
|
|
78
|
-
for (const toolName of REAL_GLOBAL_TOOLS) {
|
|
79
|
-
const cut = toolName.indexOf('_')
|
|
80
|
-
const prefix = cut > 0 ? toolName.slice(0, cut).toLowerCase() : ''
|
|
81
|
-
const owner = prefix === '' || BUILTIN_PREFIXES.has(prefix) ? undefined : index.get(prefix)
|
|
82
|
-
if (owner === undefined) { orphan.push(toolName); continue }
|
|
83
|
-
if (!grouped.has(owner.title)) grouped.set(owner.title, [])
|
|
84
|
-
grouped.get(owner.title).push(toolName)
|
|
85
|
-
}
|
|
86
|
-
for (const [title, tools] of [...grouped.entries()].sort()) {
|
|
87
|
-
console.log(`${title} (${tools.length})`)
|
|
88
|
-
console.log(` ${tools.join(', ')}`)
|
|
89
|
-
}
|
|
90
|
-
console.log('')
|
|
91
|
-
console.log(`built-in / unattributed (${orphan.length}): ${orphan.join(', ')}`)
|
package/scripts/cmd-probe.mjs
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What can a client plugin do with the command system, and how is a session id
|
|
3
|
-
* exposed to a session-scoped slot?
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync } from 'node:fs'
|
|
6
|
-
|
|
7
|
-
const BASE = 'C:/Users/JungleYXu/AppData/Local/Programs/DeepSeek Harness/resources/app.asar/dsh/node_modules/@deepseek-ai'
|
|
8
|
-
|
|
9
|
-
function show(rel, pattern, width = 260, limit = 6) {
|
|
10
|
-
let text
|
|
11
|
-
try {
|
|
12
|
-
text = readFileSync(`${BASE}/${rel}`, 'utf8')
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.log(`skip ${rel}: ${error.code}`)
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
console.log(`\n##### ${rel}`)
|
|
18
|
-
let index = text.search(pattern)
|
|
19
|
-
let shown = 0
|
|
20
|
-
while (index !== -1 && shown < limit) {
|
|
21
|
-
console.log('...' + text.slice(Math.max(0, index - 80), index + width).replace(/\s+/g, ' '))
|
|
22
|
-
index = text.slice(index + 1).search(pattern)
|
|
23
|
-
if (index !== -1) index += 1
|
|
24
|
-
shown += 1
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
show('dsh-client-ui-commands/lib/client.js', /submitCommand|runCommand|executeCommand/)
|
|
29
|
-
show('dsh-client-commands/lib/index.js', /^\s*(run|execute|dispatch|submit)\(|exports\.(run|execute|dispatch|submit)/, 200, 8)
|
|
30
|
-
show('dsh-client-ui-conversation/lib/client.js', /inject: \(sessionId\) =>/, 700, 2)
|
package/scripts/dock-probe.mjs
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Look at how the shipped bundles use the composer-area slots, so the kit bar
|
|
3
|
-
* can plug into a real surface with real props.
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync, readdirSync } from 'node:fs'
|
|
6
|
-
|
|
7
|
-
const BASE = 'C:/Users/JungleYXu/AppData/Local/Programs/DeepSeek Harness/resources/app.asar/dsh/node_modules/@deepseek-ai'
|
|
8
|
-
|
|
9
|
-
function files(pkg) {
|
|
10
|
-
const out = []
|
|
11
|
-
const walk = (dir, depth) => {
|
|
12
|
-
if (depth > 3) return
|
|
13
|
-
let names = []
|
|
14
|
-
try {
|
|
15
|
-
names = readdirSync(dir)
|
|
16
|
-
} catch {
|
|
17
|
-
return
|
|
18
|
-
}
|
|
19
|
-
for (const name of names) {
|
|
20
|
-
if (name === 'node_modules') continue
|
|
21
|
-
const full = `${dir}/${name}`
|
|
22
|
-
if (name.endsWith('.js') || name.endsWith('.d.ts')) out.push(full)
|
|
23
|
-
else if (!name.includes('.')) walk(full, depth + 1)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
walk(`${BASE}/${pkg}`, 0)
|
|
27
|
-
return out
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const targets = ['composer.dock', 'conversation.composer', 'session.header.utilities', 'shell.overlay']
|
|
31
|
-
const pkgs = readdirSync(BASE).filter((n) => n.startsWith('dsh-client-ui'))
|
|
32
|
-
|
|
33
|
-
for (const pkg of pkgs) {
|
|
34
|
-
for (const file of files(pkg)) {
|
|
35
|
-
let text
|
|
36
|
-
try {
|
|
37
|
-
text = readFileSync(file, 'utf8')
|
|
38
|
-
} catch {
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
for (const target of targets) {
|
|
42
|
-
let index = text.indexOf(target)
|
|
43
|
-
let shown = 0
|
|
44
|
-
while (index !== -1 && shown < 2) {
|
|
45
|
-
const start = Math.max(0, index - 320)
|
|
46
|
-
console.log(`\n=== ${pkg}/${file.split('/').slice(-2).join('/')} :: ${target}`)
|
|
47
|
-
console.log(text.slice(start, index + 320).replace(/\s+/g, ' '))
|
|
48
|
-
index = text.indexOf(target, index + 1)
|
|
49
|
-
shown += 1
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
package/scripts/slots-probe.mjs
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Find real slot names in the shipped client bundles.
|
|
3
|
-
* Electron's fs is asar-aware, so archive-internal paths start with '/'.
|
|
4
|
-
*/
|
|
5
|
-
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
6
|
-
|
|
7
|
-
const ROOT = '/dsh/node_modules/@deepseek-ai'
|
|
8
|
-
const found = new Map()
|
|
9
|
-
|
|
10
|
-
function safeReaddir(dir) {
|
|
11
|
-
try {
|
|
12
|
-
return readdirSync(dir, { withFileTypes: false })
|
|
13
|
-
} catch {
|
|
14
|
-
try {
|
|
15
|
-
return readdirSync(dir)
|
|
16
|
-
} catch {
|
|
17
|
-
return []
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const packages = safeReaddir(ROOT).filter((name) => name.startsWith('dsh-client-ui') || name.startsWith('dsh-client-'))
|
|
23
|
-
console.log(`packages: ${packages.length}`)
|
|
24
|
-
|
|
25
|
-
const pattern = /["']((?:shell|conversation|settings|sidebar|composer|workspace|agent|home|chat|message|session)\.[a-zA-Z.]+)["']/g
|
|
26
|
-
|
|
27
|
-
function scan(dir, depth) {
|
|
28
|
-
if (depth > 3) return
|
|
29
|
-
for (const entry of safeReaddir(dir)) {
|
|
30
|
-
const full = `${dir}/${entry}`
|
|
31
|
-
let isDir = false
|
|
32
|
-
try {
|
|
33
|
-
isDir = statSync(full).isDirectory()
|
|
34
|
-
} catch {
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
if (isDir) {
|
|
38
|
-
if (entry === 'node_modules') continue
|
|
39
|
-
scan(full, depth + 1)
|
|
40
|
-
continue
|
|
41
|
-
}
|
|
42
|
-
if (!entry.endsWith('.js')) continue
|
|
43
|
-
let text
|
|
44
|
-
try {
|
|
45
|
-
text = readFileSync(full, 'utf8')
|
|
46
|
-
} catch {
|
|
47
|
-
continue
|
|
48
|
-
}
|
|
49
|
-
for (const match of text.matchAll(pattern)) {
|
|
50
|
-
const slot = match[1]
|
|
51
|
-
if (slot.split('.').length < 2) continue
|
|
52
|
-
found.set(slot, (found.get(slot) ?? 0) + 1)
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
for (const name of packages) scan(`${ROOT}/${name}`, 0)
|
|
58
|
-
|
|
59
|
-
for (const [slot, count] of [...found.entries()].sort()) console.log(`${slot}\t${count}`)
|