dsh-novel-write-engine 0.2.6 → 0.2.8
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/CHANGELOG.md +58 -5
- package/README.md +411 -982
- package/briefs/P2.md +1 -1
- package/briefs/P8.md +4 -4
- package/lib/client.js +15 -10
- package/lib/index.d.ts +69 -3
- package/lib/index.js +284 -69
- package/lib/types/client/dto.d.ts +20 -0
- package/lib/types/client/viewmodel.d.ts +2 -0
- package/lib/types/types.d.ts +37 -1
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -308,6 +308,33 @@ function allocateIds(prefix, nextSeq, count) {
|
|
|
308
308
|
nextSeq: nextSeq + count
|
|
309
309
|
};
|
|
310
310
|
}
|
|
311
|
+
// src/domain/narrator.ts
|
|
312
|
+
/**
|
|
313
|
+
* One narrator-alias contract shared by PG-05, state_delta and lock capture.
|
|
314
|
+
*
|
|
315
|
+
* The plan format is `角色名(第一人称)` or `角色名(第一人称「我」)`. A
|
|
316
|
+
* first-person manuscript says 「我」 instead of repeating the character name;
|
|
317
|
+
* every consumer must accept that alias, and none may infer it from a bare
|
|
318
|
+
* substring such as "林" inside "林砚".
|
|
319
|
+
*/
|
|
320
|
+
const narratorName = (narrator) => narrator.split(/[((]/, 1)[0].trim();
|
|
321
|
+
/** The only documented first-person alias is the pronoun itself. */
|
|
322
|
+
const narratorAliases = (narrator) => isFirstPersonNarrator(narrator) ? ["我"] : [];
|
|
323
|
+
/** Whether the plan declares first-person narration. */
|
|
324
|
+
const isFirstPersonNarrator = (narrator) => narrator.includes("第一人称") && !narrator.includes("第三人称");
|
|
325
|
+
/**
|
|
326
|
+
* True when `name` is the declared narrator and the manuscript uses the
|
|
327
|
+
* declared first-person alias. A name that merely contains the narrator name
|
|
328
|
+
* is not an alias.
|
|
329
|
+
*/
|
|
330
|
+
const isNarratorAlias = (name, narrator, text) => {
|
|
331
|
+
if (text.includes(name)) return true;
|
|
332
|
+
if (narratorName(narrator) !== name) return false;
|
|
333
|
+
if (!isFirstPersonNarrator(narrator)) return false;
|
|
334
|
+
return narratorAliases(narrator).some((alias) => text.includes(alias));
|
|
335
|
+
};
|
|
336
|
+
/** Paragraph used for a newly locked character; prefers the named/aliased one. */
|
|
337
|
+
const narratorAliasesFor = (name, narrator) => name === narratorName(narrator) ? narratorAliases(narrator) : [];
|
|
311
338
|
// src/domain/scoring.ts
|
|
312
339
|
/** The fifteen dimensions, grouped as the commercial weights demand. */
|
|
313
340
|
const SCORE_DIMENSIONS = [
|
|
@@ -2528,7 +2555,13 @@ var FsStore = class {
|
|
|
2528
2555
|
/** Absolute path of an internal file outside the project root (locks live at root). */
|
|
2529
2556
|
async ensureDir(relativePath) {
|
|
2530
2557
|
const absolute = await this.safePath(relativePath);
|
|
2531
|
-
await
|
|
2558
|
+
await this.ensureParent(absolute);
|
|
2559
|
+
try {
|
|
2560
|
+
await promises.mkdir(absolute);
|
|
2561
|
+
} catch (error) {
|
|
2562
|
+
if (error.code !== "EEXIST") throw ioFailure(error);
|
|
2563
|
+
if (!(await promises.lstat(absolute)).isDirectory()) throw new EngineFaultError(ERROR_CODES.IO_PERMISSION, `refusing to use "${relativePath}": it exists but is not a directory`, "remove or rename the conflicting file, then retry");
|
|
2564
|
+
}
|
|
2532
2565
|
return absolute;
|
|
2533
2566
|
}
|
|
2534
2567
|
async exists(relativePath) {
|
|
@@ -3218,7 +3251,9 @@ const BACKUP_MANIFEST = "manifest.json";
|
|
|
3218
3251
|
/** Backup transaction directories, newest first by filesystem mtime. */
|
|
3219
3252
|
async function listBackups(store) {
|
|
3220
3253
|
const entries = [];
|
|
3221
|
-
for (const
|
|
3254
|
+
for (const entry of await store.entries(BACKUP_DIR)) {
|
|
3255
|
+
if (!entry.directory || entry.symbolicLink) continue;
|
|
3256
|
+
const name = entry.name;
|
|
3222
3257
|
if (name === "migrations") continue;
|
|
3223
3258
|
const info = await store.stat(joinRel(BACKUP_DIR, name));
|
|
3224
3259
|
if (info === null || !info.isDirectory) continue;
|
|
@@ -3379,6 +3414,7 @@ async function runTransaction(store, input) {
|
|
|
3379
3414
|
for (const file of input.files) {
|
|
3380
3415
|
const existing = before.get(file.path);
|
|
3381
3416
|
if (existing === null || existing === void 0) continue;
|
|
3417
|
+
await assertRootPresent();
|
|
3382
3418
|
await store.writeBytes(joinRel(staging, "before", file.path), existing);
|
|
3383
3419
|
}
|
|
3384
3420
|
if (input.backup) {
|
|
@@ -3386,8 +3422,10 @@ async function runTransaction(store, input) {
|
|
|
3386
3422
|
for (const file of input.files) {
|
|
3387
3423
|
const existing = before.get(file.path);
|
|
3388
3424
|
if (existing === null || existing === void 0) continue;
|
|
3425
|
+
await assertRootPresent();
|
|
3389
3426
|
await store.writeBytes(joinRel(stagingBackup, file.path), existing);
|
|
3390
3427
|
}
|
|
3428
|
+
await assertRootPresent();
|
|
3391
3429
|
await store.writeText(joinRel(stagingBackup, BACKUP_MARKER), "1");
|
|
3392
3430
|
await store.writeText(joinRel(stagingBackup, BACKUP_MANIFEST), `${JSON.stringify({
|
|
3393
3431
|
schema_version: 1,
|
|
@@ -3399,9 +3437,11 @@ async function runTransaction(store, input) {
|
|
|
3399
3437
|
...input.reason === void 0 ? {} : { reason: input.reason },
|
|
3400
3438
|
files: input.files.filter((file) => before.get(file.path) !== null && before.get(file.path) !== void 0).map((file) => file.path)
|
|
3401
3439
|
}, null, 2)}\n`);
|
|
3402
|
-
|
|
3403
|
-
await
|
|
3404
|
-
await
|
|
3440
|
+
await assertRootPresent();
|
|
3441
|
+
await store.ensureDir(BACKUP_DIR);
|
|
3442
|
+
await assertRootPresent();
|
|
3443
|
+
const targetBackup = await store.safePath(backupDir(input.txid));
|
|
3444
|
+
await promises.rename(await store.safePath(stagingBackup), targetBackup);
|
|
3405
3445
|
await syncDirectory(dirname(targetBackup));
|
|
3406
3446
|
backupMarked = true;
|
|
3407
3447
|
backupComplete = true;
|
|
@@ -4305,12 +4345,38 @@ function normalizeOutlineChapters(input, options) {
|
|
|
4305
4345
|
if (target[0] !== base.target_words[0] || target[1] !== base.target_words[1]) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its target_words cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4306
4346
|
const cast = raw.cast === void 0 ? base.cast : stringArray$1(raw.cast, `chapters[${index}].cast`, 8);
|
|
4307
4347
|
if (cast.length !== base.cast.length || cast.some((name) => !base.cast.includes(name))) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its cast cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4348
|
+
const goal = raw.goal === void 0 || raw.goal === base.goal ? base.goal : text$2(raw.goal, `chapter ${num} goal`);
|
|
4349
|
+
const conflict = raw.conflict === void 0 || raw.conflict === base.conflict ? base.conflict : text$2(raw.conflict, `chapter ${num} conflict`);
|
|
4350
|
+
const hook = raw.hook === void 0 || raw.hook === base.hook ? base.hook : text$2(raw.hook, `chapter ${num} hook`);
|
|
4351
|
+
const notes = raw.notes === void 0 || raw.notes === base.notes ? base.notes : optionalText(raw.notes, `chapter ${num} notes`, LIMITS.maxOutlineNotesChars);
|
|
4352
|
+
for (const [field, value, previous] of [
|
|
4353
|
+
[
|
|
4354
|
+
"goal",
|
|
4355
|
+
goal,
|
|
4356
|
+
base.goal
|
|
4357
|
+
],
|
|
4358
|
+
[
|
|
4359
|
+
"conflict",
|
|
4360
|
+
conflict,
|
|
4361
|
+
base.conflict
|
|
4362
|
+
],
|
|
4363
|
+
[
|
|
4364
|
+
"hook",
|
|
4365
|
+
hook,
|
|
4366
|
+
base.hook
|
|
4367
|
+
],
|
|
4368
|
+
[
|
|
4369
|
+
"notes",
|
|
4370
|
+
notes,
|
|
4371
|
+
base.notes
|
|
4372
|
+
]
|
|
4373
|
+
]) if (value !== previous) throw new EngineInputError(ERROR_CODES.SCHEMA_INVALID, `chapter ${num} is archived; its ${field} cannot change`, "call novel_reopen first if the manuscript must be revised");
|
|
4308
4374
|
normalized.set(num, {
|
|
4309
4375
|
...base,
|
|
4310
|
-
goal
|
|
4311
|
-
conflict
|
|
4312
|
-
hook
|
|
4313
|
-
notes
|
|
4376
|
+
goal,
|
|
4377
|
+
conflict,
|
|
4378
|
+
hook,
|
|
4379
|
+
notes,
|
|
4314
4380
|
volume: volumeForChapter(options.volumes, num, raw.volume === void 0 ? base.volume : raw.volume),
|
|
4315
4381
|
status: "archived",
|
|
4316
4382
|
archived_at: base.archived_at ?? record.archived_at ?? null,
|
|
@@ -5485,9 +5551,10 @@ const requireForeshadowFields = (value, where) => {
|
|
|
5485
5551
|
if (value.note !== void 0 && typeof value.note !== "string") fail$1(`${where}.note must be a string`);
|
|
5486
5552
|
if (value.new_info !== void 0 && typeof value.new_info !== "boolean") fail$1(`${where}.new_info must be a boolean`);
|
|
5487
5553
|
};
|
|
5554
|
+
const withoutQuotedSpeech = (text) => text.replace(/"[^"]*"|“[^”]*”|「[^」]*」|『[^』]*』/g, "");
|
|
5488
5555
|
const appearanceOf = (text, name, aliases = []) => {
|
|
5489
5556
|
const paragraphs = splitParagraphs(text);
|
|
5490
|
-
const source = paragraphs.find((paragraph) => paragraph.includes(name)
|
|
5557
|
+
const source = paragraphs.find((paragraph) => withoutQuotedSpeech(paragraph).includes(name)) ?? paragraphs.find((paragraph) => paragraph.includes(name)) ?? paragraphs.find((paragraph) => aliases.some((alias) => alias !== "" && withoutQuotedSpeech(paragraph).includes(alias))) ?? paragraphs[0] ?? "";
|
|
5491
5558
|
return Array.from(source).slice(0, 120).join("");
|
|
5492
5559
|
};
|
|
5493
5560
|
const named = (value) => isRecord(value) && typeof value.name === "string";
|
|
@@ -5578,12 +5645,8 @@ function applyStateDelta(input) {
|
|
|
5578
5645
|
const appeared = [...new Set(summary.characters_appeared)];
|
|
5579
5646
|
const byName = new Map(characters.characters.map((character) => [character.name, character]));
|
|
5580
5647
|
const narrator = input.narrator ?? "";
|
|
5581
|
-
const narratorName = narrator
|
|
5582
|
-
const
|
|
5583
|
-
const appearsInManuscript = (name) => {
|
|
5584
|
-
if (input.text.includes(name)) return true;
|
|
5585
|
-
return name === narratorName && narratorAliases.some((alias) => alias !== "" && input.text.includes(alias));
|
|
5586
|
-
};
|
|
5648
|
+
const narratorName$1 = narratorName(narrator);
|
|
5649
|
+
const appearsInManuscript = (name) => isNarratorAlias(name, narrator, input.text);
|
|
5587
5650
|
for (const name of appeared) {
|
|
5588
5651
|
if (!appearsInManuscript(name)) fail$1(`character "${name}" is listed in characters_appeared but does not appear in the manuscript`);
|
|
5589
5652
|
if (!Object.hasOwn(delta.characters, name)) fail$1(`character "${name}" appeared in the chapter but is not declared in characters`);
|
|
@@ -5595,7 +5658,7 @@ function applyStateDelta(input) {
|
|
|
5595
5658
|
"profile",
|
|
5596
5659
|
"current_state",
|
|
5597
5660
|
"growth"
|
|
5598
|
-
]) if (declared[field] !== void 0 && !isRecord(declared[field])) fail$1(`characters.${name}.${field} must be an object; expected ${field === "current_state" ? "{\"resources\":{\"
|
|
5661
|
+
]) if (declared[field] !== void 0 && !isRecord(declared[field])) fail$1(`characters.${name}.${field} must be an object; expected ${field === "current_state" ? "{\"resources\":{\"资源名\":3},\"status\":\"...\"}" : field === "profile" ? "{\"age\":31,\"occupation\":\"...\"}" : "{\"change\":\"...\",\"status_change\":\"...\"}"}`);
|
|
5599
5662
|
if (declared.growth !== void 0) {
|
|
5600
5663
|
const growth = declared.growth;
|
|
5601
5664
|
for (const field of ["change", "status_change"]) if (typeof growth[field] !== "string" || growth[field].trim() === "") fail$1(`characters.${name}.growth.${field} must be a non-empty string`);
|
|
@@ -5605,16 +5668,20 @@ function applyStateDelta(input) {
|
|
|
5605
5668
|
if (existing !== void 0) fail$1(`character "${name}" already exists; use updated or no_change`);
|
|
5606
5669
|
if (!appeared.includes(name)) fail$1(`character "${name}" is created but does not appear in characters_appeared`);
|
|
5607
5670
|
const rawHint = input.roleHints?.[name];
|
|
5608
|
-
const hinted = canonicalRole(rawHint) ?? (rawHint === void 0 && (name === narratorName || name === narrator) ? "protagonist" : "extra");
|
|
5671
|
+
const hinted = canonicalRole(rawHint) ?? (rawHint === void 0 && (name === narratorName$1 || name === narrator) ? "protagonist" : "extra");
|
|
5609
5672
|
const currentState = declared.current_state ?? {};
|
|
5610
5673
|
validateResources(currentState, `characters.${name}`);
|
|
5674
|
+
const growth = declared.growth === void 0 ? [] : [{
|
|
5675
|
+
...declared.growth,
|
|
5676
|
+
chapter
|
|
5677
|
+
}];
|
|
5611
5678
|
const created = {
|
|
5612
5679
|
name,
|
|
5613
5680
|
role: normalizeRole(declared.role, hinted),
|
|
5614
5681
|
first_appearance: chapter,
|
|
5615
5682
|
profile: declared.profile ?? {},
|
|
5616
5683
|
current_state: currentState,
|
|
5617
|
-
growth
|
|
5684
|
+
growth,
|
|
5618
5685
|
updated_at: chapter
|
|
5619
5686
|
};
|
|
5620
5687
|
characters.characters.push(created);
|
|
@@ -5626,7 +5693,7 @@ function applyStateDelta(input) {
|
|
|
5626
5693
|
const rawHint = input.roleHints?.[name];
|
|
5627
5694
|
const hinted = canonicalRole(rawHint);
|
|
5628
5695
|
if (hinted !== null && hinted !== "extra") existing.role = hinted;
|
|
5629
|
-
else if (rawHint === void 0 && (name === narratorName || name === narrator)) existing.role = "protagonist";
|
|
5696
|
+
else if (rawHint === void 0 && (name === narratorName$1 || name === narrator)) existing.role = "protagonist";
|
|
5630
5697
|
}
|
|
5631
5698
|
if (declared.role !== void 0) existing.role = normalizeRole(declared.role, existing.role);
|
|
5632
5699
|
if (declared.action === "updated") {
|
|
@@ -5809,7 +5876,7 @@ function applyStateDelta(input) {
|
|
|
5809
5876
|
setOwn(locks.characters, name, {
|
|
5810
5877
|
first_appearance: character.first_appearance,
|
|
5811
5878
|
locked_at: chapter,
|
|
5812
|
-
appearance: appearanceOf(input.text, name, name
|
|
5879
|
+
appearance: appearanceOf(input.text, name, narratorAliasesFor(name, narrator))
|
|
5813
5880
|
});
|
|
5814
5881
|
}
|
|
5815
5882
|
for (const location of world.locations) if (!Object.hasOwn(locks.locations, location.name)) setOwn(locks.locations, location.name, {
|
|
@@ -6330,8 +6397,8 @@ function buildGuide(input) {
|
|
|
6330
6397
|
},
|
|
6331
6398
|
{
|
|
6332
6399
|
id: "write",
|
|
6333
|
-
title: input.finished ? "全书章节已写完" : `写第 ${input.chapter} 章`,
|
|
6334
|
-
detail: input.finished ? `已完成 ${input.archived} 章;下一步是导出成书。` : `当前阶段 ${input.stage};${input.blocking === 0 ? "没有阻塞" : `${input.blocking} 条阻塞`}。`,
|
|
6400
|
+
title: input.finished ? "全书章节已写完" : input.progress?.current !== null && input.progress?.current !== void 0 ? `写第 ${input.progress.current.chapter} 章` : input.progress?.next !== null && input.progress?.next !== void 0 ? `准备第 ${input.progress.next.chapter} 章(P1)` : `写第 ${input.chapter} 章`,
|
|
6401
|
+
detail: input.finished ? `已完成 ${input.archived} 章;下一步是导出成书。` : `${input.progress?.label ?? `当前阶段 ${input.stage}`};${input.blocking === 0 ? "没有阻塞" : `${input.blocking} 条阻塞`}。`,
|
|
6335
6402
|
done: input.finished,
|
|
6336
6403
|
tool: input.next.tool
|
|
6337
6404
|
},
|
|
@@ -6345,7 +6412,7 @@ function buildGuide(input) {
|
|
|
6345
6412
|
];
|
|
6346
6413
|
return {
|
|
6347
6414
|
state: input.finished ? "finished" : input.stateErrors > 0 || input.blocking > 0 ? "blocked" : input.stage === "idle" && input.archived === 0 && !outlineDone && !blueprintDone && !worldbookDone ? "new" : input.stage === "idle" && input.archived === 0 ? "planning" : "writing",
|
|
6348
|
-
title: input.finished ? "全书已完成" : `第 ${input.chapter} 章 · ${input.stage}`,
|
|
6415
|
+
title: input.finished ? "全书已完成" : input.progress?.label ?? `第 ${input.chapter} 章 · ${input.stage}`,
|
|
6349
6416
|
steps,
|
|
6350
6417
|
next: input.next,
|
|
6351
6418
|
commands: [
|
|
@@ -8015,11 +8082,8 @@ const summarySchema = (input) => {
|
|
|
8015
8082
|
}
|
|
8016
8083
|
}
|
|
8017
8084
|
if (textArray(summary.characters_appeared)) {
|
|
8018
|
-
const narrator = input.pipeline.draft?.narrator ?? "";
|
|
8019
|
-
for (const name of summary.characters_appeared) {
|
|
8020
|
-
const alias = narrator.includes(name) && (narrator.includes("第一人称") || narrator.includes("「我」")) && input.text.includes("我");
|
|
8021
|
-
if (!input.text.includes(name) && !alias) problems.push(`characters_appeared lists "${name}", but the manuscript never mentions it`);
|
|
8022
|
-
}
|
|
8085
|
+
const narrator = input.narrator ?? input.pipeline.draft?.narrator ?? "";
|
|
8086
|
+
for (const name of summary.characters_appeared) if (!isNarratorAlias(name, narrator, input.text)) problems.push(`characters_appeared lists "${name}", but the manuscript never mentions it`);
|
|
8023
8087
|
}
|
|
8024
8088
|
const resources = summary.resource_changes;
|
|
8025
8089
|
if (!Array.isArray(resources) || !resources.every((item) => typeof item === "object" && item !== null && isText(item.character) && isText(item.resource) && Number.isInteger(item.delta) && item.delta !== 0 && isText(item.reason))) problems.push("resource_changes must contain character/resource/non-zero delta/reason");
|
|
@@ -8363,12 +8427,67 @@ function briefStageFor(operation, stage) {
|
|
|
8363
8427
|
}
|
|
8364
8428
|
}
|
|
8365
8429
|
// src/engine/projections.ts
|
|
8430
|
+
/**
|
|
8431
|
+
* Turn the protocol pair (`chapter` + `stage`) into the only human-readable
|
|
8432
|
+
* chapter boundary. After archiving chapter N the protocol intentionally keeps
|
|
8433
|
+
* `stage: 'done'` and advances `chapter` to N+1; `done` describes chapter N,
|
|
8434
|
+
* not the chapter the pipeline now points at.
|
|
8435
|
+
*/
|
|
8436
|
+
function projectChapterProgress(input) {
|
|
8437
|
+
const archived = [...new Set(input.archived)].filter((num) => Number.isInteger(num) && num > 0).sort((left, right) => left - right);
|
|
8438
|
+
const lastArchived = archived.length === 0 ? null : archived[archived.length - 1];
|
|
8439
|
+
const finished = input.stage === "done" && input.target !== null && archived.length >= input.target;
|
|
8440
|
+
const base = {
|
|
8441
|
+
active_chapter: input.activeChapter,
|
|
8442
|
+
stage: input.stage,
|
|
8443
|
+
last_archived: lastArchived,
|
|
8444
|
+
finished
|
|
8445
|
+
};
|
|
8446
|
+
if (finished) return {
|
|
8447
|
+
...base,
|
|
8448
|
+
current: null,
|
|
8449
|
+
next: null,
|
|
8450
|
+
label: `全书完成:已归档 ${archived.length} 章`,
|
|
8451
|
+
resume: `全书已完成(已归档 ${archived.length} 章);如需续写,先用 novel_amend 提高 targets.total_chapters。`
|
|
8452
|
+
};
|
|
8453
|
+
if (input.stage === "done") {
|
|
8454
|
+
if (lastArchived === null) return {
|
|
8455
|
+
...base,
|
|
8456
|
+
current: null,
|
|
8457
|
+
next: null,
|
|
8458
|
+
label: `当前第 ${input.activeChapter} 章 · 阶段 done(尚无归档章)`,
|
|
8459
|
+
resume: `引擎处于 done,但章节索引里还没有已归档章;先核对项目状态。`
|
|
8460
|
+
};
|
|
8461
|
+
const nextChapter = input.activeChapter > lastArchived ? input.activeChapter : lastArchived + 1;
|
|
8462
|
+
return {
|
|
8463
|
+
...base,
|
|
8464
|
+
current: null,
|
|
8465
|
+
next: {
|
|
8466
|
+
chapter: nextChapter,
|
|
8467
|
+
stage: "P1"
|
|
8468
|
+
},
|
|
8469
|
+
label: `第 ${lastArchived} 章已归档 · 下一章第 ${nextChapter} 章待开始(P1)`,
|
|
8470
|
+
resume: `第 ${lastArchived} 章已归档;当前不处于写作阶段,下一章第 ${nextChapter} 章从 P1 开始。`
|
|
8471
|
+
};
|
|
8472
|
+
}
|
|
8473
|
+
return {
|
|
8474
|
+
...base,
|
|
8475
|
+
current: {
|
|
8476
|
+
chapter: input.activeChapter,
|
|
8477
|
+
stage: input.stage
|
|
8478
|
+
},
|
|
8479
|
+
next: null,
|
|
8480
|
+
label: `第 ${input.activeChapter} 章 · 阶段 ${input.stage}`,
|
|
8481
|
+
resume: `当前第 ${input.activeChapter} 章,阶段 ${input.stage}。`
|
|
8482
|
+
};
|
|
8483
|
+
}
|
|
8366
8484
|
/** One shared prompt template for `/novel resume` and the panel copy button. */
|
|
8367
8485
|
function formatResumePrompt(input) {
|
|
8486
|
+
if (input.progress?.finished === true) return "";
|
|
8368
8487
|
return [
|
|
8369
8488
|
`请继续《${input.title}》。`,
|
|
8370
8489
|
`项目根:${input.root}(第一次工具调用必须显式传 project_root=${input.root})。`,
|
|
8371
|
-
`当前第 ${input.chapter} 章,阶段 ${input.stage}。`,
|
|
8490
|
+
input.progress?.resume ?? `当前第 ${input.chapter} 章,阶段 ${input.stage}。`,
|
|
8372
8491
|
`下一步:${input.nextTool}。`,
|
|
8373
8492
|
"先调用 novel_status 核对项目根与书名,再按 novel_pipeline 的下一步执行;",
|
|
8374
8493
|
"只操作这个项目,不要读取或写入其他书。"
|
|
@@ -8434,8 +8553,11 @@ function projectSummary(snapshot) {
|
|
|
8434
8553
|
platform: snapshot.platform,
|
|
8435
8554
|
chapter: snapshot.chapter,
|
|
8436
8555
|
stage: snapshot.stage,
|
|
8556
|
+
...snapshot.progress === void 0 ? {} : { progress: snapshot.progress },
|
|
8437
8557
|
revision: snapshot.revision,
|
|
8438
8558
|
totalWords: snapshot.totalWords,
|
|
8559
|
+
...snapshot.archivedWords === void 0 ? {} : { archivedWords: snapshot.archivedWords },
|
|
8560
|
+
...snapshot.draftWords === void 0 ? {} : { draftWords: snapshot.draftWords },
|
|
8439
8561
|
totalChapters: snapshot.chapters.length,
|
|
8440
8562
|
averageScore: snapshot.averageScore ?? (snapshot.chapters.length === 0 ? 0 : snapshot.chapters.reduce((sum, chapter) => sum + chapter.score, 0) / snapshot.chapters.length),
|
|
8441
8563
|
...snapshot.volume === void 0 ? {} : { volume: snapshot.volume },
|
|
@@ -10206,6 +10328,12 @@ var EngineService = class {
|
|
|
10206
10328
|
*/
|
|
10207
10329
|
async statusFor(snapshot, chapter) {
|
|
10208
10330
|
const metrics = await this.metricsFor(snapshot, chapter);
|
|
10331
|
+
const progress = projectChapterProgress({
|
|
10332
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10333
|
+
stage: snapshot.pipeline.stage,
|
|
10334
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10335
|
+
target: snapshot.project.targets.total_chapters
|
|
10336
|
+
});
|
|
10209
10337
|
const journal = await this.journalBlocking(snapshot);
|
|
10210
10338
|
const pending = await inspectPending(this.storeFor(snapshot.root));
|
|
10211
10339
|
const pendingWarnings = pending.state === "complete" || pending.state === "untouched" ? [{
|
|
@@ -10229,6 +10357,7 @@ var EngineService = class {
|
|
|
10229
10357
|
active_chapter: snapshot.pipeline.chapter,
|
|
10230
10358
|
manuscript_hash: snapshot.pipeline.draft !== void 0 && snapshot.text !== null ? sha256(snapshot.text) : null,
|
|
10231
10359
|
stage: snapshot.pipeline.stage,
|
|
10360
|
+
progress,
|
|
10232
10361
|
rounds: { ...snapshot.pipeline.rounds },
|
|
10233
10362
|
blocking: [...this.effectiveBlocking(snapshot), ...journal],
|
|
10234
10363
|
...pendingWarnings.length === 0 ? {} : { warnings: pendingWarnings },
|
|
@@ -10540,6 +10669,12 @@ var EngineService = class {
|
|
|
10540
10669
|
}
|
|
10541
10670
|
async advise(input = {}) {
|
|
10542
10671
|
const snapshot = await this.load(input.root);
|
|
10672
|
+
const progress = projectChapterProgress({
|
|
10673
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10674
|
+
stage: snapshot.pipeline.stage,
|
|
10675
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10676
|
+
target: snapshot.project.targets.total_chapters
|
|
10677
|
+
});
|
|
10543
10678
|
const journal = await this.journalBlocking(snapshot);
|
|
10544
10679
|
const next = journal.length > 0 && this.stateErrors(snapshot).length === 0 ? {
|
|
10545
10680
|
tool: "novel_audit",
|
|
@@ -10550,6 +10685,7 @@ var EngineService = class {
|
|
|
10550
10685
|
data: {
|
|
10551
10686
|
stage: snapshot.pipeline.stage,
|
|
10552
10687
|
chapter: snapshot.pipeline.chapter,
|
|
10688
|
+
progress,
|
|
10553
10689
|
tool: next.tool,
|
|
10554
10690
|
args: next.args,
|
|
10555
10691
|
reason: next.reason,
|
|
@@ -10650,7 +10786,7 @@ var EngineService = class {
|
|
|
10650
10786
|
} catch {}
|
|
10651
10787
|
if (oldChecks > 0) issues.push({
|
|
10652
10788
|
code: "LEGACY_CHECK_FORMAT",
|
|
10653
|
-
detail: `${oldChecks} 章 check 是 0.2.0 旧格式(advisory 混在 violations);重新跑 novel_check 或按
|
|
10789
|
+
detail: `${oldChecks} 章 check 是 0.2.0 旧格式(advisory 混在 violations);重新跑 novel_check 或按 0.2.3 迁移说明标记`
|
|
10654
10790
|
});
|
|
10655
10791
|
}
|
|
10656
10792
|
const hints = /* @__PURE__ */ new Map();
|
|
@@ -10751,7 +10887,12 @@ var EngineService = class {
|
|
|
10751
10887
|
id: "pipeline",
|
|
10752
10888
|
severity: "ok",
|
|
10753
10889
|
title: "流水线",
|
|
10754
|
-
detail: `revision ${snapshot.pipeline.revision} ·
|
|
10890
|
+
detail: `revision ${snapshot.pipeline.revision} · ${projectChapterProgress({
|
|
10891
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
10892
|
+
stage: snapshot.pipeline.stage,
|
|
10893
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
10894
|
+
target: snapshot.project.targets.total_chapters
|
|
10895
|
+
}).label}`
|
|
10755
10896
|
});
|
|
10756
10897
|
for (const error of this.stateErrors(snapshot)) add({
|
|
10757
10898
|
id: `state:${error.path}`,
|
|
@@ -10943,7 +11084,7 @@ var EngineService = class {
|
|
|
10943
11084
|
severity: "warning",
|
|
10944
11085
|
title: "旧数据升级提示",
|
|
10945
11086
|
detail: `${legacy.length} 类旧数据需要解释或人工补齐:${legacy.slice(0, 3).map((issue) => `${issue.code}(${issue.detail})`).join(";")}`,
|
|
10946
|
-
hint: "这是只读审计,不会自动改历史;按
|
|
11087
|
+
hint: "这是只读审计,不会自动改历史;按 0.2.3 的 WP0 处理,补 anchor/role 前先核对原文"
|
|
10947
11088
|
});
|
|
10948
11089
|
const locked = await store.exists(LOCK_FILE);
|
|
10949
11090
|
add({
|
|
@@ -11124,7 +11265,13 @@ var EngineService = class {
|
|
|
11124
11265
|
const snapshot = await this.loadAt(selection);
|
|
11125
11266
|
await this.recordSelectionBestEffort(selection, snapshot);
|
|
11126
11267
|
const target = snapshot.project.targets.total_chapters;
|
|
11127
|
-
const
|
|
11268
|
+
const progress = projectChapterProgress({
|
|
11269
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
11270
|
+
stage: snapshot.pipeline.stage,
|
|
11271
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
11272
|
+
target
|
|
11273
|
+
});
|
|
11274
|
+
const finished = progress.finished;
|
|
11128
11275
|
const journalBlocking = await this.journalBlocking(snapshot);
|
|
11129
11276
|
const next = journalBlocking.length > 0 && this.stateErrors(snapshot).length === 0 ? {
|
|
11130
11277
|
tool: "novel_audit",
|
|
@@ -11136,6 +11283,7 @@ var EngineService = class {
|
|
|
11136
11283
|
projectPresent: true,
|
|
11137
11284
|
chapter: snapshot.pipeline.chapter,
|
|
11138
11285
|
stage: snapshot.pipeline.stage,
|
|
11286
|
+
progress,
|
|
11139
11287
|
archived: snapshot.chapters.chapters.length,
|
|
11140
11288
|
target,
|
|
11141
11289
|
finished,
|
|
@@ -11235,7 +11383,7 @@ var EngineService = class {
|
|
|
11235
11383
|
reason: "no_change 必填;created/updated 建议写",
|
|
11236
11384
|
role: "protagonist | supporting | antagonist | extra(可选)",
|
|
11237
11385
|
profile: "object(可选)",
|
|
11238
|
-
current_state: "object(可选,如 {\"resources\":{\"
|
|
11386
|
+
current_state: "object(可选,如 {\"resources\":{\"资源名\":-3}})"
|
|
11239
11387
|
} },
|
|
11240
11388
|
foreshadows: { "<summary.planted.key 或 FS-xxx>": {
|
|
11241
11389
|
action: "created | updated | no_change",
|
|
@@ -11265,27 +11413,27 @@ var EngineService = class {
|
|
|
11265
11413
|
},
|
|
11266
11414
|
examples: [{
|
|
11267
11415
|
summary_excerpt: {
|
|
11268
|
-
characters_appeared: ["
|
|
11416
|
+
characters_appeared: ["角色甲"],
|
|
11269
11417
|
foreshadows: {
|
|
11270
11418
|
planted: [],
|
|
11271
11419
|
advanced: ["FS-001"],
|
|
11272
11420
|
resolved: []
|
|
11273
11421
|
}
|
|
11274
11422
|
},
|
|
11275
|
-
characters: {
|
|
11423
|
+
characters: { 角色甲: {
|
|
11276
11424
|
action: "updated",
|
|
11277
|
-
current_state: { resources: {
|
|
11425
|
+
current_state: { resources: { 资源名: -3 } }
|
|
11278
11426
|
} },
|
|
11279
11427
|
foreshadows: { "FS-001": {
|
|
11280
11428
|
action: "updated",
|
|
11281
|
-
note: "
|
|
11429
|
+
note: "债务已结清",
|
|
11282
11430
|
new_info: true
|
|
11283
11431
|
} }
|
|
11284
11432
|
}, {
|
|
11285
11433
|
summary_excerpt: {
|
|
11286
11434
|
events: ["收到信"],
|
|
11287
11435
|
new_elements: {
|
|
11288
|
-
locations: ["
|
|
11436
|
+
locations: ["地点甲"],
|
|
11289
11437
|
abilities: [],
|
|
11290
11438
|
artifacts: []
|
|
11291
11439
|
}
|
|
@@ -11295,31 +11443,31 @@ var EngineService = class {
|
|
|
11295
11443
|
story_time: "Day 3 清晨",
|
|
11296
11444
|
description: "收到信"
|
|
11297
11445
|
} },
|
|
11298
|
-
world: {
|
|
11446
|
+
world: { 地点甲: {
|
|
11299
11447
|
action: "created",
|
|
11300
11448
|
kind: "location",
|
|
11301
|
-
description: "
|
|
11449
|
+
description: "地点说明"
|
|
11302
11450
|
} }
|
|
11303
11451
|
}],
|
|
11304
11452
|
linked_example: {
|
|
11305
11453
|
summary: {
|
|
11306
|
-
characters_appeared: ["
|
|
11454
|
+
characters_appeared: ["角色甲"],
|
|
11307
11455
|
new_elements: {
|
|
11308
|
-
locations: ["
|
|
11456
|
+
locations: ["地点甲"],
|
|
11309
11457
|
abilities: [],
|
|
11310
11458
|
artifacts: []
|
|
11311
11459
|
},
|
|
11312
11460
|
events: ["收到信"]
|
|
11313
11461
|
},
|
|
11314
11462
|
delta: {
|
|
11315
|
-
characters: {
|
|
11463
|
+
characters: { 角色甲: {
|
|
11316
11464
|
action: "no_change",
|
|
11317
11465
|
reason: "本章无变化"
|
|
11318
11466
|
} },
|
|
11319
|
-
world: {
|
|
11467
|
+
world: { 地点甲: {
|
|
11320
11468
|
action: "created",
|
|
11321
11469
|
kind: "location",
|
|
11322
|
-
description: "
|
|
11470
|
+
description: "地点说明"
|
|
11323
11471
|
} },
|
|
11324
11472
|
timeline: { "0": {
|
|
11325
11473
|
action: "created",
|
|
@@ -11535,6 +11683,7 @@ var EngineService = class {
|
|
|
11535
11683
|
summary: input.summary,
|
|
11536
11684
|
draftTitle: input.chapter_title,
|
|
11537
11685
|
sceneType: input.scene_type,
|
|
11686
|
+
narrator: input.narrator,
|
|
11538
11687
|
planTargetWords: snapshot.plan?.target_words ?? null,
|
|
11539
11688
|
planCast: snapshot.plan?.cast === void 0 ? null : snapshot.plan.cast.map((member) => member.name)
|
|
11540
11689
|
});
|
|
@@ -12863,12 +13012,13 @@ var EngineService = class {
|
|
|
12863
13012
|
}
|
|
12864
13013
|
const summary = projectSummary(snapshot);
|
|
12865
13014
|
if (option.state !== "ok" && option.title !== "") summary.title = option.title;
|
|
12866
|
-
const resume = summary.next === void 0 ? void 0 : { text: formatResumePrompt({
|
|
13015
|
+
const resume = summary.next === void 0 || summary.progress?.finished === true ? void 0 : { text: formatResumePrompt({
|
|
12867
13016
|
title: summary.title,
|
|
12868
13017
|
root: summary.root,
|
|
12869
13018
|
chapter: summary.chapter,
|
|
12870
13019
|
stage: summary.stage,
|
|
12871
|
-
nextTool: summary.next.tool
|
|
13020
|
+
nextTool: summary.next.tool,
|
|
13021
|
+
...summary.progress === void 0 ? {} : { progress: summary.progress }
|
|
12872
13022
|
}) };
|
|
12873
13023
|
return {
|
|
12874
13024
|
projects,
|
|
@@ -12888,12 +13038,13 @@ var EngineService = class {
|
|
|
12888
13038
|
const nextTool = status.data.next?.tool;
|
|
12889
13039
|
return {
|
|
12890
13040
|
data: {
|
|
12891
|
-
text: nextTool === void 0 ? null : formatResumePrompt({
|
|
13041
|
+
text: nextTool === void 0 || status.data.progress?.finished === true ? null : formatResumePrompt({
|
|
12892
13042
|
title: status.data.project.title,
|
|
12893
13043
|
root: status.data.project.root,
|
|
12894
13044
|
chapter: status.data.chapter,
|
|
12895
13045
|
stage: status.data.stage,
|
|
12896
|
-
nextTool
|
|
13046
|
+
nextTool,
|
|
13047
|
+
...status.data.progress === void 0 ? {} : { progress: status.data.progress }
|
|
12897
13048
|
}),
|
|
12898
13049
|
selection: status.data.selection
|
|
12899
13050
|
},
|
|
@@ -12903,12 +13054,17 @@ var EngineService = class {
|
|
|
12903
13054
|
}
|
|
12904
13055
|
async stats(input = {}) {
|
|
12905
13056
|
const snapshot = await this.load(input.root);
|
|
12906
|
-
const
|
|
13057
|
+
const archivedWords = snapshot.chapters.chapters.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13058
|
+
const activeIndexed = snapshot.chapters.chapters.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13059
|
+
const draftWords = snapshot.text === null || activeIndexed || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
13060
|
+
const totalWords = archivedWords + draftWords;
|
|
12907
13061
|
const scores = snapshot.chapters.chapters.map((chapter) => chapter.score);
|
|
12908
13062
|
const outline = outlineSummary(snapshot.outline, snapshot.chapters.chapters.length, snapshot.project.targets.total_chapters, snapshot.chapters.chapters.map((record) => record.num));
|
|
12909
13063
|
const counts = worldbookCounts(snapshot.worldbook);
|
|
12910
13064
|
return {
|
|
12911
13065
|
totalWords,
|
|
13066
|
+
archivedWords,
|
|
13067
|
+
draftWords,
|
|
12912
13068
|
chapters: snapshot.chapters.chapters.length,
|
|
12913
13069
|
revision: snapshot.pipeline.revision,
|
|
12914
13070
|
rulepack: `${this.rulepack.name}@${this.rulepack.version}`,
|
|
@@ -13406,15 +13562,32 @@ var EngineService = class {
|
|
|
13406
13562
|
const archived = snapshot.chapters.chapters;
|
|
13407
13563
|
const archivedCurrent = archived.some((chapter) => chapter.num === snapshot.pipeline.chapter);
|
|
13408
13564
|
const target = snapshot.project.targets.total_chapters;
|
|
13409
|
-
const
|
|
13410
|
-
|
|
13565
|
+
const progress = projectChapterProgress({
|
|
13566
|
+
activeChapter: snapshot.pipeline.chapter,
|
|
13567
|
+
stage: snapshot.pipeline.stage,
|
|
13568
|
+
archived: archived.map((record) => record.num),
|
|
13569
|
+
target
|
|
13570
|
+
});
|
|
13571
|
+
const finished = progress.finished;
|
|
13572
|
+
const draftWords = snapshot.text === null || archivedCurrent || snapshot.pipeline.stage === "done" ? 0 : countWordMetrics(snapshot.text).total_word_count;
|
|
13573
|
+
const archivedWords = archived.reduce((total, chapter) => total + chapter.word_count, 0);
|
|
13411
13574
|
const pending = await inspectPending(this.storeFor(snapshot.root));
|
|
13412
13575
|
const stateErrorCount = this.stateErrors(snapshot).length;
|
|
13413
|
-
const panelCacheKey = `${snapshot.root}:${snapshot.pipeline.revision}:${pending.state}:${stateErrorCount}:${snapshot.outlineError ?? ""}:${snapshot.worldbookError ?? ""}:${JSON.stringify(snapshot.briefs)}`;
|
|
13414
|
-
const cachedPanel = this.panelCache.get(snapshot.root);
|
|
13415
|
-
if (cachedPanel?.key === panelCacheKey) return cachedPanel.snapshot;
|
|
13416
13576
|
const journal = await readJournal(this.storeFor(snapshot.root));
|
|
13417
13577
|
const journalBlocking = await this.journalBlocking(snapshot);
|
|
13578
|
+
const journalTail = journal.rows[journal.rows.length - 1];
|
|
13579
|
+
const journalKey = sha256(stableJson({
|
|
13580
|
+
rows: journal.rows.length,
|
|
13581
|
+
tail: journalTail === void 0 ? null : [
|
|
13582
|
+
journalTail.seq,
|
|
13583
|
+
journalTail.at,
|
|
13584
|
+
journalTail.status
|
|
13585
|
+
],
|
|
13586
|
+
blocking: journalBlocking
|
|
13587
|
+
}));
|
|
13588
|
+
const panelCacheKey = `${snapshot.root}:${snapshot.pipeline.revision}:${pending.state}:${stateErrorCount}:${snapshot.outlineError ?? ""}:${snapshot.worldbookError ?? ""}:${JSON.stringify(snapshot.briefs)}:${journalKey}`;
|
|
13589
|
+
const cachedPanel = this.panelCache.get(snapshot.root);
|
|
13590
|
+
if (cachedPanel?.key === panelCacheKey) return cachedPanel.snapshot;
|
|
13418
13591
|
const outline = outlineSummary(snapshot.outline, archived.length, target, archived.map((record) => record.num));
|
|
13419
13592
|
const counts = worldbookCounts(snapshot.worldbook);
|
|
13420
13593
|
const scores = archived.slice(-10).map((chapter) => chapter.score);
|
|
@@ -13569,6 +13742,7 @@ var EngineService = class {
|
|
|
13569
13742
|
platform: snapshot.project.platform,
|
|
13570
13743
|
chapter: snapshot.pipeline.chapter,
|
|
13571
13744
|
stage: snapshot.pipeline.stage,
|
|
13745
|
+
progress,
|
|
13572
13746
|
revision: snapshot.pipeline.revision,
|
|
13573
13747
|
rounds: snapshot.pipeline.rounds,
|
|
13574
13748
|
blocking,
|
|
@@ -13589,7 +13763,9 @@ var EngineService = class {
|
|
|
13589
13763
|
finished,
|
|
13590
13764
|
chapters: archived,
|
|
13591
13765
|
draft,
|
|
13592
|
-
totalWords:
|
|
13766
|
+
totalWords: archivedWords + draftWords,
|
|
13767
|
+
archivedWords,
|
|
13768
|
+
draftWords,
|
|
13593
13769
|
averageScore: archived.length === 0 ? 0 : archived.reduce((total, chapter) => total + chapter.score, 0) / archived.length,
|
|
13594
13770
|
phases: phaseStrip(snapshot.pipeline.stage, finished),
|
|
13595
13771
|
book,
|
|
@@ -13697,6 +13873,12 @@ var EngineService = class {
|
|
|
13697
13873
|
nextStep(snapshot) {
|
|
13698
13874
|
const stage = snapshot.pipeline.stage;
|
|
13699
13875
|
const chapter = snapshot.pipeline.chapter;
|
|
13876
|
+
const progress = projectChapterProgress({
|
|
13877
|
+
activeChapter: chapter,
|
|
13878
|
+
stage,
|
|
13879
|
+
archived: snapshot.chapters.chapters.map((record) => record.num),
|
|
13880
|
+
target: snapshot.project.targets.total_chapters
|
|
13881
|
+
});
|
|
13700
13882
|
if (snapshot.chapters.chapters.some((record) => record.num === chapter)) return {
|
|
13701
13883
|
tool: "novel_amend",
|
|
13702
13884
|
args: { path: joinRel(STATE_DIR, STATE_FILES.chapters) },
|
|
@@ -13747,8 +13929,7 @@ var EngineService = class {
|
|
|
13747
13929
|
};
|
|
13748
13930
|
}
|
|
13749
13931
|
const step = STAGE_NEXT[stage];
|
|
13750
|
-
|
|
13751
|
-
if (stage === "done" && target !== null && snapshot.chapters.chapters.length >= target) return {
|
|
13932
|
+
if (progress.finished) return {
|
|
13752
13933
|
tool: "novel_export",
|
|
13753
13934
|
args: { kind: "epub" },
|
|
13754
13935
|
reason: "全书已完成;可导出成书,或结束本书"
|
|
@@ -14356,6 +14537,7 @@ var EngineService = class {
|
|
|
14356
14537
|
briefStage: extras.briefStage ?? briefStageFor(operation, snapshot.pipeline.stage),
|
|
14357
14538
|
draftTitle: composeTitle(snapshot.pipeline.chapter, extras.draftTitle ?? snapshot.pipeline.draft?.title ?? snapshot.plan?.title ?? null, this.rulepack.format.title.pattern),
|
|
14358
14539
|
sceneType: extras.sceneType ?? snapshot.pipeline.draft?.scene_type ?? snapshot.plan?.scene_type ?? null,
|
|
14540
|
+
narrator: extras.narrator ?? snapshot.pipeline.draft?.narrator ?? snapshot.plan?.narrator ?? null,
|
|
14359
14541
|
summary: extras.summary ?? snapshot.pipeline.draft?.summary ?? snapshot.plan?.summary ?? null,
|
|
14360
14542
|
summaryProvided: extras.summaryProvided,
|
|
14361
14543
|
summaryOnly: extras.summaryOnly,
|
|
@@ -15446,6 +15628,12 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15446
15628
|
problems.push(`archived chapter ${record.num} has no previous outline card; restore its archive backup before repairing target_words/cast/archived_at`);
|
|
15447
15629
|
continue;
|
|
15448
15630
|
}
|
|
15631
|
+
for (const field of [
|
|
15632
|
+
"goal",
|
|
15633
|
+
"conflict",
|
|
15634
|
+
"hook",
|
|
15635
|
+
"notes"
|
|
15636
|
+
]) if (stableJson(before[field]) !== stableJson(card[field])) problems.push(`archived chapter ${record.num} cannot change its ${field}`);
|
|
15449
15637
|
if (JSON.stringify(before.target_words) !== JSON.stringify(card.target_words)) problems.push(`archived chapter ${record.num} cannot change its target_words`);
|
|
15450
15638
|
const beforeCast = Array.isArray(before.cast) ? before.cast : [];
|
|
15451
15639
|
const afterCast = Array.isArray(card.cast) ? card.cast : [];
|
|
@@ -15457,6 +15645,27 @@ const amendProblems = (kind, oldDocument, nextDocument, snapshot, path, archived
|
|
|
15457
15645
|
for (const [num, card] of oldCards) if (card.status === "archived") expected.add(num);
|
|
15458
15646
|
const missing = [...expected].filter((num) => byNum.get(num)?.status !== "archived");
|
|
15459
15647
|
if (missing.length > 0) problems.push(`cannot verify archived cards while chapters.yaml is unreadable; keep archived cards for ${missing.join(", ")}`);
|
|
15648
|
+
for (const [num, before] of oldCards) {
|
|
15649
|
+
if (before.status !== "archived") continue;
|
|
15650
|
+
const card = byNum.get(num);
|
|
15651
|
+
if (card === void 0) continue;
|
|
15652
|
+
if (card.status !== "archived") problems.push(`archived chapter ${num} cannot be changed back to planned`);
|
|
15653
|
+
for (const field of [
|
|
15654
|
+
"title",
|
|
15655
|
+
"scene_type",
|
|
15656
|
+
"word_count",
|
|
15657
|
+
"score",
|
|
15658
|
+
"goal",
|
|
15659
|
+
"conflict",
|
|
15660
|
+
"hook",
|
|
15661
|
+
"notes"
|
|
15662
|
+
]) if (stableJson(before[field]) !== stableJson(card[field])) problems.push(`archived chapter ${num} cannot change its ${field}`);
|
|
15663
|
+
if (JSON.stringify(before.target_words) !== JSON.stringify(card.target_words)) problems.push(`archived chapter ${num} cannot change its target_words`);
|
|
15664
|
+
const beforeCast = Array.isArray(before.cast) ? before.cast : [];
|
|
15665
|
+
const afterCast = Array.isArray(card.cast) ? card.cast : [];
|
|
15666
|
+
if (beforeCast.length !== afterCast.length || afterCast.some((name) => !beforeCast.includes(name))) problems.push(`archived chapter ${num} cannot change its cast`);
|
|
15667
|
+
if (before.archived_at !== card.archived_at && !(before.archived_at === null && typeof card.archived_at === "string")) problems.push(`archived chapter ${num} cannot change archived_at`);
|
|
15668
|
+
}
|
|
15460
15669
|
}
|
|
15461
15670
|
return problems;
|
|
15462
15671
|
}
|
|
@@ -15847,9 +16056,11 @@ function createNovelTools(engine, options = {}) {
|
|
|
15847
16056
|
render: (_args, raw) => {
|
|
15848
16057
|
const value = payload(raw);
|
|
15849
16058
|
const active = value.active_chapter ?? value.chapter;
|
|
15850
|
-
const
|
|
16059
|
+
const progressLabel = value.progress?.label ?? `第 ${value.chapter} 章 · 阶段 ${value.stage}`;
|
|
15851
16060
|
return text([
|
|
15852
|
-
`《${value.project.title}
|
|
16061
|
+
`《${value.project.title}》`,
|
|
16062
|
+
`进度:${progressLabel}`,
|
|
16063
|
+
...active === value.chapter ? [] : [`字数查询:第 ${value.chapter} 章(活动章第 ${active} 章)`],
|
|
15853
16064
|
...value.selection === void 0 ? [] : [`项目根 ${value.selection.root} · 选择来源 ${value.selection.source}`],
|
|
15854
16065
|
...value.rounds === void 0 ? [] : [`轮次:评审 ${value.rounds.review} / 修订 ${value.rounds.revise} / 校验 ${value.rounds.check}`],
|
|
15855
16066
|
value.blocking.length === 0 ? "门控:全部通过" : `门控:${value.blocking.length} 条阻塞`,
|
|
@@ -16803,9 +17014,11 @@ const failure = (error) => {
|
|
|
16803
17014
|
const formatPercent = (value) => value === null || value === void 0 ? "—" : `${Math.round(value * 1e3) / 10}%`;
|
|
16804
17015
|
const statusText = async (engine) => {
|
|
16805
17016
|
const { data } = await engine.getStatus({});
|
|
17017
|
+
const progressLabel = data.progress?.label ?? `第 ${data.chapter} 章 · 阶段 ${data.stage}`;
|
|
16806
17018
|
return [
|
|
16807
17019
|
`《${data.project.title}》 ${data.project.genre} · ${data.project.platform}`,
|
|
16808
|
-
|
|
17020
|
+
`进度:${progressLabel}`,
|
|
17021
|
+
...data.chapter === data.active_chapter ? [] : [`字数查询:第 ${data.chapter} 章(活动章第 ${data.active_chapter} 章)`],
|
|
16809
17022
|
...data.selection === void 0 ? [] : [`项目根 ${data.selection.root} · 选择来源 ${data.selection.source}`],
|
|
16810
17023
|
`轮次 评审 ${data.rounds.review} / 修订 ${data.rounds.revise} / 校验 ${data.rounds.check}`,
|
|
16811
17024
|
data.metrics === null ? "字数 —" : `字数 ${data.metrics.total_word_count}(汉字 ${data.metrics.cjk_chars} + 标点 ${data.metrics.punctuation_chars},含标点)`,
|
|
@@ -16852,11 +17065,11 @@ function createCommands(engine, host = {}) {
|
|
|
16852
17065
|
case "next": {
|
|
16853
17066
|
const { data } = await engine.advise({});
|
|
16854
17067
|
const reason = NEXT_DETAILS[data.tool] ?? data.reason;
|
|
16855
|
-
return ok(
|
|
17068
|
+
return ok(`进度:${data.progress?.label ?? `第 ${data.chapter} 章 · 阶段 ${data.stage}`}\n下一步:${data.tool}\n理由:${reason}${data.blocking.length === 0 ? "" : `\n阻塞 ${data.blocking.length} 条`}`);
|
|
16856
17069
|
}
|
|
16857
17070
|
case "stats": {
|
|
16858
17071
|
const stats = await engine.stats({});
|
|
16859
|
-
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords} · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
17072
|
+
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords}(已归档 ${stats.archivedWords} + 草稿 ${stats.draftWords}) · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
16860
17073
|
}
|
|
16861
17074
|
case "outline": {
|
|
16862
17075
|
const status = await engine.getStatus({});
|
|
@@ -17000,8 +17213,10 @@ function createCommands(engine, host = {}) {
|
|
|
17000
17213
|
}
|
|
17001
17214
|
case "resume": {
|
|
17002
17215
|
const { data } = await engine.resumePrompt({});
|
|
17003
|
-
if (data.text
|
|
17004
|
-
|
|
17216
|
+
if (data.text !== null) return ok(data.text);
|
|
17217
|
+
const status = await engine.getStatus({});
|
|
17218
|
+
if (status.data.progress?.finished === true) return ok(status.data.progress.resume);
|
|
17219
|
+
return bad("当前项目没有可续写的下一步;先用 /novel status 查看项目状态。");
|
|
17005
17220
|
}
|
|
17006
17221
|
case "admin": {
|
|
17007
17222
|
if (rest.length === 0) return ok(adminHelp());
|
|
@@ -17061,7 +17276,7 @@ function createCommands(engine, host = {}) {
|
|
|
17061
17276
|
if (invocation.signal.aborted) return bad("已取消");
|
|
17062
17277
|
try {
|
|
17063
17278
|
const stats = await engine.stats({});
|
|
17064
|
-
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords} · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
17279
|
+
return ok(`章节 ${stats.chapters} 章 · 总字数 ${stats.totalWords}(已归档 ${stats.archivedWords} + 草稿 ${stats.draftWords}) · 平均分 ${stats.averageScore} · 未回收伏笔 ${stats.openForeshadows} · 大纲 ${stats.totalCards} 卡(规划覆盖 ${formatPercent(stats.outlineCoverage)} · 归档覆盖 ${formatPercent(stats.archivedCoverage)}${stats.missingCards > 0 ? ` · 缺卡 ${stats.missingCards}` : ""})· 蓝图 ${stats.blueprintReady ? "就绪" : "未就绪"} · 严格 ${stats.strictOutline ? "开" : "关"} · 目标 ${stats.targetChapters ?? "未设"} 章 · 世界观 ${stats.worldbookEntries} 条 · revision ${stats.revision} · 规则包 ${stats.rulepack}`);
|
|
17065
17280
|
} catch (error) {
|
|
17066
17281
|
return failure(error);
|
|
17067
17282
|
}
|
|
@@ -17837,4 +18052,4 @@ function connectionFence(scope) {
|
|
|
17837
18052
|
}
|
|
17838
18053
|
if (Object.prototype.hasOwnProperty.call(candidate, "connection")) return candidate.connection;
|
|
17839
18054
|
}
|
|
17840
|
-
export { ARCHIVE_DIGESTS_FILE, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BriefLibrary, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, HISTORY_DIR, HISTORY_KINDS, INITIAL_STAGE, LIMITS, LOCK_FILE, METADATA_LOCK_FILE, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, TOOL_NAMES, WORLDBOOK_FILE, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|
|
18055
|
+
export { ARCHIVE_DIGESTS_FILE, BACKUP_DIR, BRIEF_RECEIPTS_FILE, BriefLibrary, CHAPTERS_DIR, CHECK_DIR, CORE_TOOLS, Config, DEFAULT_GENRE, DEFAULT_PLATFORM, ERROR_CODES, EXPORT_DIR, EngineBlockedError, EngineError, EngineFaultError, EngineInputError, EngineService, GENRES, HISTORY_DIR, HISTORY_KINDS, INITIAL_STAGE, LIMITS, LOCK_FILE, METADATA_LOCK_FILE, OPERATIONS, OUTLINE_DEPRECATED_FILE, OUTLINE_DIR, OUTLINE_FILE, PANEL_CHAPTER_LIMIT, PANEL_ID, PANEL_ROUTE, PHASE_STRIP, PLAN_DIR, PLATFORMS, PLUGIN_NAME, PROJECT_FILE, REGISTRY_FILE, REMOTE_VERSION, REVIEW_DIR, SCENE_TYPES, SCENE_TYPE_DEFAULT, SCHEMA_VERSION, SESSION_BINDINGS_FILE, SKILL_NAME, STAGING_DIR, STATE_DIR, STATE_FILES, TOOL_NAMES, WORLDBOOK_FILE, applicableGates, apply, blockedError, briefStageFor, chapterFile, chapterHeading, chapterHistoryFile, checkFile, checkHistoryFile, deprecatedChapterFile, deprecatedHistoryFile, evaluateGates, faultError, formatResumePrompt, gateName, inject, inputError, loadRulepack, name, parseParagraphLocation, phaseStrip, planChapterFile, planHistoryFile, planWorldFile, projectChapter, projectChapterProgress, projectChapters, projectGates, projectSummary, renderBlurb, renderCover, renderEpubChapter, renderMarkdown, renderPlainText, renderProposal, reviewFile, reviewHistoryFile, verifyReference };
|