@xia-sc/dsh-cc-studio 0.4.1 → 0.7.1
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 +112 -297
- package/lib/agent.js +898 -159
- package/lib/client.js +22 -2
- package/lib/index.js +131 -24
- package/package.json +2 -2
- package/presets/cc/agent.cordis.yml +6 -4
- package/presets/cc.patch.yml +6 -4
package/lib/client.js
CHANGED
|
@@ -216,6 +216,7 @@ window.__ModuleLoader__.load({
|
|
|
216
216
|
"workshop.greeting.groupHint":"建议 ≥1 条",
|
|
217
217
|
"workshop.field.nickname":"nickname({{char}} 用)",
|
|
218
218
|
"workshop.field.tags":"tags(逗号分隔)",
|
|
219
|
+
"workshop.field.creator":"作者(creator)",
|
|
219
220
|
"workshop.field.mesExample":"mes_example({{user}}/{{char}})",
|
|
220
221
|
"workshop.edit.title":"{title} · 大框编辑",
|
|
221
222
|
"workshop.edit.fallbackTitle":"编辑",
|
|
@@ -428,6 +429,7 @@ window.__ModuleLoader__.load({
|
|
|
428
429
|
"workshop.greeting.groupHint":"≥1 recommended",
|
|
429
430
|
"workshop.field.nickname":"nickname (used by {{char}})",
|
|
430
431
|
"workshop.field.tags":"tags (comma separated)",
|
|
432
|
+
"workshop.field.creator":"creator / author",
|
|
431
433
|
"workshop.field.mesExample":"mes_example ({{user}}/{{char}})",
|
|
432
434
|
"workshop.edit.title":"{title} · Large editor",
|
|
433
435
|
"workshop.edit.fallbackTitle":"Edit",
|
|
@@ -838,7 +840,11 @@ window.__ModuleLoader__.load({
|
|
|
838
840
|
}catch(e){ set({lastMsg: String(e && e.message || e)}); }
|
|
839
841
|
set({libraryBusy:null});
|
|
840
842
|
}
|
|
841
|
-
function clearCurrentLibraryId(){
|
|
843
|
+
function clearCurrentLibraryId(){
|
|
844
|
+
set({currentLibraryId:null, lastMsg:t("store.switchedNew")});
|
|
845
|
+
// 同步清掉宿主侧的来源 id(sidecar):否则下次保存又会按旧来源「更新」那一张
|
|
846
|
+
try{ rpc("cc_setOrigin", { sessionId: state.currentSessionId||null, id: null }); }catch(e){}
|
|
847
|
+
}
|
|
842
848
|
// 清空当前角色卡:整份草稿换回空卡(makeDraft),并解除「跟已存角色的关联」——
|
|
843
849
|
// 否则清空后点「保存当前」会把库里那张卡覆盖成空卡(要删卡请用右侧列表的删除)。
|
|
844
850
|
// 落盘与其它写入走同一条路(pushDraftToHost):刷新页面、模型侧读到的都是空卡。
|
|
@@ -852,6 +858,8 @@ window.__ModuleLoader__.load({
|
|
|
852
858
|
currentLibraryId:null,
|
|
853
859
|
lastMsg: t("store.cleared")
|
|
854
860
|
});
|
|
861
|
+
// 清空 = 与已存卡脱钩:宿主侧的来源 id(sidecar)也要清,否则下次保存会「更新」原来那张卡(把它覆盖成空卡)
|
|
862
|
+
try{ rpc("cc_setOrigin", { sessionId: state.currentSessionId||null, id: null }); }catch(e){}
|
|
855
863
|
setTimeout(pushDraftToHost, 80);
|
|
856
864
|
}
|
|
857
865
|
async function loadFromLibrary(id){
|
|
@@ -1295,6 +1303,9 @@ window.__ModuleLoader__.load({
|
|
|
1295
1303
|
field(t("workshop.field.nickname"),"nickname",{rowKey:"nickname", input:true})
|
|
1296
1304
|
),
|
|
1297
1305
|
field(t("workshop.field.tags"),"tags",{rowKey:"tags", input:true, bigBox:false, value:s.draft.data.tags.join(", "), onChange:function(v){ store.updateDraft(function(d){ d.tags=v.split(",").map(function(x){return x.trim();}).filter(Boolean); }); }}),
|
|
1306
|
+
// N1(0.7.0):data.creator 此前**没有任何写入路径**(工具侧与工坊都改不了),
|
|
1307
|
+
// 而 cc_validate 每次都警告它为空。这里补上人工入口,工具侧对应 cc_patch_character 的 creator。
|
|
1308
|
+
field(t("workshop.field.creator"),"creator",{rowKey:"creator", input:true, bigBox:false}),
|
|
1298
1309
|
field("description","description",{rowKey:"desc"}),
|
|
1299
1310
|
field("personality","personality",{rowKey:"pers"}),
|
|
1300
1311
|
field("scenario","scenario",{rowKey:"scen"}),
|
|
@@ -1439,6 +1450,10 @@ window.__ModuleLoader__.load({
|
|
|
1439
1450
|
filteredLibrary.map(function(entry){
|
|
1440
1451
|
var isBusy = s.libraryBusy && String(s.libraryBusy).indexOf(entry.id)>=0;
|
|
1441
1452
|
var isCurrent = s.currentLibraryId===entry.id;
|
|
1453
|
+
// 列表展示前 4 个标签,但「还有几个没显示」必须用 meta.tagsTotal 算:meta.tags 本身截断到 6 个,
|
|
1454
|
+
// 拿它的长度当总数会偏小(卡里 7 个、列表只显示 6 个,用户会以为标签没存上 —— LLM 侧回测报的 P1-11)
|
|
1455
|
+
var tagTotal = (typeof entry.tagsTotal==="number") ? entry.tagsTotal : (entry.tags||[]).length;
|
|
1456
|
+
var tagShown = Math.min((entry.tags||[]).length, 4);
|
|
1442
1457
|
var itemStyle = isCurrent ? Object.assign({}, S.libraryItem, {borderColor:"#7c5cff", background:"color-mix(in srgb, #7c5cff 8%, var(--dsw-alias-bg-layer-2))"}) : S.libraryItem;
|
|
1443
1458
|
return h("div",{key:entry.id, style:itemStyle},
|
|
1444
1459
|
h("div",{style:{display:"flex", alignItems:"center", justifyContent:"space-between", gap:6}},
|
|
@@ -1449,7 +1464,7 @@ window.__ModuleLoader__.load({
|
|
|
1449
1464
|
h("div",{style:{fontSize:11, color:"var(--dsw-alias-label-secondary)", marginTop:2, whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis"}}, entry.descriptionSnippet || entry.tags.join(" · ") || "—"),
|
|
1450
1465
|
h("div",{style:{display:"flex", flexWrap:"wrap", gap:4, marginTop:6}},
|
|
1451
1466
|
(entry.tags||[]).slice(0,4).map(function(t){ return h("span",{key:t, style:Object.assign({}, S.badge, {fontSize:10, padding:"1px 5px"})}, t); }),
|
|
1452
|
-
|
|
1467
|
+
tagTotal > tagShown ? h("span",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)"}}, "+"+(tagTotal-tagShown)) : null
|
|
1453
1468
|
),
|
|
1454
1469
|
h("div",{style:{fontSize:10, color:"var(--dsw-alias-label-secondary)", marginTop:4}}, new Date(entry.updatedAt*1000).toLocaleString()),
|
|
1455
1470
|
h("div",{style:{display:"flex", gap:6, marginTop:8}},
|
|
@@ -1702,6 +1717,11 @@ window.__ModuleLoader__.load({
|
|
|
1702
1717
|
try{ if(localStorage.getItem("dsh-cc-studio-legacy-notice")==="1") alts = null; }catch(e){}
|
|
1703
1718
|
}
|
|
1704
1719
|
store.setAlternateSlots(alts);
|
|
1720
|
+
// 载入来源 id(0.5.0):宿主把「这份草稿是从哪张已存卡载入的」落在盘上(sidecar),
|
|
1721
|
+
// 刷新页面后据此恢复「已载入 ID」—— 否则内存里的 currentLibraryId 一丢,
|
|
1722
|
+
// 再点保存就会新建一张同名重复卡(LLM 侧回环测试抓到的 P0-4)。
|
|
1723
|
+
var originId = (typeof res.value.originId==="string" && res.value.originId) ? res.value.originId : null;
|
|
1724
|
+
if(originId !== store.getSnapshot().currentLibraryId) set({ currentLibraryId: originId });
|
|
1705
1725
|
var draft=res.value.draft;
|
|
1706
1726
|
var draftStatus=res.value.draftStatus||null;
|
|
1707
1727
|
try{
|
package/lib/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* CCv3 工坊:校验 + 共享草稿(供 CC 模式的 LLM Tools 与浏览器胶囊同步)+ 已存角色侧边栏落盘。
|
|
4
4
|
*/
|
|
5
5
|
import { mkdir, readdir, readFile, writeFile, unlink } from "node:fs/promises";
|
|
6
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { join } from "node:path";
|
|
@@ -97,6 +97,38 @@ function makeDefaultDraft(){
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 草稿形状规范化(0.7.1,B1,与 lib/agent.js 同构)。
|
|
102
|
+
*
|
|
103
|
+
* DSH 对工具/远端返回值做 lossless-JSON 校验:**任何嵌套 `undefined` 都会让整次调用报错**。
|
|
104
|
+
* 外部导入的卡(PNG / CHARX)或早期版本写下的草稿可能缺 `spec_version` / `creation_date` 这类字段,
|
|
105
|
+
* 前端直接读它就会渲染出 `undefined`、导出的卡也缺字段。所以读盘恢复时按空卡模板补齐缺失键:
|
|
106
|
+
* 只补 `undefined`,不覆盖已有值、不改类型(类型问题交给校验报)。
|
|
107
|
+
*/
|
|
108
|
+
function normalizeCardShape(draft){
|
|
109
|
+
const tpl = makeDefaultDraft();
|
|
110
|
+
if(!draft || typeof draft !== "object") return tpl;
|
|
111
|
+
const out = {
|
|
112
|
+
spec: (draft.spec !== undefined && draft.spec !== null) ? draft.spec : tpl.spec,
|
|
113
|
+
spec_version: (draft.spec_version !== undefined && draft.spec_version !== null) ? draft.spec_version : tpl.spec_version,
|
|
114
|
+
data: {},
|
|
115
|
+
};
|
|
116
|
+
const src = (draft.data && typeof draft.data === "object") ? draft.data : {};
|
|
117
|
+
for(const k of Object.keys(tpl.data)){
|
|
118
|
+
out.data[k] = (src[k] !== undefined) ? src[k] : tpl.data[k];
|
|
119
|
+
}
|
|
120
|
+
for(const k of Object.keys(src)){ if(!(k in out.data)) out.data[k] = src[k]; }
|
|
121
|
+
const book = (out.data.character_book && typeof out.data.character_book === "object") ? out.data.character_book : {};
|
|
122
|
+
const bTpl = tpl.data.character_book;
|
|
123
|
+
const nb = {};
|
|
124
|
+
for(const k of Object.keys(bTpl)){ nb[k] = (book[k] !== undefined) ? book[k] : bTpl[k]; }
|
|
125
|
+
for(const k of Object.keys(book)){ if(!(k in nb)) nb[k] = book[k]; }
|
|
126
|
+
if(!Array.isArray(nb.entries)) nb.entries = [];
|
|
127
|
+
if(!nb.extensions || typeof nb.extensions !== "object") nb.extensions = {};
|
|
128
|
+
out.data.character_book = nb;
|
|
129
|
+
if(!out.data.extensions || typeof out.data.extensions !== "object") out.data.extensions = {};
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
100
132
|
if(!globalThis.__CC_DRAFTS__) globalThis.__CC_DRAFTS__ = new Map();
|
|
101
133
|
// —— 落盘根(0.3.6 起统一):DSH_HOME 优先,homedir()/.dsh 兜底 ——
|
|
102
134
|
// 预设目录一直走这里;草稿与角色库在 0.3.5 及以前写死 homedir()/.dsh,现在统一到同一套根,
|
|
@@ -132,6 +164,27 @@ function safeDraftFile(key){
|
|
|
132
164
|
}
|
|
133
165
|
function draftPath(key){ return join(draftDir(), safeDraftFile(key)); }
|
|
134
166
|
/** 读盘用:主根没有就回退旧根(0.3.6 之前草稿写死的地方)。找不到返回 null。 */
|
|
167
|
+
// —— 草稿的「来源 id」sidecar(0.5.0,与 lib/agent.js 同构)——
|
|
168
|
+
// 记「这份草稿是从哪张已存卡载入的」,让「载入 → 改 → 保存」更新原卡而不是新建一张同名重复卡。
|
|
169
|
+
// 浏览器侧刷新页面会丢掉内存里的 currentLibraryId,所以这份关联必须落盘;存 sidecar 而不是塞进
|
|
170
|
+
// draft.extensions,是因为后者会跟着导出进用户的卡里。
|
|
171
|
+
function originFilePath(key){ return join(draftDir(), safeDraftFile(key).replace(/\.json$/, ".origin.json")); }
|
|
172
|
+
function readDraftOrigin(key){
|
|
173
|
+
try{
|
|
174
|
+
const p = originFilePath(key);
|
|
175
|
+
if(!existsSync(p)) return null;
|
|
176
|
+
const obj = JSON.parse(readFileSync(p, "utf8"));
|
|
177
|
+
if(obj && typeof obj.libraryId === "string" && obj.libraryId) return obj;
|
|
178
|
+
}catch{}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function writeDraftOrigin(key, libraryId, name){
|
|
182
|
+
try{
|
|
183
|
+
if(!libraryId){ try{ unlinkSync(originFilePath(key)); }catch{} return; }
|
|
184
|
+
mkdirSync(draftDir(), { recursive: true });
|
|
185
|
+
writeFileSync(originFilePath(key), JSON.stringify({ key: String(key), libraryId: String(libraryId), name: String(name||""), savedAt: nowSec() }, null, 2), "utf8");
|
|
186
|
+
}catch(e){ try{ console.error("[dsh-cc-studio] persist draft origin failed:", (e && e.message) || e); }catch{} }
|
|
187
|
+
}
|
|
135
188
|
function findDraftPath(key){
|
|
136
189
|
const file = safeDraftFile(key);
|
|
137
190
|
for(const dir of draftDirs()){
|
|
@@ -154,17 +207,36 @@ function loadPersistedDraftSync(key){
|
|
|
154
207
|
function persistDraftSync(key, draft){
|
|
155
208
|
try{
|
|
156
209
|
mkdirSync(draftDir(), { recursive: true });
|
|
157
|
-
|
|
158
|
-
|
|
210
|
+
// revision(0.6.0,P0-6):与 lib/agent.js 同构 —— 工具半与宿主半各写各的文件、各持各的内存,
|
|
211
|
+
// **只有草稿文件里的 rev 是共同真相**。工坊这边每写一次盘就 +1,工具侧下次调用读到新 rev 就知道
|
|
212
|
+
// 「草稿被外部换过了」,从而重载而不是拿过期前提去写(否则会静默覆盖用户在工坊里的修改)。
|
|
213
|
+
let rev = 1;
|
|
214
|
+
try{
|
|
215
|
+
const prev = JSON.parse(readFileSync(draftPath(key), "utf8"));
|
|
216
|
+
if(prev && typeof prev.rev === "number" && prev.rev > 0) rev = prev.rev + 1;
|
|
217
|
+
}catch{}
|
|
218
|
+
writeFileSync(draftPath(key), JSON.stringify({ key: String(key), updatedAt: nowSec(), rev, draft }, null, 2), "utf8");
|
|
219
|
+
return rev;
|
|
220
|
+
}catch(e){ try{ console.error("[dsh-cc-studio] persist draft failed:", (e && e.message) || e); }catch{} return null; }
|
|
159
221
|
}
|
|
160
222
|
function draftMeta(key){
|
|
161
223
|
const k = key || "default";
|
|
162
|
-
if(!globalThis.__CC_DRAFT_META__.has(k)) globalThis.__CC_DRAFT_META__.set(k, { isNew: false, recovered: false });
|
|
224
|
+
if(!globalThis.__CC_DRAFT_META__.has(k)) globalThis.__CC_DRAFT_META__.set(k, { isNew: false, recovered: false, newReason: null });
|
|
163
225
|
return globalThis.__CC_DRAFT_META__.get(k);
|
|
164
226
|
}
|
|
227
|
+
/** 卡片内容指纹(忽略 modification_date):判断「这次写入到底有没有真的改变内容」(N4,与工具半同构)。 */
|
|
228
|
+
function cardFingerprint(draft){
|
|
229
|
+
try{
|
|
230
|
+
const d = JSON.parse(JSON.stringify((draft && draft.data) ? draft.data : {}));
|
|
231
|
+
delete d.modification_date;
|
|
232
|
+
return JSON.stringify(d);
|
|
233
|
+
}catch{ return ""; }
|
|
234
|
+
}
|
|
165
235
|
function draftStatusFor(key, draft){
|
|
166
236
|
const m = draftMeta(key);
|
|
167
|
-
|
|
237
|
+
// B1(0.7.1):缺 `creation_date` 的外部导入卡不能再漏出 `undefined`(工具侧会因此整次调用失败,
|
|
238
|
+
// 前端也会把 undefined 渲染成字面量)。
|
|
239
|
+
return { isNew: !!m.isNew, recovered: !!m.recovered, creation_date: (draft && draft.data && draft.data.creation_date !== undefined) ? draft.data.creation_date : null, key: String(key) };
|
|
168
240
|
}
|
|
169
241
|
/**
|
|
170
242
|
* 解析「这次调用读写哪把草稿槽」,并把来源一并带出来(诊断用)。
|
|
@@ -261,20 +333,25 @@ function getDraftFor(key){
|
|
|
261
333
|
// fix #2:内存 miss 时先尝试从磁盘恢复,而非静默建空
|
|
262
334
|
const restored = loadPersistedDraftSync(k);
|
|
263
335
|
if(restored){
|
|
264
|
-
globalThis.__CC_DRAFTS__.set(k, restored);
|
|
265
|
-
const m = draftMeta(k); m.isNew = false; m.recovered = true;
|
|
336
|
+
globalThis.__CC_DRAFTS__.set(k, normalizeCardShape(restored)); // B1:补齐外部导入卡缺失的字段
|
|
337
|
+
const m = draftMeta(k); m.isNew = false; m.recovered = true; m.newReason = null;
|
|
266
338
|
} else {
|
|
267
339
|
globalThis.__CC_DRAFTS__.set(k, makeDefaultDraft());
|
|
268
|
-
const m = draftMeta(k); m.isNew = true; m.recovered = false;
|
|
340
|
+
const m = draftMeta(k); m.isNew = true; m.recovered = false; m.newReason = "welcome";
|
|
269
341
|
}
|
|
270
342
|
}
|
|
271
343
|
return globalThis.__CC_DRAFTS__.get(k);
|
|
272
344
|
}
|
|
273
345
|
function setDraftFor(key, draft){
|
|
274
|
-
draft.data.modification_date = nowSec();
|
|
275
346
|
const k = key || "default";
|
|
347
|
+
const prev = globalThis.__CC_DRAFTS__.get(k);
|
|
348
|
+
// N4(0.7.0,与 lib/agent.js 同构):内容一字未变时不写盘、不递增 rev。工坊侧每次编辑都调
|
|
349
|
+
// cc_setDraft,同值覆盖(失焦、重复输入、只动了别的槽)不该白耗一个版本号 —— 否则模型手上的
|
|
350
|
+
// rev 前提会被「其实没发生的变化」打掉一次,而它只能看到一句「草稿已被外部修改」。
|
|
351
|
+
if(prev && cardFingerprint(prev) === cardFingerprint(draft)) return draft;
|
|
352
|
+
draft.data.modification_date = nowSec();
|
|
276
353
|
globalThis.__CC_DRAFTS__.set(k, draft);
|
|
277
|
-
const m = draftMeta(k); m.isNew = false; m.recovered = false;
|
|
354
|
+
const m = draftMeta(k); m.isNew = false; m.recovered = false; m.newReason = null;
|
|
278
355
|
persistDraftSync(k, draft);
|
|
279
356
|
return draft;
|
|
280
357
|
}
|
|
@@ -524,12 +601,14 @@ function makeLibraryEntryMeta(draft, existingMeta, id){
|
|
|
524
601
|
const data = draft && draft.data ? draft.data : {};
|
|
525
602
|
const now = nowSec();
|
|
526
603
|
const loreCount = Array.isArray(data.character_book?.entries) ? data.character_book.entries.length : 0;
|
|
527
|
-
const
|
|
604
|
+
const allTags = Array.isArray(data.tags) ? data.tags.map(String) : [];
|
|
605
|
+
const tags = allTags.slice(0, 6);
|
|
528
606
|
return {
|
|
529
607
|
id: String(id),
|
|
530
608
|
name: (data.name && String(data.name).trim()) || (existingMeta && existingMeta.name) || "未命名角色",
|
|
531
609
|
loreCount,
|
|
532
610
|
tags,
|
|
611
|
+
tagsTotal: allTags.length,
|
|
533
612
|
descriptionSnippet: String(data.description || "").slice(0,80),
|
|
534
613
|
createdAt: existingMeta && existingMeta.createdAt ? existingMeta.createdAt : now,
|
|
535
614
|
updatedAt: now,
|
|
@@ -574,13 +653,15 @@ async function saveLibraryEntry(draft, opts){
|
|
|
574
653
|
id = String(Date.now())+"-"+Math.random().toString(36).slice(2,6);
|
|
575
654
|
}
|
|
576
655
|
const meta = makeLibraryEntryMeta(draft, existing ? existing.meta : null, id);
|
|
577
|
-
//
|
|
578
|
-
if(opts && typeof opts.name==="string" && opts.name.trim()){
|
|
579
|
-
|
|
656
|
+
// 改名 = 单一事实来源:meta.name 与 draft.data.name 一起改(否则「列表名 / 导出名」两套,用户改名看不出效果)
|
|
657
|
+
if(opts && typeof opts.name === "string" && opts.name.trim()){
|
|
658
|
+
const n = String(opts.name).trim().slice(0,60);
|
|
659
|
+
meta.name = n;
|
|
660
|
+
draft.data.name = n;
|
|
580
661
|
}
|
|
581
662
|
const payload = { id, meta, draft };
|
|
582
663
|
await writeFile(join(dir, id+".json"), JSON.stringify(payload, null, 2), "utf8");
|
|
583
|
-
return { id, meta, draft };
|
|
664
|
+
return { id, meta, draft, action: existing ? "updated" : "created" };
|
|
584
665
|
}
|
|
585
666
|
async function loadLibraryEntry(id){
|
|
586
667
|
await ensureLibDir();
|
|
@@ -611,6 +692,8 @@ async function renameLibraryEntry(id, newName){
|
|
|
611
692
|
if(!name) throw new Error("name 不能为空");
|
|
612
693
|
obj.meta.name = name;
|
|
613
694
|
obj.meta.updatedAt = nowSec();
|
|
695
|
+
// 单一事实来源(0.5.0):列表名与卡内 data.name 一起改 —— 此前只改 meta,侧边栏改了名、导出的卡里还是旧名
|
|
696
|
+
if(obj.draft && obj.draft.data) obj.draft.data.name = name;
|
|
614
697
|
await writeFile(found.path, JSON.stringify(obj, null, 2), "utf8");
|
|
615
698
|
return obj.meta;
|
|
616
699
|
}
|
|
@@ -871,7 +954,8 @@ function apply(ctx, config){
|
|
|
871
954
|
const k = parts.key;
|
|
872
955
|
const d = getDraftFor(k);
|
|
873
956
|
const status = draftStatusFor(k, d);
|
|
874
|
-
const
|
|
957
|
+
const origin = readDraftOrigin(k);
|
|
958
|
+
const payload = { draft: d, key: k, keySource: parts.source, isCc: isCcPreset(ctx, (parts.source==="arg"||parts.source==="arg-snapshot") ? k : null), draftStatus: status, originId: origin ? origin.libraryId : null };
|
|
875
959
|
// 只在「当前槽是个新空壳」时才去盘上翻别的槽(issue #5 的救回入口)。
|
|
876
960
|
// 而且只翻**这次错位真正会牵涉到的另一把槽**,不是把所有会话的槽都端出来:
|
|
877
961
|
// · 当前是会话槽(session-<id>)→ 候选只有 default —— 旧前端在没有会话 id 时
|
|
@@ -927,6 +1011,7 @@ function apply(ctx, config){
|
|
|
927
1011
|
if(patch.mes_example!==undefined) cur.data.mes_example = String(patch.mes_example).slice(0,4000);
|
|
928
1012
|
if(Array.isArray(patch.tags)) cur.data.tags = patch.tags.map(String).slice(0,12);
|
|
929
1013
|
if(patch.creator_notes!==undefined) cur.data.creator_notes = String(patch.creator_notes).slice(0,2000);
|
|
1014
|
+
if(patch.creator!==undefined) cur.data.creator = String(patch.creator).slice(0,120); // N1:作者名此前没有写入路径
|
|
930
1015
|
if(patch.world && typeof patch.world==="object"){
|
|
931
1016
|
cur.data.extensions = cur.data.extensions || {};
|
|
932
1017
|
cur.data.extensions.cc_world = Object.assign({}, cur.data.extensions.cc_world||{}, patch.world);
|
|
@@ -981,29 +1066,49 @@ function apply(ctx, config){
|
|
|
981
1066
|
}
|
|
982
1067
|
case "cc_saveToLibrary":{
|
|
983
1068
|
// args: {draft?, id?, name?, sessionId?} draft 缺省则取当前会话草稿
|
|
1069
|
+
const k = draftKeyFrom(ctx, args);
|
|
984
1070
|
let draft = args.draft;
|
|
985
1071
|
if(!draft || typeof draft!=="object"){
|
|
986
|
-
const k = draftKeyFrom(ctx, args);
|
|
987
1072
|
draft = JSON.parse(JSON.stringify(getDraftFor(k)));
|
|
988
1073
|
}
|
|
1074
|
+
// 不传 id 时沿用草稿的**来源 id**:工坊里「载入 → 改 → 保存」应当是更新原卡,
|
|
1075
|
+
// 而不是新建一张同名重复卡(刷新页面会丢掉内存里的 currentLibraryId,所以来源落在盘上)。
|
|
1076
|
+
const explicitId = (typeof args.id==="string" && args.id) ? args.id : "";
|
|
1077
|
+
const origin = explicitId ? null : readDraftOrigin(k);
|
|
1078
|
+
const targetId = explicitId || (origin ? origin.libraryId : "");
|
|
989
1079
|
// 允许保存未完全校验的草稿,但至少需要 name?不强制,meta 会兜底“未命名角色”
|
|
990
|
-
const res = await saveLibraryEntry(draft, { id:
|
|
991
|
-
|
|
1080
|
+
const res = await saveLibraryEntry(draft, { id: targetId || undefined, name: args.name });
|
|
1081
|
+
writeDraftOrigin(k, res.id, res.meta.name);
|
|
1082
|
+
return ok({ id: res.id, meta: res.meta, action: targetId ? "updated" : "created" });
|
|
992
1083
|
}
|
|
993
1084
|
case "cc_loadFromLibrary":{
|
|
994
1085
|
const id = args.id;
|
|
995
1086
|
if(typeof id!=="string" || !id) return fail("invalid-id","id 必填");
|
|
996
1087
|
const obj = await loadLibraryEntry(id);
|
|
997
|
-
//
|
|
1088
|
+
// 载入到当前会话草稿,并记下来源 id(sidecar):之后不传 id 保存就是更新这一张
|
|
998
1089
|
const k = draftKeyFrom(ctx, args);
|
|
999
1090
|
setDraftFor(k, JSON.parse(JSON.stringify(obj.draft)));
|
|
1000
|
-
|
|
1091
|
+
writeDraftOrigin(k, obj.id, obj.meta ? obj.meta.name : "");
|
|
1092
|
+
return ok({ draft: obj.draft, meta: obj.meta, key: k, originId: obj.id, draftStatus: draftStatusFor(k, obj.draft) });
|
|
1001
1093
|
}
|
|
1002
1094
|
case "cc_deleteFromLibrary":{
|
|
1003
1095
|
const id = args.id;
|
|
1004
1096
|
if(typeof id!=="string" || !id) return fail("invalid-id","id 必填");
|
|
1097
|
+
const k = draftKeyFrom(ctx, args);
|
|
1098
|
+
const origin = readDraftOrigin(k);
|
|
1099
|
+
const wasOrigin = !!(origin && origin.libraryId === id);
|
|
1005
1100
|
await deleteLibraryEntry(id);
|
|
1006
|
-
|
|
1101
|
+
// 删掉的正是当前草稿的来源卡 → 清掉关联,否则下次保存会「更新」一张已不存在的卡
|
|
1102
|
+
if(wasOrigin) writeDraftOrigin(k, null);
|
|
1103
|
+
const entries = await listLibraryEntries();
|
|
1104
|
+
return ok({ id, remaining: entries.length, originCleared: wasOrigin });
|
|
1105
|
+
}
|
|
1106
|
+
case "cc_setOrigin":{
|
|
1107
|
+
// 前端显式设/清「当前草稿的来源 id」:id 传 null/空 = 清(清空草稿、另存为新时用)
|
|
1108
|
+
const k = draftKeyFrom(ctx, args);
|
|
1109
|
+
const id = (typeof args.id === "string" && args.id) ? args.id : null;
|
|
1110
|
+
writeDraftOrigin(k, id, args.name);
|
|
1111
|
+
return ok({ key: k, originId: id });
|
|
1007
1112
|
}
|
|
1008
1113
|
case "cc_renameInLibrary":{
|
|
1009
1114
|
const id = args.id;
|
|
@@ -1011,7 +1116,7 @@ function apply(ctx, config){
|
|
|
1011
1116
|
if(typeof id!=="string" || !id) return fail("invalid-id","id 必填");
|
|
1012
1117
|
if(typeof newName!=="string" || !newName.trim()) return fail("invalid-name","name 必填");
|
|
1013
1118
|
const meta = await renameLibraryEntry(id, newName);
|
|
1014
|
-
return ok({ meta });
|
|
1119
|
+
return ok({ meta, syncedDraftName: true });
|
|
1015
1120
|
}
|
|
1016
1121
|
case "cc_getLibraryEntry":{
|
|
1017
1122
|
const id = args.id;
|
|
@@ -1137,7 +1242,9 @@ export { apply, inject, name, planPresetInstall, installCcPreset, supportsDeclar
|
|
|
1137
1242
|
draftKeyPartsFrom, draftContentSummary, listDraftSlotsOnDisk, migrateDraftDecision,
|
|
1138
1243
|
// 落盘根与 lib 层 CRUD(供回归测试直接断言「DSH_HOME 优先 + 读时回退」)
|
|
1139
1244
|
dshHomeDir, legacyDshHomeDir, dataRoots, draftDirs, draftDir, draftPath, findDraftPath, safeDraftFile,
|
|
1140
|
-
persistDraftSync, loadPersistedDraftSync, libDirs, libDir,
|
|
1245
|
+
persistDraftSync, loadPersistedDraftSync, libDirs, libDir, getDraftFor, setDraftFor, cardFingerprint, normalizeCardShape,
|
|
1246
|
+
// 草稿来源 id 的 sidecar(0.5.0):两半同构,测试直接交叉读写验证互通
|
|
1247
|
+
readDraftOrigin, writeDraftOrigin, originFilePath,
|
|
1141
1248
|
listLibraryEntries, saveLibraryEntry, loadLibraryEntry, deleteLibraryEntry, renameLibraryEntry };
|
|
1142
1249
|
|
|
1143
1250
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xia-sc/dsh-cc-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "CCv3 角色卡工坊:融合工坊 + CC 模式 + JSON/PNG/CHARX + 中英双语(跟随全局)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"presets"
|
|
39
39
|
],
|
|
40
40
|
"scripts": {
|
|
41
|
-
"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/nav-highlight.test.mjs && node tests/clear-card.test.mjs && node tests/export-entry.test.mjs && node tests/close-button.test.mjs && node tests/data-root.test.mjs",
|
|
41
|
+
"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/nav-highlight.test.mjs && node tests/clear-card.test.mjs && node tests/export-entry.test.mjs && node tests/close-button.test.mjs && node tests/tools-contract.test.mjs && node tests/data-root.test.mjs",
|
|
42
42
|
"prepublishOnly": "npm test"
|
|
43
43
|
},
|
|
44
44
|
"dsh": {
|
|
@@ -32,12 +32,14 @@
|
|
|
32
32
|
必须走完强制工作流,不可偷懒跳过 5 维世界观和世界书:
|
|
33
33
|
1) cc_get_card — 先看草稿,告诉用户还缺什么
|
|
34
34
|
2) cc_patch_character — 补 name/nickname/description/personality/scenario/tags(至少 description/personality/scenario 非空,tags 同步胶囊风格标签)
|
|
35
|
-
3) cc_patch_world — 补五维 timeline/factions/geo/power/daily,至少填 3 项,autoLorebook=true 自动生成带 @@decorator
|
|
36
|
-
4) cc_add_lorebook_entries — 再补到至少 5
|
|
35
|
+
3) cc_patch_world — 补五维 timeline/factions/geo/power/daily,至少填 3 项,autoLorebook=true 自动生成带 @@decorator 的基础条目;**同时给 loreKeys**(每一维自报用户真会说的实体名,如 {geo:["旧校舍","广播室","天台"]})—— 不给的话自动条目的 keys 只有维度名,真实对话永远命中不了
|
|
36
|
+
4) cc_add_lorebook_entries — 再补到至少 5 条,且**至少 1 条是你与用户共创的**(纯 autoLorebook 自动条目不算完成),覆盖年表/势力/地理/力量/日常各 1,至少 1 条 constant,content 头部写 @@position/@@depth/@@activate 等,支持 CBS 占位符(双花括号的 char / random / roll 形式);写完用 cc_simulate_trigger 拿一句用户会说的话验一下命中
|
|
37
37
|
5) cc_patch_greetings — 补 first_mes、alternate_greetings(≥2)、group_only_greetings(≥1 必填)、mes_example
|
|
38
|
-
6) cc_validate — 必须调且 valid=true 才算完成,否则按返回的 errors/warnings
|
|
38
|
+
6) cc_validate — 必须调且 valid=true 才算完成,否则按返回的 errors/warnings 继续补;通过后提醒用户在工坊左下角「校验导出」入口打开导出页选 JSON / PNG / CHARX,或点「★ 保存当前」落盘 ~/.dsh/cc-library
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
辅助工具(按需,不必每步都调):cc_patch_book_meta(世界书命名 / scan_depth / token_budget)、cc_lint_lorebook(世界书静态体检)、cc_simulate_trigger(验触发词命中)、cc_preview_export(导出前干跑)、cc_new_card(重置草稿,**必须先经用户同意**再带 confirm:true)、cc_update_lorebook_entry / cc_delete_lorebook_entries(改删单条)、已存库 cc_list_library / cc_save_to_library / cc_load_from_library / cc_rename_in_library / cc_delete_from_library / cc_get_library_entry(载入后直接保存会更新原卡,不会新建重复卡)。
|
|
41
|
+
|
|
42
|
+
规则:每次对话只追问 1-2 个薄弱点,问完立刻调工具写入;用户说“随便/跳过”也要你合理推断补全,不能留空;严禁跳过 3/4 步直接做问候语;每调一次工具要简要总结“已写入 X,下一步 Y”,并让胶囊实时同步;结束前必须 cc_validate。工具返回里只有一份 workflow.next(下一步该做什么)与 workflow.hint(进度摘要),照它走即可;用户还没回答就别抢跑。
|
|
41
43
|
|
|
42
44
|
- id: agent-instructions
|
|
43
45
|
name: '@deepseek-ai/dsh-agent-instructions'
|
package/presets/cc.patch.yml
CHANGED
|
@@ -67,12 +67,14 @@
|
|
|
67
67
|
必须走完强制工作流,不可偷懒跳过 5 维世界观和世界书:
|
|
68
68
|
1) cc_get_card — 先看草稿,告诉用户还缺什么
|
|
69
69
|
2) cc_patch_character — 补 name/nickname/description/personality/scenario/tags(至少 description/personality/scenario 非空,tags 同步胶囊风格标签)
|
|
70
|
-
3) cc_patch_world — 补五维 timeline/factions/geo/power/daily,至少填 3 项,autoLorebook=true 自动生成带 @@decorator
|
|
71
|
-
4) cc_add_lorebook_entries — 再补到至少 5
|
|
70
|
+
3) cc_patch_world — 补五维 timeline/factions/geo/power/daily,至少填 3 项,autoLorebook=true 自动生成带 @@decorator 的基础条目;**同时给 loreKeys**(每一维自报用户真会说的实体名,如 {geo:["旧校舍","广播室","天台"]})—— 不给的话自动条目的 keys 只有维度名,真实对话永远命中不了
|
|
71
|
+
4) cc_add_lorebook_entries — 再补到至少 5 条,且**至少 1 条是你与用户共创的**(纯 autoLorebook 自动条目不算完成),覆盖年表/势力/地理/力量/日常各 1,至少 1 条 constant,content 头部写 @@position/@@depth/@@activate 等,支持 CBS 占位符(双花括号的 char / random / roll 形式);写完用 cc_simulate_trigger 拿一句用户会说的话验一下命中
|
|
72
72
|
5) cc_patch_greetings — 补 first_mes、alternate_greetings(≥2)、group_only_greetings(≥1 必填)、mes_example
|
|
73
|
-
6) cc_validate — 必须调且 valid=true 才算完成,否则按返回的 errors/warnings
|
|
73
|
+
6) cc_validate — 必须调且 valid=true 才算完成,否则按返回的 errors/warnings 继续补;通过后提醒用户在工坊左下角「校验导出」入口打开导出页选 JSON / PNG / CHARX,或点「★ 保存当前」落盘 ~/.dsh/cc-library
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
辅助工具(按需,不必每步都调):cc_patch_book_meta(世界书命名 / scan_depth / token_budget)、cc_lint_lorebook(世界书静态体检)、cc_simulate_trigger(验触发词命中)、cc_preview_export(导出前干跑)、cc_new_card(重置草稿,**必须先经用户同意**再带 confirm:true)、cc_update_lorebook_entry / cc_delete_lorebook_entries(改删单条)、已存库 cc_list_library / cc_save_to_library / cc_load_from_library / cc_rename_in_library / cc_delete_from_library / cc_get_library_entry(载入后直接保存会更新原卡,不会新建重复卡)。
|
|
76
|
+
|
|
77
|
+
规则:每次对话只追问 1-2 个薄弱点,问完立刻调工具写入;用户说“随便/跳过”也要你合理推断补全,不能留空;严禁跳过 3/4 步直接做问候语;每调一次工具要简要总结“已写入 X,下一步 Y”,并让胶囊实时同步;结束前必须 cc_validate。工具返回里只有一份 workflow.next(下一步该做什么)与 workflow.hint(进度摘要),照它走即可;用户还没回答就别抢跑。
|
|
76
78
|
|
|
77
79
|
- id: agent-instructions
|
|
78
80
|
name: '@deepseek-ai/dsh-agent-instructions'
|