Prechádzať zdrojové kódy

feat: 优化章节计划执行链路

Mochocyang 2 mesiacov pred
rodič
commit
9deb7bf5fb

+ 1 - 1
src-tauri/Cargo.lock

@@ -5728,7 +5728,7 @@ checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
 
 [[package]]
 name = "qmai"
-version = "2.2.32"
+version = "2.2.33"
 dependencies = [
  "arrow-array",
  "arrow-schema",

+ 1 - 1
src/App.tsx

@@ -4,7 +4,7 @@ import { useWikiStore } from "@/stores/wiki-store"
 import { useReviewStore } from "@/stores/review-store"
 import { isTauri, pickDirectory } from "@/lib/platform"
 import { useChatStore } from "@/stores/chat-store"
-import { listDirectory, openProject, fileExists } from "@/commands/fs"
+import { openProject, fileExists } from "@/commands/fs"
 import { getLastProject, saveLastProject, loadLlmConfig, loadAiChatModel, loadDefaultLlmModel, loadLanguage, loadEmbeddingConfig, loadProviderConfigs, loadActivePresetId, loadProxyConfig, loadScheduledImportConfig, saveScheduledImportConfig, loadSourceWatchConfig, loadNovelMode, loadNovelConfig, loadRevisionFeedbackWindowConfig, loadTheme, loadMaxHistoryMessages, loadUiFontFamily, loadVisualStyle, saveLlmConfig, loadLastReadChapter, loadMcpConfig } from "@/lib/project-store"
 import { loadReviewItems, loadChatHistory, saveChatHistory, saveReviewItems } from "@/lib/persist"
 import { setupAutoSave, teardownAutoSave } from "@/lib/auto-save"

+ 91 - 0
src/components/chat/chapter-plan-confirm-dialog.spec.tsx

@@ -5,6 +5,7 @@ import { createRoot, type Root } from "react-dom/client"
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
 import {
   extractChapterPlan,
+  buildChapterPlanSelfCheckPrompt,
   buildPlanConfirmMessage,
   buildPlanSkipMessage,
   CHAPTER_PLAN_MARKER_START,
@@ -52,6 +53,8 @@ describe("chapter-plan-confirm-dialog 纯函数", () => {
       const msg = buildPlanConfirmMessage("我的计划")
       expect(msg).toContain("已确认")
       expect(msg).toContain("我的计划")
+      expect(msg).toContain("已确认的章节计划")
+      expect(msg).not.toContain("蓝图")
     })
 
     it("指示不要再次输出计划", () => {
@@ -67,6 +70,18 @@ describe("chapter-plan-confirm-dialog 纯函数", () => {
       expect(msg).toContain("不要输出计划")
     })
   })
+
+  describe("buildChapterPlanSelfCheckPrompt", () => {
+    it("要求检查计划完整性并包含计划原文", () => {
+      const prompt = buildChapterPlanSelfCheckPrompt("维度四·场景序列编排:旧屋揭示")
+
+      expect(prompt).toContain("计划自检")
+      expect(prompt).not.toContain("蓝图")
+      expect(prompt).toContain("七个维度")
+      expect(prompt).toContain("维度四·场景序列编排:旧屋揭示")
+      expect(prompt).toContain("只输出一个 JSON 对象")
+    })
+  })
 })
 
 describe("ChapterPlanConfirmDialog 组件", () => {
@@ -181,6 +196,82 @@ describe("ChapterPlanConfirmDialog 组件", () => {
     expect(queryByText("修改计划")).toBeNull()
   })
 
+  it("提供 onSelfCheck 时显示自检按钮", async () => {
+    await act(async () => {
+      root.render(<ChapterPlanConfirmDialog {...baseProps} onSelfCheck={async () => "自检通过"} />)
+    })
+    expect(queryByText("自检计划")).not.toBeNull()
+  })
+
+  it("点击自检按钮后显示自检结果", async () => {
+    const onSelfCheck = vi.fn(async () => "自检结果:场景序列完整,但缺少字数预算。")
+    await act(async () => {
+      root.render(<ChapterPlanConfirmDialog {...baseProps} onSelfCheck={onSelfCheck} />)
+    })
+    const btn = queryByText("自检计划") as HTMLButtonElement
+    await act(async () => {
+      btn.click()
+    })
+
+    expect(onSelfCheck).toHaveBeenCalledOnce()
+    expect(host.textContent).toContain("自检结果:场景序列完整,但缺少字数预算。")
+  })
+
+  it("自检后可按建议修正计划并进入编辑状态", async () => {
+    const onSelfCheck = vi.fn(async () => "状态:warning\n建议:补充篇幅分配")
+    const onRevisePlan = vi.fn(async () => "修订后计划:已补充篇幅分配")
+    await act(async () => {
+      root.render(
+        <ChapterPlanConfirmDialog
+          {...baseProps}
+          onSelfCheck={onSelfCheck}
+          onRevisePlan={onRevisePlan}
+        />,
+      )
+    })
+    const selfCheckBtn = queryByText("自检计划") as HTMLButtonElement
+    await act(async () => {
+      selfCheckBtn.click()
+    })
+    const reviseBtn = queryByText("按自检建议修正") as HTMLButtonElement
+    await act(async () => {
+      reviseBtn.click()
+    })
+
+    expect(onRevisePlan).toHaveBeenCalledWith(
+      baseProps.planContent,
+      "状态:warning\n建议:补充篇幅分配",
+    )
+    const textarea = host.querySelector("textarea") as HTMLTextAreaElement
+    expect(textarea).not.toBeNull()
+    expect(textarea.value).toBe("修订后计划:已补充篇幅分配")
+  })
+
+  it("自检期间关闭弹窗后不会把旧结果带到下次打开", async () => {
+    let resolveSelfCheck!: (value: string) => void
+    const onSelfCheck = vi.fn(() => new Promise<string>((resolve) => {
+      resolveSelfCheck = resolve
+    }))
+    await act(async () => {
+      root.render(<ChapterPlanConfirmDialog {...baseProps} onSelfCheck={onSelfCheck} />)
+    })
+    const btn = queryByText("自检计划") as HTMLButtonElement
+    await act(async () => {
+      btn.click()
+    })
+    await act(async () => {
+      root.render(<ChapterPlanConfirmDialog {...baseProps} open={false} onSelfCheck={onSelfCheck} />)
+    })
+    await act(async () => {
+      resolveSelfCheck("旧的自检结果")
+    })
+    await act(async () => {
+      root.render(<ChapterPlanConfirmDialog {...baseProps} open={true} onSelfCheck={onSelfCheck} />)
+    })
+
+    expect(host.textContent).not.toContain("旧的自检结果")
+  })
+
   it("严格模式显示严格模式标识", async () => {
     await act(async () => {
       root.render(<ChapterPlanConfirmDialog {...baseProps} aiWorkflowMode="strict" />)

+ 95 - 2
src/components/chat/chapter-plan-confirm-dialog.tsx

@@ -1,5 +1,6 @@
-import { useState, useCallback } from "react"
+import { useState, useCallback, useEffect, useRef } from "react"
 import { X, Check, SkipForward, Edit3, ListChecks } from "lucide-react"
+export { buildChapterPlanSelfCheckPrompt } from "@/lib/novel/chapter-plan-self-check"
 
 export const CHAPTER_PLAN_MARKER_START = "<!-- chapter_plan -->"
 export const CHAPTER_PLAN_MARKER_END = "<!-- /chapter_plan -->"
@@ -20,7 +21,14 @@ export function extractChapterPlan(fullContent: string): { plan: string; body: s
 }
 
 export function buildPlanConfirmMessage(plan: string): string {
-  return `${CHAPTER_PLAN_CONFIRMED_PREFIX},现在进入执行阶段。请按以下计划写正文。不要再次输出计划,不要再次等待确认,直接输出正文。\n\n=== 已确认的章节计划 ===\n${plan}`
+  return [
+    `${CHAPTER_PLAN_CONFIRMED_PREFIX},现在进入执行阶段。`,
+    "请调用 run_chapter_workflow 工具生成正文,并把下面这份已确认的章节计划原文作为 planBlueprint 参数完整传入。",
+    "不要再次输出计划,不要再次等待确认,不要把计划改写或省略,直接进入正文生成。",
+    "",
+    "=== 已确认的章节计划 ===",
+    plan,
+  ].join("\n")
 }
 
 export function buildPlanSkipMessage(): string {
@@ -40,6 +48,8 @@ interface ChapterPlanConfirmDialogProps {
   onConfirm: () => void
   onSkip: () => void
   onModify?: (modifiedPlan: string) => void
+  onSelfCheck?: (planContent: string) => Promise<string> | string
+  onRevisePlan?: (planContent: string, selfCheckResult: string) => Promise<string> | string
   onCancel: () => void
 }
 
@@ -50,10 +60,27 @@ export function ChapterPlanConfirmDialog({
   onConfirm,
   onSkip,
   onModify,
+  onSelfCheck,
+  onRevisePlan,
   onCancel,
 }: ChapterPlanConfirmDialogProps) {
   const [editing, setEditing] = useState(false)
   const [editedContent, setEditedContent] = useState(planContent)
+  const [selfChecking, setSelfChecking] = useState(false)
+  const [selfCheckResult, setSelfCheckResult] = useState("")
+  const [selfCheckError, setSelfCheckError] = useState("")
+  const [revisingPlan, setRevisingPlan] = useState(false)
+  const [reviseError, setReviseError] = useState("")
+  const selfCheckRequestRef = useRef(0)
+
+  useEffect(() => {
+    selfCheckRequestRef.current += 1
+    setSelfChecking(false)
+    setSelfCheckResult("")
+    setSelfCheckError("")
+    setRevisingPlan(false)
+    setReviseError("")
+  }, [open, planContent])
 
   const handleStartEdit = useCallback(() => {
     setEditedContent(planContent)
@@ -66,6 +93,44 @@ export function ChapterPlanConfirmDialog({
     setEditing(false)
   }, [editedContent, onModify])
 
+  const handleSelfCheck = useCallback(async () => {
+    if (!onSelfCheck) return
+    const requestId = selfCheckRequestRef.current + 1
+    selfCheckRequestRef.current = requestId
+    setSelfChecking(true)
+    setSelfCheckResult("")
+    setSelfCheckError("")
+    try {
+      const targetPlan = editing ? editedContent : planContent
+      const result = await onSelfCheck(targetPlan)
+      if (selfCheckRequestRef.current !== requestId) return
+      setSelfCheckResult(result.trim() || "自检完成,未返回具体结果。")
+    } catch (error) {
+      if (selfCheckRequestRef.current !== requestId) return
+      setSelfCheckError(error instanceof Error ? error.message : String(error))
+    } finally {
+      if (selfCheckRequestRef.current === requestId) {
+        setSelfChecking(false)
+      }
+    }
+  }, [editedContent, editing, onSelfCheck, planContent])
+
+  const handleRevisePlan = useCallback(async () => {
+    if (!onRevisePlan || !selfCheckResult.trim()) return
+    setRevisingPlan(true)
+    setReviseError("")
+    try {
+      const targetPlan = editing ? editedContent : planContent
+      const revised = await onRevisePlan(targetPlan, selfCheckResult)
+      setEditedContent(revised.trim() || targetPlan)
+      setEditing(true)
+    } catch (error) {
+      setReviseError(error instanceof Error ? error.message : String(error))
+    } finally {
+      setRevisingPlan(false)
+    }
+  }, [editedContent, editing, onRevisePlan, planContent, selfCheckResult])
+
   if (!open) return null
 
   return (
@@ -98,6 +163,24 @@ export function ChapterPlanConfirmDialog({
               {planContent}
             </div>
           )}
+          {(selfChecking || selfCheckResult || selfCheckError) && (
+            <div className="mt-3 rounded-md border bg-background p-3 text-sm">
+              <div className="mb-2 font-medium">计划自检结果</div>
+              {selfChecking && <div className="text-muted-foreground">正在自检计划...</div>}
+              {selfCheckResult && <div className="whitespace-pre-wrap leading-relaxed">{selfCheckResult}</div>}
+              {selfCheckError && <div className="text-destructive">自检失败:{selfCheckError}</div>}
+              {reviseError && <div className="mt-2 text-destructive">修订失败:{reviseError}</div>}
+              {onRevisePlan && selfCheckResult && (
+                <button
+                  onClick={handleRevisePlan}
+                  disabled={revisingPlan}
+                  className="mt-3 rounded-md border px-3 py-1.5 text-sm hover:bg-accent disabled:cursor-not-allowed disabled:opacity-60"
+                >
+                  {revisingPlan ? "修订中..." : "按自检建议修正"}
+                </button>
+              )}
+            </div>
+          )}
         </div>
 
         <div className="flex items-center justify-between border-t px-4 py-3">
@@ -127,6 +210,16 @@ export function ChapterPlanConfirmDialog({
                 </button>
               )
             )}
+            {onSelfCheck && (
+              <button
+                onClick={handleSelfCheck}
+                disabled={selfChecking}
+                className="flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-sm hover:bg-accent disabled:cursor-not-allowed disabled:opacity-60"
+              >
+                <ListChecks className="h-4 w-4" />
+                {selfChecking ? "自检中..." : "自检计划"}
+              </button>
+            )}
           </div>
           <div className="flex gap-2">
             <button

+ 0 - 1
src/components/chat/chat-message.tsx

@@ -49,7 +49,6 @@ import { getHtmlLang, getTextDirection } from "@/lib/language-metadata";
 import { MermaidDiagram, unwrapMermaidPre } from "@/components/mermaid-diagram";
 import { canContinueUnfinishedDeepChapter } from "./chat-resume";
 import { getCopyableAssistantContent } from "@/lib/chat-copy-content";
-import { parseAgentResponse } from "@/lib/novel/agent-parser";
 
 interface ChatMessageProps {
   message: DisplayMessage;

+ 10 - 0
src/components/chat/chat-panel.spec.tsx

@@ -249,6 +249,16 @@ describe("chat-panel agent reference integration", () => {
     expect(source).not.toContain("await streamChat(")
   })
 
+  it("clears confirmed chapter blueprint with finally after followup send", () => {
+    const sendIndex = source.indexOf('handleSendRef.current(followupText, [], "执行已确认计划")')
+    const clearIndex = source.indexOf("confirmedBlueprintRef.current = null", sendIndex)
+    const finallyIndex = source.lastIndexOf("finally", clearIndex)
+
+    expect(sendIndex).toBeGreaterThan(-1)
+    expect(clearIndex).toBeGreaterThan(sendIndex)
+    expect(finallyIndex).toBeGreaterThan(sendIndex)
+  })
+
   it("routes continue-unfinished through the ReAct send path with compact display text", () => {
     const continueIndex = source.indexOf("const handleContinueUnfinished")
 

+ 58 - 5
src/components/chat/chat-panel.tsx

@@ -34,6 +34,11 @@ import {
 } from "@/lib/reference/providers"
 import type { ReferenceToken } from "@/lib/reference/types"
 import { runAiChatSession } from "@/lib/agent/ai-chat-session"
+import {
+  runChapterPlanRevision as runChapterPlanRevisionModel,
+  runChapterPlanSelfCheck as runChapterPlanSelfCheckModel,
+  type ChapterPlanSelfCheckContext,
+} from "@/lib/novel/chapter-plan-self-check"
 import type { AgentMessage, AgentRunRecord } from "@/lib/agent/types"
 import type { AgentToolEvent } from "@/lib/agent/types"
 import type { UserSkill } from "@/lib/novel/skill-library"
@@ -58,9 +63,6 @@ import { loadEffectiveDeAiSkillSafely, resolveAvailableDeAiSkills } from "@/lib/
 import { cleanGeneratedChapterContentWithTitle } from "@/lib/novel/chapter-content-cleanup"
 import { normalizePath } from "@/lib/path-utils"
 import { refreshProjectState } from "@/lib/project-refresh"
-import { getOutputLanguage, buildLanguageReminder } from "@/lib/output-language"
-import { isGreeting } from "@/lib/greeting-detector"
-import { computeContextBudget, computeNovelContextTokenBudget } from "@/lib/context-budget"
 import { getConversationTabTitle, sortConversationsByUpdatedAt } from "@/lib/workspace-layout"
 import { saveAiChatModel } from "@/lib/project-store"
 import {
@@ -186,6 +188,19 @@ function appendWebSearchTrace(trace: ContextTrace, event: AgentToolEvent): Conte
   }
 }
 
+function buildChapterPlanSelfCheckContext(pack: ContextPack | null): ChapterPlanSelfCheckContext | undefined {
+  if (!pack) return undefined
+  return {
+    chapterGoal: pack.chapterGoal,
+    characterStates: pack.characterStates,
+    cognitionStates: pack.cognitionStates,
+    foreshadowingStates: pack.foreshadowingStates,
+    timeline: pack.timeline,
+    canonRules: pack.canonRules,
+    mustAvoid: pack.mustAvoid,
+  }
+}
+
 function buildChatAgentSystemPrompt(options: {
   novelMode: boolean
   mode: "chat" | "ingest"
@@ -752,6 +767,10 @@ export function ChatPanel() {
       project?.name,
     ],
   )
+  // 存储用户最近确认的章节计划,供 run_chapter_workflow 兜底注入,不依赖模型是否自觉传参。
+  const confirmedBlueprintRef = useRef<string | null>(null)
+  const getConfirmedBlueprint = useCallback(() => confirmedBlueprintRef.current ?? undefined, [])
+  const chapterPlanContextRef = useRef<ContextPack | null>(null)
   const {
     config: agentConfig,
     registry: agentRegistry,
@@ -760,7 +779,28 @@ export function ChatPanel() {
     skillConfig: agentSkillConfig,
     writingSkills: agentUserWritingSkills,
     mcpCapabilities: agentMcpCapabilities,
-  } = useAgentConfig(agentSystemPrompt)
+  } = useAgentConfig(agentSystemPrompt, getConfirmedBlueprint)
+  const runChapterPlanSelfCheck = useCallback(async (planContent: string) => {
+    const trimmedPlan = planContent.trim()
+    if (!trimmedPlan) {
+      throw new Error("没有可自检的章节计划")
+    }
+    if (!agentConfig?.llmConfig) {
+      throw new Error("AI 会话模型尚未就绪,无法自检计划")
+    }
+
+    return runChapterPlanSelfCheckModel(
+      agentConfig.llmConfig,
+      trimmedPlan,
+      buildChapterPlanSelfCheckContext(chapterPlanContextRef.current),
+    )
+  }, [agentConfig?.llmConfig])
+  const runChapterPlanRevision = useCallback(async (planContent: string, selfCheckResult: string) => {
+    if (!agentConfig?.llmConfig) {
+      throw new Error("AI 会话模型尚未就绪,无法修订计划")
+    }
+    return runChapterPlanRevisionModel(agentConfig.llmConfig, planContent, selfCheckResult)
+  }, [agentConfig?.llmConfig])
   const agentDeAiSkills = useMemo(
     () => agentSkillConfig
       ? resolveAvailableDeAiSkills(agentSkillConfig).map(deAiSkillToUserSkill)
@@ -1431,6 +1471,7 @@ export function ChatPanel() {
           const fullContent = lastAssistant?.content || record.finalText || ""
           const extracted = extractChapterPlan(fullContent)
           if (extracted) {
+            chapterPlanContextRef.current = contextPack
             const action = await requestChapterPlanConfirm(
               extracted.plan,
               fullContent,
@@ -1439,14 +1480,24 @@ export function ChatPanel() {
             if (action !== "cancel") {
               let followupText: string
               if (action === "confirm") {
+                confirmedBlueprintRef.current = extracted.plan
                 followupText = buildPlanConfirmMessage(extracted.plan)
               } else if (action === "skip") {
                 followupText = buildPlanSkipMessage()
               } else {
+                confirmedBlueprintRef.current = action.modify
                 followupText = buildPlanConfirmMessage(action.modify)
               }
               setActiveConversation(capturedConvId)
-              await handleSendRef.current(followupText, [], "执行已确认计划")
+              try {
+                await handleSendRef.current(followupText, [], "执行已确认计划")
+              } finally {
+                // followup 成功、失败或被中断都清理,避免后续无关调用误注入旧计划。
+                confirmedBlueprintRef.current = null
+                chapterPlanContextRef.current = null
+              }
+            } else {
+              chapterPlanContextRef.current = null
             }
           }
         }
@@ -2039,6 +2090,8 @@ export function ChatPanel() {
             onConfirm={() => closeChapterPlanDialog("confirm")}
             onSkip={() => closeChapterPlanDialog("skip")}
             onModify={(modified) => closeChapterPlanDialog({ modify: modified })}
+            onSelfCheck={runChapterPlanSelfCheck}
+            onRevisePlan={runChapterPlanRevision}
             onCancel={() => closeChapterPlanDialog("cancel")}
           />
         )}

+ 0 - 1
src/components/sources/outline-chat-panel.tsx

@@ -75,7 +75,6 @@ import {
   collectWebResearch,
   shouldUseWebResearch,
 } from "@/lib/web-research";
-import { parseAgentResponse } from "@/lib/novel/agent-parser";
 
 const OUTLINE_CHAT_DISABLED_TOOLS = ["write_chapter", "write_memory"];
 

+ 2 - 1
src/hooks/use-agent-config.ts

@@ -26,7 +26,7 @@ export interface UseAgentConfigResult {
   mcpWarnings: string[]
 }
 
-export function useAgentConfig(systemPrompt: string): UseAgentConfigResult {
+export function useAgentConfig(systemPrompt: string, getPlanBlueprint?: () => string | undefined): UseAgentConfigResult {
   const aiChatModel = useWikiStore((s) => s.aiChatModel)
   const projectPath = useWikiStore((s) => s.project?.path)
   const dataVersion = useWikiStore((s) => s.dataVersion)
@@ -144,6 +144,7 @@ export function useAgentConfig(systemPrompt: string): UseAgentConfigResult {
       runDeepChapterGeneration,
       draftMode: novelMode,
       projectPath: normalizePath(projectPath),
+      getPlanBlueprint,
     })
 
     return {

+ 16 - 1
src/lib/agent/plugins/build-system-prompt-plugin.spec.ts

@@ -75,6 +75,21 @@ describe("BuildSystemPromptPlugin selected skills", () => {
 
     expect(result.finalSystemPrompt).toContain("章节创作计划协议")
     expect(result.finalSystemPrompt).toContain("chapter_plan")
-    expect(result.finalSystemPrompt).toContain("当前为标准模式")
+    expect(result.finalSystemPrompt).toContain("章节计划")
+    expect(result.finalSystemPrompt).not.toContain("章节蓝图")
+    expect(result.finalSystemPrompt).toContain("维度一·输入校验")
+    expect(result.finalSystemPrompt).toContain("维度四·场景序列编排(计划核心)")
+    expect(result.finalSystemPrompt).toContain("维度七·节奏、字数与结尾钩子")
+    expect(result.finalSystemPrompt).toContain("爽点/期待点设计")
+    expect(result.finalSystemPrompt).toContain("场景戏剧功能")
+    expect(result.finalSystemPrompt).toContain("对话目标")
+    expect(result.finalSystemPrompt).toContain("开头与结尾")
+    expect(result.finalSystemPrompt).toContain("S1/S2/S3")
+    expect(result.finalSystemPrompt).toContain("必须执行")
+    expect(result.finalSystemPrompt).toContain("禁止违背")
+    expect(result.finalSystemPrompt).toContain("可自由发挥")
+    expect(result.finalSystemPrompt).toContain("planBlueprint")
+    const finalPrompt = result.finalSystemPrompt ?? ""
+    expect(finalPrompt.length).toBeLessThan(3000)
   })
 })

+ 50 - 43
src/lib/agent/plugins/build-system-prompt-plugin.ts

@@ -63,51 +63,58 @@ export function createBuildSystemPromptPlugin(deps: BuildSystemPromptPluginDeps
 }
 
 function buildChapterPlanProtocol(mode: AiWorkflowMode): string {
-  const formatRules = [
-    "计划必须包裹在 `<!-- chapter_plan -->` 和 `<!-- /chapter_plan -->` 标记中。",
-    "计划只写给用户确认,不写正文,不写工具调用日志,不写旧工作流说明。",
-    "计划必须按以下字段组织:",
-    "- 任务目标:本次要生成/续写/改写什么。",
-    "- 已读取依据:列出已经读到的章节、记忆、大纲或快照。",
-    "- 缺失资料:列出未找到的大纲、章节或记忆;不要把缺失资料伪装成已读取。",
-    "- 执行步骤:确认后如何写正文。",
-    "- 确认后动作:用户点击确认后直接写正文,不再重复输出计划。",
-    "读取资料前先用 list_chapters、list_outlines、list_memories 确认可用文件名;不要凭空编造文件名。",
-  ].join("\n")
-
-  if (mode === "fast") {
-    return [
-      "## 章节创作计划协议",
-      "",
-      "当前为快速模式,并已开启 Plan Execute。写下一章时,必须按以下流程执行:",
-      "",
-      "1. 先输出轻量章节创作计划,最多 3 条。",
-      "2. 计划内容必须包含:本章目标、关键事件、结尾处理。",
-      formatRules,
-      "输出计划后暂停,等用户确认后再输出正文。",
-    ].join("\n")
-  }
-
-  if (mode === "strict") {
-    return [
-      "## 章节创作计划协议",
-      "",
-      "当前为严格模式。写下一章时,必须按以下流程执行:",
-      "",
-      "1. 先输出详细的章节创作计划。",
-      "2. 计划内容必须包含:本章目标、核心冲突、出场人物与动机、关键事件顺序、情绪曲线、伏笔推进、结尾钩子。",
-      formatRules,
-      "输出计划后暂停,等用户确认后再输出正文。",
-    ].join("\n")
-  }
+  // 章节计划统一使用完整七维度分析,不再按 fast/standard/strict 裁剪维度。
+  // mode 仅用于在协议头标注当前工作流强度,不改变计划结构。
+  const modeLabel =
+    mode === "fast" ? "快速" : mode === "strict" ? "严格" : "标准"
   return [
-    "## 章节创作计划协议",
+    "## 章节创作计划协议(章节计划)",
+    "",
+    `当前工作流强度:${modeLabel}模式,已开启 Plan Execute。写正文前先输出章节计划供用户确认。`,
+    "计划是可追溯、可执行的创作决策,不写正文片段或工具流程。",
+    "",
+    "输出规范:",
+    "1. 计划必须整体包裹在 `<!-- chapter_plan -->` 和 `<!-- /chapter_plan -->` 标记中。",
+    "2. 计划只供用户确认,不写正文、工具日志或旧工作流说明。",
+    "3. 计划必须基于会话上下文包;读取资料前先用 list_chapters、list_outlines、list_memories 确认可用文件名,绝不编造资料名称。",
+    "4. 计划必须按以下七个维度组织,每个维度都不可省略;场景用 S1/S2/S3 编号,后续正文必须按编号执行。",
+    "5. 计划末尾必须列出执行分层:必须执行 / 禁止违背 / 可自由发挥,避免正文阶段把所有内容混成同等优先级。",
+    "",
+    "维度一·输入校验",
+    "- 核对 chapterGoal、outline、previousChapterEnding、recentSummaries、characterStates、cognitionStates、foreshadowingStates、timeline、canonRules、mustDo、mustAvoid、nextChapterAdvice。",
+    "- 字段缺失则写明原因和最小补全方向,不得伪装成已读取。",
+    "",
+    "维度二·章节定位分析",
+    "- 卷/段落阶段:开篇、发展、转折、高潮或收束。",
+    "- 承接 previousChapterEnding 的待解问题、情绪或未完成动作。",
+    "- 写明本章把哪条主线推进到哪个节点,并为后文哪条伏笔铺垫。",
+    "",
+    "维度三·戏剧问题与信息流",
+    "- 本章核心戏剧问题:这一章要回答“……?”",
+    "- 章首悬念 → 章末新悬念或新问题。",
+    "- 信息差:依据 cognitionStates 写清揭示、隐藏、误导,禁止提前泄露角色未知信息。",
+    "- 伏笔动作:依据 foreshadowingStates 标明埋设/推进/回收及程度。",
+    "- 爽点/期待点设计:写清满足哪个期待、制造哪个新期待,必须有情绪、冲突或悬念回报。",
+    "",
+    "维度四·场景序列编排(计划核心)",
+    "- 列出 2-4 个场景,统一写成 S1/S2/S3;每场写明:场景戏剧功能(制造/升级/反转/暂解/引出新问题)、情绪目标、地点、在场人物及目标、进入状态 → 出场状态、转场方式。",
+    "- 场景序列必须连成“起—承—转—合(或钩)”,不得只列一个场景。",
+    "",
+    "维度五·冲突与人物引擎",
+    "- 核心冲突链:谁要什么 → 谁阻拦 → 结果如何 → 导向下一章什么问题。",
+    "- 人物动机必须溯源到 characterStates,禁止凭空给动机。",
+    "- 对话目标:写清角色想得到什么、不愿说什么、如何试探/隐瞒/压迫/诱导;对话后关系或信息状态必须变化。",
+    "- 章末人物变化:认知、关系、能力或处境。",
+    "",
+    "维度六·边界与禁忌",
+    "- 列出不得违背的 canonRules、不得超越的 timeline、不得破坏的 cognitionStates、不得提前回收的伏笔。",
     "",
-    "当前为标准模式。写下一章时,建议按以下流程:",
+    "维度七·节奏、字数与结尾钩子",
+    "- 开头与结尾:开头承接上一章并立刻给当前问题;结尾完成阶段结果,并留下下一章必须解决的问题。",
+    "- 写明情绪/张力曲线、场景篇幅预算和章末钩子(悬念/反转/未决动作/新威胁)。",
+    "- 执行分层:必须执行写漏即偏离;禁止违背写了即错误;可自由发挥只允许补环境、动作、心理、过渡和细节。",
     "",
-    "1. 先输出简短的章节创作计划。",
-    "2. 计划内容包含:本章目标、核心冲突、关键事件、结尾钩子。",
-    formatRules,
-    "输出计划后暂停,等用户确认后再输出正文。",
+    "确认后动作:用户点击确认后,把整份计划作为 run_chapter_workflow 的 planBlueprint 参数传入,再进入正文生成,不再重复输出计划。",
+    "输出计划后暂停,等用户确认后再进入正文。",
   ].join("\n")
 }

+ 2 - 0
src/lib/agent/tools/index.ts

@@ -56,6 +56,7 @@ export interface ToolFactoryOptions {
   aiWorkflowMode?: AiWorkflowMode
   runDeepChapterGeneration?: RunDeepChapterGeneration
   onToolEvent?: (event: AgentToolEvent) => void
+  getPlanBlueprint?: () => string | undefined
 }
 
 export function registerAllBuiltInTools(registry: ToolRegistry, options: ToolFactoryOptions): void {
@@ -106,6 +107,7 @@ export function registerAllBuiltInTools(registry: ToolRegistry, options: ToolFac
       aiWorkflowMode: options.aiWorkflowMode,
       runDeepChapterGeneration: options.runDeepChapterGeneration,
       onToolEvent: options.onToolEvent,
+      getPlanBlueprint: options.getPlanBlueprint,
     }))
   }
   for (const tool of options.mcpTools ?? []) {

+ 135 - 0
src/lib/agent/tools/run-chapter-workflow.spec.ts

@@ -183,4 +183,139 @@ describe("createRunChapterWorkflowTool", () => {
       kind: "extract_result",
     }))
   })
+
+  it("forwards the confirmed plan blueprint into deep chapter generation", async () => {
+    const runDeepChapterGeneration = vi.fn(async (_input, callbacks) => {
+      callbacks.onWorkflowEvent?.({
+        type: "started",
+ id: "deep_chapter:chapter_context",
+        name: "chapter_context",
+        title: "读取上下文",
+        timestamp: 100,
+      })
+      return {
+        finalContent: "最终正文",
+        taskBrief: "任务书",
+        draftContent: "初稿",
+        reviewResults: [],
+        revised: false,
+      }
+    })
+    const tool = createRunChapterWorkflowTool({
+      projectPath: "E:/Novel",
+      llmConfig,
+      aiWorkflowMode: "standard",
+      runDeepChapterGeneration,
+    })
+
+    const blueprint = "维度四·场景序列编排:1. 雨夜旧屋揭示线索 2. 屋外脚步声悬念收束"
+    await tool.execute({
+      intent: "write_chapter",
+      userRequest: "生成第3章",
+      chapterNumber: 3,
+      planBlueprint: blueprint,
+    })
+
+    expect(runDeepChapterGeneration).toHaveBeenCalledWith(
+      expect.objectContaining({
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        aiWorkflowMode: "standard",
+        planBlueprint: blueprint,
+      }),
+      expect.any(Object),
+      undefined,
+      undefined,
+    )
+  })
+  it("falls back to getPlanBlueprint when AI omits planBlueprint from tool call params", async () => {
+    const runDeepChapterGeneration = vi.fn(async () => ({
+      finalContent: "最终正文",
+      taskBrief: "任务书",
+      draftContent: "初稿",
+      reviewResults: [],
+      revised: false,
+    }))
+    const tool = createRunChapterWorkflowTool({
+      projectPath: "E:/Novel",
+      llmConfig,
+      aiWorkflowMode: "standard",
+      runDeepChapterGeneration,
+      getPlanBlueprint: () => "兜底计划:场景序列 1→2→3",
+    })
+
+    // AI 调用时未带 planBlueprint 参数
+    await tool.execute({
+      intent: "write_chapter",
+      userRequest: "生成第3章",
+      chapterNumber: 3,
+    })
+
+    expect(runDeepChapterGeneration).toHaveBeenCalledWith(
+      expect.objectContaining({
+        planBlueprint: "兜底计划:场景序列 1→2→3",
+      }),
+      expect.any(Object),
+      undefined,
+      undefined,
+    )
+  })
+
+  it("prefers AI-provided planBlueprint over the getter fallback", async () => {
+    const runDeepChapterGeneration = vi.fn(async () => ({
+      finalContent: "最终正文",
+      taskBrief: "任务书",
+      draftContent: "初稿",
+      reviewResults: [],
+      revised: false,
+    }))
+    const tool = createRunChapterWorkflowTool({
+      projectPath: "E:/Novel",
+      llmConfig,
+      aiWorkflowMode: "standard",
+      runDeepChapterGeneration,
+      getPlanBlueprint: () => "兜底计划",
+    })
+
+    await tool.execute({
+      intent: "write_chapter",
+      userRequest: "生成第3章",
+      planBlueprint: "AI传入的计划",
+    })
+
+    expect(runDeepChapterGeneration).toHaveBeenCalledWith(
+      expect.objectContaining({
+        planBlueprint: "AI传入的计划",
+      }),
+      expect.any(Object),
+      undefined,
+      undefined,
+    )
+  })
+
+  it("includes plan compliance in the tool result when available", async () => {
+    const runDeepChapterGeneration = vi.fn(async () => ({
+      finalContent: "最终正文",
+      taskBrief: "任务书",
+      draftContent: "初稿",
+      reviewResults: [],
+      revised: false,
+      planCompliance: "履约度:基本符合",
+    }))
+    const tool = createRunChapterWorkflowTool({
+      projectPath: "E:/Novel",
+      llmConfig,
+      aiWorkflowMode: "standard",
+      runDeepChapterGeneration,
+    })
+
+    const result = await tool.execute({
+      intent: "write_chapter",
+      userRequest: "生成第3章",
+    })
+
+    expect(result).toContain("计划履约度")
+    expect(result).toContain("履约度:基本符合")
+  })
 })

+ 21 - 4
src/lib/agent/tools/run-chapter-workflow.ts

@@ -23,6 +23,11 @@ export interface RunChapterWorkflowToolOptions {
   runDeepChapterGeneration: RunDeepChapterGeneration
   onToolEvent?: (event: AgentToolEvent) => void
   onActivityEvent?: (event: AgentActivityEvent) => void
+  /**
+   * 当 AI 未在工具调用参数中携带 planBlueprint 时,从这里兜底取已确认的章节计划。
+   * 保证用户确认的计划为强制约束,不依赖模型是否遵守自然语言提示。
+   */
+  getPlanBlueprint?: () => string | undefined
 }
 
 interface RunChapterWorkflowParams {
@@ -30,6 +35,7 @@ interface RunChapterWorkflowParams {
   userRequest?: string
   chapterNumber?: number
   workflowMode?: AiWorkflowMode
+  planBlueprint?: string
 }
 
 function toNumber(value: unknown): number | undefined {
@@ -50,6 +56,7 @@ function normalizeParams(params: Record<string, unknown>): RunChapterWorkflowPar
       params.workflowMode === "fast" || params.workflowMode === "standard" || params.workflowMode === "strict"
         ? params.workflowMode
         : undefined,
+    planBlueprint: typeof params.planBlueprint === "string" ? params.planBlueprint : undefined,
   }
 }
 
@@ -100,6 +107,11 @@ export function createRunChapterWorkflowTool(options: RunChapterWorkflowToolOpti
         description: "执行强度:fast、standard 或 strict。省略时使用当前 AI 会话模式。",
         enum: ["fast", "standard", "strict"],
       },
+      planBlueprint: {
+        type: "string",
+        description:
+          "用户在会话层已确认的章节计划原文。若存在,必须完整透传,作为写作任务书的权威依据;不得改写或省略。",
+      },
     },
     async execute(rawParams, signal, context?: ToolExecutionContext) {
       const params = normalizeParams(rawParams)
@@ -108,9 +120,12 @@ export function createRunChapterWorkflowTool(options: RunChapterWorkflowToolOpti
         return "错误:缺少 userRequest,无法运行章节工作流。"
       }
 
-      const parentCallId = context?.callId ?? `run_chapter_workflow:${Date.now()}`
-      const emitToolEvent = context?.onToolEvent ?? options.onToolEvent
-      const emitActivityEvent = context?.onActivityEvent ?? options.onActivityEvent
+     const parentCallId = context?.callId ?? `run_chapter_workflow:${Date.now()}`
+     const emitToolEvent = context?.onToolEvent ?? options.onToolEvent
+     const emitActivityEvent = context?.onActivityEvent ?? options.onActivityEvent
+      // 兜底:AI 未在工具调用参数中携带 planBlueprint 时,从外部 getter 补上,
+      // 保证用户确认的计划一定进入章节生成链路,不依赖模型是否遵守自然语言提示。
+      const planBlueprint = params.planBlueprint?.trim() || options.getPlanBlueprint?.()?.trim() || undefined
       const result = await options.runDeepChapterGeneration(
         {
           projectPath: options.projectPath,
@@ -118,6 +133,7 @@ export function createRunChapterWorkflowTool(options: RunChapterWorkflowToolOpti
           chapterNumber: params.chapterNumber,
           llmConfig: options.llmConfig,
           aiWorkflowMode: params.workflowMode ?? options.aiWorkflowMode,
+          planBlueprint,
         },
         {
           onWorkflowEvent: (event) => {
@@ -138,10 +154,11 @@ export function createRunChapterWorkflowTool(options: RunChapterWorkflowToolOpti
         "章节工作流完成。",
         `是否返修:${result.revised ? "是" : "否"}`,
         `任务书:${result.taskBrief}`,
+        result.planCompliance ? `计划履约度:\n${result.planCompliance}` : "",
         "",
         "最终正文:",
         result.finalContent,
-      ].join("\n")
+      ].filter((line) => line !== "").join("\n")
     },
   }
 }

+ 144 - 0
src/lib/novel/chapter-plan-compliance.spec.ts

@@ -0,0 +1,144 @@
+import { describe, expect, it, vi } from "vitest"
+import type { LlmConfig } from "@/stores/wiki-store"
+import {
+  buildChapterPlanCompliancePrompt,
+  buildChapterPlanDeviationRepairPrompt,
+  parseChapterPlanComplianceResult,
+  runChapterPlanComplianceCheck,
+  shouldRepairChapterPlanDeviation,
+} from "./chapter-plan-compliance"
+
+const streamChatMock = vi.hoisted(() => vi.fn())
+
+vi.mock("@/lib/llm-client", () => ({
+  streamChat: streamChatMock,
+}))
+
+const llmConfig: LlmConfig = {
+  provider: "custom",
+  apiKey: "test-key",
+  model: "test-model",
+  ollamaUrl: "",
+  customEndpoint: "https://example.test/v1",
+  maxContextSize: 120000,
+}
+
+describe("chapter-plan-compliance", () => {
+  it("builds a prompt that compares final content against the confirmed plan", () => {
+    const prompt = buildChapterPlanCompliancePrompt("确认计划", "最终正文")
+
+    expect(prompt).toContain("计划履约度检查")
+    expect(prompt).toContain("确认计划")
+    expect(prompt).not.toContain("蓝图")
+    expect(prompt).toContain("最终正文")
+    expect(prompt).toContain("场景序列")
+    expect(prompt).toContain("伏笔动作")
+    expect(prompt).toContain("爽点/期待点")
+    expect(prompt).toContain("场景戏剧功能")
+    expect(prompt).toContain("对话目标")
+    expect(prompt).toContain("水文")
+    expect(prompt).toContain("开头和结尾")
+    expect(prompt).toContain("JSON")
+    expect(prompt.length).toBeLessThan(900)
+  })
+
+  it("keeps both chapter opening and ending when final content is long", () => {
+    const longContent = [
+      "开头承接上一章门缝声。",
+      "中段推进。".repeat(7000),
+      "结尾出现门外第二个人影。",
+    ].join("\n")
+
+    const prompt = buildChapterPlanCompliancePrompt("确认计划", longContent)
+
+    expect(prompt).toContain("开头承接上一章门缝声")
+    expect(prompt).toContain("结尾出现门外第二个人影")
+    expect(prompt).toContain("正文中段已截断")
+    expect(prompt.length).toBeLessThan(13000)
+  })
+
+  it("parses structured compliance JSON and marks clear deviations for repair", () => {
+    const parsed = parseChapterPlanComplianceResult(JSON.stringify({
+      status: "partial_deviation",
+      summary: "旧屋揭示完成,但结尾钩子缺失。",
+      deviations: [{
+        point: "结尾钩子",
+        evidence: "正文停在解释线索,没有门外脚步声。",
+        suggestion: "补入门外第二个人影,导向下一章。",
+      }],
+    }))
+
+    expect(parsed.status).toBe("partial_deviation")
+    expect(parsed.summary).toContain("结尾钩子缺失")
+    expect(parsed.deviations).toEqual([{
+      point: "结尾钩子",
+      evidence: "正文停在解释线索,没有门外脚步声。",
+      suggestion: "补入门外第二个人影,导向下一章。",
+    }])
+    expect(shouldRepairChapterPlanDeviation(parsed)).toBe(true)
+  })
+
+  it("parses legacy text compliance results without forcing repair for mostly compliant chapters", () => {
+    const parsed = parseChapterPlanComplianceResult([
+      "履约度:基本符合",
+      "偏离点:轻微缺少环境回声。",
+      "正文证据:旧屋内部描写偏少。",
+      "建议修正:可不返修。",
+    ].join("\n"))
+
+    expect(parsed.status).toBe("mostly_compliant")
+    expect(parsed.deviations[0]?.point).toBe("轻微缺少环境回声。")
+    expect(shouldRepairChapterPlanDeviation(parsed)).toBe(false)
+  })
+
+  it("builds a lightweight repair prompt that only patches deviation points", () => {
+    const prompt = buildChapterPlanDeviationRepairPrompt(
+      "计划摘要:旧屋揭示,结尾必须出现门外第二个人影。",
+      "最终正文:主角读完旧信后结束。",
+      {
+        status: "clear_deviation",
+        summary: "明显偏离:结尾钩子缺失。",
+        deviations: [{
+          point: "章末钩子",
+          evidence: "正文没有第二个人影。",
+          suggestion: "只在结尾补入门外第二个人影。",
+        }],
+        rawText: "",
+      },
+    )
+
+    expect(prompt).toContain("只修复偏离点,不重写全章")
+    expect(prompt).toContain("计划摘要")
+    expect(prompt).toContain("章末钩子")
+    expect(prompt).toContain("最终正文")
+    expect(prompt.length).toBeLessThan(13500)
+  })
+
+  it("runs the compliance model call and returns streamed text", async () => {
+    streamChatMock.mockImplementationOnce(async (_config, messages, callbacks) => {
+      expect(messages[0].content).toContain("计划履约度检查")
+      callbacks.onToken("履约度:基本符合")
+      callbacks.onDone()
+    })
+
+    await expect(runChapterPlanComplianceCheck(llmConfig, "确认计划", "最终正文"))
+      .resolves.toBe("履约度:基本符合")
+  })
+
+  it("forwards the stop signal to the compliance model call", async () => {
+    const controller = new AbortController()
+    streamChatMock.mockImplementationOnce(async (_config, _messages, callbacks) => {
+      callbacks.onToken("履约度:符合")
+      callbacks.onDone()
+    })
+
+    await runChapterPlanComplianceCheck(llmConfig, "确认计划", "最终正文", controller.signal)
+
+    expect(streamChatMock).toHaveBeenCalledWith(
+      llmConfig,
+      expect.any(Array),
+      expect.any(Object),
+      controller.signal,
+    )
+  })
+})

+ 276 - 0
src/lib/novel/chapter-plan-compliance.ts

@@ -0,0 +1,276 @@
+import { streamChat } from "@/lib/llm-client"
+import type { LlmConfig } from "@/stores/wiki-store"
+
+const FINAL_CONTENT_EXCERPT_MAX_CHARS = 12000
+const FINAL_CONTENT_EXCERPT_MARKER = "(正文中段已截断,保留开头与结尾用于检查承接和章末钩子。)"
+
+export type ChapterPlanComplianceStatus =
+  | "compliant"
+  | "mostly_compliant"
+  | "partial_deviation"
+  | "clear_deviation"
+  | "unknown"
+
+export interface ChapterPlanComplianceDeviation {
+  point: string
+  evidence: string
+  suggestion: string
+}
+
+export interface ParsedChapterPlanComplianceResult {
+  status: ChapterPlanComplianceStatus
+  summary: string
+  deviations: ChapterPlanComplianceDeviation[]
+  rawText: string
+}
+
+export function buildChapterPlanCompliancePrompt(planBlueprint: string, finalContent: string): string {
+  return [
+    "你是小说章节计划履约度检查助手。",
+    "请对照用户已确认的章节计划,检查最终正文是否按计划执行。",
+    "",
+    "计划履约度检查维度:",
+    "1. 场景序列:主要场景、顺序和转场是否符合计划。",
+    "2. 场景戏剧功能:是否完成制造/升级/反转/暂解/引出新问题。",
+    "3. 爽点/期待点:是否兑现旧期待并制造新期待。",
+    "4. 冲突与人物:目标、阻力、结果是否符合计划。",
+    "5. 信息流:揭示、隐藏、误导是否正确,是否提前泄露角色未知信息。",
+    "6. 伏笔动作:埋设、推进、回收是否按计划执行。",
+    "7. 对话目标:是否推动试探、隐瞒、压迫、诱导、关系或信息变化。",
+    "8. 水文风险:是否有不推动剧情/人物关系/信息差/伏笔/危机的段落。",
+    "9. 开头和结尾:开头是否承接并给当前问题;结尾钩子是否导向下一章。",
+    "10. 边界禁忌:是否违背 canon、timeline、cognition、mustAvoid。",
+    "",
+    "输出要求:",
+    "1. 只输出 JSON,不改写正文。",
+    "2. status 只能是 compliant / mostly_compliant / partial_deviation / clear_deviation。",
+    "3. JSON字段:status、summary、deviations;deviations 最多 5 条,每条含 point/evidence/suggestion。",
+    "4. 只有影响正文质量的缺失、顺序错误或边界违背才标 partial_deviation / clear_deviation。",
+    "",
+    "用户已确认的章节计划:",
+    planBlueprint.trim(),
+    "",
+    "最终正文:",
+    buildFinalContentExcerpt(finalContent),
+  ].join("\n")
+}
+
+export async function runChapterPlanComplianceCheck(
+  llmConfig: LlmConfig,
+  planBlueprint: string,
+  finalContent: string,
+  signal?: AbortSignal,
+): Promise<string> {
+  if (!planBlueprint.trim()) return ""
+  if (!finalContent.trim()) return ""
+
+  let result = ""
+  let streamError: Error | undefined
+  await streamChat(
+    llmConfig,
+    [{ role: "user", content: buildChapterPlanCompliancePrompt(planBlueprint, finalContent) }],
+    {
+      onToken: (token) => { result += token },
+      onDone: () => {},
+      onError: (error) => { streamError = error },
+    },
+    signal,
+  )
+  if (streamError) throw streamError
+  return result.trim()
+}
+
+export function parseChapterPlanComplianceResult(text: string): ParsedChapterPlanComplianceResult {
+  const rawText = text.trim()
+  if (!rawText) {
+    return { status: "unknown", summary: "", deviations: [], rawText }
+  }
+
+  const jsonObject = tryParseComplianceJson(rawText)
+  if (jsonObject) {
+    const status = normalizeComplianceStatus(jsonObject.status ?? jsonObject["履约度"], rawText)
+    const summary = stringValue(jsonObject.summary ?? jsonObject["summary"] ?? jsonObject["总评"]) || firstNonEmptyLine(rawText)
+    const deviations = normalizeDeviationArray(jsonObject.deviations ?? jsonObject["偏离点"])
+    return { status, summary, deviations, rawText }
+  }
+
+  const status = normalizeComplianceStatus(undefined, rawText)
+  const deviations = parseLegacyDeviationLines(rawText)
+  return {
+    status,
+    summary: firstNonEmptyLine(rawText),
+    deviations,
+    rawText,
+  }
+}
+
+export function shouldRepairChapterPlanDeviation(result: ParsedChapterPlanComplianceResult): boolean {
+  if (result.status !== "partial_deviation" && result.status !== "clear_deviation") return false
+  return result.deviations.some((item) => item.point.trim() || item.suggestion.trim())
+}
+
+export function buildChapterPlanDeviationRepairPrompt(
+  planBlueprint: string,
+  finalContent: string,
+  complianceResult: ParsedChapterPlanComplianceResult | string,
+): string {
+  const parsed = typeof complianceResult === "string"
+    ? parseChapterPlanComplianceResult(complianceResult)
+    : complianceResult
+  const deviationText = parsed.deviations.length > 0
+    ? parsed.deviations
+      .slice(0, 5)
+      .map((item, index) =>
+        [
+          `${index + 1}. 偏离点:${item.point || "未说明"}`,
+          `   正文证据:${item.evidence || "未说明"}`,
+          `   建议修正:${item.suggestion || "未说明"}`,
+        ].join("\n"),
+      )
+      .join("\n")
+    : parsed.rawText
+
+  return [
+    "你是小说章节计划偏离点轻量返修助手。",
+    "任务:只修复偏离点,不重写全章;保留原正文结构、叙事节奏、人物口吻和已有有效内容。",
+    "禁止:扩写无关情节、替换整章、推翻已完成场景、改变计划之外的设定。",
+    "输出:只输出返修后的完整章节正文,不解释。",
+    "",
+    "用户已确认的章节计划执行摘要:",
+    planBlueprint.trim(),
+    "",
+    "计划履约检查结果:",
+    `履约状态:${parsed.status}`,
+    parsed.summary ? `总评:${parsed.summary}` : "",
+    deviationText,
+    "",
+    "最终正文:",
+    buildFinalContentExcerpt(finalContent),
+  ].filter(Boolean).join("\n")
+}
+
+export async function runChapterPlanDeviationRepair(
+  llmConfig: LlmConfig,
+  planBlueprint: string,
+  finalContent: string,
+  complianceResult: ParsedChapterPlanComplianceResult | string,
+  signal?: AbortSignal,
+): Promise<string> {
+  if (!planBlueprint.trim()) return finalContent.trim()
+  if (!finalContent.trim()) return ""
+
+  let result = ""
+  let streamError: Error | undefined
+  await streamChat(
+    llmConfig,
+    [{ role: "user", content: buildChapterPlanDeviationRepairPrompt(planBlueprint, finalContent, complianceResult) }],
+    {
+      onToken: (token) => { result += token },
+      onDone: () => {},
+      onError: (error) => { streamError = error },
+    },
+    signal,
+  )
+  if (streamError) throw streamError
+  return result.trim() || finalContent.trim()
+}
+
+function tryParseComplianceJson(text: string): Record<string, unknown> | null {
+  const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim() ?? text
+  const start = fenced.indexOf("{")
+  const end = fenced.lastIndexOf("}")
+  if (start < 0 || end <= start) return null
+
+  try {
+    const parsed = JSON.parse(fenced.slice(start, end + 1))
+    return parsed && typeof parsed === "object" && !Array.isArray(parsed)
+      ? parsed as Record<string, unknown>
+      : null
+  } catch {
+    return null
+  }
+}
+
+function normalizeComplianceStatus(value: unknown, fallbackText: string): ChapterPlanComplianceStatus {
+  const source = `${stringValue(value)}\n${fallbackText}`
+  if (/clear_deviation|明显偏离/.test(source)) return "clear_deviation"
+  if (/partial_deviation|部分偏离/.test(source)) return "partial_deviation"
+  if (/mostly_compliant|基本符合/.test(source)) return "mostly_compliant"
+  if (/compliant|履约度\s*[::]\s*符合|^符合$/m.test(source)) return "compliant"
+  return "unknown"
+}
+
+function normalizeDeviationArray(value: unknown): ChapterPlanComplianceDeviation[] {
+  if (!Array.isArray(value)) return []
+  return value
+    .slice(0, 5)
+    .map((item) => {
+      if (!item || typeof item !== "object") {
+        return { point: stringValue(item), evidence: "", suggestion: "" }
+      }
+      const record = item as Record<string, unknown>
+      return {
+        point: stringValue(record.point ?? record["偏离点"]),
+        evidence: stringValue(record.evidence ?? record["正文证据"]),
+        suggestion: stringValue(record.suggestion ?? record["建议修正"]),
+      }
+    })
+    .filter((item) => item.point || item.evidence || item.suggestion)
+}
+
+function parseLegacyDeviationLines(text: string): ChapterPlanComplianceDeviation[] {
+  const deviations: ChapterPlanComplianceDeviation[] = []
+  let current: ChapterPlanComplianceDeviation | null = null
+  for (const line of text.split(/\r?\n/)) {
+    const trimmed = line.trim()
+    if (!trimmed) continue
+    const point = matchLegacyField(trimmed, "偏离点")
+    if (point !== null) {
+      current = { point, evidence: "", suggestion: "" }
+      deviations.push(current)
+      continue
+    }
+    const evidence = matchLegacyField(trimmed, "正文证据")
+    if (evidence !== null) {
+      if (!current) {
+        current = { point: "", evidence: "", suggestion: "" }
+        deviations.push(current)
+      }
+      current.evidence = evidence
+      continue
+    }
+    const suggestion = matchLegacyField(trimmed, "建议修正")
+    if (suggestion !== null) {
+      if (!current) {
+        current = { point: "", evidence: "", suggestion: "" }
+        deviations.push(current)
+      }
+      current.suggestion = suggestion
+    }
+  }
+  return deviations.slice(0, 5).filter((item) => item.point || item.evidence || item.suggestion)
+}
+
+function matchLegacyField(line: string, field: string): string | null {
+  const match = line.match(new RegExp(`${field}\\s*[::]\\s*(.+)$`))
+  return match?.[1]?.trim() ?? null
+}
+
+function stringValue(value: unknown): string {
+  return typeof value === "string" ? value.trim() : ""
+}
+
+function firstNonEmptyLine(text: string): string {
+  return text.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? ""
+}
+
+function buildFinalContentExcerpt(finalContent: string): string {
+  const normalized = finalContent.trim()
+  if (normalized.length <= FINAL_CONTENT_EXCERPT_MAX_CHARS) return normalized
+
+  const marker = `\n\n${FINAL_CONTENT_EXCERPT_MARKER}\n\n`
+  const available = Math.max(200, FINAL_CONTENT_EXCERPT_MAX_CHARS - marker.length)
+  const headLength = Math.floor(available / 2)
+  const tailLength = available - headLength
+  return `${normalized.slice(0, headLength).trimEnd()}${marker}${normalized.slice(-tailLength).trimStart()}`
+}

+ 82 - 0
src/lib/novel/chapter-plan-execution-summary.spec.ts

@@ -0,0 +1,82 @@
+import { describe, expect, it } from "vitest"
+import { buildChapterPlanExecutionSummary } from "./chapter-plan-execution-summary"
+
+describe("buildChapterPlanExecutionSummary", () => {
+  it("outputs a fixed execution structure with scene ids and priority buckets", () => {
+    const plan = [
+      "维度二·章节定位分析:本章目标是承接门缝声,推进锈钥匙线索。",
+      "维度四·场景序列编排:1. 雨夜旧屋承接门缝声,功能:制造当前问题;2. 信纸揭示线索,功能:升级信息差;3. 屋外脚步声收束,功能:引出新威胁。",
+      "维度五·冲突与人物引擎:对话目标:主角试探小晴,小晴隐瞒旧屋主人身份。",
+      "维度六·边界与禁忌:必须推进锈钥匙;禁止提前揭露旧屋主人身份。",
+      "维度七·节奏、字数与结尾钩子:结尾钩子是第二个人影贴近门口。",
+    ].join("\n")
+
+    const summary = buildChapterPlanExecutionSummary(plan)
+
+    expect(summary).toContain("本章目标:")
+    expect(summary).toContain("场景序列:")
+    expect(summary).toContain("S1 雨夜旧屋承接门缝声")
+    expect(summary).toContain("S2 信纸揭示线索")
+    expect(summary).toContain("S3 屋外脚步声收束")
+    expect(summary).toContain("必须执行:")
+    expect(summary).toContain("禁止违背:")
+    expect(summary).toContain("可自由发挥:")
+    expect(summary).toContain("对话目标:")
+    expect(summary).toContain("伏笔动作:")
+    expect(summary).toContain("结尾钩子:")
+  })
+
+  it("keeps chapter execution constraints and removes low-value filler", () => {
+    const plan = [
+      "维度一·输入校验:本章写第3章。",
+      "补充说明:这份计划用于帮助模型理解任务,不属于正文。",
+      "维度四·场景序列编排:1. 雨夜旧屋承接门缝声;2. 信纸揭示线索;3. 屋外脚步声收束。",
+      "维度五·人物、冲突与对话目标:主角试探小晴,小晴隐瞒关键信息。",
+      "维度六·伏笔与边界禁忌:推进锈钥匙,不得提前揭露旧屋主人身份。",
+      "维度七·节奏、字数与结尾钩子:结尾停在第二个人影贴近门口。",
+      "感谢确认,下面才会开始写正文。",
+    ].join("\n")
+
+    const summary = buildChapterPlanExecutionSummary(plan)
+
+    expect(summary).toContain("用户已确认的章节计划执行摘要")
+    expect(summary).toContain("场景序列:")
+    expect(summary).toContain("对话目标")
+    expect(summary).toContain("伏笔")
+    expect(summary).toContain("禁止违背")
+    expect(summary).toContain("结尾钩子")
+    expect(summary).not.toContain("感谢确认")
+  })
+
+  it("caps long confirmed plans while preserving the strongest execution lines", () => {
+    const filler = Array.from({ length: 80 }, (_, index) => `普通说明 ${index}:只是在解释计划来源。`).join("\n")
+    const plan = [
+      filler,
+      "维度四·场景序列编排:必须先写旧屋,再写信纸,最后写门外脚步。",
+      "维度六·伏笔与边界禁忌:必须推进锈钥匙,禁止揭露旧屋主人身份。",
+      "维度七·节奏、字数与结尾钩子:章末留下第二个人影。",
+    ].join("\n")
+
+    const summary = buildChapterPlanExecutionSummary(plan, 220)
+
+    expect(summary.length).toBeLessThanOrEqual(260)
+    expect(summary).toContain("旧屋")
+    expect(summary).toContain("锈钥匙")
+    expect(summary).toContain("计划执行摘要已截断")
+  })
+
+  it("falls back to key original plan lines when the structured summary lacks required execution anchors", () => {
+    const plan = [
+      "维度一·输入校验:本章写第3章。",
+      "维度二·章节定位分析:承接上一章门缝声。",
+      "维度六·边界与禁忌:禁止提前揭露旧屋主人身份。",
+      "维度七·节奏、字数与结尾钩子:章末留下屋外脚步声。",
+    ].join("\n")
+
+    const summary = buildChapterPlanExecutionSummary(plan)
+
+    expect(summary).toContain("原计划关键片段:")
+    expect(summary).toContain("维度六·边界与禁忌:禁止提前揭露旧屋主人身份。")
+    expect(summary).toContain("维度七·节奏、字数与结尾钩子:章末留下屋外脚步声。")
+  })
+})

+ 112 - 0
src/lib/novel/chapter-plan-execution-summary.ts

@@ -0,0 +1,112 @@
+const DEFAULT_PLAN_EXECUTION_SUMMARY_MAX_CHARS = 1800
+
+const EXECUTION_KEYWORD_PATTERN =
+  /维度[一二三四五六七]|本章目标|章节目标|场景|戏剧功能|信息流|伏笔|边界|禁忌|对话目标|爽点|期待点|开头|结尾|钩子|水文|冲突|人物|必须|禁止|不得|可自由|自由发挥|mustDo|mustAvoid|canon|timeline|cognition/i
+
+const LOW_VALUE_LINE_PATTERN =
+  /感谢确认|下面开始|下面才会|工具流程|计划来源|帮助模型理解|不属于正文|补充说明/i
+
+const TRUNCATION_MARKER = "(计划执行摘要已截断,保留关键执行约束。)"
+
+export function buildChapterPlanExecutionSummary(
+  planContent: string,
+  maxChars = DEFAULT_PLAN_EXECUTION_SUMMARY_MAX_CHARS,
+): string {
+  const normalized = planContent.trim()
+  if (!normalized) return ""
+
+  const lines = normalized
+    .split(/\r?\n/)
+    .map((line) => line.trim())
+    .filter(Boolean)
+
+  const executionLines = lines.filter(
+    (line) => EXECUTION_KEYWORD_PATTERN.test(line) && !LOW_VALUE_LINE_PATTERN.test(line),
+  )
+  const header = "用户已确认的章节计划执行摘要:"
+  const sourceLines = executionLines.length > 0 ? executionLines : lines
+  const summary = [
+    header,
+    `本章目标:${extractFirstValue(sourceLines, /本章目标|章节目标|维度[一二]/)}`,
+    "场景序列:",
+    ...extractSceneItems(sourceLines).map((scene, index) => `S${index + 1} ${scene}`),
+    `必须执行:${joinValues(extractValues(sourceLines, /必须|mustDo|推进|完成|维度三|维度五/))}`,
+    `禁止违背:${joinValues(extractValues(sourceLines, /禁止|不得|不能|避免|mustAvoid|边界|禁忌|canon|timeline|cognition|提前|维度六/))}`,
+    `可自由发挥:${extractFirstValue(sourceLines, /可自由|自由发挥/) || "可补足环境、动作、心理、过渡和细节,但不得改变必须执行与禁止违背内容。"}`,
+    `对话目标:${extractFirstValue(sourceLines, /对话目标/)}`,
+    `伏笔动作:${joinValues(extractValues(sourceLines, /伏笔|埋设|回收/))}`,
+    `结尾钩子:${extractFirstValue(sourceLines, /结尾|钩子|维度七/)}`,
+  ]
+    .filter((line) => line.trim())
+    .join("\n")
+  const summaryWithFallback = ensureSummaryQuality(summary, sourceLines)
+
+  const omittedLowValueLines = executionLines.length > 0 && executionLines.length < lines.length
+  const shouldMarkTruncated = omittedLowValueLines || summaryWithFallback.length > maxChars
+
+  return capSummary(summaryWithFallback, maxChars, shouldMarkTruncated)
+}
+
+function extractValues(lines: string[], pattern: RegExp): string[] {
+  return unique(lines.filter((line) => pattern.test(line)).map(cleanPlanLine).filter(Boolean))
+}
+
+function extractFirstValue(lines: string[], pattern: RegExp): string {
+  return extractValues(lines, pattern)[0] ?? ""
+}
+
+function extractSceneItems(lines: string[]): string[] {
+  const sceneLine = lines.find((line) => /场景序列|维度四/.test(line))
+  if (!sceneLine) return []
+  const content = cleanPlanLine(sceneLine)
+  const numbered = content
+    .replace(/(?:^|[;;]\s*)(?:S)?(\d+)[.、.]\s*/gi, "\n")
+    .split(/\n|[;;]/)
+    .map((item) => item.trim())
+    .filter(Boolean)
+  const items = numbered.length > 0 ? numbered : [content]
+  return items.map((item) => item.replace(/^S\d+\s*/i, "").trim()).filter(Boolean)
+}
+
+function cleanPlanLine(line: string): string {
+  return line
+    .replace(/^[-*]\s*/, "")
+    .replace(/^维度[一二三四五六七][^::]*[::]\s*/, "")
+    .replace(/^(本章目标|章节目标|场景序列编排|场景序列|对话目标|结尾钩子|伏笔动作|边界与禁忌)[::]\s*/, "")
+    .trim()
+}
+
+function joinValues(values: string[]): string {
+  return values.join(";")
+}
+
+function unique(values: string[]): string[] {
+  return Array.from(new Set(values))
+}
+
+function capSummary(summary: string, maxChars: number, withMarker: boolean): string {
+  if (!withMarker && summary.length <= maxChars) return summary.trim()
+  const markerPart = `\n${TRUNCATION_MARKER}`
+  const available = Math.max(80, maxChars - markerPart.length)
+  if (summary.length <= available) return `${summary}${markerPart}`.trim()
+  return `${summary.slice(0, available).trimEnd()}${markerPart}`.trim()
+}
+
+function ensureSummaryQuality(summary: string, sourceLines: string[]): string {
+  const hasSceneId = /\nS\d+\s+\S/.test(summary)
+  const hasForbidden = /禁止违背:\S/.test(summary)
+  const hasEndingHook = /结尾钩子:\S/.test(summary)
+  if (hasSceneId && hasForbidden && hasEndingHook) return summary
+
+  const fallbackLines = sourceLines
+    .filter((line) => /维度四|场景序列|维度六|边界|禁忌|禁止|不得|维度七|结尾|钩子/.test(line))
+    .slice(0, 6)
+  if (fallbackLines.length === 0) return summary
+
+  return [
+    summary,
+    "",
+    "原计划关键片段:",
+    ...fallbackLines,
+  ].join("\n")
+}

+ 119 - 0
src/lib/novel/chapter-plan-self-check.spec.ts

@@ -0,0 +1,119 @@
+import { describe, expect, it, vi } from "vitest"
+import type { LlmConfig } from "@/stores/wiki-store"
+import {
+  buildChapterPlanSelfCheckPrompt,
+  buildChapterPlanRevisionPrompt,
+  parseChapterPlanSelfCheckResult,
+  runChapterPlanRevision,
+  runChapterPlanSelfCheck,
+} from "./chapter-plan-self-check"
+
+const streamChatMock = vi.hoisted(() => vi.fn())
+
+vi.mock("@/lib/llm-client", () => ({
+  streamChat: streamChatMock,
+}))
+
+const llmConfig: LlmConfig = {
+  provider: "custom",
+  apiKey: "test-key",
+  model: "test-model",
+  ollamaUrl: "",
+  customEndpoint: "https://example.test/v1",
+  maxContextSize: 120000,
+}
+
+describe("chapter-plan-self-check", () => {
+  it("builds a prompt that checks blueprint completeness and includes the source plan", () => {
+    const prompt = buildChapterPlanSelfCheckPrompt("维度四·场景序列编排:旧屋揭示")
+
+    expect(prompt).toContain("计划自检")
+    expect(prompt).not.toContain("蓝图")
+    expect(prompt).toContain("七个维度")
+    expect(prompt).toContain("维度四·场景序列编排:旧屋揭示")
+    expect(prompt).toContain("爽点/期待点")
+    expect(prompt).toContain("场景戏剧功能")
+    expect(prompt).toContain("对话目标")
+    expect(prompt).toContain("水文")
+    expect(prompt).toContain("开头和结尾")
+    expect(prompt).toContain("只输出一个 JSON 对象")
+    expect(prompt.length).toBeLessThan(760)
+  })
+
+  it("includes compressed project context when provided", () => {
+    const prompt = buildChapterPlanSelfCheckPrompt("维度四·场景序列编排:旧屋揭示", {
+      chapterGoal: "第8章目标:旧屋揭示族谱缺页。",
+      characterStates: "主角谨慎,不知道族谱已被换。",
+      cognitionStates: "主角不知道族谱已经被换过。",
+      foreshadowingStates: "旧钥匙、族谱缺页未回收。",
+      timeline: "雨夜,当晚十点。",
+      canonRules: "主角不能凭空知道旧屋主人身份。",
+      mustAvoid: "不要提前揭露旧屋主人身份。",
+    })
+
+    expect(prompt).toContain("项目上下文核对资料")
+    expect(prompt).toContain("第8章目标:旧屋揭示族谱缺页。")
+    expect(prompt).toContain("主角不知道族谱已经被换过。")
+    expect(prompt).toContain("旧钥匙、族谱缺页未回收。")
+  })
+
+  it("parses structured self-check JSON", () => {
+    const parsed = parseChapterPlanSelfCheckResult(JSON.stringify({
+      status: "warning",
+      summary: "计划基本可用,但缺少字数预算。",
+      issues: [
+        { severity: "warning", problem: "缺少字数预算", risk: "正文篇幅可能失控", suggestion: "补充每个场景的篇幅分配" },
+      ],
+    }))
+
+    expect(parsed.status).toBe("warning")
+    expect(parsed.issues).toHaveLength(1)
+    expect(parsed.formattedText).toContain("状态:warning")
+    expect(parsed.formattedText).toContain("缺少字数预算")
+  })
+
+  it("falls back to raw text when the model does not return JSON", () => {
+    const parsed = parseChapterPlanSelfCheckResult("自检通过:场景序列完整")
+
+    expect(parsed.status).toBe("unknown")
+    expect(parsed.formattedText).toBe("自检通过:场景序列完整")
+  })
+
+  it("runs the self-check model call and returns streamed text", async () => {
+    streamChatMock.mockImplementationOnce(async (_config, messages, callbacks) => {
+      expect(messages[0].content).toContain("计划自检")
+      callbacks.onToken('{"status":"pass","summary":"计划可执行","issues":[]}')
+      callbacks.onDone()
+    })
+
+    await expect(runChapterPlanSelfCheck(llmConfig, "维度四·场景序列编排:旧屋揭示"))
+      .resolves.toBe("状态:pass\n计划可执行")
+  })
+
+  it("builds a revision prompt from plan and self-check result", () => {
+    const prompt = buildChapterPlanRevisionPrompt(
+      "原计划",
+      "状态:warning\n1. [warning] 缺少字数预算\n建议:补充篇幅分配",
+    )
+
+    expect(prompt).toContain("计划修订助手")
+    expect(prompt).toContain("原计划")
+    expect(prompt).not.toContain("蓝图")
+    expect(prompt).toContain("缺少字数预算")
+    expect(prompt).toContain("爽点/期待点")
+    expect(prompt).toContain("对话目标")
+    expect(prompt).toContain("只输出修订后的章节计划")
+    expect(prompt.length).toBeLessThan(320)
+  })
+
+  it("runs plan revision and returns the revised plan", async () => {
+    streamChatMock.mockImplementationOnce(async (_config, messages, callbacks) => {
+      expect(messages[0].content).toContain("计划修订助手")
+      callbacks.onToken("修订后计划")
+      callbacks.onDone()
+    })
+
+    await expect(runChapterPlanRevision(llmConfig, "原计划", "自检建议"))
+      .resolves.toBe("修订后计划")
+  })
+})

+ 218 - 0
src/lib/novel/chapter-plan-self-check.ts

@@ -0,0 +1,218 @@
+import { streamChat } from "@/lib/llm-client"
+import type { LlmConfig } from "@/stores/wiki-store"
+
+export type ChapterPlanSelfCheckStatus = "pass" | "warning" | "error" | "unknown"
+
+export interface ChapterPlanSelfCheckIssue {
+  severity: "warning" | "error" | "info"
+  problem: string
+  risk: string
+  suggestion: string
+}
+
+export interface ParsedChapterPlanSelfCheckResult {
+  status: ChapterPlanSelfCheckStatus
+  summary: string
+  issues: ChapterPlanSelfCheckIssue[]
+  formattedText: string
+}
+
+export interface ChapterPlanSelfCheckContext {
+  chapterGoal?: string
+  characterStates?: string
+  cognitionStates?: string
+  foreshadowingStates?: string
+  timeline?: string
+  canonRules?: string
+  mustAvoid?: string
+}
+
+function buildContextSection(context?: ChapterPlanSelfCheckContext): string {
+  if (!context) return ""
+  const rows = [
+    ["当前章节目标", context.chapterGoal],
+    ["人物状态", context.characterStates],
+    ["角色认知状态", context.cognitionStates],
+    ["伏笔状态", context.foreshadowingStates],
+    ["时间线", context.timeline],
+    ["正史规则", context.canonRules],
+    ["必须避免", context.mustAvoid],
+  ].filter(([, value]) => typeof value === "string" && value.trim())
+
+  if (rows.length === 0) return ""
+  return [
+    "",
+    "项目上下文核对资料:",
+    "请把章节计划逐项对照以下资料,不得只检查计划形式完整性。",
+    ...rows.map(([label, value]) => `${label}:${String(value).trim().slice(0, 1200)}`),
+  ].join("\n")
+}
+
+export function buildChapterPlanSelfCheckPrompt(
+  planContent: string,
+  context?: ChapterPlanSelfCheckContext,
+): string {
+  const contextSection = buildContextSection(context)
+  return [
+    "你是小说章节计划自检助手。",
+    "请轻量检查这份章节计划是否足以指导后续正文生成。",
+    "",
+    "计划自检维度:",
+    "1. 七个维度是否完整:输入校验、章节定位、戏剧问题与信息流、场景序列编排、冲突与人物引擎、边界与禁忌、节奏字数与结尾钩子。",
+    "2. 场景序列能否连成起承转合/钩,是否单场景或缺转场。",
+    "3. 信息流是否写清揭示、隐藏、误导,是否提前泄露角色未知信息。",
+    "4. 伏笔动作是否清楚:埋设、推进、回收分别是什么。",
+    "5. 边界与禁忌能否约束大纲、时间线、角色认知和正史规则。",
+    "6. 结尾钩子是否具体,并自然导向下一章。",
+    "7. 爽点/期待点是否明确:满足什么期待、制造什么新期待。",
+    "8. 场景戏剧功能是否明确:制造/升级/反转/暂解/引出新问题。",
+    "9. 对话目标是否明确:想得到什么、不愿说什么、如何试探/隐瞒/压迫/诱导。",
+    "10. 是否有水文风险:只写气氛/解释/字数,不推动剧情/人物关系/信息差/伏笔/危机。",
+    "11. 开头和结尾是否成立:开头承接上一章并给当前问题;结尾完成阶段结果并留下一章问题。",
+    "",
+    "输出要求:",
+    "1. 只输出一个 JSON 对象,不改计划、不写正文、不输出 markdown 代码块。",
+    "2. 字段:status、summary、issues;status 只能是 pass、warning、error。",
+    "3. summary 用一句中文概括;issues 最多 5 条,每条含 severity、problem、risk、suggestion。",
+    "4. 可确认通过时 status 为 pass,issues 为空数组。",
+    contextSection,
+    "",
+    "待自检章节计划:",
+    planContent.trim(),
+  ].join("\n")
+}
+
+function extractJsonObject(text: string): string | null {
+  const start = text.indexOf("{")
+  const end = text.lastIndexOf("}")
+  if (start < 0 || end <= start) return null
+  return text.slice(start, end + 1)
+}
+
+function normalizeStatus(value: unknown): ChapterPlanSelfCheckStatus {
+  return value === "pass" || value === "warning" || value === "error" ? value : "unknown"
+}
+
+function normalizeSeverity(value: unknown): "warning" | "error" | "info" {
+  return value === "error" || value === "info" || value === "warning" ? value : "warning"
+}
+
+function formatSelfCheckResult(input: {
+  status: ChapterPlanSelfCheckStatus
+  summary: string
+  issues: ChapterPlanSelfCheckIssue[]
+}): string {
+  const lines = [`状态:${input.status}`]
+  if (input.summary) lines.push(input.summary)
+  if (input.issues.length > 0) {
+    lines.push("")
+    input.issues.forEach((issue, index) => {
+      lines.push(`${index + 1}. [${issue.severity}] ${issue.problem}`)
+      if (issue.risk) lines.push(`风险:${issue.risk}`)
+      if (issue.suggestion) lines.push(`建议:${issue.suggestion}`)
+    })
+  }
+  return lines.join("\n")
+}
+
+export function parseChapterPlanSelfCheckResult(text: string): ParsedChapterPlanSelfCheckResult {
+  const raw = text.trim()
+  const jsonText = extractJsonObject(raw)
+  if (!jsonText) {
+    return { status: "unknown", summary: raw, issues: [], formattedText: raw }
+  }
+
+  try {
+    const parsed = JSON.parse(jsonText) as Record<string, unknown>
+    const issues = Array.isArray(parsed.issues)
+      ? parsed.issues.map((item): ChapterPlanSelfCheckIssue => {
+          const obj = typeof item === "object" && item ? item as Record<string, unknown> : {}
+          return {
+            severity: normalizeSeverity(obj.severity),
+            problem: String(obj.problem ?? ""),
+            risk: String(obj.risk ?? ""),
+            suggestion: String(obj.suggestion ?? ""),
+          }
+        }).filter((issue) => issue.problem || issue.risk || issue.suggestion)
+      : []
+    const result = {
+      status: normalizeStatus(parsed.status),
+      summary: String(parsed.summary ?? ""),
+      issues,
+    }
+    return {
+      ...result,
+      formattedText: formatSelfCheckResult(result),
+    }
+  } catch {
+    return { status: "unknown", summary: raw, issues: [], formattedText: raw }
+  }
+}
+
+export async function runChapterPlanSelfCheck(
+  llmConfig: LlmConfig,
+  planContent: string,
+  context?: ChapterPlanSelfCheckContext,
+): Promise<string> {
+  const trimmedPlan = planContent.trim()
+  if (!trimmedPlan) {
+    throw new Error("没有可自检的章节计划")
+  }
+
+  let result = ""
+  let streamError: Error | undefined
+  await streamChat(
+    llmConfig,
+    [{ role: "user", content: buildChapterPlanSelfCheckPrompt(trimmedPlan, context) }],
+    {
+      onToken: (token) => { result += token },
+      onDone: () => {},
+      onError: (error) => { streamError = error },
+    },
+  )
+  if (streamError) throw streamError
+  return parseChapterPlanSelfCheckResult(result.trim()).formattedText || "自检完成,未返回具体结果。"
+}
+
+export function buildChapterPlanRevisionPrompt(planContent: string, selfCheckResult: string): string {
+  return [
+    "你是小说章节计划修订助手。",
+    "请基于自检结果对原章节计划做最小必要修订。",
+    "",
+    "硬性要求:",
+    "1. 只输出修订后的章节计划,不要输出解释、改动说明或正文。",
+    "2. 保留原计划中合理的章节目标、场景序列、人物动机、伏笔动作和结尾钩子。",
+    "3. 只修复自检指出的问题:缺维度、转场不清、信息流矛盾、伏笔不明、边界不足。",
+    "4. 必须补足爽点/期待点、场景戏剧功能、对话目标、开头结尾和水文风险处理。",
+    "5. 修订后的计划仍必须保持七个维度结构。",
+    "",
+    "原章节计划:",
+    planContent.trim(),
+    "",
+    "计划自检结果:",
+    selfCheckResult.trim(),
+  ].join("\n")
+}
+
+export async function runChapterPlanRevision(
+  llmConfig: LlmConfig,
+  planContent: string,
+  selfCheckResult: string,
+): Promise<string> {
+  if (!planContent.trim()) throw new Error("没有可修订的章节计划")
+  if (!selfCheckResult.trim()) throw new Error("没有可用于修订的自检结果")
+
+  let result = ""
+  let streamError: Error | undefined
+  await streamChat(
+    llmConfig,
+    [{ role: "user", content: buildChapterPlanRevisionPrompt(planContent, selfCheckResult) }],
+    {
+      onToken: (token) => { result += token },
+      onDone: () => {},
+      onError: (error) => { streamError = error },
+    },
+  )
+  if (streamError) throw streamError
+  return result.trim() || "修订失败:模型未返回修订计划。"
+}

+ 345 - 0
src/lib/novel/deep-chapter-generation.spec.ts

@@ -142,6 +142,351 @@ describe("runDeepChapterGeneration", () => {
     expect(finalPolishPrompt).toContain("中文小说去 AI 味补充规则")
     expect(finalPolishPrompt).toContain("角色声线")
     expect(finalPolishPrompt).toContain("不要按非虚构文章规则硬删副词")
+    expect(planningPrompt).not.toContain("用户已确认的章节计划")
+    expect(planningPrompt).toContain("章节节奏曲线")
+    expect(planningPrompt).toContain("对话目标")
+    expect(planningPrompt).toContain("爽点/期待点")
+    expect(draftPrompt).toContain("不要写成说明文")
+    expect(draftPrompt).toContain("动作、对话、场景细节、人物反应")
+    expect(draftPrompt).toContain("开头")
+    expect(draftPrompt).toContain("结尾")
+  })
+
+  it("injects the confirmed chapter plan into the brief prompt as an execution summary", () => {
+    const plan = "维度四·场景序列编排:1. 雨夜旧屋揭示线索 2. 屋外脚步声悬念收束"
+    const promptWithPlan = buildDeepChapterBriefPrompt(
+      "",
+      "上下文包内容",
+      "生成第3章",
+      3,
+      undefined,
+      undefined,
+      plan,
+    )
+    const promptWithoutPlan = buildDeepChapterBriefPrompt("", "上下文包内容", "生成第3章", 3)
+
+    expect(promptWithPlan).toContain("用户已确认的章节计划执行摘要")
+    expect(promptWithPlan).toContain(plan)
+    expect(promptWithPlan).toContain("逐条展开 S1/S2/S3")
+    expect(promptWithPlan).toContain("不得合并、跳过或调换顺序")
+    expect(promptWithPlan).toContain("不得推翻")
+    expect(promptWithPlan).not.toContain("蓝图")
+    expect(promptWithoutPlan).not.toContain("用户已确认的章节计划")
+  })
+
+  it("runs a final plan compliance check with the compact execution summary when a confirmed plan is provided", async () => {
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async (
+        _config: LlmConfig,
+        _plan: string,
+        _content: string,
+        _signal?: AbortSignal,
+      ) => "履约度:基本符合"),
+    }
+    const events: Array<{ name: string; result?: string }> = []
+    const fullPlan = [
+      Array.from({ length: 80 }, (_, index) => `普通说明 ${index}:这行只是解释计划来源,不是执行约束。`).join("\n"),
+      "维度四·场景序列编排:旧屋揭示,章末脚步声钩子。",
+      "维度六·伏笔与边界禁忌:推进锈钥匙,不提前揭露旧屋主人。",
+      "维度七·节奏、字数与结尾钩子:结尾停在门外第二个人影。",
+    ].join("\n")
+
+    const result = await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: fullPlan,
+      },
+      { onWorkflowEvent: (event) => events.push(event) },
+      deps,
+    )
+
+    const compliancePlanArg = vi.mocked(deps.runChapterPlanComplianceCheck).mock.calls[0]?.[1] ?? ""
+    expect(deps.runChapterPlanComplianceCheck).toHaveBeenCalledWith(
+      expect.any(Object),
+      expect.stringContaining("用户已确认的章节计划执行摘要"),
+      expect.stringContaining("最终去AI味正文"),
+      undefined,
+    )
+    expect(compliancePlanArg.length).toBeLessThan(fullPlan.length)
+    expect(compliancePlanArg).toContain("旧屋揭示")
+    expect(compliancePlanArg).toContain("锈钥匙")
+    expect(result.planCompliance).toBe("履约度:基本符合")
+    expect(events.some((event) => event.name === "chapter_plan_compliance")).toBe(true)
+  })
+
+  it("publishes final content before waiting for blueprint compliance", async () => {
+    const order: string[] = []
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => {
+        order.push("compliance-start")
+        await Promise.resolve()
+        order.push("compliance-end")
+        return "履约度:基本符合"
+      }),
+    }
+
+    await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末脚步声钩子。",
+      },
+      { onFinalContent: () => order.push("final-content") },
+      deps,
+    )
+
+    expect(order).toEqual(["final-content", "compliance-start", "compliance-end"])
+  })
+
+  it("forwards the stop signal into plan compliance", async () => {
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => "履约度:基本符合"),
+    }
+    const controller = new AbortController()
+
+    await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末脚步声钩子。",
+      },
+      {},
+      deps,
+      controller.signal,
+    )
+
+    expect(deps.runChapterPlanComplianceCheck).toHaveBeenCalledWith(
+      expect.any(Object),
+      expect.stringContaining("确认计划:旧屋揭示,章末脚步声钩子。"),
+      expect.stringContaining("最终去AI味正文"),
+      controller.signal,
+    )
+  })
+
+  it("repairs final content once when plan compliance finds actionable deviations", async () => {
+    const repairedContent = chapterText("计划偏离返修后正文", 3000)
+    const finalContents: string[] = []
+    const activityEvents: AgentActivityEvent[] = []
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => JSON.stringify({
+        status: "partial_deviation",
+        summary: "结尾钩子缺失。",
+        deviations: [{
+          point: "章末钩子",
+          evidence: "正文没有门外第二个人影。",
+          suggestion: "只在结尾补入门外第二个人影,导向下一章。",
+        }],
+      })),
+      runChapterPlanDeviationRepair: vi.fn(async (
+        _config: LlmConfig,
+        plan: string,
+        content: string,
+        compliance: unknown,
+        _signal?: AbortSignal,
+      ) => {
+        expect(plan).toContain("用户已确认的章节计划执行摘要")
+        expect(content).toContain("最终去AI味正文")
+        expect(String(compliance)).toContain("章末钩子")
+        return repairedContent
+      }),
+    }
+
+    const result = await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末必须出现门外第二个人影。",
+      },
+      {
+        onFinalContent: (content) => finalContents.push(content),
+        onActivityEvent: (event) => activityEvents.push(event),
+      },
+      deps,
+    )
+
+    expect(deps.runChapterPlanDeviationRepair).toHaveBeenCalledOnce()
+    expect(result.finalContent).toBe(repairedContent)
+    expect(result.revised).toBe(true)
+    expect(finalContents[0]).toContain("最终去AI味正文")
+    expect(finalContents[finalContents.length - 1]).toBe(repairedContent)
+    const complianceEvent = activityEvents.find((event) => event.stageId === "plan_compliance")
+    expect(complianceEvent?.content).toContain("履约状态:部分偏离")
+    expect(complianceEvent?.content).toContain("偏离点数量:1")
+    expect(complianceEvent?.content).toContain("处理决定:触发轻量返修")
+    expect(complianceEvent?.content).toContain("章末钩子")
+    const repairEvent = activityEvents.find((event) => event.stageId === "plan_deviation_repair")
+    expect(repairEvent?.content).toContain("正文已更新")
+    expect(repairEvent?.content).toContain("返修前")
+    expect(repairEvent?.content).toContain("返修后")
+  })
+
+  it("does not repair final content when plan compliance is mostly compliant", async () => {
+    const activityEvents: AgentActivityEvent[] = []
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => "履约度:基本符合"),
+      runChapterPlanDeviationRepair: vi.fn(async () => chapterText("不应出现的返修正文", 3000)),
+    }
+
+    const result = await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末脚步声钩子。",
+      },
+      { onActivityEvent: (event) => activityEvents.push(event) },
+      deps,
+    )
+
+    expect(deps.runChapterPlanDeviationRepair).not.toHaveBeenCalled()
+    expect(result.finalContent).toContain("最终去AI味正文")
+    const complianceEvent = activityEvents.find((event) => event.stageId === "plan_compliance")
+    expect(complianceEvent?.content).toContain("履约状态:基本符合")
+    expect(complianceEvent?.content).toContain("处理决定:无需返修")
+  })
+
+  it("keeps the original final content when plan deviation repair returns abnormal content", async () => {
+    const finalContents: string[] = []
+    const activityEvents: AgentActivityEvent[] = []
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => JSON.stringify({
+        status: "clear_deviation",
+        summary: "章末钩子缺失。",
+        deviations: [{
+          point: "章末钩子",
+          evidence: "正文没有门外第二个人影。",
+          suggestion: "只在结尾补入门外第二个人影。",
+        }],
+      })),
+      runChapterPlanDeviationRepair: vi.fn(async () => "短正文"),
+    }
+
+    const result = await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末必须出现门外第二个人影。",
+      },
+      {
+        onFinalContent: (content) => finalContents.push(content),
+        onActivityEvent: (event) => activityEvents.push(event),
+      },
+      deps,
+    )
+
+    expect(deps.runChapterPlanDeviationRepair).toHaveBeenCalledOnce()
+    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).not.toBe("短正文")
+    expect(finalContents).toHaveLength(1)
+    const repairEvent = activityEvents.find((event) => event.stageId === "plan_deviation_repair")
+    expect(repairEvent?.content).toContain("返修结果异常,已保留原正文")
+    expect(repairEvent?.content).toContain("原因:返修后正文明显变短")
+  })
+
+  it("keeps the original final content when plan deviation repair becomes too long", async () => {
+    const activityEvents: AgentActivityEvent[] = []
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => JSON.stringify({
+        status: "partial_deviation",
+        summary: "缺少一个结尾动作。",
+        deviations: [{ point: "结尾动作", evidence: "未出现人影。", suggestion: "补入人影。" }],
+      })),
+      runChapterPlanDeviationRepair: vi.fn(async () => chapterText("返修异常长正文", 5200)),
+    }
+
+    const result = await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末必须出现门外第二个人影。",
+      },
+      { onActivityEvent: (event) => activityEvents.push(event) },
+      deps,
+    )
+
+    expect(result.finalContent).toContain("最终去AI味正文")
+    const repairEvent = activityEvents.find((event) => event.stageId === "plan_deviation_repair")
+    expect(repairEvent?.content).toContain("原因:返修后正文明显变长")
+  })
+
+  it("keeps the original final content when plan deviation repair drops the original main content", async () => {
+    const activityEvents: AgentActivityEvent[] = []
+    const unrelatedRepair = Array.from({ length: 90 }, (_, index) =>
+      `全新段落${index}:这里改写成完全不同的事件、地点、人物和线索,绕开旧屋、钥匙、脚步声。`,
+    ).join("\n")
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => JSON.stringify({
+        status: "partial_deviation",
+        summary: "缺少一个结尾动作。",
+        deviations: [{ point: "结尾动作", evidence: "未出现人影。", suggestion: "补入人影。" }],
+      })),
+      runChapterPlanDeviationRepair: vi.fn(async () => unrelatedRepair),
+    }
+
+    const result = await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末必须出现门外第二个人影。",
+      },
+      { onActivityEvent: (event) => activityEvents.push(event) },
+      deps,
+    )
+
+    expect(result.finalContent).toContain("最终去AI味正文")
+    const repairEvent = activityEvents.find((event) => event.stageId === "plan_deviation_repair")
+    expect(repairEvent?.content).toContain("原因:返修后未保留原正文主要内容")
+  })
+
+  it("explains why unknown plan compliance results do not trigger repair", async () => {
+    const activityEvents: AgentActivityEvent[] = []
+    const deps = {
+      ...createDeps(),
+      runChapterPlanComplianceCheck: vi.fn(async () => "模型输出混乱,未给出履约度。"),
+      runChapterPlanDeviationRepair: vi.fn(async () => chapterText("不应出现的返修正文", 3000)),
+    }
+
+    await runDeepChapterGeneration(
+      {
+        projectPath: "E:/Novel",
+        userRequest: "生成第3章",
+        chapterNumber: 3,
+        llmConfig,
+        planBlueprint: "确认计划:旧屋揭示,章末脚步声钩子。",
+      },
+      { onActivityEvent: (event) => activityEvents.push(event) },
+      deps,
+    )
+
+    expect(deps.runChapterPlanDeviationRepair).not.toHaveBeenCalled()
+    const complianceEvent = activityEvents.find((event) => event.stageId === "plan_compliance")
+    expect(complianceEvent?.content).toContain("履约状态:未知")
+    expect(complianceEvent?.content).toContain("处理决定:未触发返修")
+    expect(complianceEvent?.content).toContain("原因:模型未按结构返回,已避免误改正文")
   })
 
   it("enables the multi-task generation loop for all chapter writing routes", () => {

+ 207 - 6
src/lib/novel/deep-chapter-generation.ts

@@ -14,7 +14,6 @@ import {
   withReasoningDisabled,
 } from "@/lib/reasoning-retry";
 import { computeNovelContextTokenBudget } from "@/lib/context-budget";
-import { resolveNovelModel } from "./model-resolver";
 import {
   buildContextPack,
   contextPackToPrompt,
@@ -23,6 +22,14 @@ import {
 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 {
   resolveChapterLengthSpec,
   type ChapterLengthSpec,
@@ -43,6 +50,8 @@ export interface DeepChapterGenerationInput {
   llmConfig: LlmConfig;
   aiWorkflowMode?: AiWorkflowMode;
   resumeCheckpoint?: DeepChapterGenerationResumeCheckpoint;
+  /** 用户在会话层确认的章节计划,作为写作任务书的权威依据注入 brief 阶段。 */
+  planBlueprint?: string;
 }
 
 export interface DeepChapterGenerationCallbacks {
@@ -59,6 +68,7 @@ export interface DeepChapterGenerationResult {
   draftContent: string;
   reviewResults: NovelReviewResult[];
   revised: boolean;
+  planCompliance?: string;
 }
 
 export type ChapterWorkflowEventType = "started" | "completed" | "error";
@@ -96,6 +106,8 @@ export interface DeepChapterGenerationDeps {
   buildContextPack: typeof buildContextPack;
   contextPackToPrompt: typeof contextPackToPrompt;
   reviewChapter: typeof reviewChapter;
+  runChapterPlanComplianceCheck?: typeof runChapterPlanComplianceCheck;
+  runChapterPlanDeviationRepair?: typeof runChapterPlanDeviationRepair;
   streamChat: (
     config: LlmConfig,
     messages: ChatMessage[],
@@ -411,12 +423,12 @@ export async function runDeepChapterGeneration(
   const workflowProfile = resolveChapterWorkflowProfile(input.aiWorkflowMode);
   const lengthSpec = resolveCurrentChapterLengthSpec();
   const novelConfig = useWikiStore.getState().novelConfig;
-  const deAiConfig = resolveNovelModel(input.llmConfig, novelConfig, "deAi");
   const { loadSmartDeAiSkill } = await import("./de-ai-adapter");
   const workflowBaseParams = {
     mode: workflowProfile.mode,
     chapterNumber: input.chapterNumber ?? null,
   };
+  const planExecutionSummary = buildChapterPlanExecutionSummary(input.planBlueprint ?? "");
   const contextWorkflowStep: ChapterWorkflowStepSpec = {
     name: "chapter_context",
     title: "读取上下文",
@@ -659,6 +671,7 @@ export async function runDeepChapterGeneration(
                 input.chapterNumber,
                 input.goldenThreeChapter,
                 lengthSpec,
+                planExecutionSummary,
               ),
             },
           ],
@@ -879,14 +892,14 @@ export async function runDeepChapterGeneration(
               input.projectPath,
               draftContent,
               input.chapterNumber,
-              { onThinking: callbacks.onThinking, contextPack },
+              { onThinking: callbacks.onThinking, contextPack, planBlueprint: planExecutionSummary },
               signal,
             )
           : await deps.reviewChapter(
               input.projectPath,
               draftContent,
               input.chapterNumber,
-              { onThinking: callbacks.onThinking, contextPack },
+              { onThinking: callbacks.onThinking, contextPack, planBlueprint: planExecutionSummary },
             );
       } catch (err) {
         console.error("[Deep Chapter] Review failed:", err);
@@ -1125,7 +1138,7 @@ export async function runDeepChapterGeneration(
     detail: "做最后一遍简单审查,减少复读、机械套话和 AI 味。",
     params: workflowBaseParams,
   };
-  const finalContent = workflowProfile.runFinalPolish
+  let finalContent = workflowProfile.runFinalPolish
     ? await runChapterWorkflowStep(
         callbacks,
         finalPolishWorkflowStep,
@@ -1174,6 +1187,108 @@ export async function runDeepChapterGeneration(
     title: "最终正文",
     content: `最终正文已生成,约 ${countChapterChars(finalContent)} 字。`,
   });
+  callbacks.onFinalContent?.(finalContent);
+  let planCompliance = "";
+  if (planExecutionSummary.trim()) {
+    let complianceCheckFailed = false;
+    const complianceStep = {
+      name: "chapter_plan_compliance",
+      title: "检查计划履约度",
+      detail: "对照用户确认的章节计划检查最终正文是否按计划执行。",
+      params: workflowBaseParams,
+    };
+    try {
+      const runCompliance = deps.runChapterPlanComplianceCheck || runChapterPlanComplianceCheck;
+      planCompliance = await runChapterWorkflowStep(
+        callbacks,
+        complianceStep,
+        () => runCompliance(writingConfig, planExecutionSummary, finalContent, signal),
+        (value) => value ? "计划履约度检查完成。" : "计划履约度检查完成,未返回具体结果。",
+        (value) => ({ hasComplianceResult: Boolean(value?.trim()) }),
+      );
+    } catch (error) {
+      complianceCheckFailed = true;
+      planCompliance = `计划履约度检查失败:${error instanceof Error ? error.message : String(error)}`;
+      completeChapterWorkflowStep(
+        callbacks,
+        complianceStep,
+        planCompliance,
+        { error: true },
+      );
+    }
+    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 {
+          const repairedContent = await runChapterWorkflowStep(
+            callbacks,
+            repairStep,
+            () => runRepair(writingConfig, planExecutionSummary, finalContent, planCompliance, signal),
+            (value) => {
+              const validation = validateChapterPlanRepairResult(finalContent, value);
+              return validation.accepted
+                ? `计划偏离点返修完成,正文约 ${countChapterChars(value)} 字。`
+                : `计划偏离点返修结果异常,已保留原正文。原因:${validation.reason}。`;
+            },
+            (value) => ({
+              chars: countChapterChars(value),
+              changed: validateChapterPlanRepairResult(finalContent, value).accepted,
+            }),
+          );
+          const validation = validateChapterPlanRepairResult(finalContent, repairedContent);
+          const beforeChars = countChapterChars(finalContent);
+          if (validation.accepted) {
+            finalContent = repairedContent.trim();
+            revised = true;
+            const afterChars = countChapterChars(finalContent);
+            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 {
+            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) {
+          console.error("[Deep Chapter] 计划偏离点返修失败:", error);
+        }
+      }
+    }
+  }
   completeChapterWorkflowStep(
     callbacks,
     {
@@ -1188,13 +1303,13 @@ export async function runDeepChapterGeneration(
       revised,
     },
   );
-  callbacks.onFinalContent?.(finalContent);
   return {
     finalContent,
     taskBrief,
     draftContent,
     reviewResults,
     revised,
+    planCompliance,
   };
 }
 
@@ -1387,6 +1502,92 @@ 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 validateChapterPlanRepairResult(
+  originalContent: string,
+  repairedContent: string,
+): { accepted: boolean; reason: string } {
+  const repaired = repairedContent.trim();
+  if (!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 {
   if (signal?.aborted) throw new Error(USER_ABORT_MESSAGE);
 }

+ 37 - 7
src/lib/novel/deep-chapter-prompts.ts

@@ -67,23 +67,50 @@ export function buildDeepChapterBriefPrompt(
   chapterNumber?: number,
   goldenThreeChapter?: GoldenThreeChapterRequest,
   lengthSpec: ChapterLengthSpec = DEFAULT_CHAPTER_LENGTH_SPEC,
+  planBlueprint?: string,
 ): string {
+  const blueprintSection = planBlueprint && planBlueprint.trim()
+    ? [
+        "",
+        "## 用户已确认的章节计划执行摘要",
+        "以下计划摘要来自用户确认的完整章节计划,是本阶段写作任务书的权威依据。",
+        "严格遵循计划中的场景序列、信息流、伏笔动作、边界禁忌与结尾钩子;不得推翻或新增冲突情节,只补执行细节。",
+        "",
+        planBlueprint.trim(),
+      ].join("\n")
+    : ""
+
+  const planningDirectives = planBlueprint && planBlueprint.trim()
+    ? [
+        "硬性要求:",
+        "1. 只输出任务书,不要写故事片段。",
+        "2. 以用户确认的计划为骨架逐场景落地:必须完成、禁止违背、角色状态、伏笔推进、结尾钩子都与计划一致。",
+        "3. 若计划摘要含 S1/S2/S3,任务书必须逐条展开 S1/S2/S3,不得合并、跳过或调换顺序。",
+        "4. 不新增计划未涵盖的主线推进、伏笔动作或人物变化;计划有缺失时只给最小补全方向。",
+        `5. 后续正文必须按完整章节规划,${chapterLengthBoundary(lengthSpec)}`,
+        "6. 任务书必须覆盖场景推进、冲突升级、人物互动、细节描写、章节节奏曲线、爽点/期待点、对话目标和开头/结尾执行要求。",
+      ].join("\n")
+    : [
+        "硬性要求:",
+        "1. 只输出任务书,不要写故事片段。",
+        "2. 必须列出本章必须完成、禁止违背、角色状态、伏笔推进、结尾钩子。",
+        "3. 如果上下文不足,写明缺失项,并给出最小补全方向。",
+        `4. 后续正文必须按完整章节规划,${chapterLengthBoundary(lengthSpec)}`,
+        "5. 任务书必须覆盖场景推进、冲突升级、人物互动、细节描写、章节节奏曲线、爽点/期待点、主要对话目标、开头承接方式和结尾钩子执行方式。",
+      ].join("\n")
+
   return [
     buildStableContextPrefix(outline, contextPrompt),
     "",
     "你是小说写作任务规划助手。",
     "请基于上述上下文输出一份写作任务书,供后续创作使用。",
     "",
-    "硬性要求:",
-    "1. 只输出任务书,不要写故事片段。",
-    "2. 必须列出本章必须完成、禁止违背、角色状态、伏笔推进、结尾钩子。",
-    "3. 如果上下文不足,写明缺失项,并给出最小补全方向。",
-    `4. 后续正文必须按完整章节规划,${chapterLengthBoundary(lengthSpec)}`,
-    "5. 任务书必须规划足够的场景推进、冲突升级、人物互动、细节描写和结尾钩子,避免只写一个短场景。",
+    planningDirectives,
     "",
     chapterNumber ? `目标章节:第${chapterNumber}章` : "目标章节:用户请求中的章节",
     `用户请求:${userRequest}`,
     goldenThreeChapterSection(goldenThreeChapter),
+    blueprintSection,
   ].filter(Boolean).join("\n")
 }
 
@@ -109,8 +136,11 @@ export function buildDeepChapterDraftPrompt(
     "4. 严格承接上一章结尾,遵守大纲、记忆、人设、伏笔和时间线。",
     "5. 结尾必须留下适合下一章继续推进的钩子。",
     `6. 字数必须接近完整章节长度:${chapterLengthBoundary(lengthSpec)}阶段3正文草稿最多 ${lengthSpec.draftMaxChars} 字,写到完整结尾后立即停止;不能提前收尾,也不能为了补细节新增额外场景。`,
-    "7. 必须写成完整章节,不要只写一个片段;需要包含场景铺陈、行动推进、对话交锋、情绪变化、冲突升级和结尾钩子。",
+    "7. 必须写成完整章节,不要只写片段;包含场景铺陈、行动推进、对话交锋、情绪变化、冲突升级和结尾钩子。",
     "8. 禁止复读、循环输出、重复同一段落或用相同句式堆字数;写到完整结尾后立即停止。",
+    "9. 不要写成说明文:不解释设计、不替角色总结动机、不用旁白概括冲突;信息必须通过动作、对话、场景细节、人物反应呈现。",
+    "10. 开头承接上一章并立刻给当前问题;结尾完成阶段结果,并留下下一章必须解决的动作、信息或危险。",
+    "11. 对话必须有目标和攻防,通过试探、隐瞒、压迫、诱导或回避推动关系或信息状态变化,禁止无用闲聊。",
     "",
     chapterNumber ? `目标章节:第${chapterNumber}章` : "目标章节:用户请求中的章节",
     `用户请求:${userRequest}`,

+ 18 - 0
src/lib/novel/review-adapter.spec.ts

@@ -107,6 +107,24 @@ describe("review-adapter staged review", () => {
     expect(prompt).toContain("当前伏笔状态:旧钥匙、族谱缺页、门缝冷光都未回收。")
   })
 
+  it("injects the confirmed plan blueprint as a deviation-check constraint", () => {
+    const blueprint = "维度四·场景序列编排:1. 旧屋揭示 2. 脚步声悬念收束"
+    const prompt = buildReviewPrompt(contextPack, "主角直接说出族谱被换。", false, blueprint)
+
+    expect(prompt).toContain("用户已确认的章节计划")
+    expect(prompt).toContain(blueprint)
+    expect(prompt).toContain("偏离即 error")
+    expect(prompt).toContain("是否偏离用户已确认的章节计划")
+    expect(prompt).not.toContain("章节蓝图")
+  })
+
+  it("does not inject blueprint deviation dimension when no blueprint is provided", () => {
+    const prompt = buildReviewPrompt(contextPack, "主角直接说出族谱被换。")
+
+    expect(prompt).not.toContain("用户已确认的章节计划")
+    expect(prompt).not.toContain("是否偏离用户已确认的章节计划")
+  })
+
   it("runs a single merged deep review with high reasoning and publishes thinking", async () => {
     llmConfig.reasoning = { mode: "off" }
     streamChatMock.mockImplementation(async (

+ 34 - 3
src/lib/novel/review-adapter.ts

@@ -33,6 +33,11 @@ export interface ReviewChapterOptions extends NovelReviewCallbacks {
    * 用于返修后复审,降低 token 消耗。默认 false 走全量审查。
    */
   characterOnly?: boolean
+  /**
+   * 用户在会话层确认的章节计划。提供时,审稿会额外检查正文是否偏离计划的
+   * 场景序列、信息流、伏笔动作和结尾钩子,偏离按 error 标记。
+   */
+  planBlueprint?: string
 }
 
 /** 角色一致性相关的审查维度,用于 characterOnly 轻量审查模式 */
@@ -65,6 +70,9 @@ const REVIEW_DIMENSIONS = [
   "是否缺少章节钩子",
 ]
 
+/** 当传入用户确认的章节计划时追加的审查维度 */
+const BLUEPRINT_DEVIATION_DIMENSION = "是否偏离用户已确认的章节计划(场景序列、信息流、伏笔动作、结尾钩子)"
+
 const REVIEW_STAGES = [
   "阶段1:审查任务识别",
   "阶段2:上下文检索",
@@ -95,12 +103,33 @@ function splitChapterForReview(content: string): string[] {
   return chunks
 }
 
-export function buildReviewPrompt(pack: ContextPack, chapterContent: string, characterOnly = false): string {
-  const dimensions = characterOnly ? CHARACTER_REVIEW_DIMENSIONS : REVIEW_DIMENSIONS
+export function buildReviewPrompt(
+  pack: ContextPack,
+  chapterContent: string,
+  characterOnly = false,
+  planBlueprint?: string,
+): string {
+  const baseDimensions = characterOnly ? CHARACTER_REVIEW_DIMENSIONS : REVIEW_DIMENSIONS
+  // 当传入用户确认的计划时,追加计划偏离维度(characterOnly 模式下不追加,保持轻量)
+  const dimensions = !characterOnly && planBlueprint && planBlueprint.trim()
+    ? [...baseDimensions, BLUEPRINT_DEVIATION_DIMENSION]
+    : baseDimensions
   const modeTitle = characterOnly ? "角色一致性专项审查" : "阶段式深度审查工作流"
   const modeStages = characterOnly
     ? ["阶段1:角色提取", "阶段2:记忆库对照", "阶段3:脱离判定", "阶段4:二次复核"]
     : REVIEW_STAGES
+  const blueprintSection = planBlueprint && planBlueprint.trim()
+    ? [
+        "",
+        "用户已确认的章节计划(偏离即 error):",
+        "正文必须遵循以下计划中的场景序列、信息流设计、伏笔动作和结尾钩子。",
+        "若正文在计划覆盖的维度上出现偏离(场景被跳过/互换、信息泄露与计划信息差矛盾、",
+        "伏笔动作未执行或执行方向相反、结尾钩子与计划设计不一致),必须标为 error,",
+        "evidence 引用正文偏离片段,relatedMemory 引用计划原文。",
+        "",
+        planBlueprint.trim(),
+      ].join("\n")
+    : ""
   return `${contextPackToPrompt(pack)}
 
 ${modeTitle}:
@@ -135,6 +164,8 @@ ${characterOnly ? "" : `${i18n.t("novel.reviewPrompt.specialChecksTitle")}
 `}
 
 角色命中记忆库检查(必须执行):
+${blueprintSection}
+
 1. 角色提取:先从本章正文中提取所有出现的角色名(含别名、昵称),列出角色清单。
 2. 记忆库对照:逐个角色对照上下文中的"角色光环/灵魂"、"人物状态"、"角色认知状态"字段:
    - 标注该角色是否命中记忆库(已注入光环 / 仅有状态 / 完全缺失)。
@@ -229,7 +260,7 @@ ${langReminder}`
       const chunkContent = chunks.length > 1
         ? `【第${i + 1}段/共${chunks.length}段】\n${chunk}`
         : chunk
-      const userPrompt = buildReviewPrompt(contextPack, chunkContent, options.characterOnly)
+      const userPrompt = buildReviewPrompt(contextPack, chunkContent, options.characterOnly, options.planBlueprint)
       const stageTitle = chunks.length > 1
         ? (options.characterOnly ? `角色一致性审查(第${i + 1}/${chunks.length}段)` : `深度审查(第${i + 1}/${chunks.length}段)`)
         : (options.characterOnly ? "角色一致性审查" : "深度审查")

+ 3 - 3
src/lib/novel/skill-seed.ts

@@ -49,7 +49,7 @@ const nextChapterPlanContent = `# 下一章计划
 
 ## 核心目标
 
-在本章正文动笔之前,生成一份结构化、可执行的章节蓝图,明确本章要写什么、为什么写、怎么写。这不仅是提纲,更是对"这一章存在的理由"的确认。
+在本章正文动笔之前,生成一份结构化、可执行的章节计划,明确本章要写什么、为什么写、怎么写。这不仅是提纲,更是对"这一章存在的理由"的确认。
 
 ## 适用场景
 
@@ -1611,7 +1611,7 @@ export const DEFAULT_BUILTIN_WRITING_SKILLS: UserSkill[] = [
   normalizeUserSkill({
     id: "builtin:next-chapter-plan",
     name: "下一章计划",
-    description: "生成包含目标、冲突、人物、事件、情绪、伏笔、钩子的完整章节蓝图。",
+    description: "生成包含目标、冲突、人物、事件、情绪、伏笔、钩子的完整章节计划。",
     kind: ["planning"],
     stages: ["planning"],
     modes: ["standard", "strict"],
@@ -1846,4 +1846,4 @@ export const DEFAULT_BUILTIN_WRITING_SKILLS: UserSkill[] = [
 
 export function getBuiltinSkillIds(): Set<string> {
   return new Set(DEFAULT_BUILTIN_WRITING_SKILLS.map((s) => s.id))
-}
+}

+ 179 - 0
zhangjielandu-分支说明.md

@@ -0,0 +1,179 @@
+# zhangjielandu 分支说明(章节计划分析提示词)
+
+## 分支用途
+
+本分支用于实现"AI 会话计划执行"的章节计划分析提示词升级,并把用户确认的计划打通到正文生成的写作任务书链路。
+
+核心目标:把会话层章节计划从"字段清单"升级为"七维度分析决策计划",去除三档模式裁剪,让用户确认的计划真正作为写作任务书的权威依据驱动章节生成。
+
+## 使用要求
+
+1. 本分支独立开发,基于 main 的 HEAD 创建,使用 git worktree 物理隔离。
+2. 所有改动只在本 worktree(.worktrees/zhangjielandu)内进行,不得直接修改 main 工作区的未提交改动。
+3. 改动遵循最小侵入原则,不重构无关代码,不删除已有函数。
+4. 所有面向用户的提示语使用中文。
+5. 不破坏旧功能:未传 planBlueprint 时,章节生成链路与原行为完全一致。
+
+## 改动文件清单
+
+- src/lib/agent/plugins/build-system-prompt-plugin.ts
+  - buildChapterPlanProtocol 重写为统一七维度蓝图模板(输入校验/章节定位/戏剧问题与信息流/场景序列编排/冲突与人物引擎/边界与禁忌/节奏字数与结尾钩子),去除 fast/standard/strict 三档分支,mode 仅用于标注工作流强度。
+- src/lib/agent/plugins/build-system-prompt-plugin.spec.ts
+  - 更新断言:去掉"当前为标准模式",改为断言七维度蓝图标记与 planBlueprint。
+- src/lib/novel/deep-chapter-prompts.ts
+  - buildDeepChapterBriefPrompt 增加 planBlueprint 可选参数;传入时注入"用户已确认的章节蓝图"段并切换为以蓝图为权威依据的硬性要求。
+- src/lib/novel/deep-chapter-generation.ts
+  - DeepChapterGenerationInput 增加 planBlueprint 字段;brief 阶段调用 buildDeepChapterBriefPrompt 时透传。
+- src/lib/agent/tools/run-chapter-workflow.ts
+  - RunChapterWorkflowParams 增加 planBlueprint;工具 parameters 声明 planBlueprint;execute 透传到 runDeepChapterGeneration 的 input。
+- src/lib/agent/tools/run-chapter-workflow.spec.ts
+  - 新增测试:验证 planBlueprint 被透传到 deep chapter generation。
+- src/lib/novel/deep-chapter-generation.spec.ts
+  - 新增测试:验证蓝图注入 brief 提示词;无蓝图时不注入。
+- src/components/chat/chapter-plan-confirm-dialog.tsx
+  - buildPlanConfirmMessage 增强:确认后要求 AI 把蓝图原文作为 planBlueprint 传入 run_chapter_workflow,不再只说"按计划写正文"。
+  - 增加“自检蓝图”按需按钮与结果展示区;用户点击后才触发轻量蓝图自检,避免每次计划都增加模型调用。
+- src/lib/novel/chapter-plan-self-check.ts
+  - 新增章节蓝图自检提示词与模型调用封装,chat-panel 不直接调用 streamChat,保持现有会话架构边界。
+- src/lib/novel/chapter-plan-self-check.spec.ts
+  - 新增自检提示词与流式返回聚合测试。
+
+## 验证方式
+
+- npm run typecheck
+- npm run test:mocks(含 build-system-prompt-plugin / run-chapter-workflow / deep-chapter-generation 相关测试)
+- 旧功能回归:未开启 Plan Execute 或未传 planBlueprint 时,章节生成行为与改动前一致。
+
+## 提交记录
+
+- 未提交。当前改动停留在 worktree 工作区,等待用户确认后再决定是否提交与合并。
+
+## 合并说明
+
+本分支基于 main HEAD(3525817)创建,与 main 工作区现有未提交改动(chat-message.tsx、Cargo.lock 等)和其他分支(zed-ui-redesign、huihualishianniu 等)物理隔离,互不影响。
+
+合并回 main 时:在 main 工作区执行 `git merge zhangjielandu`。该分支只触及章节蓝图与正文生成链路,与 UI 重构分支无文件重叠,预期无冲突或仅极小冲突。
+
+## 更新内容
+
+### 20260705
+- 初版实现:七维度蓝图分析提示词 + planBlueprint 透传链路。
+- 去除三档模式裁剪,统一完整七维度。
+- 打通会话层确认蓝图 → 写作任务书 → 正文生成的闭环。
+
+### 20260705(第二轮:方向1 + 方向2 闭环增强)
+- 方向1·审稿闭环:reviewChapter 增加 planBlueprint 参数与蓝图偏离审查维度;审稿提示词注入"用户已确认的章节蓝图(偏离即 error)"段。返修/去AI味阶段通过 taskBrief 间接继承蓝图约束(brief 阶段已把蓝图固化为 taskBrief 权威依据)。
+- 方向2·强制注入:run-chapter-workflow 工具增加 getPlanBlueprint 兜底 getter;useAgentConfig 透传 getPlanBlueprint 到工具工厂;chat-panel 用 confirmedBlueprintRef 存已确认蓝图,工具执行时若 AI 未带 planBlueprint 参数则从 ref 兜底注入。确认后 followup 发送完毕清除 ref,避免误注入。
+- 新增测试:审稿蓝图注入/不注入、getPlanBlueprint 兜底注入、AI传入优先于兜底。
+
+### 20260705(第三轮:方向3 按需蓝图自检)
+- 按用户选择 B 实现“按需自检”:章节计划弹窗内增加“自检蓝图”按钮,点击后才调用当前 AI 会话模型进行蓝图自检。
+- 自检提示词检查:七维度完整性、场景序列、信息流、伏笔动作、边界禁忌、结尾钩子。
+- 自检调用下沉到 src/lib/novel/chapter-plan-self-check.ts,避免 chat-panel 直接 await streamChat,保持 ReAct 会话入口架构不被破坏。
+- 新增测试:自检按钮展示、点击后结果显示、自检提示词、自检流式返回聚合。
+
+### 20260705(第四轮:稳定性与结构化自检)
+- confirmedBlueprintRef 清理改为 try/finally,followup 发送成功、失败或被中断都会清理已确认蓝图,避免后续无关工具调用误注入旧蓝图。
+- 章节计划弹窗增加自检请求编号防护:自检期间关闭弹窗或重新打开时,旧请求返回后不会污染新弹窗状态。
+- 自检模型输出改为结构化 JSON:status、summary、issues;库层解析后格式化展示。若模型返回纯文本,则原样兜底显示。
+- 新增测试:try/finally 清理源码约束、关闭弹窗后旧自检结果不回流、结构化自检解析、纯文本兜底。
+
+### 20260705(第五轮:上下文自检 + 蓝图修订 + 履约度)
+- 方向1·上下文感知自检:章节蓝图自检支持传入压缩后的 ContextPack 字段(chapterGoal、characterStates、cognitionStates、foreshadowingStates、timeline、canonRules、mustAvoid),自检不再只检查蓝图形式,也会对照真实项目资料。
+- 方向2·按自检建议修订蓝图:章节计划弹窗在自检结果出现后显示“按自检建议修正”按钮;点击后调用 AI 基于原蓝图和自检结果生成修订版,并进入编辑状态,仍由用户最终确认。
+- 方向3·蓝图履约度检查:章节工作流在最终正文生成后,若存在用户确认的 planBlueprint,会执行一次蓝图履约度检查,结果写入 DeepChapterGenerationResult.planCompliance,并透出到 run_chapter_workflow 工具结果。
+- 新增文件:src/lib/novel/chapter-plan-compliance.ts / .spec.ts。
+- 新增测试:上下文自检 prompt、蓝图修订 prompt/调用、履约度检查 prompt/调用、deep chapter workflow 履约度返回、工具结果包含履约度。
+
+### 20260705(第六轮:章节正文质量维度增强)
+- 围绕“生成更好的小说章节内容”边界,增强蓝图协议、任务书、正文草稿、自检和履约度检查提示词。
+- 新增质量维度:爽点/期待点设计、场景戏剧功能、对话目标、开头与结尾单独约束、水文风险检查。
+- 正文草稿提示词新增“不要写成说明文”约束:信息必须通过动作、对话、场景细节和人物反应呈现,避免旁白解释剧情设计或总结角色动机。
+- 自检和履约度检查同步检查:是否只有信息推进、场景是否推动剧情/人物关系/信息差/伏笔/危机、对话是否改变关系或信息状态。
+- 新增测试断言覆盖上述提示词关键词,防止后续改动误删章节质量要求。
+
+### 20260705(第七轮:提示词瘦身与优先级重排)
+- 压缩蓝图协议、自检、履约度检查和正文阶段提示词的重复表达,降低提示词堆叠导致正文生成空间被挤占的风险。
+- 明确优先级:蓝图阶段负责设计,任务书阶段负责逐场景落地,正文阶段只保留硬执行规则。
+- 保留直接影响章节质量的边界:爽点/期待点、场景戏剧功能、对话目标、开头与结尾、水文风险、不要写成说明文。
+- 新增提示词体量回归约束:章节蓝图协议和自检/履约检查提示词必须保持在较紧凑范围内,避免后续继续膨胀。
+- 已验证:build-system-prompt-plugin、deep-chapter-generation、chapter-plan-self-check、chapter-plan-compliance 相关测试通过。
+
+### 20260705(第八轮:typecheck 阻断清理与打包)
+- 清理 App、chat-message、chat-panel、outline-chat-panel、deep-chapter-generation 中未使用的 import/变量,解除 TypeScript noUnusedLocals 阻断。
+- 验证通过:npm run typecheck、npm run build、章节蓝图相关 vitest 回归测试。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe;version-info.json 显示版本 2.2.33,包含 pdfium 和 skills。
+- 补充验证:npm run test:mocks 仍有 8 个失败,集中在 outline-chat-panel 源码字符串断言、chat-message 源码字符串断言、unified-skill-library-view、story-simulation debug 文案断言、release-notes 当前版本日志断言;与本次章节蓝图链路和 typecheck 清理无直接文件重叠,暂不在本分支扩大修复范围。
+- 未提交 git,未合并 main。
+
+### 20260705(第九轮:履约检查非阻塞正文回调)
+- 调整 deep chapter 生成顺序:最终正文生成后立即触发 onFinalContent,再执行蓝图履约度检查,避免履约检查慢或失败时拖慢正文展示。
+- runChapterPlanComplianceCheck 增加 AbortSignal 透传,用户停止生成时后置履约检查也能被中止。
+- 新增回归测试:最终正文回调早于履约检查完成;履约检查接收 stop signal;底层 streamChat 收到 signal。
+- 验证通过:deep-chapter-generation/chapter-plan-compliance 目标测试、章节蓝图相关回归测试、npm run typecheck、npm run build、npm run build:portable。
+- 未提交 git,未合并 main。
+
+### 20260705(第十轮:章节计划命名与执行摘要)
+- 按用户要求将用户可见文案、模型提示词、工具结果和内置技能描述中的“蓝图”改为“计划”;内部 `planBlueprint` 参数名暂保留,避免扩大 API 兼容风险。
+- 新增计划执行摘要:完整确认计划仍由会话层和工具层保留,正文生成 brief、AI 审稿和计划履约度检查使用压缩后的“章节计划执行摘要”,降低长计划挤占上下文导致正文丢失的风险。
+- 审稿阶段补充传入计划执行摘要,正文生成过程能在审稿阶段检查是否偏离用户确认计划。
+- 新增文件:src/lib/novel/chapter-plan-execution-summary.ts / .spec.ts。
+- 验证通过:章节计划相关回归测试、npm run typecheck、npm run build、npm run build:portable。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe。
+- 未提交 git,未合并 main。
+
+### 20260705(第十一轮:结构化计划执行摘要)
+- 计划执行摘要改为固定结构:本章目标、场景序列、必须执行、禁止违背、可自由发挥、对话目标、伏笔动作、结尾钩子。
+- 场景序列会统一生成 S1/S2/S3 编号,后续任务书、审稿和履约检查可按同一编号对照,降低漏写场景的概率。
+- 章节计划协议要求模型在计划阶段就写出 S1/S2/S3,并在计划末尾列出“必须执行 / 禁止违背 / 可自由发挥”,让正文阶段知道哪些内容不能漏、哪些内容不能写、哪些只允许补细节。
+- 新增回归测试覆盖结构化摘要、场景编号和执行分层,防止后续提示词退回松散文本。
+- 验证通过:章节计划相关回归测试、npm run typecheck、npm run build、npm run build:portable。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe。
+- 未提交 git,未合并 main。
+
+### 20260705(第十二轮:摘要质量兜底与任务书场景展开)
+- 计划执行摘要增加本地质量检查:如果结构化摘要缺少 S 场景编号、禁止违背或结尾钩子,会自动追加“原计划关键片段”,避免压缩过度导致关键执行信息丢失。
+- 写作任务书提示词强化:当计划摘要包含 S1/S2/S3 时,任务书必须逐条展开 S1/S2/S3,不得合并、跳过或调换顺序。
+- 本轮不新增模型调用,不做履约失败自动返修,只增强本地摘要稳健性和任务书阶段执行约束。
+- 新增回归测试覆盖摘要质量兜底和任务书 S 编号展开。
+- 验证通过:章节计划相关回归测试、npm run typecheck、npm run build、npm run build:portable。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe。
+- 未提交 git,未合并 main。
+
+### 20260705(第十三轮:履约结构化与偏离点轻量返修)
+- 计划履约度检查改为优先要求 JSON 输出,并新增本地解析:兼容 structured JSON 和旧式纯文本结果。
+- 新增履约状态分层:符合、基本符合、部分偏离、明显偏离;只有“部分偏离 / 明显偏离”且存在可执行偏离点时才追加一次轻量返修。
+- 新增计划偏离点返修 prompt:只修复偏离点,不重写全章;保留原正文结构、节奏、人物口吻和已完成有效内容。
+- 返修完成后会再次回传最终正文,保证界面展示与返回结果一致;返修失败时保留原正文,不阻断章节生成。
+- 新增回归测试覆盖结构化解析、旧文本兼容、偏离时返修、基本符合时不返修。
+- 验证通过:章节计划相关回归测试、npm run typecheck、npm run build、npm run build:portable。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe。
+- 未提交 git,未合并 main。
+
+### 20260705(第十四轮:计划履约活动流可读性)
+- 优化计划履约检查活动流展示:从原始 JSON/纯文本改为稳定中文摘要,直接显示履约状态、偏离点数量、处理决定和偏离点明细。
+- 优化计划偏离点返修活动流展示:返修成功后明确提示“正文已更新”,并显示返修前后字数和处理范围。
+- 未改变模型调用策略:仍只有“部分偏离 / 明显偏离”且有可执行偏离点时才触发一次轻量返修。
+- 新增回归测试覆盖活动流中“触发轻量返修”和“无需返修”两种展示。
+- 验证通过:章节计划相关回归测试、npm run typecheck、npm run build、npm run build:portable。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe。
+- 未提交 git,未合并 main。
+
+### 20260705(第十五轮:返修安全校验与未知履约兜底)
+- 新增计划偏离点返修结果安全校验:返修后正文若明显变短、明显变长或未保留原正文主要内容,会保留返修前正文。
+- 返修异常时活动流明确提示“返修结果异常,已保留原正文”,并列出异常原因和前后字数。
+- 计划履约结果解析为未知时,活动流明确显示“处理决定:未触发返修”,并说明“模型未按结构返回,已避免误改正文”。
+- 本轮不增加模型调用,不改变正常返修路径,只增强异常结果兜底。
+- 新增回归测试覆盖返修过短、过长、丢失原正文主要内容、unknown 履约结果四类情况。
+- 验证通过:章节计划相关回归测试、npm run typecheck、npm run build、npm run build:portable。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe。
+- 未提交 git,未合并 main。
+
+### 20260705(第十六轮:收口审查修复)
+- 修复计划偏离点返修成功后 `revised` 状态未同步的问题:正文被计划返修更新时,工具返回和完成事件会正确标记已返修。
+- 优化长正文履约检查与偏离返修提示:不再只截取正文开头,改为保留开头和结尾,中段用截断标记提示,避免长章节的章末钩子/结尾偏离被遗漏。
+- 新增回归测试覆盖计划返修状态一致性、长正文首尾保留和提示词长度控制。
+- 验证通过:章节计划相关回归测试 9 个文件 163 条、npm run typecheck、npm run build、npm run build:portable。
+- 已生成 Windows 便携版:release-portable/QMaiWrite.exe;version-info.json 显示版本 2.2.33,builtAt 为 2026-07-05T09:05:02.970Z。
+- 补充验证:npm run test:mocks 仍有 8 个既有失败,集中在 outline-chat-panel、chat-message、unified-skill-library-view、story-simulation debug、release-notes;与本轮计划执行链路收口修复无直接文件重叠。
+- 未提交 git,未合并 main。