Sfoglia il codice sorgente

feat(writing): 严格模式阶段6改为按需局部修改

审稿干净不再全文去 AI 味;仅在有剩余问题时按清单改问题部分。

Co-authored-by: Cursor <cursoragent@cursor.com>
darknessomi 3 giorni fa
parent
commit
42727a99f5

+ 2 - 2
src/components/chat/chat-panel.spec.tsx

@@ -132,8 +132,8 @@ describe("chat-panel agent reference integration", () => {
     expect(source).toContain("description: \"完整质检")
     expect(source).toContain("快速模式像普通对话一样直接出结果")
     expect(source).toContain("读取上下文、生成任务书和正文初稿后直接完成")
-    expect(source).toContain("读取更完整上下文,执行审稿、返修、复审、去AI味和计划验收。")
-    expect(source).not.toContain("读取更完整上下文,执行审稿、返修、复审、去AI味和计划验收。会联网搜索。")
+    expect(source).toContain("读取更完整上下文,执行审稿、返修、复审、按需局部修改和计划验收。")
+    expect(source).not.toContain("读取更完整上下文,执行审稿、返修、复审、按需局部修改和计划验收。会联网搜索。")
     expect(source).not.toContain("前文与实体表")
     expect(source).toContain("workflowModeDropdownStyle.width")
     expect(source).toContain("routeDescription")

+ 1 - 1
src/components/chat/chat-panel.tsx

@@ -207,7 +207,7 @@ const aiWorkflowModeOptions: Array<{
     mode: "strict",
     label: "严格",
     description: "完整质检",
-    routeDescription: "读取更完整上下文,执行审稿、返修、复审、去AI味和计划验收。",
+    routeDescription: "读取更完整上下文,执行审稿、返修、复审、按需局部修改和计划验收。",
   },
 ]
 const currentModelNotSupportMsg = "当前模型不支持工具调用,已切换为普通对话模式"

+ 1 - 1
src/components/chat/chat-resume.ts

@@ -102,7 +102,7 @@ export function buildContinueUnfinishedDeepChapterPrompt(input: {
     "续写要求:",
     "1. 先判断原始阶段链最后停在哪个阶段,从第一次未完成的那个缺口继续。",
     "2. 不要重复阶段1上下文分析、阶段2任务书等已经完成的大段内容。",
-    "3. 如果上方已有正文草稿,就继续后续审查、返修、简单审查、去AI味或补全正文;如果还没有正文草稿,就从正文生成阶段继续。",
+    "3. 如果上方已有正文草稿,就继续后续审查、返修、简单审查与局部修改或补全正文;如果还没有正文草稿,就从正文生成阶段继续。",
     "4. 最近一次失败输出如果和原始阶段链冲突,以原始阶段链为准。",
     "5. 这次重点是节省 token:不要复述已有思考,不要解释为什么继续,直接把未完成的章节内容补完整。",
     "6. 最终输出必须是可直接保存到章节库的完整章节正文;如果需要少量承接说明,请放在思考中,不要混入正文。",

+ 3 - 3
src/lib/agent/activity-trace.spec.ts

@@ -270,7 +270,7 @@ describe("activity trace", () => {
   })
 
   it("resolves titles for post-draft strict stages", () => {
-    expect(resolveAgentStageTitle("final_polish")).toBe("去AI味")
+    expect(resolveAgentStageTitle("final_polish")).toBe("简单审查与修改")
     expect(resolveAgentStageTitle("execution_report")).toBe("执行报告")
     expect(resolveAgentStageTitle("execution_recheck")).toBe("执行复检")
     expect(resolveAgentStageTitle("plan_compliance")).toBe("计划履约")
@@ -377,10 +377,10 @@ describe("activity trace", () => {
     ])
   })
 
-  it("places 去AI味 between 校验与修正 and 最终输出", () => {
+  it("places 简单审查与修改 between 校验与修正 and 最终输出", () => {
     const stages: AgentStageTrace[] = [
       { id: "final_output", title: "最终输出", status: "done", summary: "完成", events: [], startedAt: 400 },
-      { id: "final_polish", title: "去AI味", status: "running", summary: "去AI味中", events: [], startedAt: 300 },
+      { id: "final_polish", title: "简单审查与修改", status: "running", summary: "局部修改中", events: [], startedAt: 300 },
       { id: "validate_revision", title: "校验与修正", status: "done", summary: "校验", events: [], startedAt: 200 },
     ]
 

+ 1 - 1
src/lib/agent/activity-trace.ts

@@ -397,7 +397,7 @@ export function resolveAgentStageTitle(stageId: string, fallbackTitle?: string):
     chapter_workflow: "多任务写作循环",
     generate_draft: "生成章节草稿",
     validate_revision: "校验与修正",
-    final_polish: "去AI味",
+    final_polish: "简单审查与修改",
     execution_report: "执行报告",
     execution_recheck: "执行复检",
     plan_compliance: "计划履约",

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

@@ -85,7 +85,7 @@ export function createRunChapterWorkflowTool(options: RunChapterWorkflowToolOpti
     name: "run_chapter_workflow",
     description: [
       "运行小说章节写作工作流。用于生成、续写、改写或润色章节。",
-      "调用后会读取项目上下文、生成写作任务书、生成正文,并按当前模式执行审稿、返修和去AI味。",
+      "调用后会读取项目上下文、生成写作任务书、生成正文,并按当前模式执行审稿、返修;严格模式若有剩余问题再做局部修改。",
       "正文由本工具直接交付给用户,调用成功后本轮任务即结束:不要复述、改写或补充正文。",
       "一次调用只处理一章;保存到项目文件仍需要写入工具和用户确认。",
     ].join("\n"),

+ 1 - 1
src/lib/agent/workflow-trace.spec.ts

@@ -138,7 +138,7 @@ describe("getWorkflowToolDescription", () => {
     expect(getWorkflowToolDescription(call({ id: "brief", name: "chapter_task_brief" }))).toBe("生成写作任务书")
     expect(getWorkflowToolDescription(call({ id: "draft", name: "chapter_draft", params: { chars: 3200 } }))).toBe("生成章节正文初稿")
     expect(getWorkflowToolDescription(call({ id: "review", name: "chapter_review" }))).toBe("执行 AI 审稿")
-    expect(getWorkflowToolDescription(call({ id: "polish", name: "chapter_final_polish" }))).toBe("简单审查与去AI味")
+    expect(getWorkflowToolDescription(call({ id: "polish", name: "chapter_final_polish" }))).toBe("简单审查与修改")
     expect(getWorkflowToolDescription(call({ id: "report", name: "chapter_execution_report" }))).toBe("检查章节执行清单")
     expect(getWorkflowToolDescription(call({ id: "repair", name: "chapter_execution_repair" }))).toBe("返修执行清单失败项")
     expect(getWorkflowToolDescription(call({ id: "recheck", name: "chapter_execution_recheck" }))).toBe("复检章节执行清单")

+ 1 - 1
src/lib/agent/workflow-trace.ts

@@ -296,7 +296,7 @@ export function getWorkflowToolDescription(call: WorkflowToolCall): string {
     case "chapter_post_revision_review":
       return "返修后角色一致性复审"
     case "chapter_final_polish":
-      return "简单审查与去AI味"
+      return "简单审查与修改"
     case "chapter_execution_report":
       return "检查章节执行清单"
     case "chapter_execution_repair":

+ 148 - 106
src/lib/novel/deep-chapter-generation.spec.ts

@@ -20,12 +20,6 @@ import {
 } from "./deep-chapter-prompts"
 import { contractToTaskBriefText, createEmptyContract, type ChapterExecutionContract } from "./chapter-execution-contract"
 
-const loadSmartDeAiSkillMock = vi.hoisted(() => vi.fn(async () => null))
-
-vi.mock("./de-ai-adapter", () => ({
-  loadSmartDeAiSkill: loadSmartDeAiSkillMock,
-}))
-
 const llmConfig = {
   provider: "custom",
   apiKey: "test-key",
@@ -103,6 +97,19 @@ function chapterText(prefix: string, count = 3000): string {
 
 // 写作阶段现在可能把 user 消息内容拆成带 cache_control 的文本块(见 applyCachePrefix);
 // provider 侧会把纯文本块拼回字符串,这里在测试桩里也照做,保持按关键字匹配阶段的逻辑。
+function isStage6Prompt(prompt: string): boolean {
+  return prompt.includes("简单审查") || prompt.includes("局部修改")
+}
+
+const leftoverWarning: NovelReviewResult = {
+  severity: "warning",
+  type: "plot",
+  message: "结尾钩子偏弱",
+  evidence: "正文",
+  relatedMemory: "",
+  suggestion: "补强钩子",
+}
+
 function messagesPromptText(messages: ChatMessage[]): string {
   return messages
     .map((message) =>
@@ -118,7 +125,7 @@ function createDeps(reviewResults: NovelReviewResult[] = []): DeepChapterGenerat
     "写作任务书内容",
     chapterText("初稿正文内容"),
     chapterText("返修正文内容"),
-    chapterText("最终去AI味正文"),
+    chapterText("局部修改后正文"),
   ]
   return {
     buildContextPack: vi.fn(async () => contextPack),
@@ -135,7 +142,7 @@ function createDeps(reviewResults: NovelReviewResult[] = []): DeepChapterGenerat
     })),
     streamChat: vi.fn(async (_config: LlmConfig, messages: ChatMessage[], callbacks: StreamCallbacks) => {
       const prompt = messagesPromptText(messages)
-      const content = prompt.includes("简单审查") || prompt.includes("去AI味")
+      const content = isStage6Prompt(prompt)
         ? responses[3]
         : prompt.includes("返修")
           ? responses[2]
@@ -197,7 +204,7 @@ describe("runDeepChapterGeneration", () => {
     expect(onRequestTrace).toHaveBeenCalled()
   })
 
-  it("routes workflow, prose, and de-AI stages to their configured models", async () => {
+  it("routes workflow and prose stages to their configured models", async () => {
     const previousState = useWikiStore.getState()
     useWikiStore.setState({
       aiChatModel: "custom/writer-model",
@@ -240,8 +247,8 @@ describe("runDeepChapterGeneration", () => {
       ) => {
         calledModels.push(config.model)
         const prompt = messagesPromptText(messages)
-        const content = prompt.includes("简单审查") || prompt.includes("去AI味")
-          ? chapterText("最终去AI味正文")
+        const content = isStage6Prompt(prompt)
+          ? chapterText("局部修改后正文")
           : prompt.includes("返修")
             ? chapterText("返修正文内容")
             : prompt.includes("正文")
@@ -267,7 +274,7 @@ describe("runDeepChapterGeneration", () => {
         "workflow-model",
         "writer-model",
         "writer-model",
-        "de-ai-model",
+        "writer-model",
       ])
     } finally {
       useWikiStore.setState({
@@ -279,10 +286,9 @@ describe("runDeepChapterGeneration", () => {
     }
   })
 
-  it("uses the confirmed plan when retrieving context and selecting the de-AI skill", async () => {
+  it("uses the confirmed plan when retrieving context", async () => {
     const deps = createDeps()
     const planBlueprint = "确认计划:新角色顾舟在北塔登场,并带出铜钥匙。"
-    loadSmartDeAiSkillMock.mockClear()
 
     await runDeepChapterGeneration(
       {
@@ -301,11 +307,6 @@ describe("runDeepChapterGeneration", () => {
       expect.stringContaining(planBlueprint),
       3,
     )
-    expect(loadSmartDeAiSkillMock).toHaveBeenCalledWith(
-      "E:/Novel",
-      expect.stringContaining(planBlueprint),
-      expect.any(Object),
-    )
   })
 
   it("persists the confirmed plan and execution contract in resume checkpoints", async () => {
@@ -364,7 +365,7 @@ describe("runDeepChapterGeneration", () => {
     const planningPrompt = buildDeepChapterBriefPrompt("", "上下文包内容", "生成第3章", 3)
     const draftPrompt = buildDeepChapterDraftPrompt("", "上下文包内容", "写作任务书内容", "生成第3章", 3)
     const revisionPrompt = buildDeepChapterRevisionPrompt("", "上下文包内容", "写作任务书内容", "初稿正文内容", reviewResults, "生成第3章", 3)
-    const finalPolishPrompt = buildDeepChapterFinalPolishPrompt("", "上下文包内容", "写作任务书内容", "返修正文内容", "生成第3章", 3)
+    const finalPolishPrompt = buildDeepChapterFinalPolishPrompt("", "上下文包内容", "写作任务书内容", "返修正文内容", reviewResults, "生成第3章", 3)
 
     for (const prompt of [planningPrompt, draftPrompt]) {
       expect(prompt).toContain(`低于 ${DEEP_CHAPTER_MIN_CHARS} 字`)
@@ -379,9 +380,10 @@ describe("runDeepChapterGeneration", () => {
       expect(prompt).not.toContain("全文安全上限")
       expect(prompt).not.toContain("2200-3200 字")
     }
-    expect(finalPolishPrompt).toContain("中文小说去 AI 味补充规则")
-    expect(finalPolishPrompt).toContain("角色声线")
-    expect(finalPolishPrompt).toContain("不要按非虚构文章规则硬删副词")
+    expect(finalPolishPrompt).toContain("剩余问题:")
+    expect(finalPolishPrompt).toContain("测试问题")
+    expect(finalPolishPrompt).toContain("只修改清单中指出的有问题部分")
+    expect(finalPolishPrompt).not.toContain("中文小说去 AI 味补充规则")
     expect(planningPrompt).not.toContain("用户已确认的章节计划")
     expect(planningPrompt).toContain("章节节奏曲线")
     expect(planningPrompt).toContain("对话目标")
@@ -485,7 +487,7 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     const streamPrompts = vi.mocked(deps.streamChat).mock.calls.map((call) => messagesPromptText(call[1]))
     expect(streamPrompts.join("\n")).toContain("用户确认计划的执行清单")
     expect(streamPrompts.join("\n")).toContain("完成后状态:主角进入旧屋。")
@@ -721,7 +723,7 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     expect(result.executionReport).toContain("执行报告生成失败")
     expect(result.planCompliance).toBe("")
     expect(deps.runChapterPlanComplianceCheck).not.toHaveBeenCalled()
@@ -761,7 +763,7 @@ describe("runDeepChapterGeneration", () => {
     expect(deps.runChapterPlanComplianceCheck).toHaveBeenCalledWith(
       expect.any(Object),
       expect.stringContaining("用户已确认的章节计划执行摘要"),
-      expect.stringContaining("最终去AI味正文"),
+      expect.stringContaining("初稿正文内容"),
       undefined,
     )
     expect(compliancePlanArg.length).toBeLessThan(fullPlan.length)
@@ -847,7 +849,7 @@ describe("runDeepChapterGeneration", () => {
     expect(deps.runChapterPlanComplianceCheck).toHaveBeenCalledWith(
       expect.any(Object),
       expect.stringContaining("确认计划:旧屋揭示,章末脚步声钩子。"),
-      expect.stringContaining("最终去AI味正文"),
+      expect.stringContaining("初稿正文内容"),
       controller.signal,
     )
   })
@@ -882,7 +884,7 @@ describe("runDeepChapterGeneration", () => {
         _signal?: AbortSignal,
       ) => {
         expect(plan).toContain("用户已确认的章节计划执行摘要")
-        expect(content).toContain("最终去AI味正文")
+        expect(content).toContain("初稿正文内容")
         expect(String(compliance)).toContain("章末钩子")
         return repairedContent
       }),
@@ -909,7 +911,7 @@ describe("runDeepChapterGeneration", () => {
     expect(result.revised).toBe(true)
     expect(result.planCompliance).toContain("计划偏离点返修:已完成")
     expect(result.planCompliance).not.toContain("partial_deviation")
-    expect(finalContents[0]).toContain("最终去AI味正文")
+    expect(finalContents[0]).toContain("初稿正文内容")
     expect(finalContents[finalContents.length - 1]).toBe(repairedContent)
     const complianceEvent = activityEvents.find((event) => event.stageId === "plan_compliance")
     expect(complianceEvent?.content).toContain("履约状态:部分偏离")
@@ -951,7 +953,7 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(result.revised).toBe(false)
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     expect(workflowEvents).toContainEqual(expect.objectContaining({
       type: "error",
       name: "chapter_plan_deviation_repair",
@@ -989,7 +991,7 @@ describe("runDeepChapterGeneration", () => {
 
     expect(deps.runChapterPlanComplianceCheck).toHaveBeenCalledTimes(2)
     expect(result.revised).toBe(false)
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     expect(result.finalContent).not.toBe(repairedContent)
   })
 
@@ -1014,7 +1016,7 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(deps.runChapterPlanDeviationRepair).not.toHaveBeenCalled()
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     const complianceEvent = activityEvents.find((event) => event.stageId === "plan_compliance")
     expect(complianceEvent?.content).toContain("履约状态:基本符合")
     expect(complianceEvent?.content).toContain("处理决定:无需返修")
@@ -1053,7 +1055,7 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(deps.runChapterPlanDeviationRepair).toHaveBeenCalledOnce()
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     expect(result.finalContent).not.toBe("短正文")
     expect(finalContents).toHaveLength(1)
     const repairEvent = activityEvents.find((event) => event.stageId === "plan_deviation_repair")
@@ -1085,7 +1087,7 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     const repairEvent = activityEvents.find((event) => event.stageId === "plan_deviation_repair")
     expect(repairEvent?.content).toContain("原因:返修后正文明显变长")
   })
@@ -1117,7 +1119,7 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     const repairEvent = activityEvents.find((event) => event.stageId === "plan_deviation_repair")
     expect(repairEvent?.content).toContain("原因:返修后未保留原正文主要内容")
   })
@@ -1167,13 +1169,14 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     expect(result.revised).toBe(false)
     expect(thinking.join("\n")).toContain("阶段1:上下文分析")
     expect(thinking.join("\n")).toContain("阶段2:写作任务书")
     expect(thinking.join("\n")).toContain("阶段3:正文初稿")
     expect(thinking.join("\n")).toContain("阶段4:AI审稿")
-    expect(thinking.join("\n")).toContain("阶段6:简单审查与去AI味")
+    expect(thinking.join("\n")).toContain("未发现剩余问题,已采用当前正文作为最终正文。")
+    expect(thinking.join("\n")).not.toContain("阶段6:简单审查与修改")
     expect(thinking.join("\n")).toContain("未发现阻断问题")
   })
 
@@ -1192,22 +1195,22 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("初稿正文内容")
     expect(thinking.join("\n")).toContain("AI 审稿失败")
     expect(thinking.join("\n")).not.toContain("AI 审稿完成,未发现阻断问题")
     expect(events.filter((event) => event.name === "chapter_review").map((event) => event.type))
       .toEqual(["started", "error"])
   })
 
-  it("keeps the pre-polish draft when final polish fails", async () => {
-    const deps = createDeps()
+  it("keeps the pre-fix draft when stage 6 local fix fails", async () => {
+    const deps = createDeps([leftoverWarning])
     vi.mocked(deps.streamChat).mockImplementation(async (
       _config: LlmConfig,
       messages: ChatMessage[],
       callbacks: StreamCallbacks,
     ) => {
       const prompt = messagesPromptText(messages)
-      if (prompt.includes("简单审查") || prompt.includes("去AI味")) {
+      if (isStage6Prompt(prompt)) {
         throw new Error("error decoding response body")
       }
       const content = prompt.includes("返修")
@@ -1233,9 +1236,9 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(result.finalContent).toContain("初稿正文内容")
-    expect(result.finalContent).not.toContain("最终去AI味正文")
+    expect(result.finalContent).not.toContain("局部修改后正文")
     expect(delivered[0]).toContain("初稿正文内容")
-    expect(thinking.join("\n")).toContain("已保留去AI味前的正文")
+    expect(thinking.join("\n")).toContain("已保留修改前的正文")
     expect(events.filter((event) => event.name === "chapter_final_polish").map((event) => event.type))
       .toEqual(["started", "error"])
     expect(events.some((event) => event.name === "chapter_complete" && event.type === "completed")).toBe(true)
@@ -1267,12 +1270,12 @@ describe("runDeepChapterGeneration", () => {
     expect(activityEvents.some((event) => event.kind === "stage_output" && event.content.includes("任务书"))).toBe(true)
     expect(activityEvents.some((event) => event.stageId === "final_polish")).toBe(false)
     expect(contextOutput?.content).toContain("写作任务书和正文初稿后直接完成")
-    expect(contextOutput?.content).not.toContain("审稿和最终去AI味")
+    expect(contextOutput?.content).not.toContain("审稿;若有剩余问题再局部修改")
     expect(activityEvents.some((event) => event.content.includes("进入阶段6"))).toBe(false)
   })
 
-  it("emits a dedicated 去AI味 stage between 校验与修正 and 最终输出", async () => {
-    const deps = createDeps()
+  it("emits a dedicated local-fix stage between 校验与修正 and 最终输出 when leftover issues remain", async () => {
+    const deps = createDeps([leftoverWarning])
     const activityEvents: AgentActivityEvent[] = []
 
     await runDeepChapterGeneration(
@@ -1307,9 +1310,9 @@ describe("runDeepChapterGeneration", () => {
       (event) => event.stageId === "read_context" && event.kind === "stage_output",
     )
 
-    expect(polishStarted?.content).toContain("去除复读、机械套话和 AI 味")
-    expect(polishOutput?.content).toContain("简单审查与去AI味完成")
-    expect(contextOutput?.content).toContain("审稿和最终去AI味阶段")
+    expect(polishStarted?.content).toContain("按剩余问题做局部修改")
+    expect(polishOutput?.content).toContain("简单审查与修改完成")
+    expect(contextOutput?.content).toContain("审稿;若有剩余问题再局部修改")
     expect(validateOutputIndex).toBeGreaterThanOrEqual(0)
     expect(polishStartedIndex).toBeGreaterThan(validateOutputIndex)
     expect(finalOutputIndex).toBeGreaterThan(polishStartedIndex)
@@ -1326,7 +1329,7 @@ describe("runDeepChapterGeneration", () => {
     vi.mocked(deps.streamChat).mockImplementation(async (_config: LlmConfig, messages: ChatMessage[], callbacks: StreamCallbacks) => {
       const prompt = messagesPromptText(messages)
       capturedPrompts.push(prompt)
-      const content = prompt.includes("简单审查") || prompt.includes("去AI味")
+      const content = isStage6Prompt(prompt)
         ? chapterText("最终文风正文", 3000)
         : prompt.includes("返修")
           ? chapterText("返修文风正文", 3000)
@@ -1366,7 +1369,7 @@ describe("runDeepChapterGeneration", () => {
     ) => {
       requests.push({ model: config.model, maxTokens: requestOverrides?.max_tokens })
       const prompt = messagesPromptText(messages)
-      const content = prompt.includes("简单审查") || prompt.includes("去AI味")
+      const content = isStage6Prompt(prompt)
         ? chapterText("最终无上限正文", 3000)
         : prompt.includes("返修")
           ? chapterText("返修无上限正文", 3000)
@@ -1428,9 +1431,9 @@ describe("runDeepChapterGeneration", () => {
 
       expect(requests).toContainEqual({ model: "workflow-model", maxTokens: 40_000 })
       expect(requests).toContainEqual({ model: "writer-model", maxTokens: 39_321 })
-      expect(requests).toContainEqual({ model: "de-ai-model", maxTokens: 30_720 })
-      // 共享资料包仍受最小的 204800-token 去 AI 味模型限制
-      expect(sharedContextTokenBudget).toBe(133_120)
+      expect(requests.some((item) => item.model === "de-ai-model")).toBe(false)
+      // 共享资料包按写作模型窗口规划,不再被去 AI 味模型窗口压低
+      expect(sharedContextTokenBudget).toBeGreaterThan(133_120)
     } finally {
       useWikiStore.setState({
         aiChatModel: previousState.aiChatModel,
@@ -1466,11 +1469,9 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(overrides.length).toBeGreaterThan(0)
-    // Analysis is raised to the 16384 thinking floor; generation stays at
-    // the larger window-fraction budget (30720 on the shared 204800 window).
+    // Analysis is raised to the 16384 thinking floor; generation uses the writing window.
     expect(overrides.every((item) => (item?.max_tokens ?? 0) >= 16_384)).toBe(true)
     expect(overrides.some((item) => item?.max_tokens === 16_384)).toBe(true)
-    expect(overrides.some((item) => item?.max_tokens === 30_720)).toBe(true)
   })
 
   it("preserves configured model reasoning for chapter generation calls", async () => {
@@ -1519,7 +1520,7 @@ describe("runDeepChapterGeneration", () => {
       }
 
       const prompt = messagesPromptText(messages)
-      const content = prompt.includes("简单审查") || prompt.includes("去AI味")
+      const content = isStage6Prompt(prompt)
         ? chapterText("最终兜底正文", 3000)
         : prompt.includes("返修")
           ? chapterText("返修兜底正文", 3000)
@@ -1536,7 +1537,7 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终兜底正文")
+    expect(result.finalContent).toContain("初稿兜底正文")
     expect(overrides[0]?.reasoning).toBeUndefined()
     expect(overrides[1]).toEqual({
       // Budgets are planned once per run, from the configured "high" reasoning
@@ -1563,7 +1564,7 @@ describe("runDeepChapterGeneration", () => {
       callbacks: StreamCallbacks,
     ) => {
       const prompt = messagesPromptText(messages)
-      const body = prompt.includes("简单审查") || prompt.includes("去AI味")
+      const body = isStage6Prompt(prompt)
         ? chapterText("最终兜底正文", 3000)
         : prompt.includes("返修")
           ? chapterText("返修兜底正文", 3000)
@@ -1606,7 +1607,7 @@ describe("runDeepChapterGeneration", () => {
     expect(fastDeps.reviewChapter).not.toHaveBeenCalled()
     expect(skippedCollect).not.toHaveBeenCalled()
     expect(fastActivity.some((event) => event.content.includes("进入阶段6"))).toBe(false)
-    expect(fastActivity.some((event) => event.content.includes("审稿和最终去AI味"))).toBe(false)
+    expect(fastActivity.some((event) => event.content.includes("若有剩余问题再局部修改"))).toBe(false)
 
     const standardCollect = vi.fn(async () => ({ markdown: "", searchedNames: [], notes: [] }))
     const standardDeps = { ...createDeps(), collectWritingEntityWebSearch: standardCollect }
@@ -1626,7 +1627,7 @@ describe("runDeepChapterGeneration", () => {
     expect(standardThinking.join("\n")).not.toContain("快速模式")
     expect(standardThinking.join("\n")).not.toContain("进入阶段6")
     expect(standardActivity.some((event) => event.content.includes("进入阶段6"))).toBe(false)
-    expect(standardActivity.some((event) => event.content.includes("审稿和最终去AI味"))).toBe(false)
+    expect(standardActivity.some((event) => event.content.includes("若有剩余问题再局部修改"))).toBe(false)
     expect(standardCollect).toHaveBeenCalled()
 
     const collectWritingEntityWebSearch = vi.fn(async () => ({
@@ -1640,7 +1641,7 @@ describe("runDeepChapterGeneration", () => {
       {},
       strictDeps,
     )
-    expect(strictDeps.streamChat).toHaveBeenCalledTimes(3)
+    expect(strictDeps.streamChat).toHaveBeenCalledTimes(2)
     expect(strictDeps.reviewChapter).toHaveBeenCalled()
     expect(collectWritingEntityWebSearch).toHaveBeenCalled()
   })
@@ -1811,10 +1812,10 @@ describe("runDeepChapterGeneration", () => {
       "completed:chapter_draft",
       "started:chapter_review",
       "completed:chapter_review",
-      "started:chapter_final_polish",
-      "completed:chapter_final_polish",
       "completed:chapter_complete",
     ]))
+    expect(eventKeys).not.toContain("started:chapter_final_polish")
+    expect(events.find((event) => event.name === "chapter_final_polish")?.result).toContain("未发现剩余问题,跳过阶段6")
     expect(events.find((event) => event.name === "chapter_task_brief")?.title).toBe("生成写作任务书")
     expect(events.find((event) => event.name === "chapter_complete")?.result).toContain("多任务写作循环完成")
   })
@@ -1997,27 +1998,80 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toContain("最终去AI味正文")
+    expect(result.finalContent).toContain("局部修改后正文")
     expect(result.revised).toBe(true)
     expect(deps.streamChat).toHaveBeenCalledTimes(4)
     expect(thinking.join("\n")).toContain("阶段5:自动返修")
-    expect(thinking.join("\n")).toContain("阶段6:简单审查与去AI味")
+    expect(thinking.join("\n")).toContain("阶段6:简单审查与修改")
     expect(thinking.join("\n")).toContain("没有承接上一章门缝声")
+    const stage6Prompt = vi.mocked(deps.streamChat).mock.calls
+      .map((call) => messagesPromptText(call[1]))
+      .find((prompt) => isStage6Prompt(prompt))
+    expect(stage6Prompt).toContain("没有承接上一章门缝声")
+    expect(stage6Prompt).toContain("只修改清单中指出的有问题部分")
+  })
+
+  it("skips stage 6 when post-revision review is clean", async () => {
+    const blockingIssue: NovelReviewResult = {
+      severity: "error",
+      type: "plot",
+      message: "没有承接上一章门缝声。",
+      evidence: "初稿正文内容",
+      relatedMemory: "上一章结尾",
+      suggestion: "补上门缝声的承接。",
+    }
+    const deps = createDeps([blockingIssue])
+    vi.mocked(deps.reviewChapter)
+      .mockResolvedValueOnce([blockingIssue])
+      .mockResolvedValueOnce([])
+    const thinking: string[] = []
+
+    const result = await runDeepChapterGeneration(
+      { projectPath: "E:/Novel", userRequest: "生成第3章", chapterNumber: 3, llmConfig },
+      { onThinking: (content) => thinking.push(content) },
+      deps,
+    )
+
+    expect(result.finalContent).toContain("返修正文内容")
+    expect(result.revised).toBe(true)
+    expect(deps.streamChat).toHaveBeenCalledTimes(3)
+    expect(thinking.join("\n")).toContain("返修后复审未发现新的阻断问题")
+    expect(thinking.join("\n")).toContain("返修后未发现剩余问题,已采用返修后正文作为最终正文。")
+    expect(thinking.join("\n")).not.toContain("阶段6:简单审查与修改")
+  })
+
+  it("runs stage 6 only for leftover warnings and keeps the issue list in the prompt", async () => {
+    const deps = createDeps([leftoverWarning])
+    const thinking: string[] = []
+
+    const result = await runDeepChapterGeneration(
+      { projectPath: "E:/Novel", userRequest: "生成第3章", chapterNumber: 3, llmConfig },
+      { onThinking: (content) => thinking.push(content) },
+      deps,
+    )
+
+    expect(result.finalContent).toContain("局部修改后正文")
+    expect(result.revised).toBe(false)
+    expect(deps.streamChat).toHaveBeenCalledTimes(3)
+    expect(thinking.join("\n")).toContain("阶段6:简单审查与修改")
+    const stage6Prompt = vi.mocked(deps.streamChat).mock.calls
+      .map((call) => messagesPromptText(call[1]))
+      .find((prompt) => isStage6Prompt(prompt))
+    expect(stage6Prompt).toContain("结尾钩子偏弱")
+    expect(stage6Prompt).toContain("只修改清单中指出的有问题部分")
+    expect(stage6Prompt).not.toContain("中文小说去 AI 味补充规则")
   })
 
   it("automatically expands a too-short draft before review and final output", async () => {
     const shortDraft = chapterText("短稿", 800)
     const expandedDraft = chapterText("扩写后正文", 3000)
-    const finalPolished = chapterText("最终去AI味正文", 3000)
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
       contextPackToPrompt: vi.fn(() => "上下文包内容"),
       reviewChapter: vi.fn(async () => []),
       streamChat: vi.fn(async (_config: LlmConfig, messages: ChatMessage[], callbacks: StreamCallbacks) => {
         const prompt = messagesPromptText(messages)
-        const content = prompt.includes("简单审查") || prompt.includes("去AI味")
-          ? finalPolished
-          : prompt.includes("扩写补足")
+        const content = prompt.includes("扩写补足")
           ? expandedDraft
           : prompt.includes("章节正文")
             ? shortDraft
@@ -2034,11 +2088,11 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toBe(finalPolished)
-    expect(deps.streamChat).toHaveBeenCalledTimes(4)
+    expect(result.finalContent).toBe(expandedDraft)
+    expect(deps.streamChat).toHaveBeenCalledTimes(3)
     expect(deps.reviewChapter).toHaveBeenCalledWith("E:/Novel", expandedDraft, 3, expect.objectContaining({}))
     expect(thinking.join("\n")).toContain("阶段3:正文扩写补足")
-    expect(thinking.join("\n")).toContain("阶段6:简单审查与去AI味")
+    expect(thinking.join("\n")).not.toContain("阶段6:简单审查与修改")
   })
 
   it("fails the workflow when expansion is still far below the minimum chapter length", async () => {
@@ -2081,13 +2135,11 @@ describe("runDeepChapterGeneration", () => {
     }))
   })
 
-  it("does not force expansion after final polish even when the result is short", async () => {
+  it("does not force expansion after a clean review even when the draft is short of later polish", async () => {
     const draft = chapterText("初稿正文内容", 3000)
-    const shortFinal = chapterText("最终润色后过短", 1800)
     const responses = [
       "写作任务书内容",
       draft,
-      shortFinal,
     ]
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
@@ -2107,10 +2159,11 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toBe(shortFinal)
-    expect(deps.streamChat).toHaveBeenCalledTimes(3)
+    expect(result.finalContent).toBe(draft)
+    expect(deps.streamChat).toHaveBeenCalledTimes(2)
     expect(thinking.join("\n")).toContain("阶段5:无需自动返修")
-    expect(thinking.join("\n")).toContain("阶段6:简单审查与去AI味")
+    expect(thinking.join("\n")).toContain("未发现剩余问题,已采用当前正文作为最终正文。")
+    expect(thinking.join("\n")).not.toContain("阶段6:简单审查与修改")
     expect(thinking.join("\n")).not.toContain("阶段6:字数检查未达标")
     expect(thinking.join("\n")).not.toContain("阶段3:正文扩写补足")
   })
@@ -2119,12 +2172,10 @@ describe("runDeepChapterGeneration", () => {
     const repeatUnit = "屋外雨声小了些,风还从门缝挤进来。旧木箱的盖子松松地合上,那东西还在。小晴在床上动了动,掌心湿热,像两股不同的水在交汇。\n"
     const runawayDraft = repeatUnit.repeat(900)
     const optimizedDraft = chapterText("阶段4优化后正文", 3000)
-    const finalPolished = chapterText("最终去AI味正文", 3000)
     const responses = [
       "写作任务书内容",
       runawayDraft,
       optimizedDraft,
-      finalPolished,
     ]
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
@@ -2144,18 +2195,16 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(result.draftContent).toBe(optimizedDraft)
-    expect(result.finalContent).toBe(finalPolished)
+    expect(result.finalContent).toBe(optimizedDraft)
     expect(deps.reviewChapter).toHaveBeenCalledWith("E:/Novel", optimizedDraft, 3, expect.objectContaining({}))
     expect(thinking.join("\n")).toContain("检测到模型重复输出")
   })
 
   it("does not stop the AI chat stream at the old chapter hard max", async () => {
     const longDraft = chapterText("超过旧硬上限但不是重复输出的正文", 6500)
-    const finalPolished = chapterText("最终去AI味正文", 3000)
     const responses = [
       "写作任务书内容",
       longDraft,
-      finalPolished,
     ]
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
@@ -2182,11 +2231,9 @@ describe("runDeepChapterGeneration", () => {
 
   it("sends long drafts directly to review without a stage 4 length rewrite", async () => {
     const overlongDraft = chapterText("过长初稿正文", 5200)
-    const finalPolished = chapterText("最终去AI味正文", 3000)
     const responses = [
       "写作任务书内容",
       overlongDraft,
-      finalPolished,
     ]
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
@@ -2206,18 +2253,16 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(deps.reviewChapter).toHaveBeenCalledWith("E:/Novel", overlongDraft, 3, expect.objectContaining({}))
-    expect(deps.streamChat).toHaveBeenCalledTimes(3)
+    expect(deps.streamChat).toHaveBeenCalledTimes(2)
     expect(thinking.join("\n")).not.toContain("2200-3200")
     expect(thinking.join("\n")).not.toContain("字数优化")
   })
 
   it("does not optimize the stage 3 draft in stage 4 before review", async () => {
     const draft = chapterText("阶段3较长初稿", 5500)
-    const finalPolished = chapterText("最终去AI味正文", 3000)
     const responses = [
       "写作任务书内容",
       draft,
-      finalPolished,
     ]
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
@@ -2237,17 +2282,15 @@ describe("runDeepChapterGeneration", () => {
     )
 
     expect(deps.reviewChapter).toHaveBeenCalledWith("E:/Novel", draft, 3, expect.objectContaining({}))
-    expect(deps.streamChat).toHaveBeenCalledTimes(3)
+    expect(deps.streamChat).toHaveBeenCalledTimes(2)
     expect(thinking.join("\n")).not.toContain("2200-3200")
   })
 
   it("does not retry stage 4 length optimization when the draft stays long", async () => {
     const draft = chapterText("阶段3超长初稿", 5500)
-    const finalPolished = chapterText("最终去AI味正文", 3000)
     const responses = [
       "写作任务书内容",
       draft,
-      finalPolished,
     ]
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
@@ -2266,20 +2309,18 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toBe(finalPolished)
+    expect(result.finalContent).toBe(draft)
     expect(deps.reviewChapter).toHaveBeenCalledWith("E:/Novel", draft, 3, expect.objectContaining({}))
-    expect(deps.streamChat).toHaveBeenCalledTimes(3)
+    expect(deps.streamChat).toHaveBeenCalledTimes(2)
     expect(thinking.join("\n")).not.toContain("2200-3200")
     expect(thinking.join("\n")).not.toContain("连续尝试")
   })
 
-  it("does not force a length rewrite after final polish", async () => {
+  it("does not force a length rewrite after a clean review", async () => {
     const draft = chapterText("初稿正文内容", 3000)
-    const overlongFinal = chapterText("简单审查后过长正文", 5200)
     const responses = [
       "写作任务书内容",
       draft,
-      overlongFinal,
     ]
     const deps: DeepChapterGenerationDeps = {
       buildContextPack: vi.fn(async () => contextPack),
@@ -2298,22 +2339,23 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toBe(overlongFinal)
-    expect(deps.streamChat).toHaveBeenCalledTimes(3)
-    expect(thinking.join("\n")).toContain("阶段6:简单审查与去AI味")
+    expect(result.finalContent).toBe(draft)
+    expect(deps.streamChat).toHaveBeenCalledTimes(2)
+    expect(thinking.join("\n")).toContain("未发现剩余问题,已采用当前正文作为最终正文。")
+    expect(thinking.join("\n")).not.toContain("阶段6:简单审查与修改")
     expect(thinking.join("\n")).not.toContain("2200-3200")
     expect(thinking.join("\n")).not.toContain("字数检查与正文优化")
   })
 
   it("resumes from a saved review checkpoint instead of regenerating earlier stages", async () => {
-    const finalPolished = chapterText("恢复后的最终正文", 3000)
+    const draftContent = chapterText("阶段4完成后的正文草稿", 3000)
     const checkpoint: DeepChapterGenerationResumeCheckpoint = {
       version: 1,
       originalRequest: "生成第3章",
       chapterNumber: 3,
       stage: "after_review",
       taskBrief: "写作任务书内容",
-      draftContent: chapterText("阶段4完成后的正文草稿", 3000),
+      draftContent,
       reviewResults: [],
     }
     const deps: DeepChapterGenerationDeps = {
@@ -2323,7 +2365,7 @@ describe("runDeepChapterGeneration", () => {
         throw new Error("resume should not rerun review")
       }),
       streamChat: vi.fn(async (_config: LlmConfig, _messages: ChatMessage[], callbacks: StreamCallbacks) => {
-        callbacks.onToken(finalPolished)
+        callbacks.onToken(chapterText("不应再生成的正文", 3000))
         callbacks.onDone()
       }),
     }
@@ -2341,9 +2383,9 @@ describe("runDeepChapterGeneration", () => {
       deps,
     )
 
-    expect(result.finalContent).toBe(finalPolished)
+    expect(result.finalContent).toBe(draftContent)
     expect(result.revised).toBe(false)
-    expect(deps.streamChat).toHaveBeenCalledTimes(1)
+    expect(deps.streamChat).not.toHaveBeenCalled()
     expect(deps.reviewChapter).not.toHaveBeenCalled()
     expect(thinking.join("\n")).not.toContain("阶段1:上下文分析")
     expect(thinking.join("\n")).not.toContain("阶段2:写作任务书")

+ 85 - 79
src/lib/novel/deep-chapter-generation.ts

@@ -36,7 +36,7 @@ import {
   type CollectWritingEntityWebSearchInput,
   type WritingEntityWebSearchResult,
 } from "./writing-entity-web-search";
-import { resolveDefaultModel, resolveNovelModel } from "./model-resolver";
+import { resolveDefaultModel } from "./model-resolver";
 import { reviewChapter, type NovelReviewResult } from "./review-adapter";
 import type { TaskRouteResult } from "./task-router";
 import type { GoldenThreeChapterRequest } from "./golden-three-chapters";
@@ -219,7 +219,7 @@ interface ChapterWorkflowProfile {
   mode: AiWorkflowMode;
   runExecutionContractBuild: boolean;
   runAiReview: boolean;
-  runFinalPolish: boolean;
+  allowFinalIssueFix: boolean;
   runPostRevisionReview: boolean;
   runPostDraftPlanAudits: boolean;
   completionTitle: string;
@@ -235,7 +235,7 @@ function resolveChapterWorkflowProfile(
       mode: "fast",
       runExecutionContractBuild: false,
       runAiReview: false,
-      runFinalPolish: false,
+      allowFinalIssueFix: false,
       runPostRevisionReview: false,
       runPostDraftPlanAudits: false,
       completionTitle: "完成快速写作",
@@ -247,7 +247,7 @@ function resolveChapterWorkflowProfile(
       mode: "standard",
       runExecutionContractBuild: false,
       runAiReview: false,
-      runFinalPolish: false,
+      allowFinalIssueFix: false,
       runPostRevisionReview: false,
       runPostDraftPlanAudits: false,
       completionTitle: "完成标准写作",
@@ -258,7 +258,7 @@ function resolveChapterWorkflowProfile(
     mode: "strict",
     runExecutionContractBuild: true,
     runAiReview: true,
-    runFinalPolish: true,
+    allowFinalIssueFix: true,
     runPostRevisionReview: true,
     runPostDraftPlanAudits: true,
     completionTitle: "完成多任务写作循环",
@@ -273,20 +273,39 @@ function workflowModeLabel(profile: ChapterWorkflowProfile): string {
 }
 
 function describeContextPackHandoff(profile: ChapterWorkflowProfile): string {
-  if (profile.runAiReview || profile.runFinalPolish) {
-    return "已形成章节生成约束包,将传入写作任务书、正文初稿、审稿和最终去AI味阶段。";
+  if (profile.runAiReview || profile.allowFinalIssueFix) {
+    return "已形成章节生成约束包,将传入写作任务书、正文初稿和审稿;若有剩余问题再局部修改。";
   }
   return "已形成章节生成约束包,将传入写作任务书和正文初稿后直接完成。";
 }
 
 function describeSkippedReviewAndPolish(profile: ChapterWorkflowProfile): string {
   const skipped = ["AI 审稿", "返修"];
-  if (!profile.runFinalPolish) skipped.push("最终去AI味");
+  if (!profile.allowFinalIssueFix) skipped.push("阶段6局部修改");
   return `${workflowModeLabel(profile)}跳过 ${skipped.join("、")},直接使用阶段3正文初稿。`;
 }
 
 function describeSkippedFinalPolish(profile: ChapterWorkflowProfile): string {
-  return `${workflowModeLabel(profile)}跳过最终去AI味,直接采用阶段3正文作为最终正文。`;
+  return `${workflowModeLabel(profile)}跳过阶段6局部修改,直接采用阶段3正文作为最终正文。`;
+}
+
+function describeSkippedFinalIssueFix(): string {
+  return "未发现剩余问题,跳过阶段6。";
+}
+
+function isPendingFixIssue(item: NovelReviewResult): boolean {
+  return item.severity === "error" || item.severity === "warning";
+}
+
+function collectPendingFixIssues(input: {
+  reviewFailed: boolean;
+  revised: boolean;
+  reviewResults: NovelReviewResult[];
+  postRevisionBlockingIssues: NovelReviewResult[];
+}): NovelReviewResult[] {
+  if (input.reviewFailed) return [];
+  if (input.revised) return input.postRevisionBlockingIssues;
+  return input.reviewResults.filter(isPendingFixIssue);
 }
 
 function describeSkippedPostDraftCompletion(profile: ChapterWorkflowProfile): string {
@@ -503,10 +522,8 @@ export async function runDeepChapterGeneration(
   const novelConfig = useWikiStore.getState().novelConfig;
   const writingConfig = resolveWritingConfig(input.llmConfig);
   const workflowConfig = resolveDefaultModel(input.llmConfig);
-  const deAiConfig = resolveNovelModel(input.llmConfig, novelConfig, "deAi");
   const workflowProfile = resolveChapterWorkflowProfile(input.aiWorkflowMode);
   const lengthSpec = resolveCurrentChapterLengthSpec();
-  const { loadSmartDeAiSkill } = await import("./de-ai-adapter");
   const planBlueprint =
     input.planBlueprint?.trim() || resumeCheckpoint?.planBlueprint?.trim() || undefined;
   const contextRequest = [
@@ -559,9 +576,6 @@ export async function runDeepChapterGeneration(
     startChapterWorkflowStep(callbacks, contextWorkflowStep);
   }
 
-  // 将在阶段1构建contextPack后再加载skill(需要contextPack用于场景检测)
-  let customDeAiSkill: string | null = null;
-
   // 阶段0:前情分析。快速模式始终跳过;标准/严格模式跟随写作设置。
   // 记忆库的近期摘要与上一章结尾仍会注入。
   let previousChaptersAnalysis = "";
@@ -685,15 +699,7 @@ export async function runDeepChapterGeneration(
     });
   }
 
-  // 阶段1后:加载智能skill(传递contextPack用于场景检测)
-  customDeAiSkill = await loadSmartDeAiSkill(
-    input.projectPath,
-    contextRequest,
-    contextPack,
-  );
-  throwIfAborted(signal);
-
-  // 任务书(workflowConfig)、初稿/返修(writingConfig)、去AI味(deAiConfig)可能
+  // 任务书(workflowConfig)、初稿/返修/阶段6局部修改(writingConfig)可能
   // 使用不同模型。每个阶段的输出预算必须按实际调用模型的窗口与输出上限计算;
   // 否则一个小窗口的辅助模型会错误压缩大窗口工作流模型的任务书输出。
   const chapterAnalysisBudget = planChapterRequestBudget({
@@ -714,21 +720,11 @@ export async function runDeepChapterGeneration(
       writingConfig.reasoning ?? { mode: "auto" },
     ),
   });
-  const chapterDeAiBudget = planChapterRequestBudget({
-    maxContextSize: deAiConfig.maxContextSize,
-    chapterTargetChars: novelConfig.chapterTargetChars,
-    stage: "generation",
-    maxOutputTokens: getEffectiveMaxOutputTokens(deAiConfig),
-    thinkingFloorTokens: thinkingMinMaxTokens(
-      deAiConfig.reasoning ?? { mode: "auto" },
-    ),
-  });
-  // 三个阶段仍复用同一份 outlinePrompt + contextPrompt。共享输入取各阶段
+  // 各写作阶段仍复用同一份 outlinePrompt + contextPrompt。共享输入取各阶段
   // 实际可用上下文预算的最小值,确保任一模型都无需依赖 llm-client 末级截断。
   const totalContextTokenBudget = Math.min(
     chapterAnalysisBudget.contextTokenBudget,
     chapterGenerationBudget.contextTokenBudget,
-    chapterDeAiBudget.contextTokenBudget,
   );
   const analysisRequestOverrides: RequestOverrides = {
     max_tokens: chapterAnalysisBudget.outputTokens,
@@ -736,9 +732,6 @@ export async function runDeepChapterGeneration(
   const generationRequestOverrides: RequestOverrides = {
     max_tokens: chapterGenerationBudget.outputTokens,
   };
-  const deAiRequestOverrides: RequestOverrides = {
-    max_tokens: chapterDeAiBudget.outputTokens,
-  };
   // Same density as the token estimator / trimContextPack (CJK 1, English 4).
   const charsPerToken = charsPerTokenForLanguage();
   const totalContextCharBudget = totalContextTokenBudget * charsPerToken;
@@ -787,7 +780,7 @@ export async function runDeepChapterGeneration(
     .filter(Boolean)
     .join("\n\n");
 
-  // 稳定上下文前缀:与任务书/初稿/扩写/返修/去AI味各阶段提示词开头逐字节一致。
+  // 稳定上下文前缀:与任务书/初稿/扩写/返修/阶段6局部修改各阶段提示词开头逐字节一致。
   // 作为显式 prompt 缓存断点传入(Anthropic/MiniMax 走 cache_control;
   // OpenAI/DeepSeek 该断点被折叠回字符串、由其自动前缀缓存命中)。
   const cachePrefix = buildStableContextPrefix(outlinePrompt, contextPrompt);
@@ -1226,7 +1219,7 @@ export async function runDeepChapterGeneration(
     callbacks.onThinking?.(
       formatStageThinking(
         "阶段5:已跳过自动返修",
-        "AI 审稿失败,无法安全判断阻断问题,已跳过自动返修并继续最终简单审查。",
+        "AI 审稿失败,无法安全判断阻断问题,已跳过自动返修。",
       ),
     );
   } else if (blockingIssues.length === 0) {
@@ -1245,7 +1238,7 @@ export async function runDeepChapterGeneration(
       callbacks.onThinking?.(
         formatStageThinking(
           "阶段5:无需自动返修",
-          "AI审稿未发现阻断问题,跳过自动返修,进入阶段6简单审查与去AI味。",
+          "AI审稿未发现阻断问题,跳过自动返修。",
         ),
       );
     }
@@ -1331,6 +1324,7 @@ export async function runDeepChapterGeneration(
 
   // 阶段5.5:返修后复审(只在发生了返修时执行,只审查角色一致性维度,降低token消耗,不再自动返修避免循环)
   const shouldRunPostRevisionReview = workflowProfile.runPostRevisionReview;
+  let postRevisionBlockingIssues: NovelReviewResult[] = [];
   if (revised && shouldRunPostRevisionReview) {
     const postRevisionWorkflowStep: ChapterWorkflowStepSpec = {
       name: "chapter_post_revision_review",
@@ -1372,19 +1366,19 @@ export async function runDeepChapterGeneration(
               onRequestTrace: callbacks.onRequestTrace,
             },
           );
-      const postBlockingIssues = (postRevisionResults || []).filter(
+      postRevisionBlockingIssues = (postRevisionResults || []).filter(
         (item) => item.severity === "error",
       );
-      if (postBlockingIssues.length > 0) {
+      if (postRevisionBlockingIssues.length > 0) {
         callbacks.onThinking?.(
           formatStageThinking(
             "阶段5.5:返修后复审",
             [
-              `返修后复审发现 ${postBlockingIssues.length} 个阻断问题(不再自动返修,避免循环):`,
+              `返修后复审发现 ${postRevisionBlockingIssues.length} 个阻断问题(不再自动返修,避免循环):`,
               "",
-              formatReviewIssueList(postBlockingIssues),
+              formatReviewIssueList(postRevisionBlockingIssues),
               "",
-              "这些问题将在阶段6去AI味时一并处理,或需要手动修改。",
+              "这些问题将在阶段6按问题局部修改。",
             ].join("\n"),
           ),
         );
@@ -1393,15 +1387,15 @@ export async function runDeepChapterGeneration(
         callbacks.onThinking?.(
           formatStageThinking(
             "阶段5.5:返修后复审",
-            "返修后复审未发现新的阻断问题,进入阶段6。",
+            "返修后复审未发现新的阻断问题。",
           ),
         );
       }
       completeChapterWorkflowStep(
         callbacks,
         postRevisionWorkflowStep,
-        `返修后复审完成,发现 ${postBlockingIssues.length} 个阻断问题。`,
-        { blockingIssueCount: postBlockingIssues.length },
+        `返修后复审完成,发现 ${postRevisionBlockingIssues.length} 个阻断问题。`,
+        { blockingIssueCount: postRevisionBlockingIssues.length },
       );
     } catch (err) {
       rethrowIfUserAbort(err, signal);
@@ -1410,26 +1404,34 @@ export async function runDeepChapterGeneration(
       callbacks.onThinking?.(
         formatStageThinking(
           "阶段5.5:返修后复审",
-          `返修后复审失败:${getErrorMessage(err)}。已继续进入最终简单审查,请在保存前手动复核正文。`,
+          `返修后复审失败:${getErrorMessage(err)}。已跳过阶段6局部修改,请在保存前手动复核正文。`,
         ),
       );
     }
   }
 
+  const pendingFixIssues = collectPendingFixIssues({
+    reviewFailed: Boolean(reviewFailureMessage),
+    revised,
+    reviewResults,
+    postRevisionBlockingIssues,
+  });
+  const shouldRunFinalFix =
+    workflowProfile.allowFinalIssueFix && pendingFixIssues.length > 0;
   const finalPolishWorkflowStep: ChapterWorkflowStepSpec = {
     name: "chapter_final_polish",
-    title: "简单审查与去AI味",
-    detail: "做最后一遍简单审查,减少复读、机械套话和 AI 味。",
+    title: "简单审查与修改",
+    detail: "按剩余问题做局部修改。",
     params: workflowBaseParams,
   };
   let polishFailureMessage = "";
   let finalContent = currentContent;
-  if (workflowProfile.runFinalPolish) {
+  if (shouldRunFinalFix) {
     emitDeepChapterStageStarted(
       callbacks,
       "final_polish",
-      "去AI味",
-      "正在做最后一遍简单审查,去除复读、机械套话和 AI 味。",
+      "简单审查与修改",
+      "按剩余问题做局部修改。",
     );
     try {
       finalContent = await runChapterWorkflowStep(
@@ -1437,43 +1439,43 @@ export async function runDeepChapterGeneration(
         finalPolishWorkflowStep,
         () =>
           finalPolishChapter(
-            deAiConfig,
+            writingConfig,
             outlinePrompt,
             contextPrompt,
             taskBrief,
             currentContent,
+            pendingFixIssues,
             input,
             contextPack,
             callbacks,
             deps,
             signal,
-            customDeAiSkill || undefined,
             cachePrefix,
-            deAiRequestOverrides,
+            generationRequestOverrides,
           ),
         (value) =>
-          `简单审查与去AI味完成,最终正文约 ${countChapterChars(value)} 字。`,
+          `简单审查与修改完成,最终正文约 ${countChapterChars(value)} 字。`,
         (value) => ({ chars: countChapterChars(value) }),
       );
       emitDeepChapterActivity(callbacks, {
         id: `deep_chapter:final_polish:output:${Date.now()}`,
         stageId: "final_polish",
         kind: "stage_output",
-        title: "去AI味",
-        content: `简单审查与去AI味完成,最终正文约 ${countChapterChars(finalContent)} 字。`,
+        title: "简单审查与修改",
+        content: `简单审查与修改完成,最终正文约 ${countChapterChars(finalContent)} 字。`,
       });
     } catch (err) {
       rethrowIfUserAbort(err, signal);
-      polishFailureMessage = `简单审查与去AI味失败:${getErrorMessage(err)}。已保留去AI味前的正文。`;
+      polishFailureMessage = `简单审查与修改失败:${getErrorMessage(err)}。已保留修改前的正文。`;
       finalContent = currentContent;
       callbacks.onThinking?.(
-        formatStageThinking("阶段6:简单审查与去AI味", polishFailureMessage),
+        formatStageThinking("阶段6:简单审查与修改", polishFailureMessage),
       );
       emitDeepChapterActivity(callbacks, {
         id: `deep_chapter:final_polish:error:${Date.now()}`,
         stageId: "final_polish",
         kind: "analysis",
-        title: "去AI味失败",
+        title: "简单审查与修改失败",
         content: polishFailureMessage,
       });
     }
@@ -1481,7 +1483,9 @@ export async function runDeepChapterGeneration(
     completeChapterWorkflowStep(
       callbacks,
       finalPolishWorkflowStep,
-      describeSkippedFinalPolish(workflowProfile),
+      workflowProfile.allowFinalIssueFix
+        ? describeSkippedFinalIssueFix()
+        : describeSkippedFinalPolish(workflowProfile),
       { skipped: true, chars: countChapterChars(finalContent) },
     );
   }
@@ -1489,14 +1493,16 @@ export async function runDeepChapterGeneration(
     formatStageThinking(
       "阶段7:完成",
       polishFailureMessage
-        ? "简单审查与去AI味失败,已保留去AI味前的正文作为最终正文。"
-        : workflowProfile.runFinalPolish
-          ? reviewFailureMessage
-            ? "AI 审稿失败;已保留正文并完成最后一遍简单审查与去AI味,请在保存前手动复核。"
-            : revised
-              ? "采用返修并完成简单审查、去AI味后的正文作为最终正文。"
-              : "未发现阻断问题,已完成最后一遍简单审查与去AI味。"
-          : describeSkippedPostDraftCompletion(workflowProfile),
+        ? "简单审查与修改失败,已保留修改前的正文作为最终正文。"
+        : reviewFailureMessage
+          ? "AI 审稿失败;已保留正文作为最终正文,请在保存前手动复核。"
+          : shouldRunFinalFix
+            ? "已按剩余问题完成阶段6局部修改。"
+            : workflowProfile.allowFinalIssueFix
+              ? revised
+                ? "返修后未发现剩余问题,已采用返修后正文作为最终正文。"
+                : "未发现剩余问题,已采用当前正文作为最终正文。"
+              : describeSkippedPostDraftCompletion(workflowProfile),
     ),
   );
   emitDeepChapterActivity(callbacks, {
@@ -1841,29 +1847,29 @@ export async function runDeepChapterGeneration(
 }
 
 async function finalPolishChapter(
-  deAiConfig: LlmConfig,
+  writingConfig: LlmConfig,
   outlinePrompt: string,
   contextPrompt: string,
   taskBrief: string,
   currentContent: string,
+  pendingFixIssues: NovelReviewResult[],
   input: DeepChapterGenerationInput,
   _contextPack: ContextPack,
   callbacks: DeepChapterGenerationCallbacks,
   deps: DeepChapterGenerationDeps,
   signal?: AbortSignal,
-  customDeAiSkill?: string,
   cachePrefix?: string,
   requestOverrides?: RequestOverrides,
 ): Promise<string> {
   assertNotAborted(signal);
   callbacks.onThinking?.(
     formatStageThinking(
-      "阶段6:简单审查与去AI味",
-      "正在进行最后一遍简单审查,去除复读、机械套话和 AI 味。",
+      "阶段6:简单审查与修改",
+      "正在按剩余问题做局部修改。",
     ),
   );
   const polished = await collectModelText(
-    deAiConfig,
+    writingConfig,
     [
       {
         role: "user",
@@ -1872,10 +1878,10 @@ async function finalPolishChapter(
           contextPrompt,
           taskBrief,
           currentContent,
+          pendingFixIssues,
           input.userRequest,
           input.chapterNumber,
           input.goldenThreeChapter,
-          customDeAiSkill,
         ),
       },
     ],
@@ -1883,7 +1889,7 @@ async function finalPolishChapter(
     signal,
     (partial) =>
       callbacks.onThinking?.(
-        formatStageThinking("阶段6:简单审查与去AI味", partial),
+        formatStageThinking("阶段6:简单审查与修改", partial),
       ),
     requestOverrides,
     cachePrefix,

+ 1 - 1
src/lib/novel/deep-chapter-golden.spec.ts

@@ -25,7 +25,7 @@ describe("deep chapter prompts with golden three chapter constraints", () => {
       buildDeepChapterDraftPrompt("", "上下文包内容", "写作任务书内容", "生成前三章", 1, golden),
       buildDeepChapterRevisionPrompt("", "上下文包内容", "写作任务书内容", "初稿正文内容", reviewResults, "生成前三章", 1, golden),
       buildDeepChapterExpansionPrompt("", "上下文包内容", "写作任务书内容", "当前正文", "生成前三章", 1, golden),
-      buildDeepChapterFinalPolishPrompt("", "上下文包内容", "写作任务书内容", "当前正文", "生成前三章", 1, golden),
+      buildDeepChapterFinalPolishPrompt("", "上下文包内容", "写作任务书内容", "当前正文", reviewResults, "生成前三章", 1, golden),
     ]
 
     for (const prompt of prompts) {

+ 3 - 3
src/lib/novel/deep-chapter-prompts.cache.spec.ts

@@ -35,7 +35,7 @@ const stages: Record<string, string> = {
   draft: buildDeepChapterDraftPrompt(outline, contextPrompt, "写作任务书", "生成第3章", 3),
   expansion: buildDeepChapterExpansionPrompt(outline, contextPrompt, "写作任务书", "过短正文", "生成第3章", 3),
   revision: buildDeepChapterRevisionPrompt(outline, contextPrompt, "写作任务书", "初稿正文", reviewResults, "生成第3章", 3),
-  polish: buildDeepChapterFinalPolishPrompt(outline, contextPrompt, "写作任务书", "当前正文", "生成第3章", 3),
+  polish: buildDeepChapterFinalPolishPrompt(outline, contextPrompt, "写作任务书", "当前正文", reviewResults, "生成第3章", 3),
 }
 const stageList = Object.values(stages)
 // buildStableContextPrefix(outline, contextPrompt) 的输出:[outline,"上下文:",contextPrompt].join("\n")
@@ -56,8 +56,8 @@ describe("deep chapter prompts share a stable cacheable prefix (stage 2)", () =>
     // ── 测量报告(chars/4 估算 token,与 context-budget.ts 同口径)──
     const prefixChars = stableBlock.length
     const estTok = (chars: number) => Math.round(chars / 4)
-    // 写作侧实际会重复携带这段前缀的阶段:任务书 / 初稿 /(扩写)/(返修)/ 去AI味
-    // 取最常见路径:任务书 + 初稿 + 去AI味 = 3 次必然发生,扩写/返修按需。
+    // 写作侧实际会重复携带这段前缀的阶段:任务书 / 初稿 /(扩写)/(返修)/ 阶段6局部修改
+    // 取最常见路径:任务书 + 初稿 + 阶段6局部修改 = 3 次,扩写/返修/阶段6按需。
     const writingStages = 3
     const repeatedPrefixChars = prefixChars * (writingStages - 1) // 第2、3次重复携带
     // 自动前缀缓存命中后,重复前缀约按 1/10 计费。

+ 13 - 16
src/lib/novel/deep-chapter-prompts.ts

@@ -1,6 +1,5 @@
 import type { NovelReviewResult } from "./review-adapter"
 import { buildGoldenThreeChapterDirective, type GoldenThreeChapterRequest } from "./golden-three-chapters"
-import { CHINESE_NOVEL_DE_AI_RULES } from "./de-ai-rules"
 
 export const DEEP_CHAPTER_TARGET_CHARS = 3000
 export const DEEP_CHAPTER_MIN_CHARS = 2200
@@ -44,7 +43,7 @@ function chapterLengthBoundary(lengthSpec: ChapterLengthSpec): string {
  * 跨阶段稳定上下文前缀:把“完整大纲 + 上下文包”放在每个阶段提示词的最前面,
  * 且在同一次章节生成里逐字节相同。
  *
- * 任务书 / 初稿 / 扩写 / 返修 / 去AI味这几个阶段都由同一份 outline + contextPrompt
+ * 任务书 / 初稿 / 扩写 / 返修 / 阶段6局部修改这几个阶段都由同一份 outline + contextPrompt
  * 拼出相同前缀,DeepSeek / OpenAI 的自动前缀缓存即可命中这段最大的内容,
  * 重复阶段按命中价计费(约 1/10),不再每阶段全价重发整段上下文。
  *
@@ -270,28 +269,23 @@ export function buildDeepChapterFinalPolishPrompt(
   contextPrompt: string,
   taskBrief: string,
   currentContent: string,
+  pendingFixIssues: NovelReviewResult[],
   userRequest: string,
   chapterNumber?: number,
   goldenThreeChapter?: GoldenThreeChapterRequest,
-  customDeAiSkill?: string,
 ): string {
-  const deAiRules = customDeAiSkill && customDeAiSkill.trim() ? customDeAiSkill.trim() : CHINESE_NOVEL_DE_AI_RULES
   return [
     buildStableContextPrefix(outline, contextPrompt),
     "",
-    "你是小说正文最终质检与去AI味助手。",
-    "请对二次审查/返修后的章节做最后一遍简单审查,并进行去AI味处理。",
+    "你是小说正文简单审查与局部修改助手。",
+    "请只根据下方剩余问题清单,对当前正文做局部修改。",
     "",
     "处理目标:",
-    "1. 检查是否存在明显复读、循环段落、前后矛盾、突兀跳转、解释腔和机械套话。",
-    "2. 去掉 AI 味:减少总结腔、模板句、过度解释、相同句式堆叠和空泛形容。",
-    "3. 保留原有剧情事实、人物关系、时间线、伏笔和章节结尾钩子,不要另起新剧情。",
-    "4. 只做必要的自然化、顺滑化和轻量修补,不要大幅重写。",
-    "5. 不再强制压缩到固定字数区间;只做必要的自然化、顺滑化和轻量修补,禁止为了凑字数复读。",
-    `6. ${chapterTitleRequirement()}`,
-    "7. 只输出最终可保存的小说正文,不要输出审查报告、解释或修改说明。",
-    "",
-    deAiRules,
+    "1. 只修改清单中指出的有问题部分。",
+    "2. 保留原有剧情事实、人物关系、时间线、伏笔和章节结尾钩子,不要另起新剧情。",
+    "3. 不再强制压缩到固定字数区间;禁止为了凑字数复读。",
+    `4. ${chapterTitleRequirement()}`,
+    "5. 只输出修改后的完整小说正文,不要输出审查报告、解释或修改说明。",
     "",
     chapterNumber ? `目标章节:第${chapterNumber}章` : "目标章节:用户请求中的章节",
     `用户请求:${userRequest}`,
@@ -300,7 +294,10 @@ export function buildDeepChapterFinalPolishPrompt(
     "写作任务书:",
     taskBrief,
     "",
-    "待最终简单审查与去AI味正文:",
+    "剩余问题:",
+    formatReviewIssues(pendingFixIssues),
+    "",
+    "待局部修改正文:",
     currentContent,
   ].filter(Boolean).join("\n")
 }