@xia-sc/dsh-cc-studio 0.3.4 → 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 +48 -20
- package/lib/agent.js +80 -37
- package/lib/client.js +153 -23
- package/lib/index.js +258 -64
- 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,22 +256,26 @@ 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
|
-
│ │ # cc_validateDraft, 已存库 cc_*Library, 容器 cc_importFromPng/cc_exportPng(+imageB64)/
|
|
262
|
+
│ │ # cc_validateDraft, 草稿槽 cc_migrateDraft, 已存库 cc_*Library, 容器 cc_importFromPng/cc_exportPng(+imageB64)/
|
|
241
263
|
│ │ # cc_importFromCharx/cc_exportCharx, CRC32/ZIP/STORE&DEFLATE)
|
|
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,8 @@ 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 项。
|
|
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`)。
|
|
283
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 项预设行契约守卫。
|
|
284
312
|
- `0.3.3` 发布流程自动化:打 `v*` tag 即由 GitHub Actions 发布到 npm(OIDC trusted publishing,零密钥)并自动建 GitHub Release;手动触发默认只做安全自检。插件运行时行为未变。
|
|
285
313
|
- `0.3.2` 包名迁移至 `@xia-sc/dsh-cc-studio`(原 `@dsh-plugins` 不是本项目持有的 npm scope),并首次发布到 npm;新增 `publishConfig.access` / `prepublishOnly` 等发布元数据。
|
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 };
|