dsh-preset-studio 0.0.0-stage → 0.1.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/README.md +387 -2
- package/cordis.patch.yml +15 -0
- package/lib/client.js +3482 -0
- package/lib/index.js +2040 -0
- package/lib/patch-doc.js +683 -0
- package/package.json +45 -4
package/README.md
CHANGED
|
@@ -1,3 +1,388 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-preset-studio
|
|
2
|
+
|
|
3
|
+
DSH 的**预设工作室**:在设置里用一个整页看清、并且**直接改**本 profile 的
|
|
4
|
+
Agent 预设——它们由哪些插件行组成、系统提示词写了什么、哪里坏了、来自配置文件
|
|
5
|
+
的哪一行;然后新建、编辑、复制、删除、对比、导出,每一步都可回滚。
|
|
6
|
+
|
|
7
|
+
## 它解决什么
|
|
8
|
+
|
|
9
|
+
Agent 预设(如「快准狠模式」)在配置里是一大块 YAML:一个
|
|
10
|
+
`@deepseek-ai/dsh-agent-preset` 声明行,`config.plugins` 里塞着几十行插件、
|
|
11
|
+
组、`!!js` 条件开关,再加上一段 persona 提示词。这套结构平时只能靠翻
|
|
12
|
+
`cordis.patch.yml` 才能看明白,而那份文件通常上千行。
|
|
13
|
+
|
|
14
|
+
这个插件把那块内容变成可操作的页面:
|
|
15
|
+
|
|
16
|
+
| 呈现 | 说明 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| 预设清单 | 本 profile 声明的 + 内置的,按 `order` 排序,标注默认项与健康状况 |
|
|
19
|
+
| 组成树 | 每个插件行的启用 / 关闭 / 条件状态,组的嵌套缩进,`isolate` 标注 |
|
|
20
|
+
| 组成统计 | 声明行数、去重后的插件模块数、启用 / 关闭 / 条件 / 组计数 |
|
|
21
|
+
| 系统提示词 | persona 行的 `prefix` 与 `suffix` 分段展示,以及用到的 `{{变量}}` |
|
|
22
|
+
| 提示词校验 | `{{变量}}` 写错的位置与原因(见下) |
|
|
23
|
+
| 装配诊断 | 预设装配失败时,registry 报出的原始原因 |
|
|
24
|
+
| 来源定位 | 该预设来自 patch 文件的第几行,是顶层行还是 `insert` 行,能否就地编辑 |
|
|
25
|
+
| 编辑器 | 改标题 / 描述 / 排序 / 插件方块网格(点方块即开关、一键全开全关、条件、`isolate`)/ 提示词 |
|
|
26
|
+
| 新建预设 | 默认照「标准模式」把插件都摆上,取消掉不要的即可,不必从空白一个个加 |
|
|
27
|
+
| 插件分类 | 方块按来源分两栏:「官方自带」与「你自己装的」 |
|
|
28
|
+
| 对比 | 与任一其他预设逐行比:只有它有、只有对方有、开关不同 |
|
|
29
|
+
| 复制为副本 | 从任意预设(含内置的「标准模式」)派生一份新预设,页面内表单填 id 与名称 |
|
|
30
|
+
| 快照与回滚 | 每次写入前的自动快照,可列出、可回滚,回滚本身也可回滚 |
|
|
31
|
+
| 导出为 bundle | 直接生成可发布的 `dsh-preset-<id>` 包(三个文件)到导出目录 |
|
|
32
|
+
| MCP 服务器 | 本 profile 声明的 MCP 服务器:传输方式、启动命令、超时、重连、启用状态,可增删改 |
|
|
33
|
+
| 工具可见性 | 每个预设能看到哪些工具:家族整体开关 + 展开后逐工具开关,写回它自己的 `denyPrefixes` |
|
|
34
|
+
|
|
35
|
+
导航是单列多级下钻:清单 → 某个预设 → 某个分区(组成 / 系统提示词 / 属性 /
|
|
36
|
+
运行时行状态),每层都是整宽单列,窄屏与移动端不需要横向挤压,返回即面包屑。
|
|
37
|
+
「MCP 与工具」与「快照与回滚」是清单层并列的入口,不在某个预设里面——它们管的是
|
|
38
|
+
整个 profile 的运行时形状,不属于某一个预设。
|
|
39
|
+
|
|
40
|
+
## 提示词变量为什么值得单独校验
|
|
41
|
+
|
|
42
|
+
`{{变量}}` 在 DSH 里是**严格校验**的,不是宽松的模板替换。渲染一个引用
|
|
43
|
+
了未注册变量的 section 会**抛错**,后果是该 persona section 注册失败、
|
|
44
|
+
整个预设装配不出来。而报错信息只在服务端日志里,界面上只会看到一个
|
|
45
|
+
装配失败的预设,看不出是哪个词写错了。
|
|
46
|
+
|
|
47
|
+
本插件复刻 `@deepseek-ai/dsh-system-prompt` 的扫描逻辑,在页面上直接指出:
|
|
48
|
+
|
|
49
|
+
- **未知变量** —— `{{nope}}`,并列出实际可用的三个:`cwd`、`model`、`provider`
|
|
50
|
+
- **变量名不合法** —— 不符合 `[a-z][a-z0-9_]*`,例如 `{{Cwd}}`
|
|
51
|
+
- **引用写法不完整** —— 有 `{{` 也有后面的 `}}`,但中间不是简单名字
|
|
52
|
+
|
|
53
|
+
扫描同时复刻了它的一处细节:**孤立的 `{{` 之后如果没有 `}}`,那是普通文本,
|
|
54
|
+
不是错误**。所以这里不会对散文里出现的 `{{` 误报。
|
|
55
|
+
|
|
56
|
+
编辑器在保存前用同一套规则挡住未知变量,不让一次手误把预设写坏。
|
|
57
|
+
|
|
58
|
+
## 安装
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
dsh plugin --profile <profile> add dsh-preset-studio
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
或手动在 profile 的 `cordis.patch.yml` 里加:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
- insert:
|
|
68
|
+
- id: preset-studio
|
|
69
|
+
name: 'dsh-preset-studio'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
装好后重启 DSH,打开**设置 → 预设工作室**。
|
|
73
|
+
|
|
74
|
+
## 入口为什么是独立一页
|
|
75
|
+
|
|
76
|
+
它注册在 `settings.section`,id `preset-studio`,`order: 21`——紧挨在
|
|
77
|
+
官方「Agent 预设」页(`order: 20`)后面,因此侧栏里两页相邻。
|
|
78
|
+
|
|
79
|
+
之所以不能把功能**并进**官方那一页,有三个硬原因:
|
|
80
|
+
|
|
81
|
+
1. 官方页的 `settings.section` 注册**没有声明 `children`**,第三方无法往它内部
|
|
82
|
+
挂任何东西;往里塞内容只能改官方包源码。
|
|
83
|
+
2. 提供该页的包同时提供新会话的预设选择 chip 与会话头标签,禁用它腾出位置会
|
|
84
|
+
连带失去选择器。
|
|
85
|
+
3. `settings.section` 是 `list` 类型(并列追加),没有「顶掉同位置条目」的
|
|
86
|
+
选举语义,所以也无法用自己的条目替换官方条目。
|
|
87
|
+
|
|
88
|
+
## 写入路径:为什么不能用官方的配置编辑器
|
|
89
|
+
|
|
90
|
+
`@deepseek-ai/dsh-config-editor` 的 `entries()` 只认 `fiber.entry?.id === "include"`
|
|
91
|
+
的行,且 `configuration()` / `edit()` 会**主动排除带 `insert` 的行**。而本机两个
|
|
92
|
+
预设恰好都是 `- insert:` 的子行,官方编辑器根本看不见它们。它也是 asar 专属包,
|
|
93
|
+
外部插件装不上。
|
|
94
|
+
|
|
95
|
+
所以 host 半自己实现 YAML 读写(`lib/patch-doc.js`),复刻官方 `edit()` 的安全
|
|
96
|
+
手法,并针对预设结构做了三件官方编辑器不会做的事:
|
|
97
|
+
|
|
98
|
+
| 手法 | 原因 |
|
|
99
|
+
|---|---|
|
|
100
|
+
| `yaml`(eemeli)的 `parseDocument` + `toString` | 保留注释与格式;`js-yaml` 的 load/dump 会把整份文件的注释抹掉 |
|
|
101
|
+
| `carryComments(oldNode, newNode)` | `config` 是**整块替换**(`applyEntryPatches` 里是 `target[key] = value`,不是深合并),替换后必须把原节点的 `commentBefore` / `comment` / `spaceBefore` 搬回来,否则一次开关翻转就会丢掉注释 |
|
|
102
|
+
| `!!js` 标记改写为带 tag 的标量 | `createNode` 无法设置自定义 tag,条件开关必须以 `!!js` 写回,否则会被序列化成字符串 |
|
|
103
|
+
|
|
104
|
+
对真实那份 1057 行、357 条注释的 `cordis.patch.yml` 做整份重写,注释一条不少,
|
|
105
|
+
字节增量在 3 KB 以内,且结果在 boot 方言下可加载。
|
|
106
|
+
|
|
107
|
+
**写入的三条硬约束:**
|
|
108
|
+
|
|
109
|
+
1. `config` 整块替换 → 界面持有完整插件树,哪怕只翻一个开关也会重述整个
|
|
110
|
+
`plugins` 数组。
|
|
111
|
+
2. **本插件新建的预设写成 `- insert:` 子行**,理由见下一节——顶层 `- id:` 行在
|
|
112
|
+
找不到目标时会被**静默跳过**,看起来像写成功了,实际什么都没挂上。
|
|
113
|
+
3. `update` 永不改写 `id`:会话里存的是它启动时的预设 id,改了会让既有会话脱钩。
|
|
114
|
+
|
|
115
|
+
## 为什么新建的行必须是 insert 行
|
|
116
|
+
|
|
117
|
+
`applyEntryPatches` 组装配置树时是从**空树**开始的:
|
|
118
|
+
|
|
119
|
+
```js
|
|
120
|
+
function composeEntries(layers, warn) {
|
|
121
|
+
return applyEntryPatches([], structuredClone(layers.flat()), warn);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
也就是说树只由 `- insert:` 行搭起来。随后每条补丁按 `id` 在树里找目标:
|
|
126
|
+
|
|
127
|
+
- **带 `id` 的顶层行** —— 只作为**覆盖目标**参与,找不到就
|
|
128
|
+
`warn("patch: entry %C not found")` 然后 `continue`。它自己不建树。
|
|
129
|
+
- **`- insert:` 的子行** —— 无条件挂上去,并立刻进索引,后面的补丁可以再定位它。
|
|
130
|
+
|
|
131
|
+
用真实的三层(`dsh-base` + `dsh-web-app` + profile,共 171 行)实测过:
|
|
132
|
+
本机两个预设 `quickfix` / `razor` 都是 insert 子行,都能挂上;而 profile 里两条
|
|
133
|
+
用户自己写的顶层行(`ui-settings-account`、`agent-preset-registry`)今天就是
|
|
134
|
+
**死行**——它们的 id 在整棵组合树里根本不存在,只在启动日志里留两条警告。
|
|
135
|
+
|
|
136
|
+
所以新建预设、新建 MCP 服务器一律走 `appendInsertChild`,唯一合法的形状是
|
|
137
|
+
`- insert: [ … ]`。
|
|
138
|
+
|
|
139
|
+
**导出是刻意的反向选择**:bundle 补丁同样用 `- insert:`,因为它要被叠加到别人那份
|
|
140
|
+
还没有这个预设的 composition 上;写成 id 定位的顶层行会匹配不到任何东西,被
|
|
141
|
+
静默跳过并只留一条警告。
|
|
142
|
+
|
|
143
|
+
### 插件方块与开关
|
|
144
|
+
|
|
145
|
+
插件列表是一格一格的方块,一个方块就是一个插件,**点方块本身就是开关**——
|
|
146
|
+
不用先找到那个小拨杆。关掉的方块留在原位只是变淡、边框变虚线,位置不乱跳,
|
|
147
|
+
这样才看得出「我刚才关了哪个」。方块右上角的状态点说明当前状态:绿=开着,
|
|
148
|
+
灰=关着,黄=看条件。方块右上角悬停出现两个小按钮:改设置、移出预设。
|
|
149
|
+
|
|
150
|
+
方块按来源分两栏:**「官方自带」**(`@deepseek-ai/` 下的包,以及 `cordis:group`
|
|
151
|
+
这种 Loader 内建伪模块)与**「你自己装的」**(其余包名)。分组自己也是一个方块,
|
|
152
|
+
占满整行,子方块摆在它里面——它本身不是插件,只是把几个插件收在一起。
|
|
153
|
+
|
|
154
|
+
点方块的铅笔打开它的设置面板,面板就插在**那个方块的正下方**。这不是排版偏好:
|
|
155
|
+
面板原先排在整片网格的末尾,预设一长网格好几排高,点中间的方块面板就落到
|
|
156
|
+
屏幕外,点下去看着像没反应。插在正下方之后,「点了哪一格」和「面板在哪」
|
|
157
|
+
始终是同一眼能看到的两个位置。
|
|
158
|
+
|
|
159
|
+
卡片头的「全部启用 / 全部关闭」作用于顶层。两种开关的落点不同:
|
|
160
|
+
|
|
161
|
+
- **叶子方块**的开关翻转它自己的 `disabled`。
|
|
162
|
+
- **分组的开关**作用于整棵子树。这不是设计选择而是被迫的:Loader 的
|
|
163
|
+
`Entry.disabled` 对 `options.group` 直接返回 `false`,写一个组自己的 `disabled`
|
|
164
|
+
等于什么都没写(`dsh-app-boot` 自己的 `deny()` 也要靠 `row.group = false` 绕开)。
|
|
165
|
+
分组显示的是子树状态,全是关才显示关,混着就显示条件色。
|
|
166
|
+
|
|
167
|
+
**带 `!!js` 条件表达式的行不会被批量开关动到**:那是条件,不是开关,被「全部关闭」
|
|
168
|
+
覆盖成 `disabled: true` 会把一份平台判断永久销毁。真实那份 patch 里有 win32 的成对
|
|
169
|
+
条件行,所以这条不是假想。条件行自己的开关点下去仍然会写成 `disabled: true`——
|
|
170
|
+
那是用户明确指着这一行说要关。组里全是条件行时,组开关与批量按钮都置灰。
|
|
171
|
+
|
|
172
|
+
### 新建预设的默认值
|
|
173
|
+
|
|
174
|
+
新建不从空白开始。默认值取 `@deepseek-ai/dsh-agent-presets` 自带的**「标准模式」**
|
|
175
|
+
(`presets/standard/agent.cordis.yml`)——那是官方对「一个完整 Agent 该开哪些插件」
|
|
176
|
+
给出的答案。用户要做的只是取消掉不要的,比一个个加省事得多。
|
|
177
|
+
|
|
178
|
+
读它必须用 boot 的方言(`js-yaml` + `cordis-plugin-include` 的 `entryListSchema`),
|
|
179
|
+
不能用 `yaml`:那份文件里 `tool-bash` / `tool-pwsh` 的 `disabled` 是 `!!js` 条件,
|
|
180
|
+
`yaml` 会把它退化成普通字符串,写回补丁时就成了**恒真的 `disabled`**,等于把两个
|
|
181
|
+
shell 工具一起关死。方言产出 `{ __jsExpr }`,正好接上 `toPlain` 的 `{ $js }` 约定,
|
|
182
|
+
页面照原样渲染成 `!!js …`。
|
|
183
|
+
|
|
184
|
+
解析链上找不到那个包时,`seed` 是 `null`,新建退回空列表——降级,不是失败。
|
|
185
|
+
|
|
186
|
+
## 目录型预设:有内容,但没有声明行
|
|
187
|
+
|
|
188
|
+
DSH 的预设有两套并存的机制。桌面端走的是**声明行**:`cordis.patch.yml` 里一行
|
|
189
|
+
`- id: preset-xxx` 加一个 `config`。而 `@deepseek-ai/dsh-agent-presets` 这个包还带
|
|
190
|
+
**目录型**预设——`presets/<id>/agent.cordis.yml` 一份文件就是一个预设,`standard`
|
|
191
|
+
(标准模式)就是这种。
|
|
192
|
+
|
|
193
|
+
麻烦在于:目录型预设**在本 profile 里没有声明行**,但它是真实存在、真实可用的
|
|
194
|
+
预设。只认声明行的代码会在它身上出三种错,而且看起来互不相关:
|
|
195
|
+
|
|
196
|
+
| 症状 | 真实原因 |
|
|
197
|
+
|---|---|
|
|
198
|
+
| 「与 standard 对比」报「找不到声明行」 | 读对手配置时只查了声明行 |
|
|
199
|
+
| 「复制为副本」点了没反应 | 复制要读源配置,同样只查了声明行 |
|
|
200
|
+
| Standard 页显示「0 声明行 · 0 插件模块」 | 行数取自声明行的 `config.plugins` |
|
|
201
|
+
|
|
202
|
+
三处共用一个修法:**读不到声明行时,退到它自己的 composition 文件**。于是
|
|
203
|
+
「与 standard 对比」能列出它 31 行,复制能从它派生新预设,它自己的页面也报出
|
|
204
|
+
真实的 31 行 / 25 个模块 / 3 个组——和对比页给出的数字一致。
|
|
205
|
+
|
|
206
|
+
读那份文件同样必须用 boot 的方言(`js-yaml` + `entryListSchema`),理由和新建
|
|
207
|
+
预设的种子一样:`tool-bash` / `tool-pwsh` 的 `disabled` 是 `!!js` 条件,用 `yaml`
|
|
208
|
+
解析会退化成普通字符串,复制出来的新预设里那两个工具就永久关死了。
|
|
209
|
+
|
|
210
|
+
「复制为副本」的按钮门控也据此分开:**能不能就地改**(`editable`,要求有声明行)
|
|
211
|
+
和**能不能拿它当模板**(`writable`,只要求本 profile 可写)是两件事。用前者去
|
|
212
|
+
禁后者,等于标准模式永远复制不出来——而它恰恰是最值得照抄的那一份。
|
|
213
|
+
|
|
214
|
+
## MCP 服务器与工具可见性
|
|
215
|
+
|
|
216
|
+
一个预设能调用哪些工具,由**两件事**共同决定,而这两件事在配置里离得很远:
|
|
217
|
+
|
|
218
|
+
1. **有哪些工具** —— `@deepseek-ai/dsh-mcp-client` 的声明行。每台服务器贡献一批
|
|
219
|
+
工具,名字固定是 `mcp__<serverName>__<原名>`。
|
|
220
|
+
2. **这个预设允许用哪些** —— 预设自己 `config.plugins` 里的
|
|
221
|
+
`dsh-preset-tool-restrict` 行,`denyPrefixes` 是一串**前缀**。
|
|
222
|
+
|
|
223
|
+
「MCP 与工具」这一页把两件事放在同一屏:上面是服务器清单(可增删改),下面是
|
|
224
|
+
选中预设的工具家族开关。之所以不拆成两页,是因为把一个家族显示成「关」而工具清单
|
|
225
|
+
是旧的时候,界面就在撒谎。
|
|
226
|
+
|
|
227
|
+
### 工具清单读的是「已知名」,不是「可见名」
|
|
228
|
+
|
|
229
|
+
`ctx.tools.view(scope)` 返回 `{ visible, knownNames, restrictableNames }`:
|
|
230
|
+
`knownNames` **保留已被隐藏的工具**,`visible` 不保留。编辑器必须用前者——否则
|
|
231
|
+
一个预设关掉的工具会从清单里消失,用户再也没有办法把它打开。页面底部会写明这次
|
|
232
|
+
数据来自哪个来源(`view` / `schemas` / 读不到),读不到时不假装有数据。
|
|
233
|
+
|
|
234
|
+
### 服务器编辑
|
|
235
|
+
|
|
236
|
+
支持 `@deepseek-ai/dsh-mcp-client` 的两种传输,字段按它的 zod schema 一一对应:
|
|
237
|
+
|
|
238
|
+
| 传输 | 字段 |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `stdio` | `command`、`args`(每行一个)、`env`(每行 `KEY=VALUE`)、`cwd` |
|
|
241
|
+
| `streamable-http` | `url`、`headers`(每行 `Name: value`) |
|
|
242
|
+
|
|
243
|
+
两种共有 `serverName`(`^[A-Za-z0-9_-]{1,32}$`)、`toolCallTimeoutMs`、
|
|
244
|
+
`failOnStartupError`、`reconnect`(JSON),以及补丁行上的 `disabled`。
|
|
245
|
+
|
|
246
|
+
**切换传输时会清掉另一种传输的遗留键。** `config` 是整块替换,如果只写新键,
|
|
247
|
+
`url` 会留在原来那台 stdio 服务器上。所以保存时把 schema 里这次用不到的已知键
|
|
248
|
+
显式标成 `null`,`setRowConfig` 见到 `null` 就删键;新建路径反过来,用
|
|
249
|
+
`withoutDeletions()` 把删除标记摘掉,免得新行里出现 `url: null`。
|
|
250
|
+
|
|
251
|
+
`serverName` 在 profile 内唯一,重名报 `duplicate-id`。
|
|
252
|
+
|
|
253
|
+
### 预设的工具开关
|
|
254
|
+
|
|
255
|
+
这一页打开时选中的是 **profile 的默认预设**(`selectedDefault`),不是列表里的
|
|
256
|
+
第一个。这一条不是小事:如果落在第一个预设上,用户会看着 A 的开关去改 B 的行,
|
|
257
|
+
现象正是「我改了一个预设,另一个预设的设置也跟着变了」。
|
|
258
|
+
|
|
259
|
+
开关按**家族**分组:`mcp__<serverName>__` 是 MCP 家族,其余按第一个 `__` 前的
|
|
260
|
+
前缀分组,没有前缀的是单个工具。家族顺序是 MCP 优先,然后按工具数量降序。
|
|
261
|
+
|
|
262
|
+
**家族可以展开,展开后每个工具名自己有一个开关。** 这一层不是装饰:家族开关只能
|
|
263
|
+
整个家族一起关,而「这个预设里我只要 `mcp__playwright-mcp__browser_click` 别看
|
|
264
|
+
得见」是一个真实且常见的意图,没有单工具开关就没有任何开关能表达它。两种隐藏方式
|
|
265
|
+
在存储上是两回事——家族前缀和单个工具名都是 `denyPrefixes` 里的条目——所以
|
|
266
|
+
|
|
267
|
+
- 家族里**部分**被隐藏时,家族开关显示为第三种状态(黄色,副标题写「N / M 个已隐藏」),
|
|
268
|
+
它既不是「全开」也不是「全关」,点一下表示「整个家族都关」。
|
|
269
|
+
- 单独放行一个被家族前缀盖住的工具,会把那个前缀换成它其余兄弟的名字,结果精确等于
|
|
270
|
+
「这一个可见,其余照旧隐藏」。
|
|
271
|
+
- 单独关一个工具就是追加它自己的名字。
|
|
272
|
+
|
|
273
|
+
翻转一个开关就是重写这个预设 `denyPrefixes` 的整个数组(同样是整块替换)。
|
|
274
|
+
两种情况要分清:
|
|
275
|
+
|
|
276
|
+
- **预设自己有 `tool-restrict` 行** —— 开关直接写这一行。
|
|
277
|
+
- **预设没有这一行** —— 此时生效的是插件内置默认(`mcp__playwright-mcp__`、
|
|
278
|
+
`mcp__droidmind__`),界面按默认值显示开关状态并写明这一点。拨动任意一个开关
|
|
279
|
+
(包括「全部启用」)都会为它新建这一行。
|
|
280
|
+
|
|
281
|
+
**空列表是一个真实的意图,不是「没写」。** 显式写出 `denyPrefixes: []` 的含义是
|
|
282
|
+
「这个预设不屏蔽任何工具」,它是权威的,不会再回落到插件默认——否则「全部启用」
|
|
283
|
+
在一个从来没有这一行的预设上就什么也改不动。只有**键缺省**(行不存在,或行里
|
|
284
|
+
没有 `denyPrefixes`)才继承插件默认。
|
|
285
|
+
|
|
286
|
+
**原始 `denyPrefixes` 文本框默认是折叠的**,收在「高级:直接编辑原始列表」后面。
|
|
287
|
+
它是逃生口,不是主控件:普通用户的每一个意图都能用上面的开关表达,而一个要求
|
|
288
|
+
「按 `denyPrefixes` 语法写」的文本框对其他人就是一面墙。需要家族开关覆盖不到的
|
|
289
|
+
组合(比如一条自定义前缀)时再展开它,直接写前缀或单个工具名。清空它并保存就是
|
|
290
|
+
上面那个空列表。
|
|
291
|
+
|
|
292
|
+
## 安全手法
|
|
293
|
+
|
|
294
|
+
每次写入都走同一条路(`applyWrite`):
|
|
295
|
+
|
|
296
|
+
1. 解析当前文档 → 在内存里改
|
|
297
|
+
2. **在 boot 方言下校验**(`yaml.parseDocument` 总量检查 + `entryListSchema`
|
|
298
|
+
可加载性检查)——不过就不写
|
|
299
|
+
3. 把**改动前**的内容存成快照 `auto-<动作>`
|
|
300
|
+
4. 原子落盘:写临时文件再 `rename`,权限 `0o600`
|
|
301
|
+
5. 失败则回滚到快照
|
|
302
|
+
|
|
303
|
+
校验在快照之前,所以被拒绝的编辑不会留下快照垃圾。回滚本身先存一份
|
|
304
|
+
`auto-before-rollback`,因此回滚是可撤销的。自动快照保留最近 40 个。
|
|
305
|
+
|
|
306
|
+
删除当前默认预设时,默认项回落到 registry 自己的 `default`(再不行 `"standard"`),
|
|
307
|
+
保证下次启动仍有默认值。
|
|
308
|
+
|
|
309
|
+
写入后**不需要手动调用任何内部 API**:`dsh-hmr` 在监听 `profile.patchPath`、
|
|
310
|
+
home patch 与 `package.json`,文件一变它自己会 `reconcileProfilePatches`。
|
|
311
|
+
(`lib/` 下的代码变更不在监听范围内,改插件自身代码要重启 DSH。)
|
|
312
|
+
|
|
313
|
+
## 数据来源
|
|
314
|
+
|
|
315
|
+
页面数据全部来自 host 半在每次请求时**实时读取**的服务,没有会过期的缓存:
|
|
316
|
+
|
|
317
|
+
- `loader.entries()` —— 声明行本身,其 `config` 已是解析后的对象
|
|
318
|
+
- `agentPresets`(registry)—— 各预设的激活诊断,以及每行的实际启用状态
|
|
319
|
+
- `tools`(`ctx.get("tools")`)—— 运行时注册表里已知的工具名,用于分组与统计
|
|
320
|
+
- `profileContext.patchPath` —— 打开 YAML 文档,用于报告来源行并执行写入
|
|
321
|
+
|
|
322
|
+
`profileContext` 与 `webRuntime` 用 `ctx.get()` 读取,**不列为必需注入**:桌面版
|
|
323
|
+
(`dsh/lib/profile-boot-BZ2ZjNWi.js`,asar 内)会 `provide("profileContext", …)`,
|
|
324
|
+
而 CLI 启动的 profile(`dsh/lib/profile-boot-CuwbWsnH.js`)从不 provide 它。若把
|
|
325
|
+
它们写进 `inject`,CLI 下整个插件会停在 `pending (waiting for service:
|
|
326
|
+
profileContext)`——路由不注册、页面不出现。降级后 CLI 下仍可读,只是
|
|
327
|
+
`patchPath` 为 `null`、写按钮全部禁用,并附一句
|
|
328
|
+
`this profile exposes no patch path`。
|
|
329
|
+
|
|
330
|
+
`!!js` 条件开关在传输时被标记为 `{ "$js": "…" }`,页面上按 `!!js …` 原样
|
|
331
|
+
呈现。若把它当成普通对象序列化,会变成一个空的 `{}`——那会把「这个开关
|
|
332
|
+
有条件」误报成「没有条件」。
|
|
333
|
+
|
|
334
|
+
## 接口
|
|
335
|
+
|
|
336
|
+
host 半只开一个路由,带与其他插件路由相同的围栏
|
|
337
|
+
(loopback / 已配置的信任域 Host 头 + 同源浏览器标记;这是防 DNS rebinding,
|
|
338
|
+
不是身份认证)。请求体上限 1 MB。
|
|
339
|
+
|
|
340
|
+
```
|
|
341
|
+
POST /preset-studio/api
|
|
342
|
+
{ "action": "list" }
|
|
343
|
+
{ "action": "create", "config": { id, name, description, order, plugins } }
|
|
344
|
+
{ "action": "update", "id": "razor", "config": { … } }
|
|
345
|
+
{ "action": "delete", "id": "razor" }
|
|
346
|
+
{ "action": "duplicate", "id": "razor", "newId": "razor-2" }
|
|
347
|
+
{ "action": "setDefault", "id": "razor" }
|
|
348
|
+
{ "action": "snapshots" }
|
|
349
|
+
{ "action": "snapshot", "label": "手动" }
|
|
350
|
+
{ "action": "rollback", "name": "<快照文件名>" }
|
|
351
|
+
{ "action": "export", "id": "razor" }
|
|
352
|
+
{ "action": "diff", "id": "razor", "against": "standard" }
|
|
353
|
+
{ "action": "mcp" }
|
|
354
|
+
{ "action": "mcpSave", "rowId": "", "config": { transport, serverName, … }, "disabled": false }
|
|
355
|
+
{ "action": "mcpDelete", "rowId": "codegraph-mcp" }
|
|
356
|
+
{ "action": "toolRestrict", "id": "razor", "denyPrefixes": ["mcp__playwright-mcp__"] }
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
`mcpSave` 的 `rowId` 为空表示新建。`toolRestrict` 在预设还没有 `tool-restrict`
|
|
360
|
+
行时按需创建,列表为空则不写入。
|
|
361
|
+
|
|
362
|
+
错误码到 HTTP 的映射:`bad-input` / `bad-id` / `bad-name` / `bad-plugins` /
|
|
363
|
+
`bad-snapshot` / `bad-mcp` → 400;`not-found` → 404;`duplicate-id` /
|
|
364
|
+
`not-declared` → 409;`no-profile` / `no-registry` / `yaml-unavailable` → 503;
|
|
365
|
+
其余 500。
|
|
366
|
+
|
|
367
|
+
## 权限
|
|
368
|
+
|
|
369
|
+
- **可写,但只写两处**:profile 的 `cordis.patch.yml`,以及 profile 目录下的
|
|
370
|
+
`.preset-studio-snapshots/`;导出另写 `$DSH_HOME/preset-studio-exports/`。
|
|
371
|
+
- 写入前一律先校验、先快照;任何一步失败都不落盘。
|
|
372
|
+
- 不发送任何网络请求,不引入运行时依赖。
|
|
373
|
+
|
|
374
|
+
## 兼容性
|
|
375
|
+
|
|
376
|
+
- 需要 `@deepseek-ai/dsh-agent-preset`(预设声明机制)与
|
|
377
|
+
`@deepseek-ai/dsh-agent-preset-registry`(`agentPresets` 服务)存在。
|
|
378
|
+
后者缺失时页面仍能列出声明,只是没有运行时诊断,且不能新建(没有默认项可回落)。
|
|
379
|
+
- MCP 与工具那一页不需要额外依赖:服务器清单是直接读补丁文件里的
|
|
380
|
+
`@deepseek-ai/dsh-mcp-client` 行。工具清单需要运行时 `tools` 服务;读不到时
|
|
381
|
+
家族开关不可用,页面会写明来源是「读不到」而不是显示一份空清单。
|
|
382
|
+
- `yaml` 用于读写 patch 文件;不可用时页面会显示一条说明,读仍可用,
|
|
383
|
+
写会明确报 `yaml-unavailable`,而不是静默失败。
|
|
384
|
+
|
|
385
|
+
## License
|
|
386
|
+
|
|
387
|
+
MIT
|
|
2
388
|
|
|
3
|
-
This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# dsh-preset-studio bundle patch — one loader entry mounting the plugin package.
|
|
2
|
+
#
|
|
3
|
+
# 单个 loader entry 同时挂载:
|
|
4
|
+
# * host 半(lib/index.js)—— 只读的 /preset-studio/api 路由,读取本 profile
|
|
5
|
+
# 的预设声明与运行时状态;
|
|
6
|
+
# * 浏览器半(lib/client.js)—— 设置侧栏的「预设工作室」整页。
|
|
7
|
+
#
|
|
8
|
+
# 浏览器半由 dsh-client-modules 通过 package.json 的 dsh.client 声明 +
|
|
9
|
+
# exports["./client"] 自动编入,无需在此声明。
|
|
10
|
+
#
|
|
11
|
+
# 手动挂载:把下面这行加进 profile 的 cordis.patch.yml,或直接
|
|
12
|
+
# dsh plugin --profile <name> add dsh-preset-studio
|
|
13
|
+
- insert:
|
|
14
|
+
- id: preset-studio
|
|
15
|
+
name: 'dsh-preset-studio'
|