@xia-sc/dsh-cc-studio 0.3.5 → 0.3.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/README.md +46 -19
- package/lib/agent.js +80 -37
- package/lib/client.js +8 -4
- package/lib/index.js +159 -67
- package/package.json +6 -3
- package/presets/cc.patch.yml +328 -0
package/README.md
CHANGED
|
@@ -15,10 +15,11 @@ DSH(DeepSeek Harness)插件:输入框上方的胶囊 → 全屏融合工
|
|
|
15
15
|
| 项目 | 值 |
|
|
16
16
|
| --- | --- |
|
|
17
17
|
| 插件包名 | `@xia-sc/dsh-cc-studio` |
|
|
18
|
-
| 当前版本 | `0.3.
|
|
19
|
-
| 宿主 RPC | `/dsh-cc-studio-rpc`(自带路由,适配 dsh ≥ `0.1.5-rc.1`;已在 `0.1.6-alpha.2` 实测) |
|
|
18
|
+
| 当前版本 | `0.3.6` |
|
|
19
|
+
| 宿主 RPC | `/dsh-cc-studio-rpc`(自带路由,适配 dsh ≥ `0.1.5-rc.1`;已在 `0.1.6-alpha.2` / `0.1.7-alpha.1` 实测) |
|
|
20
|
+
| CC 预设 | `presets/cc.patch.yml`(dsh ≥ `0.1.7-alpha.1`,组合声明行)/`<DSH_HOME>/.agent-presets/cc`(≤ `0.1.6-alpha.2`,目录形态) |
|
|
20
21
|
| 客户端挂载点 | `conversation.input.dock`(胶囊)+ `shell.overlay`(工坊)+ `settings.section` |
|
|
21
|
-
| 落盘位置 |
|
|
22
|
+
| 落盘位置 | `<DSH_HOME>/cc-library/`(角色卡)、`<DSH_HOME>/cc-drafts/`(会话草稿);`DSH_HOME` 默认 `~/.dsh`。0.3.6 起认 `DSH_HOME`,旧的 `~/.dsh` 位置**仍然读得到**(读时回退) |
|
|
22
23
|
|
|
23
24
|
## 安装
|
|
24
25
|
|
|
@@ -28,7 +29,7 @@ DSH(DeepSeek Harness)插件:输入框上方的胶囊 → 全屏融合工
|
|
|
28
29
|
dsh plugin --profile web add @xia-sc/dsh-cc-studio
|
|
29
30
|
|
|
30
31
|
# 锁定版本
|
|
31
|
-
dsh plugin --profile web add @xia-sc/dsh-cc-studio@0.3.
|
|
32
|
+
dsh plugin --profile web add @xia-sc/dsh-cc-studio@0.3.6
|
|
32
33
|
```
|
|
33
34
|
|
|
34
35
|
**或从 GitHub 源安装**:
|
|
@@ -55,11 +56,29 @@ dsh plugin --profile web add .
|
|
|
55
56
|
|
|
56
57
|
相对路径按**你执行命令时所在的目录**解析(dsh 会先把 `.`、`./xxx` 重写成绝对路径再交给 pnpm,避免误链到 profile 目录)。装出来的是 `link:` 到源码目录:改 `lib/*.js` 后重启 dsh web 生效;只改 `lib/client.js` 时刷新页面即可(若同时跑着 dsh 仓库的 `pnpm run dev:web`,客户端 bundle 会重建,连刷新都省了)。
|
|
57
58
|
|
|
58
|
-
### 3. CC
|
|
59
|
+
### 3. CC 预设(随插件自动可用,无需手工拷贝)
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
装完插件、重启 `dsh web` 后即可在会话模式里选到 `CC 模式`(preset id 恒为 `cc` —— 宿主 `isCcPreset()` 与浏览器 `CC_PRESET_ID` 都按它精确比较)。**投递方式按 dsh 版本分两代,两代都不需要你手工拷文件**:
|
|
61
62
|
|
|
62
|
-
|
|
63
|
+
| dsh 版本 | 预设从哪来 | 你要做什么 |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| ≥ `0.1.7-alpha.1` | 插件随包带的补丁层 `presets/cc.patch.yml`:往组合里 insert 一行 `@deepseek-ai/dsh-agent-preset` 的**声明**(与内置 `standard` / `ptc` / `minimal` / `cordis` 完全同构) | 什么都不用做 |
|
|
66
|
+
| ≤ `0.1.6-alpha.2` | 插件挂载时把 `presets/cc` 装到 `<DSH_HOME>/.agent-presets/cc`(目录形态,行为与 0.3.5 之前一致) | 什么都不用做 |
|
|
67
|
+
|
|
68
|
+
**为什么 0.1.7 必须换机制**:那一版起 dsh 的预设**只来自组合里的声明行**,`<DSH_HOME>/.agent-presets/` 已经没有任何代码读取 —— 升级后 `CC 模式` 从 roster 里消失就是这个原因:目录还在,只是**没人再看它**。插件因此把声明写进 `package.json` 的 `dsh.bundle.patch` 第二层(第一层是宿主插件行),插件装进哪个 profile,`CC 模式` 就出现在哪个 profile 的 roster 里;新版上**不会再往磁盘写一个字**。
|
|
69
|
+
|
|
70
|
+
**关掉 `CC 模式`(≥ `0.1.7-alpha.1`)**:不必卸载插件,在自己的补丁层里按 id 关掉那一行即可(`<profile>/cordis.patch.yml`,或对所有 profile 生效的 `$DSH_HOME/cordis.patch.yml`):
|
|
71
|
+
|
|
72
|
+
```yaml
|
|
73
|
+
- id: preset-cc
|
|
74
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
75
|
+
disabled: true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
<details>
|
|
79
|
+
<summary>≤ 0.1.6-alpha.2:目录安装的更新策略、开关与手工兜底</summary>
|
|
80
|
+
|
|
81
|
+
**旧版上插件挂载时仍会把 `presets/cc` 装到 `<DSH_HOME>/.agent-presets/cc`**(`DSH_HOME` 默认 `~/.dsh`),目录名固定为 `cc`。之所以需要这一步:那条路径上 dsh 的预设发现只扫三个根——**shipped 根**(`dsh-agent-presets` 包内自带)+ **部署 `config.roots`** + **用户根 `<DSH_HOME>/.agent-presets`**;插件包内的 `presets/` 不在其中,**「随包分发」≠「已注册」**,所以由插件在启动时把它装进用户根。
|
|
63
82
|
|
|
64
83
|
更新策略是幂等的,且**绝不静默覆盖你的改动**:
|
|
65
84
|
|
|
@@ -112,6 +131,8 @@ cp -R ~/.dsh/profiles/web/node_modules/@xia-sc/dsh-cc-studio/presets/cc/. ~/.dsh
|
|
|
112
131
|
|
|
113
132
|
> 手工拷贝的目录**没有**安装记录,因此会被自动安装流程视为「你的自有文件」而跳过;想交回插件管理,删掉该目录后重启即可。
|
|
114
133
|
|
|
134
|
+
> 升级到 `0.1.7-alpha.1` 后这个目录既不会被读取也不会再被写入:插件**不会**替你删任何东西,想删就自己删(`Remove-Item "$env:USERPROFILE\.dsh\.agent-presets\cc" -Recurse -Force`)。
|
|
135
|
+
|
|
115
136
|
</details>
|
|
116
137
|
|
|
117
138
|
> 卸载/删除插件**不会**删除 `<DSH_HOME>/.agent-presets/cc/`(自动安装也不注册删除动作)。
|
|
@@ -189,7 +210,8 @@ dsh plugin --profile web add @xia-sc/dsh-cc-studio
|
|
|
189
210
|
|
|
190
211
|
| 现象 | 处理 |
|
|
191
212
|
| --- | --- |
|
|
192
|
-
| 输入框上方没有胶囊 | 确认当前会话模式是 `CC
|
|
213
|
+
| 输入框上方没有胶囊 | 确认当前会话模式是 `CC 模式`;**dsh ≥ `0.1.7-alpha.1`**:`dsh --profile web --dump-config` 里应看到 `- id: preset-cc`(在 `# == @xia-sc/dsh-cc-studio` 那一层下),再看设置 →「Agent 预设」里 `CC 模式` 是否带「加载失败」诊断;**≤ `0.1.6-alpha.2`**:确认 `~/.dsh/.agent-presets/cc/` 下 `preset.yml` 与 `agent.cordis.yml` 都在且目录名是 `cc`(正常应由插件自动安装);两种情况都要重启 dsh web 后刷新页面 |
|
|
214
|
+
| 想直接读预设台账(诊断用) | dsh 把每个 Remote 挂成 `POST /api/<namespace>/<method>`,envelope 与 connection RPC 相同(cookie 用 `/?token=…` 换)。`/api/agentPresets/list` 会剥掉 `name` 与 `broken`,**完整台账**要 `/api/pluginInventory/list`(看 `agentPresets` 里 `cc` 的 `broken` 是否为空、`cc-agent` 的 `fiberPhase` 是否 `active`)。另注:插件的 `info` 日志只进 Cordis logger,**不会**出现在 `dsh web` 的 stdout/stderr —— 别指望 grep `[dsh-cc-studio]` 判断装没装上 |
|
|
193
215
|
| 工坊出现橙色条「草稿已新建」 | 该会话没有历史草稿(首次创建或草稿目录被清);已存档会话恢复时会改为蓝色条并显示 `creation_date`,下次写入后提示消失 |
|
|
194
216
|
| 切 CC 模式报 `invalid config: S.prefix missing required value` | `presets/cc/agent.cordis.yml` 是 0.2.22 之前的旧版(persona 用了 `text:`),重新拷贝新模板 |
|
|
195
217
|
| dsh 启动报 `cannot get property "webServer" without inject` | 插件版本低于 0.2.22,更新插件 |
|
|
@@ -234,7 +256,7 @@ dsh plugin --profile web add @xia-sc/dsh-cc-studio
|
|
|
234
256
|
```
|
|
235
257
|
dsh-cc-studio/
|
|
236
258
|
├── package.json # @xia-sc/dsh-cc-studio, dsh.bundle.patch + dsh.client, exports ./client ./agent
|
|
237
|
-
├── cordis.patch.yml #
|
|
259
|
+
├── cordis.patch.yml # 补丁层 1:宿主行插入(id dsh-cc-studio)
|
|
238
260
|
├── lib/
|
|
239
261
|
│ ├── index.js # host: /dsh-cc-studio-rpc(validate, cc_getDraft/cc_setDraft/cc_patchDraft, cc_isCcMode,
|
|
240
262
|
│ │ # cc_validateDraft, 草稿槽 cc_migrateDraft, 已存库 cc_*Library, 容器 cc_importFromPng/cc_exportPng(+imageB64)/
|
|
@@ -242,14 +264,18 @@ dsh-cc-studio/
|
|
|
242
264
|
│ ├── agent.js # CC 模式 Tools:6 步共创 + 2 Lorebook 管理 + 6 已存库 CRUD = 14 个,含「先与用户讨论」提示
|
|
243
265
|
│ └── client.js # client: dock 胶囊 + overlay 工坊 + settings.section(DSW Token 深浅色、品牌紫、
|
|
244
266
|
│ # 五维回显、长文本大框、JSON/PNG/CHARX 导入导出/写入)
|
|
245
|
-
├── presets/
|
|
246
|
-
│ ├──
|
|
247
|
-
│ └──
|
|
248
|
-
├──
|
|
249
|
-
│
|
|
250
|
-
|
|
251
|
-
│ ├──
|
|
252
|
-
│
|
|
267
|
+
├── presets/
|
|
268
|
+
│ ├── cc.patch.yml # 补丁层 2:CC 预设声明行(dsh ≥ 0.1.7-alpha.1)
|
|
269
|
+
│ └── cc/ # CC 预设目录模板(dsh ≤ 0.1.6-alpha.2,插件挂载时安装到用户根)
|
|
270
|
+
│ ├── preset.yml
|
|
271
|
+
│ └── agent.cordis.yml
|
|
272
|
+
├── tests/ # 6 个测试文件、278 项断言(不随包发布)
|
|
273
|
+
│ ├── rpc-channel.test.mjs # host 半 RPC 通道回归(假 ctx + 真实 http,45 项断言)
|
|
274
|
+
│ ├── preset-install.test.mjs # 预设安装决策 + native 让位 + 两份清单防漂移 + 行契约,66 项断言
|
|
275
|
+
│ ├── cc-detection.test.mjs # CC 模式探测的源码级守卫,50 项断言
|
|
276
|
+
│ ├── client-greetings.test.mjs # 客户端问候语与 i18n 守卫,62 项断言
|
|
277
|
+
│ ├── draft-slot-sync.test.mjs # 草稿槽行为级回归(假 React/slot/RPC harness),18 项断言
|
|
278
|
+
│ └── data-root.test.mjs # 落盘根:DSH_HOME 优先 + 读时回退 ~/.dsh,37 项断言
|
|
253
279
|
├── CHANGELOG.md # 完整版本历史
|
|
254
280
|
├── README.md # 中文(默认)
|
|
255
281
|
└── README_EN.md # English
|
|
@@ -260,8 +286,8 @@ dsh-cc-studio/
|
|
|
260
286
|
## 开发与测试
|
|
261
287
|
|
|
262
288
|
```bash
|
|
263
|
-
npm test #
|
|
264
|
-
node tests/rpc-channel.test.mjs # 只跑 host 半 RPC 通道回归(
|
|
289
|
+
npm test # 6 个文件、278 项断言(六个 node 脚本串联)
|
|
290
|
+
node tests/rpc-channel.test.mjs # 只跑 host 半 RPC 通道回归(45 项断言)
|
|
265
291
|
```
|
|
266
292
|
|
|
267
293
|
- **改客户端**:`lib/client.js` 改动刷新页面即可;跑着 dsh 仓库的 `pnpm run dev:web` 时可热更新。
|
|
@@ -280,6 +306,7 @@ node tests/rpc-channel.test.mjs # 只跑 host 半 RPC 通道回归(
|
|
|
280
306
|
|
|
281
307
|
完整历史见 [CHANGELOG.md](./CHANGELOG.md)。最近几版:
|
|
282
308
|
|
|
309
|
+
- `0.3.6` 修 dsh `0.1.7-alpha.1` 上「`CC 模式` 预设消失」+ 落盘根统一。① 预设:那一版起 dsh 的预设只来自组合里的**声明行**,`<DSH_HOME>/.agent-presets/` 已无人读取(目录还在、只是没人看),插件改为随包带一层补丁 `presets/cc.patch.yml`(与内置 `standard` 同构的 `@deepseek-ai/dsh-agent-preset` 声明),新版上不再写任何文件,目录安装只为 ≤ `0.1.6-alpha.2` 保留并在新版自动让位。② 草稿与角色库:以前写死 `~/.dsh`、不认 `DSH_HOME`(自定义 `DSH_HOME` 的用户两套根并存,隔离实测还会写进真实 `~/.dsh`),现在改为 **`DSH_HOME` 优先 + 读时回退 `~/.dsh`**,未设置时路径与以前完全一致。测试 216 → 278 项。
|
|
283
310
|
- `0.3.5` 修 #5「前端与 Tools 读到不同草稿槽」:`useCcPreset` 读的 `useSessions().current` 字段在 dsh `0.1.6-alpha.2` 的 `SessionListState` 里并不存在(恒为 `null`),加上启动时那次无 key 的拉取占掉了全局节流,前端一直用 `default` 槽 —— 而模型经 Tools 写的是 `session-<会话id>` 槽,于是「模型说写好了 / 工坊说没数据」。现在会话 id 只从 slot props 取并发布到 store、拉草稿没有会话 id 就一次都不发、节流按 key、主机回传 key 不一致时不渲染而是告警;另修胶囊闪退(根域实例每秒钟把会话域实例判定的 CC 状态清掉)并新增 `cc_migrateDraft` 与「迁入本会话槽」一键救回。测试 158 → 216 项(新增行为级 `tests/draft-slot-sync.test.mjs`)。
|
|
284
311
|
- `0.3.4` 适配 dsh `0.1.6-alpha.2` 并补回两处预设漏抄:CC 预设缺 `present` 行(CC 模式下模型没有「登记交付物」的工具,且不报错)、`tool-subagent` 行缺 `modelSelectionSettings: true`(子代理的指定模型入口被静默关掉);顺带补上 alpha.2 新增的 `tool-plugin-manager` 行(`disabled`,与内置 `standard` 对齐到只剩有意的行差)。新增 7 项预设行契约守卫。
|
|
285
312
|
- `0.3.3` 发布流程自动化:打 `v*` tag 即由 GitHub Actions 发布到 npm(OIDC trusted publishing,零密钥)并自动建 GitHub Release;手动触发默认只做安全自检。插件运行时行为未变。
|
package/lib/agent.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 强制工作流:cc_get_card → cc_patch_character → cc_patch_world(≥3维,autoLorebook) → cc_add_lorebook_entries(≥5条) → cc_patch_greetings → cc_validate
|
|
5
5
|
*/
|
|
6
6
|
import { mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
7
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
const name = "dsh-cc-studio-agent";
|
|
@@ -12,11 +12,26 @@ const inject = ["tools", "agents"];
|
|
|
12
12
|
|
|
13
13
|
function nowSec(){ return Math.floor(Date.now()/1000); }
|
|
14
14
|
function draftsMap(){ if(!globalThis.__CC_DRAFTS__) globalThis.__CC_DRAFTS__ = new Map(); return globalThis.__CC_DRAFTS__; }
|
|
15
|
-
// ——
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
// —— 落盘根(0.3.6 起统一,与 lib/index.js 是同一套):DSH_HOME 优先,homedir()/.dsh 兜底 ——
|
|
16
|
+
// 草稿与角色库都必须与宿主半算出**同一个路径**,否则又会退化成 issue #5 那种「两边互相看不见」。
|
|
17
|
+
// 读盘回退旧根(0.3.6 之前写死 homedir()/.dsh),写在主根。
|
|
18
|
+
function legacyDshHomeDir(){
|
|
19
|
+
try{ return join(homedir(), ".dsh"); }catch{ return join(process.cwd(), ".dsh"); }
|
|
20
|
+
}
|
|
21
|
+
function dshHomeDir(){
|
|
22
|
+
const env = process.env.DSH_HOME;
|
|
23
|
+
if(typeof env === "string" && env.trim().length > 0) return env.trim();
|
|
24
|
+
return legacyDshHomeDir();
|
|
25
|
+
}
|
|
26
|
+
function dataRoots(sub){
|
|
27
|
+
const primary = join(dshHomeDir(), sub);
|
|
28
|
+
const legacy = join(legacyDshHomeDir(), sub);
|
|
29
|
+
return primary === legacy ? [primary] : [primary, legacy];
|
|
19
30
|
}
|
|
31
|
+
// —— 草稿持久化(fix #2):与 host 半共享 <主根>/cc-drafts,变更即落盘,重启后恢复 ——
|
|
32
|
+
if(!globalThis.__CC_DRAFT_META__) globalThis.__CC_DRAFT_META__ = new Map();
|
|
33
|
+
function draftDirs(){ return dataRoots("cc-drafts"); }
|
|
34
|
+
function draftDir(){ return draftDirs()[0]; }
|
|
20
35
|
function safeDraftFile(key){
|
|
21
36
|
const raw = String(key || "default");
|
|
22
37
|
let h = 0;
|
|
@@ -26,9 +41,20 @@ function safeDraftFile(key){
|
|
|
26
41
|
return safe + "-" + hex + ".json";
|
|
27
42
|
}
|
|
28
43
|
function draftPath(key){ return join(draftDir(), safeDraftFile(key)); }
|
|
44
|
+
/** 读盘用:主根没有就回退旧根(0.3.6 之前草稿写死的地方)。找不到返回 null。 */
|
|
45
|
+
function findDraftPath(key){
|
|
46
|
+
const file = safeDraftFile(key);
|
|
47
|
+
for(const dir of draftDirs()){
|
|
48
|
+
const p = join(dir, file);
|
|
49
|
+
try{ if(existsSync(p)) return p; }catch{}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
29
53
|
function loadPersistedDraftSync(key){
|
|
30
54
|
try{
|
|
31
|
-
const
|
|
55
|
+
const p = findDraftPath(key);
|
|
56
|
+
if(!p) return null;
|
|
57
|
+
const raw = readFileSync(p, "utf8");
|
|
32
58
|
const obj = JSON.parse(raw);
|
|
33
59
|
const d = obj && obj.draft ? obj.draft : null;
|
|
34
60
|
if(d && typeof d === "object" && d.spec === "chara_card_v3" && d.data && typeof d.data === "object") return d;
|
|
@@ -117,14 +143,24 @@ function setDraft(ctx, draft, exec){
|
|
|
117
143
|
return draft;
|
|
118
144
|
}
|
|
119
145
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
146
|
+
// —— 已存角色库:<主根>/cc-library/<id>.json(读盘回退 0.3.6 之前的 homedir()/.dsh)——
|
|
147
|
+
function libDirs(){ return dataRoots("cc-library"); }
|
|
148
|
+
function libDir(){ return libDirs()[0]; }
|
|
123
149
|
async function ensureLibDir(){
|
|
124
150
|
const dir=libDir();
|
|
125
151
|
await mkdir(dir,{recursive:true});
|
|
126
152
|
return dir;
|
|
127
153
|
}
|
|
154
|
+
/** 读盘用:主根没有就回退旧根。返回 {path, raw};两边都没有时抛错(与原 readFile 语义一致)。 */
|
|
155
|
+
async function readLibraryRaw(id){
|
|
156
|
+
let lastErr=null;
|
|
157
|
+
for(const dir of libDirs()){
|
|
158
|
+
const p=join(dir,String(id)+".json");
|
|
159
|
+
try{ return { path:p, raw: await readFile(p,"utf8") }; }
|
|
160
|
+
catch(e){ lastErr=e; }
|
|
161
|
+
}
|
|
162
|
+
throw lastErr || new Error("找不到角色卡: " + String(id));
|
|
163
|
+
}
|
|
128
164
|
function makeLibraryEntryMeta(draft, existingMeta, id){
|
|
129
165
|
const data=draft&&draft.data?draft.data:{};
|
|
130
166
|
const now=nowSec();
|
|
@@ -140,23 +176,24 @@ function makeLibraryEntryMeta(draft, existingMeta, id){
|
|
|
140
176
|
};
|
|
141
177
|
}
|
|
142
178
|
async function listLibraryEntries(){
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
}
|
|
179
|
+
await ensureLibDir();
|
|
180
|
+
const seen=new Map(); // id → meta:先到先得,主根排在前 → 同名卡片主根优先
|
|
181
|
+
for(const dir of libDirs()){
|
|
182
|
+
let files=[];
|
|
183
|
+
try{ files=await readdir(dir); }catch{ continue; }
|
|
184
|
+
for(const f of files){
|
|
185
|
+
if(!f.endsWith(".json")) continue;
|
|
186
|
+
const id=f.replace(/\.json$/,"");
|
|
187
|
+
if(seen.has(id)) continue;
|
|
188
|
+
try{
|
|
189
|
+
const raw=await readFile(join(dir,f),"utf8");
|
|
190
|
+
const obj=JSON.parse(raw);
|
|
191
|
+
if(obj && obj.meta && obj.id) seen.set(id,obj.meta);
|
|
192
|
+
else if(obj && obj.draft) seen.set(id,makeLibraryEntryMeta(obj.draft,null,id));
|
|
193
|
+
}catch{}
|
|
194
|
+
}
|
|
159
195
|
}
|
|
196
|
+
const entries=[...seen.values()];
|
|
160
197
|
entries.sort((a,b)=>(b.updatedAt||0)-(a.updatedAt||0));
|
|
161
198
|
return entries;
|
|
162
199
|
}
|
|
@@ -165,7 +202,8 @@ async function saveLibraryEntry(draft, opts){
|
|
|
165
202
|
let id=opts && opts.id ? String(opts.id) : "";
|
|
166
203
|
let existing=null;
|
|
167
204
|
if(id){
|
|
168
|
-
|
|
205
|
+
// 已存在的那份可能在旧根(0.3.6 之前保存的),读过来只为继承 createdAt / 旧 name
|
|
206
|
+
try{ existing=JSON.parse((await readLibraryRaw(id)).raw); }catch{}
|
|
169
207
|
}
|
|
170
208
|
if(!id) id=String(Date.now())+"-"+Math.random().toString(36).slice(2,6);
|
|
171
209
|
const meta=makeLibraryEntryMeta(draft, existing?existing.meta:null, id);
|
|
@@ -175,27 +213,32 @@ async function saveLibraryEntry(draft, opts){
|
|
|
175
213
|
return {id,meta,draft};
|
|
176
214
|
}
|
|
177
215
|
async function loadLibraryEntry(id){
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const obj=JSON.parse(raw);
|
|
216
|
+
await ensureLibDir();
|
|
217
|
+
const obj=JSON.parse((await readLibraryRaw(id)).raw);
|
|
181
218
|
if(!obj || !obj.draft) throw new Error("文件格式错误");
|
|
182
219
|
return obj;
|
|
183
220
|
}
|
|
221
|
+
/** 删除:两个根里的同名副本都要删,否则读盘回退会把刚删掉的卡「复活」;一个都没删到则抛错。 */
|
|
184
222
|
async function deleteLibraryEntry(id){
|
|
185
|
-
|
|
186
|
-
|
|
223
|
+
await ensureLibDir();
|
|
224
|
+
let removed=0;
|
|
225
|
+
for(const dir of libDirs()){
|
|
226
|
+
try{ await unlink(join(dir,String(id)+".json")); removed+=1; }
|
|
227
|
+
catch(e){ if(e && e.code!=="ENOENT") throw e; }
|
|
228
|
+
}
|
|
229
|
+
if(removed===0) throw Object.assign(new Error("找不到角色卡: " + String(id)), { code:"ENOENT" });
|
|
187
230
|
}
|
|
231
|
+
/** 改名是就地改 meta.name:写在解析到的那一份上(主根优先),不搬家、不复制。 */
|
|
188
232
|
async function renameLibraryEntry(id,newName){
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
const obj=JSON.parse(raw);
|
|
233
|
+
await ensureLibDir();
|
|
234
|
+
const found=await readLibraryRaw(id);
|
|
235
|
+
const obj=JSON.parse(found.raw);
|
|
193
236
|
if(!obj || !obj.meta) throw new Error("文件格式错误");
|
|
194
237
|
const name=String(newName||"").trim().slice(0,60);
|
|
195
238
|
if(!name) throw new Error("name 不能为空");
|
|
196
239
|
obj.meta.name=name;
|
|
197
240
|
obj.meta.updatedAt=nowSec();
|
|
198
|
-
await writeFile(
|
|
241
|
+
await writeFile(found.path, JSON.stringify(obj,null,2), "utf8");
|
|
199
242
|
return obj.meta;
|
|
200
243
|
}
|
|
201
244
|
|
|
@@ -611,4 +654,4 @@ function apply(ctx){
|
|
|
611
654
|
});
|
|
612
655
|
}
|
|
613
656
|
|
|
614
|
-
export { apply, inject, name };
|
|
657
|
+
export { apply, inject, name, safeDraftFile, draftDirs, libDirs, dataRoots };
|
package/lib/client.js
CHANGED
|
@@ -1001,9 +1001,11 @@ window.__ModuleLoader__.load({
|
|
|
1001
1001
|
* ① session 域 slot 的标准属性 `sessionId`(SessionId);
|
|
1002
1002
|
* ② `session`(会话域 ownerProps 里的 SessionSnapshot,id 在 .sessionId 上);
|
|
1003
1003
|
* ③ 异形兜底:props.sessionId 是对象时读它的 .sessionId。
|
|
1004
|
-
* 注意:**不要**再读 useSessions() 的 `current` 字段 ——
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1004
|
+
* 注意:**不要**再读 useSessions() 的 `current` 字段 —— dsh 的 SessionListState 里从来没有它
|
|
1005
|
+
* (0.1.6-alpha.2 是 ids / byId / phase;0.1.7-alpha.1 又加了 projectionsBySession,仍然没有 current;
|
|
1006
|
+
* 见 dsh-api-session-controller 的 sessions/service.d.ts)。旧代码读它恒为 null,于是「会话投影 =
|
|
1007
|
+
* 权威」这条路径从来没生效过(issue #5)。**只准读 ids / byId / phase**:这个结构的字段集已经改过
|
|
1008
|
+
* 两轮(subagentsByParent / jobsBySession → projectionsBySession),多读一个字段就是下一个静默回归。
|
|
1007
1009
|
*/
|
|
1008
1010
|
function ccSessionIdOfProps(props){
|
|
1009
1011
|
try{
|
|
@@ -1080,7 +1082,9 @@ window.__ModuleLoader__.load({
|
|
|
1080
1082
|
if(!s.isCcMode && !s.panelOpen) return null;
|
|
1081
1083
|
var draft=s.draft; var loreCount=draft.data.character_book.entries.length;
|
|
1082
1084
|
var valid=s.valid && s.valid.valid;
|
|
1083
|
-
//
|
|
1085
|
+
// 计数文案走词表。注:dsh 的 t(key, params?) **是支持 `{name}` 插值的**(0.1.6 / 0.1.7 都支持),
|
|
1086
|
+
// 这里手写 replace 只因为这些键历史上就不带参数;新键请直接用 t(key, {n: ...}),
|
|
1087
|
+
// 千万别对同一个键既传 params 又手写 replace(会替换两次)。
|
|
1084
1088
|
var loreText=t("common.loreCount").replace("{n}", String(loreCount));
|
|
1085
1089
|
var text=(t("capsule.open")+" · "+loreText+" · "+ (valid? t("capsule.exported"): t("capsule.draft")) + (s.slotWarn? (" · "+t("capsule.slotWarn")): ""));
|
|
1086
1090
|
return h("div",{style:S.dockWrap, "data-dsh-cc-studio":"dock-row"},
|
package/lib/index.js
CHANGED
|
@@ -98,11 +98,30 @@ function makeDefaultDraft(){
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
if(!globalThis.__CC_DRAFTS__) globalThis.__CC_DRAFTS__ = new Map();
|
|
101
|
-
// ——
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
// —— 落盘根(0.3.6 起统一):DSH_HOME 优先,homedir()/.dsh 兜底 ——
|
|
102
|
+
// 预设目录一直走这里;草稿与角色库在 0.3.5 及以前写死 homedir()/.dsh,现在统一到同一套根,
|
|
103
|
+
// 但**读盘必须回退旧根** —— 否则自定义了 DSH_HOME 的用户升级后会看到「草稿和角色卡全没了」。
|
|
104
|
+
function legacyDshHomeDir(){
|
|
105
|
+
try{ return join(homedir(), ".dsh"); }catch{ return join(process.cwd(), ".dsh"); }
|
|
106
|
+
}
|
|
107
|
+
function dshHomeDir(){
|
|
108
|
+
const env = process.env.DSH_HOME;
|
|
109
|
+
if(typeof env === "string" && env.trim().length > 0) return env.trim();
|
|
110
|
+
return legacyDshHomeDir();
|
|
105
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* 某个子目录的候选根:`[主根, 旧根]`。写盘只写 candidates[0],读盘依次回退。
|
|
114
|
+
* `DSH_HOME` 未设置时两个根是同一个目录 → 去重成一条,**默认安装的行为与 0.3.5 完全一致**。
|
|
115
|
+
*/
|
|
116
|
+
function dataRoots(sub){
|
|
117
|
+
const primary = join(dshHomeDir(), sub);
|
|
118
|
+
const legacy = join(legacyDshHomeDir(), sub);
|
|
119
|
+
return primary === legacy ? [primary] : [primary, legacy];
|
|
120
|
+
}
|
|
121
|
+
// —— 草稿持久化(fix #2):<主根>/cc-drafts/<session>.json,变更即落盘,重启后按会话 id 恢复 ——
|
|
122
|
+
if(!globalThis.__CC_DRAFT_META__) globalThis.__CC_DRAFT_META__ = new Map();
|
|
123
|
+
function draftDirs(){ return dataRoots("cc-drafts"); }
|
|
124
|
+
function draftDir(){ return draftDirs()[0]; }
|
|
106
125
|
function safeDraftFile(key){
|
|
107
126
|
const raw = String(key || "default");
|
|
108
127
|
let h = 0;
|
|
@@ -112,9 +131,20 @@ function safeDraftFile(key){
|
|
|
112
131
|
return safe + "-" + hex + ".json";
|
|
113
132
|
}
|
|
114
133
|
function draftPath(key){ return join(draftDir(), safeDraftFile(key)); }
|
|
134
|
+
/** 读盘用:主根没有就回退旧根(0.3.6 之前草稿写死的地方)。找不到返回 null。 */
|
|
135
|
+
function findDraftPath(key){
|
|
136
|
+
const file = safeDraftFile(key);
|
|
137
|
+
for(const dir of draftDirs()){
|
|
138
|
+
const p = join(dir, file);
|
|
139
|
+
try{ if(existsSync(p)) return p; }catch{}
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
115
143
|
function loadPersistedDraftSync(key){
|
|
116
144
|
try{
|
|
117
|
-
const
|
|
145
|
+
const p = findDraftPath(key);
|
|
146
|
+
if(!p) return null;
|
|
147
|
+
const raw = readFileSync(p, "utf8");
|
|
118
148
|
const obj = JSON.parse(raw);
|
|
119
149
|
const d = obj && obj.draft ? obj.draft : null;
|
|
120
150
|
if(d && typeof d === "object" && d.spec === "chara_card_v3" && d.data && typeof d.data === "object") return d;
|
|
@@ -185,21 +215,27 @@ function draftContentSummary(draft){
|
|
|
185
215
|
/**
|
|
186
216
|
* 盘上还有哪些草稿槽(按写入时间倒序)。只用于「当前槽是空壳」时的救回提示 ——
|
|
187
217
|
* 这是 issue #5 里用户唯一能自救的入口:内容没丢,只是被写进了另一把 key。
|
|
218
|
+
* 两个根都扫:主根优先,同一把槽只报一次(主根那份解析失败时才让旧根补位)。
|
|
188
219
|
*/
|
|
189
220
|
function listDraftSlotsOnDisk(){
|
|
190
221
|
const out = [];
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
for(const dir of draftDirs()){
|
|
224
|
+
let names = [];
|
|
225
|
+
try{ names = readdirSync(dir); }catch{ continue; }
|
|
226
|
+
for(const f of names){
|
|
227
|
+
if(!/\.json$/i.test(String(f))) continue;
|
|
228
|
+
if(seen.has(String(f))) continue;
|
|
229
|
+
let obj = null;
|
|
230
|
+
try{ obj = JSON.parse(readFileSync(join(dir, String(f)), "utf8")); }catch{ continue; }
|
|
231
|
+
const d = obj && obj.draft;
|
|
232
|
+
if(!d || typeof d !== "object" || d.spec !== "chara_card_v3" || !d.data || typeof d.data !== "object") continue;
|
|
233
|
+
const key = obj.key == null ? null : String(obj.key);
|
|
234
|
+
if(!key) continue;
|
|
235
|
+
seen.add(String(f));
|
|
236
|
+
const sum = draftContentSummary(d);
|
|
237
|
+
out.push({ key: key, updatedAt: Number(obj.updatedAt) || 0, name: sum.name, entries: sum.entries, hasContent: sum.filled > 0 });
|
|
238
|
+
}
|
|
203
239
|
}
|
|
204
240
|
out.sort(function(a,b){ return b.updatedAt - a.updatedAt; });
|
|
205
241
|
return out;
|
|
@@ -270,15 +306,24 @@ function isCcPreset(ctx, sessionId){
|
|
|
270
306
|
return false;
|
|
271
307
|
}
|
|
272
308
|
|
|
273
|
-
// —— 已存角色侧边栏:落盘到
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
}
|
|
309
|
+
// —— 已存角色侧边栏:落盘到 <主根>/cc-library/<id>.json(读盘回退 0.3.6 之前的 homedir()/.dsh)——
|
|
310
|
+
function libDirs(){ return dataRoots("cc-library"); }
|
|
311
|
+
function libDir(){ return libDirs()[0]; }
|
|
277
312
|
async function ensureLibDir(){
|
|
278
313
|
const dir = libDir();
|
|
279
314
|
await mkdir(dir, { recursive: true });
|
|
280
315
|
return dir;
|
|
281
316
|
}
|
|
317
|
+
/** 读盘用:主根没有就回退旧根。返回 `{path, raw}`;两边都没有时抛最后一个错误(与原 readFile 语义一致)。 */
|
|
318
|
+
async function readLibraryRaw(id){
|
|
319
|
+
let lastErr = null;
|
|
320
|
+
for(const dir of libDirs()){
|
|
321
|
+
const p = join(dir, String(id)+".json");
|
|
322
|
+
try{ return { path: p, raw: await readFile(p, "utf8") }; }
|
|
323
|
+
catch(e){ lastErr = e; }
|
|
324
|
+
}
|
|
325
|
+
throw lastErr || new Error("找不到角色卡: " + String(id));
|
|
326
|
+
}
|
|
282
327
|
// ========== PNG / CHARX 共享 ==========
|
|
283
328
|
function normalizeDraft(d){
|
|
284
329
|
if(!d || typeof d!=="object") throw new Error("draft 必须是对象");
|
|
@@ -491,26 +536,29 @@ function makeLibraryEntryMeta(draft, existingMeta, id){
|
|
|
491
536
|
};
|
|
492
537
|
}
|
|
493
538
|
async function listLibraryEntries(){
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
539
|
+
await ensureLibDir();
|
|
540
|
+
const seen = new Map(); // id → meta:先到先得,主根排在前 → 同名卡片主根优先
|
|
541
|
+
for(const dir of libDirs()){
|
|
542
|
+
let files=[];
|
|
543
|
+
try{ files = await readdir(dir); }catch{ continue; }
|
|
544
|
+
for(const f of files){
|
|
545
|
+
if(!f.endsWith(".json")) continue;
|
|
546
|
+
const id = f.replace(/\.json$/,"");
|
|
547
|
+
if(seen.has(id)) continue;
|
|
548
|
+
try{
|
|
549
|
+
const raw = await readFile(join(dir, f), "utf8");
|
|
550
|
+
const obj = JSON.parse(raw);
|
|
551
|
+
if(obj && obj.meta && obj.id){
|
|
552
|
+
// 兼容:meta.id 与文件名 id 一致
|
|
553
|
+
seen.set(id, obj.meta);
|
|
554
|
+
} else if(obj && obj.draft){
|
|
555
|
+
// 旧格式兜底:从 draft 重建 meta
|
|
556
|
+
seen.set(id, makeLibraryEntryMeta(obj.draft, null, id));
|
|
557
|
+
}
|
|
558
|
+
}catch{}
|
|
559
|
+
}
|
|
513
560
|
}
|
|
561
|
+
const entries = [...seen.values()];
|
|
514
562
|
entries.sort((a,b)=> (b.updatedAt||0)-(a.updatedAt||0));
|
|
515
563
|
return entries;
|
|
516
564
|
}
|
|
@@ -519,10 +567,8 @@ async function saveLibraryEntry(draft, opts){
|
|
|
519
567
|
let id = opts && opts.id ? String(opts.id) : "";
|
|
520
568
|
let existing = null;
|
|
521
569
|
if(id){
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
existing = JSON.parse(raw);
|
|
525
|
-
}catch{}
|
|
570
|
+
// 已存在的那份可能在旧根(0.3.6 之前保存的),读过来只为继承 createdAt / 旧 name
|
|
571
|
+
try{ existing = JSON.parse((await readLibraryRaw(id)).raw); }catch{}
|
|
526
572
|
}
|
|
527
573
|
if(!id){
|
|
528
574
|
id = String(Date.now())+"-"+Math.random().toString(36).slice(2,6);
|
|
@@ -537,29 +583,35 @@ async function saveLibraryEntry(draft, opts){
|
|
|
537
583
|
return { id, meta, draft };
|
|
538
584
|
}
|
|
539
585
|
async function loadLibraryEntry(id){
|
|
540
|
-
|
|
541
|
-
const
|
|
542
|
-
const raw = await readFile(p, "utf8");
|
|
543
|
-
const obj = JSON.parse(raw);
|
|
586
|
+
await ensureLibDir();
|
|
587
|
+
const obj = JSON.parse((await readLibraryRaw(id)).raw);
|
|
544
588
|
if(!obj || !obj.draft) throw new Error("文件格式错误");
|
|
545
589
|
return obj;
|
|
546
590
|
}
|
|
591
|
+
/**
|
|
592
|
+
* 删除:**两个根里的同名副本都要删**,否则读盘回退会把刚删掉的卡「复活」。
|
|
593
|
+
* 一个都没删到时仍然抛错(保持「删不存在的 id 是失败」的既有语义)。
|
|
594
|
+
*/
|
|
547
595
|
async function deleteLibraryEntry(id){
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
596
|
+
await ensureLibDir();
|
|
597
|
+
let removed = 0;
|
|
598
|
+
for(const dir of libDirs()){
|
|
599
|
+
try{ await unlink(join(dir, String(id)+".json")); removed += 1; }
|
|
600
|
+
catch(e){ if(e && e.code !== "ENOENT") throw e; }
|
|
601
|
+
}
|
|
602
|
+
if(removed === 0) throw Object.assign(new Error("找不到角色卡: " + String(id)), { code: "ENOENT" });
|
|
551
603
|
}
|
|
604
|
+
/** 改名是**就地改** meta.name:写在解析到的那一份上(主根优先),不搬家、不复制。 */
|
|
552
605
|
async function renameLibraryEntry(id, newName){
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
const
|
|
556
|
-
const obj = JSON.parse(raw);
|
|
606
|
+
await ensureLibDir();
|
|
607
|
+
const found = await readLibraryRaw(id);
|
|
608
|
+
const obj = JSON.parse(found.raw);
|
|
557
609
|
if(!obj || !obj.meta) throw new Error("文件格式错误");
|
|
558
610
|
const name = String(newName||"").trim().slice(0,60);
|
|
559
611
|
if(!name) throw new Error("name 不能为空");
|
|
560
612
|
obj.meta.name = name;
|
|
561
613
|
obj.meta.updatedAt = nowSec();
|
|
562
|
-
await writeFile(
|
|
614
|
+
await writeFile(found.path, JSON.stringify(obj, null, 2), "utf8");
|
|
563
615
|
return obj.meta;
|
|
564
616
|
}
|
|
565
617
|
|
|
@@ -622,11 +674,18 @@ function sendEnvelope(res, status, payload){
|
|
|
622
674
|
function rpcFull(rpcId, result){ return { type:"server-response", rpcId, result }; }
|
|
623
675
|
function rpcError(rpcId, code, message, details){ return rpcFull(rpcId, { ok:false, error:{ code, message, details } }); }
|
|
624
676
|
|
|
625
|
-
// ========== CC
|
|
626
|
-
//
|
|
677
|
+
// ========== CC 预设安装 ==========
|
|
678
|
+
// dsh >= 0.1.7-alpha.1:预设机制换代了。预设不再是「<DSH_HOME>/.agent-presets/<id>/ 目录」,
|
|
679
|
+
// 而是**组合里的一个声明行**(内置 standard/ptc/minimal/cordis 就是这么发的),目录形态在 0.1.7
|
|
680
|
+
// 已经没有任何代码读取 —— 这就是「升级到 0.1.7-alpha.1 后 CC 模式从 roster 里消失」的原因:
|
|
681
|
+
// 文件还在,只是没人再看它。本插件因此把声明写进随包的补丁层 presets/cc.patch.yml
|
|
682
|
+
// (package.json 的 dsh.bundle.patch 第二层):插件装进哪个 profile,CC 模式就出现在哪个
|
|
683
|
+
// profile 的 roster 里,不再依赖「挂载时往用户目录写文件」。
|
|
684
|
+
//
|
|
685
|
+
// 下面这套目录安装**只为更老的 dsh(<= 0.1.6-alpha.2)保留**:那条路径上预设发现只扫三个根
|
|
627
686
|
// ——shipped 根(dsh-agent-presets 包内)+ 部署 config.roots + 用户根 <DSH_HOME>/.agent-presets
|
|
628
687
|
// (见 dsh-agent-presets lib/index.js:1300-1310);插件包内的 presets/ 不在其中,
|
|
629
|
-
//
|
|
688
|
+
// 「随包分发」≠「已注册」,所以必须挂载时把模板装进用户根,「装完插件即有 CC 模式」才成立。
|
|
630
689
|
//
|
|
631
690
|
// 安全策略(绝不静默覆盖用户改动):
|
|
632
691
|
// · 目标文件不存在 → 写入模板
|
|
@@ -635,15 +694,31 @@ function rpcError(rpcId, code, message, details){ return rpcFull(rpcId, { ok:fal
|
|
|
635
694
|
// · 其余(用户改过 / 无记录的历史手工拷贝)→ 跳过并告警,交由用户决定
|
|
636
695
|
// 安装记录是目标目录里的 JSON 侧车文件,含每个受管文件写入时的 sha256,
|
|
637
696
|
// 因此不依赖版本号比较也能正确判定「这是不是我们写的、用户有没有动过」。
|
|
638
|
-
// config.presetInstall: "auto"(默认)| "off" | "force"
|
|
697
|
+
// config.presetInstall: "auto"(默认)| "off" | "force"(连用户改动一起覆盖)。
|
|
698
|
+
// 新版 dsh 上这份目录安装自动让位(status: "native"),开关也无意义 —— 想关 CC 模式就在
|
|
699
|
+
// profile 补丁里按 id 关掉 preset-cc 那一行(presets/cc.patch.yml 顶部有现成的写法)。
|
|
639
700
|
const PRESET_ID = "cc";
|
|
640
701
|
const PRESET_MANAGED_FILES = ["preset.yml", "agent.cordis.yml"];
|
|
641
702
|
const PRESET_MARKER_FILE = ".dsh-cc-studio-preset.json";
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
703
|
+
/**
|
|
704
|
+
* 当前 dsh 是否已经用「声明行」发预设(>= 0.1.7-alpha.1)。
|
|
705
|
+
* 只用来决定「要不要往那份已经没人读的目录里写字」,判错的代价并不对称:
|
|
706
|
+
* · 误判 false(在老版上)→ 照旧写一次目录,无害;
|
|
707
|
+
* · 误判 true(在真需要目录的老版上)→ 才是真的少一个预设。
|
|
708
|
+
* 所以要求两个方法**同时**存在,且**不许简化成其中任何一个**:
|
|
709
|
+
* 0.1.6 的 `dsh-agent-presets` 也有 `compositionInventory`(`async compositionInventory()`),
|
|
710
|
+
* 单看它会把老版误判成新版;`register(definition)` 才是 0.1.7 新注册表独有的(老服务里
|
|
711
|
+
* `register` 只出现在 `settings.register` / `sessionProjections.register` 上)。
|
|
712
|
+
*/
|
|
713
|
+
function supportsDeclaredPresets(ctx){
|
|
714
|
+
try{
|
|
715
|
+
const presets = ctx && typeof ctx.get === "function" ? ctx.get("agentPresets") : null;
|
|
716
|
+
return !!(presets && typeof presets.register === "function" && typeof presets.compositionInventory === "function");
|
|
717
|
+
}catch{ return false; }
|
|
646
718
|
}
|
|
719
|
+
|
|
720
|
+
// `dshHomeDir()` / `legacyDshHomeDir()` / `dataRoots()` 定义在文件顶部(草稿那一节之前)——
|
|
721
|
+
// 这里**不再重复定义**:两个同名 function 声明会「后者悄悄赢」,是漂移的温床。
|
|
647
722
|
function presetSourceRoot(){
|
|
648
723
|
try{ return fileURLToPath(new URL("../presets/"+PRESET_ID+"/", import.meta.url)); }catch{ return null; }
|
|
649
724
|
}
|
|
@@ -675,7 +750,8 @@ function planPresetInstall(targets, templates, marker, force){
|
|
|
675
750
|
return { write, skip, ok };
|
|
676
751
|
}
|
|
677
752
|
/**
|
|
678
|
-
* 把 presets/cc 安装到 <DSH_HOME>/.agent-presets/cc
|
|
753
|
+
* 把 presets/cc 安装到 <DSH_HOME>/.agent-presets/cc(**只对 < 0.1.7-alpha.1 有意义**)。
|
|
754
|
+
* 同步执行(只有两个小文件),幂等。
|
|
679
755
|
* 注意:卸载插件**不会**删除已安装的预设(与 README 的既有说明一致),因此这里不注册删除型 disposer。
|
|
680
756
|
*/
|
|
681
757
|
function installCcPreset(ctx, config){
|
|
@@ -692,6 +768,18 @@ function installCcPreset(ctx, config){
|
|
|
692
768
|
if(l && typeof l[level] === "function") l[level]("[dsh-cc-studio] " + msg);
|
|
693
769
|
else if(level === "warn") console.warn("[dsh-cc-studio] " + msg);
|
|
694
770
|
}catch{} };
|
|
771
|
+
// dsh >= 0.1.7-alpha.1:预设已随包声明(presets/cc.patch.yml),目录形态被彻底忽略。
|
|
772
|
+
// 这里一个字都不写 —— 否则只会留下一个没人读的目录,下次有人「修预设」时还要多查一层。
|
|
773
|
+
if(supportsDeclaredPresets(ctx)){
|
|
774
|
+
out.status = "native";
|
|
775
|
+
out.reason = "dsh >= 0.1.7-alpha.1:CC 预设由随包补丁层 presets/cc.patch.yml 声明,目录形态已不再被读取,跳过安装";
|
|
776
|
+
log("info", out.reason);
|
|
777
|
+
try{
|
|
778
|
+
const dst = presetTargetRoot();
|
|
779
|
+
if(existsSync(dst)) log("info", "检测到旧版安装目录 " + dst + "(0.1.6 及更早的形态):0.1.7 起不再被读取,可以安全删除。");
|
|
780
|
+
}catch{}
|
|
781
|
+
return out;
|
|
782
|
+
}
|
|
695
783
|
try{
|
|
696
784
|
const src = presetSourceRoot();
|
|
697
785
|
const templates = {};
|
|
@@ -1040,7 +1128,11 @@ function apply(ctx, config){
|
|
|
1040
1128
|
}), "dsh-cc-studio: POST "+RPC_CHANNEL+"/*");
|
|
1041
1129
|
}
|
|
1042
1130
|
|
|
1043
|
-
export { apply, inject, name, planPresetInstall, installCcPreset, presetTargetRoot, presetSourceRoot,
|
|
1044
|
-
draftKeyPartsFrom, draftContentSummary, listDraftSlotsOnDisk, migrateDraftDecision
|
|
1131
|
+
export { apply, inject, name, planPresetInstall, installCcPreset, supportsDeclaredPresets, presetTargetRoot, presetSourceRoot,
|
|
1132
|
+
draftKeyPartsFrom, draftContentSummary, listDraftSlotsOnDisk, migrateDraftDecision,
|
|
1133
|
+
// 落盘根与 lib 层 CRUD(供回归测试直接断言「DSH_HOME 优先 + 读时回退」)
|
|
1134
|
+
dshHomeDir, legacyDshHomeDir, dataRoots, draftDirs, draftDir, draftPath, findDraftPath, safeDraftFile,
|
|
1135
|
+
persistDraftSync, loadPersistedDraftSync, libDirs, libDir,
|
|
1136
|
+
listLibraryEntries, saveLibraryEntry, loadLibraryEntry, deleteLibraryEntry, renameLibraryEntry };
|
|
1045
1137
|
|
|
1046
1138
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xia-sc/dsh-cc-studio",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "CCv3 角色卡工坊:融合工坊 + CC 模式 + JSON/PNG/CHARX + 中英双语(跟随全局)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
"presets"
|
|
36
36
|
],
|
|
37
37
|
"scripts": {
|
|
38
|
-
"test": "node tests/rpc-channel.test.mjs && node tests/preset-install.test.mjs && node tests/cc-detection.test.mjs && node tests/client-greetings.test.mjs && node tests/draft-slot-sync.test.mjs",
|
|
38
|
+
"test": "node tests/rpc-channel.test.mjs && node tests/preset-install.test.mjs && node tests/cc-detection.test.mjs && node tests/client-greetings.test.mjs && node tests/draft-slot-sync.test.mjs && node tests/data-root.test.mjs",
|
|
39
39
|
"prepublishOnly": "npm test"
|
|
40
40
|
},
|
|
41
41
|
"dsh": {
|
|
42
42
|
"bundle": {
|
|
43
|
-
"patch":
|
|
43
|
+
"patch": [
|
|
44
|
+
"./cordis.patch.yml",
|
|
45
|
+
"./presets/cc.patch.yml"
|
|
46
|
+
]
|
|
44
47
|
},
|
|
45
48
|
"client": {
|
|
46
49
|
"platform": "web",
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# @xia-sc/dsh-cc-studio — 「CC 模式」预设声明层(dsh >= 0.1.7-alpha.1)
|
|
2
|
+
#
|
|
3
|
+
# 为什么单独一层:dsh 0.1.7-alpha.1 起,预设不再是「<DSH_HOME>/.agent-presets/<id>/ 目录」,
|
|
4
|
+
# 而是**组合里的一个声明行** —— 内置的 standard/ptc/minimal/cordis 就是这么发的(见
|
|
5
|
+
# @deepseek-ai/dsh-web-app 的 presets/*.patch.yml)。目录形态在 0.1.7 已经没有任何代码读取,
|
|
6
|
+
# 所以必须随包带一层补丁:插件装进哪个 profile,「CC 模式」就出现在哪个 profile 的 roster 里。
|
|
7
|
+
#
|
|
8
|
+
# package.json 的 dsh.bundle.patch 列两层:cordis.patch.yml(宿主插件行)+ 本文件(预设声明)。
|
|
9
|
+
#
|
|
10
|
+
# ⚠️ 本文件的 plugins 必须与 presets/cc/agent.cordis.yml **同名同序**:
|
|
11
|
+
# · 本文件(声明行) → dsh >= 0.1.7-alpha.1
|
|
12
|
+
# · agent.cordis.yml(目录形态,插件挂载时安装到 <DSH_HOME>/.agent-presets/cc)→ 更老的 dsh
|
|
13
|
+
# tests/preset-install.test.mjs 第 11/12 节把「两份清单不许漂移」钉死了:改一边必须改另一边。
|
|
14
|
+
# 两份清单的行逐字一致,只有上面那两处解释「健康检查」的历史注释按 0.1.7 的事实改写过。
|
|
15
|
+
#
|
|
16
|
+
# 想关掉 CC 模式(>=0.1.7):不必卸载插件,在自己 profile 的补丁层里按 id 关掉这一行即可 ——
|
|
17
|
+
# - id: preset-cc
|
|
18
|
+
# name: '@deepseek-ai/dsh-agent-preset'
|
|
19
|
+
# disabled: true
|
|
20
|
+
# profile 补丁 = <profile>/cordis.patch.yml;$DSH_HOME/cordis.patch.yml 对全部 profile 生效。
|
|
21
|
+
- insert:
|
|
22
|
+
- id: preset-cc
|
|
23
|
+
name: '@deepseek-ai/dsh-agent-preset'
|
|
24
|
+
config:
|
|
25
|
+
# 预设 id 是硬契约:lib/agent.js 只在 CC 预设下挂载,浏览器 lib/client.js 的
|
|
26
|
+
# CC_PRESET_ID 与宿主 isCcPreset() 都按 'cc' 精确比较,改名会让两边同时失联。
|
|
27
|
+
id: cc
|
|
28
|
+
name: CC 模式
|
|
29
|
+
description: CCv3 角色卡工坊 — LLM 通过 6 个 Tool 引导填表(cc_get_card / cc_patch_character / cc_patch_world / cc_add_lorebook_entries / cc_patch_greetings / cc_validate),胶囊实时同步,工坊全屏编辑,导出 chara_card_v3。
|
|
30
|
+
# 排序只在 roster 里可见:内置四个是 1..4,CC 模式排在它们后面。
|
|
31
|
+
order: 50
|
|
32
|
+
plugins:
|
|
33
|
+
# The `standard` agent preset: the full coding agent, mounted once per process.
|
|
34
|
+
#
|
|
35
|
+
# This file is an AGENT-PLANE composition. The roster mounts it ONCE under a
|
|
36
|
+
# standing scope; every session naming it joins by scope parentage, so the
|
|
37
|
+
# tools and prompt sections registered here cover each joined agent while a
|
|
38
|
+
# session's own state stays keyed per Session/Agent inside the plugins. The
|
|
39
|
+
# host composition (`base.cordis.yml` + `web.cordis.yml`) keeps everything a
|
|
40
|
+
# preset must not own: the registries themselves, the sandbox and approval
|
|
41
|
+
# stack, persistence, and the model route.
|
|
42
|
+
#
|
|
43
|
+
# A service row here MUST sit inside a group carrying an `isolate` realm.
|
|
44
|
+
# Without one it publishes into the root realm, where it is process-global —
|
|
45
|
+
# another preset publishing the same name collides, and a host reader would
|
|
46
|
+
# resolve one preset's instance for every session; `dsh-agent-presets` rejects
|
|
47
|
+
# that at mount. `true` means an entry-local realm: this standing mount's own
|
|
48
|
+
# private instance, apart from every other preset's. (A shared label does NOT
|
|
49
|
+
# pool instances — `provide()` throws on the second registration under the
|
|
50
|
+
# same realm symbol; labels join REALMS, and are not what this file needs.)
|
|
51
|
+
|
|
52
|
+
# ── identity ────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
# The preset's own persona, shadowing the deployment default for this agent.
|
|
55
|
+
# dsh >= 0.1.5-rc.1: the persona row takes `prefix` (required) + optional `suffix`
|
|
56
|
+
# — the old `text` key was replaced and now fails the config schema.
|
|
57
|
+
# `{{model}}` and `{{cwd}}` resolve from the agent's own route and workspace.
|
|
58
|
+
- id: persona
|
|
59
|
+
name: '@deepseek-ai/dsh-persona'
|
|
60
|
+
config:
|
|
61
|
+
prefix: |
|
|
62
|
+
你是 CC 角色卡工坊的专属引导员(CC 模式),模型 {{model}},工作目录 {{cwd}}。
|
|
63
|
+
你的唯一任务是把用户的一句话点子变成可导入 SillyTavern / Risu 的完整 CCv3 卡(spec: chara_card_v3)。
|
|
64
|
+
必须走完强制工作流,不可偷懒跳过 5 维世界观和世界书:
|
|
65
|
+
1) cc_get_card — 先看草稿,告诉用户还缺什么
|
|
66
|
+
2) cc_patch_character — 补 name/nickname/description/personality/scenario/tags(至少 description/personality/scenario 非空,tags 同步胶囊风格标签)
|
|
67
|
+
3) cc_patch_world — 补五维 timeline/factions/geo/power/daily,至少填 3 项,autoLorebook=true 自动生成带 @@decorator 的基础条目
|
|
68
|
+
4) cc_add_lorebook_entries — 再补到至少 5 条,覆盖年表/势力/地理/力量/日常各 1,至少 1 条 constant,content 头部写 @@position/@@depth/@@activate 等,支持 CBS 占位符(双花括号的 char / random / roll 形式)
|
|
69
|
+
5) cc_patch_greetings — 补 first_mes、alternate_greetings(≥2)、group_only_greetings(≥1 必填)、mes_example
|
|
70
|
+
6) cc_validate — 必须调且 valid=true 才算完成,否则按返回的 errors/warnings 继续补;通过后提醒用户在工坊点“导出 JSON”或“★ 保存到侧边栏”(落盘 ~/.dsh/cc-library)
|
|
71
|
+
|
|
72
|
+
规则:每次对话只追问 1-2 个薄弱点,问完立刻调工具写入;用户说“随便/跳过”也要你合理推断补全,不能留空;严禁跳过 3/4 步直接做问候语;每调一次工具要简要总结“已写入 X,下一步 Y”,并让胶囊实时同步;结束前必须 cc_validate。
|
|
73
|
+
|
|
74
|
+
- id: agent-instructions
|
|
75
|
+
name: '@deepseek-ai/dsh-agent-instructions'
|
|
76
|
+
config:
|
|
77
|
+
maxBytes: 65536
|
|
78
|
+
|
|
79
|
+
# ── shell ───────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
# `shell-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
|
82
|
+
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
|
83
|
+
# the criterion for host-plane ownership — injection resolves before any session
|
|
84
|
+
# exists, so there is no agent to key by. Behind a preset realm those variables
|
|
85
|
+
# never reached the model's shell at all. Both shell tools consume the host
|
|
86
|
+
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
|
87
|
+
# host-plane too.
|
|
88
|
+
- id: tool-bash
|
|
89
|
+
name: '@deepseek-ai/dsh-tool-bash'
|
|
90
|
+
disabled: !!js process.platform === 'win32'
|
|
91
|
+
|
|
92
|
+
- id: tool-pwsh
|
|
93
|
+
name: '@deepseek-ai/dsh-tool-pwsh'
|
|
94
|
+
disabled: !!js process.platform !== 'win32'
|
|
95
|
+
|
|
96
|
+
# ── filesystem ──────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
# Both register into the host `tools` registry and provide nothing, so
|
|
99
|
+
# they need no realm. The `fs` service and its policy stay in the host.
|
|
100
|
+
- id: tool-fs
|
|
101
|
+
name: '@deepseek-ai/dsh-tool-fs'
|
|
102
|
+
|
|
103
|
+
- id: tool-fs-search
|
|
104
|
+
name: '@deepseek-ai/dsh-tool-fs-search'
|
|
105
|
+
config:
|
|
106
|
+
sampleOverCapGlobResults: false
|
|
107
|
+
|
|
108
|
+
# ── background jobs ────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
# Only the model-facing controls. The task REGISTRY stays on the host plane:
|
|
111
|
+
# its producers sit outside any realm this file could put it in — `tool-bash`
|
|
112
|
+
# above resolves it with `ctx.get`, and an entry-local realm here is invisible
|
|
113
|
+
# to every sibling row, so `run_in_background` would answer "background jobs
|
|
114
|
+
# unavailable" while these controls sat in the catalog. The registry is keyed by
|
|
115
|
+
# owning agent anyway, so one host instance serves every session. What a preset
|
|
116
|
+
# chooses is whether its agent can collect and stop background work at all.
|
|
117
|
+
- id: tool-jobs
|
|
118
|
+
name: '@deepseek-ai/dsh-tool-jobs'
|
|
119
|
+
|
|
120
|
+
# ── skills ──────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
# The skill REGISTRY lives in the host composition and is layered per scope:
|
|
123
|
+
# these rows register into THIS preset's layer of it, so they need no realm.
|
|
124
|
+
# `skill-filesystem` contributes local-root discovery for agents on this preset, and
|
|
125
|
+
# `tool-skill` gives them the catalog and loader; the merged catalog also
|
|
126
|
+
# carries whatever the deployment registered globally (repository plugins).
|
|
127
|
+
- id: skill-filesystem
|
|
128
|
+
name: '@deepseek-ai/dsh-skill-filesystem'
|
|
129
|
+
disabled: true
|
|
130
|
+
|
|
131
|
+
- id: tool-skill
|
|
132
|
+
name: '@deepseek-ai/dsh-tool-skill'
|
|
133
|
+
disabled: true
|
|
134
|
+
|
|
135
|
+
# ── goals ───────────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
# Only the model-facing tool. The goal SERVICE, its session driver, and the
|
|
138
|
+
# `/goal` command stay on the host plane: the Gateway serves the goal domain as
|
|
139
|
+
# Remote endpoints whose receiver comes from a generated descriptor, so it
|
|
140
|
+
# resolves `goals` on the host and an entry-local realm here would hide it. The
|
|
141
|
+
# registry is keyed by session anyway, so one host instance serves every
|
|
142
|
+
# session. What a preset chooses is whether its agent can call the goal tool.
|
|
143
|
+
- id: tool-goal
|
|
144
|
+
name: '@deepseek-ai/dsh-tool-goal'
|
|
145
|
+
|
|
146
|
+
# ── plan mode ───────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
# Plan state is per-agent by nature, so an entry-local realm is not a
|
|
149
|
+
# workaround here — it is the correct lifetime.
|
|
150
|
+
- id: planning
|
|
151
|
+
name: cordis:group
|
|
152
|
+
group: true
|
|
153
|
+
isolate:
|
|
154
|
+
planMode: true
|
|
155
|
+
config:
|
|
156
|
+
- id: plan-mode
|
|
157
|
+
name: '@deepseek-ai/dsh-plan-mode'
|
|
158
|
+
config:
|
|
159
|
+
section: |
|
|
160
|
+
You are in plan mode. Stay in plan mode until exit_plan_mode succeeds or the user switches the session mode. Imperative language to implement changes means plan the implementation, not execute it. A user's conversational agreement — including an answer confirming something you asked — approves nothing and does not end plan mode; fold the confirmed decision into the plan and submit it through exit_plan_mode.
|
|
161
|
+
|
|
162
|
+
Explore first. Use non-mutating reads, searches, static analysis, and checks to ground the plan in the actual repository. Do not edit or write files, change configuration, run formatters or code generation that rewrites tracked files, commit, or otherwise carry out the plan. Prefer existing functions and patterns over new machinery.
|
|
163
|
+
|
|
164
|
+
The tool catalog stays the same across modes for request-cache stability. These plan-mode rules override any later tool description or guidance that suggests using mutation tools; those tools remain listed to keep the tool catalog unchanged. Do not use todo_write to track this planning phase: it tracks implementation after an approved plan, while the plan itself belongs in exit_plan_mode.
|
|
165
|
+
|
|
166
|
+
Resolve discoverable facts by inspection. Use ask_user_question only for user-owned choices or material ambiguity that inspection cannot answer. Do not ask the user where code lives or how current behavior works when you can find out.
|
|
167
|
+
|
|
168
|
+
Make the plan decision-complete: state the goal and success criteria; group implementation changes by subsystem; identify public API, schema, and data-flow changes; cover edge cases, failure modes, tests, acceptance criteria, and explicit assumptions. Keep it concise enough to review but detailed enough that another engineer can implement it without making design decisions.
|
|
169
|
+
|
|
170
|
+
When ready, call exit_plan_mode with the complete plan markdown, starting with a # title. Make exit_plan_mode the only and final tool call in that assistant response: it presents the plan for approval, and implementation begins only in a later step after approval. Do not paste the final plan as a plain reply or ask "should I proceed?" through prose or ask_user_question. If review rejects it, incorporate the feedback and present again. If the review channel is unavailable or aborted, stay in plan mode and ask the user to switch modes manually; do not proceed with implementation.
|
|
171
|
+
|
|
172
|
+
# ── compaction ──────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
# `compaction-basic` reads `toolResultPrune` through `ctx.get`, so the pruner must
|
|
175
|
+
# share this realm rather than sit outside it.
|
|
176
|
+
#
|
|
177
|
+
# `tokenMeter` is deliberately NOT in this realm: the meter stays on the HOST
|
|
178
|
+
# plane, and the rows here resolve that one instance. It takes no configuration,
|
|
179
|
+
# keys every fold by Session, and owns the context-meter projection units the
|
|
180
|
+
# browser reads for every session — behind a realm those units would come and go
|
|
181
|
+
# with whichever presets happen to be mounted. What a preset chooses is whether
|
|
182
|
+
# its agent compacts at all, which is `compaction-basic` below.
|
|
183
|
+
- id: compaction
|
|
184
|
+
name: cordis:group
|
|
185
|
+
group: true
|
|
186
|
+
isolate:
|
|
187
|
+
compaction: true
|
|
188
|
+
toolResultPruner: true
|
|
189
|
+
config:
|
|
190
|
+
- id: compaction-basic
|
|
191
|
+
name: '@deepseek-ai/dsh-compaction-basic'
|
|
192
|
+
|
|
193
|
+
- id: command-compact
|
|
194
|
+
name: '@deepseek-ai/dsh-command-compact'
|
|
195
|
+
|
|
196
|
+
- id: tool-result-pruner
|
|
197
|
+
name: '@deepseek-ai/dsh-compaction-tool-result-pruner'
|
|
198
|
+
config:
|
|
199
|
+
thresholdChars: 8192
|
|
200
|
+
headChars: 4096
|
|
201
|
+
tailChars: 1024
|
|
202
|
+
|
|
203
|
+
# ── delegation and workflows ────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
# The `subagents` registry and its spawn/fork backends live in the HOST
|
|
206
|
+
# composition: the registry is a process singleton whose cross-session queries
|
|
207
|
+
# the api-proxy serves to the browser, and a provider name may only be
|
|
208
|
+
# registered once. This preset contributes the delegation TOOLS, which resolve
|
|
209
|
+
# that host registry.
|
|
210
|
+
#
|
|
211
|
+
# `workflows` is different — nothing outside an agent reads it — so every row
|
|
212
|
+
# that reaches it shares one entry-local realm here, and a consumer left
|
|
213
|
+
# outside would resolve a host registry this preset does not populate.
|
|
214
|
+
#
|
|
215
|
+
# `tool-subagent-report` is host-plane for the same reason as the registry,
|
|
216
|
+
# not because a preset may not want it: it registers a CONTINUABLE SETUP on
|
|
217
|
+
# that singleton rather than a tool this agent calls, and the setup list is
|
|
218
|
+
# not scope-aware — one copy per mounted preset means every child gets
|
|
219
|
+
# `report` registered once per live session, which throws on the second.
|
|
220
|
+
- id: delegation
|
|
221
|
+
name: cordis:group
|
|
222
|
+
group: true
|
|
223
|
+
isolate:
|
|
224
|
+
workflowEngine: true
|
|
225
|
+
config:
|
|
226
|
+
- id: tool-subagent-control
|
|
227
|
+
name: '@deepseek-ai/dsh-tool-subagent-control'
|
|
228
|
+
|
|
229
|
+
- id: tool-subagent-list-agents
|
|
230
|
+
name: '@deepseek-ai/dsh-tool-subagent-control/list-agents'
|
|
231
|
+
|
|
232
|
+
# 0.3.4 对齐 dsh 0.1.6-alpha.2 内置 standard:spawn 行此前漏抄 modelSelectionSettings。
|
|
233
|
+
# 该键在 dsh-tool-subagent 里是 `z.boolean().default(false)`,漏掉既不报错也不改 schema,
|
|
234
|
+
# 只是子代理的「指定模型」入口被静默关掉 —— 最难发现的那类漂移。
|
|
235
|
+
# fork 行故意不加:provider/model 必须等于父代理,继承的历史才能继续命中 KV cache。
|
|
236
|
+
- id: tool-subagent
|
|
237
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
238
|
+
config:
|
|
239
|
+
provider: spawn
|
|
240
|
+
toolName: subagent
|
|
241
|
+
modelSelectionSettings: true
|
|
242
|
+
backgroundMode: continuable
|
|
243
|
+
|
|
244
|
+
- id: tool-subagent-fork
|
|
245
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
246
|
+
config:
|
|
247
|
+
provider: fork
|
|
248
|
+
toolName: subagent_fork
|
|
249
|
+
backgroundMode: continuable
|
|
250
|
+
|
|
251
|
+
# Production dsh does not install these optional providers. Install the
|
|
252
|
+
# matching Bundle in this Profile and restart the Host, then copy this
|
|
253
|
+
# preset and remove `disabled` from the matching tool row. Host availability
|
|
254
|
+
# alone grants no tool.
|
|
255
|
+
- id: tool-subagent-codex
|
|
256
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
257
|
+
disabled: true
|
|
258
|
+
config:
|
|
259
|
+
provider: codex
|
|
260
|
+
toolName: subagent_codex
|
|
261
|
+
backgroundMode: one-shot
|
|
262
|
+
maxDepth: provider-managed
|
|
263
|
+
|
|
264
|
+
- id: tool-subagent-claude-code
|
|
265
|
+
name: '@deepseek-ai/dsh-tool-subagent'
|
|
266
|
+
disabled: true
|
|
267
|
+
config:
|
|
268
|
+
provider: claude-code
|
|
269
|
+
toolName: subagent_claude_code
|
|
270
|
+
backgroundMode: one-shot
|
|
271
|
+
maxDepth: provider-managed
|
|
272
|
+
|
|
273
|
+
# dsh 0.1.6-alpha.1: `dsh-workflow-worker-thread` is gone from the release;
|
|
274
|
+
# the workflow engine is now `dsh-workflow-ptc` (sandboxed Node PTC runtime,
|
|
275
|
+
# host row `ptc-runtime` → `dsh-ptc-runtime-node`).
|
|
276
|
+
# dsh 0.1.7-alpha.1: 包名取不到的行由 `dsh-agent-preset-registry` 的 mountPreset →
|
|
277
|
+
# auditRows 判死,落在 record.broken 上 —— roster 里仍看得到「CC 模式」,但带着
|
|
278
|
+
# 诊断且组不出会话。失败形态变了,结论没变:包名必须与内置 standard 逐行对得上。
|
|
279
|
+
- id: workflow-ptc
|
|
280
|
+
name: '@deepseek-ai/dsh-workflow-ptc'
|
|
281
|
+
config:
|
|
282
|
+
provider: spawn
|
|
283
|
+
|
|
284
|
+
- id: tool-workflow
|
|
285
|
+
name: '@deepseek-ai/dsh-tool-workflow'
|
|
286
|
+
|
|
287
|
+
- id: tool-ralph
|
|
288
|
+
name: '@deepseek-ai/dsh-tool-ralph'
|
|
289
|
+
config:
|
|
290
|
+
subagentProvider: spawn
|
|
291
|
+
maxRounds: 64
|
|
292
|
+
|
|
293
|
+
# ── remaining model-facing rows ─────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
- id: tool-ask-user
|
|
296
|
+
name: '@deepseek-ai/dsh-tool-ask-user'
|
|
297
|
+
|
|
298
|
+
- id: tool-todo
|
|
299
|
+
name: '@deepseek-ai/dsh-tool-todo'
|
|
300
|
+
config:
|
|
301
|
+
allowParallelInProgress: true
|
|
302
|
+
|
|
303
|
+
# The `web` service and its search provider stay in the host composition; only
|
|
304
|
+
# the model-facing tool is per-session.
|
|
305
|
+
- id: tool-web
|
|
306
|
+
name: '@deepseek-ai/dsh-tool-web'
|
|
307
|
+
config:
|
|
308
|
+
fetch: false
|
|
309
|
+
searchTimeoutMs: 60000
|
|
310
|
+
|
|
311
|
+
# 0.3.4 补回:`present` 是 standard 一直有的行,本预设此前整行漏抄。
|
|
312
|
+
# 症状很隐蔽 —— CC 模式下模型没有「把成品登记为交付物」的工具,不报任何错,只是少了能力。
|
|
313
|
+
# 它和下面那行都必须与 dsh 内置 standard 逐行对得上(升级清单见 AGENTS.md §4)。
|
|
314
|
+
- id: present
|
|
315
|
+
name: '@deepseek-ai/dsh-tool-present'
|
|
316
|
+
|
|
317
|
+
# dsh 0.1.6-alpha.2 新增行:standard 里就是 disabled(默认关,需要的人自己放开)。
|
|
318
|
+
# 抄过来是为了让「本预设 vs 内置 standard」的行差以后只剩有意为之的那几处,
|
|
319
|
+
# 下次升级 diff 时不必再逐个判断「这行是新加的、还是我们漏抄了」。
|
|
320
|
+
# 保留 disabled 也安全:0.1.7 的注册表对 disabled 行既不 import 也不参与
|
|
321
|
+
# auditRows 行审计(dsh-agent-preset-registry lib/index.js:199)。
|
|
322
|
+
- id: tool-plugin-manager
|
|
323
|
+
name: '@deepseek-ai/dsh-plugin-manager/tools'
|
|
324
|
+
disabled: true
|
|
325
|
+
|
|
326
|
+
# ── CC 角色卡工坊(CC 模式专用) ───────────────────────────────────
|
|
327
|
+
- id: cc-agent
|
|
328
|
+
name: '@xia-sc/dsh-cc-studio/agent'
|