inkos-n-core 1.2.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/ai-tells.d.ts +26 -0
- package/dist/agents/ai-tells.d.ts.map +1 -0
- package/dist/agents/ai-tells.js +140 -0
- package/dist/agents/ai-tells.js.map +1 -0
- package/dist/agents/architect.d.ts +34 -0
- package/dist/agents/architect.d.ts.map +1 -0
- package/dist/agents/architect.js +906 -0
- package/dist/agents/architect.js.map +1 -0
- package/dist/agents/base.d.ts +30 -0
- package/dist/agents/base.d.ts.map +1 -0
- package/dist/agents/base.js +70 -0
- package/dist/agents/base.js.map +1 -0
- package/dist/agents/chapter-analyzer.d.ts +30 -0
- package/dist/agents/chapter-analyzer.d.ts.map +1 -0
- package/dist/agents/chapter-analyzer.js +476 -0
- package/dist/agents/chapter-analyzer.js.map +1 -0
- package/dist/agents/composer.d.ts +36 -0
- package/dist/agents/composer.d.ts.map +1 -0
- package/dist/agents/composer.js +319 -0
- package/dist/agents/composer.js.map +1 -0
- package/dist/agents/consolidator.d.ts +23 -0
- package/dist/agents/consolidator.d.ts.map +1 -0
- package/dist/agents/consolidator.js +141 -0
- package/dist/agents/consolidator.js.map +1 -0
- package/dist/agents/continuity.d.ts +39 -0
- package/dist/agents/continuity.d.ts.map +1 -0
- package/dist/agents/continuity.js +612 -0
- package/dist/agents/continuity.js.map +1 -0
- package/dist/agents/detection-insights.d.ts +9 -0
- package/dist/agents/detection-insights.d.ts.map +1 -0
- package/dist/agents/detection-insights.js +54 -0
- package/dist/agents/detection-insights.js.map +1 -0
- package/dist/agents/detector.d.ts +17 -0
- package/dist/agents/detector.d.ts.map +1 -0
- package/dist/agents/detector.js +77 -0
- package/dist/agents/detector.js.map +1 -0
- package/dist/agents/en-prompt-sections.d.ts +8 -0
- package/dist/agents/en-prompt-sections.d.ts.map +1 -0
- package/dist/agents/en-prompt-sections.js +120 -0
- package/dist/agents/en-prompt-sections.js.map +1 -0
- package/dist/agents/fanfic-canon-importer.d.ts +15 -0
- package/dist/agents/fanfic-canon-importer.d.ts.map +1 -0
- package/dist/agents/fanfic-canon-importer.js +117 -0
- package/dist/agents/fanfic-canon-importer.js.map +1 -0
- package/dist/agents/fanfic-dimensions.d.ts +14 -0
- package/dist/agents/fanfic-dimensions.d.ts.map +1 -0
- package/dist/agents/fanfic-dimensions.js +63 -0
- package/dist/agents/fanfic-dimensions.js.map +1 -0
- package/dist/agents/fanfic-prompt-sections.d.ts +5 -0
- package/dist/agents/fanfic-prompt-sections.d.ts.map +1 -0
- package/dist/agents/fanfic-prompt-sections.js +85 -0
- package/dist/agents/fanfic-prompt-sections.js.map +1 -0
- package/dist/agents/foundation-reviewer.d.ts +29 -0
- package/dist/agents/foundation-reviewer.d.ts.map +1 -0
- package/dist/agents/foundation-reviewer.js +153 -0
- package/dist/agents/foundation-reviewer.js.map +1 -0
- 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/observer-prompts.d.ts +10 -0
- package/dist/agents/observer-prompts.d.ts.map +1 -0
- package/dist/agents/observer-prompts.js +113 -0
- package/dist/agents/observer-prompts.js.map +1 -0
- package/dist/agents/planner.d.ts +57 -0
- package/dist/agents/planner.d.ts.map +1 -0
- package/dist/agents/planner.js +594 -0
- package/dist/agents/planner.js.map +1 -0
- package/dist/agents/post-write-validator.d.ts +34 -0
- package/dist/agents/post-write-validator.d.ts.map +1 -0
- package/dist/agents/post-write-validator.js +696 -0
- package/dist/agents/post-write-validator.js.map +1 -0
- package/dist/agents/radar-source.d.ts +38 -0
- package/dist/agents/radar-source.d.ts.map +1 -0
- package/dist/agents/radar-source.js +92 -0
- package/dist/agents/radar-source.js.map +1 -0
- package/dist/agents/radar.d.ts +24 -0
- package/dist/agents/radar.d.ts.map +1 -0
- package/dist/agents/radar.js +85 -0
- package/dist/agents/radar.js.map +1 -0
- package/dist/agents/reviser.d.ts +32 -0
- package/dist/agents/reviser.d.ts.map +1 -0
- package/dist/agents/reviser.js +282 -0
- package/dist/agents/reviser.js.map +1 -0
- package/dist/agents/rules-reader.d.ts +27 -0
- package/dist/agents/rules-reader.d.ts.map +1 -0
- package/dist/agents/rules-reader.js +99 -0
- package/dist/agents/rules-reader.js.map +1 -0
- package/dist/agents/sensitive-words.d.ts +24 -0
- package/dist/agents/sensitive-words.d.ts.map +1 -0
- package/dist/agents/sensitive-words.js +103 -0
- package/dist/agents/sensitive-words.js.map +1 -0
- 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 +40 -0
- package/dist/agents/settler-delta-parser.js.map +1 -0
- package/dist/agents/settler-parser.d.ts +13 -0
- package/dist/agents/settler-parser.d.ts.map +1 -0
- package/dist/agents/settler-parser.js +20 -0
- package/dist/agents/settler-parser.js.map +1 -0
- package/dist/agents/settler-prompts.d.ts +22 -0
- package/dist/agents/settler-prompts.d.ts.map +1 -0
- package/dist/agents/settler-prompts.js +193 -0
- package/dist/agents/settler-prompts.js.map +1 -0
- package/dist/agents/state-validator.d.ts +26 -0
- package/dist/agents/state-validator.d.ts.map +1 -0
- package/dist/agents/state-validator.js +229 -0
- package/dist/agents/state-validator.js.map +1 -0
- package/dist/agents/style-analyzer.d.ts +11 -0
- package/dist/agents/style-analyzer.d.ts.map +1 -0
- package/dist/agents/style-analyzer.js +81 -0
- package/dist/agents/style-analyzer.js.map +1 -0
- package/dist/agents/writer-parser.d.ts +17 -0
- package/dist/agents/writer-parser.d.ts.map +1 -0
- package/dist/agents/writer-parser.js +131 -0
- package/dist/agents/writer-parser.js.map +1 -0
- package/dist/agents/writer-prompts.d.ts +11 -0
- package/dist/agents/writer-prompts.d.ts.map +1 -0
- package/dist/agents/writer-prompts.js +549 -0
- package/dist/agents/writer-prompts.js.map +1 -0
- package/dist/agents/writer.d.ts +103 -0
- package/dist/agents/writer.d.ts.map +1 -0
- package/dist/agents/writer.js +1052 -0
- package/dist/agents/writer.js.map +1 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/interaction/edit-controller.d.ts +55 -0
- package/dist/interaction/edit-controller.d.ts.map +1 -0
- package/dist/interaction/edit-controller.js +187 -0
- package/dist/interaction/edit-controller.js.map +1 -0
- package/dist/interaction/events.d.ts +45 -0
- package/dist/interaction/events.d.ts.map +1 -0
- package/dist/interaction/events.js +32 -0
- package/dist/interaction/events.js.map +1 -0
- package/dist/interaction/intents.d.ts +96 -0
- package/dist/interaction/intents.d.ts.map +1 -0
- package/dist/interaction/intents.js +58 -0
- package/dist/interaction/intents.js.map +1 -0
- package/dist/interaction/modes.d.ts +5 -0
- package/dist/interaction/modes.d.ts.map +1 -0
- package/dist/interaction/modes.js +7 -0
- package/dist/interaction/modes.js.map +1 -0
- package/dist/interaction/nl-router.d.ts +8 -0
- package/dist/interaction/nl-router.d.ts.map +1 -0
- package/dist/interaction/nl-router.js +218 -0
- package/dist/interaction/nl-router.js.map +1 -0
- package/dist/interaction/project-control.d.ts +85 -0
- package/dist/interaction/project-control.d.ts.map +1 -0
- package/dist/interaction/project-control.js +123 -0
- package/dist/interaction/project-control.js.map +1 -0
- package/dist/interaction/project-session-store.d.ts +7 -0
- package/dist/interaction/project-session-store.d.ts.map +1 -0
- package/dist/interaction/project-session-store.js +46 -0
- package/dist/interaction/project-session-store.js.map +1 -0
- package/dist/interaction/project-tools.d.ts +20 -0
- package/dist/interaction/project-tools.d.ts.map +1 -0
- package/dist/interaction/project-tools.js +527 -0
- package/dist/interaction/project-tools.js.map +1 -0
- package/dist/interaction/request-router.d.ts +3 -0
- package/dist/interaction/request-router.d.ts.map +1 -0
- package/dist/interaction/request-router.js +5 -0
- package/dist/interaction/request-router.js.map +1 -0
- package/dist/interaction/runtime.d.ts +54 -0
- package/dist/interaction/runtime.d.ts.map +1 -0
- package/dist/interaction/runtime.js +943 -0
- package/dist/interaction/runtime.js.map +1 -0
- package/dist/interaction/session.d.ts +352 -0
- package/dist/interaction/session.d.ts.map +1 -0
- package/dist/interaction/session.js +98 -0
- package/dist/interaction/session.js.map +1 -0
- package/dist/interaction/truth-authority.d.ts +4 -0
- package/dist/interaction/truth-authority.d.ts.map +1 -0
- package/dist/interaction/truth-authority.js +37 -0
- package/dist/interaction/truth-authority.js.map +1 -0
- package/dist/llm/provider.d.ts +87 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +798 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/models/book-rules.d.ts +118 -0
- package/dist/models/book-rules.d.ts.map +1 -0
- package/dist/models/book-rules.js +55 -0
- package/dist/models/book-rules.js.map +1 -0
- package/dist/models/book.d.ts +51 -0
- package/dist/models/book.d.ts.map +1 -0
- package/dist/models/book.js +27 -0
- package/dist/models/book.js.map +1 -0
- package/dist/models/chapter.d.ts +136 -0
- package/dist/models/chapter.d.ts.map +1 -0
- package/dist/models/chapter.js +38 -0
- package/dist/models/chapter.js.map +1 -0
- package/dist/models/detection.d.ts +25 -0
- package/dist/models/detection.d.ts.map +1 -0
- package/dist/models/detection.js +2 -0
- package/dist/models/detection.js.map +1 -0
- package/dist/models/genre-profile.d.ts +45 -0
- package/dist/models/genre-profile.d.ts.map +1 -0
- package/dist/models/genre-profile.js +26 -0
- package/dist/models/genre-profile.js.map +1 -0
- package/dist/models/input-governance.d.ts +516 -0
- package/dist/models/input-governance.d.ts.map +1 -0
- package/dist/models/input-governance.js +112 -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 +475 -0
- package/dist/models/project.d.ts.map +1 -0
- package/dist/models/project.js +100 -0
- package/dist/models/project.js.map +1 -0
- package/dist/models/runtime-state.d.ts +588 -0
- package/dist/models/runtime-state.d.ts.map +1 -0
- package/dist/models/runtime-state.js +93 -0
- package/dist/models/runtime-state.js.map +1 -0
- package/dist/models/state.d.ts +48 -0
- package/dist/models/state.d.ts.map +1 -0
- package/dist/models/state.js +6 -0
- package/dist/models/state.js.map +1 -0
- package/dist/models/style-profile.d.ts +16 -0
- package/dist/models/style-profile.d.ts.map +1 -0
- package/dist/models/style-profile.js +2 -0
- package/dist/models/style-profile.js.map +1 -0
- package/dist/notify/dispatcher.d.ts +10 -0
- package/dist/notify/dispatcher.d.ts.map +1 -0
- package/dist/notify/dispatcher.js +55 -0
- package/dist/notify/dispatcher.js.map +1 -0
- package/dist/notify/feishu.d.ts +5 -0
- package/dist/notify/feishu.d.ts.map +1 -0
- package/dist/notify/feishu.js +26 -0
- package/dist/notify/feishu.js.map +1 -0
- package/dist/notify/telegram.d.ts +6 -0
- package/dist/notify/telegram.d.ts.map +1 -0
- package/dist/notify/telegram.js +17 -0
- package/dist/notify/telegram.js.map +1 -0
- package/dist/notify/webhook.d.ts +15 -0
- package/dist/notify/webhook.d.ts.map +1 -0
- package/dist/notify/webhook.js +28 -0
- package/dist/notify/webhook.js.map +1 -0
- package/dist/notify/wechat-work.d.ts +5 -0
- package/dist/notify/wechat-work.d.ts.map +1 -0
- package/dist/notify/wechat-work.js +15 -0
- package/dist/notify/wechat-work.js.map +1 -0
- package/dist/pipeline/agent.d.ts +15 -0
- package/dist/pipeline/agent.d.ts.map +1 -0
- package/dist/pipeline/agent.js +550 -0
- package/dist/pipeline/agent.js.map +1 -0
- package/dist/pipeline/chapter-persistence.d.ts +33 -0
- package/dist/pipeline/chapter-persistence.d.ts.map +1 -0
- package/dist/pipeline/chapter-persistence.js +35 -0
- package/dist/pipeline/chapter-persistence.js.map +1 -0
- package/dist/pipeline/chapter-review-cycle.d.ts +79 -0
- package/dist/pipeline/chapter-review-cycle.d.ts.map +1 -0
- package/dist/pipeline/chapter-review-cycle.js +97 -0
- package/dist/pipeline/chapter-review-cycle.js.map +1 -0
- package/dist/pipeline/chapter-state-recovery.d.ts +59 -0
- package/dist/pipeline/chapter-state-recovery.d.ts.map +1 -0
- package/dist/pipeline/chapter-state-recovery.js +133 -0
- package/dist/pipeline/chapter-state-recovery.js.map +1 -0
- package/dist/pipeline/chapter-truth-validation.d.ts +41 -0
- package/dist/pipeline/chapter-truth-validation.d.ts.map +1 -0
- package/dist/pipeline/chapter-truth-validation.js +67 -0
- package/dist/pipeline/chapter-truth-validation.js.map +1 -0
- package/dist/pipeline/detection-runner.d.ts +31 -0
- package/dist/pipeline/detection-runner.d.ts.map +1 -0
- package/dist/pipeline/detection-runner.js +109 -0
- package/dist/pipeline/detection-runner.js.map +1 -0
- package/dist/pipeline/persisted-governed-plan.d.ts +4 -0
- package/dist/pipeline/persisted-governed-plan.d.ts.map +1 -0
- package/dist/pipeline/persisted-governed-plan.js +85 -0
- package/dist/pipeline/persisted-governed-plan.js.map +1 -0
- package/dist/pipeline/runner.d.ts +212 -0
- package/dist/pipeline/runner.d.ts.map +1 -0
- package/dist/pipeline/runner.js +2265 -0
- package/dist/pipeline/runner.js.map +1 -0
- package/dist/pipeline/scheduler.d.ts +58 -0
- package/dist/pipeline/scheduler.d.ts.map +1 -0
- package/dist/pipeline/scheduler.js +322 -0
- package/dist/pipeline/scheduler.js.map +1 -0
- package/dist/state/manager.d.ts +48 -0
- package/dist/state/manager.d.ts.map +1 -0
- package/dist/state/manager.js +435 -0
- package/dist/state/manager.js.map +1 -0
- package/dist/state/memory-db.d.ts +77 -0
- package/dist/state/memory-db.d.ts.map +1 -0
- package/dist/state/memory-db.js +249 -0
- package/dist/state/memory-db.js.map +1 -0
- package/dist/state/runtime-state-store.d.ts +25 -0
- package/dist/state/runtime-state-store.d.ts.map +1 -0
- package/dist/state/runtime-state-store.js +108 -0
- package/dist/state/runtime-state-store.js.map +1 -0
- package/dist/state/state-bootstrap.d.ts +21 -0
- package/dist/state/state-bootstrap.d.ts.map +1 -0
- package/dist/state/state-bootstrap.js +434 -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 +166 -0
- package/dist/state/state-projections.js.map +1 -0
- package/dist/state/state-reducer.d.ts +13 -0
- package/dist/state/state-reducer.d.ts.map +1 -0
- package/dist/state/state-reducer.js +194 -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 +67 -0
- package/dist/state/state-validator.js.map +1 -0
- package/dist/utils/analytics.d.ts +39 -0
- package/dist/utils/analytics.d.ts.map +1 -0
- package/dist/utils/analytics.js +50 -0
- package/dist/utils/analytics.js.map +1 -0
- package/dist/utils/cadence-policy.d.ts +36 -0
- package/dist/utils/cadence-policy.d.ts.map +1 -0
- package/dist/utils/cadence-policy.js +38 -0
- package/dist/utils/cadence-policy.js.map +1 -0
- package/dist/utils/chapter-cadence.d.ts +34 -0
- package/dist/utils/chapter-cadence.d.ts.map +1 -0
- package/dist/utils/chapter-cadence.js +142 -0
- package/dist/utils/chapter-cadence.js.map +1 -0
- package/dist/utils/chapter-splitter.d.ts +18 -0
- package/dist/utils/chapter-splitter.d.ts.map +1 -0
- package/dist/utils/chapter-splitter.js +60 -0
- package/dist/utils/chapter-splitter.js.map +1 -0
- package/dist/utils/config-loader.d.ts +15 -0
- package/dist/utils/config-loader.d.ts.map +1 -0
- package/dist/utils/config-loader.js +136 -0
- package/dist/utils/config-loader.js.map +1 -0
- package/dist/utils/context-filter.d.ts +20 -0
- package/dist/utils/context-filter.d.ts.map +1 -0
- package/dist/utils/context-filter.js +134 -0
- package/dist/utils/context-filter.js.map +1 -0
- package/dist/utils/governed-context.d.ts +11 -0
- package/dist/utils/governed-context.d.ts.map +1 -0
- package/dist/utils/governed-context.js +42 -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 +296 -0
- package/dist/utils/governed-working-set.js.map +1 -0
- package/dist/utils/hook-agenda.d.ts +21 -0
- package/dist/utils/hook-agenda.d.ts.map +1 -0
- package/dist/utils/hook-agenda.js +95 -0
- package/dist/utils/hook-agenda.js.map +1 -0
- package/dist/utils/hook-arbiter.d.ts +15 -0
- package/dist/utils/hook-arbiter.d.ts.map +1 -0
- package/dist/utils/hook-arbiter.js +268 -0
- package/dist/utils/hook-arbiter.js.map +1 -0
- package/dist/utils/hook-governance.d.ts +28 -0
- package/dist/utils/hook-governance.d.ts.map +1 -0
- package/dist/utils/hook-governance.js +144 -0
- package/dist/utils/hook-governance.js.map +1 -0
- package/dist/utils/hook-health.d.ts +15 -0
- package/dist/utils/hook-health.d.ts.map +1 -0
- package/dist/utils/hook-health.js +128 -0
- package/dist/utils/hook-health.js.map +1 -0
- package/dist/utils/hook-lifecycle.d.ts +34 -0
- package/dist/utils/hook-lifecycle.d.ts.map +1 -0
- package/dist/utils/hook-lifecycle.js +125 -0
- package/dist/utils/hook-lifecycle.js.map +1 -0
- package/dist/utils/hook-policy.d.ts +74 -0
- package/dist/utils/hook-policy.d.ts.map +1 -0
- package/dist/utils/hook-policy.js +126 -0
- package/dist/utils/hook-policy.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/logger.d.ts +31 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +79 -0
- package/dist/utils/logger.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 +406 -0
- package/dist/utils/long-span-fatigue.js.map +1 -0
- package/dist/utils/memory-retrieval.d.ts +25 -0
- package/dist/utils/memory-retrieval.d.ts.map +1 -0
- package/dist/utils/memory-retrieval.js +319 -0
- package/dist/utils/memory-retrieval.js.map +1 -0
- package/dist/utils/pov-filter.d.ts +30 -0
- package/dist/utils/pov-filter.d.ts.map +1 -0
- package/dist/utils/pov-filter.js +129 -0
- package/dist/utils/pov-filter.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/dist/utils/story-markdown.d.ts +13 -0
- package/dist/utils/story-markdown.d.ts.map +1 -0
- package/dist/utils/story-markdown.js +218 -0
- package/dist/utils/story-markdown.js.map +1 -0
- package/dist/utils/web-search.d.ts +23 -0
- package/dist/utils/web-search.d.ts.map +1 -0
- package/dist/utils/web-search.js +68 -0
- package/dist/utils/web-search.js.map +1 -0
- package/genres/cozy.md +43 -0
- package/genres/cultivation.md +42 -0
- package/genres/dungeon-core.md +40 -0
- package/genres/horror.md +51 -0
- package/genres/isekai.md +43 -0
- package/genres/litrpg.md +43 -0
- package/genres/other.md +24 -0
- package/genres/progression.md +41 -0
- package/genres/romantasy.md +45 -0
- package/genres/sci-fi.md +42 -0
- package/genres/system-apocalypse.md +40 -0
- package/genres/tower-climber.md +41 -0
- package/genres/urban.md +53 -0
- package/genres/xianxia.md +46 -0
- package/genres/xuanhuan.md +64 -0
- package/package.json +58 -0
|
@@ -0,0 +1,2265 @@
|
|
|
1
|
+
import { chatCompletion, createLLMClient } from "../llm/provider.js";
|
|
2
|
+
import { ArchitectAgent } from "../agents/architect.js";
|
|
3
|
+
import { FoundationReviewerAgent } from "../agents/foundation-reviewer.js";
|
|
4
|
+
import { PlannerAgent } from "../agents/planner.js";
|
|
5
|
+
import { ComposerAgent } from "../agents/composer.js";
|
|
6
|
+
import { WriterAgent } from "../agents/writer.js";
|
|
7
|
+
import { LengthNormalizerAgent } from "../agents/length-normalizer.js";
|
|
8
|
+
import { ChapterAnalyzerAgent } from "../agents/chapter-analyzer.js";
|
|
9
|
+
import { ContinuityAuditor } from "../agents/continuity.js";
|
|
10
|
+
import { ReviserAgent, DEFAULT_REVISE_MODE } from "../agents/reviser.js";
|
|
11
|
+
import { StateValidatorAgent } from "../agents/state-validator.js";
|
|
12
|
+
import { RadarAgent } from "../agents/radar.js";
|
|
13
|
+
import { readGenreProfile } from "../agents/rules-reader.js";
|
|
14
|
+
import { analyzeAITells } from "../agents/ai-tells.js";
|
|
15
|
+
import { analyzeSensitiveWords } from "../agents/sensitive-words.js";
|
|
16
|
+
import { StateManager } from "../state/manager.js";
|
|
17
|
+
import { MemoryDB } from "../state/memory-db.js";
|
|
18
|
+
import { dispatchNotification, dispatchWebhookEvent } from "../notify/dispatcher.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, rename, rm, stat } from "node:fs/promises";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { parseStateDegradedReviewNote, resolveStateDegradedBaseStatus, retrySettlementAfterValidationFailure, } from "./chapter-state-recovery.js";
|
|
26
|
+
import { persistChapterArtifacts } from "./chapter-persistence.js";
|
|
27
|
+
import { runChapterReviewCycle } from "./chapter-review-cycle.js";
|
|
28
|
+
import { validateChapterTruthPersistence } from "./chapter-truth-validation.js";
|
|
29
|
+
import { loadPersistedPlan, relativeToBookDir } from "./persisted-governed-plan.js";
|
|
30
|
+
const SEQUENCE_LEVEL_CATEGORIES = new Set([
|
|
31
|
+
"Pacing Monotony", "节奏单调",
|
|
32
|
+
"Mood Monotony", "情绪单调",
|
|
33
|
+
"Title Collapse", "标题重复",
|
|
34
|
+
"Title Clustering", "标题聚集",
|
|
35
|
+
"Opening Pattern Repetition", "开头同构",
|
|
36
|
+
"Ending Pattern Repetition", "结尾同构",
|
|
37
|
+
]);
|
|
38
|
+
function isSequenceLevelCategory(category) {
|
|
39
|
+
return SEQUENCE_LEVEL_CATEGORIES.has(category);
|
|
40
|
+
}
|
|
41
|
+
export class PipelineRunner {
|
|
42
|
+
state;
|
|
43
|
+
config;
|
|
44
|
+
agentClients = new Map();
|
|
45
|
+
memoryIndexFallbackWarned = false;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = config;
|
|
48
|
+
this.state = new StateManager(config.projectRoot);
|
|
49
|
+
}
|
|
50
|
+
localize(language, messages) {
|
|
51
|
+
return language === "en" ? messages.en : messages.zh;
|
|
52
|
+
}
|
|
53
|
+
async resolveBookLanguage(book) {
|
|
54
|
+
if (book.language) {
|
|
55
|
+
return book.language;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const { profile } = await this.loadGenreProfile(book.genre);
|
|
59
|
+
return profile.language;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return "zh";
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async resolveBookLanguageById(bookId) {
|
|
66
|
+
try {
|
|
67
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
68
|
+
return await this.resolveBookLanguage(book);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return "zh";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
languageFromLengthSpec(lengthSpec) {
|
|
75
|
+
return lengthSpec.countingMode === "en_words" ? "en" : "zh";
|
|
76
|
+
}
|
|
77
|
+
logStage(language, message) {
|
|
78
|
+
this.config.logger?.info(`${this.localize(language, { zh: "阶段:", en: "Stage: " })}${this.localize(language, message)}`);
|
|
79
|
+
}
|
|
80
|
+
logInfo(language, message) {
|
|
81
|
+
this.config.logger?.info(this.localize(language, message));
|
|
82
|
+
}
|
|
83
|
+
logWarn(language, message) {
|
|
84
|
+
this.config.logger?.warn(this.localize(language, message));
|
|
85
|
+
}
|
|
86
|
+
async tryGenerateStyleGuide(bookId, referenceText, sourceName, language) {
|
|
87
|
+
try {
|
|
88
|
+
await this.generateStyleGuide(bookId, referenceText, sourceName);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const resolvedLanguage = language ?? await this.resolveBookLanguageById(bookId);
|
|
92
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
93
|
+
this.logWarn(resolvedLanguage, {
|
|
94
|
+
zh: `风格指纹提取失败,已跳过:${detail}`,
|
|
95
|
+
en: `Style fingerprint extraction failed and was skipped: ${detail}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async generateAndReviewFoundation(params) {
|
|
100
|
+
const maxRetries = params.maxRetries ?? 2;
|
|
101
|
+
let foundation = await params.generate();
|
|
102
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
103
|
+
this.logStage(params.stageLanguage, {
|
|
104
|
+
zh: `审核基础设定(第${attempt + 1}轮)`,
|
|
105
|
+
en: `reviewing foundation (round ${attempt + 1})`,
|
|
106
|
+
});
|
|
107
|
+
const review = await params.reviewer.review({
|
|
108
|
+
foundation,
|
|
109
|
+
mode: params.mode,
|
|
110
|
+
sourceCanon: params.sourceCanon,
|
|
111
|
+
styleGuide: params.styleGuide,
|
|
112
|
+
language: params.language,
|
|
113
|
+
});
|
|
114
|
+
this.config.logger?.info(`Foundation review: ${review.totalScore}/100 ${review.passed ? "PASSED" : "REJECTED"}`);
|
|
115
|
+
for (const dim of review.dimensions) {
|
|
116
|
+
this.config.logger?.info(` [${dim.score}] ${dim.name.slice(0, 40)}`);
|
|
117
|
+
}
|
|
118
|
+
if (review.passed) {
|
|
119
|
+
return foundation;
|
|
120
|
+
}
|
|
121
|
+
this.logWarn(params.stageLanguage, {
|
|
122
|
+
zh: `基础设定未通过审核(${review.totalScore}分),正在重新生成...`,
|
|
123
|
+
en: `Foundation rejected (${review.totalScore}/100), regenerating...`,
|
|
124
|
+
});
|
|
125
|
+
foundation = await params.generate(this.buildFoundationReviewFeedback(review, params.language));
|
|
126
|
+
}
|
|
127
|
+
// Final review
|
|
128
|
+
const finalReview = await params.reviewer.review({
|
|
129
|
+
foundation,
|
|
130
|
+
mode: params.mode,
|
|
131
|
+
sourceCanon: params.sourceCanon,
|
|
132
|
+
styleGuide: params.styleGuide,
|
|
133
|
+
language: params.language,
|
|
134
|
+
});
|
|
135
|
+
this.config.logger?.info(`Foundation final review: ${finalReview.totalScore}/100 ${finalReview.passed ? "PASSED" : "ACCEPTED (max retries)"}`);
|
|
136
|
+
return foundation;
|
|
137
|
+
}
|
|
138
|
+
buildFoundationReviewFeedback(review, language) {
|
|
139
|
+
const dimensionLines = review.dimensions
|
|
140
|
+
.map((dimension) => (language === "en"
|
|
141
|
+
? `- ${dimension.name} [${dimension.score}]: ${dimension.feedback}`
|
|
142
|
+
: `- ${dimension.name}(${dimension.score}分):${dimension.feedback}`))
|
|
143
|
+
.join("\n");
|
|
144
|
+
return language === "en"
|
|
145
|
+
? [
|
|
146
|
+
"## Overall Feedback",
|
|
147
|
+
review.overallFeedback,
|
|
148
|
+
"",
|
|
149
|
+
"## Dimension Notes",
|
|
150
|
+
dimensionLines || "- none",
|
|
151
|
+
].join("\n")
|
|
152
|
+
: [
|
|
153
|
+
"## 总评",
|
|
154
|
+
review.overallFeedback,
|
|
155
|
+
"",
|
|
156
|
+
"## 分项问题",
|
|
157
|
+
dimensionLines || "- 无",
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
agentCtx(bookId) {
|
|
161
|
+
return {
|
|
162
|
+
client: this.config.client,
|
|
163
|
+
model: this.config.model,
|
|
164
|
+
projectRoot: this.config.projectRoot,
|
|
165
|
+
bookId,
|
|
166
|
+
logger: this.config.logger,
|
|
167
|
+
onStreamProgress: this.config.onStreamProgress,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
resolveOverride(agentName) {
|
|
171
|
+
const override = this.config.modelOverrides?.[agentName];
|
|
172
|
+
if (!override) {
|
|
173
|
+
return { model: this.config.model, client: this.config.client };
|
|
174
|
+
}
|
|
175
|
+
if (typeof override === "string") {
|
|
176
|
+
return { model: override, client: this.config.client };
|
|
177
|
+
}
|
|
178
|
+
// Full override — needs its own client if baseUrl differs
|
|
179
|
+
if (!override.baseUrl) {
|
|
180
|
+
return { model: override.model, client: this.config.client };
|
|
181
|
+
}
|
|
182
|
+
const base = this.config.defaultLLMConfig;
|
|
183
|
+
const provider = override.provider ?? base?.provider ?? "custom";
|
|
184
|
+
const apiKeySource = override.apiKeyEnv
|
|
185
|
+
? `env:${override.apiKeyEnv}`
|
|
186
|
+
: `base:${base?.apiKey ?? ""}`;
|
|
187
|
+
const stream = override.stream ?? base?.stream ?? true;
|
|
188
|
+
const apiFormat = base?.apiFormat ?? "chat";
|
|
189
|
+
const cacheKey = [
|
|
190
|
+
provider,
|
|
191
|
+
override.baseUrl,
|
|
192
|
+
apiKeySource,
|
|
193
|
+
`stream:${stream}`,
|
|
194
|
+
`format:${apiFormat}`,
|
|
195
|
+
].join("|");
|
|
196
|
+
let client = this.agentClients.get(cacheKey);
|
|
197
|
+
if (!client) {
|
|
198
|
+
const apiKey = override.apiKeyEnv
|
|
199
|
+
? process.env[override.apiKeyEnv] ?? ""
|
|
200
|
+
: base?.apiKey ?? "";
|
|
201
|
+
client = createLLMClient({
|
|
202
|
+
provider,
|
|
203
|
+
baseUrl: override.baseUrl,
|
|
204
|
+
apiKey,
|
|
205
|
+
model: override.model,
|
|
206
|
+
temperature: base?.temperature ?? 0.7,
|
|
207
|
+
maxTokens: base?.maxTokens ?? 8192,
|
|
208
|
+
thinkingBudget: base?.thinkingBudget ?? 0,
|
|
209
|
+
apiFormat,
|
|
210
|
+
stream,
|
|
211
|
+
});
|
|
212
|
+
this.agentClients.set(cacheKey, client);
|
|
213
|
+
}
|
|
214
|
+
return { model: override.model, client };
|
|
215
|
+
}
|
|
216
|
+
agentCtxFor(agent, bookId) {
|
|
217
|
+
const { model, client } = this.resolveOverride(agent);
|
|
218
|
+
return {
|
|
219
|
+
client,
|
|
220
|
+
model,
|
|
221
|
+
projectRoot: this.config.projectRoot,
|
|
222
|
+
bookId,
|
|
223
|
+
logger: this.config.logger?.child(agent),
|
|
224
|
+
onStreamProgress: this.config.onStreamProgress,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async pathExists(path) {
|
|
228
|
+
try {
|
|
229
|
+
await stat(path);
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async loadGenreProfile(genre) {
|
|
237
|
+
const parsed = await readGenreProfile(this.config.projectRoot, genre);
|
|
238
|
+
return { profile: parsed.profile };
|
|
239
|
+
}
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Atomic operations (composable by OpenClaw or agent mode)
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
async runRadar() {
|
|
244
|
+
const radar = new RadarAgent(this.agentCtxFor("radar"), this.config.radarSources);
|
|
245
|
+
return radar.scan();
|
|
246
|
+
}
|
|
247
|
+
async initBook(book, options = {}) {
|
|
248
|
+
const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
|
|
249
|
+
const bookDir = this.state.bookDir(book.id);
|
|
250
|
+
const stagingBookDir = join(this.state.booksDir, `.tmp-book-create-${book.id}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`);
|
|
251
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
252
|
+
this.logStage(stageLanguage, { zh: "生成基础设定", en: "generating foundation" });
|
|
253
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
254
|
+
const reviewer = new FoundationReviewerAgent(this.agentCtxFor("foundation-reviewer", book.id));
|
|
255
|
+
const resolvedLanguage = (book.language ?? gp.language) === "en" ? "en" : "zh";
|
|
256
|
+
const foundation = await this.generateAndReviewFoundation({
|
|
257
|
+
generate: (reviewFeedback) => architect.generateFoundation(book, options.externalContext ?? this.config.externalContext, reviewFeedback),
|
|
258
|
+
reviewer,
|
|
259
|
+
mode: "original",
|
|
260
|
+
language: resolvedLanguage,
|
|
261
|
+
stageLanguage,
|
|
262
|
+
});
|
|
263
|
+
try {
|
|
264
|
+
this.logStage(stageLanguage, { zh: "保存书籍配置", en: "saving book config" });
|
|
265
|
+
await this.state.saveBookConfigAt(stagingBookDir, book);
|
|
266
|
+
this.logStage(stageLanguage, { zh: "写入基础设定文件", en: "writing foundation files" });
|
|
267
|
+
await architect.writeFoundationFiles(stagingBookDir, foundation, gp.numericalSystem, book.language ?? gp.language);
|
|
268
|
+
this.logStage(stageLanguage, { zh: "初始化控制文档", en: "initializing control documents" });
|
|
269
|
+
await this.state.ensureControlDocumentsAt(stagingBookDir, book.language ?? gp.language, options.authorIntent ?? this.config.externalContext);
|
|
270
|
+
if (options.currentFocus?.trim()) {
|
|
271
|
+
await writeFile(join(stagingBookDir, "story", "current_focus.md"), options.currentFocus.trimEnd() + "\n", "utf-8");
|
|
272
|
+
}
|
|
273
|
+
await this.state.saveChapterIndexAt(stagingBookDir, []);
|
|
274
|
+
this.logStage(stageLanguage, { zh: "创建初始快照", en: "creating initial snapshot" });
|
|
275
|
+
await this.state.snapshotStateAt(stagingBookDir, 0);
|
|
276
|
+
if (await this.pathExists(bookDir)) {
|
|
277
|
+
if (await this.state.isCompleteBookDirectory(bookDir)) {
|
|
278
|
+
throw new Error(`Book "${book.id}" already exists at books/${book.id}/. Use a different title or delete the existing book first.`);
|
|
279
|
+
}
|
|
280
|
+
await rm(bookDir, { recursive: true, force: true });
|
|
281
|
+
}
|
|
282
|
+
await rename(stagingBookDir, bookDir);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
await rm(stagingBookDir, { recursive: true, force: true }).catch(() => undefined);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Import external source material and generate fanfic_canon.md */
|
|
290
|
+
async importFanficCanon(bookId, sourceText, sourceName, fanficMode) {
|
|
291
|
+
const { FanficCanonImporter } = await import("../agents/fanfic-canon-importer.js");
|
|
292
|
+
const importer = new FanficCanonImporter(this.agentCtxFor("fanfic-canon-importer", bookId));
|
|
293
|
+
const result = await importer.importFromText(sourceText, sourceName, fanficMode);
|
|
294
|
+
const bookDir = this.state.bookDir(bookId);
|
|
295
|
+
const storyDir = join(bookDir, "story");
|
|
296
|
+
await mkdir(storyDir, { recursive: true });
|
|
297
|
+
await writeFile(join(storyDir, "fanfic_canon.md"), result.fullDocument, "utf-8");
|
|
298
|
+
return result.fullDocument;
|
|
299
|
+
}
|
|
300
|
+
/** One-step fanfic book creation: create book + import canon + generate foundation */
|
|
301
|
+
async initFanficBook(book, sourceText, sourceName, fanficMode) {
|
|
302
|
+
const bookDir = this.state.bookDir(book.id);
|
|
303
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
304
|
+
this.logStage(stageLanguage, { zh: "保存书籍配置", en: "saving book config" });
|
|
305
|
+
await this.state.saveBookConfig(book.id, book);
|
|
306
|
+
// Step 1: Import source material → fanfic_canon.md
|
|
307
|
+
this.logStage(stageLanguage, { zh: "导入同人正典", en: "importing fanfic canon" });
|
|
308
|
+
const fanficCanon = await this.importFanficCanon(book.id, sourceText, sourceName, fanficMode);
|
|
309
|
+
// Step 2: Generate foundation with review loop
|
|
310
|
+
const architect = new ArchitectAgent(this.agentCtxFor("architect", book.id));
|
|
311
|
+
const reviewer = new FoundationReviewerAgent(this.agentCtxFor("foundation-reviewer", book.id));
|
|
312
|
+
this.logStage(stageLanguage, { zh: "生成同人基础设定", en: "generating fanfic foundation" });
|
|
313
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
314
|
+
const resolvedLanguage = (book.language ?? gp.language) === "en" ? "en" : "zh";
|
|
315
|
+
const foundation = await this.generateAndReviewFoundation({
|
|
316
|
+
generate: (reviewFeedback) => architect.generateFanficFoundation(book, fanficCanon, fanficMode, reviewFeedback),
|
|
317
|
+
reviewer,
|
|
318
|
+
mode: "fanfic",
|
|
319
|
+
sourceCanon: fanficCanon,
|
|
320
|
+
language: resolvedLanguage,
|
|
321
|
+
stageLanguage,
|
|
322
|
+
});
|
|
323
|
+
this.logStage(stageLanguage, { zh: "写入基础设定文件", en: "writing foundation files" });
|
|
324
|
+
await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, book.language ?? gp.language);
|
|
325
|
+
this.logStage(stageLanguage, { zh: "初始化控制文档", en: "initializing control documents" });
|
|
326
|
+
await this.state.ensureControlDocuments(book.id, this.config.externalContext);
|
|
327
|
+
// Step 3: Generate style guide from source material
|
|
328
|
+
if (sourceText.length >= 500) {
|
|
329
|
+
this.logStage(stageLanguage, { zh: "提取原作风格指纹", en: "extracting source style fingerprint" });
|
|
330
|
+
await this.tryGenerateStyleGuide(book.id, sourceText, sourceName, stageLanguage);
|
|
331
|
+
}
|
|
332
|
+
// Step 4: Initialize chapters directory + snapshot
|
|
333
|
+
this.logStage(stageLanguage, { zh: "创建初始快照", en: "creating initial snapshot" });
|
|
334
|
+
await mkdir(join(bookDir, "chapters"), { recursive: true });
|
|
335
|
+
await this.state.saveChapterIndex(book.id, []);
|
|
336
|
+
await this.state.snapshotState(book.id, 0);
|
|
337
|
+
}
|
|
338
|
+
/** Write a single draft chapter. Saves chapter file + truth files + index + snapshot. */
|
|
339
|
+
async writeDraft(bookId, context, wordCount) {
|
|
340
|
+
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
341
|
+
try {
|
|
342
|
+
await this.state.ensureControlDocuments(bookId);
|
|
343
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
344
|
+
const bookDir = this.state.bookDir(bookId);
|
|
345
|
+
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
346
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
347
|
+
this.logStage(stageLanguage, { zh: "准备章节输入", en: "preparing chapter inputs" });
|
|
348
|
+
const writeInput = await this.prepareWriteInput(book, bookDir, chapterNumber, context ?? this.config.externalContext);
|
|
349
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
350
|
+
const lengthSpec = buildLengthSpec(wordCount ?? book.chapterWordCount, book.language ?? gp.language);
|
|
351
|
+
const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
|
|
352
|
+
this.logStage(stageLanguage, { zh: "撰写章节草稿", en: "writing chapter draft" });
|
|
353
|
+
const output = await writer.writeChapter({
|
|
354
|
+
book,
|
|
355
|
+
bookDir,
|
|
356
|
+
chapterNumber,
|
|
357
|
+
...writeInput,
|
|
358
|
+
lengthSpec,
|
|
359
|
+
...(wordCount ? { wordCountOverride: wordCount } : {}),
|
|
360
|
+
});
|
|
361
|
+
const writerCount = countChapterLength(output.content, lengthSpec.countingMode);
|
|
362
|
+
let totalUsage = output.tokenUsage ?? {
|
|
363
|
+
promptTokens: 0,
|
|
364
|
+
completionTokens: 0,
|
|
365
|
+
totalTokens: 0,
|
|
366
|
+
};
|
|
367
|
+
const normalizedDraft = await this.normalizeDraftLengthIfNeeded({
|
|
368
|
+
bookId,
|
|
369
|
+
chapterNumber,
|
|
370
|
+
chapterContent: output.content,
|
|
371
|
+
lengthSpec,
|
|
372
|
+
chapterIntent: writeInput.chapterIntent,
|
|
373
|
+
});
|
|
374
|
+
totalUsage = PipelineRunner.addUsage(totalUsage, normalizedDraft.tokenUsage);
|
|
375
|
+
const draftOutput = {
|
|
376
|
+
...output,
|
|
377
|
+
content: normalizedDraft.content,
|
|
378
|
+
wordCount: normalizedDraft.wordCount,
|
|
379
|
+
tokenUsage: totalUsage,
|
|
380
|
+
};
|
|
381
|
+
const lengthWarnings = this.buildLengthWarnings(chapterNumber, draftOutput.wordCount, lengthSpec);
|
|
382
|
+
const lengthTelemetry = this.buildLengthTelemetry({
|
|
383
|
+
lengthSpec,
|
|
384
|
+
writerCount,
|
|
385
|
+
postWriterNormalizeCount: normalizedDraft.wordCount,
|
|
386
|
+
postReviseCount: 0,
|
|
387
|
+
finalCount: draftOutput.wordCount,
|
|
388
|
+
normalizeApplied: normalizedDraft.applied,
|
|
389
|
+
lengthWarning: lengthWarnings.length > 0,
|
|
390
|
+
});
|
|
391
|
+
this.logLengthWarnings(lengthWarnings);
|
|
392
|
+
// Save chapter file
|
|
393
|
+
const chaptersDir = join(bookDir, "chapters");
|
|
394
|
+
const paddedNum = String(chapterNumber).padStart(4, "0");
|
|
395
|
+
const sanitized = draftOutput.title.replace(/[/\\?%*:|"<>]/g, "").replace(/\s+/g, "_").slice(0, 50);
|
|
396
|
+
const filename = `${paddedNum}_${sanitized}.md`;
|
|
397
|
+
const filePath = join(chaptersDir, filename);
|
|
398
|
+
const resolvedLang = book.language ?? gp.language;
|
|
399
|
+
const heading = resolvedLang === "en"
|
|
400
|
+
? `# Chapter ${chapterNumber}: ${draftOutput.title}`
|
|
401
|
+
: `# 第${chapterNumber}章 ${draftOutput.title}`;
|
|
402
|
+
await writeFile(filePath, `${heading}\n\n${draftOutput.content}`, "utf-8");
|
|
403
|
+
// Save truth files
|
|
404
|
+
this.logStage(stageLanguage, { zh: "落盘草稿与真相文件", en: "persisting draft and truth files" });
|
|
405
|
+
await writer.saveChapter(bookDir, draftOutput, gp.numericalSystem, resolvedLang);
|
|
406
|
+
await writer.saveNewTruthFiles(bookDir, draftOutput, resolvedLang);
|
|
407
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, draftOutput);
|
|
408
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
409
|
+
// Update index
|
|
410
|
+
const existingIndex = await this.state.loadChapterIndex(bookId);
|
|
411
|
+
const now = new Date().toISOString();
|
|
412
|
+
const newEntry = {
|
|
413
|
+
number: chapterNumber,
|
|
414
|
+
title: draftOutput.title,
|
|
415
|
+
status: "drafted",
|
|
416
|
+
wordCount: draftOutput.wordCount,
|
|
417
|
+
createdAt: now,
|
|
418
|
+
updatedAt: now,
|
|
419
|
+
auditIssues: [],
|
|
420
|
+
lengthWarnings,
|
|
421
|
+
lengthTelemetry,
|
|
422
|
+
...(draftOutput.tokenUsage ? { tokenUsage: draftOutput.tokenUsage } : {}),
|
|
423
|
+
};
|
|
424
|
+
await this.state.saveChapterIndex(bookId, [...existingIndex, newEntry]);
|
|
425
|
+
await this.markBookActiveIfNeeded(bookId);
|
|
426
|
+
// Snapshot
|
|
427
|
+
this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" });
|
|
428
|
+
await this.state.snapshotState(bookId, chapterNumber);
|
|
429
|
+
await this.syncCurrentStateFactHistory(bookId, chapterNumber);
|
|
430
|
+
await this.emitWebhook("chapter-complete", bookId, chapterNumber, {
|
|
431
|
+
title: draftOutput.title,
|
|
432
|
+
wordCount: draftOutput.wordCount,
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
chapterNumber,
|
|
436
|
+
title: draftOutput.title,
|
|
437
|
+
wordCount: draftOutput.wordCount,
|
|
438
|
+
filePath,
|
|
439
|
+
lengthWarnings,
|
|
440
|
+
lengthTelemetry,
|
|
441
|
+
tokenUsage: draftOutput.tokenUsage,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
await releaseLock();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
async planChapter(bookId, context) {
|
|
449
|
+
await this.state.ensureControlDocuments(bookId);
|
|
450
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
451
|
+
const bookDir = this.state.bookDir(bookId);
|
|
452
|
+
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
453
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
454
|
+
this.logStage(stageLanguage, { zh: "规划下一章意图", en: "planning next chapter intent" });
|
|
455
|
+
const { plan } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, context ?? this.config.externalContext, { reuseExistingIntentWhenContextMissing: false });
|
|
456
|
+
return {
|
|
457
|
+
bookId,
|
|
458
|
+
chapterNumber,
|
|
459
|
+
intentPath: relativeToBookDir(bookDir, plan.runtimePath),
|
|
460
|
+
goal: plan.intent.goal,
|
|
461
|
+
conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
async composeChapter(bookId, context) {
|
|
465
|
+
await this.state.ensureControlDocuments(bookId);
|
|
466
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
467
|
+
const bookDir = this.state.bookDir(bookId);
|
|
468
|
+
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
469
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
470
|
+
this.logStage(stageLanguage, { zh: "组装章节运行时上下文", en: "composing chapter runtime context" });
|
|
471
|
+
const { plan, composed } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, context ?? this.config.externalContext, { reuseExistingIntentWhenContextMissing: true });
|
|
472
|
+
return {
|
|
473
|
+
bookId,
|
|
474
|
+
chapterNumber,
|
|
475
|
+
intentPath: relativeToBookDir(bookDir, plan.runtimePath),
|
|
476
|
+
goal: plan.intent.goal,
|
|
477
|
+
conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
|
|
478
|
+
contextPath: relativeToBookDir(bookDir, composed.contextPath),
|
|
479
|
+
ruleStackPath: relativeToBookDir(bookDir, composed.ruleStackPath),
|
|
480
|
+
tracePath: relativeToBookDir(bookDir, composed.tracePath),
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/** Audit the latest (or specified) chapter. Read-only, no lock needed. */
|
|
484
|
+
async auditDraft(bookId, chapterNumber) {
|
|
485
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
486
|
+
const bookDir = this.state.bookDir(bookId);
|
|
487
|
+
const targetChapter = chapterNumber ?? (await this.state.getNextChapterNumber(bookId)) - 1;
|
|
488
|
+
if (targetChapter < 1) {
|
|
489
|
+
throw new Error(`No chapters to audit for "${bookId}"`);
|
|
490
|
+
}
|
|
491
|
+
const content = await this.readChapterContent(bookDir, targetChapter);
|
|
492
|
+
const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
|
|
493
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
494
|
+
const language = book.language ?? gp.language;
|
|
495
|
+
this.logStage(language, {
|
|
496
|
+
zh: `审计第${targetChapter}章`,
|
|
497
|
+
en: `auditing chapter ${targetChapter}`,
|
|
498
|
+
});
|
|
499
|
+
const evaluation = await this.evaluateMergedAudit({
|
|
500
|
+
auditor,
|
|
501
|
+
book,
|
|
502
|
+
bookDir,
|
|
503
|
+
chapterContent: content,
|
|
504
|
+
chapterNumber: targetChapter,
|
|
505
|
+
language,
|
|
506
|
+
});
|
|
507
|
+
const result = evaluation.auditResult;
|
|
508
|
+
// Update index with audit result
|
|
509
|
+
const index = await this.state.loadChapterIndex(bookId);
|
|
510
|
+
const updated = index.map((ch) => ch.number === targetChapter
|
|
511
|
+
? {
|
|
512
|
+
...ch,
|
|
513
|
+
status: (result.passed ? "ready-for-review" : "audit-failed"),
|
|
514
|
+
updatedAt: new Date().toISOString(),
|
|
515
|
+
auditIssues: result.issues.map((i) => `[${i.severity}] ${i.description}`),
|
|
516
|
+
}
|
|
517
|
+
: ch);
|
|
518
|
+
await this.state.saveChapterIndex(bookId, updated);
|
|
519
|
+
const latestChapter = index.length > 0 ? Math.max(...index.map((chapter) => chapter.number)) : targetChapter;
|
|
520
|
+
if (targetChapter === latestChapter) {
|
|
521
|
+
await this.persistAuditDriftGuidance({
|
|
522
|
+
bookDir,
|
|
523
|
+
chapterNumber: targetChapter,
|
|
524
|
+
issues: result.issues.filter((issue) => issue.severity === "critical" || issue.severity === "warning"),
|
|
525
|
+
language,
|
|
526
|
+
}).catch(() => undefined);
|
|
527
|
+
}
|
|
528
|
+
await this.emitWebhook(result.passed ? "audit-passed" : "audit-failed", bookId, targetChapter, { summary: result.summary, issueCount: result.issues.length });
|
|
529
|
+
return { ...result, chapterNumber: targetChapter };
|
|
530
|
+
}
|
|
531
|
+
/** Revise the latest (or specified) chapter based on audit issues. */
|
|
532
|
+
async reviseDraft(bookId, chapterNumber, mode = DEFAULT_REVISE_MODE) {
|
|
533
|
+
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
534
|
+
try {
|
|
535
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
536
|
+
const bookDir = this.state.bookDir(bookId);
|
|
537
|
+
const targetChapter = chapterNumber ?? (await this.state.getNextChapterNumber(bookId)) - 1;
|
|
538
|
+
if (targetChapter < 1) {
|
|
539
|
+
throw new Error(`No chapters to revise for "${bookId}"`);
|
|
540
|
+
}
|
|
541
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
542
|
+
// Read the current audit issues from index
|
|
543
|
+
this.logStage(stageLanguage, {
|
|
544
|
+
zh: `加载第${targetChapter}章修订上下文`,
|
|
545
|
+
en: `loading revision context for chapter ${targetChapter}`,
|
|
546
|
+
});
|
|
547
|
+
const index = await this.state.loadChapterIndex(bookId);
|
|
548
|
+
const chapterMeta = index.find((ch) => ch.number === targetChapter);
|
|
549
|
+
if (!chapterMeta) {
|
|
550
|
+
throw new Error(`Chapter ${targetChapter} not found in index`);
|
|
551
|
+
}
|
|
552
|
+
// Re-audit to get structured issues (index only stores strings)
|
|
553
|
+
const content = await this.readChapterContent(bookDir, targetChapter);
|
|
554
|
+
const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
|
|
555
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
556
|
+
const language = book.language ?? gp.language;
|
|
557
|
+
const countingMode = resolveLengthCountingMode(language);
|
|
558
|
+
const reviseControlInput = (this.config.inputGovernanceMode ?? "v2") === "legacy"
|
|
559
|
+
? undefined
|
|
560
|
+
: await this.createGovernedArtifacts(book, bookDir, targetChapter, this.config.externalContext, { reuseExistingIntentWhenContextMissing: true });
|
|
561
|
+
const preRevision = await this.evaluateMergedAudit({
|
|
562
|
+
auditor,
|
|
563
|
+
book,
|
|
564
|
+
bookDir,
|
|
565
|
+
chapterContent: content,
|
|
566
|
+
chapterNumber: targetChapter,
|
|
567
|
+
language,
|
|
568
|
+
auditOptions: reviseControlInput
|
|
569
|
+
? {
|
|
570
|
+
chapterIntent: reviseControlInput.plan.intentMarkdown,
|
|
571
|
+
contextPackage: reviseControlInput.composed.contextPackage,
|
|
572
|
+
ruleStack: reviseControlInput.composed.ruleStack,
|
|
573
|
+
}
|
|
574
|
+
: undefined,
|
|
575
|
+
});
|
|
576
|
+
if (preRevision.blockingCount === 0 && preRevision.aiTellCount === 0) {
|
|
577
|
+
return {
|
|
578
|
+
chapterNumber: targetChapter,
|
|
579
|
+
wordCount: countChapterLength(content, countingMode),
|
|
580
|
+
fixedIssues: [],
|
|
581
|
+
applied: false,
|
|
582
|
+
status: "unchanged",
|
|
583
|
+
skippedReason: "No warning, critical, or AI-tell issues to fix.",
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
const chapterLengthTarget = chapterMeta.lengthTelemetry?.target ?? book.chapterWordCount;
|
|
587
|
+
const lengthLanguage = chapterMeta.lengthTelemetry?.countingMode === "en_words"
|
|
588
|
+
? "en"
|
|
589
|
+
: language;
|
|
590
|
+
const lengthSpec = buildLengthSpec(chapterLengthTarget, lengthLanguage);
|
|
591
|
+
const reviser = new ReviserAgent(this.agentCtxFor("reviser", bookId));
|
|
592
|
+
this.logStage(stageLanguage, {
|
|
593
|
+
zh: `修订第${targetChapter}章`,
|
|
594
|
+
en: `revising chapter ${targetChapter}`,
|
|
595
|
+
});
|
|
596
|
+
const reviseOutput = await reviser.reviseChapter(bookDir, content, targetChapter, preRevision.auditResult.issues, mode, book.genre, reviseControlInput
|
|
597
|
+
? {
|
|
598
|
+
chapterIntent: reviseControlInput.plan.intentMarkdown,
|
|
599
|
+
contextPackage: reviseControlInput.composed.contextPackage,
|
|
600
|
+
ruleStack: reviseControlInput.composed.ruleStack,
|
|
601
|
+
lengthSpec,
|
|
602
|
+
}
|
|
603
|
+
: { lengthSpec });
|
|
604
|
+
if (reviseOutput.revisedContent.length === 0) {
|
|
605
|
+
throw new Error("Reviser returned empty content");
|
|
606
|
+
}
|
|
607
|
+
const normalizedRevision = await this.normalizeDraftLengthIfNeeded({
|
|
608
|
+
bookId,
|
|
609
|
+
chapterNumber: targetChapter,
|
|
610
|
+
chapterContent: reviseOutput.revisedContent,
|
|
611
|
+
lengthSpec,
|
|
612
|
+
});
|
|
613
|
+
const postRevision = await this.evaluateMergedAudit({
|
|
614
|
+
auditor,
|
|
615
|
+
book,
|
|
616
|
+
bookDir,
|
|
617
|
+
chapterContent: normalizedRevision.content,
|
|
618
|
+
chapterNumber: targetChapter,
|
|
619
|
+
language,
|
|
620
|
+
auditOptions: reviseControlInput
|
|
621
|
+
? {
|
|
622
|
+
temperature: 0,
|
|
623
|
+
chapterIntent: reviseControlInput.plan.intentMarkdown,
|
|
624
|
+
contextPackage: reviseControlInput.composed.contextPackage,
|
|
625
|
+
ruleStack: reviseControlInput.composed.ruleStack,
|
|
626
|
+
truthFileOverrides: {
|
|
627
|
+
currentState: reviseOutput.updatedState !== "(状态卡未更新)" ? reviseOutput.updatedState : undefined,
|
|
628
|
+
ledger: reviseOutput.updatedLedger !== "(账本未更新)" ? reviseOutput.updatedLedger : undefined,
|
|
629
|
+
hooks: reviseOutput.updatedHooks !== "(伏笔池未更新)" ? reviseOutput.updatedHooks : undefined,
|
|
630
|
+
},
|
|
631
|
+
}
|
|
632
|
+
: {
|
|
633
|
+
temperature: 0,
|
|
634
|
+
truthFileOverrides: {
|
|
635
|
+
currentState: reviseOutput.updatedState !== "(状态卡未更新)" ? reviseOutput.updatedState : undefined,
|
|
636
|
+
ledger: reviseOutput.updatedLedger !== "(账本未更新)" ? reviseOutput.updatedLedger : undefined,
|
|
637
|
+
hooks: reviseOutput.updatedHooks !== "(伏笔池未更新)" ? reviseOutput.updatedHooks : undefined,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
const effectivePostRevision = this.restoreActionableAuditIfLost(preRevision, postRevision);
|
|
642
|
+
const revisionBaseCount = countChapterLength(content, lengthSpec.countingMode);
|
|
643
|
+
const lengthWarnings = this.buildLengthWarnings(targetChapter, normalizedRevision.wordCount, lengthSpec);
|
|
644
|
+
const lengthTelemetry = this.buildLengthTelemetry({
|
|
645
|
+
lengthSpec,
|
|
646
|
+
writerCount: revisionBaseCount,
|
|
647
|
+
postWriterNormalizeCount: 0,
|
|
648
|
+
postReviseCount: normalizedRevision.wordCount,
|
|
649
|
+
finalCount: normalizedRevision.wordCount,
|
|
650
|
+
normalizeApplied: normalizedRevision.applied,
|
|
651
|
+
lengthWarning: lengthWarnings.length > 0,
|
|
652
|
+
});
|
|
653
|
+
const improvedBlocking = effectivePostRevision.blockingCount < preRevision.blockingCount;
|
|
654
|
+
const improvedAITells = effectivePostRevision.aiTellCount < preRevision.aiTellCount;
|
|
655
|
+
const blockingDidNotWorsen = effectivePostRevision.blockingCount <= preRevision.blockingCount;
|
|
656
|
+
const criticalDidNotWorsen = effectivePostRevision.criticalCount <= preRevision.criticalCount;
|
|
657
|
+
const aiDidNotWorsen = effectivePostRevision.aiTellCount <= preRevision.aiTellCount;
|
|
658
|
+
const shouldApplyRevision = blockingDidNotWorsen
|
|
659
|
+
&& criticalDidNotWorsen
|
|
660
|
+
&& aiDidNotWorsen
|
|
661
|
+
&& (improvedBlocking || improvedAITells);
|
|
662
|
+
if (!shouldApplyRevision) {
|
|
663
|
+
return {
|
|
664
|
+
chapterNumber: targetChapter,
|
|
665
|
+
wordCount: revisionBaseCount,
|
|
666
|
+
fixedIssues: [],
|
|
667
|
+
applied: false,
|
|
668
|
+
status: "unchanged",
|
|
669
|
+
skippedReason: "Manual revision did not improve merged audit or AI-tell metrics; kept original chapter.",
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
this.logLengthWarnings(lengthWarnings);
|
|
673
|
+
// Save revised chapter file
|
|
674
|
+
this.logStage(stageLanguage, {
|
|
675
|
+
zh: `落盘第${targetChapter}章修订结果`,
|
|
676
|
+
en: `persisting revision for chapter ${targetChapter}`,
|
|
677
|
+
});
|
|
678
|
+
const chaptersDir = join(bookDir, "chapters");
|
|
679
|
+
const files = await readdir(chaptersDir);
|
|
680
|
+
const paddedNum = String(targetChapter).padStart(4, "0");
|
|
681
|
+
const existingFile = files.find((f) => f.startsWith(paddedNum) && f.endsWith(".md"));
|
|
682
|
+
if (!existingFile) {
|
|
683
|
+
throw new Error(`Chapter ${targetChapter} file not found in ${chaptersDir} (expected filename starting with ${paddedNum})`);
|
|
684
|
+
}
|
|
685
|
+
const reviseLang = book.language ?? gp.language;
|
|
686
|
+
const reviseHeading = reviseLang === "en"
|
|
687
|
+
? `# Chapter ${targetChapter}: ${chapterMeta.title}`
|
|
688
|
+
: `# 第${targetChapter}章 ${chapterMeta.title}`;
|
|
689
|
+
await writeFile(join(chaptersDir, existingFile), `${reviseHeading}\n\n${normalizedRevision.content}`, "utf-8");
|
|
690
|
+
// Update truth files
|
|
691
|
+
const storyDir = join(bookDir, "story");
|
|
692
|
+
if (reviseOutput.updatedState !== "(状态卡未更新)") {
|
|
693
|
+
await writeFile(join(storyDir, "current_state.md"), reviseOutput.updatedState, "utf-8");
|
|
694
|
+
}
|
|
695
|
+
if (gp.numericalSystem && reviseOutput.updatedLedger && reviseOutput.updatedLedger !== "(账本未更新)") {
|
|
696
|
+
await writeFile(join(storyDir, "particle_ledger.md"), reviseOutput.updatedLedger, "utf-8");
|
|
697
|
+
}
|
|
698
|
+
if (reviseOutput.updatedHooks !== "(伏笔池未更新)") {
|
|
699
|
+
await writeFile(join(storyDir, "pending_hooks.md"), reviseOutput.updatedHooks, "utf-8");
|
|
700
|
+
}
|
|
701
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, targetChapter);
|
|
702
|
+
// Update index
|
|
703
|
+
const updatedIndex = index.map((ch) => ch.number === targetChapter
|
|
704
|
+
? {
|
|
705
|
+
...ch,
|
|
706
|
+
status: (effectivePostRevision.auditResult.passed ? "ready-for-review" : "audit-failed"),
|
|
707
|
+
wordCount: normalizedRevision.wordCount,
|
|
708
|
+
updatedAt: new Date().toISOString(),
|
|
709
|
+
auditIssues: effectivePostRevision.auditResult.issues.map((i) => `[${i.severity}] ${i.description}`),
|
|
710
|
+
lengthWarnings,
|
|
711
|
+
lengthTelemetry,
|
|
712
|
+
}
|
|
713
|
+
: ch);
|
|
714
|
+
await this.state.saveChapterIndex(bookId, updatedIndex);
|
|
715
|
+
const latestChapter = index.length > 0 ? Math.max(...index.map((chapter) => chapter.number)) : targetChapter;
|
|
716
|
+
if (targetChapter === latestChapter) {
|
|
717
|
+
await this.persistAuditDriftGuidance({
|
|
718
|
+
bookDir,
|
|
719
|
+
chapterNumber: targetChapter,
|
|
720
|
+
issues: effectivePostRevision.auditResult.issues.filter((issue) => issue.severity === "critical" || issue.severity === "warning"),
|
|
721
|
+
language,
|
|
722
|
+
}).catch(() => undefined);
|
|
723
|
+
}
|
|
724
|
+
// Re-snapshot
|
|
725
|
+
this.logStage(stageLanguage, {
|
|
726
|
+
zh: `更新第${targetChapter}章索引与快照`,
|
|
727
|
+
en: `updating chapter index and snapshots for chapter ${targetChapter}`,
|
|
728
|
+
});
|
|
729
|
+
await this.state.snapshotState(bookId, targetChapter);
|
|
730
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
731
|
+
await this.syncCurrentStateFactHistory(bookId, targetChapter);
|
|
732
|
+
await this.emitWebhook("revision-complete", bookId, targetChapter, {
|
|
733
|
+
wordCount: normalizedRevision.wordCount,
|
|
734
|
+
fixedCount: reviseOutput.fixedIssues.length,
|
|
735
|
+
});
|
|
736
|
+
return {
|
|
737
|
+
chapterNumber: targetChapter,
|
|
738
|
+
wordCount: normalizedRevision.wordCount,
|
|
739
|
+
fixedIssues: reviseOutput.fixedIssues,
|
|
740
|
+
applied: true,
|
|
741
|
+
status: effectivePostRevision.auditResult.passed ? "ready-for-review" : "audit-failed",
|
|
742
|
+
lengthWarnings,
|
|
743
|
+
lengthTelemetry,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
finally {
|
|
747
|
+
await releaseLock();
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
/** Read all truth files for a book. */
|
|
751
|
+
async readTruthFiles(bookId) {
|
|
752
|
+
const bookDir = this.state.bookDir(bookId);
|
|
753
|
+
const storyDir = join(bookDir, "story");
|
|
754
|
+
const readSafe = async (path) => {
|
|
755
|
+
try {
|
|
756
|
+
return await readFile(path, "utf-8");
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return "(文件不存在)";
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
const [currentState, particleLedger, pendingHooks, storyBible, volumeOutline, bookRules] = await Promise.all([
|
|
763
|
+
readSafe(join(storyDir, "current_state.md")),
|
|
764
|
+
readSafe(join(storyDir, "particle_ledger.md")),
|
|
765
|
+
readSafe(join(storyDir, "pending_hooks.md")),
|
|
766
|
+
readSafe(join(storyDir, "story_bible.md")),
|
|
767
|
+
readSafe(join(storyDir, "volume_outline.md")),
|
|
768
|
+
readSafe(join(storyDir, "book_rules.md")),
|
|
769
|
+
]);
|
|
770
|
+
return { currentState, particleLedger, pendingHooks, storyBible, volumeOutline, bookRules };
|
|
771
|
+
}
|
|
772
|
+
/** Get book status overview. */
|
|
773
|
+
async getBookStatus(bookId) {
|
|
774
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
775
|
+
const chapters = await this.state.loadChapterIndex(bookId);
|
|
776
|
+
const nextChapter = await this.state.getNextChapterNumber(bookId);
|
|
777
|
+
const totalWords = chapters.reduce((sum, ch) => sum + ch.wordCount, 0);
|
|
778
|
+
return {
|
|
779
|
+
bookId,
|
|
780
|
+
title: book.title,
|
|
781
|
+
genre: book.genre,
|
|
782
|
+
platform: book.platform,
|
|
783
|
+
status: book.status,
|
|
784
|
+
chaptersWritten: chapters.length,
|
|
785
|
+
totalWords,
|
|
786
|
+
nextChapter,
|
|
787
|
+
chapters: [...chapters],
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
// ---------------------------------------------------------------------------
|
|
791
|
+
// Full pipeline (convenience — runs draft + audit + revise in one shot)
|
|
792
|
+
// ---------------------------------------------------------------------------
|
|
793
|
+
async writeNextChapter(bookId, wordCount, temperatureOverride) {
|
|
794
|
+
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
795
|
+
try {
|
|
796
|
+
return await this._writeNextChapterLocked(bookId, wordCount, temperatureOverride);
|
|
797
|
+
}
|
|
798
|
+
finally {
|
|
799
|
+
await releaseLock();
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
async repairChapterState(bookId, chapterNumber) {
|
|
803
|
+
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
804
|
+
try {
|
|
805
|
+
return await this._repairChapterStateLocked(bookId, chapterNumber);
|
|
806
|
+
}
|
|
807
|
+
finally {
|
|
808
|
+
await releaseLock();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
async resyncChapterArtifacts(bookId, chapterNumber) {
|
|
812
|
+
const releaseLock = await this.state.acquireBookLock(bookId);
|
|
813
|
+
try {
|
|
814
|
+
return await this._resyncChapterArtifactsLocked(bookId, chapterNumber);
|
|
815
|
+
}
|
|
816
|
+
finally {
|
|
817
|
+
await releaseLock();
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async _writeNextChapterLocked(bookId, wordCount, temperatureOverride) {
|
|
821
|
+
await this.state.ensureControlDocuments(bookId);
|
|
822
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
823
|
+
const bookDir = this.state.bookDir(bookId);
|
|
824
|
+
await this.assertNoPendingStateRepair(bookId);
|
|
825
|
+
const chapterNumber = await this.state.getNextChapterNumber(bookId);
|
|
826
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
827
|
+
this.logStage(stageLanguage, { zh: "准备章节输入", en: "preparing chapter inputs" });
|
|
828
|
+
const writeInput = await this.prepareWriteInput(book, bookDir, chapterNumber, this.config.externalContext);
|
|
829
|
+
const reducedControlInput = writeInput.chapterIntent && writeInput.contextPackage && writeInput.ruleStack
|
|
830
|
+
? {
|
|
831
|
+
chapterIntent: writeInput.chapterIntent,
|
|
832
|
+
contextPackage: writeInput.contextPackage,
|
|
833
|
+
ruleStack: writeInput.ruleStack,
|
|
834
|
+
}
|
|
835
|
+
: undefined;
|
|
836
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
837
|
+
const pipelineLang = book.language ?? gp.language;
|
|
838
|
+
const lengthSpec = buildLengthSpec(wordCount ?? book.chapterWordCount, pipelineLang);
|
|
839
|
+
// 1. Write chapter
|
|
840
|
+
const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
|
|
841
|
+
this.logStage(stageLanguage, { zh: "撰写章节草稿", en: "writing chapter draft" });
|
|
842
|
+
const output = await writer.writeChapter({
|
|
843
|
+
book,
|
|
844
|
+
bookDir,
|
|
845
|
+
chapterNumber,
|
|
846
|
+
...writeInput,
|
|
847
|
+
lengthSpec,
|
|
848
|
+
...(wordCount ? { wordCountOverride: wordCount } : {}),
|
|
849
|
+
...(temperatureOverride ? { temperatureOverride } : {}),
|
|
850
|
+
});
|
|
851
|
+
const writerCount = countChapterLength(output.content, lengthSpec.countingMode);
|
|
852
|
+
// Token usage accumulator
|
|
853
|
+
let totalUsage = output.tokenUsage ?? { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
854
|
+
const auditor = new ContinuityAuditor(this.agentCtxFor("auditor", bookId));
|
|
855
|
+
const reviewResult = await runChapterReviewCycle({
|
|
856
|
+
book: { genre: book.genre },
|
|
857
|
+
bookDir,
|
|
858
|
+
chapterNumber,
|
|
859
|
+
initialOutput: output,
|
|
860
|
+
reducedControlInput,
|
|
861
|
+
lengthSpec,
|
|
862
|
+
initialUsage: totalUsage,
|
|
863
|
+
createReviser: () => new ReviserAgent(this.agentCtxFor("reviser", bookId)),
|
|
864
|
+
auditor,
|
|
865
|
+
normalizeDraftLengthIfNeeded: (chapterContent) => this.normalizeDraftLengthIfNeeded({
|
|
866
|
+
bookId,
|
|
867
|
+
chapterNumber,
|
|
868
|
+
chapterContent,
|
|
869
|
+
lengthSpec,
|
|
870
|
+
chapterIntent: writeInput.chapterIntent,
|
|
871
|
+
}),
|
|
872
|
+
assertChapterContentNotEmpty: (content, stage) => this.assertChapterContentNotEmpty(content, chapterNumber, stage),
|
|
873
|
+
addUsage: PipelineRunner.addUsage,
|
|
874
|
+
restoreLostAuditIssues: (previous, next) => this.restoreLostAuditIssues(previous, next),
|
|
875
|
+
analyzeAITells,
|
|
876
|
+
analyzeSensitiveWords,
|
|
877
|
+
logWarn: (message) => this.logWarn(pipelineLang, message),
|
|
878
|
+
logStage: (message) => this.logStage(stageLanguage, message),
|
|
879
|
+
});
|
|
880
|
+
totalUsage = reviewResult.totalUsage;
|
|
881
|
+
let finalContent = reviewResult.finalContent;
|
|
882
|
+
let finalWordCount = reviewResult.finalWordCount;
|
|
883
|
+
let revised = reviewResult.revised;
|
|
884
|
+
let auditResult = reviewResult.auditResult;
|
|
885
|
+
const postReviseCount = reviewResult.postReviseCount;
|
|
886
|
+
const normalizeApplied = reviewResult.normalizeApplied;
|
|
887
|
+
// 4. Save the final chapter and truth files from a single persistence source
|
|
888
|
+
this.logStage(stageLanguage, { zh: "落盘最终章节", en: "persisting final chapter" });
|
|
889
|
+
this.logStage(stageLanguage, { zh: "生成最终真相文件", en: "rebuilding final truth files" });
|
|
890
|
+
const chapterIndexBeforePersist = await this.state.loadChapterIndex(bookId);
|
|
891
|
+
const { resolveDuplicateTitle } = await import("../agents/post-write-validator.js");
|
|
892
|
+
const initialTitleResolution = resolveDuplicateTitle(output.title, chapterIndexBeforePersist.map((chapter) => chapter.title), pipelineLang, { content: finalContent });
|
|
893
|
+
let persistenceOutput = await this.buildPersistenceOutput(bookId, book, bookDir, chapterNumber, initialTitleResolution.title === output.title
|
|
894
|
+
? output
|
|
895
|
+
: { ...output, title: initialTitleResolution.title }, finalContent, lengthSpec.countingMode, reducedControlInput);
|
|
896
|
+
const finalTitleResolution = resolveDuplicateTitle(persistenceOutput.title, chapterIndexBeforePersist.map((chapter) => chapter.title), pipelineLang, { content: finalContent });
|
|
897
|
+
if (finalTitleResolution.title !== persistenceOutput.title) {
|
|
898
|
+
persistenceOutput = {
|
|
899
|
+
...persistenceOutput,
|
|
900
|
+
title: finalTitleResolution.title,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
if (persistenceOutput.title !== output.title) {
|
|
904
|
+
const description = pipelineLang === "en"
|
|
905
|
+
? `Chapter title "${output.title}" was auto-adjusted to "${persistenceOutput.title}".`
|
|
906
|
+
: `章节标题"${output.title}"已自动调整为"${persistenceOutput.title}"。`;
|
|
907
|
+
this.config.logger?.warn(`[title] ${description}`);
|
|
908
|
+
auditResult = {
|
|
909
|
+
...auditResult,
|
|
910
|
+
issues: [...auditResult.issues, {
|
|
911
|
+
severity: "warning",
|
|
912
|
+
category: "title-dedup",
|
|
913
|
+
description,
|
|
914
|
+
suggestion: pipelineLang === "en"
|
|
915
|
+
? "If the auto-renamed title is weak, revise the chapter title manually."
|
|
916
|
+
: "如果自动改名不理想,可以在后续手动修订章节标题。",
|
|
917
|
+
}],
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const longSpanFatigue = await analyzeLongSpanFatigue({
|
|
921
|
+
bookDir,
|
|
922
|
+
chapterNumber,
|
|
923
|
+
chapterContent: finalContent,
|
|
924
|
+
chapterSummary: persistenceOutput.chapterSummary,
|
|
925
|
+
language: pipelineLang,
|
|
926
|
+
});
|
|
927
|
+
auditResult = {
|
|
928
|
+
...auditResult,
|
|
929
|
+
issues: [
|
|
930
|
+
...auditResult.issues,
|
|
931
|
+
...longSpanFatigue.issues,
|
|
932
|
+
...(persistenceOutput.hookHealthIssues ?? []),
|
|
933
|
+
],
|
|
934
|
+
};
|
|
935
|
+
finalWordCount = persistenceOutput.wordCount;
|
|
936
|
+
const lengthWarnings = this.buildLengthWarnings(chapterNumber, finalWordCount, lengthSpec);
|
|
937
|
+
const lengthTelemetry = this.buildLengthTelemetry({
|
|
938
|
+
lengthSpec,
|
|
939
|
+
writerCount,
|
|
940
|
+
postWriterNormalizeCount: reviewResult.preAuditNormalizedWordCount,
|
|
941
|
+
postReviseCount,
|
|
942
|
+
finalCount: finalWordCount,
|
|
943
|
+
normalizeApplied,
|
|
944
|
+
lengthWarning: lengthWarnings.length > 0,
|
|
945
|
+
});
|
|
946
|
+
this.logLengthWarnings(lengthWarnings);
|
|
947
|
+
// 4.1 Validate settler output before writing
|
|
948
|
+
this.logStage(stageLanguage, { zh: "校验真相文件变更", en: "validating truth file updates" });
|
|
949
|
+
const storyDir = join(bookDir, "story");
|
|
950
|
+
const [oldState, oldHooks, oldLedger] = await Promise.all([
|
|
951
|
+
readFile(join(storyDir, "current_state.md"), "utf-8").catch(() => ""),
|
|
952
|
+
readFile(join(storyDir, "pending_hooks.md"), "utf-8").catch(() => ""),
|
|
953
|
+
readFile(join(storyDir, "particle_ledger.md"), "utf-8").catch(() => ""),
|
|
954
|
+
]);
|
|
955
|
+
const validator = new StateValidatorAgent(this.agentCtxFor("state-validator", bookId));
|
|
956
|
+
const truthValidation = await validateChapterTruthPersistence({
|
|
957
|
+
writer,
|
|
958
|
+
validator,
|
|
959
|
+
book,
|
|
960
|
+
bookDir,
|
|
961
|
+
chapterNumber,
|
|
962
|
+
title: persistenceOutput.title,
|
|
963
|
+
content: finalContent,
|
|
964
|
+
persistenceOutput,
|
|
965
|
+
auditResult,
|
|
966
|
+
previousTruth: {
|
|
967
|
+
oldState,
|
|
968
|
+
oldHooks,
|
|
969
|
+
oldLedger,
|
|
970
|
+
},
|
|
971
|
+
reducedControlInput,
|
|
972
|
+
language: pipelineLang,
|
|
973
|
+
logWarn: (message) => this.logWarn(pipelineLang, message),
|
|
974
|
+
logger: this.config.logger,
|
|
975
|
+
});
|
|
976
|
+
let chapterStatus = truthValidation.chapterStatus;
|
|
977
|
+
let degradedIssues = truthValidation.degradedIssues;
|
|
978
|
+
persistenceOutput = truthValidation.persistenceOutput;
|
|
979
|
+
auditResult = truthValidation.auditResult;
|
|
980
|
+
// 4.2 Final paragraph shape check on persisted content (post-normalize, post-revise)
|
|
981
|
+
{
|
|
982
|
+
const { detectParagraphLengthDrift, detectParagraphShapeWarnings, } = await import("../agents/post-write-validator.js");
|
|
983
|
+
const chapDir = join(bookDir, "chapters");
|
|
984
|
+
const recentFiles = (await readdir(chapDir).catch(() => []))
|
|
985
|
+
.filter((f) => f.endsWith(".md") && /^\d{4}/.test(f))
|
|
986
|
+
.sort()
|
|
987
|
+
.slice(-5);
|
|
988
|
+
const recentContent = (await Promise.all(recentFiles.map((f) => readFile(join(chapDir, f), "utf-8").catch(() => "")))).join("\n\n");
|
|
989
|
+
const paragraphIssues = [
|
|
990
|
+
...detectParagraphShapeWarnings(finalContent, pipelineLang),
|
|
991
|
+
...detectParagraphLengthDrift(finalContent, recentContent, pipelineLang),
|
|
992
|
+
];
|
|
993
|
+
if (paragraphIssues.length > 0) {
|
|
994
|
+
for (const issue of paragraphIssues) {
|
|
995
|
+
this.config.logger?.warn(`[paragraph] ${issue.description}`);
|
|
996
|
+
}
|
|
997
|
+
auditResult = {
|
|
998
|
+
...auditResult,
|
|
999
|
+
issues: [...auditResult.issues, ...paragraphIssues.map((v) => ({
|
|
1000
|
+
severity: v.severity,
|
|
1001
|
+
category: "paragraph-shape",
|
|
1002
|
+
description: v.description,
|
|
1003
|
+
suggestion: v.suggestion,
|
|
1004
|
+
}))],
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const resolvedStatus = chapterStatus ?? (auditResult.passed ? "ready-for-review" : "audit-failed");
|
|
1009
|
+
await persistChapterArtifacts({
|
|
1010
|
+
chapterNumber,
|
|
1011
|
+
chapterTitle: persistenceOutput.title,
|
|
1012
|
+
status: resolvedStatus,
|
|
1013
|
+
auditResult,
|
|
1014
|
+
finalWordCount,
|
|
1015
|
+
lengthWarnings,
|
|
1016
|
+
lengthTelemetry,
|
|
1017
|
+
degradedIssues,
|
|
1018
|
+
tokenUsage: totalUsage,
|
|
1019
|
+
loadChapterIndex: () => this.state.loadChapterIndex(bookId),
|
|
1020
|
+
saveChapter: () => writer.saveChapter(bookDir, persistenceOutput, gp.numericalSystem, pipelineLang),
|
|
1021
|
+
saveTruthFiles: async () => {
|
|
1022
|
+
await writer.saveNewTruthFiles(bookDir, persistenceOutput, pipelineLang);
|
|
1023
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, persistenceOutput);
|
|
1024
|
+
this.logStage(stageLanguage, { zh: "同步记忆索引", en: "syncing memory indexes" });
|
|
1025
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
1026
|
+
},
|
|
1027
|
+
saveChapterIndex: (index) => this.state.saveChapterIndex(bookId, index),
|
|
1028
|
+
markBookActiveIfNeeded: () => this.markBookActiveIfNeeded(bookId),
|
|
1029
|
+
persistAuditDriftGuidance: (issues) => this.persistAuditDriftGuidance({
|
|
1030
|
+
bookDir,
|
|
1031
|
+
chapterNumber,
|
|
1032
|
+
issues,
|
|
1033
|
+
language: stageLanguage,
|
|
1034
|
+
}).catch(() => undefined),
|
|
1035
|
+
snapshotState: () => this.state.snapshotState(bookId, chapterNumber),
|
|
1036
|
+
syncCurrentStateFactHistory: () => this.syncCurrentStateFactHistory(bookId, chapterNumber),
|
|
1037
|
+
logSnapshotStage: () => this.logStage(stageLanguage, { zh: "更新章节索引与快照", en: "updating chapter index and snapshots" }),
|
|
1038
|
+
});
|
|
1039
|
+
// 6. Send notification
|
|
1040
|
+
if (this.config.notifyChannels && this.config.notifyChannels.length > 0) {
|
|
1041
|
+
const statusEmoji = resolvedStatus === "state-degraded"
|
|
1042
|
+
? "🧯"
|
|
1043
|
+
: auditResult.passed ? "✅" : "⚠️";
|
|
1044
|
+
const chapterLength = formatLengthCount(finalWordCount, lengthSpec.countingMode);
|
|
1045
|
+
await dispatchNotification(this.config.notifyChannels, {
|
|
1046
|
+
title: `${statusEmoji} ${book.title} 第${chapterNumber}章`,
|
|
1047
|
+
body: [
|
|
1048
|
+
`**${persistenceOutput.title}** | ${chapterLength}`,
|
|
1049
|
+
revised ? "📝 已自动修正" : "",
|
|
1050
|
+
resolvedStatus === "state-degraded"
|
|
1051
|
+
? "状态结算: 已降级保存,需先修复 state 再继续"
|
|
1052
|
+
: `审稿: ${auditResult.passed ? "通过" : "需人工审核"}`,
|
|
1053
|
+
...auditResult.issues
|
|
1054
|
+
.filter((i) => i.severity !== "info")
|
|
1055
|
+
.map((i) => `- [${i.severity}] ${i.description}`),
|
|
1056
|
+
]
|
|
1057
|
+
.filter(Boolean)
|
|
1058
|
+
.join("\n"),
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
await this.emitWebhook("pipeline-complete", bookId, chapterNumber, {
|
|
1062
|
+
title: persistenceOutput.title,
|
|
1063
|
+
wordCount: finalWordCount,
|
|
1064
|
+
passed: auditResult.passed,
|
|
1065
|
+
revised,
|
|
1066
|
+
status: resolvedStatus,
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
chapterNumber,
|
|
1070
|
+
title: persistenceOutput.title,
|
|
1071
|
+
wordCount: finalWordCount,
|
|
1072
|
+
auditResult,
|
|
1073
|
+
revised,
|
|
1074
|
+
status: resolvedStatus,
|
|
1075
|
+
lengthWarnings,
|
|
1076
|
+
lengthTelemetry,
|
|
1077
|
+
tokenUsage: totalUsage,
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
async _repairChapterStateLocked(bookId, chapterNumber) {
|
|
1081
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
1082
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1083
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
1084
|
+
const index = [...(await this.state.loadChapterIndex(bookId))];
|
|
1085
|
+
if (index.length === 0) {
|
|
1086
|
+
throw new Error(`Book "${bookId}" has no persisted chapters to repair.`);
|
|
1087
|
+
}
|
|
1088
|
+
const targetChapter = chapterNumber ?? index[index.length - 1].number;
|
|
1089
|
+
const targetIndex = index.findIndex((chapter) => chapter.number === targetChapter);
|
|
1090
|
+
if (targetIndex < 0) {
|
|
1091
|
+
throw new Error(`Chapter ${targetChapter} not found in "${bookId}".`);
|
|
1092
|
+
}
|
|
1093
|
+
const targetMeta = index[targetIndex];
|
|
1094
|
+
const latestChapter = Math.max(...index.map((chapter) => chapter.number));
|
|
1095
|
+
if (targetMeta.status !== "state-degraded") {
|
|
1096
|
+
throw new Error(`Chapter ${targetChapter} is not state-degraded.`);
|
|
1097
|
+
}
|
|
1098
|
+
if (targetChapter !== latestChapter) {
|
|
1099
|
+
throw new Error(`Only the latest state-degraded chapter can be repaired safely (latest is ${latestChapter}).`);
|
|
1100
|
+
}
|
|
1101
|
+
this.logStage(stageLanguage, { zh: "修复章节状态结算", en: "repairing chapter state settlement" });
|
|
1102
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
1103
|
+
const pipelineLang = book.language ?? gp.language;
|
|
1104
|
+
const content = await this.readChapterContent(bookDir, targetChapter);
|
|
1105
|
+
const storyDir = join(bookDir, "story");
|
|
1106
|
+
const [oldState, oldHooks] = await Promise.all([
|
|
1107
|
+
readFile(join(storyDir, "current_state.md"), "utf-8").catch(() => ""),
|
|
1108
|
+
readFile(join(storyDir, "pending_hooks.md"), "utf-8").catch(() => ""),
|
|
1109
|
+
]);
|
|
1110
|
+
const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
|
|
1111
|
+
let repairedOutput = await writer.settleChapterState({
|
|
1112
|
+
book,
|
|
1113
|
+
bookDir,
|
|
1114
|
+
chapterNumber: targetChapter,
|
|
1115
|
+
title: targetMeta.title,
|
|
1116
|
+
content,
|
|
1117
|
+
allowReapply: true,
|
|
1118
|
+
});
|
|
1119
|
+
const validator = new StateValidatorAgent(this.agentCtxFor("state-validator", bookId));
|
|
1120
|
+
let validation = await validator.validate(content, targetChapter, oldState, repairedOutput.updatedState, oldHooks, repairedOutput.updatedHooks, pipelineLang);
|
|
1121
|
+
if (!validation.passed) {
|
|
1122
|
+
const recovery = await retrySettlementAfterValidationFailure({
|
|
1123
|
+
writer,
|
|
1124
|
+
validator,
|
|
1125
|
+
book,
|
|
1126
|
+
bookDir,
|
|
1127
|
+
chapterNumber: targetChapter,
|
|
1128
|
+
title: targetMeta.title,
|
|
1129
|
+
content,
|
|
1130
|
+
oldState,
|
|
1131
|
+
oldHooks,
|
|
1132
|
+
originalValidation: validation,
|
|
1133
|
+
language: pipelineLang,
|
|
1134
|
+
logWarn: (message) => this.logWarn(pipelineLang, message),
|
|
1135
|
+
logger: this.config.logger,
|
|
1136
|
+
});
|
|
1137
|
+
if (recovery.kind !== "recovered") {
|
|
1138
|
+
throw new Error(recovery.issues[0]?.description
|
|
1139
|
+
?? `State repair still failed for chapter ${targetChapter}.`);
|
|
1140
|
+
}
|
|
1141
|
+
repairedOutput = recovery.output;
|
|
1142
|
+
validation = recovery.validation;
|
|
1143
|
+
}
|
|
1144
|
+
if (!validation.passed) {
|
|
1145
|
+
throw new Error(`State repair still failed for chapter ${targetChapter}.`);
|
|
1146
|
+
}
|
|
1147
|
+
await writer.saveChapter(bookDir, repairedOutput, gp.numericalSystem, pipelineLang);
|
|
1148
|
+
await writer.saveNewTruthFiles(bookDir, repairedOutput, pipelineLang);
|
|
1149
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, targetChapter, repairedOutput);
|
|
1150
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
1151
|
+
await this.state.snapshotState(bookId, targetChapter);
|
|
1152
|
+
await this.syncCurrentStateFactHistory(bookId, targetChapter);
|
|
1153
|
+
const baseStatus = resolveStateDegradedBaseStatus(targetMeta);
|
|
1154
|
+
const degradedMetadata = parseStateDegradedReviewNote(targetMeta.reviewNote);
|
|
1155
|
+
const injectedIssues = new Set(degradedMetadata?.injectedIssues ?? []);
|
|
1156
|
+
index[targetIndex] = {
|
|
1157
|
+
...targetMeta,
|
|
1158
|
+
status: baseStatus,
|
|
1159
|
+
updatedAt: new Date().toISOString(),
|
|
1160
|
+
auditIssues: targetMeta.auditIssues.filter((issue) => !injectedIssues.has(issue)),
|
|
1161
|
+
reviewNote: undefined,
|
|
1162
|
+
};
|
|
1163
|
+
await this.state.saveChapterIndex(bookId, index);
|
|
1164
|
+
const repairedPassesAudit = baseStatus !== "audit-failed";
|
|
1165
|
+
return {
|
|
1166
|
+
chapterNumber: targetChapter,
|
|
1167
|
+
title: targetMeta.title,
|
|
1168
|
+
wordCount: targetMeta.wordCount,
|
|
1169
|
+
auditResult: {
|
|
1170
|
+
passed: repairedPassesAudit,
|
|
1171
|
+
issues: [],
|
|
1172
|
+
summary: repairedPassesAudit ? "state repaired" : "state repaired but chapter still needs review",
|
|
1173
|
+
},
|
|
1174
|
+
revised: false,
|
|
1175
|
+
status: baseStatus,
|
|
1176
|
+
lengthWarnings: targetMeta.lengthWarnings,
|
|
1177
|
+
lengthTelemetry: targetMeta.lengthTelemetry,
|
|
1178
|
+
tokenUsage: targetMeta.tokenUsage,
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
async _resyncChapterArtifactsLocked(bookId, chapterNumber) {
|
|
1182
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
1183
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1184
|
+
const stageLanguage = await this.resolveBookLanguage(book);
|
|
1185
|
+
const index = [...(await this.state.loadChapterIndex(bookId))];
|
|
1186
|
+
if (index.length === 0) {
|
|
1187
|
+
throw new Error(`Book "${bookId}" has no persisted chapters to sync.`);
|
|
1188
|
+
}
|
|
1189
|
+
const targetChapter = chapterNumber ?? index[index.length - 1].number;
|
|
1190
|
+
const targetIndex = index.findIndex((chapter) => chapter.number === targetChapter);
|
|
1191
|
+
if (targetIndex < 0) {
|
|
1192
|
+
throw new Error(`Chapter ${targetChapter} not found in "${bookId}".`);
|
|
1193
|
+
}
|
|
1194
|
+
const targetMeta = index[targetIndex];
|
|
1195
|
+
const latestChapter = Math.max(...index.map((chapter) => chapter.number));
|
|
1196
|
+
if (targetChapter !== latestChapter) {
|
|
1197
|
+
throw new Error(`Only the latest persisted chapter can be synced safely (latest is ${latestChapter}).`);
|
|
1198
|
+
}
|
|
1199
|
+
this.logStage(stageLanguage, { zh: "根据已编辑正文同步真相文件与索引", en: "syncing truth files and indexes from edited chapter body" });
|
|
1200
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
1201
|
+
const pipelineLang = book.language ?? gp.language;
|
|
1202
|
+
const content = await this.readChapterContent(bookDir, targetChapter);
|
|
1203
|
+
const storyDir = join(bookDir, "story");
|
|
1204
|
+
const [oldState, oldHooks] = await Promise.all([
|
|
1205
|
+
readFile(join(storyDir, "current_state.md"), "utf-8").catch(() => ""),
|
|
1206
|
+
readFile(join(storyDir, "pending_hooks.md"), "utf-8").catch(() => ""),
|
|
1207
|
+
]);
|
|
1208
|
+
const reducedControlInput = (this.config.inputGovernanceMode ?? "v2") === "legacy"
|
|
1209
|
+
? undefined
|
|
1210
|
+
: await this.createGovernedArtifacts(book, bookDir, targetChapter, this.config.externalContext, { reuseExistingIntentWhenContextMissing: true });
|
|
1211
|
+
const writer = new WriterAgent(this.agentCtxFor("writer", bookId));
|
|
1212
|
+
let syncedOutput = await writer.settleChapterState({
|
|
1213
|
+
book,
|
|
1214
|
+
bookDir,
|
|
1215
|
+
chapterNumber: targetChapter,
|
|
1216
|
+
title: targetMeta.title,
|
|
1217
|
+
content,
|
|
1218
|
+
chapterIntent: reducedControlInput?.plan.intentMarkdown,
|
|
1219
|
+
contextPackage: reducedControlInput?.composed.contextPackage,
|
|
1220
|
+
ruleStack: reducedControlInput?.composed.ruleStack,
|
|
1221
|
+
allowReapply: true,
|
|
1222
|
+
});
|
|
1223
|
+
const validator = new StateValidatorAgent(this.agentCtxFor("state-validator", bookId));
|
|
1224
|
+
let validation = await validator.validate(content, targetChapter, oldState, syncedOutput.updatedState, oldHooks, syncedOutput.updatedHooks, pipelineLang);
|
|
1225
|
+
if (!validation.passed) {
|
|
1226
|
+
const recovery = await retrySettlementAfterValidationFailure({
|
|
1227
|
+
writer,
|
|
1228
|
+
validator,
|
|
1229
|
+
book,
|
|
1230
|
+
bookDir,
|
|
1231
|
+
chapterNumber: targetChapter,
|
|
1232
|
+
title: targetMeta.title,
|
|
1233
|
+
content,
|
|
1234
|
+
reducedControlInput: reducedControlInput
|
|
1235
|
+
? {
|
|
1236
|
+
chapterIntent: reducedControlInput.plan.intentMarkdown,
|
|
1237
|
+
contextPackage: reducedControlInput.composed.contextPackage,
|
|
1238
|
+
ruleStack: reducedControlInput.composed.ruleStack,
|
|
1239
|
+
}
|
|
1240
|
+
: undefined,
|
|
1241
|
+
oldState,
|
|
1242
|
+
oldHooks,
|
|
1243
|
+
originalValidation: validation,
|
|
1244
|
+
language: pipelineLang,
|
|
1245
|
+
logWarn: (message) => this.logWarn(pipelineLang, message),
|
|
1246
|
+
logger: this.config.logger,
|
|
1247
|
+
});
|
|
1248
|
+
if (recovery.kind !== "recovered") {
|
|
1249
|
+
throw new Error(recovery.issues[0]?.description
|
|
1250
|
+
?? `Chapter sync still failed for chapter ${targetChapter}.`);
|
|
1251
|
+
}
|
|
1252
|
+
syncedOutput = recovery.output;
|
|
1253
|
+
validation = recovery.validation;
|
|
1254
|
+
}
|
|
1255
|
+
if (!validation.passed) {
|
|
1256
|
+
throw new Error(`Chapter sync still failed for chapter ${targetChapter}.`);
|
|
1257
|
+
}
|
|
1258
|
+
await writer.saveChapter(bookDir, syncedOutput, gp.numericalSystem, pipelineLang);
|
|
1259
|
+
await writer.saveNewTruthFiles(bookDir, syncedOutput, pipelineLang);
|
|
1260
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, targetChapter, syncedOutput);
|
|
1261
|
+
await this.syncNarrativeMemoryIndex(bookId);
|
|
1262
|
+
await this.state.snapshotState(bookId, targetChapter);
|
|
1263
|
+
await this.syncCurrentStateFactHistory(bookId, targetChapter);
|
|
1264
|
+
const finalStatus = targetMeta.status === "state-degraded"
|
|
1265
|
+
? resolveStateDegradedBaseStatus(targetMeta)
|
|
1266
|
+
: "ready-for-review";
|
|
1267
|
+
if (targetMeta.status === "state-degraded") {
|
|
1268
|
+
const degradedMetadata = parseStateDegradedReviewNote(targetMeta.reviewNote);
|
|
1269
|
+
const injectedIssues = new Set(degradedMetadata?.injectedIssues ?? []);
|
|
1270
|
+
index[targetIndex] = {
|
|
1271
|
+
...targetMeta,
|
|
1272
|
+
status: finalStatus,
|
|
1273
|
+
updatedAt: new Date().toISOString(),
|
|
1274
|
+
auditIssues: targetMeta.auditIssues.filter((issue) => !injectedIssues.has(issue)),
|
|
1275
|
+
reviewNote: undefined,
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
1278
|
+
else {
|
|
1279
|
+
index[targetIndex] = {
|
|
1280
|
+
...targetMeta,
|
|
1281
|
+
status: "ready-for-review",
|
|
1282
|
+
updatedAt: new Date().toISOString(),
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
await this.state.saveChapterIndex(bookId, index);
|
|
1286
|
+
return {
|
|
1287
|
+
chapterNumber: targetChapter,
|
|
1288
|
+
title: targetMeta.title,
|
|
1289
|
+
wordCount: targetMeta.wordCount,
|
|
1290
|
+
auditResult: {
|
|
1291
|
+
passed: finalStatus !== "audit-failed",
|
|
1292
|
+
issues: [],
|
|
1293
|
+
summary: finalStatus === "audit-failed"
|
|
1294
|
+
? "chapter truth/state resynced from edited body, but chapter still needs audit fixes"
|
|
1295
|
+
: "chapter truth/state resynced from edited body",
|
|
1296
|
+
},
|
|
1297
|
+
revised: false,
|
|
1298
|
+
status: finalStatus,
|
|
1299
|
+
lengthWarnings: targetMeta.lengthWarnings,
|
|
1300
|
+
lengthTelemetry: targetMeta.lengthTelemetry,
|
|
1301
|
+
tokenUsage: targetMeta.tokenUsage,
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
// ---------------------------------------------------------------------------
|
|
1305
|
+
// Import operations (style imitation + canon for spinoff)
|
|
1306
|
+
// ---------------------------------------------------------------------------
|
|
1307
|
+
/**
|
|
1308
|
+
* Generate a qualitative style guide from reference text via LLM.
|
|
1309
|
+
* Also saves the statistical style_profile.json.
|
|
1310
|
+
*/
|
|
1311
|
+
async generateStyleGuide(bookId, referenceText, sourceName) {
|
|
1312
|
+
if (referenceText.length < 500) {
|
|
1313
|
+
throw new Error(`Reference text too short (${referenceText.length} chars, minimum 500). Provide at least 2000 chars for reliable style extraction.`);
|
|
1314
|
+
}
|
|
1315
|
+
const { analyzeStyle } = await import("../agents/style-analyzer.js");
|
|
1316
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1317
|
+
const storyDir = join(bookDir, "story");
|
|
1318
|
+
await mkdir(storyDir, { recursive: true });
|
|
1319
|
+
// Statistical fingerprint
|
|
1320
|
+
const profile = analyzeStyle(referenceText, sourceName);
|
|
1321
|
+
await writeFile(join(storyDir, "style_profile.json"), JSON.stringify(profile, null, 2), "utf-8");
|
|
1322
|
+
// LLM qualitative extraction
|
|
1323
|
+
const response = await chatCompletion(this.config.client, this.config.model, [
|
|
1324
|
+
{
|
|
1325
|
+
role: "system",
|
|
1326
|
+
content: `你是一位文学风格分析专家。分析参考文本的写作风格,提取可供模仿的定性特征。
|
|
1327
|
+
|
|
1328
|
+
输出格式(Markdown):
|
|
1329
|
+
## 叙事声音与语气
|
|
1330
|
+
(冷峻/热烈/讽刺/温情/...,附1-2个原文例句)
|
|
1331
|
+
|
|
1332
|
+
## 对话风格
|
|
1333
|
+
(角色说话的共性特征:句子长短、口头禅倾向、方言痕迹、对话节奏)
|
|
1334
|
+
|
|
1335
|
+
## 场景描写特征
|
|
1336
|
+
(五感偏好、意象选择、描写密度、环境与情绪的关联方式)
|
|
1337
|
+
|
|
1338
|
+
## 转折与衔接手法
|
|
1339
|
+
(场景如何切换、时间跳跃的处理方式、段落间的过渡特征)
|
|
1340
|
+
|
|
1341
|
+
## 节奏特征
|
|
1342
|
+
(长短句分布、段落长度偏好、高潮/舒缓的交替方式)
|
|
1343
|
+
|
|
1344
|
+
## 词汇偏好
|
|
1345
|
+
(高频特色用词、比喻/修辞倾向、口语化程度)
|
|
1346
|
+
|
|
1347
|
+
## 情绪表达方式
|
|
1348
|
+
(直白抒情 vs 动作外化、内心独白的频率和风格)
|
|
1349
|
+
|
|
1350
|
+
## 独特习惯
|
|
1351
|
+
(任何值得模仿的个人写作习惯)
|
|
1352
|
+
|
|
1353
|
+
分析必须基于原文实际特征,不要泛泛而谈。每个部分用1-2个原文例句佐证。`,
|
|
1354
|
+
},
|
|
1355
|
+
{
|
|
1356
|
+
role: "user",
|
|
1357
|
+
content: `分析以下参考文本的写作风格:\n\n${referenceText.slice(0, 20000)}`,
|
|
1358
|
+
},
|
|
1359
|
+
], { temperature: 0.3, maxTokens: 4096 });
|
|
1360
|
+
await writeFile(join(storyDir, "style_guide.md"), response.content, "utf-8");
|
|
1361
|
+
return response.content;
|
|
1362
|
+
}
|
|
1363
|
+
/**
|
|
1364
|
+
* Import canon from parent book for spinoff writing.
|
|
1365
|
+
* Reads parent's truth files, uses LLM to generate parent_canon.md in target book.
|
|
1366
|
+
*/
|
|
1367
|
+
async importCanon(targetBookId, parentBookId) {
|
|
1368
|
+
// Validate both books exist
|
|
1369
|
+
const bookIds = await this.state.listBooks();
|
|
1370
|
+
if (!bookIds.includes(parentBookId)) {
|
|
1371
|
+
throw new Error(`Parent book "${parentBookId}" not found. Available: ${bookIds.join(", ") || "(none)"}`);
|
|
1372
|
+
}
|
|
1373
|
+
if (!bookIds.includes(targetBookId)) {
|
|
1374
|
+
throw new Error(`Target book "${targetBookId}" not found. Available: ${bookIds.join(", ") || "(none)"}`);
|
|
1375
|
+
}
|
|
1376
|
+
const parentDir = this.state.bookDir(parentBookId);
|
|
1377
|
+
const targetDir = this.state.bookDir(targetBookId);
|
|
1378
|
+
const storyDir = join(targetDir, "story");
|
|
1379
|
+
await mkdir(storyDir, { recursive: true });
|
|
1380
|
+
const readSafe = async (path) => {
|
|
1381
|
+
try {
|
|
1382
|
+
return await readFile(path, "utf-8");
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
return "(无)";
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
const parentBook = await this.state.loadBookConfig(parentBookId);
|
|
1389
|
+
const [storyBible, currentState, ledger, hooks, summaries, subplots, emotions, matrix] = await Promise.all([
|
|
1390
|
+
readSafe(join(parentDir, "story/story_bible.md")),
|
|
1391
|
+
readSafe(join(parentDir, "story/current_state.md")),
|
|
1392
|
+
readSafe(join(parentDir, "story/particle_ledger.md")),
|
|
1393
|
+
readSafe(join(parentDir, "story/pending_hooks.md")),
|
|
1394
|
+
readSafe(join(parentDir, "story/chapter_summaries.md")),
|
|
1395
|
+
readSafe(join(parentDir, "story/subplot_board.md")),
|
|
1396
|
+
readSafe(join(parentDir, "story/emotional_arcs.md")),
|
|
1397
|
+
readSafe(join(parentDir, "story/character_matrix.md")),
|
|
1398
|
+
]);
|
|
1399
|
+
const response = await chatCompletion(this.config.client, this.config.model, [
|
|
1400
|
+
{
|
|
1401
|
+
role: "system",
|
|
1402
|
+
content: `你是一位网络小说架构师。基于正传的全部设定和状态文件,生成一份完整的"正传正典参照"文档,供番外写作和审计使用。
|
|
1403
|
+
|
|
1404
|
+
输出格式(Markdown):
|
|
1405
|
+
# 正传正典(《{正传书名}》)
|
|
1406
|
+
|
|
1407
|
+
## 世界规则(完整,来自正传设定)
|
|
1408
|
+
(力量体系、地理设定、阵营关系、核心规则——完整复制,不压缩)
|
|
1409
|
+
|
|
1410
|
+
## 正典约束(不可违反的事实)
|
|
1411
|
+
| 约束ID | 类型 | 约束内容 | 严重性 |
|
|
1412
|
+
|---|---|---|---|
|
|
1413
|
+
| C01 | 人物存亡 | ... | critical |
|
|
1414
|
+
(列出所有硬性约束:谁活着、谁死了、什么事件已经发生、什么规则不可违反)
|
|
1415
|
+
|
|
1416
|
+
## 角色快照
|
|
1417
|
+
| 角色 | 当前状态 | 性格底色 | 对话特征 | 已知信息 | 未知信息 |
|
|
1418
|
+
|---|---|---|---|---|---|
|
|
1419
|
+
(从状态卡和角色矩阵中提取每个重要角色的完整快照)
|
|
1420
|
+
|
|
1421
|
+
## 角色双态处理原则
|
|
1422
|
+
- 未来会变强的角色:写潜力暗示
|
|
1423
|
+
- 未来会黑化的角色:写微小裂痕
|
|
1424
|
+
- 未来会死的角色:写导致死亡的性格底色
|
|
1425
|
+
|
|
1426
|
+
## 关键事件时间线
|
|
1427
|
+
| 章节 | 事件 | 涉及角色 | 对番外的约束 |
|
|
1428
|
+
|---|---|---|---|
|
|
1429
|
+
(从章节摘要中提取关键事件)
|
|
1430
|
+
|
|
1431
|
+
## 伏笔状态
|
|
1432
|
+
| Hook ID | 类型 | 状态 | 内容 | 预期回收 |
|
|
1433
|
+
|---|---|---|---|---|
|
|
1434
|
+
|
|
1435
|
+
## 资源账本快照
|
|
1436
|
+
(当前资源状态)
|
|
1437
|
+
|
|
1438
|
+
---
|
|
1439
|
+
meta:
|
|
1440
|
+
parentBookId: "{parentBookId}"
|
|
1441
|
+
parentTitle: "{正传书名}"
|
|
1442
|
+
generatedAt: "{ISO timestamp}"
|
|
1443
|
+
|
|
1444
|
+
要求:
|
|
1445
|
+
1. 世界规则完整复制,不压缩——准确性优先
|
|
1446
|
+
2. 正典约束必须穷尽,遗漏会导致番外与正传矛盾
|
|
1447
|
+
3. 角色快照必须包含信息边界(已知/未知),防止番外中角色引用不该知道的信息`,
|
|
1448
|
+
},
|
|
1449
|
+
{
|
|
1450
|
+
role: "user",
|
|
1451
|
+
content: `正传书名:${parentBook.title}
|
|
1452
|
+
正传ID:${parentBookId}
|
|
1453
|
+
|
|
1454
|
+
## 正传世界设定
|
|
1455
|
+
${storyBible}
|
|
1456
|
+
|
|
1457
|
+
## 正传当前状态卡
|
|
1458
|
+
${currentState}
|
|
1459
|
+
|
|
1460
|
+
## 正传资源账本
|
|
1461
|
+
${ledger}
|
|
1462
|
+
|
|
1463
|
+
## 正传伏笔池
|
|
1464
|
+
${hooks}
|
|
1465
|
+
|
|
1466
|
+
## 正传章节摘要
|
|
1467
|
+
${summaries}
|
|
1468
|
+
|
|
1469
|
+
## 正传支线进度
|
|
1470
|
+
${subplots}
|
|
1471
|
+
|
|
1472
|
+
## 正传情感弧线
|
|
1473
|
+
${emotions}
|
|
1474
|
+
|
|
1475
|
+
## 正传角色矩阵
|
|
1476
|
+
${matrix}`,
|
|
1477
|
+
},
|
|
1478
|
+
], { temperature: 0.3, maxTokens: 16384 });
|
|
1479
|
+
// Append deterministic meta block (LLM may hallucinate timestamps)
|
|
1480
|
+
const metaBlock = [
|
|
1481
|
+
"",
|
|
1482
|
+
"---",
|
|
1483
|
+
"meta:",
|
|
1484
|
+
` parentBookId: "${parentBookId}"`,
|
|
1485
|
+
` parentTitle: "${parentBook.title}"`,
|
|
1486
|
+
` generatedAt: "${new Date().toISOString()}"`,
|
|
1487
|
+
].join("\n");
|
|
1488
|
+
const canon = response.content + metaBlock;
|
|
1489
|
+
await writeFile(join(storyDir, "parent_canon.md"), canon, "utf-8");
|
|
1490
|
+
// Also generate style guide from parent's chapter text if available
|
|
1491
|
+
const parentChaptersDir = join(parentDir, "chapters");
|
|
1492
|
+
const parentChapterText = await this.readParentChapterSample(parentChaptersDir);
|
|
1493
|
+
if (parentChapterText.length >= 500) {
|
|
1494
|
+
await this.tryGenerateStyleGuide(targetBookId, parentChapterText, parentBook.title);
|
|
1495
|
+
}
|
|
1496
|
+
return canon;
|
|
1497
|
+
}
|
|
1498
|
+
async readParentChapterSample(chaptersDir) {
|
|
1499
|
+
try {
|
|
1500
|
+
const entries = await readdir(chaptersDir);
|
|
1501
|
+
const mdFiles = entries
|
|
1502
|
+
.filter((file) => file.endsWith(".md"))
|
|
1503
|
+
.sort()
|
|
1504
|
+
.slice(0, 5);
|
|
1505
|
+
const chunks = [];
|
|
1506
|
+
let totalLength = 0;
|
|
1507
|
+
for (const file of mdFiles) {
|
|
1508
|
+
if (totalLength >= 20000)
|
|
1509
|
+
break;
|
|
1510
|
+
const content = await readFile(join(chaptersDir, file), "utf-8");
|
|
1511
|
+
chunks.push(content);
|
|
1512
|
+
totalLength += content.length;
|
|
1513
|
+
}
|
|
1514
|
+
return chunks.join("\n\n---\n\n");
|
|
1515
|
+
}
|
|
1516
|
+
catch {
|
|
1517
|
+
return "";
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
// ---------------------------------------------------------------------------
|
|
1521
|
+
// Chapter import (for continuation writing from existing chapters)
|
|
1522
|
+
// ---------------------------------------------------------------------------
|
|
1523
|
+
/**
|
|
1524
|
+
* Import existing chapters into a book. Reverse-engineers all truth files
|
|
1525
|
+
* via sequential replay so the Writer and Auditor can continue naturally.
|
|
1526
|
+
*
|
|
1527
|
+
* Step 1: Generate foundation (story_bible, volume_outline, book_rules) from all chapters.
|
|
1528
|
+
* Step 2: Sequentially replay each chapter through ChapterAnalyzer to build truth files.
|
|
1529
|
+
*/
|
|
1530
|
+
async importChapters(input) {
|
|
1531
|
+
const releaseLock = await this.state.acquireBookLock(input.bookId);
|
|
1532
|
+
try {
|
|
1533
|
+
const book = await this.state.loadBookConfig(input.bookId);
|
|
1534
|
+
const bookDir = this.state.bookDir(input.bookId);
|
|
1535
|
+
const { profile: gp } = await this.loadGenreProfile(book.genre);
|
|
1536
|
+
const resolvedLanguage = book.language ?? gp.language;
|
|
1537
|
+
const startFrom = input.resumeFrom ?? 1;
|
|
1538
|
+
const log = this.config.logger?.child("import");
|
|
1539
|
+
// Step 1: Generate foundation on first run (not on resume)
|
|
1540
|
+
if (startFrom === 1) {
|
|
1541
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1542
|
+
zh: `步骤 1:从 ${input.chapters.length} 章生成基础设定...`,
|
|
1543
|
+
en: `Step 1: Generating foundation from ${input.chapters.length} chapters...`,
|
|
1544
|
+
}));
|
|
1545
|
+
const allText = input.chapters.map((c, i) => resolvedLanguage === "en"
|
|
1546
|
+
? `Chapter ${i + 1}: ${c.title}\n\n${c.content}`
|
|
1547
|
+
: `第${i + 1}章 ${c.title}\n\n${c.content}`).join("\n\n---\n\n");
|
|
1548
|
+
const architect = new ArchitectAgent(this.agentCtxFor("architect", input.bookId));
|
|
1549
|
+
const isSeries = input.importMode === "series";
|
|
1550
|
+
const foundation = isSeries
|
|
1551
|
+
? await this.generateAndReviewFoundation({
|
|
1552
|
+
generate: (reviewFeedback) => architect.generateFoundationFromImport(book, allText, undefined, reviewFeedback, { importMode: "series" }),
|
|
1553
|
+
reviewer: new FoundationReviewerAgent(this.agentCtxFor("foundation-reviewer", input.bookId)),
|
|
1554
|
+
mode: "series",
|
|
1555
|
+
language: resolvedLanguage === "en" ? "en" : "zh",
|
|
1556
|
+
stageLanguage: resolvedLanguage,
|
|
1557
|
+
})
|
|
1558
|
+
: await architect.generateFoundationFromImport(book, allText);
|
|
1559
|
+
await architect.writeFoundationFiles(bookDir, foundation, gp.numericalSystem, resolvedLanguage);
|
|
1560
|
+
await this.resetImportReplayTruthFiles(bookDir, resolvedLanguage);
|
|
1561
|
+
await this.state.saveChapterIndex(input.bookId, []);
|
|
1562
|
+
await this.state.snapshotState(input.bookId, 0);
|
|
1563
|
+
// Generate style guide from imported chapters
|
|
1564
|
+
if (allText.length >= 500) {
|
|
1565
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1566
|
+
zh: "提取原文风格指纹...",
|
|
1567
|
+
en: "Extracting source style fingerprint...",
|
|
1568
|
+
}));
|
|
1569
|
+
await this.tryGenerateStyleGuide(input.bookId, allText, book.title, resolvedLanguage);
|
|
1570
|
+
}
|
|
1571
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1572
|
+
zh: "基础设定已生成。",
|
|
1573
|
+
en: "Foundation generated.",
|
|
1574
|
+
}));
|
|
1575
|
+
}
|
|
1576
|
+
// Step 2: Sequential replay
|
|
1577
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1578
|
+
zh: `步骤 2:从第 ${startFrom} 章开始顺序回放...`,
|
|
1579
|
+
en: `Step 2: Sequential replay from chapter ${startFrom}...`,
|
|
1580
|
+
}));
|
|
1581
|
+
const analyzer = new ChapterAnalyzerAgent(this.agentCtxFor("chapter-analyzer", input.bookId));
|
|
1582
|
+
const writer = new WriterAgent(this.agentCtxFor("writer", input.bookId));
|
|
1583
|
+
const countingMode = resolveLengthCountingMode(book.language ?? gp.language);
|
|
1584
|
+
let totalWords = 0;
|
|
1585
|
+
let importedCount = 0;
|
|
1586
|
+
for (let i = startFrom - 1; i < input.chapters.length; i++) {
|
|
1587
|
+
const ch = input.chapters[i];
|
|
1588
|
+
const chapterNumber = i + 1;
|
|
1589
|
+
const governedInput = await this.prepareWriteInput(book, bookDir, chapterNumber);
|
|
1590
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1591
|
+
zh: `分析章节 ${chapterNumber}/${input.chapters.length}:${ch.title}...`,
|
|
1592
|
+
en: `Analyzing chapter ${chapterNumber}/${input.chapters.length}: ${ch.title}...`,
|
|
1593
|
+
}));
|
|
1594
|
+
// Analyze chapter to get truth file updates
|
|
1595
|
+
const output = await analyzer.analyzeChapter({
|
|
1596
|
+
book,
|
|
1597
|
+
bookDir,
|
|
1598
|
+
chapterNumber,
|
|
1599
|
+
chapterContent: ch.content,
|
|
1600
|
+
chapterTitle: ch.title,
|
|
1601
|
+
chapterIntent: governedInput.chapterIntent,
|
|
1602
|
+
contextPackage: governedInput.contextPackage,
|
|
1603
|
+
ruleStack: governedInput.ruleStack,
|
|
1604
|
+
});
|
|
1605
|
+
// Save chapter file + core truth files (state, ledger, hooks)
|
|
1606
|
+
await writer.saveChapter(bookDir, {
|
|
1607
|
+
...output,
|
|
1608
|
+
postWriteErrors: [],
|
|
1609
|
+
postWriteWarnings: [],
|
|
1610
|
+
}, gp.numericalSystem, resolvedLanguage);
|
|
1611
|
+
// Save extended truth files (summaries, subplots, emotional arcs, character matrix)
|
|
1612
|
+
await writer.saveNewTruthFiles(bookDir, {
|
|
1613
|
+
...output,
|
|
1614
|
+
postWriteErrors: [],
|
|
1615
|
+
postWriteWarnings: [],
|
|
1616
|
+
}, resolvedLanguage);
|
|
1617
|
+
await this.syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, output);
|
|
1618
|
+
await this.syncNarrativeMemoryIndex(input.bookId);
|
|
1619
|
+
// Update chapter index
|
|
1620
|
+
const existingIndex = await this.state.loadChapterIndex(input.bookId);
|
|
1621
|
+
const now = new Date().toISOString();
|
|
1622
|
+
const chapterWordCount = countChapterLength(ch.content, countingMode);
|
|
1623
|
+
const newEntry = {
|
|
1624
|
+
number: chapterNumber,
|
|
1625
|
+
title: output.title,
|
|
1626
|
+
status: "imported",
|
|
1627
|
+
wordCount: chapterWordCount,
|
|
1628
|
+
createdAt: now,
|
|
1629
|
+
updatedAt: now,
|
|
1630
|
+
auditIssues: [],
|
|
1631
|
+
lengthWarnings: [],
|
|
1632
|
+
};
|
|
1633
|
+
// Replace if exists (resume case), otherwise append
|
|
1634
|
+
const existingIdx = existingIndex.findIndex((e) => e.number === chapterNumber);
|
|
1635
|
+
const updatedIndex = existingIdx >= 0
|
|
1636
|
+
? existingIndex.map((e, idx) => idx === existingIdx ? newEntry : e)
|
|
1637
|
+
: [...existingIndex, newEntry];
|
|
1638
|
+
await this.state.saveChapterIndex(input.bookId, updatedIndex);
|
|
1639
|
+
// Snapshot state after each chapter for rollback + resume support
|
|
1640
|
+
await this.state.snapshotState(input.bookId, chapterNumber);
|
|
1641
|
+
importedCount++;
|
|
1642
|
+
totalWords += chapterWordCount;
|
|
1643
|
+
}
|
|
1644
|
+
if (input.chapters.length > 0) {
|
|
1645
|
+
await this.markBookActiveIfNeeded(input.bookId);
|
|
1646
|
+
await this.syncCurrentStateFactHistory(input.bookId, input.chapters.length);
|
|
1647
|
+
}
|
|
1648
|
+
const nextChapter = input.chapters.length + 1;
|
|
1649
|
+
log?.info(this.localize(resolvedLanguage, {
|
|
1650
|
+
zh: `完成。已导入 ${importedCount} 章,共 ${formatLengthCount(totalWords, countingMode)}。下一章:${nextChapter}`,
|
|
1651
|
+
en: `Done. ${importedCount} chapters imported, ${formatLengthCount(totalWords, countingMode)}. Next chapter: ${nextChapter}`,
|
|
1652
|
+
}));
|
|
1653
|
+
return {
|
|
1654
|
+
bookId: input.bookId,
|
|
1655
|
+
importedCount,
|
|
1656
|
+
totalWords,
|
|
1657
|
+
nextChapter,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
finally {
|
|
1661
|
+
await releaseLock();
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
static addUsage(a, b) {
|
|
1665
|
+
if (!b)
|
|
1666
|
+
return a;
|
|
1667
|
+
return {
|
|
1668
|
+
promptTokens: a.promptTokens + b.promptTokens,
|
|
1669
|
+
completionTokens: a.completionTokens + b.completionTokens,
|
|
1670
|
+
totalTokens: a.totalTokens + b.totalTokens,
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
async buildPersistenceOutput(bookId, book, bookDir, chapterNumber, output, finalContent, countingMode, reducedControlInput) {
|
|
1674
|
+
if (finalContent === output.content) {
|
|
1675
|
+
return output;
|
|
1676
|
+
}
|
|
1677
|
+
const analyzer = new ChapterAnalyzerAgent(this.agentCtxFor("chapter-analyzer", bookId));
|
|
1678
|
+
const analyzed = await analyzer.analyzeChapter({
|
|
1679
|
+
book,
|
|
1680
|
+
bookDir,
|
|
1681
|
+
chapterNumber,
|
|
1682
|
+
chapterContent: finalContent,
|
|
1683
|
+
chapterTitle: output.title,
|
|
1684
|
+
chapterIntent: reducedControlInput?.chapterIntent,
|
|
1685
|
+
contextPackage: reducedControlInput?.contextPackage,
|
|
1686
|
+
ruleStack: reducedControlInput?.ruleStack,
|
|
1687
|
+
});
|
|
1688
|
+
return {
|
|
1689
|
+
...analyzed,
|
|
1690
|
+
content: finalContent,
|
|
1691
|
+
wordCount: countChapterLength(finalContent, countingMode),
|
|
1692
|
+
postWriteErrors: [],
|
|
1693
|
+
postWriteWarnings: [],
|
|
1694
|
+
hookHealthIssues: output.hookHealthIssues,
|
|
1695
|
+
tokenUsage: output.tokenUsage,
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
async assertNoPendingStateRepair(bookId) {
|
|
1699
|
+
const existingIndex = await this.state.loadChapterIndex(bookId);
|
|
1700
|
+
const latestChapter = [...existingIndex].sort((left, right) => right.number - left.number)[0];
|
|
1701
|
+
if (latestChapter?.status !== "state-degraded") {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
throw new Error(`Latest chapter ${latestChapter.number} is state-degraded. Repair state or rewrite that chapter before continuing.`);
|
|
1705
|
+
}
|
|
1706
|
+
// ---------------------------------------------------------------------------
|
|
1707
|
+
// Helpers
|
|
1708
|
+
// ---------------------------------------------------------------------------
|
|
1709
|
+
async prepareWriteInput(book, bookDir, chapterNumber, externalContext) {
|
|
1710
|
+
if ((this.config.inputGovernanceMode ?? "v2") === "legacy") {
|
|
1711
|
+
return { externalContext };
|
|
1712
|
+
}
|
|
1713
|
+
const { plan, composed } = await this.createGovernedArtifacts(book, bookDir, chapterNumber, externalContext, { reuseExistingIntentWhenContextMissing: true });
|
|
1714
|
+
return {
|
|
1715
|
+
chapterIntent: plan.intentMarkdown,
|
|
1716
|
+
contextPackage: composed.contextPackage,
|
|
1717
|
+
ruleStack: composed.ruleStack,
|
|
1718
|
+
trace: composed.trace,
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
async resetImportReplayTruthFiles(bookDir, language) {
|
|
1722
|
+
const storyDir = join(bookDir, "story");
|
|
1723
|
+
await Promise.all([
|
|
1724
|
+
writeFile(join(storyDir, "current_state.md"), this.buildImportReplayStateSeed(language), "utf-8"),
|
|
1725
|
+
writeFile(join(storyDir, "pending_hooks.md"), this.buildImportReplayHooksSeed(language), "utf-8"),
|
|
1726
|
+
rm(join(storyDir, "chapter_summaries.md"), { force: true }),
|
|
1727
|
+
rm(join(storyDir, "subplot_board.md"), { force: true }),
|
|
1728
|
+
rm(join(storyDir, "emotional_arcs.md"), { force: true }),
|
|
1729
|
+
rm(join(storyDir, "character_matrix.md"), { force: true }),
|
|
1730
|
+
rm(join(storyDir, "volume_summaries.md"), { force: true }),
|
|
1731
|
+
rm(join(storyDir, "particle_ledger.md"), { force: true }),
|
|
1732
|
+
rm(join(storyDir, "memory.db"), { force: true }),
|
|
1733
|
+
rm(join(storyDir, "memory.db-shm"), { force: true }),
|
|
1734
|
+
rm(join(storyDir, "memory.db-wal"), { force: true }),
|
|
1735
|
+
rm(join(storyDir, "state"), { recursive: true, force: true }),
|
|
1736
|
+
rm(join(storyDir, "snapshots"), { recursive: true, force: true }),
|
|
1737
|
+
]);
|
|
1738
|
+
}
|
|
1739
|
+
buildImportReplayStateSeed(language) {
|
|
1740
|
+
if (language === "en") {
|
|
1741
|
+
return [
|
|
1742
|
+
"# Current State",
|
|
1743
|
+
"",
|
|
1744
|
+
"| Field | Value |",
|
|
1745
|
+
"| --- | --- |",
|
|
1746
|
+
"| Current Chapter | 0 |",
|
|
1747
|
+
"| Current Location | (not set) |",
|
|
1748
|
+
"| Protagonist State | (not set) |",
|
|
1749
|
+
"| Current Goal | (not set) |",
|
|
1750
|
+
"| Current Constraint | (not set) |",
|
|
1751
|
+
"| Current Alliances | (not set) |",
|
|
1752
|
+
"| Current Conflict | (not set) |",
|
|
1753
|
+
"",
|
|
1754
|
+
].join("\n");
|
|
1755
|
+
}
|
|
1756
|
+
return [
|
|
1757
|
+
"# 当前状态",
|
|
1758
|
+
"",
|
|
1759
|
+
"| 字段 | 值 |",
|
|
1760
|
+
"| --- | --- |",
|
|
1761
|
+
"| 当前章节 | 0 |",
|
|
1762
|
+
"| 当前位置 | (未设定) |",
|
|
1763
|
+
"| 主角状态 | (未设定) |",
|
|
1764
|
+
"| 当前目标 | (未设定) |",
|
|
1765
|
+
"| 当前限制 | (未设定) |",
|
|
1766
|
+
"| 当前敌我 | (未设定) |",
|
|
1767
|
+
"| 当前冲突 | (未设定) |",
|
|
1768
|
+
"",
|
|
1769
|
+
].join("\n");
|
|
1770
|
+
}
|
|
1771
|
+
buildImportReplayHooksSeed(language) {
|
|
1772
|
+
if (language === "en") {
|
|
1773
|
+
return [
|
|
1774
|
+
"# Pending Hooks",
|
|
1775
|
+
"",
|
|
1776
|
+
"| hook_id | start_chapter | type | status | last_advanced_chapter | expected_payoff | notes |",
|
|
1777
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
1778
|
+
"",
|
|
1779
|
+
].join("\n");
|
|
1780
|
+
}
|
|
1781
|
+
return [
|
|
1782
|
+
"# 伏笔池",
|
|
1783
|
+
"",
|
|
1784
|
+
"| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |",
|
|
1785
|
+
"| --- | --- | --- | --- | --- | --- | --- |",
|
|
1786
|
+
"",
|
|
1787
|
+
].join("\n");
|
|
1788
|
+
}
|
|
1789
|
+
async normalizeDraftLengthIfNeeded(params) {
|
|
1790
|
+
const writerCount = countChapterLength(params.chapterContent, params.lengthSpec.countingMode);
|
|
1791
|
+
if (!isOutsideSoftRange(writerCount, params.lengthSpec)) {
|
|
1792
|
+
return {
|
|
1793
|
+
content: params.chapterContent,
|
|
1794
|
+
wordCount: writerCount,
|
|
1795
|
+
applied: false,
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
const normalizer = new LengthNormalizerAgent(this.agentCtxFor("length-normalizer", params.bookId));
|
|
1799
|
+
const normalized = await normalizer.normalizeChapter({
|
|
1800
|
+
chapterContent: params.chapterContent,
|
|
1801
|
+
lengthSpec: params.lengthSpec,
|
|
1802
|
+
chapterIntent: params.chapterIntent,
|
|
1803
|
+
});
|
|
1804
|
+
// Safety net: if normalizer output is less than 25% of original, it was too destructive.
|
|
1805
|
+
// Reject and keep original content.
|
|
1806
|
+
if (normalized.finalCount < writerCount * 0.25) {
|
|
1807
|
+
this.logWarn(this.languageFromLengthSpec(params.lengthSpec), {
|
|
1808
|
+
zh: `字数归一化被拒绝:第${params.chapterNumber}章 ${writerCount} -> ${normalized.finalCount}(砍了${Math.round((1 - normalized.finalCount / writerCount) * 100)}%,超过安全阈值)`,
|
|
1809
|
+
en: `Length normalization rejected for chapter ${params.chapterNumber}: ${writerCount} -> ${normalized.finalCount} (cut ${Math.round((1 - normalized.finalCount / writerCount) * 100)}%, exceeds safety threshold)`,
|
|
1810
|
+
});
|
|
1811
|
+
return {
|
|
1812
|
+
content: params.chapterContent,
|
|
1813
|
+
wordCount: writerCount,
|
|
1814
|
+
applied: false,
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
this.logInfo(this.languageFromLengthSpec(params.lengthSpec), {
|
|
1818
|
+
zh: `审计前字数归一化:第${params.chapterNumber}章 ${writerCount} -> ${normalized.finalCount}`,
|
|
1819
|
+
en: `Length normalization before audit for chapter ${params.chapterNumber}: ${writerCount} -> ${normalized.finalCount}`,
|
|
1820
|
+
});
|
|
1821
|
+
return {
|
|
1822
|
+
content: normalized.normalizedContent,
|
|
1823
|
+
wordCount: normalized.finalCount,
|
|
1824
|
+
applied: normalized.applied,
|
|
1825
|
+
tokenUsage: normalized.tokenUsage,
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
assertChapterContentNotEmpty(content, chapterNumber, stage) {
|
|
1829
|
+
if (content.trim().length > 0)
|
|
1830
|
+
return;
|
|
1831
|
+
throw new Error(`Chapter ${chapterNumber} has empty chapter content after ${stage}`);
|
|
1832
|
+
}
|
|
1833
|
+
async syncCurrentStateFactHistory(bookId, uptoChapter) {
|
|
1834
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1835
|
+
try {
|
|
1836
|
+
await this.rebuildCurrentStateFactHistory(bookDir, uptoChapter);
|
|
1837
|
+
}
|
|
1838
|
+
catch (error) {
|
|
1839
|
+
if (this.isMemoryIndexUnavailableError(error)) {
|
|
1840
|
+
if (this.canOpenMemoryIndex(bookDir)) {
|
|
1841
|
+
try {
|
|
1842
|
+
await this.rebuildCurrentStateFactHistory(bookDir, uptoChapter);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
catch (retryError) {
|
|
1846
|
+
error = retryError;
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
if (!this.memoryIndexFallbackWarned) {
|
|
1851
|
+
this.memoryIndexFallbackWarned = true;
|
|
1852
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1853
|
+
zh: "当前 Node 运行时不支持 SQLite 记忆索引,继续使用 Markdown 回退方案。",
|
|
1854
|
+
en: "SQLite memory index unavailable on this Node runtime; continuing with markdown fallback.",
|
|
1855
|
+
});
|
|
1856
|
+
await this.logMemoryIndexDebugInfo(bookId, error);
|
|
1857
|
+
}
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1862
|
+
zh: `状态事实同步已跳过:${String(error)}`,
|
|
1863
|
+
en: `State fact sync skipped: ${String(error)}`,
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
async syncLegacyStructuredStateFromMarkdown(bookDir, chapterNumber, output) {
|
|
1868
|
+
if (output?.runtimeStateDelta || output?.runtimeStateSnapshot) {
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
await rewriteStructuredStateFromMarkdown({
|
|
1872
|
+
bookDir,
|
|
1873
|
+
fallbackChapter: chapterNumber,
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
async syncNarrativeMemoryIndex(bookId) {
|
|
1877
|
+
const bookDir = this.state.bookDir(bookId);
|
|
1878
|
+
try {
|
|
1879
|
+
await this.rebuildNarrativeMemoryIndex(bookDir);
|
|
1880
|
+
}
|
|
1881
|
+
catch (error) {
|
|
1882
|
+
if (this.isMemoryIndexUnavailableError(error)) {
|
|
1883
|
+
if (this.canOpenMemoryIndex(bookDir)) {
|
|
1884
|
+
try {
|
|
1885
|
+
await this.rebuildNarrativeMemoryIndex(bookDir);
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
catch (retryError) {
|
|
1889
|
+
error = retryError;
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
else {
|
|
1893
|
+
if (!this.memoryIndexFallbackWarned) {
|
|
1894
|
+
this.memoryIndexFallbackWarned = true;
|
|
1895
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1896
|
+
zh: "当前 Node 运行时不支持 SQLite 记忆索引,继续使用 Markdown 回退方案。",
|
|
1897
|
+
en: "SQLite memory index unavailable on this Node runtime; continuing with markdown fallback.",
|
|
1898
|
+
});
|
|
1899
|
+
await this.logMemoryIndexDebugInfo(bookId, error);
|
|
1900
|
+
}
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
1905
|
+
zh: `叙事记忆同步已跳过:${String(error)}`,
|
|
1906
|
+
en: `Narrative memory sync skipped: ${String(error)}`,
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
async rebuildCurrentStateFactHistory(bookDir, uptoChapter) {
|
|
1911
|
+
const memoryDb = await this.withMemoryIndexRetry(async () => {
|
|
1912
|
+
const db = new MemoryDB(bookDir);
|
|
1913
|
+
try {
|
|
1914
|
+
db.resetFacts();
|
|
1915
|
+
const activeFacts = new Map();
|
|
1916
|
+
for (let chapter = 0; chapter <= uptoChapter; chapter++) {
|
|
1917
|
+
const snapshotFacts = await loadSnapshotCurrentStateFacts(bookDir, chapter);
|
|
1918
|
+
if (snapshotFacts.length === 0)
|
|
1919
|
+
continue;
|
|
1920
|
+
const nextFacts = new Map();
|
|
1921
|
+
for (const fact of snapshotFacts) {
|
|
1922
|
+
nextFacts.set(this.factKey(fact), {
|
|
1923
|
+
subject: fact.subject,
|
|
1924
|
+
predicate: fact.predicate,
|
|
1925
|
+
object: fact.object,
|
|
1926
|
+
validFromChapter: chapter,
|
|
1927
|
+
validUntilChapter: null,
|
|
1928
|
+
sourceChapter: chapter,
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
for (const [key, previous] of activeFacts.entries()) {
|
|
1932
|
+
const next = nextFacts.get(key);
|
|
1933
|
+
if (!next || next.object !== previous.object) {
|
|
1934
|
+
db.invalidateFact(previous.id, chapter);
|
|
1935
|
+
activeFacts.delete(key);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
for (const [key, fact] of nextFacts.entries()) {
|
|
1939
|
+
if (activeFacts.has(key))
|
|
1940
|
+
continue;
|
|
1941
|
+
const id = db.addFact(fact);
|
|
1942
|
+
activeFacts.set(key, { id, object: fact.object });
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
return db;
|
|
1946
|
+
}
|
|
1947
|
+
catch (error) {
|
|
1948
|
+
db.close();
|
|
1949
|
+
throw error;
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
try {
|
|
1953
|
+
// No-op: keep the db open only for the duration of the rebuild.
|
|
1954
|
+
}
|
|
1955
|
+
finally {
|
|
1956
|
+
memoryDb.close();
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
async rebuildNarrativeMemoryIndex(bookDir) {
|
|
1960
|
+
const memorySeed = await loadNarrativeMemorySeed(bookDir);
|
|
1961
|
+
const memoryDb = await this.withMemoryIndexRetry(() => {
|
|
1962
|
+
const db = new MemoryDB(bookDir);
|
|
1963
|
+
try {
|
|
1964
|
+
db.replaceSummaries(memorySeed.summaries);
|
|
1965
|
+
db.replaceHooks(memorySeed.hooks);
|
|
1966
|
+
return db;
|
|
1967
|
+
}
|
|
1968
|
+
catch (error) {
|
|
1969
|
+
db.close();
|
|
1970
|
+
throw error;
|
|
1971
|
+
}
|
|
1972
|
+
});
|
|
1973
|
+
try {
|
|
1974
|
+
// No-op: keep the db open only for the duration of the rebuild.
|
|
1975
|
+
}
|
|
1976
|
+
finally {
|
|
1977
|
+
memoryDb.close();
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
canOpenMemoryIndex(bookDir) {
|
|
1981
|
+
let memoryDb = null;
|
|
1982
|
+
try {
|
|
1983
|
+
memoryDb = new MemoryDB(bookDir);
|
|
1984
|
+
return true;
|
|
1985
|
+
}
|
|
1986
|
+
catch {
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1989
|
+
finally {
|
|
1990
|
+
memoryDb?.close();
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
async logMemoryIndexDebugInfo(bookId, error) {
|
|
1994
|
+
if (process.env.INKOS_DEBUG_SQLITE_MEMORY !== "1") {
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
1998
|
+
? String(error.code ?? "")
|
|
1999
|
+
: "";
|
|
2000
|
+
const message = error instanceof Error
|
|
2001
|
+
? error.message
|
|
2002
|
+
: String(error);
|
|
2003
|
+
this.logWarn(await this.resolveBookLanguageById(bookId), {
|
|
2004
|
+
zh: `SQLite 记忆索引调试:node=${process.version}; execArgv=${JSON.stringify(process.execArgv)}; code=${code || "(none)"}; message=${message}`,
|
|
2005
|
+
en: `SQLite memory debug: node=${process.version}; execArgv=${JSON.stringify(process.execArgv)}; code=${code || "(none)"}; message=${message}`,
|
|
2006
|
+
});
|
|
2007
|
+
}
|
|
2008
|
+
async withMemoryIndexRetry(operation) {
|
|
2009
|
+
const retryDelaysMs = [0, 25, 75];
|
|
2010
|
+
let lastError;
|
|
2011
|
+
for (let attempt = 0; attempt < retryDelaysMs.length; attempt += 1) {
|
|
2012
|
+
try {
|
|
2013
|
+
return await operation();
|
|
2014
|
+
}
|
|
2015
|
+
catch (error) {
|
|
2016
|
+
lastError = error;
|
|
2017
|
+
if (!this.isMemoryIndexBusyError(error) || attempt === retryDelaysMs.length - 1) {
|
|
2018
|
+
throw error;
|
|
2019
|
+
}
|
|
2020
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelaysMs[attempt + 1]));
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
throw lastError;
|
|
2024
|
+
}
|
|
2025
|
+
isMemoryIndexUnavailableError(error) {
|
|
2026
|
+
if (!error)
|
|
2027
|
+
return false;
|
|
2028
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
2029
|
+
? String(error.code ?? "")
|
|
2030
|
+
: "";
|
|
2031
|
+
const message = error instanceof Error
|
|
2032
|
+
? error.message
|
|
2033
|
+
: String(error);
|
|
2034
|
+
const normalizedMessage = message.trim();
|
|
2035
|
+
return /^No such built-in module:\s*node:sqlite$/i.test(normalizedMessage)
|
|
2036
|
+
|| /^Cannot find module ['"]node:sqlite['"]$/i.test(normalizedMessage)
|
|
2037
|
+
|| (code === "ERR_UNKNOWN_BUILTIN_MODULE" && /\bnode:sqlite\b/i.test(normalizedMessage));
|
|
2038
|
+
}
|
|
2039
|
+
isMemoryIndexBusyError(error) {
|
|
2040
|
+
if (!error)
|
|
2041
|
+
return false;
|
|
2042
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
2043
|
+
? String(error.code ?? "")
|
|
2044
|
+
: "";
|
|
2045
|
+
const message = error instanceof Error
|
|
2046
|
+
? error.message
|
|
2047
|
+
: String(error);
|
|
2048
|
+
return code === "SQLITE_BUSY"
|
|
2049
|
+
|| code === "SQLITE_LOCKED"
|
|
2050
|
+
|| /\bSQLITE_BUSY\b/i.test(message)
|
|
2051
|
+
|| /\bSQLITE_LOCKED\b/i.test(message)
|
|
2052
|
+
|| /database is locked/i.test(message)
|
|
2053
|
+
|| /database is busy/i.test(message);
|
|
2054
|
+
}
|
|
2055
|
+
factKey(fact) {
|
|
2056
|
+
return `${fact.subject}::${fact.predicate}`;
|
|
2057
|
+
}
|
|
2058
|
+
buildLengthWarnings(chapterNumber, finalCount, lengthSpec) {
|
|
2059
|
+
if (!isOutsideHardRange(finalCount, lengthSpec)) {
|
|
2060
|
+
return [];
|
|
2061
|
+
}
|
|
2062
|
+
return [
|
|
2063
|
+
this.localize(this.languageFromLengthSpec(lengthSpec), {
|
|
2064
|
+
zh: `第${chapterNumber}章经过一次字数归一化后仍超出硬区间(${lengthSpec.hardMin}-${lengthSpec.hardMax},实际 ${finalCount})。`,
|
|
2065
|
+
en: `Chapter ${chapterNumber} remains outside hard range (${lengthSpec.hardMin}-${lengthSpec.hardMax}, actual ${finalCount}) after a single normalization pass.`,
|
|
2066
|
+
}),
|
|
2067
|
+
];
|
|
2068
|
+
}
|
|
2069
|
+
buildLengthTelemetry(params) {
|
|
2070
|
+
return {
|
|
2071
|
+
target: params.lengthSpec.target,
|
|
2072
|
+
softMin: params.lengthSpec.softMin,
|
|
2073
|
+
softMax: params.lengthSpec.softMax,
|
|
2074
|
+
hardMin: params.lengthSpec.hardMin,
|
|
2075
|
+
hardMax: params.lengthSpec.hardMax,
|
|
2076
|
+
countingMode: params.lengthSpec.countingMode,
|
|
2077
|
+
writerCount: params.writerCount,
|
|
2078
|
+
postWriterNormalizeCount: params.postWriterNormalizeCount,
|
|
2079
|
+
postReviseCount: params.postReviseCount,
|
|
2080
|
+
finalCount: params.finalCount,
|
|
2081
|
+
normalizeApplied: params.normalizeApplied,
|
|
2082
|
+
lengthWarning: params.lengthWarning,
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
async persistAuditDriftGuidance(params) {
|
|
2086
|
+
const storyDir = join(params.bookDir, "story");
|
|
2087
|
+
const driftPath = join(storyDir, "audit_drift.md");
|
|
2088
|
+
const statePath = join(storyDir, "current_state.md");
|
|
2089
|
+
const currentState = await readFile(statePath, "utf-8").catch(() => "");
|
|
2090
|
+
const sanitizedState = this.stripAuditDriftCorrectionBlock(currentState).trimEnd();
|
|
2091
|
+
if (sanitizedState !== currentState) {
|
|
2092
|
+
await writeFile(statePath, sanitizedState, "utf-8");
|
|
2093
|
+
}
|
|
2094
|
+
if (params.issues.length === 0) {
|
|
2095
|
+
await rm(driftPath, { force: true }).catch(() => undefined);
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
const block = [
|
|
2099
|
+
this.localize(params.language, {
|
|
2100
|
+
zh: "# 审计纠偏",
|
|
2101
|
+
en: "# Audit Drift",
|
|
2102
|
+
}),
|
|
2103
|
+
"",
|
|
2104
|
+
this.localize(params.language, {
|
|
2105
|
+
zh: "## 审计纠偏(自动生成,下一章写作前参照)",
|
|
2106
|
+
en: "## Audit Drift Correction",
|
|
2107
|
+
}),
|
|
2108
|
+
"",
|
|
2109
|
+
this.localize(params.language, {
|
|
2110
|
+
zh: `> 第${params.chapterNumber}章审计发现以下问题,下一章写作时必须避免:`,
|
|
2111
|
+
en: `> Chapter ${params.chapterNumber} audit found the following issues to avoid in the next chapter:`,
|
|
2112
|
+
}),
|
|
2113
|
+
...params.issues.map((issue) => `> - [${issue.severity}] ${issue.category}: ${issue.description}`),
|
|
2114
|
+
"",
|
|
2115
|
+
].join("\n");
|
|
2116
|
+
await writeFile(driftPath, block, "utf-8");
|
|
2117
|
+
}
|
|
2118
|
+
stripAuditDriftCorrectionBlock(currentState) {
|
|
2119
|
+
const headers = [
|
|
2120
|
+
"## 审计纠偏(自动生成,下一章写作前参照)",
|
|
2121
|
+
"## Audit Drift Correction",
|
|
2122
|
+
"# 审计纠偏",
|
|
2123
|
+
"# Audit Drift",
|
|
2124
|
+
];
|
|
2125
|
+
let cutIndex = -1;
|
|
2126
|
+
for (const header of headers) {
|
|
2127
|
+
const index = currentState.indexOf(header);
|
|
2128
|
+
if (index >= 0 && (cutIndex < 0 || index < cutIndex)) {
|
|
2129
|
+
cutIndex = index;
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (cutIndex < 0) {
|
|
2133
|
+
return currentState;
|
|
2134
|
+
}
|
|
2135
|
+
return currentState.slice(0, cutIndex).trimEnd();
|
|
2136
|
+
}
|
|
2137
|
+
logLengthWarnings(lengthWarnings) {
|
|
2138
|
+
for (const warning of lengthWarnings) {
|
|
2139
|
+
this.config.logger?.warn(warning);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
restoreLostAuditIssues(previous, next) {
|
|
2143
|
+
if (next.passed || next.issues.length > 0 || previous.issues.length === 0) {
|
|
2144
|
+
return next;
|
|
2145
|
+
}
|
|
2146
|
+
return {
|
|
2147
|
+
...next,
|
|
2148
|
+
issues: previous.issues,
|
|
2149
|
+
summary: next.summary || previous.summary,
|
|
2150
|
+
};
|
|
2151
|
+
}
|
|
2152
|
+
restoreActionableAuditIfLost(previous, next) {
|
|
2153
|
+
const auditResult = this.restoreLostAuditIssues(previous.auditResult, next.auditResult);
|
|
2154
|
+
if (auditResult === next.auditResult) {
|
|
2155
|
+
return next;
|
|
2156
|
+
}
|
|
2157
|
+
return {
|
|
2158
|
+
...next,
|
|
2159
|
+
auditResult,
|
|
2160
|
+
revisionBlockingIssues: previous.revisionBlockingIssues,
|
|
2161
|
+
blockingCount: previous.blockingCount,
|
|
2162
|
+
criticalCount: previous.criticalCount,
|
|
2163
|
+
};
|
|
2164
|
+
}
|
|
2165
|
+
async evaluateMergedAudit(params) {
|
|
2166
|
+
const llmAudit = await params.auditor.auditChapter(params.bookDir, params.chapterContent, params.chapterNumber, params.book.genre, params.auditOptions);
|
|
2167
|
+
const aiTells = analyzeAITells(params.chapterContent, params.language);
|
|
2168
|
+
const sensitiveResult = analyzeSensitiveWords(params.chapterContent, undefined, params.language);
|
|
2169
|
+
const longSpanFatigue = await analyzeLongSpanFatigue({
|
|
2170
|
+
bookDir: params.bookDir,
|
|
2171
|
+
chapterNumber: params.chapterNumber,
|
|
2172
|
+
chapterContent: params.chapterContent,
|
|
2173
|
+
language: params.language,
|
|
2174
|
+
});
|
|
2175
|
+
const hasBlockedWords = sensitiveResult.found.some((f) => f.severity === "block");
|
|
2176
|
+
const issues = [
|
|
2177
|
+
...llmAudit.issues,
|
|
2178
|
+
...aiTells.issues,
|
|
2179
|
+
...sensitiveResult.issues,
|
|
2180
|
+
...longSpanFatigue.issues,
|
|
2181
|
+
];
|
|
2182
|
+
// revisionBlockingIssues excludes long-span-fatigue issues by
|
|
2183
|
+
// construction (not by category name) so that an LLM-reported issue
|
|
2184
|
+
// sharing a category label with a long-span issue is still counted.
|
|
2185
|
+
const revisionBlockingIssues = [
|
|
2186
|
+
...llmAudit.issues,
|
|
2187
|
+
...aiTells.issues,
|
|
2188
|
+
...sensitiveResult.issues,
|
|
2189
|
+
];
|
|
2190
|
+
return {
|
|
2191
|
+
auditResult: {
|
|
2192
|
+
passed: hasBlockedWords ? false : llmAudit.passed,
|
|
2193
|
+
issues,
|
|
2194
|
+
summary: llmAudit.summary,
|
|
2195
|
+
tokenUsage: llmAudit.tokenUsage,
|
|
2196
|
+
},
|
|
2197
|
+
aiTellCount: aiTells.issues.length,
|
|
2198
|
+
blockingCount: revisionBlockingIssues.filter((issue) => issue.severity === "warning" || issue.severity === "critical").length,
|
|
2199
|
+
criticalCount: revisionBlockingIssues.filter((issue) => issue.severity === "critical").length,
|
|
2200
|
+
revisionBlockingIssues,
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
async markBookActiveIfNeeded(bookId) {
|
|
2204
|
+
const book = await this.state.loadBookConfig(bookId);
|
|
2205
|
+
if (book.status !== "outlining")
|
|
2206
|
+
return;
|
|
2207
|
+
await this.state.saveBookConfig(bookId, {
|
|
2208
|
+
...book,
|
|
2209
|
+
status: "active",
|
|
2210
|
+
updatedAt: new Date().toISOString(),
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
async createGovernedArtifacts(book, bookDir, chapterNumber, externalContext, options) {
|
|
2214
|
+
const plan = await this.resolveGovernedPlan(book, bookDir, chapterNumber, externalContext, options);
|
|
2215
|
+
const composer = new ComposerAgent(this.agentCtxFor("composer", book.id));
|
|
2216
|
+
const composed = await composer.composeChapter({
|
|
2217
|
+
book,
|
|
2218
|
+
bookDir,
|
|
2219
|
+
chapterNumber,
|
|
2220
|
+
plan,
|
|
2221
|
+
});
|
|
2222
|
+
return { plan, composed };
|
|
2223
|
+
}
|
|
2224
|
+
async resolveGovernedPlan(book, bookDir, chapterNumber, externalContext, options) {
|
|
2225
|
+
if (options?.reuseExistingIntentWhenContextMissing &&
|
|
2226
|
+
(!externalContext || externalContext.trim().length === 0)) {
|
|
2227
|
+
const persisted = await loadPersistedPlan(bookDir, chapterNumber);
|
|
2228
|
+
if (persisted)
|
|
2229
|
+
return persisted;
|
|
2230
|
+
}
|
|
2231
|
+
const planner = new PlannerAgent(this.agentCtxFor("planner", book.id));
|
|
2232
|
+
return planner.planChapter({
|
|
2233
|
+
book,
|
|
2234
|
+
bookDir,
|
|
2235
|
+
chapterNumber,
|
|
2236
|
+
externalContext,
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
async emitWebhook(event, bookId, chapterNumber, data) {
|
|
2240
|
+
if (!this.config.notifyChannels || this.config.notifyChannels.length === 0)
|
|
2241
|
+
return;
|
|
2242
|
+
await dispatchWebhookEvent(this.config.notifyChannels, {
|
|
2243
|
+
event,
|
|
2244
|
+
bookId,
|
|
2245
|
+
chapterNumber,
|
|
2246
|
+
timestamp: new Date().toISOString(),
|
|
2247
|
+
data,
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
async readChapterContent(bookDir, chapterNumber) {
|
|
2251
|
+
const chaptersDir = join(bookDir, "chapters");
|
|
2252
|
+
const files = await readdir(chaptersDir);
|
|
2253
|
+
const paddedNum = String(chapterNumber).padStart(4, "0");
|
|
2254
|
+
const chapterFile = files.find((f) => f.startsWith(paddedNum) && f.endsWith(".md"));
|
|
2255
|
+
if (!chapterFile) {
|
|
2256
|
+
throw new Error(`Chapter ${chapterNumber} file not found in ${chaptersDir}`);
|
|
2257
|
+
}
|
|
2258
|
+
const raw = await readFile(join(chaptersDir, chapterFile), "utf-8");
|
|
2259
|
+
// Strip the title line
|
|
2260
|
+
const lines = raw.split("\n");
|
|
2261
|
+
const contentStart = lines.findIndex((l, i) => i > 0 && l.trim().length > 0);
|
|
2262
|
+
return contentStart >= 0 ? lines.slice(contentStart).join("\n") : raw;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
//# sourceMappingURL=runner.js.map
|