@xia-sc/dsh-cc-studio 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -12
- package/lib/agent.js +217 -47
- package/lib/client.js +289 -51
- package/lib/draft-lock.js +98 -0
- package/lib/group-layer.js +4 -4
- package/lib/index.js +218 -9
- package/package.json +2 -2
- package/presets/cc/agent.cordis.yml +12 -4
- package/presets/cc.patch.yml +12 -4
package/lib/agent.js
CHANGED
|
@@ -14,15 +14,46 @@ import { REL_TYPES, REL_TYPE_LABELS, REL_BEGIN, REL_END,
|
|
|
14
14
|
WORLD_DIMS, AUTO_NAMES, isAutoEntry, looksLikeLegacyAuto,
|
|
15
15
|
estimateTokens, lorebookTokens, contentKeyOf, scanSharedEntries,
|
|
16
16
|
assembleScope, groupMetrics, lorebookMetrics, metricsBrief, auditLoreEntries, runGroupChecks } from "./group-layer.js";
|
|
17
|
+
// 草稿锁的判定与常量(0.11.0):两半共用 lib/draft-lock.js —— 「锁算不算 active / 超时算不算过期」只有一份实现。
|
|
18
|
+
// 与 group-layer.js 同一个教训:两半各写一份判定必然漂移,一边说锁住了、另一边照写,比没有锁更危险。
|
|
19
|
+
// 这里只 import 工具半真正用到的:上锁时写 owner、读回时归一、返回时给摘要。
|
|
20
|
+
import { LOCK_OWNER_LLM, normalizeLock, lockSummary } from "./draft-lock.js";
|
|
17
21
|
const name = "dsh-cc-studio-agent";
|
|
18
22
|
// 版本标识(0.7.1):每次工具返回都带 `build`。
|
|
19
23
|
// 由来:第五轮实测里测试者第一遍跑出「四项全无变化」,其实只是**没重启 dsh** —— LLM 侧完全无法
|
|
20
24
|
// 分辨「代码没改」和「改了但没加载」。有了这个字段,任何一次调用都能先确认自己测的是哪一版。
|
|
21
25
|
// 发版时与 package.json 的 version 同步。
|
|
22
|
-
const BUILD = "0.
|
|
26
|
+
const BUILD = "0.11.0";
|
|
23
27
|
const inject = ["tools", "agents"];
|
|
24
28
|
|
|
25
29
|
function nowSec(){ return Math.floor(Date.now()/1000); }
|
|
30
|
+
|
|
31
|
+
// ── 字面转义解码(0.10.3,用户实机报告 P1)──────────────────────────────────────────
|
|
32
|
+
// 模型在 tool call 的 JSON 里经常把换行写成**字面** `\n`(JSON 层多转义一层:`"A\\nB"`),
|
|
33
|
+
// JSON.parse 之后字符串里就是「反斜杠 + n」两个字符。以前这些工具原样落盘,导出的卡在
|
|
34
|
+
// SillyTavern 里就显示「说完这句话。\n\n下一段」——同一张卡里有的字段正常、有的不正常
|
|
35
|
+
// (模型这次写对了),肉眼极难发现(用户实测 18 次调用里 6 次中招)。
|
|
36
|
+
// 卡内文本没有「字面反斜杠 n」的正当用途,所以写类工具统一把字面 `\n` / `\r` / `\t` 解成
|
|
37
|
+
// 真字符;多层转义(`A\\\\nB`)逐层收敛(最多 4 轮)。只作用于**模型输入的工具参数**,
|
|
38
|
+
// 不动工坊保存与导入路径(那两条链路本来就是真换行)。
|
|
39
|
+
function decodeEscapedText(v){
|
|
40
|
+
if(typeof v !== "string" || v.indexOf("\\") < 0) return v;
|
|
41
|
+
var s = v;
|
|
42
|
+
for(var round = 0; round < 4; round++){
|
|
43
|
+
var next = s
|
|
44
|
+
.replace(/\\\\/g, "\u0000") // 成对反斜杠先占位,避免被下面的 \n 规则重复匹配
|
|
45
|
+
.replace(/\\n/g, "\n")
|
|
46
|
+
.replace(/\\r/g, "\r")
|
|
47
|
+
.replace(/\\t/g, "\t")
|
|
48
|
+
.replace(/\u0000/g, "\\");
|
|
49
|
+
if(next === s) break;
|
|
50
|
+
s = next;
|
|
51
|
+
}
|
|
52
|
+
return s;
|
|
53
|
+
}
|
|
54
|
+
/** 工具参数里的文本字段:先解码字面转义,再按旧规则 String()。 */
|
|
55
|
+
function textArg(v){ return decodeEscapedText(String(v == null ? "" : v)); }
|
|
56
|
+
|
|
26
57
|
function draftsMap(){ if(!globalThis.__CC_DRAFTS__) globalThis.__CC_DRAFTS__ = new Map(); return globalThis.__CC_DRAFTS__; }
|
|
27
58
|
// —— 落盘根(0.3.6 起统一,与 lib/index.js 是同一套):DSH_HOME 优先,homedir()/.dsh 兜底 ——
|
|
28
59
|
// 草稿与角色库都必须与宿主半算出**同一个路径**,否则又会退化成 issue #5 那种「两边互相看不见」。
|
|
@@ -119,6 +150,66 @@ function writeOriginSync(key, libraryId, name){
|
|
|
119
150
|
writeFileSync(originPath(key), JSON.stringify({ key: String(key), libraryId: String(libraryId), name: String(name||""), savedAt: nowSec() }, null, 2), "utf8");
|
|
120
151
|
}catch(e){ try{ console.error("[dsh-cc-studio] persist draft origin failed:", (e && e.message) || e); }catch{} }
|
|
121
152
|
}
|
|
153
|
+
// —— 草稿锁的 IO(0.11.0)——
|
|
154
|
+
// 背景:LLM 正在工坊草稿里创作 A 卡时,用户点「载入」B 卡会把同一会话槽的草稿整份替换,
|
|
155
|
+
// LLM 已写入但未保存的内容只能从历史对话里捞。现在工具半**每次写草稿**都给这把槽上一把创作锁,
|
|
156
|
+
// 用户侧(宿主 RPC)看到 active 锁就只读 / 进预览;交还由 LLM 调 `cc_release_draft` 完成,
|
|
157
|
+
// 另有 TTL 兜底(防 LLM 崩溃后把工坊永久锁死)。
|
|
158
|
+
//
|
|
159
|
+
// 判定(active / stale / 能不能写)全在 lib/draft-lock.js,两半共用 —— 这里只负责落盘,
|
|
160
|
+
// 与 .origin.json 同模式:<safeDraftFile 去 .json>.lock.json。**不写进草稿 JSON**:
|
|
161
|
+
// 用户只读与 LLM 交还都不重写草稿,草稿的 `rev` 仍然只反映内容变化。
|
|
162
|
+
function draftLockPath(key){ return join(draftDir(), safeDraftFile(key).replace(/\.json$/, ".lock.json")); }
|
|
163
|
+
/** 读锁:主根没有就回退旧根(与草稿/来源 sidecar 同一套规则)。损坏 / key 不符一律当「无锁」(见 normalizeLock)。 */
|
|
164
|
+
function readDraftLockSync(key){
|
|
165
|
+
const file = safeDraftFile(key).replace(/\.json$/, ".lock.json");
|
|
166
|
+
for(const dir of draftDirs()){
|
|
167
|
+
const p = join(dir, file);
|
|
168
|
+
try{
|
|
169
|
+
if(!existsSync(p)) continue;
|
|
170
|
+
return normalizeLock(JSON.parse(readFileSync(p, "utf8")), String(key));
|
|
171
|
+
}catch(e){}
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
/** 写锁:失败只 console.error 不抛 —— 锁是保护用户数据的附加层,写锁失败不能反过来弄坏一次正常的工具调用。 */
|
|
176
|
+
function writeDraftLockSync(key, lock){
|
|
177
|
+
try{
|
|
178
|
+
mkdirSync(draftDir(), { recursive: true });
|
|
179
|
+
writeFileSync(draftLockPath(key), JSON.stringify(lock, null, 2), "utf8");
|
|
180
|
+
return true;
|
|
181
|
+
}catch(e){ try{ console.error("[dsh-cc-studio] persist draft lock failed:", (e && e.message) || e); }catch{} return false; }
|
|
182
|
+
}
|
|
183
|
+
/** 清锁:两个根都删 —— 旧根若残留同一把锁,回退读会把它当成本槽的锁,LLM 的「交还」就不算数了。 */
|
|
184
|
+
function clearDraftLockSync(key){
|
|
185
|
+
const file = safeDraftFile(key).replace(/\.json$/, ".lock.json");
|
|
186
|
+
for(const dir of draftDirs()){
|
|
187
|
+
try{ unlinkSync(join(dir, file)); }catch(e){}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function draftLockSummaryFor(key){ return lockSummary(readDraftLockSync(key), nowSec()); }
|
|
191
|
+
/**
|
|
192
|
+
* 上锁 / 续期:LLM 每次写草稿(含「内容无变化」的空写)都调这里。
|
|
193
|
+
* `since` 只在第一次上锁时记下来 —— 它回答「从什么时候开始创作」,每次写都重置就没有意义了。
|
|
194
|
+
* `rev` 取本次落盘后的版本(空写时取内存里记着 / 盘上实际那个),**绝不留 undefined**
|
|
195
|
+
* (B1 的 lossless-JSON 教训:一个 undefined 就让整次工具调用报错)。
|
|
196
|
+
*/
|
|
197
|
+
function refreshDraftLock(key, draft, rev){
|
|
198
|
+
const k = String(key);
|
|
199
|
+
const prev = readDraftLockSync(k);
|
|
200
|
+
const now = nowSec();
|
|
201
|
+
const cardName = (draft && draft.data && String(draft.data.name || "").trim()) || "";
|
|
202
|
+
const lock = {
|
|
203
|
+
key: k,
|
|
204
|
+
owner: LOCK_OWNER_LLM,
|
|
205
|
+
since: (prev && prev.since > 0) ? prev.since : now,
|
|
206
|
+
updatedAt: now,
|
|
207
|
+
rev: (typeof rev === "number" && rev > 0) ? rev : ((prev && prev.rev) || 0),
|
|
208
|
+
cardName: cardName.slice(0, 120),
|
|
209
|
+
};
|
|
210
|
+
writeDraftLockSync(k, lock);
|
|
211
|
+
return lock;
|
|
212
|
+
}
|
|
122
213
|
function draftMeta(key){
|
|
123
214
|
const k = String(key || "default");
|
|
124
215
|
if(!globalThis.__CC_DRAFT_META__.has(k)) globalThis.__CC_DRAFT_META__.set(k, { isNew: false, recovered: false, rev: null, pendingFresh: null });
|
|
@@ -144,6 +235,10 @@ function draftStatusFor(key, draft){
|
|
|
144
235
|
cardId: (origin && origin.libraryId) || null,
|
|
145
236
|
cardName: (draft && draft.data && String(draft.data.name||"").trim()) || "",
|
|
146
237
|
rev: (typeof m.rev === "number") ? m.rev : null,
|
|
238
|
+
// 草稿锁摘要(0.11.0):active=true 表示 LLM 正在创作,工坊只读 / 载入只预览。
|
|
239
|
+
// 无锁时**显式 null**(不是省略)—— 客户端与测试都按「有值 = 摘要、null = 无锁」判断,
|
|
240
|
+
// 而 undefined 还会踩 lossless-JSON(B1)。
|
|
241
|
+
lock: draftLockSummaryFor(k),
|
|
147
242
|
// watchedRev 由 tool() 包装在调用前捕获后注入:它 = 「本次调用开始前工具半记着的盘上版本」。
|
|
148
243
|
watchedRev: null,
|
|
149
244
|
};
|
|
@@ -219,7 +314,7 @@ function makeEmptyCard(){
|
|
|
219
314
|
first_mes:"", alternate_greetings:[], group_only_greetings:[], mes_example:"",
|
|
220
315
|
creator_notes:"", creator_notes_multilingual:{}, source:[], assets:[{type:"icon",uri:"ccdefault:",name:"main",ext:"png"}],
|
|
221
316
|
creation_date: nowSec(), modification_date: nowSec(),
|
|
222
|
-
character_book:{ name:"", description:"", scan_depth:4, token_budget:
|
|
317
|
+
character_book:{ name:"", description:"", scan_depth:4, token_budget:2400, recursive_scanning:false, extensions:{}, entries:[] }
|
|
223
318
|
}
|
|
224
319
|
};
|
|
225
320
|
}
|
|
@@ -310,17 +405,31 @@ function getDraft(ctx, exec){
|
|
|
310
405
|
function setDraft(ctx, draft, exec){
|
|
311
406
|
const m = draftsMap();
|
|
312
407
|
const k = keyFor(ctx, exec);
|
|
408
|
+
const meta = draftMeta(k);
|
|
313
409
|
const prev = m.get(k);
|
|
314
410
|
// N4(0.7.0):**内容一字未变时不落盘、不递增 rev**。空改动(传了不生效的参数、同值覆盖、
|
|
315
411
|
// 删 0 条条目…)原来也会把 rev 推进一格,于是持有旧 rev 的调用方会无辜失败一次
|
|
316
412
|
// (第四轮实测:`changed: []` 让 rev 7 → 8)。modification_date 也要跟着不动,否则指纹永远不等。
|
|
317
|
-
if(prev && cardFingerprint(prev) === cardFingerprint(draft))
|
|
413
|
+
if(prev && cardFingerprint(prev) === cardFingerprint(draft)){
|
|
414
|
+
// 但「内容没变」不代表 LLM 停手了:锁必须**续期**。updatedAt 是「还在创作」的心跳,
|
|
415
|
+
// 不是「内容变了」的记号 —— 否则 LLM 连着几轮只讨论不改稿,锁会提前超时,
|
|
416
|
+
// 用户又能趁它还在创作时载入别的卡。rev 优先用内存里记着的(上次自己写下的),
|
|
417
|
+
// 再退盘上实际那个,绝不留 undefined(B1)。
|
|
418
|
+
const idleRev = (typeof meta.rev === "number" && meta.rev > 0) ? meta.rev : (draftRevOnDisk(k) || 0);
|
|
419
|
+
refreshDraftLock(k, draft, idleRev);
|
|
420
|
+
return draft;
|
|
421
|
+
}
|
|
318
422
|
draft.data.modification_date = nowSec();
|
|
319
423
|
m.set(k, draft);
|
|
320
424
|
// pendingFresh 刻意不在这里清:新草稿提示由 tool() 包装在**调用之后**统一发,这里清掉就发不出来了。
|
|
321
|
-
|
|
425
|
+
meta.isNew = false; meta.recovered = false;
|
|
322
426
|
const rev = persistDraftSync(k, draft);
|
|
323
|
-
if(typeof rev === "number")
|
|
427
|
+
if(typeof rev === "number"){
|
|
428
|
+
meta.rev = rev; // 自己写的这一次不算「外部修改」
|
|
429
|
+
// 写盘成功后上锁 / 续期(0.11.0):从这一刻起,用户侧对这份草稿的写入一律被拦,
|
|
430
|
+
// 载入/导入只进只读预览 —— 直到 LLM 自己调 cc_release_draft 或锁超时。
|
|
431
|
+
refreshDraftLock(k, draft, rev);
|
|
432
|
+
}
|
|
324
433
|
return draft;
|
|
325
434
|
}
|
|
326
435
|
|
|
@@ -601,7 +710,7 @@ async function libraryCardsFor(ids){
|
|
|
601
710
|
name: String(d.name || ""),
|
|
602
711
|
description: String(d.description || ""),
|
|
603
712
|
constantTokens: entries.filter(e => e && e.constant).reduce((n, e) => n + estimateTokensLite(e && e.content), 0),
|
|
604
|
-
tokenBudget: Number(d.character_book && d.character_book.token_budget) ||
|
|
713
|
+
tokenBudget: Number(d.character_book && d.character_book.token_budget) || 2400,
|
|
605
714
|
});
|
|
606
715
|
}
|
|
607
716
|
return out;
|
|
@@ -989,7 +1098,7 @@ function progressHint(step, state, worldFilled, loreCount, manualLoreCount, done
|
|
|
989
1098
|
+ " " + mark(state.world) + "五维(" + worldFilled + "/5)"
|
|
990
1099
|
+ " " + mark(state.lore) + "世界书(" + loreCount + (sharedLoreCount ? "+" + sharedLoreCount : "") + (manualLoreCount > 0 ? ",共创" + manualLoreCount : "") + ")"
|
|
991
1100
|
+ " " + mark(state.greetings) + "问候语"
|
|
992
|
-
+ (done ? " ·
|
|
1101
|
+
+ (done ? " · 已完成,可导出/保存;记得调用 cc_release_draft 交还编辑权" : "");
|
|
993
1102
|
}
|
|
994
1103
|
// —— 世界书口径与体检的实现都在 lib/group-layer.js(0.10.0)——
|
|
995
1104
|
// estimateTokens / lorebookTokens / lorebookMetrics / auditLoreEntries / worstCaseTrigger 等
|
|
@@ -1018,6 +1127,10 @@ function apply(ctx){
|
|
|
1018
1127
|
const meta = draftMeta(k);
|
|
1019
1128
|
const expected = (args && typeof args.expectedRev === "number") ? args.expectedRev : null;
|
|
1020
1129
|
const beforeRev = draftRevOnDisk(k);
|
|
1130
|
+
// 0.11.0 流程引导:记下本次调用前有没有锁。第一次建立锁时给模型一条明确提示 ——
|
|
1131
|
+
// 只靠 cc_validate 终态的 next 提醒,弱模型经常「写完直接总结」而不调 cc_release_draft,
|
|
1132
|
+
// 工坊就一直只读。让它第一次写草稿时就知道「完成时必须交还」。
|
|
1133
|
+
const lockBefore = readDraftLockSync(k);
|
|
1021
1134
|
// S1 补充(0.7.1):把「本次调用开始前工具半记着的盘上版本」捕下来,返回里一并报出 ——
|
|
1022
1135
|
// 模型可以随时对账:`watchedRev ≠ rev` 就说明这两次调用之间有别人写过。第五轮实测里,
|
|
1023
1136
|
// 测试者想验证「不传 expectedRev 也能发现漂移」,却在工具序列内造不出漂移(自己写的 rev
|
|
@@ -1066,6 +1179,17 @@ function apply(ctx){
|
|
|
1066
1179
|
res.warnings = (Array.isArray(res.warnings) ? res.warnings : []).concat([note]);
|
|
1067
1180
|
}
|
|
1068
1181
|
}
|
|
1182
|
+
// 首次上锁的流程引导(见函数开头 lockBefore 的注释):只在「无锁 → 有锁」这一次出现,
|
|
1183
|
+
// 之后每次写只是续期,不再刷屏。
|
|
1184
|
+
if(!lockBefore){
|
|
1185
|
+
const lockAfter = readDraftLockSync(k);
|
|
1186
|
+
if(lockAfter){
|
|
1187
|
+
const lockNote = "🔒 已为当前草稿建立创作锁(工坊在此期间只读、用户载入只进预览)。创作完成时(cc_validate 通过后)**必须调用 cc_release_draft 交还草稿编辑权**,否则用户无法在工坊微调;你继续写会自动续期,不会重复锁。";
|
|
1188
|
+
if(!(Array.isArray(res.warnings) && res.warnings.indexOf(lockNote) >= 0)){
|
|
1189
|
+
res.warnings = (Array.isArray(res.warnings) ? res.warnings : []).concat([lockNote]);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1069
1193
|
}
|
|
1070
1194
|
}catch(e){}
|
|
1071
1195
|
// B1 的第二道防线:无论各工具返回什么,出门前统一过一遍 JSON 往返,绝不把 `undefined`
|
|
@@ -1104,7 +1228,7 @@ function apply(ctx){
|
|
|
1104
1228
|
|
|
1105
1229
|
tool({
|
|
1106
1230
|
name: "cc_patch_character",
|
|
1107
|
-
description: "【工作流第2步·共创】写入角色基础(name/nickname/description/personality/scenario/system_prompt/tags)。调用前必须已与用户讨论:至少问 1 个开放问题(如想要的气质/性别/年龄/与你的关系张力/禁忌),拿到用户偏好后再填;禁止仅凭世界观自行推断一次性填满。必须在 cc_get_card 之后、五维之前调用,之后下一步是 cc_patch_world
|
|
1231
|
+
description: "【工作流第2步·共创】写入角色基础(name/nickname/description/personality/scenario/system_prompt/tags)。调用前必须已与用户讨论:至少问 1 个开放问题(如想要的气质/性别/年龄/与你的关系张力/禁忌),拿到用户偏好后再填;禁止仅凭世界观自行推断一次性填满。必须在 cc_get_card 之后、五维之前调用,之后下一步是 cc_patch_world。一次只传 1-3 个字段(长字段分多次调用写);tags 等数组字段单独一次调用。参数必须是严格合法的 JSON(最后一个字段/数组元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号;字符串内换行要转义)。",
|
|
1108
1232
|
parameters: {
|
|
1109
1233
|
type:"object",
|
|
1110
1234
|
properties:{
|
|
@@ -1135,16 +1259,16 @@ function apply(ctx){
|
|
|
1135
1259
|
const changed = [];
|
|
1136
1260
|
// B2(0.7.1):`changed` 与 `rev` 用同一套判据 —— 只有值真的变了才记改动,否则会出现
|
|
1137
1261
|
// 「返回说改了 creator、rev 却纹丝不动」这种自相矛盾(第五轮实测 B2)。
|
|
1138
|
-
if(args.name!==undefined) assignIfChanged(cur.data, "name",
|
|
1139
|
-
if(args.nickname!==undefined) assignIfChanged(cur.data, "nickname",
|
|
1140
|
-
if(args.description!==undefined) assignIfChanged(cur.data, "description",
|
|
1141
|
-
if(args.personality!==undefined) assignIfChanged(cur.data, "personality",
|
|
1142
|
-
if(args.scenario!==undefined) assignIfChanged(cur.data, "scenario",
|
|
1143
|
-
if(args.system_prompt!==undefined) assignIfChanged(cur.data, "system_prompt",
|
|
1144
|
-
if(args.post_history_instructions!==undefined) assignIfChanged(cur.data, "post_history_instructions",
|
|
1145
|
-
if(Array.isArray(args.tags)) assignIfChanged(cur.data, "tags", args.tags.map(
|
|
1146
|
-
if(args.creator_notes!==undefined) assignIfChanged(cur.data, "creator_notes",
|
|
1147
|
-
if(args.creator!==undefined) assignIfChanged(cur.data, "creator",
|
|
1262
|
+
if(args.name!==undefined) assignIfChanged(cur.data, "name", textArg(args.name).slice(0,120), changed);
|
|
1263
|
+
if(args.nickname!==undefined) assignIfChanged(cur.data, "nickname", textArg(args.nickname).slice(0,120), changed);
|
|
1264
|
+
if(args.description!==undefined) assignIfChanged(cur.data, "description", textArg(args.description).slice(0,5000), changed);
|
|
1265
|
+
if(args.personality!==undefined) assignIfChanged(cur.data, "personality", textArg(args.personality).slice(0,5000), changed);
|
|
1266
|
+
if(args.scenario!==undefined) assignIfChanged(cur.data, "scenario", textArg(args.scenario).slice(0,5000), changed);
|
|
1267
|
+
if(args.system_prompt!==undefined) assignIfChanged(cur.data, "system_prompt", textArg(args.system_prompt).slice(0,3000), changed);
|
|
1268
|
+
if(args.post_history_instructions!==undefined) assignIfChanged(cur.data, "post_history_instructions", textArg(args.post_history_instructions).slice(0,3000), changed);
|
|
1269
|
+
if(Array.isArray(args.tags)) assignIfChanged(cur.data, "tags", args.tags.map(textArg).slice(0,12), changed);
|
|
1270
|
+
if(args.creator_notes!==undefined) assignIfChanged(cur.data, "creator_notes", textArg(args.creator_notes).slice(0,3000), changed);
|
|
1271
|
+
if(args.creator!==undefined) assignIfChanged(cur.data, "creator", textArg(args.creator).slice(0,120), changed);
|
|
1148
1272
|
setDraft(ctx, cur, exec);
|
|
1149
1273
|
const wf = await workflowFor(ctx, exec, cur);
|
|
1150
1274
|
const warnings = [];
|
|
@@ -1161,7 +1285,7 @@ function apply(ctx){
|
|
|
1161
1285
|
|
|
1162
1286
|
tool({
|
|
1163
1287
|
name: "cc_patch_world",
|
|
1164
|
-
description: "【工作流第3步·共创·必调】写入 5 维世界观(timeline/factions/geo/power/daily 至少 3 项)。若用户已提供世界观文档,先用 2-3 句归纳你理解的 5 维,再问用户“哪几维最想展开?有无私设要加?”;若世界观薄弱,则与用户讨论 1-2 个薄弱点后再填。严禁未与用户讨论就直接照搬文档填满 5 维。⚠️ 若本卡已在**有共享世界书**的分组里,本工具默认**不再**往卡内抄五维骨架(那些共同设定属于共享书,卡内副本会与它双重插入)—— 要卡内副本才显式传 autoLorebook:true。调后下一步是 cc_add_lorebook_entries。",
|
|
1288
|
+
description: "【工作流第3步·共创·必调】写入 5 维世界观(timeline/factions/geo/power/daily 至少 3 项)。若用户已提供世界观文档,先用 2-3 句归纳你理解的 5 维,再问用户“哪几维最想展开?有无私设要加?”;若世界观薄弱,则与用户讨论 1-2 个薄弱点后再填。严禁未与用户讨论就直接照搬文档填满 5 维。⚠️ 若本卡已在**有共享世界书**的分组里,本工具默认**不再**往卡内抄五维骨架(那些共同设定属于共享书,卡内副本会与它双重插入)—— 要卡内副本才显式传 autoLorebook:true。调后下一步是 cc_add_lorebook_entries。loreKeys 这类对象字段建议单独一次调用;参数必须是严格合法的 JSON(最后一个字段/数组元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号)。",
|
|
1165
1289
|
parameters: {
|
|
1166
1290
|
type:"object",
|
|
1167
1291
|
properties:{
|
|
@@ -1188,11 +1312,11 @@ function apply(ctx){
|
|
|
1188
1312
|
const recNote = recoveredNotice(k, cur);
|
|
1189
1313
|
cur.data.extensions = cur.data.extensions || {};
|
|
1190
1314
|
const w = cur.data.extensions.cc_world || {};
|
|
1191
|
-
if(args.timeline!==undefined) w.timeline =
|
|
1192
|
-
if(args.factions!==undefined) w.factions =
|
|
1193
|
-
if(args.geo!==undefined) w.geo =
|
|
1194
|
-
if(args.power!==undefined) w.power =
|
|
1195
|
-
if(args.daily!==undefined) w.daily =
|
|
1315
|
+
if(args.timeline!==undefined) w.timeline = textArg(args.timeline).slice(0,1000);
|
|
1316
|
+
if(args.factions!==undefined) w.factions = textArg(args.factions).slice(0,1000);
|
|
1317
|
+
if(args.geo!==undefined) w.geo = textArg(args.geo).slice(0,1000);
|
|
1318
|
+
if(args.power!==undefined) w.power = textArg(args.power).slice(0,1000);
|
|
1319
|
+
if(args.daily!==undefined) w.daily = textArg(args.daily).slice(0,1000);
|
|
1196
1320
|
cur.data.extensions.cc_world = w;
|
|
1197
1321
|
// 0.10.0:默认值必须看得见「组里已经有什么」——
|
|
1198
1322
|
// 没显式传 autoLorebook 时,若本卡所属分组的共享书已覆盖这些维度,就**不在卡内生成副本**:
|
|
@@ -1247,7 +1371,7 @@ function apply(ctx){
|
|
|
1247
1371
|
if(w.timeline) push("timeline", "@@position after_desc\n"+w.timeline, 10);
|
|
1248
1372
|
if(w.factions) push("factions", "@@depth 4\n"+w.factions, 9);
|
|
1249
1373
|
if(w.geo) push("geo", "@@position scenario\n"+w.geo, 8);
|
|
1250
|
-
if(w.power) push("power", "@@activate\n"+w.power, 9, true);
|
|
1374
|
+
if(w.power) push("power", "@@activate 100\n"+w.power, 9, true);
|
|
1251
1375
|
if(w.daily) push("daily", "@@depth 5\n"+w.daily, 7);
|
|
1252
1376
|
}
|
|
1253
1377
|
setDraft(ctx, cur, exec);
|
|
@@ -1309,7 +1433,7 @@ function apply(ctx){
|
|
|
1309
1433
|
|
|
1310
1434
|
tool({
|
|
1311
1435
|
name: "cc_add_lorebook_entries",
|
|
1312
|
-
description: "【工作流第4步·共创·必调】添加 1-5 条 Lorebook,补到至少 5 条(年表/势力/地理/力量/日常各 1,至少 1 条 constant,@@decorator
|
|
1436
|
+
description: "【工作流第4步·共创·必调】添加 1-5 条 Lorebook,补到至少 5 条(年表/势力/地理/力量/日常各 1,至少 1 条 constant,@@decorator)。调用前应与用户讨论:问用户想要哪些触发词/常驻条目/要藏什么彩蛋,拿到偏好后再生成。必须在五维之后、问候语之前调用,禁止一次性无讨论批量生成。条目较多时分成多次调用(一次 1-2 条);参数必须是严格合法的 JSON(数组元素之间不写尾逗号,最后一个元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号)。",
|
|
1313
1437
|
parameters: {
|
|
1314
1438
|
type:"object",
|
|
1315
1439
|
properties:{
|
|
@@ -1341,14 +1465,14 @@ function apply(ctx){
|
|
|
1341
1465
|
const recNote = recoveredNotice(k, cur);
|
|
1342
1466
|
args.entries.forEach(function(e){
|
|
1343
1467
|
cur.data.character_book.entries.push({
|
|
1344
|
-
keys: Array.isArray(e.keys)? e.keys.map(
|
|
1345
|
-
content:
|
|
1468
|
+
keys: Array.isArray(e.keys)? e.keys.map(textArg): [],
|
|
1469
|
+
content: textArg(e.content).slice(0,5000),
|
|
1346
1470
|
enabled:true,
|
|
1347
1471
|
insertion_order: cur.data.character_book.entries.length,
|
|
1348
1472
|
case_sensitive:false,
|
|
1349
1473
|
use_regex: !!e.use_regex,
|
|
1350
1474
|
constant: !!e.constant,
|
|
1351
|
-
name:
|
|
1475
|
+
name: textArg(e.name||"未命名").slice(0,60),
|
|
1352
1476
|
priority: typeof e.priority==="number"? e.priority: 5,
|
|
1353
1477
|
id: Date.now()+Math.floor(Math.random()*10000)
|
|
1354
1478
|
});
|
|
@@ -1430,9 +1554,9 @@ function apply(ctx){
|
|
|
1430
1554
|
const e = cur.data.character_book.entries.find(x=> x.id===args.id);
|
|
1431
1555
|
if(!e) return { ok:false, error:"未找到 id="+args.id };
|
|
1432
1556
|
const changed = [];
|
|
1433
|
-
if(args.name!==undefined) assignIfChanged(e, "name",
|
|
1434
|
-
if(Array.isArray(args.keys)) assignIfChanged(e, "keys", args.keys.map(
|
|
1435
|
-
if(args.content!==undefined) assignIfChanged(e, "content",
|
|
1557
|
+
if(args.name!==undefined) assignIfChanged(e, "name", textArg(args.name).slice(0,60), changed);
|
|
1558
|
+
if(Array.isArray(args.keys)) assignIfChanged(e, "keys", args.keys.map(textArg), changed);
|
|
1559
|
+
if(args.content!==undefined) assignIfChanged(e, "content", textArg(args.content).slice(0,5000), changed);
|
|
1436
1560
|
if(args.enabled!==undefined) assignIfChanged(e, "enabled", !!args.enabled, changed);
|
|
1437
1561
|
if(args.constant!==undefined) assignIfChanged(e, "constant", !!args.constant, changed);
|
|
1438
1562
|
if(args.use_regex!==undefined) assignIfChanged(e, "use_regex", !!args.use_regex, changed);
|
|
@@ -1451,7 +1575,7 @@ function apply(ctx){
|
|
|
1451
1575
|
|
|
1452
1576
|
tool({
|
|
1453
1577
|
name: "cc_patch_greetings",
|
|
1454
|
-
description: "【工作流第5步·共创】写入开场与示例(first_mes / alternate_greetings≥2 / group_only_greetings≥1 / mes_example)。调用前必须问用户偏好的开场场景/关系阶段/群聊与私聊区分,拿到偏好后再写;禁止未与用户讨论就自行编造全部问候语。必须在角色+五维+世界书(≥5)
|
|
1578
|
+
description: "【工作流第5步·共创】写入开场与示例(first_mes / alternate_greetings≥2 / group_only_greetings≥1 / mes_example)。调用前必须问用户偏好的开场场景/关系阶段/群聊与私聊区分,拿到偏好后再写;禁止未与用户讨论就自行编造全部问候语。必须在角色+五维+世界书(≥5)后调用。一次只传 1-2 个字段(长问候语分多次调用写);alternate_greetings / group_only_greetings 这类数组字段单独一次调用。参数必须是严格合法的 JSON(最后一个字段/数组元素后不写逗号,不要 ```json 代码块包裹,不要注释与单引号)。",
|
|
1455
1579
|
parameters: {
|
|
1456
1580
|
type:"object",
|
|
1457
1581
|
properties:{
|
|
@@ -1469,10 +1593,10 @@ function apply(ctx){
|
|
|
1469
1593
|
const k = keyFor(ctx, exec);
|
|
1470
1594
|
const cur = JSON.parse(JSON.stringify(getDraft(ctx, exec)));
|
|
1471
1595
|
const recNote = recoveredNotice(k, cur);
|
|
1472
|
-
if(args.first_mes!==undefined) cur.data.first_mes =
|
|
1473
|
-
if(Array.isArray(args.alternate_greetings)) cur.data.alternate_greetings = args.alternate_greetings.map(
|
|
1474
|
-
if(Array.isArray(args.group_only_greetings)) cur.data.group_only_greetings = args.group_only_greetings.map(
|
|
1475
|
-
if(args.mes_example!==undefined) cur.data.mes_example =
|
|
1596
|
+
if(args.first_mes!==undefined) cur.data.first_mes = textArg(args.first_mes).slice(0,5000);
|
|
1597
|
+
if(Array.isArray(args.alternate_greetings)) cur.data.alternate_greetings = args.alternate_greetings.map(textArg).slice(0,10);
|
|
1598
|
+
if(Array.isArray(args.group_only_greetings)) cur.data.group_only_greetings = args.group_only_greetings.map(textArg).slice(0,10);
|
|
1599
|
+
if(args.mes_example!==undefined) cur.data.mes_example = textArg(args.mes_example).slice(0,5000);
|
|
1476
1600
|
setDraft(ctx, cur, exec);
|
|
1477
1601
|
const wf = await workflowFor(ctx, exec, cur);
|
|
1478
1602
|
// 若世界书/五维未齐,警告但仍写入,让 validate 卡住
|
|
@@ -1625,7 +1749,7 @@ function apply(ctx){
|
|
|
1625
1749
|
// 【世界书元信息】此前 name/description/scan_depth/token_budget/recursive_scanning 一个都改不了
|
|
1626
1750
|
tool({
|
|
1627
1751
|
name: "cc_patch_book_meta",
|
|
1628
|
-
description: "【世界书·元信息】改 character_book 的顶层参数:name / description / scan_depth / token_budget / recursive_scanning
|
|
1752
|
+
description: "【世界书·元信息】改 character_book 的顶层参数:name / description / scan_depth / token_budget / recursive_scanning。这些字段此前没有任何工具能改,导致导出的世界书永远是无名书、预算也调不了。合并式更新,只改传入的字段。参数必须是严格合法的 JSON(不要尾逗号、不要注释、不要单引号)。",
|
|
1629
1753
|
parameters: {
|
|
1630
1754
|
type:"object",
|
|
1631
1755
|
properties:{
|
|
@@ -1645,8 +1769,8 @@ function apply(ctx){
|
|
|
1645
1769
|
cur.data.character_book = cur.data.character_book || {};
|
|
1646
1770
|
const book = cur.data.character_book;
|
|
1647
1771
|
const changed = [];
|
|
1648
|
-
if(args.name!==undefined) assignIfChanged(book, "name",
|
|
1649
|
-
if(args.description!==undefined) assignIfChanged(book, "description",
|
|
1772
|
+
if(args.name!==undefined) assignIfChanged(book, "name", textArg(args.name).slice(0,120), changed);
|
|
1773
|
+
if(args.description!==undefined) assignIfChanged(book, "description", textArg(args.description).slice(0,500), changed);
|
|
1650
1774
|
if(typeof args.scan_depth==="number") assignIfChanged(book, "scan_depth", Math.max(1, Math.min(50, Math.round(args.scan_depth))), changed);
|
|
1651
1775
|
if(typeof args.token_budget==="number") assignIfChanged(book, "token_budget", Math.max(0, Math.round(args.token_budget)), changed);
|
|
1652
1776
|
if(args.recursive_scanning!==undefined) assignIfChanged(book, "recursive_scanning", !!args.recursive_scanning, changed);
|
|
@@ -2438,7 +2562,7 @@ function apply(ctx){
|
|
|
2438
2562
|
|
|
2439
2563
|
tool({
|
|
2440
2564
|
name: "cc_validate",
|
|
2441
|
-
description: "【工作流第6步·收口必调】校验 CC 角色卡(SPEC_V3 + 角色四件套/五维≥3/世界书≥5/问候语齐)。valid=false 按 errors
|
|
2565
|
+
description: "【工作流第6步·收口必调】校验 CC 角色卡(SPEC_V3 + 角色四件套/五维≥3/世界书≥5/问候语齐)。valid=false 按 errors 与用户讨论继续补;**valid=true 即创作完成,必须立刻调用 cc_release_draft 交还草稿编辑权**(不交还的话工坊一直只读,用户无法微调),然后邀请用户在工坊微调后“导出 JSON”或“★ 保存到侧边栏”。全程强调共创:多问用户、多给草案供挑选。",
|
|
2442
2566
|
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
2443
2567
|
output: out({ type:"object", additionalProperties:true }),
|
|
2444
2568
|
async execute(args, exec){
|
|
@@ -2486,14 +2610,19 @@ function apply(ctx){
|
|
|
2486
2610
|
if(budget>0 && used > budget) warnings.push("世界书内容估算约 "+used+" token,已超出 token_budget="+budget+",建议调大预算或精简条目(cc_patch_book_meta)");
|
|
2487
2611
|
// —— 终态:校验通过后 next 不再指向 cc_validate(P0-3:那正是弱模型死循环的诱因)——
|
|
2488
2612
|
const valid = errors.length===0;
|
|
2613
|
+
// 0.11.0:终态要主动提醒交还草稿锁 —— 不交还时工坊一直是只读的,用户只能预览、无法微调,
|
|
2614
|
+
// 而「校验通过」正是「创作告一段落」的唯一信号。保留「校验导出」「保存当前」两个既有指引入口
|
|
2615
|
+
//(工坊左下角入口名与 0.4.1 的 UI 一致;tools-contract ②i 依赖这两处字样)。
|
|
2489
2616
|
const next = valid
|
|
2490
|
-
? "校验通过 ✓ 工作流已完成(step 7/7
|
|
2617
|
+
? "校验通过 ✓ 工作流已完成(step 7/7)。**下一步必须调用 cc_release_draft 交还草稿编辑权**(否则工坊保持只读,用户只能在预览里看、无法微调);交还后引导用户在工坊左下角「校验导出」入口打开导出页选 JSON / PNG / CHARX,或点「★ 保存当前」落盘到 ~/.dsh/cc-library。还想打磨就直接说改哪里(你继续写会自动重新上锁)。"
|
|
2491
2618
|
: "校验未通过,按 errors 逐项补齐(严禁跳步),第一项:" + errors[0];
|
|
2492
2619
|
// R1(0.6.0):终态的 `hint` 必须与 `next` 同源 —— 否则会出现「next 说工作流已完成、hint 说下一步必须去校验」
|
|
2493
2620
|
// 这种自相矛盾(弱模型会读信息最全的 hint,于是反复调 cc_validate)。半修的状态机比没修更危险。
|
|
2494
2621
|
const wfDone = Object.assign({}, wf, {
|
|
2495
2622
|
step: valid ? 7 : wf.step,
|
|
2496
2623
|
done: valid,
|
|
2624
|
+
// 模型可见的显式收口信号:valid=true 时 releaseRequired=true,提示它这一步还没做完。
|
|
2625
|
+
releaseRequired: valid,
|
|
2497
2626
|
readyToValidate: !valid && wf.readyToValidate,
|
|
2498
2627
|
next,
|
|
2499
2628
|
hint: progressHint(valid ? 7 : wf.step, wf.state, wf.worldFilled, wf.loreCount, wf.manualLoreCount, valid, wf.sharedLoreCount),
|
|
@@ -2517,10 +2646,40 @@ function apply(ctx){
|
|
|
2517
2646
|
message: valid
|
|
2518
2647
|
? "校验通过 ✓ 工作流完成(世界书 " + wf.loreTotal + " 条"
|
|
2519
2648
|
+ (wf.sharedLoreCount ? " = 卡内 " + wf.loreCount + " + 共享书 " + wf.sharedLoreCount : "")
|
|
2520
|
-
+ " / 估算 " + (used + sharedUsed) + " token
|
|
2649
|
+
+ " / 估算 " + (used + sharedUsed) + " token)。下一步:调用 cc_release_draft 交还草稿锁,用户才能在工坊微调/导出。"
|
|
2521
2650
|
: "校验未通过:" + errors.length + " 项待补。" };
|
|
2522
2651
|
}
|
|
2523
2652
|
});
|
|
2653
|
+
|
|
2654
|
+
// 【交还草稿锁】0.11.0:权限完全由 LLM 掌握 —— 它说创作完了、或用户明确要求强制释放时才放开。
|
|
2655
|
+
// 为什么不做成自动释放:工具半无法判断「这一轮对话是否真的结束」,误放一次,
|
|
2656
|
+
// 用户接着载入别的卡就会把 LLM 还没保存的内容覆盖掉 —— 正是 0.11.0 要修的那个事故。
|
|
2657
|
+
tool({
|
|
2658
|
+
name: "cc_release_draft",
|
|
2659
|
+
description: "【收口·交还草稿】**cc_validate 通过(valid=true)= 创作完成,必须立刻调用本工具**,否则工坊一直只读、用户无法微调(这是工作流的最后一步)。释放当前会话的草稿创作锁,把工坊还给用户编辑。调用前先向用户说明当前卡名与进度(释放后用户就能在工坊里直接改这张卡)。用户明确要求强制释放时也调用;释放后若你再次写草稿,会自动重新锁定;当前没有锁时调用也安全(幂等成功,released:false)。",
|
|
2660
|
+
parameters: { type:"object", properties:{}, additionalProperties:false },
|
|
2661
|
+
output: out({ type:"object", additionalProperties:true }),
|
|
2662
|
+
async execute(args, exec){
|
|
2663
|
+
const k = keyFor(ctx, exec);
|
|
2664
|
+
const lock = readDraftLockSync(k);
|
|
2665
|
+
const st = draftStatusFor(k, getDraft(ctx, exec));
|
|
2666
|
+
const released = !!lock;
|
|
2667
|
+
clearDraftLockSync(k);
|
|
2668
|
+
// 卡名优先用锁里记下的那个:锁可能是草稿还叫旧名时建的,交还时要说清「交还的是哪张卡」。
|
|
2669
|
+
const cardName = (lock && String(lock.cardName || "").trim()) || String(st.cardName || "").trim();
|
|
2670
|
+
const rev = (typeof st.rev === "number" && st.rev > 0) ? st.rev : ((lock && lock.rev) || null);
|
|
2671
|
+
return {
|
|
2672
|
+
ok: true,
|
|
2673
|
+
released,
|
|
2674
|
+
sessionKey: k,
|
|
2675
|
+
cardName: cardName || null,
|
|
2676
|
+
rev,
|
|
2677
|
+
message: released
|
|
2678
|
+
? "已释放草稿锁「" + (cardName || "未命名") + "」:工坊恢复可编辑,用户现在可以载入/导入别的卡,不会覆盖你的草稿。若你继续写草稿,会自动重新锁定。"
|
|
2679
|
+
: "当前没有草稿锁(可能已释放过,或已超时自动失效),无需重复释放。如果你继续写草稿,会自动重新锁定。",
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2524
2683
|
}
|
|
2525
2684
|
|
|
2526
2685
|
// ——————————————————————————————————————————————————————————————
|
|
@@ -2704,8 +2863,8 @@ async function patchSharedBook(groupId, args){
|
|
|
2704
2863
|
error: "共享世界书已被外部修改(期望 rev=" + args.expectedBookRev + ",实际 rev=" + bookRevOf(cur) + ")—— 先 cc_get_shared_book 重新读一遍再改。" };
|
|
2705
2864
|
}
|
|
2706
2865
|
const applied = [], rejected = [];
|
|
2707
|
-
if(args.name !== undefined) cur.data.name =
|
|
2708
|
-
if(args.description !== undefined) cur.data.description =
|
|
2866
|
+
if(args.name !== undefined) cur.data.name = textArg(args.name).slice(0, 80);
|
|
2867
|
+
if(args.description !== undefined) cur.data.description = textArg(args.description).slice(0, 300);
|
|
2709
2868
|
if(args.scan_depth !== undefined) cur.data.scan_depth = Math.max(1, Math.round(Number(args.scan_depth) || 4));
|
|
2710
2869
|
if(args.token_budget !== undefined) cur.data.token_budget = Math.max(100, Math.round(Number(args.token_budget) || 2000));
|
|
2711
2870
|
if(args.recursive_scanning !== undefined) cur.data.recursive_scanning = args.recursive_scanning === true;
|
|
@@ -2713,7 +2872,12 @@ async function patchSharedBook(groupId, args){
|
|
|
2713
2872
|
(Array.isArray(args.ops) ? args.ops : []).forEach(op => {
|
|
2714
2873
|
const kind = String((op && op.op) || "");
|
|
2715
2874
|
if(kind === "addEntry"){
|
|
2716
|
-
|
|
2875
|
+
// 0.10.3:模型可能把换行写成字面 \n —— 共享书条目也是卡内文本,先解码再归一化。
|
|
2876
|
+
const rawEntry = Object.assign({}, op.entry);
|
|
2877
|
+
if(rawEntry.content !== undefined) rawEntry.content = textArg(rawEntry.content);
|
|
2878
|
+
if(rawEntry.name !== undefined) rawEntry.name = textArg(rawEntry.name);
|
|
2879
|
+
if(Array.isArray(rawEntry.keys)) rawEntry.keys = rawEntry.keys.map(textArg);
|
|
2880
|
+
const e = makeBookEntry(rawEntry);
|
|
2717
2881
|
if(!e.content.trim()){ rejected.push({ op, error: "content 不能为空" }); return; }
|
|
2718
2882
|
if(cur.data.entries.some(x => contentKeyOf(x) === contentKeyOf(e))){ rejected.push({ op, error: "同内容的条目已存在" }); return; }
|
|
2719
2883
|
if(cur.data.entries.length >= BOOK_MAX_ENTRIES){ rejected.push({ op, error: "条目数已达上限 " + BOOK_MAX_ENTRIES }); return; }
|
|
@@ -2722,7 +2886,11 @@ async function patchSharedBook(groupId, args){
|
|
|
2722
2886
|
} else if(kind === "updateEntry"){
|
|
2723
2887
|
const idx = cur.data.entries.findIndex(x => contentKeyOf(x) === String(op.contentKey || ""));
|
|
2724
2888
|
if(idx < 0){ rejected.push({ op, error: "找不到条目(contentKey 不匹配)" }); return; }
|
|
2725
|
-
|
|
2889
|
+
const rawPatch = Object.assign({}, (op.patch && typeof op.patch === "object") ? op.patch : {});
|
|
2890
|
+
if(rawPatch.content !== undefined) rawPatch.content = textArg(rawPatch.content);
|
|
2891
|
+
if(rawPatch.name !== undefined) rawPatch.name = textArg(rawPatch.name);
|
|
2892
|
+
if(Array.isArray(rawPatch.keys)) rawPatch.keys = rawPatch.keys.map(textArg);
|
|
2893
|
+
cur.data.entries[idx] = makeBookEntry(Object.assign({}, cur.data.entries[idx], rawPatch));
|
|
2726
2894
|
applied.push({ op: kind, key: contentKeyOf(cur.data.entries[idx]) });
|
|
2727
2895
|
} else if(kind === "removeEntry"){
|
|
2728
2896
|
// 支持两种定位:`index`(工坊列表直接用它)或 `contentKey`(模型读到的签名)
|
|
@@ -2945,6 +3113,8 @@ async function sharedBookCoversDims(ctx, exec){
|
|
|
2945
3113
|
}
|
|
2946
3114
|
|
|
2947
3115
|
export { apply, inject, name, safeDraftFile, draftDirs, libDirs, dataRoots, originPath, readOriginSync, writeOriginSync,
|
|
3116
|
+
// 草稿锁(0.11.0):与宿主半同构的落盘与摘要 —— 测试直接比对两半路径与行为
|
|
3117
|
+
draftLockPath, readDraftLockSync, writeDraftLockSync, clearDraftLockSync, draftLockSummaryFor,
|
|
2948
3118
|
// 卡组层(0.10.0):卡库快照 + 卡组作用域 —— 卡组数据的唯一入口,两半共用 lib/group-layer.js 的纯组装
|
|
2949
3119
|
readLibrarySnapshot, groupScope,
|
|
2950
3120
|
// 人物关系图谱(0.8.0):导出纯函数与落盘根,供 tests/relations.test.mjs 与宿主半逐字比对同构实现
|