@actalk/inkos-core 0.5.1 → 0.6.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/dist/agents/architect.d.ts +6 -1
- package/dist/agents/architect.d.ts.map +1 -1
- package/dist/agents/architect.js +362 -83
- package/dist/agents/architect.js.map +1 -1
- package/dist/agents/chapter-analyzer.d.ts +6 -0
- package/dist/agents/chapter-analyzer.d.ts.map +1 -1
- package/dist/agents/chapter-analyzer.js +220 -17
- package/dist/agents/chapter-analyzer.js.map +1 -1
- package/dist/agents/composer.d.ts +28 -0
- package/dist/agents/composer.d.ts.map +1 -0
- package/dist/agents/composer.js +154 -0
- package/dist/agents/composer.js.map +1 -0
- package/dist/agents/consolidator.d.ts.map +1 -1
- package/dist/agents/consolidator.js +17 -8
- package/dist/agents/consolidator.js.map +1 -1
- package/dist/agents/continuity.d.ts +10 -0
- package/dist/agents/continuity.d.ts.map +1 -1
- package/dist/agents/continuity.js +312 -133
- package/dist/agents/continuity.js.map +1 -1
- package/dist/agents/en-prompt-sections.d.ts.map +1 -1
- package/dist/agents/en-prompt-sections.js +1 -0
- package/dist/agents/en-prompt-sections.js.map +1 -1
- package/dist/agents/length-normalizer.d.ts +32 -0
- package/dist/agents/length-normalizer.d.ts.map +1 -0
- package/dist/agents/length-normalizer.js +156 -0
- package/dist/agents/length-normalizer.js.map +1 -0
- package/dist/agents/planner.d.ts +42 -0
- package/dist/agents/planner.d.ts.map +1 -0
- package/dist/agents/planner.js +382 -0
- package/dist/agents/planner.js.map +1 -0
- package/dist/agents/post-write-validator.d.ts +6 -1
- package/dist/agents/post-write-validator.d.ts.map +1 -1
- package/dist/agents/post-write-validator.js +88 -2
- package/dist/agents/post-write-validator.js.map +1 -1
- package/dist/agents/reviser.d.ts +10 -1
- package/dist/agents/reviser.d.ts.map +1 -1
- package/dist/agents/reviser.js +151 -36
- package/dist/agents/reviser.js.map +1 -1
- package/dist/agents/rules-reader.d.ts +1 -0
- package/dist/agents/rules-reader.d.ts.map +1 -1
- package/dist/agents/rules-reader.js +13 -0
- package/dist/agents/rules-reader.js.map +1 -1
- package/dist/agents/settler-delta-parser.d.ts +7 -0
- package/dist/agents/settler-delta-parser.d.ts.map +1 -0
- package/dist/agents/settler-delta-parser.js +35 -0
- package/dist/agents/settler-delta-parser.js.map +1 -0
- package/dist/agents/settler-prompts.d.ts +2 -0
- package/dist/agents/settler-prompts.d.ts.map +1 -1
- package/dist/agents/settler-prompts.js +77 -63
- package/dist/agents/settler-prompts.js.map +1 -1
- package/dist/agents/writer-parser.d.ts +3 -2
- package/dist/agents/writer-parser.d.ts.map +1 -1
- package/dist/agents/writer-parser.js +44 -13
- package/dist/agents/writer-parser.js.map +1 -1
- package/dist/agents/writer-prompts.d.ts +2 -1
- package/dist/agents/writer-prompts.d.ts.map +1 -1
- package/dist/agents/writer-prompts.js +65 -21
- package/dist/agents/writer-prompts.js.map +1 -1
- package/dist/agents/writer.d.ts +28 -1
- package/dist/agents/writer.d.ts.map +1 -1
- package/dist/agents/writer.js +426 -67
- package/dist/agents/writer.js.map +1 -1
- package/dist/index.d.ts +18 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/llm/provider.d.ts +1 -0
- package/dist/llm/provider.d.ts.map +1 -1
- package/dist/llm/provider.js +19 -6
- package/dist/llm/provider.js.map +1 -1
- package/dist/models/chapter.d.ts +71 -0
- package/dist/models/chapter.d.ts.map +1 -1
- package/dist/models/chapter.js +3 -0
- package/dist/models/chapter.js.map +1 -1
- package/dist/models/input-governance.d.ts +351 -0
- package/dist/models/input-governance.d.ts.map +1 -0
- package/dist/models/input-governance.js +78 -0
- package/dist/models/input-governance.js.map +1 -0
- package/dist/models/length-governance.d.ts +93 -0
- package/dist/models/length-governance.d.ts.map +1 -0
- package/dist/models/length-governance.js +34 -0
- package/dist/models/length-governance.js.map +1 -0
- package/dist/models/project.d.ts +5 -0
- package/dist/models/project.d.ts.map +1 -1
- package/dist/models/project.js +2 -0
- package/dist/models/project.js.map +1 -1
- package/dist/models/runtime-state.d.ts +521 -0
- package/dist/models/runtime-state.d.ts.map +1 -0
- package/dist/models/runtime-state.js +78 -0
- package/dist/models/runtime-state.js.map +1 -0
- package/dist/pipeline/agent.d.ts +2 -1
- package/dist/pipeline/agent.d.ts.map +1 -1
- package/dist/pipeline/agent.js +90 -5
- package/dist/pipeline/agent.js.map +1 -1
- package/dist/pipeline/runner.d.ts +65 -1
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +1029 -73
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/state/manager.d.ts +14 -0
- package/dist/state/manager.d.ts.map +1 -1
- package/dist/state/manager.js +114 -0
- package/dist/state/manager.js.map +1 -1
- package/dist/state/memory-db.d.ts +15 -0
- package/dist/state/memory-db.d.ts.map +1 -1
- package/dist/state/memory-db.js +119 -10
- package/dist/state/memory-db.js.map +1 -1
- package/dist/state/runtime-state-store.d.ts +23 -0
- package/dist/state/runtime-state-store.d.ts.map +1 -0
- package/dist/state/runtime-state-store.js +100 -0
- package/dist/state/runtime-state-store.js.map +1 -0
- package/dist/state/state-bootstrap.d.ts +19 -0
- package/dist/state/state-bootstrap.d.ts.map +1 -0
- package/dist/state/state-bootstrap.js +394 -0
- package/dist/state/state-bootstrap.js.map +1 -0
- package/dist/state/state-projections.d.ts +5 -0
- package/dist/state/state-projections.d.ts.map +1 -0
- package/dist/state/state-projections.js +164 -0
- package/dist/state/state-projections.js.map +1 -0
- package/dist/state/state-reducer.d.ts +12 -0
- package/dist/state/state-reducer.d.ts.map +1 -0
- package/dist/state/state-reducer.js +146 -0
- package/dist/state/state-reducer.js.map +1 -0
- package/dist/state/state-validator.d.ts +12 -0
- package/dist/state/state-validator.d.ts.map +1 -0
- package/dist/state/state-validator.js +56 -0
- package/dist/state/state-validator.js.map +1 -0
- package/dist/utils/chapter-splitter.d.ts +2 -0
- package/dist/utils/chapter-splitter.d.ts.map +1 -1
- package/dist/utils/chapter-splitter.js +22 -4
- package/dist/utils/chapter-splitter.js.map +1 -1
- package/dist/utils/config-loader.d.ts +3 -1
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/config-loader.js +14 -3
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/context-filter.js +1 -1
- package/dist/utils/context-filter.js.map +1 -1
- package/dist/utils/governed-context.d.ts +7 -0
- package/dist/utils/governed-context.d.ts.map +1 -0
- package/dist/utils/governed-context.js +22 -0
- package/dist/utils/governed-context.js.map +1 -0
- package/dist/utils/governed-working-set.d.ts +18 -0
- package/dist/utils/governed-working-set.d.ts.map +1 -0
- package/dist/utils/governed-working-set.js +295 -0
- package/dist/utils/governed-working-set.js.map +1 -0
- package/dist/utils/hook-governance.d.ts +26 -0
- package/dist/utils/hook-governance.d.ts.map +1 -0
- package/dist/utils/hook-governance.js +128 -0
- package/dist/utils/hook-governance.js.map +1 -0
- package/dist/utils/hook-health.d.ts +14 -0
- package/dist/utils/hook-health.d.ts.map +1 -0
- package/dist/utils/hook-health.js +68 -0
- package/dist/utils/hook-health.js.map +1 -0
- package/dist/utils/length-metrics.d.ts +10 -0
- package/dist/utils/length-metrics.d.ts.map +1 -0
- package/dist/utils/length-metrics.js +85 -0
- package/dist/utils/length-metrics.js.map +1 -0
- package/dist/utils/long-span-fatigue.d.ts +28 -0
- package/dist/utils/long-span-fatigue.d.ts.map +1 -0
- package/dist/utils/long-span-fatigue.js +359 -0
- package/dist/utils/long-span-fatigue.js.map +1 -0
- package/dist/utils/memory-retrieval.d.ts +39 -0
- package/dist/utils/memory-retrieval.d.ts.map +1 -0
- package/dist/utils/memory-retrieval.js +574 -0
- package/dist/utils/memory-retrieval.js.map +1 -0
- package/dist/utils/spot-fix-patches.d.ts +14 -0
- package/dist/utils/spot-fix-patches.d.ts.map +1 -0
- package/dist/utils/spot-fix-patches.js +75 -0
- package/dist/utils/spot-fix-patches.js.map +1 -0
- package/package.json +1 -1
package/dist/pipeline/runner.js
CHANGED
|
@@ -1,26 +1,72 @@
|
|
|
1
1
|
import { chatCompletion, createLLMClient } from "../llm/provider.js";
|
|
2
2
|
import { ArchitectAgent } from "../agents/architect.js";
|
|
3
|
+
import { PlannerAgent } from "../agents/planner.js";
|
|
4
|
+
import { ComposerAgent } from "../agents/composer.js";
|
|
3
5
|
import { WriterAgent } from "../agents/writer.js";
|
|
6
|
+
import { LengthNormalizerAgent } from "../agents/length-normalizer.js";
|
|
4
7
|
import { ChapterAnalyzerAgent } from "../agents/chapter-analyzer.js";
|
|
5
8
|
import { ContinuityAuditor } from "../agents/continuity.js";
|
|
6
|
-
import { ReviserAgent } from "../agents/reviser.js";
|
|
9
|
+
import { ReviserAgent, DEFAULT_REVISE_MODE } from "../agents/reviser.js";
|
|
7
10
|
import { StateValidatorAgent } from "../agents/state-validator.js";
|
|
8
11
|
import { RadarAgent } from "../agents/radar.js";
|
|
9
12
|
import { readGenreProfile } from "../agents/rules-reader.js";
|
|
10
13
|
import { analyzeAITells } from "../agents/ai-tells.js";
|
|
11
14
|
import { analyzeSensitiveWords } from "../agents/sensitive-words.js";
|
|
12
15
|
import { StateManager } from "../state/manager.js";
|
|
16
|
+
import { MemoryDB } from "../state/memory-db.js";
|
|
13
17
|
import { dispatchNotification, dispatchWebhookEvent } from "../notify/dispatcher.js";
|
|
14
|
-
import {
|
|
18
|
+
import { ChapterIntentSchema } from "../models/input-governance.js";
|
|
19
|
+
import { buildLengthSpec, countChapterLength, formatLengthCount, isOutsideHardRange, isOutsideSoftRange, resolveLengthCountingMode } from "../utils/length-metrics.js";
|
|
20
|
+
import { analyzeLongSpanFatigue } from "../utils/long-span-fatigue.js";
|
|
21
|
+
import { loadNarrativeMemorySeed, loadSnapshotCurrentStateFacts } from "../state/runtime-state-store.js";
|
|
22
|
+
import { rewriteStructuredStateFromMarkdown } from "../state/state-bootstrap.js";
|
|
23
|
+
import { readFile, readdir, writeFile, mkdir, rm } from "node:fs/promises";
|
|
15
24
|
import { join } from "node:path";
|
|
16
25
|
export class PipelineRunner {
|
|
17
26
|
state;
|
|
18
27
|
config;
|
|
19
28
|
agentClients = new Map();
|
|
29
|
+
memoryIndexFallbackWarned = false;
|
|
20
30
|
constructor(config) {
|
|
21
31
|
this.config = config;
|
|
22
32
|
this.state = new StateManager(config.projectRoot);
|
|
23
33
|
}
|
|
34
|
+
localize(language, messages) {
|
|
35
|
+
return language === "en" ? messages.en : messages.zh;
|
|
36
|
+
}
|
|
37
|
+
async resolveBookLanguage(book) {
|
|
38
|
+
if (book.language) {
|
|
39
|
+
return book.language;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const { profile } = await this.loadGenreProfile(book.genre);
|
|
43
|
+
return profile.language;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return "zh";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async resolveBookLanguageById(bookId) {
|
|
50
|
+
try {
|
|
51
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
52
|
+
return await this.resolveBookLanguage(book);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return "zh";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
languageFromLengthSpec(lengthSpec) {
|
|
59
|
+
return lengthSpec.countingMode === "en_words" ? "en" : "zh";
|
|
60
|
+
}
|
|
61
|
+
logStage(language, message) {
|
|
62
|
+
this.config.logger?.info(`${this.localize(language, { zh: "阶段:", en: "Stage: " })}${this.localize(language, message)}`);
|
|
63
|
+
}
|
|
64
|
+
logInfo(language, message) {
|
|
65
|
+
this.config.logger?.info(this.localize(language, message));
|
|
66
|
+
}
|
|
67
|
+
logWarn(language, message) {
|
|
68
|
+
this.config.logger?.warn(this.localize(language, message));
|
|
69
|
+
}
|
|
24
70
|
agentCtx(bookId) {
|
|
25
71
|
return {
|
|
26
72
|
client: this.config.client,
|
|
@@ -102,14 +148,21 @@ export class PipelineRunner {
|
|
|
102
148
|
async initBook(book) {
|
|
103
149
|
const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
|
|
104
150
|
const bookDir = this.state.bookDir(book.id);
|
|
151
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
152
|
+
this.logStage(stageLanguage, { zh: "保存书籍配置", en: "saving book config" });
|
|
105
153
|
await this.state.saveBookConfig(book.id, book);
|
|
154
|
+
this.logStage(stageLanguage, { zh: "生成基础设定", en: "generating foundation" });
|
|
106
155
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
107
156
|
const foundation = await architect.generateFoundation(book, this.config.externalContext);
|
|
108
|
-
|
|
157
|
+
this.logStage(stageLanguage, { zh: "写入基础设定文件", en: "writing foundation files" });
|
|
158
|
+
await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, book.language ?? gp.language);
|
|
159
|
+
this.logStage(stageLanguage, { zh: "初始化控制文档", en: "initializing control documents" });
|
|
160
|
+
await this.state.ensureControlDocuments(book.id, this.config.externalContext);
|
|
109
161
|
// Ensure chapters directory exists (prevents ENOENT if init was previously interrupted)
|
|
110
162
|
await mkdir(join(bookDir, "chapters"), { recursive: true });
|
|
111
163
|
await this.state.saveChapterIndex(book.id, []);
|
|
112
164
|
// Snapshot initial state so rewrite of chapter 1 can restore to pre-chapter state
|
|
165
|
+
this.logStage(stageLanguage, { zh: "创建初始快照", en: "creating initial snapshot" });
|
|
113
166
|
await this.state.snapshotState(book.id, 0);
|
|
114
167
|
}
|
|
115
168
|
/** Import external source material and generate fanfic_canon.md */
|
|
@@ -126,15 +179,23 @@ export class PipelineRunner {
|
|
|
126
179
|
/** One-step fanfic book creation: create book + import canon + generate foundation */
|
|
127
180
|
async initFanficBook(book, sourceText, sourceName, fanficMode) {
|
|
128
181
|
const bookDir = this.state.bookDir(book.id);
|
|
182
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
183
|
+
this.logStage(stageLanguage, { zh: "保存书籍配置", en: "saving book config" });
|
|
129
184
|
await this.state.saveBookConfig(book.id, book);
|
|
130
185
|
// Step 1: Import source material → fanfic_canon.md
|
|
186
|
+
this.logStage(stageLanguage, { zh: "导入同人正典", en: "importing fanfic canon" });
|
|
131
187
|
const fanficCanon = await this.importFanficCanon(book.id, sourceText, sourceName, fanficMode);
|
|
132
188
|
// Step 2: Generate foundation from fanfic canon (not from scratch)
|
|
133
189
|
const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
|
|
190
|
+
this.logStage(stageLanguage, { zh: "生成同人基础设定", en: "generating fanfic foundation" });
|
|
134
191
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
135
192
|
const foundation = await architect.generateFanficFoundation(book, fanficCanon, fanficMode);
|
|
136
|
-
|
|
193
|
+
this.logStage(stageLanguage, { zh: "写入基础设定文件", en: "writing foundation files" });
|
|
194
|
+
await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, book.language ?? gp.language);
|
|
195
|
+
this.logStage(stageLanguage, { zh: "初始化控制文档", en: "initializing control documents" });
|
|
196
|
+
await this.state.ensureControlDocuments(book.id, this.config.externalContext);
|
|
137
197
|
// Step 3: Initialize chapters directory + snapshot
|
|
198
|
+
this.logStage(stageLanguage, { zh: "创建初始快照", en: "creating initial snapshot" });
|
|
138
199
|
await mkdir(join(bookDir, "chapters"), { recursive: true });
|
|
139
200
|
await this.state.saveChapterIndex(book.id, []);
|
|
140
201
|
await this.state.snapshotState(book.id, 0);
|
|
@@ -143,58 +204,147 @@ export class PipelineRunner {
|
|
|
143
204
|
async writeDraft(bookId, context, wordCount) {
|
|
144
205
|
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
145
206
|
try {
|
|
207
|
+
await this.state.ensureControlDocuments(bookId);
|
|
146
208
|
const book = await this.state.loadBookConfig(bookId);
|
|
147
209
|
const bookDir = this.state.bookDir(bookId);
|
|
148
210
|
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
211
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
212
|
+
this.logStage(stageLanguage, { zh: "准备章节输入", en: "preparing chapter inputs" });
|
|
213
|
+
const writeInput = await this.prepareWriteInput(book, bookDir, chapterNumber, context ?? this.config.externalContext);
|
|
149
214
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
215
|
+
const lengthSpec = buildLengthSpec(wordCount ?? book.chapterWordCount, book.language ?? gp.language);
|
|
150
216
|
const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
|
|
217
|
+
this.logStage(stageLanguage, { zh: "撰写章节草稿", en: "writing chapter draft" });
|
|
151
218
|
const output = await writer.writeChapter({
|
|
152
219
|
book,
|
|
153
220
|
bookDir,
|
|
154
221
|
chapterNumber,
|
|
155
|
-
|
|
222
|
+
...writeInput,
|
|
223
|
+
lengthSpec,
|
|
156
224
|
...(wordCount ? { wordCountOverride: wordCount } : {}),
|
|
157
225
|
});
|
|
226
|
+
const writerCount = countChapterLength(output.content, lengthSpec.countingMode);
|
|
227
|
+
let totalUsage = output.tokenUsage ?? {
|
|
228
|
+
promptTokens: 0,
|
|
229
|
+
completionTokens: 0,
|
|
230
|
+
totalTokens: 0,
|
|
231
|
+
};
|
|
232
|
+
const normalizedDraft = await this.normalizeDraftLengthIfNeeded({
|
|
233
|
+
bookId,
|
|
234
|
+
chapterNumber,
|
|
235
|
+
chapterContent: output.content,
|
|
236
|
+
lengthSpec,
|
|
237
|
+
chapterIntent: writeInput.chapterIntent,
|
|
238
|
+
});
|
|
239
|
+
totalUsage = PipelineRunner.addUsage(totalUsage, normalizedDraft.tokenUsage);
|
|
240
|
+
const draftOutput = {
|
|
241
|
+
...output,
|
|
242
|
+
content: normalizedDraft.content,
|
|
243
|
+
wordCount: normalizedDraft.wordCount,
|
|
244
|
+
tokenUsage: totalUsage,
|
|
245
|
+
};
|
|
246
|
+
const lengthWarnings = this.buildLengthWarnings(chapterNumber, draftOutput.wordCount, lengthSpec);
|
|
247
|
+
const lengthTelemetry = this.buildLengthTelemetry({
|
|
248
|
+
lengthSpec,
|
|
249
|
+
writerCount,
|
|
250
|
+
postWriterNormalizeCount: normalizedDraft.wordCount,
|
|
251
|
+
postReviseCount: 0,
|
|
252
|
+
finalCount: draftOutput.wordCount,
|
|
253
|
+
normalizeApplied: normalizedDraft.applied,
|
|
254
|
+
lengthWarning: lengthWarnings.length > 0,
|
|
255
|
+
});
|
|
256
|
+
this.logLengthWarnings(lengthWarnings);
|
|
158
257
|
// Save chapter file
|
|
159
258
|
const chaptersDir = join(bookDir, "chapters");
|
|
160
259
|
const paddedNum = String(chapterNumber).padStart(4, "0");
|
|
161
|
-
const sanitized =
|
|
260
|
+
const sanitized = draftOutput.title.replace(/[/\\?%*:|"<>]/g, "").replace(/\s+/g, "_").slice(0, 50);
|
|
162
261
|
const filename = `${paddedNum}_${sanitized}.md`;
|
|
163
262
|
const filePath = join(chaptersDir, filename);
|
|
164
263
|
const resolvedLang = book.language ?? gp.language;
|
|
165
264
|
const heading = resolvedLang === "en"
|
|
166
|
-
? `# Chapter ${chapterNumber}: ${
|
|
167
|
-
: `# 第${chapterNumber}章 ${
|
|
168
|
-
await writeFile(filePath, `${heading}\n\n${
|
|
265
|
+
? `# Chapter ${chapterNumber}: ${draftOutput.title}`
|
|
266
|
+
: `# 第${chapterNumber}章 ${draftOutput.title}`;
|
|
267
|
+
await writeFile(filePath, `${heading}\n\n${draftOutput.content}`, "utf-8");
|
|
169
268
|
// Save truth files
|
|
170
|
-
|
|
171
|
-
await writer.
|
|
269
|
+
this.logStage(stageLanguage, { zh: "落盘草稿与真相文件", en: "persisting draft and truth files" });
|
|
270
|
+
await writer.saveChapter(bookDir, draftOutput, gp.numericalSystem, resolvedLang);
|
|
271
|
+
await writer.saveNewTruthFiles(bookDir, draftOutput, resolvedLang);
|
|
272
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, draftOutput);
|
|
273
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
172
274
|
// Update index
|
|
173
275
|
const existingIndex = await this.state.loadChapterIndex(bookId);
|
|
174
276
|
const now = new Date().toISOString();
|
|
175
277
|
const newEntry = {
|
|
176
278
|
number: chapterNumber,
|
|
177
|
-
title:
|
|
279
|
+
title: draftOutput.title,
|
|
178
280
|
status: "drafted",
|
|
179
|
-
wordCount:
|
|
281
|
+
wordCount: draftOutput.wordCount,
|
|
180
282
|
createdAt: now,
|
|
181
283
|
updatedAt: now,
|
|
182
284
|
auditIssues: [],
|
|
183
|
-
|
|
285
|
+
lengthWarnings,
|
|
286
|
+
lengthTelemetry,
|
|
287
|
+
...(draftOutput.tokenUsage ? { tokenUsage: draftOutput.tokenUsage } : {}),
|
|
184
288
|
};
|
|
185
289
|
await this.state.saveChapterIndex(bookId, [...existingIndex, newEntry]);
|
|
290
|
+
await this.markBookActiveIfNeeded(bookId);
|
|
186
291
|
// Snapshot
|
|
292
|
+
this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" });
|
|
187
293
|
await this.state.snapshotState(bookId, chapterNumber);
|
|
294
|
+
await this.syncCurrentStateFactHistory(bookId, chapterNumber);
|
|
188
295
|
await this.emitWebhook("chapter-complete", bookId, chapterNumber, {
|
|
189
|
-
title:
|
|
190
|
-
wordCount:
|
|
296
|
+
title: draftOutput.title,
|
|
297
|
+
wordCount: draftOutput.wordCount,
|
|
191
298
|
});
|
|
192
|
-
return {
|
|
299
|
+
return {
|
|
300
|
+
chapterNumber,
|
|
301
|
+
title: draftOutput.title,
|
|
302
|
+
wordCount: draftOutput.wordCount,
|
|
303
|
+
filePath,
|
|
304
|
+
lengthWarnings,
|
|
305
|
+
lengthTelemetry,
|
|
306
|
+
tokenUsage: draftOutput.tokenUsage,
|
|
307
|
+
};
|
|
193
308
|
}
|
|
194
309
|
finally {
|
|
195
310
|
await releaseLock();
|
|
196
311
|
}
|
|
197
312
|
}
|
|
313
|
+
async planChapter(bookId, context) {
|
|
314
|
+
await this.state.ensureControlDocuments(bookId);
|
|
315
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
316
|
+
const bookDir = this.state.bookDir(bookId);
|
|
317
|
+
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
318
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
319
|
+
this.logStage(stageLanguage, { zh: "规划下一章意图", en: "planning next chapter intent" });
|
|
320
|
+
const { plan } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, context ?? this.config.externalContext, { reuseExistingIntentWhenContextMissing: false });
|
|
321
|
+
return {
|
|
322
|
+
bookId,
|
|
323
|
+
chapterNumber,
|
|
324
|
+
intentPath: this.relativeToBookDir(bookDir, plan.runtimePath),
|
|
325
|
+
goal: plan.intent.goal,
|
|
326
|
+
conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async composeChapter(bookId, context) {
|
|
330
|
+
await this.state.ensureControlDocuments(bookId);
|
|
331
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
332
|
+
const bookDir = this.state.bookDir(bookId);
|
|
333
|
+
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
334
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
335
|
+
this.logStage(stageLanguage, { zh: "组装章节运行时上下文", en: "composing chapter runtime context" });
|
|
336
|
+
const { plan, composed } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, context ?? this.config.externalContext, { reuseExistingIntentWhenContextMissing: true });
|
|
337
|
+
return {
|
|
338
|
+
bookId,
|
|
339
|
+
chapterNumber,
|
|
340
|
+
intentPath: this.relativeToBookDir(bookDir, plan.runtimePath),
|
|
341
|
+
goal: plan.intent.goal,
|
|
342
|
+
conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
|
|
343
|
+
contextPath: this.relativeToBookDir(bookDir, composed.contextPath),
|
|
344
|
+
ruleStackPath: this.relativeToBookDir(bookDir, composed.ruleStackPath),
|
|
345
|
+
tracePath: this.relativeToBookDir(bookDir, composed.tracePath),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
198
348
|
/** Audit the latest (or specified) chapter. Read-only, no lock needed. */
|
|
199
349
|
async auditDraft(bookId, chapterNumber) {
|
|
200
350
|
const book = await this.state.loadBookConfig(bookId);
|
|
@@ -205,22 +355,21 @@ export class PipelineRunner {
|
|
|
205
355
|
}
|
|
206
356
|
const content = await this.readChapterContent(bookDir, targetChapter);
|
|
207
357
|
const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
};
|
|
358
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
359
|
+
const language = book.language ?? gp.language;
|
|
360
|
+
this.logStage(language, {
|
|
361
|
+
zh: `审计第${targetChapter}章`,
|
|
362
|
+
en: `auditing chapter ${targetChapter}`,
|
|
363
|
+
});
|
|
364
|
+
const evaluation = await this.evaluateMergedAudit({
|
|
365
|
+
auditor,
|
|
366
|
+
book,
|
|
367
|
+
bookDir,
|
|
368
|
+
chapterContent: content,
|
|
369
|
+
chapterNumber: targetChapter,
|
|
370
|
+
language,
|
|
371
|
+
});
|
|
372
|
+
const result = evaluation.auditResult;
|
|
224
373
|
// Update index with audit result
|
|
225
374
|
const index = await this.state.loadChapterIndex(bookId);
|
|
226
375
|
const updated = index.map((ch) => ch.number === targetChapter
|
|
@@ -236,7 +385,7 @@ export class PipelineRunner {
|
|
|
236
385
|
return { ...result, chapterNumber: targetChapter };
|
|
237
386
|
}
|
|
238
387
|
/** Revise the latest (or specified) chapter based on audit issues. */
|
|
239
|
-
async reviseDraft(bookId, chapterNumber, mode =
|
|
388
|
+
async reviseDraft(bookId, chapterNumber, mode = DEFAULT_REVISE_MODE) {
|
|
240
389
|
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
241
390
|
try {
|
|
242
391
|
const book = await this.state.loadBookConfig(bookId);
|
|
@@ -245,7 +394,12 @@ export class PipelineRunner {
|
|
|
245
394
|
if (targetChapter < 1) {
|
|
246
395
|
throw new Error(`No chapters to revise for "${bookId}"`);
|
|
247
396
|
}
|
|
397
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
248
398
|
// Read the current audit issues from index
|
|
399
|
+
this.logStage(stageLanguage, {
|
|
400
|
+
zh: `加载第${targetChapter}章修订上下文`,
|
|
401
|
+
en: `loading revision context for chapter ${targetChapter}`,
|
|
402
|
+
});
|
|
249
403
|
const index = await this.state.loadChapterIndex(bookId);
|
|
250
404
|
const chapterMeta = index.find((ch) => ch.number === targetChapter);
|
|
251
405
|
if (!chapterMeta) {
|
|
@@ -254,17 +408,129 @@ export class PipelineRunner {
|
|
|
254
408
|
// Re-audit to get structured issues (index only stores strings)
|
|
255
409
|
const content = await this.readChapterContent(bookDir, targetChapter);
|
|
256
410
|
const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
|
|
257
|
-
const auditResult = await auditor.auditChapter(bookDir, content, targetChapter, book.genre);
|
|
258
|
-
if (auditResult.passed && auditResult.issues.filter(i => i.severity === "warning" || i.severity === "critical").length === 0) {
|
|
259
|
-
return { chapterNumber: targetChapter, wordCount: content.length, fixedIssues: [] };
|
|
260
|
-
}
|
|
261
411
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
412
|
+
const language = book.language ?? gp.language;
|
|
413
|
+
const countingMode = resolveLengthCountingMode(language);
|
|
414
|
+
const reviseControlInput = (this.config.inputGovernanceMode ?? "v2") === "legacy"
|
|
415
|
+
? undefined
|
|
416
|
+
: await this.createGovernedArtifacts(book, bookDir, targetChapter, undefined, { reuseExistingIntentWhenContextMissing: true });
|
|
417
|
+
const preRevision = await this.evaluateMergedAudit({
|
|
418
|
+
auditor,
|
|
419
|
+
book,
|
|
420
|
+
bookDir,
|
|
421
|
+
chapterContent: content,
|
|
422
|
+
chapterNumber: targetChapter,
|
|
423
|
+
language,
|
|
424
|
+
auditOptions: reviseControlInput
|
|
425
|
+
? {
|
|
426
|
+
chapterIntent: reviseControlInput.plan.intentMarkdown,
|
|
427
|
+
contextPackage: reviseControlInput.composed.contextPackage,
|
|
428
|
+
ruleStack: reviseControlInput.composed.ruleStack,
|
|
429
|
+
}
|
|
430
|
+
: undefined,
|
|
431
|
+
});
|
|
432
|
+
if (preRevision.blockingCount === 0 && preRevision.aiTellCount === 0) {
|
|
433
|
+
return {
|
|
434
|
+
chapterNumber: targetChapter,
|
|
435
|
+
wordCount: countChapterLength(content, countingMode),
|
|
436
|
+
fixedIssues: [],
|
|
437
|
+
applied: false,
|
|
438
|
+
status: "unchanged",
|
|
439
|
+
skippedReason: "No warning, critical, or AI-tell issues to fix.",
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
const chapterLengthTarget = chapterMeta.lengthTelemetry?.target ?? book.chapterWordCount;
|
|
443
|
+
const lengthLanguage = chapterMeta.lengthTelemetry?.countingMode === "en_words"
|
|
444
|
+
? "en"
|
|
445
|
+
: language;
|
|
446
|
+
const lengthSpec = buildLengthSpec(chapterLengthTarget, lengthLanguage);
|
|
262
447
|
const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
|
|
263
|
-
|
|
448
|
+
this.logStage(stageLanguage, {
|
|
449
|
+
zh: `修订第${targetChapter}章`,
|
|
450
|
+
en: `revising chapter ${targetChapter}`,
|
|
451
|
+
});
|
|
452
|
+
const reviseOutput = await reviser.reviseChapter(bookDir, content, targetChapter, preRevision.auditResult.issues, mode, book.genre, reviseControlInput
|
|
453
|
+
? {
|
|
454
|
+
chapterIntent: reviseControlInput.plan.intentMarkdown,
|
|
455
|
+
contextPackage: reviseControlInput.composed.contextPackage,
|
|
456
|
+
ruleStack: reviseControlInput.composed.ruleStack,
|
|
457
|
+
lengthSpec,
|
|
458
|
+
}
|
|
459
|
+
: { lengthSpec });
|
|
264
460
|
if (reviseOutput.revisedContent.length === 0) {
|
|
265
461
|
throw new Error("Reviser returned empty content");
|
|
266
462
|
}
|
|
463
|
+
const normalizedRevision = await this.normalizeDraftLengthIfNeeded({
|
|
464
|
+
bookId,
|
|
465
|
+
chapterNumber: targetChapter,
|
|
466
|
+
chapterContent: reviseOutput.revisedContent,
|
|
467
|
+
lengthSpec,
|
|
468
|
+
});
|
|
469
|
+
const postRevision = await this.evaluateMergedAudit({
|
|
470
|
+
auditor,
|
|
471
|
+
book,
|
|
472
|
+
bookDir,
|
|
473
|
+
chapterContent: normalizedRevision.content,
|
|
474
|
+
chapterNumber: targetChapter,
|
|
475
|
+
language,
|
|
476
|
+
auditOptions: reviseControlInput
|
|
477
|
+
? {
|
|
478
|
+
temperature: 0,
|
|
479
|
+
chapterIntent: reviseControlInput.plan.intentMarkdown,
|
|
480
|
+
contextPackage: reviseControlInput.composed.contextPackage,
|
|
481
|
+
ruleStack: reviseControlInput.composed.ruleStack,
|
|
482
|
+
truthFileOverrides: {
|
|
483
|
+
currentState: reviseOutput.updatedState !== "(状态卡未更新)" ? reviseOutput.updatedState : undefined,
|
|
484
|
+
ledger: reviseOutput.updatedLedger !== "(账本未更新)" ? reviseOutput.updatedLedger : undefined,
|
|
485
|
+
hooks: reviseOutput.updatedHooks !== "(伏笔池未更新)" ? reviseOutput.updatedHooks : undefined,
|
|
486
|
+
},
|
|
487
|
+
}
|
|
488
|
+
: {
|
|
489
|
+
temperature: 0,
|
|
490
|
+
truthFileOverrides: {
|
|
491
|
+
currentState: reviseOutput.updatedState !== "(状态卡未更新)" ? reviseOutput.updatedState : undefined,
|
|
492
|
+
ledger: reviseOutput.updatedLedger !== "(账本未更新)" ? reviseOutput.updatedLedger : undefined,
|
|
493
|
+
hooks: reviseOutput.updatedHooks !== "(伏笔池未更新)" ? reviseOutput.updatedHooks : undefined,
|
|
494
|
+
},
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
const effectivePostRevision = this.restoreActionableAuditIfLost(preRevision, postRevision);
|
|
498
|
+
const revisionBaseCount = countChapterLength(content, lengthSpec.countingMode);
|
|
499
|
+
const lengthWarnings = this.buildLengthWarnings(targetChapter, normalizedRevision.wordCount, lengthSpec);
|
|
500
|
+
const lengthTelemetry = this.buildLengthTelemetry({
|
|
501
|
+
lengthSpec,
|
|
502
|
+
writerCount: revisionBaseCount,
|
|
503
|
+
postWriterNormalizeCount: 0,
|
|
504
|
+
postReviseCount: normalizedRevision.wordCount,
|
|
505
|
+
finalCount: normalizedRevision.wordCount,
|
|
506
|
+
normalizeApplied: normalizedRevision.applied,
|
|
507
|
+
lengthWarning: lengthWarnings.length > 0,
|
|
508
|
+
});
|
|
509
|
+
const improvedBlocking = effectivePostRevision.blockingCount < preRevision.blockingCount;
|
|
510
|
+
const improvedAITells = effectivePostRevision.aiTellCount < preRevision.aiTellCount;
|
|
511
|
+
const blockingDidNotWorsen = effectivePostRevision.blockingCount <= preRevision.blockingCount;
|
|
512
|
+
const criticalDidNotWorsen = effectivePostRevision.criticalCount <= preRevision.criticalCount;
|
|
513
|
+
const aiDidNotWorsen = effectivePostRevision.aiTellCount <= preRevision.aiTellCount;
|
|
514
|
+
const shouldApplyRevision = blockingDidNotWorsen
|
|
515
|
+
&& criticalDidNotWorsen
|
|
516
|
+
&& aiDidNotWorsen
|
|
517
|
+
&& (improvedBlocking || improvedAITells);
|
|
518
|
+
if (!shouldApplyRevision) {
|
|
519
|
+
return {
|
|
520
|
+
chapterNumber: targetChapter,
|
|
521
|
+
wordCount: revisionBaseCount,
|
|
522
|
+
fixedIssues: [],
|
|
523
|
+
applied: false,
|
|
524
|
+
status: "unchanged",
|
|
525
|
+
skippedReason: "Manual revision did not improve merged audit or AI-tell metrics; kept original chapter.",
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
this.logLengthWarnings(lengthWarnings);
|
|
267
529
|
// Save revised chapter file
|
|
530
|
+
this.logStage(stageLanguage, {
|
|
531
|
+
zh: `落盘第${targetChapter}章修订结果`,
|
|
532
|
+
en: `persisting revision for chapter ${targetChapter}`,
|
|
533
|
+
});
|
|
268
534
|
const chaptersDir = join(bookDir, "chapters");
|
|
269
535
|
const files = await readdir(chaptersDir);
|
|
270
536
|
const paddedNum = String(targetChapter).padStart(4, "0");
|
|
@@ -276,7 +542,7 @@ export class PipelineRunner {
|
|
|
276
542
|
const reviseHeading = reviseLang === "en"
|
|
277
543
|
? `# Chapter ${targetChapter}: ${chapterMeta.title}`
|
|
278
544
|
: `# 第${targetChapter}章 ${chapterMeta.title}`;
|
|
279
|
-
await writeFile(join(chaptersDir, existingFile), `${reviseHeading}\n\n${
|
|
545
|
+
await writeFile(join(chaptersDir, existingFile), `${reviseHeading}\n\n${normalizedRevision.content}`, "utf-8");
|
|
280
546
|
// Update truth files
|
|
281
547
|
const storyDir = join(bookDir, "story");
|
|
282
548
|
if (reviseOutput.updatedState !== "(状态卡未更新)") {
|
|
@@ -288,26 +554,40 @@ export class PipelineRunner {
|
|
|
288
554
|
if (reviseOutput.updatedHooks !== "(伏笔池未更新)") {
|
|
289
555
|
await writeFile(join(storyDir, "pending_hooks.md"), reviseOutput.updatedHooks, "utf-8");
|
|
290
556
|
}
|
|
557
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, targetChapter);
|
|
291
558
|
// Update index
|
|
292
559
|
const updatedIndex = index.map((ch) => ch.number === targetChapter
|
|
293
560
|
? {
|
|
294
561
|
...ch,
|
|
295
|
-
status: "ready-for-review",
|
|
296
|
-
wordCount:
|
|
562
|
+
status: (effectivePostRevision.auditResult.passed ? "ready-for-review" : "audit-failed"),
|
|
563
|
+
wordCount: normalizedRevision.wordCount,
|
|
297
564
|
updatedAt: new Date().toISOString(),
|
|
565
|
+
auditIssues: effectivePostRevision.auditResult.issues.map((i) => `[${i.severity}] ${i.description}`),
|
|
566
|
+
lengthWarnings,
|
|
567
|
+
lengthTelemetry,
|
|
298
568
|
}
|
|
299
569
|
: ch);
|
|
300
570
|
await this.state.saveChapterIndex(bookId, updatedIndex);
|
|
301
571
|
// Re-snapshot
|
|
572
|
+
this.logStage(stageLanguage, {
|
|
573
|
+
zh: `更新第${targetChapter}章索引与快照`,
|
|
574
|
+
en: `updating chapter index and snapshots for chapter ${targetChapter}`,
|
|
575
|
+
});
|
|
302
576
|
await this.state.snapshotState(bookId, targetChapter);
|
|
577
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
578
|
+
await this.syncCurrentStateFactHistory(bookId, targetChapter);
|
|
303
579
|
await this.emitWebhook("revision-complete", bookId, targetChapter, {
|
|
304
|
-
wordCount:
|
|
580
|
+
wordCount: normalizedRevision.wordCount,
|
|
305
581
|
fixedCount: reviseOutput.fixedIssues.length,
|
|
306
582
|
});
|
|
307
583
|
return {
|
|
308
584
|
chapterNumber: targetChapter,
|
|
309
|
-
wordCount:
|
|
585
|
+
wordCount: normalizedRevision.wordCount,
|
|
310
586
|
fixedIssues: reviseOutput.fixedIssues,
|
|
587
|
+
applied: true,
|
|
588
|
+
status: effectivePostRevision.auditResult.passed ? "ready-for-review" : "audit-failed",
|
|
589
|
+
lengthWarnings,
|
|
590
|
+
lengthTelemetry,
|
|
311
591
|
};
|
|
312
592
|
}
|
|
313
593
|
finally {
|
|
@@ -367,28 +647,49 @@ export class PipelineRunner {
|
|
|
367
647
|
}
|
|
368
648
|
}
|
|
369
649
|
async _writeNextChapterLocked(bookId, wordCount, temperatureOverride) {
|
|
650
|
+
await this.state.ensureControlDocuments(bookId);
|
|
370
651
|
const book = await this.state.loadBookConfig(bookId);
|
|
371
652
|
const bookDir = this.state.bookDir(bookId);
|
|
372
653
|
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
654
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
655
|
+
this.logStage(stageLanguage, { zh: "准备章节输入", en: "preparing chapter inputs" });
|
|
656
|
+
const writeInput = await this.prepareWriteInput(book, bookDir, chapterNumber, this.config.externalContext);
|
|
657
|
+
const reducedControlInput = writeInput.chapterIntent && writeInput.contextPackage && writeInput.ruleStack
|
|
658
|
+
? {
|
|
659
|
+
chapterIntent: writeInput.chapterIntent,
|
|
660
|
+
contextPackage: writeInput.contextPackage,
|
|
661
|
+
ruleStack: writeInput.ruleStack,
|
|
662
|
+
}
|
|
663
|
+
: undefined;
|
|
373
664
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
665
|
+
const pipelineLang = book.language ?? gp.language;
|
|
666
|
+
const lengthSpec = buildLengthSpec(wordCount ?? book.chapterWordCount, pipelineLang);
|
|
374
667
|
// 1. Write chapter
|
|
375
668
|
const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
|
|
669
|
+
this.logStage(stageLanguage, { zh: "撰写章节草稿", en: "writing chapter draft" });
|
|
376
670
|
const output = await writer.writeChapter({
|
|
377
671
|
book,
|
|
378
672
|
bookDir,
|
|
379
673
|
chapterNumber,
|
|
380
|
-
|
|
674
|
+
...writeInput,
|
|
675
|
+
lengthSpec,
|
|
381
676
|
...(wordCount ? { wordCountOverride: wordCount } : {}),
|
|
382
677
|
...(temperatureOverride ? { temperatureOverride } : {}),
|
|
383
678
|
});
|
|
679
|
+
const writerCount = countChapterLength(output.content, lengthSpec.countingMode);
|
|
384
680
|
// Token usage accumulator
|
|
385
681
|
let totalUsage = output.tokenUsage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
682
|
+
let postReviseCount = 0;
|
|
683
|
+
let normalizeApplied = false;
|
|
386
684
|
// 2a. Post-write error gate: if deterministic rules found errors, auto-fix before LLM audit
|
|
387
685
|
let finalContent = output.content;
|
|
388
686
|
let finalWordCount = output.wordCount;
|
|
389
687
|
let revised = false;
|
|
390
688
|
if (output.postWriteErrors.length > 0) {
|
|
391
|
-
this.
|
|
689
|
+
this.logWarn(pipelineLang, {
|
|
690
|
+
zh: `检测到 ${output.postWriteErrors.length} 个后写错误,审计前触发 spot-fix 修补`,
|
|
691
|
+
en: `${output.postWriteErrors.length} post-write errors detected, triggering spot-fix before audit`,
|
|
692
|
+
});
|
|
392
693
|
const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
|
|
393
694
|
const spotFixIssues = output.postWriteErrors.map((v) => ({
|
|
394
695
|
severity: "critical",
|
|
@@ -396,7 +697,10 @@ export class PipelineRunner {
|
|
|
396
697
|
description: v.description,
|
|
397
698
|
suggestion: v.suggestion,
|
|
398
699
|
}));
|
|
399
|
-
const fixResult = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, spotFixIssues, "spot-fix", book.genre
|
|
700
|
+
const fixResult = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, spotFixIssues, "spot-fix", book.genre, {
|
|
701
|
+
...reducedControlInput,
|
|
702
|
+
lengthSpec,
|
|
703
|
+
});
|
|
400
704
|
totalUsage = PipelineRunner.addUsage(totalUsage, fixResult.tokenUsage);
|
|
401
705
|
if (fixResult.revisedContent.length > 0) {
|
|
402
706
|
finalContent = fixResult.revisedContent;
|
|
@@ -404,9 +708,21 @@ export class PipelineRunner {
|
|
|
404
708
|
revised = true;
|
|
405
709
|
}
|
|
406
710
|
}
|
|
711
|
+
const normalizedBeforeAudit = await this.normalizeDraftLengthIfNeeded({
|
|
712
|
+
bookId,
|
|
713
|
+
chapterNumber,
|
|
714
|
+
chapterContent: finalContent,
|
|
715
|
+
lengthSpec,
|
|
716
|
+
chapterIntent: writeInput.chapterIntent,
|
|
717
|
+
});
|
|
718
|
+
totalUsage = PipelineRunner.addUsage(totalUsage, normalizedBeforeAudit.tokenUsage);
|
|
719
|
+
finalContent = normalizedBeforeAudit.content;
|
|
720
|
+
finalWordCount = normalizedBeforeAudit.wordCount;
|
|
721
|
+
normalizeApplied = normalizeApplied || normalizedBeforeAudit.applied;
|
|
407
722
|
// 2b. LLM audit
|
|
408
723
|
const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
|
|
409
|
-
|
|
724
|
+
this.logStage(stageLanguage, { zh: "审计草稿", en: "auditing draft" });
|
|
725
|
+
const llmAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, reducedControlInput);
|
|
410
726
|
totalUsage = PipelineRunner.addUsage(totalUsage, llmAudit.tokenUsage);
|
|
411
727
|
const aiTellsResult = analyzeAITells(finalContent);
|
|
412
728
|
const sensitiveWriteResult = analyzeSensitiveWords(finalContent);
|
|
@@ -421,42 +737,84 @@ export class PipelineRunner {
|
|
|
421
737
|
const criticalIssues = auditResult.issues.filter((i) => i.severity === "critical");
|
|
422
738
|
if (criticalIssues.length > 0) {
|
|
423
739
|
const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
|
|
424
|
-
|
|
740
|
+
this.logStage(stageLanguage, { zh: "自动修复关键问题", en: "auto-revising critical issues" });
|
|
741
|
+
const reviseOutput = await reviser.reviseChapter(bookDir, finalContent, chapterNumber, auditResult.issues, "spot-fix", book.genre, {
|
|
742
|
+
...reducedControlInput,
|
|
743
|
+
lengthSpec,
|
|
744
|
+
});
|
|
425
745
|
totalUsage = PipelineRunner.addUsage(totalUsage, reviseOutput.tokenUsage);
|
|
426
746
|
if (reviseOutput.revisedContent.length > 0) {
|
|
747
|
+
const normalizedRevision = await this.normalizeDraftLengthIfNeeded({
|
|
748
|
+
bookId,
|
|
749
|
+
chapterNumber,
|
|
750
|
+
chapterContent: reviseOutput.revisedContent,
|
|
751
|
+
lengthSpec,
|
|
752
|
+
chapterIntent: writeInput.chapterIntent,
|
|
753
|
+
});
|
|
754
|
+
totalUsage = PipelineRunner.addUsage(totalUsage, normalizedRevision.tokenUsage);
|
|
755
|
+
postReviseCount = normalizedRevision.wordCount;
|
|
756
|
+
normalizeApplied = normalizeApplied || normalizedRevision.applied;
|
|
427
757
|
// Guard: reject revision if AI markers increased
|
|
428
758
|
const preMarkers = analyzeAITells(finalContent);
|
|
429
|
-
const postMarkers = analyzeAITells(
|
|
759
|
+
const postMarkers = analyzeAITells(normalizedRevision.content);
|
|
430
760
|
const preCount = preMarkers.issues.length;
|
|
431
761
|
const postCount = postMarkers.issues.length;
|
|
432
762
|
if (postCount > preCount) {
|
|
433
763
|
// Revision made text MORE AI-like — discard it, keep original
|
|
434
764
|
}
|
|
435
765
|
else {
|
|
436
|
-
finalContent =
|
|
437
|
-
finalWordCount =
|
|
766
|
+
finalContent = normalizedRevision.content;
|
|
767
|
+
finalWordCount = normalizedRevision.wordCount;
|
|
438
768
|
revised = true;
|
|
439
769
|
}
|
|
440
770
|
// Re-audit the (possibly revised) content
|
|
441
|
-
const reAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, { temperature: 0 });
|
|
771
|
+
const reAudit = await auditor.auditChapter(bookDir, finalContent, chapterNumber, book.genre, { ...reducedControlInput, temperature: 0 });
|
|
442
772
|
totalUsage = PipelineRunner.addUsage(totalUsage, reAudit.tokenUsage);
|
|
443
773
|
const reAITells = analyzeAITells(finalContent);
|
|
444
774
|
const reSensitive = analyzeSensitiveWords(finalContent);
|
|
445
775
|
const reHasBlocked = reSensitive.found.some((f) => f.severity === "block");
|
|
446
|
-
auditResult = {
|
|
776
|
+
auditResult = this.restoreLostAuditIssues(auditResult, {
|
|
447
777
|
passed: reHasBlocked ? false : reAudit.passed,
|
|
448
778
|
issues: [...reAudit.issues, ...reAITells.issues, ...reSensitive.issues],
|
|
449
779
|
summary: reAudit.summary,
|
|
450
|
-
};
|
|
780
|
+
});
|
|
451
781
|
}
|
|
452
782
|
}
|
|
453
783
|
}
|
|
454
784
|
// 4. Save the final chapter and truth files from a single persistence source
|
|
785
|
+
this.logStage(stageLanguage, { zh: "落盘最终章节", en: "persisting final chapter" });
|
|
786
|
+
this.logStage(stageLanguage, { zh: "生成最终真相文件", en: "rebuilding final truth files" });
|
|
455
787
|
const persistenceOutput = await this.buildPersistenceOutput(bookId, book, bookDir, chapterNumber, output, finalContent);
|
|
788
|
+
const longSpanFatigue = await analyzeLongSpanFatigue({
|
|
789
|
+
bookDir,
|
|
790
|
+
chapterNumber,
|
|
791
|
+
chapterContent: finalContent,
|
|
792
|
+
chapterSummary: persistenceOutput.chapterSummary,
|
|
793
|
+
language: pipelineLang,
|
|
794
|
+
});
|
|
795
|
+
auditResult = {
|
|
796
|
+
...auditResult,
|
|
797
|
+
issues: [
|
|
798
|
+
...auditResult.issues,
|
|
799
|
+
...longSpanFatigue.issues,
|
|
800
|
+
...(persistenceOutput.hookHealthIssues ?? []),
|
|
801
|
+
],
|
|
802
|
+
};
|
|
456
803
|
finalWordCount = persistenceOutput.wordCount;
|
|
457
|
-
const
|
|
804
|
+
const lengthWarnings = this.buildLengthWarnings(chapterNumber, finalWordCount, lengthSpec);
|
|
805
|
+
const lengthTelemetry = this.buildLengthTelemetry({
|
|
806
|
+
lengthSpec,
|
|
807
|
+
writerCount,
|
|
808
|
+
postWriterNormalizeCount: normalizedBeforeAudit.wordCount,
|
|
809
|
+
postReviseCount,
|
|
810
|
+
finalCount: finalWordCount,
|
|
811
|
+
normalizeApplied,
|
|
812
|
+
lengthWarning: lengthWarnings.length > 0,
|
|
813
|
+
});
|
|
814
|
+
this.logLengthWarnings(lengthWarnings);
|
|
458
815
|
// 4.1 Validate settler output before writing (non-blocking)
|
|
459
816
|
try {
|
|
817
|
+
this.logStage(stageLanguage, { zh: "校验真相文件变更", en: "validating truth file updates" });
|
|
460
818
|
const storyDir = join(bookDir, "story");
|
|
461
819
|
const [oldState, oldHooks] = await Promise.all([
|
|
462
820
|
readFile(join(storyDir, "current_state.md"), "utf-8").catch(() => ""),
|
|
@@ -465,17 +823,26 @@ export class PipelineRunner {
|
|
|
465
823
|
const validator = new StateValidatorAgent(this.agentCtxFor("state-validator", bookId));
|
|
466
824
|
const validation = await validator.validate(finalContent, chapterNumber, oldState, persistenceOutput.updatedState, oldHooks, persistenceOutput.updatedHooks, pipelineLang);
|
|
467
825
|
if (validation.warnings.length > 0) {
|
|
468
|
-
this.
|
|
826
|
+
this.logWarn(pipelineLang, {
|
|
827
|
+
zh: `状态校验:第${chapterNumber}章发现 ${validation.warnings.length} 条警告`,
|
|
828
|
+
en: `State validation: ${validation.warnings.length} warning(s) for chapter ${chapterNumber}`,
|
|
829
|
+
});
|
|
469
830
|
for (const w of validation.warnings) {
|
|
470
831
|
this.config.logger?.warn(` [${w.category}] ${w.description}`);
|
|
471
832
|
}
|
|
472
833
|
}
|
|
473
834
|
}
|
|
474
835
|
catch (e) {
|
|
475
|
-
this.
|
|
836
|
+
this.logWarn(pipelineLang, {
|
|
837
|
+
zh: `状态校验已跳过:${String(e)}`,
|
|
838
|
+
en: `State validation skipped: ${String(e)}`,
|
|
839
|
+
});
|
|
476
840
|
}
|
|
477
841
|
await writer.saveChapter(bookDir, persistenceOutput, gp.numericalSystem, pipelineLang);
|
|
478
|
-
await writer.saveNewTruthFiles(bookDir, persistenceOutput);
|
|
842
|
+
await writer.saveNewTruthFiles(bookDir, persistenceOutput, pipelineLang);
|
|
843
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, persistenceOutput);
|
|
844
|
+
this.logStage(stageLanguage, { zh: "同步记忆索引", en: "syncing memory indexes" });
|
|
845
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
479
846
|
// 5. Update chapter index
|
|
480
847
|
const existingIndex = await this.state.loadChapterIndex(bookId);
|
|
481
848
|
const now = new Date().toISOString();
|
|
@@ -487,9 +854,12 @@ export class PipelineRunner {
|
|
|
487
854
|
createdAt: now,
|
|
488
855
|
updatedAt: now,
|
|
489
856
|
auditIssues: auditResult.issues.map((i) => `[${i.severity}] ${i.description}`),
|
|
857
|
+
lengthWarnings,
|
|
858
|
+
lengthTelemetry,
|
|
490
859
|
tokenUsage: totalUsage,
|
|
491
860
|
};
|
|
492
861
|
await this.state.saveChapterIndex(bookId, [...existingIndex, newEntry]);
|
|
862
|
+
await this.markBookActiveIfNeeded(bookId);
|
|
493
863
|
// 5.5 Audit drift correction — feed audit findings back into state
|
|
494
864
|
// This prevents the writer from repeating mistakes in the next chapter
|
|
495
865
|
const driftIssues = auditResult.issues.filter((i) => i.severity === "critical" || i.severity === "warning");
|
|
@@ -499,10 +869,16 @@ export class PipelineRunner {
|
|
|
499
869
|
const statePath = join(storyDir, "current_state.md");
|
|
500
870
|
const currentState = await readFile(statePath, "utf-8").catch(() => "");
|
|
501
871
|
// Append drift correction section (or replace existing one)
|
|
502
|
-
const correctionHeader =
|
|
872
|
+
const correctionHeader = this.localize(stageLanguage, {
|
|
873
|
+
zh: "## 审计纠偏(自动生成,下一章写作前参照)",
|
|
874
|
+
en: "## Audit Drift Correction",
|
|
875
|
+
});
|
|
503
876
|
const correctionBlock = [
|
|
504
877
|
correctionHeader,
|
|
505
|
-
|
|
878
|
+
this.localize(stageLanguage, {
|
|
879
|
+
zh: `> 第${chapterNumber}章审计发现以下问题,下一章写作时必须避免:`,
|
|
880
|
+
en: `> Chapter ${chapterNumber} audit found the following issues to avoid in the next chapter:`,
|
|
881
|
+
}),
|
|
506
882
|
...driftIssues.map((i) => `> - [${i.severity}] ${i.category}: ${i.description}`),
|
|
507
883
|
"",
|
|
508
884
|
].join("\n");
|
|
@@ -518,14 +894,17 @@ export class PipelineRunner {
|
|
|
518
894
|
}
|
|
519
895
|
}
|
|
520
896
|
// 5.6 Snapshot state for rollback support
|
|
897
|
+
this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" });
|
|
521
898
|
await this.state.snapshotState(bookId, chapterNumber);
|
|
899
|
+
await this.syncCurrentStateFactHistory(bookId, chapterNumber);
|
|
522
900
|
// 6. Send notification
|
|
523
901
|
if (this.config.notifyChannels && this.config.notifyChannels.length > 0) {
|
|
524
902
|
const statusEmoji = auditResult.passed ? "✅" : "⚠️";
|
|
903
|
+
const chapterLength = formatLengthCount(finalWordCount, lengthSpec.countingMode);
|
|
525
904
|
await dispatchNotification(this.config.notifyChannels, {
|
|
526
905
|
title: `${statusEmoji} ${book.title} 第${chapterNumber}章`,
|
|
527
906
|
body: [
|
|
528
|
-
`**${persistenceOutput.title}** | ${
|
|
907
|
+
`**${persistenceOutput.title}** | ${chapterLength}`,
|
|
529
908
|
revised ? "📝 已自动修正" : "",
|
|
530
909
|
`审稿: ${auditResult.passed ? "通过" : "需人工审核"}`,
|
|
531
910
|
...auditResult.issues
|
|
@@ -549,6 +928,8 @@ export class PipelineRunner {
|
|
|
549
928
|
auditResult,
|
|
550
929
|
revised,
|
|
551
930
|
status: auditResult.passed ? "ready-for-review" : "audit-failed",
|
|
931
|
+
lengthWarnings,
|
|
932
|
+
lengthTelemetry,
|
|
552
933
|
tokenUsage: totalUsage,
|
|
553
934
|
};
|
|
554
935
|
}
|
|
@@ -756,28 +1137,46 @@ ${matrix}`,
|
|
|
756
1137
|
const book = await this.state.loadBookConfig(input.bookId);
|
|
757
1138
|
const bookDir = this.state.bookDir(input.bookId);
|
|
758
1139
|
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
1140
|
+
const resolvedLanguage = book.language ?? gp.language;
|
|
759
1141
|
const startFrom = input.resumeFrom ?? 1;
|
|
760
1142
|
const log = this.config.logger?.child("import");
|
|
761
1143
|
// Step 1: Generate foundation on first run (not on resume)
|
|
762
1144
|
if (startFrom === 1) {
|
|
763
|
-
log?.info(
|
|
764
|
-
|
|
1145
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1146
|
+
zh: `步骤 1:从 ${input.chapters.length} 章生成基础设定...`,
|
|
1147
|
+
en: `Step 1: Generating foundation from ${input.chapters.length} chapters...`,
|
|
1148
|
+
}));
|
|
1149
|
+
const allText = input.chapters.map((c, i) => resolvedLanguage === "en"
|
|
1150
|
+
? `Chapter ${i + 1}: ${c.title}\n\n${c.content}`
|
|
1151
|
+
: `第${i + 1}章 ${c.title}\n\n${c.content}`).join("\n\n---\n\n");
|
|
765
1152
|
const architect = new ArchitectAgent(this.agentCtxFor("architect", input.bookId));
|
|
766
1153
|
const foundation = await architect.generateFoundationFromImport(book, allText);
|
|
767
|
-
await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem);
|
|
1154
|
+
await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, resolvedLanguage);
|
|
1155
|
+
await this.resetImportReplayTruthFiles(bookDir, resolvedLanguage);
|
|
768
1156
|
await this.state.saveChapterIndex(input.bookId, []);
|
|
769
|
-
|
|
1157
|
+
await this.state.snapshotState(input.bookId, 0);
|
|
1158
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1159
|
+
zh: "基础设定已生成。",
|
|
1160
|
+
en: "Foundation generated.",
|
|
1161
|
+
}));
|
|
770
1162
|
}
|
|
771
1163
|
// Step 2: Sequential replay
|
|
772
|
-
log?.info(
|
|
1164
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1165
|
+
zh: `步骤 2:从第 ${startFrom} 章开始顺序回放...`,
|
|
1166
|
+
en: `Step 2: Sequential replay from chapter ${startFrom}...`,
|
|
1167
|
+
}));
|
|
773
1168
|
const analyzer = new ChapterAnalyzerAgent(this.agentCtxFor("chapter-analyzer", input.bookId));
|
|
774
1169
|
const writer = new WriterAgent(this.agentCtxFor("writer", input.bookId));
|
|
1170
|
+
const countingMode = resolveLengthCountingMode(book.language ?? gp.language);
|
|
775
1171
|
let totalWords = 0;
|
|
776
1172
|
let importedCount = 0;
|
|
777
1173
|
for (let i = startFrom - 1; i < input.chapters.length; i++) {
|
|
778
1174
|
const ch = input.chapters[i];
|
|
779
1175
|
const chapterNumber = i + 1;
|
|
780
|
-
log?.info(
|
|
1176
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1177
|
+
zh: `分析章节 ${chapterNumber}/${input.chapters.length}:${ch.title}...`,
|
|
1178
|
+
en: `Analyzing chapter ${chapterNumber}/${input.chapters.length}: ${ch.title}...`,
|
|
1179
|
+
}));
|
|
781
1180
|
// Analyze chapter to get truth file updates
|
|
782
1181
|
const output = await analyzer.analyzeChapter({
|
|
783
1182
|
book,
|
|
@@ -791,24 +1190,28 @@ ${matrix}`,
|
|
|
791
1190
|
...output,
|
|
792
1191
|
postWriteErrors: [],
|
|
793
1192
|
postWriteWarnings: [],
|
|
794
|
-
}, gp.numericalSystem);
|
|
1193
|
+
}, gp.numericalSystem, resolvedLanguage);
|
|
795
1194
|
// Save extended truth files (summaries, subplots, emotional arcs, character matrix)
|
|
796
1195
|
await writer.saveNewTruthFiles(bookDir, {
|
|
797
1196
|
...output,
|
|
798
1197
|
postWriteErrors: [],
|
|
799
1198
|
postWriteWarnings: [],
|
|
800
|
-
});
|
|
1199
|
+
}, resolvedLanguage);
|
|
1200
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, output);
|
|
1201
|
+
await this.syncNarrativeMemoryIndex(input.bookId);
|
|
801
1202
|
// Update chapter index
|
|
802
1203
|
const existingIndex = await this.state.loadChapterIndex(input.bookId);
|
|
803
1204
|
const now = new Date().toISOString();
|
|
1205
|
+
const chapterWordCount = countChapterLength(ch.content, countingMode);
|
|
804
1206
|
const newEntry = {
|
|
805
1207
|
number: chapterNumber,
|
|
806
1208
|
title: output.title,
|
|
807
1209
|
status: "imported",
|
|
808
|
-
wordCount:
|
|
1210
|
+
wordCount: chapterWordCount,
|
|
809
1211
|
createdAt: now,
|
|
810
1212
|
updatedAt: now,
|
|
811
1213
|
auditIssues: [],
|
|
1214
|
+
lengthWarnings: [],
|
|
812
1215
|
};
|
|
813
1216
|
// Replace if exists (resume case), otherwise append
|
|
814
1217
|
const existingIdx = existingIndex.findIndex((e) => e.number === chapterNumber);
|
|
@@ -819,10 +1222,17 @@ ${matrix}`,
|
|
|
819
1222
|
// Snapshot state after each chapter for rollback + resume support
|
|
820
1223
|
await this.state.snapshotState(input.bookId, chapterNumber);
|
|
821
1224
|
importedCount++;
|
|
822
|
-
totalWords +=
|
|
1225
|
+
totalWords += chapterWordCount;
|
|
1226
|
+
}
|
|
1227
|
+
if (input.chapters.length > 0) {
|
|
1228
|
+
await this.markBookActiveIfNeeded(input.bookId);
|
|
1229
|
+
await this.syncCurrentStateFactHistory(input.bookId, input.chapters.length);
|
|
823
1230
|
}
|
|
824
1231
|
const nextChapter = input.chapters.length + 1;
|
|
825
|
-
log?.info(
|
|
1232
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1233
|
+
zh: `完成。已导入 ${importedCount} 章,共 ${formatLengthCount(totalWords, countingMode)}。下一章:${nextChapter}`,
|
|
1234
|
+
en: `Done. ${importedCount} chapters imported, ${formatLengthCount(totalWords, countingMode)}. Next chapter: ${nextChapter}`,
|
|
1235
|
+
}));
|
|
826
1236
|
return {
|
|
827
1237
|
bookId: input.bookId,
|
|
828
1238
|
importedCount,
|
|
@@ -859,12 +1269,558 @@ ${matrix}`,
|
|
|
859
1269
|
...analyzed,
|
|
860
1270
|
postWriteErrors: [],
|
|
861
1271
|
postWriteWarnings: [],
|
|
1272
|
+
hookHealthIssues: output.hookHealthIssues,
|
|
862
1273
|
tokenUsage: output.tokenUsage,
|
|
863
1274
|
};
|
|
864
1275
|
}
|
|
865
1276
|
// ---------------------------------------------------------------------------
|
|
866
1277
|
// Helpers
|
|
867
1278
|
// ---------------------------------------------------------------------------
|
|
1279
|
+
async prepareWriteInput(book, bookDir, chapterNumber, externalContext) {
|
|
1280
|
+
if ((this.config.inputGovernanceMode ?? "v2") === "legacy") {
|
|
1281
|
+
return { externalContext };
|
|
1282
|
+
}
|
|
1283
|
+
const { plan, composed } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, externalContext, { reuseExistingIntentWhenContextMissing: true });
|
|
1284
|
+
return {
|
|
1285
|
+
chapterIntent: plan.intentMarkdown,
|
|
1286
|
+
contextPackage: composed.contextPackage,
|
|
1287
|
+
ruleStack: composed.ruleStack,
|
|
1288
|
+
trace: composed.trace,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
async resetImportReplayTruthFiles(bookDir, language) {
|
|
1292
|
+
const storyDir = join(bookDir, "story");
|
|
1293
|
+
await Promise.all([
|
|
1294
|
+
writeFile(join(storyDir, "current_state.md"), this.buildImportReplayStateSeed(language), "utf-8"),
|
|
1295
|
+
writeFile(join(storyDir, "pending_hooks.md"), this.buildImportReplayHooksSeed(language), "utf-8"),
|
|
1296
|
+
rm(join(storyDir, "chapter_summaries.md"), { force: true }),
|
|
1297
|
+
rm(join(storyDir, "subplot_board.md"), { force: true }),
|
|
1298
|
+
rm(join(storyDir, "emotional_arcs.md"), { force: true }),
|
|
1299
|
+
rm(join(storyDir, "character_matrix.md"), { force: true }),
|
|
1300
|
+
rm(join(storyDir, "volume_summaries.md"), { force: true }),
|
|
1301
|
+
rm(join(storyDir, "particle_ledger.md"), { force: true }),
|
|
1302
|
+
rm(join(storyDir, "memory.db"), { force: true }),
|
|
1303
|
+
rm(join(storyDir, "memory.db-shm"), { force: true }),
|
|
1304
|
+
rm(join(storyDir, "memory.db-wal"), { force: true }),
|
|
1305
|
+
rm(join(storyDir, "state"), { recursive: true, force: true }),
|
|
1306
|
+
rm(join(storyDir, "snapshots"), { recursive: true, force: true }),
|
|
1307
|
+
]);
|
|
1308
|
+
}
|
|
1309
|
+
buildImportReplayStateSeed(language) {
|
|
1310
|
+
if (language === "en") {
|
|
1311
|
+
return [
|
|
1312
|
+
"# Current State",
|
|
1313
|
+
"",
|
|
1314
|
+
"| Field | Value |",
|
|
1315
|
+
"| --- | --- |",
|
|
1316
|
+
"| Current Chapter | 0 |",
|
|
1317
|
+
"| Current Location | (not set) |",
|
|
1318
|
+
"| Protagonist State | (not set) |",
|
|
1319
|
+
"| Current Goal | (not set) |",
|
|
1320
|
+
"| Current Constraint | (not set) |",
|
|
1321
|
+
"| Current Alliances | (not set) |",
|
|
1322
|
+
"| Current Conflict | (not set) |",
|
|
1323
|
+
"",
|
|
1324
|
+
].join("\n");
|
|
1325
|
+
}
|
|
1326
|
+
return [
|
|
1327
|
+
"# 当前状态",
|
|
1328
|
+
"",
|
|
1329
|
+
"| 字段 | 值 |",
|
|
1330
|
+
"| --- | --- |",
|
|
1331
|
+
"| 当前章节 | 0 |",
|
|
1332
|
+
"| 当前位置 | (未设定) |",
|
|
1333
|
+
"| 主角状态 | (未设定) |",
|
|
1334
|
+
"| 当前目标 | (未设定) |",
|
|
1335
|
+
"| 当前限制 | (未设定) |",
|
|
1336
|
+
"| 当前敌我 | (未设定) |",
|
|
1337
|
+
"| 当前冲突 | (未设定) |",
|
|
1338
|
+
"",
|
|
1339
|
+
].join("\n");
|
|
1340
|
+
}
|
|
1341
|
+
buildImportReplayHooksSeed(language) {
|
|
1342
|
+
if (language === "en") {
|
|
1343
|
+
return [
|
|
1344
|
+
"# Pending Hooks",
|
|
1345
|
+
"",
|
|
1346
|
+
"| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | notes |",
|
|
1347
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
1348
|
+
"",
|
|
1349
|
+
].join("\n");
|
|
1350
|
+
}
|
|
1351
|
+
return [
|
|
1352
|
+
"# 伏笔池",
|
|
1353
|
+
"",
|
|
1354
|
+
"| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |",
|
|
1355
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
1356
|
+
"",
|
|
1357
|
+
].join("\n");
|
|
1358
|
+
}
|
|
1359
|
+
async normalizeDraftLengthIfNeeded(params) {
|
|
1360
|
+
const writerCount = countChapterLength(params.chapterContent, params.lengthSpec.countingMode);
|
|
1361
|
+
if (!isOutsideSoftRange(writerCount, params.lengthSpec)) {
|
|
1362
|
+
return {
|
|
1363
|
+
content: params.chapterContent,
|
|
1364
|
+
wordCount: writerCount,
|
|
1365
|
+
applied: false,
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
const normalizer = new LengthNormalizerAgent(this.agentCtxFor("length-normalizer", params.bookId));
|
|
1369
|
+
const normalized = await normalizer.normalizeChapter({
|
|
1370
|
+
chapterContent: params.chapterContent,
|
|
1371
|
+
lengthSpec: params.lengthSpec,
|
|
1372
|
+
chapterIntent: params.chapterIntent,
|
|
1373
|
+
});
|
|
1374
|
+
// Safety net: if normalizer output is less than 25% of original, it was too destructive.
|
|
1375
|
+
// Reject and keep original content.
|
|
1376
|
+
if (normalized.finalCount < writerCount * 0.25) {
|
|
1377
|
+
this.logWarn(this.languageFromLengthSpec(params.lengthSpec), {
|
|
1378
|
+
zh: `字数归一化被拒绝:第${params.chapterNumber}章 ${writerCount} -> ${normalized.finalCount}(砍了${Math.round((1 - normalized.finalCount / writerCount) * 100)}%,超过安全阈值)`,
|
|
1379
|
+
en: `Length normalization rejected for chapter ${params.chapterNumber}: ${writerCount} -> ${normalized.finalCount} (cut ${Math.round((1 - normalized.finalCount / writerCount) * 100)}%, exceeds safety threshold)`,
|
|
1380
|
+
});
|
|
1381
|
+
return {
|
|
1382
|
+
content: params.chapterContent,
|
|
1383
|
+
wordCount: writerCount,
|
|
1384
|
+
applied: false,
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
this.logInfo(this.languageFromLengthSpec(params.lengthSpec), {
|
|
1388
|
+
zh: `审计前字数归一化:第${params.chapterNumber}章 ${writerCount} -> ${normalized.finalCount}`,
|
|
1389
|
+
en: `Length normalization before audit for chapter ${params.chapterNumber}: ${writerCount} -> ${normalized.finalCount}`,
|
|
1390
|
+
});
|
|
1391
|
+
return {
|
|
1392
|
+
content: normalized.normalizedContent,
|
|
1393
|
+
wordCount: normalized.finalCount,
|
|
1394
|
+
applied: normalized.applied,
|
|
1395
|
+
tokenUsage: normalized.tokenUsage,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
async syncCurrentStateFactHistory(bookId, uptoChapter) {
|
|
1399
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1400
|
+
try {
|
|
1401
|
+
await this.rebuildCurrentStateFactHistory(bookDir, uptoChapter);
|
|
1402
|
+
}
|
|
1403
|
+
catch (error) {
|
|
1404
|
+
if (this.isMemoryIndexUnavailableError(error)) {
|
|
1405
|
+
if (this.canOpenMemoryIndex(bookDir)) {
|
|
1406
|
+
try {
|
|
1407
|
+
await this.rebuildCurrentStateFactHistory(bookDir, uptoChapter);
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
catch (retryError) {
|
|
1411
|
+
error = retryError;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
else {
|
|
1415
|
+
if (!this.memoryIndexFallbackWarned) {
|
|
1416
|
+
this.memoryIndexFallbackWarned = true;
|
|
1417
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1418
|
+
zh: "当前 Node 运行时不支持 SQLite 记忆索引,继续使用 Markdown 回退方案。",
|
|
1419
|
+
en: "SQLite memory index unavailable on this Node runtime; continuing with markdown fallback.",
|
|
1420
|
+
});
|
|
1421
|
+
await this.logMemoryIndexDebugInfo(bookId, error);
|
|
1422
|
+
}
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1427
|
+
zh: `状态事实同步已跳过:${String(error)}`,
|
|
1428
|
+
en: `State fact sync skipped: ${String(error)}`,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
async syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, output) {
|
|
1433
|
+
if (output?.runtimeStateDelta || output?.runtimeStateSnapshot) {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
await rewriteStructuredStateFromMarkdown({
|
|
1437
|
+
bookDir,
|
|
1438
|
+
fallbackChapter: chapterNumber,
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
async syncNarrativeMemoryIndex(bookId) {
|
|
1442
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1443
|
+
try {
|
|
1444
|
+
await this.rebuildNarrativeMemoryIndex(bookDir);
|
|
1445
|
+
}
|
|
1446
|
+
catch (error) {
|
|
1447
|
+
if (this.isMemoryIndexUnavailableError(error)) {
|
|
1448
|
+
if (this.canOpenMemoryIndex(bookDir)) {
|
|
1449
|
+
try {
|
|
1450
|
+
await this.rebuildNarrativeMemoryIndex(bookDir);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
catch (retryError) {
|
|
1454
|
+
error = retryError;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
else {
|
|
1458
|
+
if (!this.memoryIndexFallbackWarned) {
|
|
1459
|
+
this.memoryIndexFallbackWarned = true;
|
|
1460
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1461
|
+
zh: "当前 Node 运行时不支持 SQLite 记忆索引,继续使用 Markdown 回退方案。",
|
|
1462
|
+
en: "SQLite memory index unavailable on this Node runtime; continuing with markdown fallback.",
|
|
1463
|
+
});
|
|
1464
|
+
await this.logMemoryIndexDebugInfo(bookId, error);
|
|
1465
|
+
}
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1470
|
+
zh: `叙事记忆同步已跳过:${String(error)}`,
|
|
1471
|
+
en: `Narrative memory sync skipped: ${String(error)}`,
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
async rebuildCurrentStateFactHistory(bookDir, uptoChapter) {
|
|
1476
|
+
const memoryDb = await this.withMemoryIndexRetry(async () => {
|
|
1477
|
+
const db = new MemoryDB(bookDir);
|
|
1478
|
+
try {
|
|
1479
|
+
db.resetFacts();
|
|
1480
|
+
const activeFacts = new Map();
|
|
1481
|
+
for (let chapter = 0; chapter <= uptoChapter; chapter++) {
|
|
1482
|
+
const snapshotFacts = await loadSnapshotCurrentStateFacts(bookDir, chapter);
|
|
1483
|
+
if (snapshotFacts.length === 0)
|
|
1484
|
+
continue;
|
|
1485
|
+
const nextFacts = new Map();
|
|
1486
|
+
for (const fact of snapshotFacts) {
|
|
1487
|
+
nextFacts.set(this.factKey(fact), {
|
|
1488
|
+
subject: fact.subject,
|
|
1489
|
+
predicate: fact.predicate,
|
|
1490
|
+
object: fact.object,
|
|
1491
|
+
validFromChapter: chapter,
|
|
1492
|
+
validUntilChapter: null,
|
|
1493
|
+
sourceChapter: chapter,
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
for (const [key, previous] of activeFacts.entries()) {
|
|
1497
|
+
const next = nextFacts.get(key);
|
|
1498
|
+
if (!next || next.object !== previous.object) {
|
|
1499
|
+
db.invalidateFact(previous.id, chapter);
|
|
1500
|
+
activeFacts.delete(key);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
for (const [key, fact] of nextFacts.entries()) {
|
|
1504
|
+
if (activeFacts.has(key))
|
|
1505
|
+
continue;
|
|
1506
|
+
const id = db.addFact(fact);
|
|
1507
|
+
activeFacts.set(key, { id, object: fact.object });
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return db;
|
|
1511
|
+
}
|
|
1512
|
+
catch (error) {
|
|
1513
|
+
db.close();
|
|
1514
|
+
throw error;
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
try {
|
|
1518
|
+
// No-op: keep the db open only for the duration of the rebuild.
|
|
1519
|
+
}
|
|
1520
|
+
finally {
|
|
1521
|
+
memoryDb.close();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
async rebuildNarrativeMemoryIndex(bookDir) {
|
|
1525
|
+
const memorySeed = await loadNarrativeMemorySeed(bookDir);
|
|
1526
|
+
const memoryDb = await this.withMemoryIndexRetry(() => {
|
|
1527
|
+
const db = new MemoryDB(bookDir);
|
|
1528
|
+
try {
|
|
1529
|
+
db.replaceSummaries(memorySeed.summaries);
|
|
1530
|
+
db.replaceHooks(memorySeed.hooks);
|
|
1531
|
+
return db;
|
|
1532
|
+
}
|
|
1533
|
+
catch (error) {
|
|
1534
|
+
db.close();
|
|
1535
|
+
throw error;
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
try {
|
|
1539
|
+
// No-op: keep the db open only for the duration of the rebuild.
|
|
1540
|
+
}
|
|
1541
|
+
finally {
|
|
1542
|
+
memoryDb.close();
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
canOpenMemoryIndex(bookDir) {
|
|
1546
|
+
let memoryDb = null;
|
|
1547
|
+
try {
|
|
1548
|
+
memoryDb = new MemoryDB(bookDir);
|
|
1549
|
+
return true;
|
|
1550
|
+
}
|
|
1551
|
+
catch {
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
finally {
|
|
1555
|
+
memoryDb?.close();
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
async logMemoryIndexDebugInfo(bookId, error) {
|
|
1559
|
+
if (process.env.INKOS_DEBUG_SQLITE_MEMORY !== "1") {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
1563
|
+
? String(error.code ?? "")
|
|
1564
|
+
: "";
|
|
1565
|
+
const message = error instanceof Error
|
|
1566
|
+
? error.message
|
|
1567
|
+
: String(error);
|
|
1568
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1569
|
+
zh: `SQLite 记忆索引调试:node=${process.version}; execArgv=${JSON.stringify(process.execArgv)}; code=${code || "(none)"}; message=${message}`,
|
|
1570
|
+
en: `SQLite memory debug: node=${process.version}; execArgv=${JSON.stringify(process.execArgv)}; code=${code || "(none)"}; message=${message}`,
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
async withMemoryIndexRetry(operation) {
|
|
1574
|
+
const retryDelaysMs = [0, 25, 75];
|
|
1575
|
+
let lastError;
|
|
1576
|
+
for (let attempt = 0; attempt < retryDelaysMs.length; attempt += 1) {
|
|
1577
|
+
try {
|
|
1578
|
+
return await operation();
|
|
1579
|
+
}
|
|
1580
|
+
catch (error) {
|
|
1581
|
+
lastError = error;
|
|
1582
|
+
if (!this.isMemoryIndexBusyError(error) || attempt === retryDelaysMs.length - 1) {
|
|
1583
|
+
throw error;
|
|
1584
|
+
}
|
|
1585
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt + 1]));
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
throw lastError;
|
|
1589
|
+
}
|
|
1590
|
+
isMemoryIndexUnavailableError(error) {
|
|
1591
|
+
if (!error)
|
|
1592
|
+
return false;
|
|
1593
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
1594
|
+
? String(error.code ?? "")
|
|
1595
|
+
: "";
|
|
1596
|
+
const message = error instanceof Error
|
|
1597
|
+
? error.message
|
|
1598
|
+
: String(error);
|
|
1599
|
+
const normalizedMessage = message.trim();
|
|
1600
|
+
return /^No such built-in module:\s*node:sqlite$/i.test(normalizedMessage)
|
|
1601
|
+
|| /^Cannot find module ['"]node:sqlite['"]$/i.test(normalizedMessage)
|
|
1602
|
+
|| (code === "ERR_UNKNOWN_BUILTIN_MODULE" && /\bnode:sqlite\b/i.test(normalizedMessage));
|
|
1603
|
+
}
|
|
1604
|
+
isMemoryIndexBusyError(error) {
|
|
1605
|
+
if (!error)
|
|
1606
|
+
return false;
|
|
1607
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
1608
|
+
? String(error.code ?? "")
|
|
1609
|
+
: "";
|
|
1610
|
+
const message = error instanceof Error
|
|
1611
|
+
? error.message
|
|
1612
|
+
: String(error);
|
|
1613
|
+
return code === "SQLITE_BUSY"
|
|
1614
|
+
|| code === "SQLITE_LOCKED"
|
|
1615
|
+
|| /\bSQLITE_BUSY\b/i.test(message)
|
|
1616
|
+
|| /\bSQLITE_LOCKED\b/i.test(message)
|
|
1617
|
+
|| /database is locked/i.test(message)
|
|
1618
|
+
|| /database is busy/i.test(message);
|
|
1619
|
+
}
|
|
1620
|
+
factKey(fact) {
|
|
1621
|
+
return `${fact.subject}::${fact.predicate}`;
|
|
1622
|
+
}
|
|
1623
|
+
buildLengthWarnings(chapterNumber, finalCount, lengthSpec) {
|
|
1624
|
+
if (!isOutsideHardRange(finalCount, lengthSpec)) {
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1627
|
+
return [
|
|
1628
|
+
this.localize(this.languageFromLengthSpec(lengthSpec), {
|
|
1629
|
+
zh: `第${chapterNumber}章经过一次字数归一化后仍超出硬区间(${lengthSpec.hardMin}-${lengthSpec.hardMax},实际 ${finalCount})。`,
|
|
1630
|
+
en: `Chapter ${chapterNumber} remains outside hard range (${lengthSpec.hardMin}-${lengthSpec.hardMax}, actual ${finalCount}) after a single normalization pass.`,
|
|
1631
|
+
}),
|
|
1632
|
+
];
|
|
1633
|
+
}
|
|
1634
|
+
buildLengthTelemetry(params) {
|
|
1635
|
+
return {
|
|
1636
|
+
target: params.lengthSpec.target,
|
|
1637
|
+
softMin: params.lengthSpec.softMin,
|
|
1638
|
+
softMax: params.lengthSpec.softMax,
|
|
1639
|
+
hardMin: params.lengthSpec.hardMin,
|
|
1640
|
+
hardMax: params.lengthSpec.hardMax,
|
|
1641
|
+
countingMode: params.lengthSpec.countingMode,
|
|
1642
|
+
writerCount: params.writerCount,
|
|
1643
|
+
postWriterNormalizeCount: params.postWriterNormalizeCount,
|
|
1644
|
+
postReviseCount: params.postReviseCount,
|
|
1645
|
+
finalCount: params.finalCount,
|
|
1646
|
+
normalizeApplied: params.normalizeApplied,
|
|
1647
|
+
lengthWarning: params.lengthWarning,
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
logLengthWarnings(lengthWarnings) {
|
|
1651
|
+
for (const warning of lengthWarnings) {
|
|
1652
|
+
this.config.logger?.warn(warning);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
restoreLostAuditIssues(previous, next) {
|
|
1656
|
+
if (next.passed || next.issues.length > 0 || previous.issues.length === 0) {
|
|
1657
|
+
return next;
|
|
1658
|
+
}
|
|
1659
|
+
return {
|
|
1660
|
+
...next,
|
|
1661
|
+
issues: previous.issues,
|
|
1662
|
+
summary: next.summary || previous.summary,
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
restoreActionableAuditIfLost(previous, next) {
|
|
1666
|
+
const auditResult = this.restoreLostAuditIssues(previous.auditResult, next.auditResult);
|
|
1667
|
+
if (auditResult === next.auditResult) {
|
|
1668
|
+
return next;
|
|
1669
|
+
}
|
|
1670
|
+
return {
|
|
1671
|
+
...next,
|
|
1672
|
+
auditResult,
|
|
1673
|
+
blockingCount: auditResult.issues.filter((issue) => issue.severity === "warning" || issue.severity === "critical").length,
|
|
1674
|
+
criticalCount: auditResult.issues.filter((issue) => issue.severity === "critical").length,
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
async evaluateMergedAudit(params) {
|
|
1678
|
+
const llmAudit = await params.auditor.auditChapter(params.bookDir, params.chapterContent, params.chapterNumber, params.book.genre, params.auditOptions);
|
|
1679
|
+
const aiTells = analyzeAITells(params.chapterContent);
|
|
1680
|
+
const sensitiveResult = analyzeSensitiveWords(params.chapterContent);
|
|
1681
|
+
const longSpanFatigue = await analyzeLongSpanFatigue({
|
|
1682
|
+
bookDir: params.bookDir,
|
|
1683
|
+
chapterNumber: params.chapterNumber,
|
|
1684
|
+
chapterContent: params.chapterContent,
|
|
1685
|
+
language: params.language,
|
|
1686
|
+
});
|
|
1687
|
+
const hasBlockedWords = sensitiveResult.found.some((f) => f.severity === "block");
|
|
1688
|
+
const issues = [
|
|
1689
|
+
...llmAudit.issues,
|
|
1690
|
+
...aiTells.issues,
|
|
1691
|
+
...sensitiveResult.issues,
|
|
1692
|
+
...longSpanFatigue.issues,
|
|
1693
|
+
];
|
|
1694
|
+
return {
|
|
1695
|
+
auditResult: {
|
|
1696
|
+
passed: hasBlockedWords ? false : llmAudit.passed,
|
|
1697
|
+
issues,
|
|
1698
|
+
summary: llmAudit.summary,
|
|
1699
|
+
tokenUsage: llmAudit.tokenUsage,
|
|
1700
|
+
},
|
|
1701
|
+
aiTellCount: aiTells.issues.length,
|
|
1702
|
+
blockingCount: issues.filter((issue) => issue.severity === "warning" || issue.severity === "critical").length,
|
|
1703
|
+
criticalCount: issues.filter((issue) => issue.severity === "critical").length,
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
async markBookActiveIfNeeded(bookId) {
|
|
1707
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
1708
|
+
if (book.status !== "outlining")
|
|
1709
|
+
return;
|
|
1710
|
+
await this.state.saveBookConfig(bookId, {
|
|
1711
|
+
...book,
|
|
1712
|
+
status: "active",
|
|
1713
|
+
updatedAt: new Date().toISOString(),
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
async createGovernedArtifacts(book, bookDir, chapterNumber, externalContext, options) {
|
|
1717
|
+
const plan = await this.resolveGovernedPlan(book, bookDir, chapterNumber, externalContext, options);
|
|
1718
|
+
const composer = new ComposerAgent(this.agentCtxFor("composer", book.id));
|
|
1719
|
+
const composed = await composer.composeChapter({
|
|
1720
|
+
book,
|
|
1721
|
+
bookDir,
|
|
1722
|
+
chapterNumber,
|
|
1723
|
+
plan,
|
|
1724
|
+
});
|
|
1725
|
+
return { plan, composed };
|
|
1726
|
+
}
|
|
1727
|
+
async resolveGovernedPlan(book, bookDir, chapterNumber, externalContext, options) {
|
|
1728
|
+
if (options?.reuseExistingIntentWhenContextMissing &&
|
|
1729
|
+
(!externalContext || externalContext.trim().length === 0)) {
|
|
1730
|
+
const persisted = await this.loadPersistedPlan(bookDir, chapterNumber);
|
|
1731
|
+
if (persisted)
|
|
1732
|
+
return persisted;
|
|
1733
|
+
}
|
|
1734
|
+
const planner = new PlannerAgent(this.agentCtxFor("planner", book.id));
|
|
1735
|
+
return planner.planChapter({
|
|
1736
|
+
book,
|
|
1737
|
+
bookDir,
|
|
1738
|
+
chapterNumber,
|
|
1739
|
+
externalContext,
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
async loadPersistedPlan(bookDir, chapterNumber) {
|
|
1743
|
+
const runtimePath = join(bookDir, "story", "runtime", `chapter-${String(chapterNumber).padStart(4, "0")}.intent.md`);
|
|
1744
|
+
try {
|
|
1745
|
+
const intentMarkdown = await readFile(runtimePath, "utf-8");
|
|
1746
|
+
const sections = this.parseIntentSections(intentMarkdown);
|
|
1747
|
+
const goal = this.readIntentScalar(sections, "Goal");
|
|
1748
|
+
if (!goal || this.isInvalidPersistedIntentScalar(goal))
|
|
1749
|
+
return null;
|
|
1750
|
+
const outlineNode = this.readIntentScalar(sections, "Outline Node");
|
|
1751
|
+
if (outlineNode && outlineNode !== "(not found)" && this.isInvalidPersistedIntentScalar(outlineNode)) {
|
|
1752
|
+
return null;
|
|
1753
|
+
}
|
|
1754
|
+
const conflicts = this.readIntentList(sections, "Conflicts")
|
|
1755
|
+
.map((line) => {
|
|
1756
|
+
const separator = line.indexOf(":");
|
|
1757
|
+
if (separator < 0)
|
|
1758
|
+
return null;
|
|
1759
|
+
const type = line.slice(0, separator).trim();
|
|
1760
|
+
const resolution = line.slice(separator + 1).trim();
|
|
1761
|
+
if (!type || !resolution)
|
|
1762
|
+
return null;
|
|
1763
|
+
return { type, resolution };
|
|
1764
|
+
})
|
|
1765
|
+
.filter((conflict) => conflict !== null);
|
|
1766
|
+
return {
|
|
1767
|
+
intent: ChapterIntentSchema.parse({
|
|
1768
|
+
chapter: chapterNumber,
|
|
1769
|
+
goal,
|
|
1770
|
+
outlineNode: outlineNode && outlineNode !== "(not found)" ? outlineNode : undefined,
|
|
1771
|
+
mustKeep: this.readIntentList(sections, "Must Keep"),
|
|
1772
|
+
mustAvoid: this.readIntentList(sections, "Must Avoid"),
|
|
1773
|
+
styleEmphasis: this.readIntentList(sections, "Style Emphasis"),
|
|
1774
|
+
conflicts,
|
|
1775
|
+
}),
|
|
1776
|
+
intentMarkdown,
|
|
1777
|
+
plannerInputs: [runtimePath],
|
|
1778
|
+
runtimePath,
|
|
1779
|
+
};
|
|
1780
|
+
}
|
|
1781
|
+
catch {
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
parseIntentSections(markdown) {
|
|
1786
|
+
const sections = new Map();
|
|
1787
|
+
let current = null;
|
|
1788
|
+
for (const line of markdown.split("\n")) {
|
|
1789
|
+
if (line.startsWith("## ")) {
|
|
1790
|
+
current = line.slice(3).trim();
|
|
1791
|
+
sections.set(current, []);
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
if (!current)
|
|
1795
|
+
continue;
|
|
1796
|
+
sections.get(current)?.push(line);
|
|
1797
|
+
}
|
|
1798
|
+
return sections;
|
|
1799
|
+
}
|
|
1800
|
+
readIntentScalar(sections, name) {
|
|
1801
|
+
const lines = sections.get(name) ?? [];
|
|
1802
|
+
const value = lines.map((line) => line.trim()).find((line) => line.length > 0);
|
|
1803
|
+
return value && value !== "- none" ? value : undefined;
|
|
1804
|
+
}
|
|
1805
|
+
readIntentList(sections, name) {
|
|
1806
|
+
return (sections.get(name) ?? [])
|
|
1807
|
+
.map((line) => line.trim())
|
|
1808
|
+
.filter((line) => line.startsWith("-") && line !== "- none")
|
|
1809
|
+
.map((line) => line.replace(/^-\s*/, ""));
|
|
1810
|
+
}
|
|
1811
|
+
isInvalidPersistedIntentScalar(value) {
|
|
1812
|
+
const normalized = value.trim();
|
|
1813
|
+
if (!normalized)
|
|
1814
|
+
return true;
|
|
1815
|
+
if (/^[*_`~::|.-]+$/.test(normalized))
|
|
1816
|
+
return true;
|
|
1817
|
+
return (/^\((describe|briefly describe|write)\b[\s\S]*\)$/i.test(normalized)
|
|
1818
|
+
|| /^((?:在这里描述|描述|填写|写下)[\s\S]*)$/u.test(normalized));
|
|
1819
|
+
}
|
|
1820
|
+
relativeToBookDir(bookDir, absolutePath) {
|
|
1821
|
+
const prefix = `${bookDir}/`;
|
|
1822
|
+
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
|
|
1823
|
+
}
|
|
868
1824
|
async emitWebhook(event, bookId, chapterNumber, data) {
|
|
869
1825
|
if (!this.config.notifyChannels || this.config.notifyChannels.length === 0)
|
|
870
1826
|
return;
|