| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458 |
- import type { LlmConfig } from "@/stores/wiki-store";
- import {
- streamChat,
- type ChatMessage,
- type RequestOverrides,
- type StreamCallbacks,
- } from "@/lib/llm-client";
- import { useWikiStore } from "@/stores/wiki-store";
- import type { AiWorkflowMode } from "@/lib/agent/workflow-mode";
- import type { AgentActivityEvent, AgentActivityKind } from "@/lib/agent/types";
- import type { LlmRequestCacheTrace } from "@/lib/llm-request-trace";
- import {
- isReasoningDisabled,
- isReasoningOnlyResponseError,
- withReasoningDisabled,
- } from "@/lib/reasoning-retry";
- import {
- charsPerTokenForLanguage,
- planChapterRequestBudget,
- } from "@/lib/context-budget";
- import {
- getEffectiveMaxOutputTokens,
- thinkingMinMaxTokens,
- } from "@/lib/llm-providers";
- import { USER_ABORT_MESSAGE, rethrowIfUserAbort, throwIfAborted } from "@/lib/user-abort";
- import {
- buildContextPack,
- contextPackToPrompt,
- type ContextPack,
- } from "./context-engine";
- import {
- collectWritingEntityWebSearch,
- type CollectWritingEntityWebSearchInput,
- type WritingEntityWebSearchResult,
- } from "./writing-entity-web-search";
- import { resolveDefaultModel, resolveNovelModel } from "./model-resolver";
- import { reviewChapter, type NovelReviewResult } from "./review-adapter";
- import type { TaskRouteResult } from "./task-router";
- import type { GoldenThreeChapterRequest } from "./golden-three-chapters";
- import {
- type ParsedChapterPlanComplianceResult,
- parseChapterPlanComplianceResult,
- runChapterPlanComplianceCheck,
- runChapterPlanDeviationRepair,
- shouldRepairChapterPlanDeviation,
- } from "./chapter-plan-compliance";
- import { buildChapterPlanExecutionSummary } from "./chapter-plan-execution-summary";
- import { capOutlineSourcesToBudget } from "./outline-context-index";
- import {
- contractToTaskBriefText,
- fallbackParseChapterExecutionContract,
- runChapterExecutionContractBuild,
- type ChapterExecutionContract,
- } from "./chapter-execution-contract";
- import {
- executionReportToToolSummary,
- extractExecutionRepairItems,
- runChapterExecutionReportCheck,
- type ChapterExecutionReport,
- } from "./chapter-execution-report";
- import {
- resolveChapterLengthSpec,
- type ChapterLengthSpec,
- buildDeepChapterBriefPrompt,
- buildDeepChapterDraftPrompt,
- buildDeepChapterExpansionPrompt,
- buildDeepChapterFinalPolishPrompt,
- buildDeepChapterRevisionPrompt,
- buildStableContextPrefix,
- } from "./deep-chapter-prompts";
- export interface DeepChapterGenerationInput {
- projectPath: string;
- userRequest: string;
- chapterNumber?: number;
- goldenThreeChapter?: GoldenThreeChapterRequest;
- dismantlingReferenceDirective?: string;
- llmConfig: LlmConfig;
- aiWorkflowMode?: AiWorkflowMode;
- resumeCheckpoint?: DeepChapterGenerationResumeCheckpoint;
- /** 用户在会话层确认的章节计划,作为写作任务书的权威依据注入 brief 阶段。 */
- planBlueprint?: string;
- /** 本轮启用的写作 Skill 约束,注入任务书/初稿/返修(稳定缓存前缀之后)。 */
- skillsPrompt?: string;
- }
- export interface DeepChapterGenerationCallbacks {
- onThinking?: (content: string) => void;
- onFinalContent?: (content: string) => void;
- onCheckpoint?: (checkpoint: DeepChapterGenerationResumeCheckpoint) => void;
- onWorkflowEvent?: (event: ChapterWorkflowEvent) => void;
- onActivityEvent?: (event: AgentActivityEvent) => void;
- onRequestTrace?: (trace: LlmRequestCacheTrace) => void;
- }
- export interface DeepChapterGenerationResult {
- finalContent: string;
- taskBrief: string;
- draftContent: string;
- reviewResults: NovelReviewResult[];
- revised: boolean;
- planCompliance?: string;
- executionReport?: string;
- }
- export type ChapterWorkflowEventType = "started" | "completed" | "error";
- export interface ChapterWorkflowEvent {
- type: ChapterWorkflowEventType;
- id: string;
- name: string;
- title: string;
- detail?: string;
- result?: string;
- params?: Record<string, unknown>;
- timestamp: number;
- }
- export type DeepChapterGenerationResumeStage =
- | "after_context"
- | "after_task_brief"
- | "after_draft"
- | "after_review"
- | "after_revision";
- export interface DeepChapterGenerationResumeCheckpoint {
- version: 1;
- originalRequest: string;
- chapterNumber?: number;
- stage: DeepChapterGenerationResumeStage;
- planBlueprint?: string;
- executionContract?: ChapterExecutionContract;
- taskBrief?: string;
- draftContent?: string;
- reviewResults?: NovelReviewResult[];
- currentContent?: string;
- }
- export interface DeepChapterGenerationDeps {
- buildContextPack: typeof buildContextPack;
- contextPackToPrompt: typeof contextPackToPrompt;
- reviewChapter: typeof reviewChapter;
- runChapterPlanComplianceCheck?: typeof runChapterPlanComplianceCheck;
- runChapterPlanDeviationRepair?: typeof runChapterPlanDeviationRepair;
- runChapterExecutionContractBuild?: typeof runChapterExecutionContractBuild;
- runChapterExecutionReportCheck?: typeof runChapterExecutionReportCheck;
- streamChat: (
- config: LlmConfig,
- messages: ChatMessage[],
- callbacks: StreamCallbacks,
- signal?: AbortSignal,
- requestOverrides?: RequestOverrides,
- ) => Promise<void>;
- collectWritingEntityWebSearch?: (
- input: CollectWritingEntityWebSearchInput,
- ) => Promise<WritingEntityWebSearchResult>;
- }
- const defaultDeps: DeepChapterGenerationDeps = {
- buildContextPack,
- contextPackToPrompt,
- reviewChapter,
- streamChat,
- };
- const REPEAT_CHECK_MIN_CHARS = 600;
- const REPEAT_WINDOW_CHARS = 120;
- const REPEAT_HIT_LIMIT = 3;
- function hasUsableChapterExecutionContract(contract: ChapterExecutionContract | null): contract is ChapterExecutionContract {
- if (!contract) return false;
- const hasSceneChecks = contract.sceneSteps.some((step) =>
- step.title.trim() ||
- step.purpose.trim() ||
- step.conflict.trim() ||
- step.turn.trim() ||
- step.requiredOutcome.trim() ||
- step.acceptanceCriteria.some((item) => item.trim()),
- );
- const hasInformationFlow =
- contract.informationFlow.reveal.some((item) => item.trim()) ||
- contract.informationFlow.hide.some((item) => item.trim()) ||
- contract.informationFlow.mislead.some((item) => item.trim()) ||
- contract.informationFlow.foreshadow.some((item) => item.trim());
- return (
- hasSceneChecks ||
- contract.mustDo.some((item) => item.trim()) ||
- contract.mustAvoid.some((item) => item.trim()) ||
- contract.dialogueGoals.some((item) => item.trim()) ||
- hasInformationFlow ||
- Boolean(contract.finalHook.trim())
- );
- }
- /** The mandatory outline may consume at most this share of the total context
- * budget. The remainder is left for memory/settings/search context so the
- * outline can never crowd the whole window out on its own. */
- const DEEP_CHAPTER_OUTLINE_MAX_FRAC = 0.7;
- /** Floor (tokens) for the non-outline context so a huge outline still leaves
- * some room for memory/settings/search hits. */
- const DEEP_CHAPTER_REST_TOKEN_FLOOR = 2000;
- export function shouldUseDeepChapterGeneration(
- _route: TaskRouteResult | null,
- enabled: boolean,
- ): boolean {
- void enabled;
- return (
- _route?.intent === "write_chapter" ||
- _route?.intent === "continue_chapter" ||
- _route?.intent === "rewrite_chapter" ||
- _route?.intent === "polish_chapter"
- );
- }
- interface ChapterWorkflowProfile {
- mode: AiWorkflowMode;
- runExecutionContractBuild: boolean;
- runAiReview: boolean;
- runFinalPolish: boolean;
- runPostRevisionReview: boolean;
- runPostDraftPlanAudits: boolean;
- completionTitle: string;
- completionResultPrefix: string;
- }
- function resolveChapterWorkflowProfile(
- mode: AiWorkflowMode | undefined,
- ): ChapterWorkflowProfile {
- const resolvedMode = mode ?? "strict";
- if (resolvedMode === "fast") {
- return {
- mode: "fast",
- runExecutionContractBuild: false,
- runAiReview: false,
- runFinalPolish: false,
- runPostRevisionReview: false,
- runPostDraftPlanAudits: false,
- completionTitle: "完成快速写作",
- completionResultPrefix: "快速模式写作完成",
- };
- }
- if (resolvedMode === "standard") {
- return {
- mode: "standard",
- runExecutionContractBuild: false,
- runAiReview: false,
- runFinalPolish: false,
- runPostRevisionReview: false,
- runPostDraftPlanAudits: false,
- completionTitle: "完成标准写作",
- completionResultPrefix: "标准模式写作完成",
- };
- }
- return {
- mode: "strict",
- runExecutionContractBuild: true,
- runAiReview: true,
- runFinalPolish: true,
- runPostRevisionReview: true,
- runPostDraftPlanAudits: true,
- completionTitle: "完成多任务写作循环",
- completionResultPrefix: "多任务写作循环完成",
- };
- }
- interface ChapterWorkflowStepSpec {
- name: string;
- title: string;
- detail?: string;
- params?: Record<string, unknown>;
- }
- function emitChapterWorkflowEvent(
- callbacks: DeepChapterGenerationCallbacks,
- type: ChapterWorkflowEventType,
- spec: ChapterWorkflowStepSpec,
- payload: {
- result?: string;
- detail?: string;
- params?: Record<string, unknown>;
- } = {},
- ): void {
- callbacks.onWorkflowEvent?.({
- type,
- id: `deep_chapter:${spec.name}`,
- name: spec.name,
- title: spec.title,
- detail: payload.detail ?? spec.detail,
- result: payload.result,
- params: {
- ...(spec.params ?? {}),
- ...(payload.params ?? {}),
- },
- timestamp: Date.now(),
- });
- }
- function emitDeepChapterActivity(
- callbacks: DeepChapterGenerationCallbacks,
- input: {
- id: string;
- stageId: string;
- kind: AgentActivityKind;
- title: string;
- content: string;
- timestamp?: number;
- },
- ): void {
- callbacks.onActivityEvent?.({
- id: input.id,
- stageId: input.stageId,
- kind: input.kind,
- title: input.title,
- content: input.content.trim() || "本阶段未返回可展示内容。",
- timestamp: input.timestamp ?? Date.now(),
- });
- }
- function emitDeepChapterStageStarted(
- callbacks: DeepChapterGenerationCallbacks,
- stageId: string,
- title: string,
- summary: string,
- ): void {
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:${stageId}:started:${Date.now()}`,
- stageId,
- kind: "stage_started",
- title: "进入阶段",
- content: `${title}\n${summary}`,
- });
- }
- function startChapterWorkflowStep(
- callbacks: DeepChapterGenerationCallbacks,
- spec: ChapterWorkflowStepSpec,
- ): void {
- emitChapterWorkflowEvent(callbacks, "started", spec);
- }
- function completeChapterWorkflowStep(
- callbacks: DeepChapterGenerationCallbacks,
- spec: ChapterWorkflowStepSpec,
- result: string,
- params?: Record<string, unknown>,
- ): void {
- emitChapterWorkflowEvent(callbacks, "completed", spec, { result, params });
- }
- function errorChapterWorkflowStep(
- callbacks: DeepChapterGenerationCallbacks,
- spec: ChapterWorkflowStepSpec,
- error: unknown,
- ): void {
- emitChapterWorkflowEvent(callbacks, "error", spec, {
- result: getErrorMessage(error),
- });
- }
- async function runChapterWorkflowStep<T>(
- callbacks: DeepChapterGenerationCallbacks,
- spec: ChapterWorkflowStepSpec,
- action: () => Promise<T>,
- formatResult: (value: T) => string,
- formatParams?: (value: T) => Record<string, unknown>,
- ): Promise<T> {
- startChapterWorkflowStep(callbacks, spec);
- try {
- const value = await action();
- completeChapterWorkflowStep(
- callbacks,
- spec,
- formatResult(value),
- formatParams?.(value),
- );
- return value;
- } catch (error) {
- errorChapterWorkflowStep(callbacks, spec, error);
- throw error;
- }
- }
- function createResumeCheckpoint(
- input: DeepChapterGenerationInput,
- stage: DeepChapterGenerationResumeStage,
- data: Partial<DeepChapterGenerationResumeCheckpoint> = {},
- ): DeepChapterGenerationResumeCheckpoint {
- const originalRequest =
- input.resumeCheckpoint?.originalRequest?.trim() || input.userRequest.trim();
- return {
- version: 1,
- originalRequest,
- chapterNumber: input.resumeCheckpoint?.chapterNumber ?? input.chapterNumber,
- stage,
- ...data,
- };
- }
- function checkpointStageAtLeast(
- checkpoint: DeepChapterGenerationResumeCheckpoint | null | undefined,
- target: DeepChapterGenerationResumeStage,
- ): boolean {
- if (!checkpoint) return false;
- const order: DeepChapterGenerationResumeStage[] = [
- "after_context",
- "after_task_brief",
- "after_draft",
- "after_review",
- "after_revision",
- ];
- return order.indexOf(checkpoint.stage) >= order.indexOf(target);
- }
- function hasCheckpointTaskBrief(
- checkpoint?: DeepChapterGenerationResumeCheckpoint | null,
- ): checkpoint is DeepChapterGenerationResumeCheckpoint & { taskBrief: string } {
- return (
- Boolean(checkpoint?.taskBrief?.trim()) &&
- checkpointStageAtLeast(checkpoint, "after_task_brief")
- );
- }
- function hasCheckpointDraft(
- checkpoint?: DeepChapterGenerationResumeCheckpoint | null,
- ): checkpoint is DeepChapterGenerationResumeCheckpoint & {
- taskBrief: string;
- draftContent: string;
- } {
- return (
- hasCheckpointTaskBrief(checkpoint) &&
- Boolean(checkpoint.draftContent?.trim()) &&
- checkpointStageAtLeast(checkpoint, "after_draft")
- );
- }
- function hasCheckpointReview(
- checkpoint?: DeepChapterGenerationResumeCheckpoint | null,
- ): checkpoint is DeepChapterGenerationResumeCheckpoint & {
- taskBrief: string;
- draftContent: string;
- reviewResults: NovelReviewResult[];
- } {
- return (
- hasCheckpointDraft(checkpoint) &&
- Array.isArray(checkpoint.reviewResults) &&
- checkpointStageAtLeast(checkpoint, "after_review")
- );
- }
- function hasCheckpointRevision(
- checkpoint?: DeepChapterGenerationResumeCheckpoint | null,
- ): checkpoint is DeepChapterGenerationResumeCheckpoint & {
- taskBrief: string;
- draftContent: string;
- reviewResults: NovelReviewResult[];
- currentContent: string;
- } {
- return (
- hasCheckpointReview(checkpoint) &&
- Boolean(checkpoint.currentContent?.trim()) &&
- checkpointStageAtLeast(checkpoint, "after_revision")
- );
- }
- export async function runDeepChapterGeneration(
- input: DeepChapterGenerationInput,
- callbacks: DeepChapterGenerationCallbacks = {},
- deps: DeepChapterGenerationDeps = defaultDeps,
- signal?: AbortSignal,
- ): Promise<DeepChapterGenerationResult> {
- throwIfAborted(signal);
- const resumeCheckpoint = input.resumeCheckpoint;
- const novelConfig = useWikiStore.getState().novelConfig;
- const writingConfig = resolveWritingConfig(input.llmConfig);
- const workflowConfig = resolveDefaultModel(input.llmConfig);
- const deAiConfig = resolveNovelModel(input.llmConfig, novelConfig, "deAi");
- const workflowProfile = resolveChapterWorkflowProfile(input.aiWorkflowMode);
- const lengthSpec = resolveCurrentChapterLengthSpec();
- const { loadSmartDeAiSkill } = await import("./de-ai-adapter");
- const planBlueprint =
- input.planBlueprint?.trim() || resumeCheckpoint?.planBlueprint?.trim() || undefined;
- const contextRequest = [
- input.userRequest.trim(),
- planBlueprint ? `用户已确认的章节计划:\n${planBlueprint}` : "",
- ].filter(Boolean).join("\n\n");
- const workflowBaseParams = {
- mode: workflowProfile.mode,
- chapterNumber: input.chapterNumber ?? null,
- };
- const planExecutionSummary = buildChapterPlanExecutionSummary(planBlueprint ?? "");
- let executionContract: ChapterExecutionContract | null =
- resumeCheckpoint?.executionContract ?? null;
- if (executionContract && !hasUsableChapterExecutionContract(executionContract)) {
- executionContract = null;
- }
- let executionContractText = "";
- if (!executionContract && workflowProfile.runExecutionContractBuild && planBlueprint) {
- try {
- const buildContract = deps.runChapterExecutionContractBuild || runChapterExecutionContractBuild;
- executionContract = callbacks.onRequestTrace
- ? await buildContract(workflowConfig, planBlueprint, signal, callbacks.onRequestTrace)
- : await buildContract(workflowConfig, planBlueprint, signal);
- } catch (error) {
- rethrowIfUserAbort(error, signal);
- console.warn("[Deep Chapter] 执行清单生成失败,使用本地兜底解析:", error);
- executionContract = fallbackParseChapterExecutionContract(planBlueprint);
- }
- }
- if (hasUsableChapterExecutionContract(executionContract)) {
- executionContractText = contractToTaskBriefText(executionContract);
- } else {
- executionContract = null;
- }
- const createCheckpoint = (
- stage: DeepChapterGenerationResumeStage,
- data: Partial<DeepChapterGenerationResumeCheckpoint> = {},
- ) => createResumeCheckpoint(input, stage, {
- planBlueprint,
- executionContract: executionContract ?? undefined,
- ...data,
- });
- const contextWorkflowStep: ChapterWorkflowStepSpec = {
- name: "chapter_context",
- title: "读取上下文",
- detail: "读取大纲、章节目标、近期剧情、人物状态、伏笔和时间线。",
- params: workflowBaseParams,
- };
- if (!resumeCheckpoint) {
- startChapterWorkflowStep(callbacks, contextWorkflowStep);
- }
- // 将在阶段1构建contextPack后再加载skill(需要contextPack用于场景检测)
- let customDeAiSkill: string | null = null;
- // 阶段0:前情分析。快速模式始终跳过;标准/严格模式跟随写作设置。
- // 记忆库的近期摘要与上一章结尾仍会注入。
- let previousChaptersAnalysis = "";
- if (
- workflowProfile.mode !== "fast" &&
- input.chapterNumber &&
- input.chapterNumber > 1 &&
- !resumeCheckpoint &&
- novelConfig.deepPreviousChaptersAnalysis
- ) {
- callbacks.onThinking?.(
- formatStageThinking("阶段0:前情分析", "正在读取并分析前3章完整内容..."),
- );
- const { analyzePreviousChapters } =
- await import("./previous-chapters-analysis");
- try {
- previousChaptersAnalysis = await analyzePreviousChapters(
- input.projectPath,
- input.chapterNumber,
- workflowConfig,
- 3,
- signal,
- );
- if (previousChaptersAnalysis) {
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段0:前情分析",
- `已完成前情分析(${previousChaptersAnalysis.length}字)\n\n${previousChaptersAnalysis.slice(0, 500)}...`,
- ),
- );
- }
- } catch (error) {
- rethrowIfUserAbort(error, signal);
- console.error("[deep-chapter-generation] 前情分析失败:", error);
- }
- }
- throwIfAborted(signal);
- let contextPack = await safeBuildChapterContextPack(
- deps,
- input.projectPath,
- contextRequest,
- input.chapterNumber,
- signal,
- );
- assertNotAborted(signal);
- if (workflowProfile.mode === "strict") {
- contextPack = await maybeInjectWritingEntityWebSearch({
- input,
- deps,
- contextPack,
- previousChaptersAnalysis,
- planBlueprint,
- workflowConfig,
- callbacks,
- signal,
- });
- assertNotAborted(signal);
- }
- if (!resumeCheckpoint) {
- emitDeepChapterStageStarted(
- callbacks,
- "read_context",
- "读取上下文",
- "读取大纲、上一章结尾、近期剧情、人物状态、伏笔和时间线。",
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:read_context:sources:${Date.now()}`,
- stageId: "read_context",
- kind: "read_source",
- title: "读取内容",
- content: [
- contextPack.outline?.trim()
- ? "已读取:作品完整大纲"
- : "未读取到:作品完整大纲",
- contextPack.previousChapterEnding?.trim()
- ? "已读取:上一章结尾"
- : "未读取到:上一章结尾",
- Array.isArray(contextPack.recentSummaries) &&
- contextPack.recentSummaries.length > 0
- ? `已读取:近期剧情 ${contextPack.recentSummaries.length} 条`
- : "未读取到:近期剧情",
- contextPack.characterStates?.trim()
- ? "已读取:人物状态"
- : "未读取到:人物状态",
- contextPack.foreshadowingStates?.trim()
- ? "已读取:伏笔状态"
- : "未读取到:伏笔状态",
- contextPack.timeline?.trim() ? "已读取:时间线" : "未读取到:时间线",
- ].join("\n"),
- });
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:read_context:goals:${Date.now()}`,
- stageId: "read_context",
- kind: "extract_goal",
- title: "提取目标",
- content: [
- "上一章结尾",
- "本章章节目标",
- "大纲要求推进的事件",
- "人物当前状态",
- "必须承接的伏笔",
- "必须完成与禁止违背内容",
- ].join("\n"),
- });
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:read_context:results:${Date.now()}`,
- stageId: "read_context",
- kind: "extract_result",
- title: "提取结果",
- content: formatContextExtractionResult(input, contextPack),
- });
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:read_context:output:${Date.now()}`,
- stageId: "read_context",
- kind: "stage_output",
- title: "阶段产物",
- content:
- "已形成章节生成约束包,将传入写作任务书、正文初稿、审稿和最终去AI味阶段。",
- });
- }
- // 阶段1后:加载智能skill(传递contextPack用于场景检测)
- customDeAiSkill = await loadSmartDeAiSkill(
- input.projectPath,
- contextRequest,
- contextPack,
- );
- throwIfAborted(signal);
- // 任务书(workflowConfig)、初稿/返修(writingConfig)、去AI味(deAiConfig)复用同一份
- // outlinePrompt + contextPrompt。这三个环节可能用不同模型、各有独立上下文窗口,
- // 因此预算取三者窗口的最小值:确保任一环节都不必依赖 llm-client 末级无差别截断
- // (末级截断按字符砍,会绕过 ContextPack 的字段优先级),并让各环节看到一致的上下文。
- const sharedContextWindows = [
- writingConfig.maxContextSize,
- workflowConfig.maxContextSize,
- deAiConfig.maxContextSize,
- ].filter((size): size is number => typeof size === "number" && size > 0);
- const sharedContextWindow = sharedContextWindows.length > 0
- ? Math.min(...sharedContextWindows)
- : input.llmConfig.maxContextSize;
- // 大纲与其余上下文共用同一窗口预算:资料包按三阶段最小窗分配。
- // 输出上限与思考地板按各阶段实际调用的模型重算,不再只读入口 llmConfig。
- const chapterAnalysisBudget = planChapterRequestBudget({
- maxContextSize: sharedContextWindow,
- chapterTargetChars: novelConfig.chapterTargetChars,
- stage: "analysis",
- maxOutputTokens: getEffectiveMaxOutputTokens(workflowConfig),
- thinkingFloorTokens: thinkingMinMaxTokens(
- workflowConfig.reasoning ?? { mode: "auto" },
- ),
- });
- const chapterGenerationBudget = planChapterRequestBudget({
- maxContextSize: sharedContextWindow,
- chapterTargetChars: novelConfig.chapterTargetChars,
- stage: "generation",
- maxOutputTokens: getEffectiveMaxOutputTokens(writingConfig),
- thinkingFloorTokens: thinkingMinMaxTokens(
- writingConfig.reasoning ?? { mode: "auto" },
- ),
- });
- const chapterDeAiBudget = planChapterRequestBudget({
- maxContextSize: sharedContextWindow,
- chapterTargetChars: novelConfig.chapterTargetChars,
- stage: "generation",
- maxOutputTokens: getEffectiveMaxOutputTokens(deAiConfig),
- thinkingFloorTokens: thinkingMinMaxTokens(
- deAiConfig.reasoning ?? { mode: "auto" },
- ),
- });
- const totalContextTokenBudget = chapterGenerationBudget.contextTokenBudget;
- const analysisRequestOverrides: RequestOverrides = {
- max_tokens: chapterAnalysisBudget.outputTokens,
- };
- const generationRequestOverrides: RequestOverrides = {
- max_tokens: chapterGenerationBudget.outputTokens,
- };
- const deAiRequestOverrides: RequestOverrides = {
- max_tokens: chapterDeAiBudget.outputTokens,
- };
- // Same density as the token estimator / trimContextPack (CJK 1, English 4).
- const charsPerToken = charsPerTokenForLanguage();
- const totalContextCharBudget = totalContextTokenBudget * charsPerToken;
- const outlineCharCap = Math.floor(
- totalContextCharBudget * DEEP_CHAPTER_OUTLINE_MAX_FRAC,
- );
- const outlineText = capOutlineSourcesToBudget(
- contextPack.outline ?? "",
- outlineCharCap,
- );
- // 独立提取大纲,不通过contextPackToPrompt
- const outlinePrompt = outlineText
- ? [
- "# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
- "# 【强制遵守】作品完整大纲",
- "# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
- "",
- "**重要:以下是本作品的完整大纲,这是强制性要求。**",
- "你必须严格遵守大纲中的情节发展、角色行为、关键事件、故事走向。",
- "大纲内容必须完整体现在生成的章节中,不可偏离。",
- "",
- outlineText,
- "",
- "# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
- "",
- ].join("\n")
- : "";
- // 其余上下文的预算 = 总预算 − 大纲已占用(换算成 token),并保留下限。
- // 这样「大纲 + 其余上下文」整体不超过窗口派生的总预算。
- const restContextTokenBudget = Math.max(
- DEEP_CHAPTER_REST_TOKEN_FLOOR,
- totalContextTokenBudget -
- Math.ceil(outlineText.length / charsPerToken),
- );
- const contextPrompt = [
- previousChaptersAnalysis
- ? `## 前情分析\n\n${previousChaptersAnalysis}`
- : "",
- deps.contextPackToPrompt(contextPack, restContextTokenBudget, {
- excludeOutline: true,
- }),
- input.dismantlingReferenceDirective,
- ]
- .filter(Boolean)
- .join("\n\n");
- // 稳定上下文前缀:与任务书/初稿/扩写/返修/去AI味各阶段提示词开头逐字节一致。
- // 作为显式 prompt 缓存断点传入(Anthropic/MiniMax 走 cache_control;
- // OpenAI/DeepSeek 该断点被折叠回字符串、由其自动前缀缓存命中)。
- const cachePrefix = buildStableContextPrefix(outlinePrompt, contextPrompt);
- if (!resumeCheckpoint) {
- callbacks.onThinking?.(formatContextThinking(input, contextPack));
- callbacks.onCheckpoint?.(createCheckpoint("after_context"));
- completeChapterWorkflowStep(
- callbacks,
- contextWorkflowStep,
- "上下文读取完成。",
- {
- recentSummaryCount: Array.isArray(contextPack.recentSummaries)
- ? contextPack.recentSummaries.length
- : 0,
- hasOutline: Boolean(contextPack.outline?.trim()),
- hasPreviousChapterEnding: Boolean(
- contextPack.previousChapterEnding?.trim(),
- ),
- },
- );
- }
- assertNotAborted(signal);
- let taskBrief = hasCheckpointTaskBrief(resumeCheckpoint)
- ? resumeCheckpoint.taskBrief.trim()
- : "";
- if (!taskBrief) {
- emitDeepChapterStageStarted(
- callbacks,
- "plot_analysis",
- "分析剧情走向",
- "根据章节生成约束包拆解本章目标、关键情节和写作约束。",
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:plot_analysis:input:${Date.now()}`,
- stageId: "plot_analysis",
- kind: "stage_input",
- title: "接收内容",
- content: "章节生成约束包",
- });
- taskBrief = await runChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_task_brief",
- title: "生成写作任务书",
- detail: "根据上下文拆解本章目标、关键情节和写作约束。",
- params: workflowBaseParams,
- },
- () =>
- collectModelText(
- workflowConfig,
- [
- {
- role: "user",
- content: buildDeepChapterBriefPrompt(
- outlinePrompt,
- contextPrompt,
- input.userRequest,
- input.chapterNumber,
- input.goldenThreeChapter,
- lengthSpec,
- planExecutionSummary,
- executionContractText,
- input.skillsPrompt,
- ),
- },
- ],
- deps,
- signal,
- (partial) =>
- callbacks.onThinking?.(
- formatStageThinking("阶段2:写作任务书", partial),
- ),
- analysisRequestOverrides,
- cachePrefix,
- callbacks.onRequestTrace,
- ),
- (value) => `写作任务书完成,约 ${countChapterChars(value)} 字。`,
- (value) => ({ chars: countChapterChars(value) }),
- );
- assertNotAborted(signal);
- callbacks.onThinking?.(formatStageThinking("阶段2:写作任务书", taskBrief));
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:task_brief:output:${Date.now()}`,
- stageId: "plot_analysis",
- kind: "stage_output",
- title: "写作任务书",
- content: taskBrief,
- });
- callbacks.onCheckpoint?.(
- createCheckpoint("after_task_brief", { taskBrief }),
- );
- }
- let draftContent = hasCheckpointDraft(resumeCheckpoint)
- ? resumeCheckpoint.draftContent.trim()
- : "";
- if (!draftContent) {
- emitDeepChapterStageStarted(
- callbacks,
- "generate_draft",
- "生成章节草稿",
- "读取章节生成约束包和写作任务书,生成正文初稿。",
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:generate_draft:input:${Date.now()}`,
- stageId: "generate_draft",
- kind: "stage_input",
- title: "接收内容",
- content: [
- "章节生成约束包",
- "写作任务书",
- input.chapterNumber
- ? `目标章节:第${input.chapterNumber}章`
- : "目标章节:从用户请求识别",
- ].join("\n"),
- });
- draftContent = await runChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_draft",
- title: "生成章节正文初稿",
- detail: "按任务书输出本章正文初稿。",
- params: workflowBaseParams,
- },
- () =>
- collectModelText(
- writingConfig,
- [
- {
- role: "user",
- content: buildDeepChapterDraftPrompt(
- outlinePrompt,
- contextPrompt,
- taskBrief,
- input.userRequest,
- input.chapterNumber,
- input.goldenThreeChapter,
- lengthSpec,
- input.skillsPrompt,
- ),
- },
- ],
- deps,
- signal,
- (partial) =>
- callbacks.onThinking?.(
- formatStageThinking("阶段3:正文初稿", partial),
- ),
- generationRequestOverrides,
- cachePrefix,
- callbacks.onRequestTrace,
- ),
- (value) => {
- const chars = countChapterChars(value);
- return chars < lengthSpec.minChars
- ? `正文初稿仅约 ${chars} 字,低于最低完成线 ${lengthSpec.minChars} 字,进入扩写补足。`
- : `正文初稿完成,约 ${chars} 字。`;
- },
- (value) => ({ chars: countChapterChars(value) }),
- );
- assertNotAborted(signal);
- if (countChapterChars(draftContent) < lengthSpec.minChars) {
- draftContent = await runChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_expansion",
- title: "正文扩写补足",
- detail: "初稿低于本章目标字数,补足场景和人物行动。",
- params: workflowBaseParams,
- },
- async () => {
- const expanded = await collectModelText(
- writingConfig,
- [
- {
- role: "user",
- content: buildDeepChapterExpansionPrompt(
- outlinePrompt,
- contextPrompt,
- taskBrief,
- draftContent,
- input.userRequest,
- input.chapterNumber,
- input.goldenThreeChapter,
- lengthSpec,
- ),
- },
- ],
- deps,
- signal,
- (partial) =>
- callbacks.onThinking?.(
- formatStageThinking("阶段3:正文扩写补足", partial),
- ),
- generationRequestOverrides,
- cachePrefix,
- callbacks.onRequestTrace,
- );
- const expandedChars = countChapterChars(expanded);
- if (expandedChars < lengthSpec.minChars) {
- throw new Error(
- `章节正文生成失败:扩写后仅约 ${expandedChars} 字,低于最低完成线 ${lengthSpec.minChars} 字。`,
- );
- }
- return expanded;
- },
- (value) => `正文扩写补足完成,约 ${countChapterChars(value)} 字。`,
- (value) => ({ chars: countChapterChars(value) }),
- );
- assertNotAborted(signal);
- }
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段3:正文初稿",
- [
- draftContent,
- "",
- `初稿生成完成,约 ${countChapterChars(draftContent)} 字。`,
- ].join("\n"),
- ),
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:generate_draft:output:${Date.now()}`,
- stageId: "generate_draft",
- kind: "stage_output",
- title: "章节草稿",
- content: `正文初稿完成,约 ${countChapterChars(draftContent)} 字。`,
- });
- callbacks.onCheckpoint?.(
- createCheckpoint("after_draft", { taskBrief, draftContent }),
- );
- }
- if (!workflowProfile.runPostDraftPlanAudits) {
- const finalContent = draftContent;
- const isFastMode = workflowProfile.mode === "fast";
- callbacks.onThinking?.(
- formatStageThinking(
- isFastMode ? "阶段4:快速完成" : "阶段4:标准完成",
- isFastMode
- ? "快速模式已完成任务书与正文初稿生成,直接采用正文初稿作为最终正文。"
- : "标准模式已完成任务书与正文初稿生成,直接采用正文初稿作为最终正文。",
- ),
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:final_output:output:${Date.now()}`,
- stageId: "final_output",
- kind: "final_output",
- title: "最终正文",
- content: `最终正文已生成,约 ${countChapterChars(finalContent)} 字。`,
- });
- callbacks.onFinalContent?.(finalContent);
- completeChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_complete",
- title: workflowProfile.completionTitle,
- detail: "汇总本次章节生成结果。",
- params: workflowBaseParams,
- },
- `${workflowProfile.completionResultPrefix},最终正文约 ${countChapterChars(finalContent)} 字。`,
- {
- chars: countChapterChars(finalContent),
- revised: false,
- },
- );
- return {
- finalContent,
- taskBrief,
- draftContent,
- reviewResults: [],
- revised: false,
- planCompliance: "",
- executionReport: "",
- };
- }
- let reviewResults = hasCheckpointReview(resumeCheckpoint)
- ? resumeCheckpoint.reviewResults
- : [];
- let reviewFailureMessage = "";
- emitDeepChapterStageStarted(
- callbacks,
- "validate_revision",
- "校验与修正",
- "检查正文完整性、剧情连续性、人物一致性和阻断问题。",
- );
- // 审稿由工作流模式决定:严格模式始终审稿,快速/标准模式跳过。
- const shouldRunAiReview = workflowProfile.runAiReview;
- if (!hasCheckpointReview(resumeCheckpoint)) {
- if (!shouldRunAiReview) {
- completeChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_review",
- title: "执行 AI 审稿",
- detail: "根据当前模式决定是否执行 AI 审稿。",
- params: workflowBaseParams,
- },
- workflowProfile.mode === "fast"
- ? "快速模式跳过 AI 审稿、返修和最终去AI味,直接使用阶段3正文初稿。"
- : "标准模式跳过 AI 审稿与自动返修,初稿将进入阶段6简单审查与去AI味。",
- { skipped: true },
- );
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段4-5:已跳过审稿与返修",
- workflowProfile.mode === "fast"
- ? "快速模式跳过 AI 审稿、返修和最终去AI味,直接使用阶段3正文初稿。"
- : "标准模式跳过 AI 审稿与自动返修,初稿将进入阶段6简单审查与去AI味。",
- ),
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:validate_revision:analysis:${Date.now()}`,
- stageId: "validate_revision",
- kind: "analysis",
- title: "审稿分析",
- content:
- workflowProfile.mode === "fast"
- ? "快速模式跳过 AI 审稿、返修和最终去AI味,直接使用阶段3正文初稿。"
- : "标准模式跳过 AI 审稿与自动返修,初稿将进入阶段6简单审查与去AI味。",
- });
- } else {
- const reviewWorkflowStep: ChapterWorkflowStepSpec = {
- name: "chapter_review",
- title: "执行 AI 审稿",
- detail: "检查正文完整性、剧情连续性、截断和阻断问题。",
- params: workflowBaseParams,
- };
- startChapterWorkflowStep(callbacks, reviewWorkflowStep);
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段4:AI审稿",
- "正在检查正文完整性、剧情连续性、是否被截断以及是否存在阻断问题。",
- ),
- );
- try {
- // 复用阶段1已构建的 contextPack,避免审稿内部再 buildContextPack 一次
- // (会重复跑检索 / 向量 / 图谱)。
- reviewResults = signal
- ? await deps.reviewChapter(
- input.projectPath,
- draftContent,
- input.chapterNumber,
- {
- onThinking: callbacks.onThinking,
- contextPack,
- planBlueprint: planExecutionSummary,
- throwOnFailure: true,
- onRequestTrace: callbacks.onRequestTrace,
- },
- signal,
- )
- : await deps.reviewChapter(
- input.projectPath,
- draftContent,
- input.chapterNumber,
- {
- onThinking: callbacks.onThinking,
- contextPack,
- planBlueprint: planExecutionSummary,
- throwOnFailure: true,
- onRequestTrace: callbacks.onRequestTrace,
- },
- );
- } catch (err) {
- rethrowIfUserAbort(err, signal);
- console.error("[Deep Chapter] Review failed:", err);
- reviewFailureMessage = `AI 审稿失败:${getErrorMessage(err)}。已跳过自动返修,请在保存前手动复核正文。`;
- errorChapterWorkflowStep(callbacks, reviewWorkflowStep, err);
- callbacks.onThinking?.(
- formatStageThinking("阶段4:AI审稿", reviewFailureMessage),
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:validate_revision:error:${Date.now()}`,
- stageId: "validate_revision",
- kind: "analysis",
- title: "审稿失败",
- content: reviewFailureMessage,
- });
- reviewResults = [];
- }
- reviewResults = reviewResults || [];
- assertNotAborted(signal);
- if (!reviewFailureMessage) {
- completeChapterWorkflowStep(
- callbacks,
- reviewWorkflowStep,
- reviewResults.length === 0
- ? "AI 审稿完成,未发现阻断问题。"
- : `AI 审稿完成,发现 ${reviewResults.length} 个问题,其中 ${reviewResults.filter((item) => item.severity === "error").length} 个阻断问题。`,
- {
- issueCount: reviewResults.length,
- blockingIssueCount: reviewResults.filter(
- (item) => item.severity === "error",
- ).length,
- },
- );
- callbacks.onThinking?.(formatReviewThinking(reviewResults));
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:validate_revision:analysis:${Date.now()}`,
- stageId: "validate_revision",
- kind: "analysis",
- title: "审稿分析",
- content: formatReviewThinking(reviewResults)
- .replace(/^##\s*阶段4:AI审稿\s*/, "")
- .trim(),
- });
- callbacks.onCheckpoint?.(
- createCheckpoint("after_review", {
- taskBrief,
- draftContent,
- reviewResults,
- }),
- );
- }
- }
- }
- const blockingIssues = reviewResults.filter(
- (item) => item.severity === "error",
- );
- let currentContent = draftContent;
- let revised = false;
- if (hasCheckpointRevision(resumeCheckpoint)) {
- currentContent = resumeCheckpoint.currentContent.trim();
- revised = true;
- } else if (reviewFailureMessage) {
- completeChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_revision",
- title: "自动返修",
- detail: "根据 AI 审稿结果决定是否返修。",
- params: workflowBaseParams,
- },
- "AI 审稿失败,无法安全判断返修项,已跳过自动返修。",
- { skipped: true, reviewFailed: true },
- );
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段5:已跳过自动返修",
- "AI 审稿失败,无法安全判断阻断问题,已跳过自动返修并继续最终简单审查。",
- ),
- );
- } else if (blockingIssues.length === 0) {
- if (shouldRunAiReview) {
- completeChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_revision",
- title: "自动返修",
- detail: "根据 AI 审稿结果决定是否返修。",
- params: workflowBaseParams,
- },
- "AI审稿未发现阻断问题,跳过自动返修。",
- { skipped: true },
- );
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段5:无需自动返修",
- "AI审稿未发现阻断问题,跳过自动返修,进入阶段6简单审查与去AI味。",
- ),
- );
- }
- } else {
- const revisedContent = await runChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_revision",
- title: "自动返修",
- detail: "根据阻断问题自动返修一次。",
- params: {
- ...workflowBaseParams,
- blockingIssueCount: blockingIssues.length,
- },
- },
- () =>
- collectModelText(
- writingConfig,
- [
- {
- role: "user",
- content: buildDeepChapterRevisionPrompt(
- outlinePrompt,
- contextPrompt,
- taskBrief,
- draftContent,
- blockingIssues,
- input.userRequest,
- input.chapterNumber,
- input.goldenThreeChapter,
- input.skillsPrompt,
- ),
- },
- ],
- deps,
- signal,
- (partial) =>
- callbacks.onThinking?.(
- formatStageThinking("阶段5:自动返修", partial),
- ),
- generationRequestOverrides,
- cachePrefix,
- callbacks.onRequestTrace,
- ),
- (value) =>
- `检测到 ${blockingIssues.length} 个阻断问题,已自动返修一次。返修后正文约 ${countChapterChars(value)} 字。`,
- (value) => ({ chars: countChapterChars(value) }),
- );
- assertNotAborted(signal);
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段5:自动返修",
- [
- `检测到 ${blockingIssues.length} 个阻断问题,已自动返修一次。`,
- "",
- formatReviewIssueList(blockingIssues),
- "",
- `返修后正文约 ${countChapterChars(revisedContent)} 字。`,
- ].join("\n"),
- ),
- );
- currentContent = revisedContent;
- revised = true;
- callbacks.onCheckpoint?.(
- createCheckpoint("after_revision", {
- taskBrief,
- draftContent,
- reviewResults,
- currentContent: revisedContent,
- }),
- );
- }
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:validate_revision:output:${Date.now()}`,
- stageId: "validate_revision",
- kind: "stage_output",
- title: "修正结果",
- content: revised
- ? "已根据阻断问题完成一次自动返修。"
- : "未执行自动返修,当前正文进入最终审查。",
- });
- // 阶段5.5:返修后复审(只在发生了返修时执行,只审查角色一致性维度,降低token消耗,不再自动返修避免循环)
- const shouldRunPostRevisionReview = workflowProfile.runPostRevisionReview;
- if (revised && shouldRunPostRevisionReview) {
- const postRevisionWorkflowStep: ChapterWorkflowStepSpec = {
- name: "chapter_post_revision_review",
- title: "返修后复审",
- detail: "对返修后的正文做角色一致性专项复审。",
- params: workflowBaseParams,
- };
- startChapterWorkflowStep(callbacks, postRevisionWorkflowStep);
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段5.5:返修后角色一致性复审",
- "正在对返修后的正文进行角色一致性专项复审(轻量模式,只检查角色相关维度),确认返修是否引入新的角色偏差。",
- ),
- );
- try {
- const postRevisionResults = signal
- ? await deps.reviewChapter(
- input.projectPath,
- currentContent,
- input.chapterNumber,
- {
- onThinking: callbacks.onThinking,
- contextPack,
- characterOnly: true,
- throwOnFailure: true,
- onRequestTrace: callbacks.onRequestTrace,
- },
- signal,
- )
- : await deps.reviewChapter(
- input.projectPath,
- currentContent,
- input.chapterNumber,
- {
- onThinking: callbacks.onThinking,
- contextPack,
- characterOnly: true,
- throwOnFailure: true,
- onRequestTrace: callbacks.onRequestTrace,
- },
- );
- const postBlockingIssues = (postRevisionResults || []).filter(
- (item) => item.severity === "error",
- );
- if (postBlockingIssues.length > 0) {
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段5.5:返修后复审",
- [
- `返修后复审发现 ${postBlockingIssues.length} 个阻断问题(不再自动返修,避免循环):`,
- "",
- formatReviewIssueList(postBlockingIssues),
- "",
- "这些问题将在阶段6去AI味时一并处理,或需要手动修改。",
- ].join("\n"),
- ),
- );
- reviewResults = [...reviewResults, ...(postRevisionResults || [])];
- } else {
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段5.5:返修后复审",
- "返修后复审未发现新的阻断问题,进入阶段6。",
- ),
- );
- }
- completeChapterWorkflowStep(
- callbacks,
- postRevisionWorkflowStep,
- `返修后复审完成,发现 ${postBlockingIssues.length} 个阻断问题。`,
- { blockingIssueCount: postBlockingIssues.length },
- );
- } catch (err) {
- rethrowIfUserAbort(err, signal);
- console.error("[Deep Chapter] 返修后复审失败:", err);
- errorChapterWorkflowStep(callbacks, postRevisionWorkflowStep, err);
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段5.5:返修后复审",
- `返修后复审失败:${getErrorMessage(err)}。已继续进入最终简单审查,请在保存前手动复核正文。`,
- ),
- );
- }
- }
- const finalPolishWorkflowStep: ChapterWorkflowStepSpec = {
- name: "chapter_final_polish",
- title: "简单审查与去AI味",
- detail: "做最后一遍简单审查,减少复读、机械套话和 AI 味。",
- params: workflowBaseParams,
- };
- if (workflowProfile.runFinalPolish) {
- emitDeepChapterStageStarted(
- callbacks,
- "final_polish",
- "去AI味",
- "正在做最后一遍简单审查,去除复读、机械套话和 AI 味。",
- );
- }
- let finalContent = workflowProfile.runFinalPolish
- ? await runChapterWorkflowStep(
- callbacks,
- finalPolishWorkflowStep,
- () =>
- finalPolishChapter(
- deAiConfig,
- outlinePrompt,
- contextPrompt,
- taskBrief,
- currentContent,
- input,
- contextPack,
- callbacks,
- deps,
- signal,
- customDeAiSkill || undefined,
- cachePrefix,
- deAiRequestOverrides,
- ),
- (value) =>
- `简单审查与去AI味完成,最终正文约 ${countChapterChars(value)} 字。`,
- (value) => ({ chars: countChapterChars(value) }),
- )
- : currentContent;
- if (!workflowProfile.runFinalPolish) {
- completeChapterWorkflowStep(
- callbacks,
- finalPolishWorkflowStep,
- "快速模式跳过最终去AI味,直接采用阶段3正文作为最终正文。",
- { skipped: true, chars: countChapterChars(finalContent) },
- );
- } else {
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:final_polish:output:${Date.now()}`,
- stageId: "final_polish",
- kind: "stage_output",
- title: "去AI味",
- content: `简单审查与去AI味完成,最终正文约 ${countChapterChars(finalContent)} 字。`,
- });
- }
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段7:完成",
- workflowProfile.runFinalPolish
- ? reviewFailureMessage
- ? "AI 审稿失败;已保留正文并完成最后一遍简单审查与去AI味,请在保存前手动复核。"
- : revised
- ? "采用返修并完成简单审查、去AI味后的正文作为最终正文。"
- : "未发现阻断问题,已完成最后一遍简单审查与去AI味。"
- : "快速模式已完成任务书与正文初稿生成,直接采用阶段3正文作为最终正文。",
- ),
- );
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:final_output:output:${Date.now()}`,
- stageId: "final_output",
- kind: "final_output",
- title: "最终正文",
- content: `最终正文已生成,约 ${countChapterChars(finalContent)} 字。`,
- });
- callbacks.onFinalContent?.(finalContent);
- let executionReportSummary = "";
- if (workflowProfile.runPostDraftPlanAudits && executionContract) {
- try {
- const runReport = deps.runChapterExecutionReportCheck || runChapterExecutionReportCheck;
- const report = await runChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_execution_report",
- title: "检查执行清单",
- detail: "按场景验收标准逐项检查最终正文。",
- params: workflowBaseParams,
- },
- () => callbacks.onRequestTrace
- ? runReport(workflowConfig, executionContract!, finalContent, signal, callbacks.onRequestTrace)
- : runReport(workflowConfig, executionContract!, finalContent, signal),
- (value) => executionReportToToolSummary(value),
- (value) => ({
- status: value.status,
- repairItemCount: extractExecutionRepairItems(value).length,
- }),
- );
- const repairItems = extractExecutionRepairItems(report);
- executionReportSummary = executionReportToToolSummary(report);
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:execution_report:output:${Date.now()}`,
- stageId: "execution_report",
- kind: "stage_output",
- title: "执行报告",
- content: executionReportSummary,
- });
- if (repairItems.length > 0) {
- const runRepair = deps.runChapterPlanDeviationRepair || runChapterPlanDeviationRepair;
- const repairStep = {
- name: "chapter_execution_repair",
- title: "返修执行失败项",
- detail: "只修复执行报告中的失败项,不重写全章。",
- params: workflowBaseParams,
- };
- startChapterWorkflowStep(callbacks, repairStep);
- let repairedContent = "";
- try {
- repairedContent = callbacks.onRequestTrace
- ? await runRepair(
- writingConfig,
- contractToTaskBriefText(executionContract!),
- finalContent,
- repairItems.join("\n"),
- signal,
- callbacks.onRequestTrace,
- )
- : await runRepair(
- writingConfig,
- contractToTaskBriefText(executionContract!),
- finalContent,
- repairItems.join("\n"),
- signal,
- );
- } catch (error) {
- errorChapterWorkflowStep(callbacks, repairStep, error);
- throw error;
- }
- const validation = validateChapterPlanRepairResult(finalContent, repairedContent);
- if (!validation.accepted) {
- errorChapterWorkflowStep(callbacks, repairStep, new Error(validation.reason));
- } else {
- const repairedCandidate = repairedContent.trim();
- let recheckReport: ChapterExecutionReport;
- try {
- recheckReport = await runChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_execution_recheck",
- title: "复检执行清单",
- detail: "返修后再次检查执行清单失败项是否消除。",
- params: workflowBaseParams,
- },
- () => callbacks.onRequestTrace
- ? runReport(workflowConfig, executionContract!, repairedCandidate, signal, callbacks.onRequestTrace)
- : runReport(workflowConfig, executionContract!, repairedCandidate, signal),
- (value) => executionReportToToolSummary(value),
- (value) => ({
- status: value.status,
- repairItemCount: extractExecutionRepairItems(value).length,
- }),
- );
- } catch (error) {
- errorChapterWorkflowStep(callbacks, repairStep, error);
- throw error;
- }
- const recheckPassed =
- extractExecutionRepairItems(recheckReport).length === 0 &&
- recheckReport.status !== "fail" &&
- recheckReport.status !== "unknown";
- executionReportSummary = executionReportToToolSummary(recheckReport, recheckPassed);
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:execution_recheck:output:${Date.now()}`,
- stageId: "execution_recheck",
- kind: "stage_output",
- title: "执行复检",
- content: executionReportSummary,
- });
- if (recheckPassed) {
- completeChapterWorkflowStep(
- callbacks,
- repairStep,
- `执行失败项返修完成,正文约 ${countChapterChars(repairedCandidate)} 字。`,
- { chars: countChapterChars(repairedCandidate), changed: true },
- );
- finalContent = repairedCandidate;
- revised = true;
- callbacks.onFinalContent?.(finalContent);
- } else {
- errorChapterWorkflowStep(
- callbacks,
- repairStep,
- new Error("返修后执行清单复检仍未通过"),
- );
- }
- }
- }
- } catch (error) {
- rethrowIfUserAbort(error, signal);
- executionReportSummary = `执行报告生成失败,已保留正文:${error instanceof Error ? error.message : String(error)}`;
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:execution_report:error:${Date.now()}`,
- stageId: "execution_report",
- kind: "stage_output",
- title: "执行报告",
- content: executionReportSummary,
- });
- }
- }
- let planCompliance = "";
- if (workflowProfile.runPostDraftPlanAudits && planExecutionSummary.trim() && !executionContract) {
- let complianceCheckFailed = false;
- const complianceStep = {
- name: "chapter_plan_compliance",
- title: "检查计划履约度",
- detail: "对照用户确认的章节计划检查最终正文是否按计划执行。",
- params: workflowBaseParams,
- };
- const runCompliance = deps.runChapterPlanComplianceCheck || runChapterPlanComplianceCheck;
- try {
- planCompliance = await runChapterWorkflowStep(
- callbacks,
- complianceStep,
- () => callbacks.onRequestTrace
- ? runCompliance(workflowConfig, planExecutionSummary, finalContent, signal, callbacks.onRequestTrace)
- : runCompliance(workflowConfig, planExecutionSummary, finalContent, signal),
- (value) => value ? "计划履约度检查完成。" : "计划履约度检查完成,未返回具体结果。",
- (value) => ({ hasComplianceResult: Boolean(value?.trim()) }),
- );
- } catch (error) {
- rethrowIfUserAbort(error, signal);
- complianceCheckFailed = true;
- planCompliance = `计划履约度检查失败:${error instanceof Error ? error.message : String(error)}`;
- }
- if (planCompliance.trim() && !complianceCheckFailed) {
- const parsedCompliance = parseChapterPlanComplianceResult(planCompliance);
- const shouldRepairPlanDeviation = shouldRepairChapterPlanDeviation(parsedCompliance);
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:plan_compliance:output:${Date.now()}`,
- stageId: "plan_compliance",
- kind: "stage_output",
- title: "计划履约度",
- content: formatPlanComplianceActivityContent(parsedCompliance, shouldRepairPlanDeviation),
- });
- if (shouldRepairPlanDeviation) {
- const repairStep = {
- name: "chapter_plan_deviation_repair",
- title: "返修计划偏离点",
- detail: "只修复计划履约检查发现的偏离点,不重写全章。",
- params: workflowBaseParams,
- };
- const runRepair = deps.runChapterPlanDeviationRepair || runChapterPlanDeviationRepair;
- try {
- startChapterWorkflowStep(callbacks, repairStep);
- const complianceBeforeRepair = planCompliance;
- let repairedContent = "";
- try {
- repairedContent = callbacks.onRequestTrace
- ? await runRepair(
- writingConfig,
- planExecutionSummary,
- finalContent,
- complianceBeforeRepair,
- signal,
- callbacks.onRequestTrace,
- )
- : await runRepair(
- writingConfig,
- planExecutionSummary,
- finalContent,
- complianceBeforeRepair,
- signal,
- );
- } catch (error) {
- errorChapterWorkflowStep(callbacks, repairStep, error);
- throw error;
- }
- const validation = validateChapterPlanRepairResult(finalContent, repairedContent);
- const beforeChars = countChapterChars(finalContent);
- if (validation.accepted) {
- const repairedCandidate = repairedContent.trim();
- const recheckStep = {
- name: "chapter_plan_deviation_recheck",
- title: "复检计划履约度",
- detail: "返修后再次检查计划偏离点是否消除。",
- params: workflowBaseParams,
- };
- let recheckResult = "";
- try {
- recheckResult = await runChapterWorkflowStep(
- callbacks,
- recheckStep,
- () => callbacks.onRequestTrace
- ? runCompliance(
- workflowConfig,
- planExecutionSummary,
- repairedCandidate,
- signal,
- callbacks.onRequestTrace,
- )
- : runCompliance(
- workflowConfig,
- planExecutionSummary,
- repairedCandidate,
- signal,
- ),
- (value) => value ? "计划返修复检完成。" : "计划返修复检未返回具体结果。",
- (value) => ({ hasComplianceResult: Boolean(value?.trim()) }),
- );
- } catch (error) {
- errorChapterWorkflowStep(callbacks, repairStep, error);
- throw error;
- }
- const parsedRecheck = parseChapterPlanComplianceResult(recheckResult);
- const recheckPassed =
- parsedRecheck.status === "compliant" ||
- parsedRecheck.status === "mostly_compliant";
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:plan_deviation_recheck:output:${Date.now()}`,
- stageId: "plan_deviation_recheck",
- kind: "stage_output",
- title: "计划履约复检",
- content: formatPlanComplianceActivityContent(
- parsedRecheck,
- shouldRepairChapterPlanDeviation(parsedRecheck),
- ),
- });
- if (recheckPassed) {
- completeChapterWorkflowStep(
- callbacks,
- repairStep,
- `计划偏离点返修完成,正文约 ${countChapterChars(repairedCandidate)} 字。`,
- { chars: countChapterChars(repairedCandidate), changed: true },
- );
- finalContent = repairedCandidate;
- revised = true;
- const afterChars = countChapterChars(finalContent);
- planCompliance = buildPlanDeviationRepairReturnContent(parsedCompliance, beforeChars, afterChars);
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:plan_deviation_repair:output:${Date.now()}`,
- stageId: "plan_deviation_repair",
- kind: "stage_output",
- title: "计划偏离点返修",
- content: [
- "正文已更新:已按计划履约偏离点完成轻量返修。",
- `返修前:约 ${beforeChars} 字。`,
- `返修后:约 ${afterChars} 字。`,
- "处理范围:只修复履约偏离点,未重写全章。",
- ].join("\n"),
- });
- callbacks.onFinalContent?.(finalContent);
- } else {
- const reason = "返修后计划履约复检仍未通过";
- errorChapterWorkflowStep(callbacks, repairStep, new Error(reason));
- planCompliance = `${reason}:${recheckResult || "未返回有效结果"}`;
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:plan_deviation_repair:output:${Date.now()}`,
- stageId: "plan_deviation_repair",
- kind: "stage_output",
- title: "计划偏离点返修",
- content: `${reason},已保留原正文。`,
- });
- }
- } else {
- errorChapterWorkflowStep(callbacks, repairStep, new Error(validation.reason));
- emitDeepChapterActivity(callbacks, {
- id: `deep_chapter:plan_deviation_repair:output:${Date.now()}`,
- stageId: "plan_deviation_repair",
- kind: "stage_output",
- title: "计划偏离点返修",
- content: [
- "返修结果异常,已保留原正文。",
- `原因:${validation.reason}。`,
- `原正文:约 ${beforeChars} 字。`,
- `返修结果:约 ${countChapterChars(repairedContent)} 字。`,
- ].join("\n"),
- });
- }
- } catch (error) {
- rethrowIfUserAbort(error, signal);
- console.error("[Deep Chapter] 计划偏离点返修失败:", error);
- }
- }
- }
- }
- completeChapterWorkflowStep(
- callbacks,
- {
- name: "chapter_complete",
- title: workflowProfile.completionTitle,
- detail: "汇总本次章节生成结果。",
- params: workflowBaseParams,
- },
- `${workflowProfile.completionResultPrefix},最终正文约 ${countChapterChars(finalContent)} 字。`,
- {
- chars: countChapterChars(finalContent),
- revised,
- },
- );
- return {
- finalContent,
- taskBrief,
- draftContent,
- reviewResults,
- revised,
- planCompliance,
- executionReport: executionReportSummary,
- };
- }
- async function finalPolishChapter(
- deAiConfig: LlmConfig,
- outlinePrompt: string,
- contextPrompt: string,
- taskBrief: string,
- currentContent: string,
- input: DeepChapterGenerationInput,
- _contextPack: ContextPack,
- callbacks: DeepChapterGenerationCallbacks,
- deps: DeepChapterGenerationDeps,
- signal?: AbortSignal,
- customDeAiSkill?: string,
- cachePrefix?: string,
- requestOverrides?: RequestOverrides,
- ): Promise<string> {
- assertNotAborted(signal);
- callbacks.onThinking?.(
- formatStageThinking(
- "阶段6:简单审查与去AI味",
- "正在进行最后一遍简单审查,去除复读、机械套话和 AI 味。",
- ),
- );
- const polished = await collectModelText(
- deAiConfig,
- [
- {
- role: "user",
- content: buildDeepChapterFinalPolishPrompt(
- outlinePrompt,
- contextPrompt,
- taskBrief,
- currentContent,
- input.userRequest,
- input.chapterNumber,
- input.goldenThreeChapter,
- customDeAiSkill,
- ),
- },
- ],
- deps,
- signal,
- (partial) =>
- callbacks.onThinking?.(
- formatStageThinking("阶段6:简单审查与去AI味", partial),
- ),
- requestOverrides,
- cachePrefix,
- callbacks.onRequestTrace,
- );
- assertNotAborted(signal);
- return polished.trim() ? polished : currentContent;
- }
- function resolveCurrentChapterLengthSpec(): ChapterLengthSpec {
- const novelConfig = useWikiStore.getState().novelConfig;
- return resolveChapterLengthSpec(novelConfig?.chapterTargetChars);
- }
- function resolveWritingConfig(llmConfig: LlmConfig): LlmConfig {
- // 写作模型已移除,始终使用 AI 会话当前模型。
- // llmConfig 已在 chat-panel.tsx 中通过 effectiveChatLlmConfig 正确解析,
- // 不再通过 resolveNovelModel 重新解析,避免二次解析使用不同 API 端点/密钥
- return llmConfig;
- }
- /**
- * 把以 cachePrefix 开头的 user 字符串消息拆成 [前缀块(cacheControl), 余下块],
- * 让 provider 在稳定上下文前缀上打缓存断点。其余消息原样返回。
- * 注:Anthropic/MiniMax 会据此发出 cache_control;OpenAI/DeepSeek 端纯文本块会被
- * 折叠回与原字符串逐字节一致的内容,不影响其自动前缀缓存。
- */
- function applyCachePrefix(
- messages: ChatMessage[],
- cachePrefix?: string,
- ): ChatMessage[] {
- if (!cachePrefix) return messages;
- return messages.map((message) => {
- if (
- message.role === "user" &&
- typeof message.content === "string" &&
- message.content.startsWith(cachePrefix)
- ) {
- const rest = message.content.slice(cachePrefix.length);
- return {
- role: message.role,
- content: [
- { type: "text" as const, text: cachePrefix, cacheControl: true },
- ...(rest ? [{ type: "text" as const, text: rest }] : []),
- ],
- };
- }
- return message;
- });
- }
- async function collectModelText(
- config: LlmConfig,
- messages: ChatMessage[],
- deps: DeepChapterGenerationDeps,
- signal?: AbortSignal,
- onUpdate?: (content: string) => void,
- requestOverrides?: RequestOverrides,
- cachePrefix?: string,
- onRequestTrace?: StreamCallbacks["onRequestTrace"],
- ): Promise<string> {
- let content = "";
- let reasoningBuffer = "";
- let streamError: Error | null = null;
- let cutoffReason: string | null = null;
- const streamController = new AbortController();
- const combinedSignal = combineAbortSignals(signal, streamController.signal);
- const stopStream = (reason: string) => {
- if (cutoffReason) return;
- cutoffReason = reason;
- streamController.abort();
- };
- assertNotAborted(signal);
- const callbacks: StreamCallbacks = {
- onToken: (token) => {
- if (signal?.aborted) {
- stopStream(USER_ABORT_MESSAGE);
- return;
- }
- content += token;
- const loopStart = findRepeatedTailStart(content);
- if (loopStart !== null) {
- content = content.slice(0, loopStart).trimEnd();
- onUpdate?.(
- `${content}\n\n(已检测到模型重复输出,已自动停止重复内容。)`,
- );
- stopStream("检测到模型重复输出,已自动停止重复内容。");
- return;
- }
- onUpdate?.(content);
- },
- onReasoningToken: (token) => {
- if (signal?.aborted) {
- stopStream(USER_ABORT_MESSAGE);
- return;
- }
- // 推理 token 只用于进度显示,不计入最终 content
- reasoningBuffer += token;
- if (!content) {
- onUpdate?.(reasoningBuffer);
- }
- },
- onDone: () => {},
- onError: (error) => {
- streamError = error;
- },
- onRequestTrace,
- };
- const streamOnce = async (effectiveOverrides?: RequestOverrides) => {
- streamError = null;
- cutoffReason = null;
- await deps.streamChat(
- config,
- applyCachePrefix(messages, cachePrefix),
- callbacks,
- combinedSignal,
- effectiveOverrides,
- );
- };
- await streamOnce(requestOverrides);
- if (
- streamError &&
- isReasoningOnlyResponseError(streamError) &&
- !isReasoningDisabled(config, requestOverrides)
- ) {
- content = "";
- reasoningBuffer = "";
- onUpdate?.("模型只返回思考过程,正在自动切换为非推理模式重试当前阶段。");
- await streamOnce(withReasoningDisabled(requestOverrides));
- }
- if (signal?.aborted) throw new Error(USER_ABORT_MESSAGE);
- if (streamError && !(cutoffReason && isRequestCancelledError(streamError)))
- throw streamError;
- if (cutoffReason) {
- onUpdate?.(`${content.trim()}\n\n(${cutoffReason})`);
- }
- return content.trim();
- }
- function countChapterChars(content: string): number {
- return content.replace(/\s+/g, "").length;
- }
- function formatPlanComplianceActivityContent(
- result: ParsedChapterPlanComplianceResult,
- willRepair: boolean,
- ): string {
- const decision = result.status === "unknown"
- ? "未触发返修"
- : willRepair
- ? "触发轻量返修"
- : "无需返修";
- const lines = [
- `履约状态:${chapterPlanComplianceStatusLabel(result.status)}`,
- `偏离点数量:${result.deviations.length}`,
- `处理决定:${decision}`,
- ];
- if (result.status === "unknown") {
- lines.push("原因:模型未按结构返回,已避免误改正文");
- }
- if (result.summary.trim()) {
- lines.push(`总评:${result.summary.trim()}`);
- }
- if (result.deviations.length > 0) {
- lines.push("");
- lines.push("偏离点:");
- for (const [index, item] of result.deviations.slice(0, 5).entries()) {
- lines.push(`${index + 1}. ${item.point || "未说明偏离点"}`);
- if (item.evidence) lines.push(` 正文证据:${item.evidence}`);
- if (item.suggestion) lines.push(` 建议修正:${item.suggestion}`);
- }
- }
- return lines.join("\n");
- }
- function chapterPlanComplianceStatusLabel(
- status: ParsedChapterPlanComplianceResult["status"],
- ): string {
- if (status === "compliant") return "符合";
- if (status === "mostly_compliant") return "基本符合";
- if (status === "partial_deviation") return "部分偏离";
- if (status === "clear_deviation") return "明显偏离";
- return "未知";
- }
- function buildPlanDeviationRepairReturnContent(
- result: ParsedChapterPlanComplianceResult,
- beforeChars: number,
- afterChars: number,
- ): string {
- const lines = [
- "计划偏离点返修:已完成",
- `返修前履约状态:${chapterPlanComplianceStatusLabel(result.status)}`,
- `已处理偏离点数量:${result.deviations.length}`,
- `返修前:约 ${beforeChars} 字。`,
- `返修后:约 ${afterChars} 字。`,
- "最终正文已按计划偏离点轻量返修结果更新。",
- ];
- const handledPoints = result.deviations
- .map((item) => item.point.trim())
- .filter(Boolean)
- .slice(0, 5);
- if (handledPoints.length > 0) {
- lines.push("");
- lines.push("已处理偏离点:");
- handledPoints.forEach((point, index) => {
- lines.push(`${index + 1}. ${point}`);
- });
- }
- return lines.join("\n");
- }
- function validateChapterPlanRepairResult(
- originalContent: string,
- repairedContent: string,
- ): { accepted: boolean; reason: string } {
- const repaired = repairedContent.trim();
- if (!repaired) {
- return { accepted: false, reason: "返修未返回有效正文" };
- }
- if (
- normalizeContentForAnchorCheck(originalContent) ===
- normalizeContentForAnchorCheck(repaired)
- ) {
- return { accepted: false, reason: "返修正文没有发生有效变化" };
- }
- const originalChars = countChapterChars(originalContent);
- const repairedChars = countChapterChars(repaired);
- if (originalChars >= 1000 && repairedChars < originalChars * 0.7) {
- return { accepted: false, reason: "返修后正文明显变短" };
- }
- if (originalChars >= 1000 && repairedChars > originalChars * 1.5) {
- return { accepted: false, reason: "返修后正文明显变长" };
- }
- if (originalChars >= 1000 && !hasEnoughOriginalContentAnchors(originalContent, repaired)) {
- return { accepted: false, reason: "返修后未保留原正文主要内容" };
- }
- return { accepted: true, reason: "" };
- }
- function hasEnoughOriginalContentAnchors(originalContent: string, repairedContent: string): boolean {
- const original = normalizeContentForAnchorCheck(originalContent);
- const repaired = normalizeContentForAnchorCheck(repairedContent);
- if (original.length < 240 || repaired.length < 240) return true;
- const anchors: string[] = [];
- const windowSize = 14;
- const step = Math.max(80, Math.floor(original.length / 20));
- for (let index = 0; index + windowSize <= original.length; index += step) {
- anchors.push(original.slice(index, index + windowSize));
- if (anchors.length >= 20) break;
- }
- if (anchors.length === 0) return true;
- const preservedCount = anchors.filter((anchor) => repaired.includes(anchor)).length;
- return preservedCount >= Math.max(2, Math.ceil(anchors.length * 0.15));
- }
- function normalizeContentForAnchorCheck(content: string): string {
- return content.replace(/[\s\p{P}\p{S}]+/gu, "");
- }
- function assertNotAborted(signal?: AbortSignal): void {
- throwIfAborted(signal);
- }
- function isRequestCancelledError(error: Error): boolean {
- return /request cancelled|request canceled|aborted|aborterror/i.test(
- error.message,
- );
- }
- function getErrorMessage(error: unknown): string {
- return error instanceof Error ? error.message : String(error);
- }
- function combineAbortSignals(
- ...signals: Array<AbortSignal | undefined>
- ): AbortSignal | undefined {
- const activeSignals = signals.filter(Boolean) as AbortSignal[];
- if (activeSignals.length === 0) return undefined;
- if (activeSignals.length === 1) return activeSignals[0];
- const controller = new AbortController();
- const abort = () => controller.abort();
- for (const signal of activeSignals) {
- if (signal.aborted) {
- controller.abort();
- break;
- }
- signal.addEventListener("abort", abort, { once: true });
- }
- return controller.signal;
- }
- function findRepeatedTailStart(content: string): number | null {
- const normalized = content.replace(/\r\n/g, "\n");
- const compact = normalized.replace(/\s+/g, "");
- if (compact.length < REPEAT_CHECK_MIN_CHARS) return null;
- const tail = compact.slice(-REPEAT_WINDOW_CHARS);
- const first = compact.indexOf(tail);
- if (first === -1 || first >= compact.length - REPEAT_WINDOW_CHARS)
- return null;
- let hits = 0;
- let searchIndex = 0;
- while (true) {
- const found = compact.indexOf(tail, searchIndex);
- if (found === -1) break;
- hits += 1;
- if (hits >= REPEAT_HIT_LIMIT) {
- return sourceIndexFromCompactIndex(
- normalized,
- first + REPEAT_WINDOW_CHARS,
- );
- }
- searchIndex = found + Math.max(1, tail.length);
- }
- return null;
- }
- function sourceIndexFromCompactIndex(
- content: string,
- compactIndex: number,
- ): number {
- let seen = 0;
- for (let index = 0; index < content.length; index += 1) {
- if (/\s/.test(content[index])) continue;
- seen += 1;
- if (seen >= compactIndex) return index + 1;
- }
- return content.length;
- }
- function formatContextThinking(
- input: DeepChapterGenerationInput,
- pack: ContextPack,
- ): string {
- const recentSummaries = Array.isArray(pack.recentSummaries)
- ? pack.recentSummaries
- : [];
- const goldenThreeHints = resolveGoldenThreeThinkingHints(
- input.goldenThreeChapter,
- );
- return formatStageThinking(
- "阶段1:上下文分析",
- [
- ...goldenThreeHints,
- input.chapterNumber
- ? `目标章节:第${input.chapterNumber}章`
- : "目标章节:从用户请求中识别",
- `章节目标:${fallback(pack.chapterGoal, "未读取到明确章节目标")}`,
- `上一章结尾:${fallback(pack.previousChapterEnding, "未读取到上一章结尾")}`,
- `近期剧情:${recentSummaries.length} 条`,
- `人物状态:${summaryText(pack.characterStates)}`,
- `伏笔状态:${summaryText(pack.foreshadowingStates)}`,
- `时间线:${summaryText(pack.timeline)}`,
- `禁止违背:${fallback(pack.mustAvoid, "暂无明确禁止项")}`,
- `必须完成:${fallback(pack.mustDo, "暂无明确必做项")}`,
- ].join("\n"),
- );
- }
- function formatContextExtractionResult(
- input: DeepChapterGenerationInput,
- pack: ContextPack,
- ): string {
- const recentSummaries = Array.isArray(pack.recentSummaries)
- ? pack.recentSummaries
- : [];
- return [
- input.chapterNumber
- ? `目标章节:第${input.chapterNumber}章`
- : "目标章节:从用户请求识别",
- `章节目标:${fallback(pack.chapterGoal, "未读取到明确章节目标")}`,
- `上一章结尾:${fallback(pack.previousChapterEnding, "未读取到上一章结尾")}`,
- `近期剧情:${recentSummaries.length} 条`,
- `人物状态:${summaryText(pack.characterStates)}`,
- `伏笔状态:${summaryText(pack.foreshadowingStates)}`,
- `时间线:${summaryText(pack.timeline)}`,
- `必须完成:${fallback(pack.mustDo, "暂无明确必做项")}`,
- `禁止违背:${fallback(pack.mustAvoid, "暂无明确禁止项")}`,
- ].join("\n");
- }
- function formatReviewThinking(reviewResults: NovelReviewResult[]): string {
- if (reviewResults.length === 0) {
- return formatStageThinking("阶段4:AI审稿", "未发现阻断问题。");
- }
- const characterIssues = reviewResults.filter(
- (item) => item.type === "character_consistency",
- );
- const otherIssues = reviewResults.filter(
- (item) => item.type !== "character_consistency",
- );
- const errorCount = reviewResults.filter(
- (item) => item.severity === "error",
- ).length;
- const sections: string[] = [
- `发现 ${reviewResults.length} 个问题,其中阻断问题 ${errorCount} 个。`,
- ];
- // 角色命中记忆库报告(单独展示 character_consistency 类型的问题)
- if (characterIssues.length > 0) {
- sections.push("");
- sections.push("【角色命中记忆库报告】");
- sections.push(formatReviewIssueList(characterIssues));
- }
- // 其他问题
- if (otherIssues.length > 0) {
- sections.push("");
- sections.push("【其他审查问题】");
- sections.push(formatReviewIssueList(otherIssues));
- }
- return formatStageThinking("阶段4:AI审稿", sections.join("\n"));
- }
- function formatStageThinking(title: string, content: string): string {
- return `## ${title}\n${content.trim()}`;
- }
- function formatReviewIssueList(reviewResults: NovelReviewResult[]): string {
- return reviewResults
- .map((item, index) =>
- [
- `${index + 1}. [${severityLabel(item.severity)}] ${item.message}`,
- item.evidence ? ` - 证据:${item.evidence}` : "",
- item.relatedMemory ? ` - 相关记忆:${item.relatedMemory}` : "",
- item.suggestion ? ` - 建议:${item.suggestion}` : "",
- ]
- .filter(Boolean)
- .join("\n"),
- )
- .join("\n");
- }
- function fallback(
- value: string | null | undefined,
- fallbackText: string,
- ): string {
- const trimmed = typeof value === "string" ? value.trim() : "";
- return trimmed ? trimForThinking(trimmed, 180) : fallbackText;
- }
- function summaryText(value: string | null | undefined): string {
- const trimmed = typeof value === "string" ? value.trim() : "";
- return trimmed ? trimForThinking(trimmed, 140) : "暂无";
- }
- function trimForThinking(value: string, maxLength: number): string {
- const normalized = value.replace(/\s+/g, " ").trim();
- if (normalized.length <= maxLength) return normalized;
- return `${normalized.slice(0, maxLength)}...`;
- }
- function severityLabel(severity: NovelReviewResult["severity"]): string {
- if (severity === "error") return "严重";
- if (severity === "warning") return "提醒";
- return "信息";
- }
- function resolveGoldenThreeThinkingHints(
- goldenThreeChapter?: GoldenThreeChapterRequest,
- ): string[] {
- if (!goldenThreeChapter?.enabled || !goldenThreeChapter.targetChapter)
- return [];
- if (goldenThreeChapter.outputMode === "first_chapter_with_directions") {
- return [
- "黄金三章:已启用",
- "执行策略:当前按黄金三章规则生成第1章正文,并在正文后给出第2章、第3章写作方向。",
- ];
- }
- return [
- "黄金三章:已启用",
- `执行策略:当前按黄金三章规则生成第${goldenThreeChapter.targetChapter}章正文。`,
- ];
- }
- async function maybeInjectWritingEntityWebSearch(args: {
- input: DeepChapterGenerationInput;
- deps: DeepChapterGenerationDeps;
- contextPack: ContextPack;
- previousChaptersAnalysis: string;
- planBlueprint?: string;
- workflowConfig: LlmConfig;
- callbacks: DeepChapterGenerationCallbacks;
- signal?: AbortSignal;
- }): Promise<ContextPack> {
- const collect = args.deps.collectWritingEntityWebSearch ?? collectWritingEntityWebSearch;
- try {
- args.callbacks.onThinking?.(
- formatStageThinking("联网搜索", "正在核对本库实体,必要时联网补搜..."),
- );
- const result = await collect({
- projectPath: args.input.projectPath,
- userRequest: args.input.userRequest,
- outline: args.contextPack.outline,
- planBlueprint: args.planBlueprint,
- contextPack: args.contextPack,
- chapterNumber: args.input.chapterNumber,
- previousChaptersAnalysis: args.previousChaptersAnalysis,
- streamChat: args.deps.streamChat,
- llmConfig: args.workflowConfig,
- searchApiConfig: useWikiStore.getState().searchApiConfig,
- signal: args.signal,
- onRequestTrace: args.callbacks.onRequestTrace,
- });
- if (result.searchedNames.length > 0 || result.markdown.trim()) {
- emitDeepChapterActivity(args.callbacks, {
- id: `deep_chapter:entity_web_search:${Date.now()}`,
- stageId: "read_context",
- kind: "web_search",
- title: "联网搜索",
- content: [
- result.searchedNames.length > 0
- ? `已搜索:${result.searchedNames.join("、")}`
- : "未发起联网搜索",
- ...result.notes,
- ].filter(Boolean).join("\n"),
- });
- }
- if (!result.markdown.trim()) return args.contextPack;
- return {
- ...args.contextPack,
- searchResults: [args.contextPack.searchResults?.trim(), result.markdown.trim()]
- .filter(Boolean)
- .join("\n\n"),
- };
- } catch (error) {
- rethrowIfUserAbort(error, args.signal);
- console.error("[deep-chapter-generation] 实体联网补搜失败:", error);
- return args.contextPack;
- }
- }
- async function safeBuildChapterContextPack(
- deps: DeepChapterGenerationDeps,
- projectPath: string,
- userRequest: string,
- chapterNumber?: number,
- signal?: AbortSignal,
- ): Promise<ContextPack> {
- try {
- throwIfAborted(signal);
- return await waitForAbortable(
- deps.buildContextPack(projectPath, userRequest, chapterNumber),
- signal,
- );
- } catch (error) {
- rethrowIfUserAbort(error, signal);
- return {
- task: userRequest,
- chapterGoal: "",
- outline: "",
- recentSummaries: [],
- previousChapterEnding: "",
- characterStates: "",
- soulDoc: "",
- characterAuras: "",
- storyFrameworkBinding: "",
- cognitionStates: "",
- foreshadowingStates: "",
- timeline: "",
- relatedSettings: "",
- canonRules: "",
- writingStyle: "",
- searchResults: "",
- graphSearchResults: "",
- mustDo: "",
- mustAvoid: "",
- nextChapterAdvice: "",
- revisionDirectives: "",
- };
- }
- }
- function waitForAbortable<T>(operation: Promise<T>, signal?: AbortSignal): Promise<T> {
- if (!signal) return operation;
- throwIfAborted(signal);
- return new Promise<T>((resolve, reject) => {
- const handleAbort = () => reject(new Error(USER_ABORT_MESSAGE));
- signal.addEventListener("abort", handleAbort, { once: true });
- operation.then(
- (value) => {
- signal.removeEventListener("abort", handleAbort);
- resolve(value);
- },
- (error) => {
- signal.removeEventListener("abort", handleAbort);
- reject(error);
- },
- );
- });
- }
|