ソースを参照

添加AI大纲会话窗口交互重构实施计划

Mochocyang 2 ヶ月 前
親
コミット
aea942574b

+ 1377 - 0
docs/superpowers/plans/2026-07-09-ai-outline-intent-staged-workflow.html

@@ -0,0 +1,1377 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>AI大纲会话窗口交互重构实施计划</title>
+<style>
+  :root {
+    --bg: #0f1117;
+    --surface: #1a1d27;
+    --surface-2: #232733;
+    --border: #2d3142;
+    --text: #e6e8ed;
+    --text-muted: #9aa0ae;
+    --accent: #6cb6ff;
+    --accent-2: #8b7bf0;
+    --success: #4ade80;
+    --warning: #fbbf24;
+    --danger: #f87171;
+    --amber: #f59e0b;
+  }
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  body {
+    background: var(--bg);
+    color: var(--text);
+    font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
+    line-height: 1.7;
+    padding: 2rem;
+    max-width: 1000px;
+    margin: 0 auto;
+  }
+  h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
+  h2 { font-size: 1.35rem; margin-top: 2.5rem; margin-bottom: 1rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.5rem; }
+  h3 { font-size: 1.15rem; margin-top: 1.8rem; margin-bottom: 0.8rem; color: var(--accent-2); }
+  p { margin-bottom: 0.8rem; }
+  ul, ol { margin-left: 1.5rem; margin-bottom: 0.8rem; }
+  li { margin-bottom: 0.3rem; }
+  code {
+    background: var(--surface-2);
+    padding: 0.15rem 0.4rem;
+    border-radius: 4px;
+    font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;
+    font-size: 0.85em;
+    color: var(--amber);
+  }
+  pre {
+    background: var(--surface-2);
+    border: 1px solid var(--border);
+    border-radius: 8px;
+    padding: 1rem 1.2rem;
+    overflow-x: auto;
+    margin: 1rem 0;
+    font-size: 0.82rem;
+    line-height: 1.55;
+  }
+  pre code { background: none; padding: 0; color: var(--text); }
+  table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
+  th, td { border: 1px solid var(--border); padding: 0.5rem 0.7rem; text-align: left; }
+  th { background: var(--surface-2); color: var(--accent); font-weight: 600; }
+  tr:nth-child(even) { background: var(--surface); }
+  .meta { color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1.5rem; }
+  blockquote {
+    background: var(--surface);
+    border-left: 3px solid var(--accent);
+    padding: 0.8rem 1rem;
+    border-radius: 0 6px 6px 0;
+    margin: 1rem 0;
+  }
+  .task-block {
+    background: var(--surface);
+    border: 1px solid var(--border);
+    border-radius: 8px;
+    padding: 1.2rem 1.5rem;
+    margin: 1.5rem 0;
+  }
+  .task-block h3 { margin-top: 0; }
+  .checkbox {
+    display: inline-block;
+    width: 1rem;
+    height: 1rem;
+    border: 1.5px solid var(--text-muted);
+    border-radius: 3px;
+    margin-right: 0.4rem;
+    vertical-align: text-bottom;
+  }
+  .step { margin: 0.6rem 0; padding-left: 0.5rem; }
+  .tag { display: inline-block; padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.75rem; font-weight: 600; }
+  .tag-create { background: rgba(74,222,128,0.15); color: var(--success); }
+  .tag-modify { background: rgba(251,191,36,0.15); color: var(--warning); }
+  .tag-test { background: rgba(107,182,255,0.15); color: var(--accent); }
+</style>
+</head>
+<body>
+
+<h1>AI大纲会话窗口交互重构实施计划</h1>
+
+<blockquote>
+  <strong>执行方式:</strong> 推荐使用 superpowers:subagent-driven-development 逐任务执行,或使用 superpowers:executing-plans 在当前会话中执行。步骤使用 checkbox 风格追踪。
+</blockquote>
+
+<p class="meta">
+  <strong>目标:</strong> 重构AI大纲会话窗口的交互逻辑,新增意图清晰度分析层、渐进式思考阶段展示、每章独立文件输出和下一步推荐机制。<br>
+  <strong>架构:</strong> 方案B — 显式意图闸门阶段 + 独立阶段状态机。所有新逻辑在 outline 专属文件中实现,不改动共享的 workflow-trace.ts / agent-workflow-panel.tsx。<br>
+  <strong>技术栈:</strong> TypeScript, React, Zustand, Tauri<br>
+  <strong>设计文档:</strong> <code>docs/superpowers/specs/2026-07-09-ai-outline-intent-staged-workflow-design.html</code>
+</p>
+
+<hr>
+
+<h2>文件结构总览</h2>
+
+<h3>新增文件</h3>
+<table>
+  <tr><th>文件</th><th>职责</th></tr>
+  <tr><td><code>src/lib/novel/outline-intent-clarity.ts</code></td><td>意图清晰度分析:prompt 构造、IntentClarityResult 解析</td></tr>
+  <tr><td><code>src/lib/novel/outline-stage-trace.ts</code></td><td>大纲专属阶段追踪:基于 Agent 事件构建渐进式 7 阶段</td></tr>
+  <tr><td><code>src/components/sources/outline-workflow-stages.tsx</code></td><td>渐进式阶段渲染组件</td></tr>
+  <tr><td><code>src/lib/novel/outline-next-step.ts</code></td><td>下一步推荐:prompt 构造、NextStepRecommendation 解析</td></tr>
+  <tr><td><code>src/lib/novel/outline-intent-clarity.spec.ts</code></td><td>意图清晰度解析测试</td></tr>
+  <tr><td><code>src/lib/novel/outline-stage-trace.spec.ts</code></td><td>阶段追踪测试</td></tr>
+  <tr><td><code>src/lib/novel/outline-next-step.spec.ts</code></td><td>下一步推荐解析测试</td></tr>
+</table>
+
+<h3>修改文件</h3>
+<table>
+  <tr><th>文件</th><th>改动概要</th></tr>
+  <tr><td><code>src/lib/novel/outline-section-configs.ts</code></td><td>新增 outputMode 字段</td></tr>
+  <tr><td><code>src/lib/novel/outline-workflow-state.ts</code></td><td>新增 intent_analysis / waiting_user_input 阶段</td></tr>
+  <tr><td><code>src/stores/outline-chat-store.ts</code></td><td>消息类型新增 intentPhase / nextStepRecommendation 字段</td></tr>
+  <tr><td><code>src/components/sources/outline-save-confirm-dialog.tsx</code></td><td>支持批量文件列表、部分取消</td></tr>
+  <tr><td><code>src/components/sources/outline-chat-panel.tsx</code></td><td>核心改造:两阶段流程、渐进式面板、意图解析、推荐渲染</td></tr>
+</table>
+
+<hr>
+
+<!-- ==================== Task 1 ==================== -->
+
+<div class="task-block">
+<h3>Task 1: 意图清晰度分析模块 — 类型与解析</h3>
+
+<p><span class="tag tag-create">Create</span> <code>src/lib/novel/outline-intent-clarity.ts</code><br>
+<span class="tag tag-test">Test</span> <code>src/lib/novel/outline-intent-clarity.spec.ts</code></p>
+
+<p><strong>目标:</strong> 定义 IntentClarityResult 类型,实现 parseIntentClarity 函数从 AI 输出中提取 <code>&lt;!-- intent_clarity --&gt;</code> 标记块。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 编写意图解析的失败测试</strong></li>
+</ul>
+
+<pre><code>// src/lib/novel/outline-intent-clarity.spec.ts
+import { describe, it, expect } from "vitest"
+import { parseIntentClarity } from "./outline-intent-clarity"
+
+describe("parseIntentClarity", () => {
+  it("解析 clear 意图", () => {
+    const text = `&lt;!-- intent_clarity --&gt;
+{"clarity":"clear","module":"章节细纲","analysis":"检测到3章缺细纲","detectedScope":"第1-3章","missingItems":["第1章细纲"],"options":[],"question":""}
+&lt;!-- /intent_clarity --&gt;`
+    const result = parseIntentClarity(text)
+    expect(result).not.toBeNull()
+    expect(result!.clarity).toBe("clear")
+    expect(result!.module).toBe("章节细纲")
+    expect(result!.detectedScope).toBe("第1-3章")
+  })
+
+  it("解析 needs_input 意图并提取选项", () => {
+    const text = `&lt;!-- intent_clarity --&gt;
+{"clarity":"needs_input","module":"章节细纲","analysis":"0章有细纲","detectedScope":"","missingItems":[],"options":[{"id":"A","label":"生成全部缺失细纲","description":"第1-35章"},{"id":"D","label":"自定义","description":"自行描述"}],"question":"请问要生成哪些章节的细纲?"}
+&lt;!-- /intent_clarity --&gt;`
+    const result = parseIntentClarity(text)
+    expect(result).not.toBeNull()
+    expect(result!.clarity).toBe("needs_input")
+    expect(result!.options).toHaveLength(2)
+    expect(result!.options[0].id).toBe("A")
+    expect(result!.question).toContain("哪些章节")
+  })
+
+  it("无标记块时返回 null", () => {
+    expect(parseIntentClarity("普通文本无标记")).toBeNull()
+  })
+
+  it("JSON 格式错误时返回 null", () => {
+    const text = `&lt;!-- intent_clarity --&gt;
+{invalid json}
+&lt;!-- /intent_clarity --&gt;`
+    expect(parseIntentClarity(text)).toBeNull()
+  })
+})</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 运行测试验证失败</strong></li>
+</ul>
+
+<p>运行:<code>npx vitest run src/lib/novel/outline-intent-clarity.spec.ts</code><br>
+预期:FAIL — 模块不存在</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 实现意图清晰度分析模块</strong></li>
+</ul>
+
+<pre><code>// src/lib/novel/outline-intent-clarity.ts
+
+export type IntentClarity = "clear" | "needs_input"
+
+export interface IntentClarityOption {
+  id: string
+  label: string
+  description: string
+}
+
+export interface IntentClarityResult {
+  clarity: IntentClarity
+  module: string
+  analysis: string
+  detectedScope: string
+  missingItems: string[]
+  options: IntentClarityOption[]
+  question: string
+}
+
+const CLARITY_PATTERN = /&lt;!--\s*intent_clarity\s*--&gt;([\s\S]*?)&lt;!--\s*\/intent_clarity\s*--&gt;/i
+
+export function parseIntentClarity(text: string): IntentClarityResult | null {
+  const match = text.match(CLARITY_PATTERN)
+  if (!match) return null
+
+  let payload: unknown
+  try {
+    payload = JSON.parse(match[1].trim())
+  } catch {
+    return null
+  }
+
+  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null
+
+  const raw = payload as Record&lt;string, unknown&gt;
+  const clarity = String(raw.clarity ?? "")
+  if (clarity !== "clear" &amp;&amp; clarity !== "needs_input") return null
+
+  const options: IntentClarityOption[] = Array.isArray(raw.options)
+    ? raw.options
+        .filter((item): item is Record&lt;string, unknown&gt; =&gt;
+          Boolean(item) &amp;&amp; typeof item === "object" &amp;&amp; !Array.isArray(item))
+        .map((item) =&gt; ({
+          id: String(item.id ?? ""),
+          label: String(item.label ?? ""),
+          description: String(item.description ?? ""),
+        }))
+        .filter((item) =&gt; item.id &amp;&amp; item.label)
+    : []
+
+  return {
+    clarity,
+    module: String(raw.module ?? ""),
+    analysis: String(raw.analysis ?? ""),
+    detectedScope: String(raw.detectedScope ?? ""),
+    missingItems: Array.isArray(raw.missingItems)
+      ? raw.missingItems.filter((item): item is string =&gt; typeof item === "string")
+      : [],
+    options,
+    question: String(raw.question ?? ""),
+  }
+}
+
+export function buildIntentAnalysisPrompt(title: string, requestHint: string): string {
+  return [
+    `请对以下请求进行意图分析:「生成${title}」`,
+    "",
+    "## 任务",
+    "1. 调用 list_outlines、list_chapters、read_outline 读取已有资料",
+    "2. 判断用户意图是否清晰(能否确定具体生成范围)",
+    "",
+    `## 本分项内容要求`,
+    requestHint,
+    "",
+    "## 章节类模块判定规则",
+    "- 检测已有章节列表,判断是否缺失细纲",
+    "- 若能确定范围(如仅3章缺细纲)→ clear",
+    "- 若大范围缺细纲或无章节信息 → needs_input",
+    "",
+    "## 非章节类模块判定规则",
+    "- 读取已有设定/角色,判断当前卷范围",
+    "- 列出已有项和缺失项",
+    "- 若范围明确且缺失项清晰 → clear",
+    "- 若卷范围不明确 → needs_input",
+    "",
+    "## needs_input 时的推荐选项策略",
+    "必须提供4类选项:",
+    "A. 全部缺失项生成(如「生成前面缺失的细纲」)",
+    "B. 基于已有内容推断(如「根据已有章节内容分析后生成后续细纲」)",
+    "C. 最近范围生成(如「生成最近5-10章的细纲」)",
+    "D. 自定义(由用户描述要生成的内容范围或故事方向)",
+    "",
+    "## 输出格式(必须严格遵守)",
+    "&lt;!-- intent_clarity --&gt;",
+    '按 JSON 输出,字段:clarity("clear"|"needs_input")、module、analysis、detectedScope(clear时填写)、missingItems(数组)、options(needs_input时填4个选项,clear时为空数组)、question(needs_input时填写自然语言提问)',
+    "&lt;!-- /intent_clarity --&gt;",
+    "",
+    "clear 时:只输出上述 JSON,不生成正文。",
+    "needs_input 时:输出 JSON 后,用自然语言在会话中提出澄清问题 + 推荐选项。",
+  ].join("\n")
+}
+</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 4: 运行测试验证通过</strong></li>
+</ul>
+
+<p>运行:<code>npx vitest run src/lib/novel/outline-intent-clarity.spec.ts</code><br>
+预期:PASS</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 5: 提交</strong></p>
+
+<pre><code>git add src/lib/novel/outline-intent-clarity.ts src/lib/novel/outline-intent-clarity.spec.ts
+git commit -m "新增意图清晰度分析模块及测试"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 2 ==================== -->
+
+<div class="task-block">
+<h3>Task 2: 渐进式阶段追踪模块</h3>
+
+<p><span class="tag tag-create">Create</span> <code>src/lib/novel/outline-stage-trace.ts</code><br>
+<span class="tag tag-test">Test</span> <code>src/lib/novel/outline-stage-trace.spec.ts</code></p>
+
+<p><strong>目标:</strong> 定义 7 个大纲专属阶段,基于 Agent 事件流构建渐进式阶段列表。阶段初始为 hidden,检测到对应事件时变为 active/done。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 编写阶段追踪的失败测试</strong></li>
+</ul>
+
+<pre><code>// src/lib/novel/outline-stage-trace.spec.ts
+import { describe, it, expect } from "vitest"
+import { buildOutlineStages, type OutlineStageInput } from "./outline-stage-trace"
+import type { ToolCallRecord } from "@/lib/agent/types"
+
+function makeToolCall(name: string, status: "done" | "running" = "done"): ToolCallRecord {
+  return {
+    id: `${name}-1`,
+    name,
+    params: {},
+    status,
+    startedAt: Date.now() - 1000,
+    finishedAt: status === "done" ? Date.now() : undefined,
+  } as ToolCallRecord
+}
+
+describe("buildOutlineStages", () => {
+  it("无事件时所有阶段为 hidden", () => {
+    const input: OutlineStageInput = { toolCalls: [], content: "", isStreaming: false }
+    const stages = buildOutlineStages(input)
+    expect(stages.every((s) =&gt; s.status === "hidden")).toBe(true)
+  })
+
+  it("检测到 route_task 后任务理解阶段激活", () => {
+    const input: OutlineStageInput = {
+      toolCalls: [makeToolCall("route_task")],
+      content: "",
+      isStreaming: true,
+    }
+    const stages = buildOutlineStages(input)
+    const intentStage = stages.find((s) =&gt; s.kind === "intent")
+    expect(intentStage?.status).toBe("active")
+    // 后续阶段仍为 hidden
+    const skillStage = stages.find((s) =&gt; s.kind === "skill")
+    expect(skillStage?.status).toBe("hidden")
+  })
+
+  it("检测到 intent_clarity 后范围分析阶段激活", () => {
+    const content = `&lt;!-- intent_clarity --&gt;
+{"clarity":"clear","module":"章节细纲","analysis":"3章缺细纲","detectedScope":"第1-3章","missingItems":[],"options":[],"question":""}
+&lt;!-- /intent_clarity --&gt;`
+    const input: OutlineStageInput = {
+      toolCalls: [makeToolCall("route_task")],
+      content,
+      isStreaming: true,
+    }
+    const stages = buildOutlineStages(input)
+    const scopeStage = stages.find((s) =&gt; s.kind === "scope")
+    expect(scopeStage?.status).toBe("active")
+  })
+
+  it("检测到 apply_skill 后技能选择阶段激活,前一阶段变 done", () => {
+    const input: OutlineStageInput = {
+      toolCalls: [makeToolCall("route_task"), makeToolCall("apply_skill")],
+      content: "",
+      isStreaming: true,
+    }
+    const stages = buildOutlineStages(input)
+    const intentStage = stages.find((s) =&gt; s.kind === "intent")
+    const skillStage = stages.find((s) =&gt; s.kind === "skill")
+    expect(intentStage?.status).toBe("done")
+    expect(skillStage?.status).toBe("active")
+  })
+
+  it("检测到 read_outline 后上下文准备阶段激活", () => {
+    const input: OutlineStageInput = {
+      toolCalls: [makeToolCall("route_task"), makeToolCall("read_outline")],
+      content: "",
+      isStreaming: true,
+    }
+    const stages = buildOutlineStages(input)
+    const ctxStage = stages.find((s) =&gt; s.kind === "context")
+    expect(ctxStage?.status).toBe("active")
+  })
+
+  it("检测到 thinking 块后思考阶段激活", () => {
+    const input: OutlineStageInput = {
+      toolCalls: [],
+      content: "&lt;thinking&gt;正在分析角色动机...&lt;/thinking&gt;",
+      isStreaming: true,
+    }
+    const stages = buildOutlineStages(input)
+    const thinkingStage = stages.find((s) =&gt; s.kind === "thinking")
+    expect(thinkingStage?.status).toBe("active")
+  })
+
+  it("流结束后所有已激活阶段变为 done", () => {
+    const input: OutlineStageInput = {
+      toolCalls: [makeToolCall("route_task"), makeToolCall("apply_skill")],
+      content: "",
+      isStreaming: false,
+    }
+    const stages = buildOutlineStages(input)
+    const activeStages = stages.filter((s) =&gt; s.status === "active")
+    const doneStages = stages.filter((s) =&gt; s.status === "done")
+    expect(activeStages.length).toBe(0)
+    expect(doneStages.length).toBeGreaterThanOrEqual(2)
+  })
+})</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 运行测试验证失败</strong></li>
+</ul>
+
+<p>运行:<code>npx vitest run src/lib/novel/outline-stage-trace.spec.ts</code><br>
+预期:FAIL — 模块不存在</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 实现阶段追踪模块</strong></li>
+</ul>
+
+<pre><code>// src/lib/novel/outline-stage-trace.ts
+import type { ToolCallRecord } from "@/lib/agent/types"
+
+export type OutlineStageKind =
+  | "intent"
+  | "scope"
+  | "skill"
+  | "context"
+  | "tool"
+  | "thinking"
+  | "generation"
+
+export type OutlineStageStatus = "hidden" | "active" | "done"
+
+export interface OutlineStage {
+  kind: OutlineStageKind
+  title: string
+  status: OutlineStageStatus
+  summary: string
+  details: string[]
+  startedAt?: number
+  finishedAt?: number
+}
+
+export interface OutlineStageInput {
+  toolCalls: ToolCallRecord[]
+  content: string
+  isStreaming: boolean
+}
+
+const LIST_TOOLS = new Set(["list_chapters", "list_outlines", "list_memories", "list_deductions"])
+const READ_TOOLS = new Set(["read_chapter", "read_outline", "read_memory", "read_deduction", "read_chat_history"])
+const SKILL_TOOLS = new Set(["apply_skill"])
+const ROUTE_TOOLS = new Set(["route_task"])
+const WRITE_TOOLS = new Set(["write_outline_node", "write_chapter_outline"])
+const INTENT_CLARITY_PATTERN = /&lt;!--\s*intent_clarity\s*--&gt;[\s\S]*?&lt;!--\s*\/intent_clarity\s*--&gt;/i
+const THINKING_PATTERN = /&lt;(?:think|thinking)&gt;([\s\S]*?)&lt;\/\1&gt;/i
+
+function hasIntentClarity(content: string): boolean {
+  return INTENT_CLARITY_PATTERN.test(content)
+}
+
+function hasThinking(content: string): boolean {
+  return THINKING_PATTERN.test(content)
+}
+
+function hasGenerationStarted(content: string): boolean {
+  // 正文输出开始 = 有非 thinking、非 intent_clarity 的文本
+  const withoutThinking = content.replace(THINKING_PATTERN, "")
+  const withoutClarity = withoutThinking.replace(INTENT_CLARITY_PATTERN, "")
+  return withoutClarity.trim().length > 0
+}
+
+export function buildOutlineStages(input: OutlineStageInput): OutlineStage[] {
+  const { toolCalls, content, isStreaming } = input
+
+  const hasRoute = toolCalls.some((c) =&gt; ROUTE_TOOLS.has(c.name))
+  const hasSkill = toolCalls.some((c) =&gt; SKILL_TOOLS.has(c.name))
+  const hasRead = toolCalls.some((c) =&gt; LIST_TOOLS.has(c.name) || READ_TOOLS.has(c.name))
+  const hasWrite = toolCalls.some((c) =&gt; WRITE_TOOLS.has(c.name))
+  const hasClarity = hasIntentClarity(content)
+  const hasThinkingBlock = hasThinking(content)
+  const hasOutput = hasGenerationStarted(content)
+
+  // 阶段激活顺序
+  const activations = [hasRoute, hasClarity, hasSkill, hasRead, hasWrite, hasThinkingBlock, hasOutput]
+
+  const stageDefs: Array&lt;Omit&lt;OutlineStage, "status"&gt;&gt; = [
+    { kind: "intent", title: "任务理解", summary: "", details: [] },
+    { kind: "scope", title: "范围分析", summary: "", details: [] },
+    { kind: "skill", title: "技能选择", summary: "", details: [] },
+    { kind: "context", title: "上下文准备", summary: "", details: [] },
+    { kind: "tool", title: "工具调用", summary: "", details: [] },
+    { kind: "thinking", title: "思考与角色", summary: "", details: [] },
+    { kind: "generation", title: "生成与校验", summary: "", details: [] },
+  ]
+
+  // 找到最后一个激活的阶段索引
+  let lastActiveIndex = -1
+  for (let i = activations.length - 1; i &gt;= 0; i--) {
+    if (activations[i]) {
+      lastActiveIndex = i
+      break
+    }
+  }
+
+  return stageDefs.map((def, index) =&gt; {
+    const activated = activations[index]
+    let status: OutlineStageStatus = "hidden"
+
+    if (activated) {
+      if (!isStreaming || index &lt; lastActiveIndex) {
+        status = "done"
+      } else {
+        status = "active"
+      }
+    }
+
+    // 流结束时所有已激活阶段为 done
+    if (activated &amp;&amp; !isStreaming) {
+      status = "done"
+    }
+
+    return { ...def, status }
+  })
+}
+</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 4: 运行测试验证通过</strong></li>
+</ul>
+
+<p>运行:<code>npx vitest run src/lib/novel/outline-stage-trace.spec.ts</code><br>
+预期:PASS</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 5: 提交</strong></p>
+
+<pre><code>git add src/lib/novel/outline-stage-trace.ts src/lib/novel/outline-stage-trace.spec.ts
+git commit -m "新增大纲渐进式阶段追踪模块及测试"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 3 ==================== -->
+
+<div class="task-block">
+<h3>Task 3: 下一步推荐模块</h3>
+
+<p><span class="tag tag-create">Create</span> <code>src/lib/novel/outline-next-step.ts</code><br>
+<span class="tag tag-test">Test</span> <code>src/lib/novel/outline-next-step.spec.ts</code></p>
+
+<p><strong>目标:</strong> 定义 NextStepRecommendation 类型,实现 parseNextStep 函数从 AI 输出中提取 <code>&lt;!-- next_step --&gt;</code> 标记块。严禁推荐正文生成。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 编写下一步推荐的失败测试</strong></li>
+</ul>
+
+<pre><code>// src/lib/novel/outline-next-step.spec.ts
+import { describe, it, expect } from "vitest"
+import { parseNextStep, isRecommendationForbidden } from "./outline-next-step"
+
+describe("parseNextStep", () => {
+  it("解析正常推荐", () => {
+    const text = `&lt;!-- next_step --&gt;
+{"completedModule":"章节细纲","completedScope":"第1-3章细纲","recommendations":[{"id":"A","label":"完善人物小传","reason":"提及3个未立传角色"},{"id":"D","label":"自定义","reason":"由你描述"}]}
+&lt;!-- /next_step --&gt;`
+    const result = parseNextStep(text)
+    expect(result).not.toBeNull()
+    expect(result!.completedModule).toBe("章节细纲")
+    expect(result!.recommendations).toHaveLength(2)
+  })
+
+  it("无标记块时返回 null", () =&gt; {
+    expect(parseNextStep("普通文本")).toBeNull()
+  })
+})
+
+describe("isRecommendationForbidden", () => {
+  it("禁止包含正文生成关键词", () =&gt; {
+    expect(isRecommendationForbidden("生成对应章节正文")).toBe(true)
+    expect(isRecommendationForbidden("写正文")).toBe(true)
+    expect(isRecommendationForbidden("生成章节内容")).toBe(true)
+  })
+
+  it("允许大纲体系内的推荐", () =&gt; {
+    expect(isRecommendationForbidden("完善人物小传")).toBe(false)
+    expect(isRecommendationForbidden("生成组织势力设定")).toBe(false)
+  })
+})
+</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 运行测试验证失败</strong></li>
+</ul>
+
+<p>运行:<code>npx vitest run src/lib/novel/outline-next-step.spec.ts</code><br>
+预期:FAIL — 模块不存在</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 实现下一步推荐模块</strong></li>
+</ul>
+
+<pre><code>// src/lib/novel/outline-next-step.ts
+
+export interface NextStepRecommendationItem {
+  id: string
+  label: string
+  reason: string
+}
+
+export interface NextStepRecommendation {
+  completedModule: string
+  completedScope: string
+  recommendations: NextStepRecommendationItem[]
+}
+
+const NEXT_STEP_PATTERN = /&lt;!--\s*next_step\s*--&gt;([\s\S]*?)&lt;!--\s*\/next_step\s*--&gt;/i
+
+// 严禁推荐的正文生成关键词
+const FORBIDDEN_PATTERNS = [
+  /生成.*正文/,
+  /写.*正文/,
+  /生成.*章节内容/,
+  /生成.*正文章节/,
+  /章节正文/,
+  /写正文/,
+  /生成正文/,
+]
+
+export function isRecommendationForbidden(label: string): boolean {
+  return FORBIDDEN_PATTERNS.some((pattern) =&gt; pattern.test(label))
+}
+
+export function parseNextStep(text: string): NextStepRecommendation | null {
+  const match = text.match(NEXT_STEP_PATTERN)
+  if (!match) return null
+
+  let payload: unknown
+  try {
+    payload = JSON.parse(match[1].trim())
+  } catch {
+    return null
+  }
+
+  if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null
+
+  const raw = payload as Record&lt;string, unknown&gt;
+
+  const recommendations: NextStepRecommendationItem[] = Array.isArray(raw.recommendations)
+    ? raw.recommendations
+        .filter((item): item is Record&lt;string, unknown&gt; =&gt;
+          Boolean(item) &amp;&amp; typeof item === "object" &amp;&amp; !Array.isArray(item))
+        .map((item) =&gt; ({
+          id: String(item.id ?? ""),
+          label: String(item.label ?? ""),
+          reason: String(item.reason ?? ""),
+        }))
+        .filter((item) =&gt; item.id &amp;&amp; item.label)
+        // 过滤掉被禁止的推荐(正文生成)
+        .filter((item) =&gt; !isRecommendationForbidden(item.label))
+    : []
+
+  return {
+    completedModule: String(raw.completedModule ?? ""),
+    completedScope: String(raw.completedScope ?? ""),
+    recommendations,
+  }
+}
+
+export function buildNextStepPromptSuffix(): string {
+  return [
+    "",
+    "## 下一步推荐输出要求",
+    "生成完成后,在回复末尾附加 &lt;!-- next_step --&gt; 标记块,按 JSON 输出。",
+    '字段:completedModule、completedScope、recommendations(数组,每项含 id、label、reason)。',
+    "推荐方向仅限大纲体系内:人物小传、组织势力设定、力量体系、金手指设定、背景设定、地理设定、伏笔计划、地点设定、章节细纲。",
+    "严禁推荐「生成章节正文」「写正文」等正文生成类操作。",
+    "推荐应由 AI 根据刚生成内容中提及但尚未建立的关联项动态推断。",
+    "必须包含一个 id 为 D 的「自定义」选项。",
+  ].join("\n")
+}
+</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 4: 运行测试验证通过</strong></li>
+</ul>
+
+<p>运行:<code>npx vitest run src/lib/novel/outline-next-step.spec.ts</code><br>
+预期:PASS</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 5: 提交</strong></p>
+
+<pre><code>git add src/lib/novel/outline-next-step.ts src/lib/novel/outline-next-step.spec.ts
+git commit -m "新增下一步推荐模块及测试,含正文生成禁止过滤"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 4 ==================== -->
+
+<div class="task-block">
+<h3>Task 4: 扩展配置与状态机</h3>
+
+<p><span class="tag tag-modify">Modify</span> <code>src/lib/novel/outline-section-configs.ts</code><br>
+<span class="tag tag-modify">Modify</span> <code>src/lib/novel/outline-workflow-state.ts</code><br>
+<span class="tag tag-modify">Modify</span> <code>src/stores/outline-chat-store.ts</code></p>
+
+<p><strong>目标:</strong> 在配置中新增 outputMode 字段,在状态机中新增 intent_analysis / waiting_user_input 阶段,在消息类型中新增 intentPhase / nextStepRecommendation 字段。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 修改 outline-section-configs.ts,新增 outputMode</strong></li>
+</ul>
+
+<pre><code>// 在 OutlineSectionGenerationConfig 接口新增字段
+export type OutlineOutputMode = "per_chapter" | "per_item" | "single"
+
+export interface OutlineSectionGenerationConfig {
+  key: OutlineSectionGenerationKey
+  title: string
+  englishTitle: string
+  englishFileName: string
+  requestHint: string
+  outputMode: OutlineOutputMode  // 新增
+}
+
+// 在每个配置项中添加 outputMode
+// chapterOutlines → "per_chapter"
+// characterBriefs → "per_item"
+// organizationsOutline → "per_item"
+// powerSystem → "per_item"
+// goldenFinger → "per_item"
+// backgroundSetting → "single"
+// geographySetting → "per_item"
+// foreshadowingPlan → "per_item"
+// locationsOutline → "per_item"</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 修改 outline-workflow-state.ts,新增两个阶段</strong></li>
+</ul>
+
+<pre><code>// 在 OutlineWorkflowStage 类型新增
+export type OutlineWorkflowStage =
+  | "idle"
+  | "intent_analysis"          // 新增
+  | "waiting_user_input"       // 新增
+  | "collecting_requirements"
+  | "sufficiency_check"
+  | "generation_plan"
+  | "waiting_user_confirm"
+  | "multi_agent_planning"
+  | "running_subagents"
+  | "merging_results"
+  | "quality_check"
+  | "auto_repair"
+  | "preview"
+  | "waiting_save_confirm"
+  | "saved"
+
+// 在 ALLOWED_TRANSITIONS 新增
+const ALLOWED_TRANSITIONS: Record&lt;OutlineWorkflowStage, OutlineWorkflowStage[]&gt; = {
+  idle: ["intent_analysis", "collecting_requirements"],
+  intent_analysis: ["sufficiency_check", "waiting_user_input"],
+  waiting_user_input: ["sufficiency_check"],
+  collecting_requirements: ["sufficiency_check"],
+  // ... 其余不变
+}</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 修改 outline-chat-store.ts,消息类型新增字段</strong></li>
+</ul>
+
+<pre><code>// 在 OutlineChatMessage 接口新增
+import type { IntentClarityResult } from "@/lib/novel/outline-intent-clarity"
+import type { NextStepRecommendation } from "@/lib/novel/outline-next-step"
+
+export interface OutlineChatMessage {
+  id: string
+  role: "user" | "assistant"
+  content: string
+  sources?: string[]
+  agentToolCalls?: AgentRunRecord["toolCalls"]
+  multiAgentRun?: OutlineMultiAgentRunState
+  showThinkingProcess?: boolean
+  isAgentRunning?: boolean
+  attachedReferences?: ReferenceToken[]
+  intentPhase?: "intent_analysis" | "generation" | "waiting_user_input"  // 新增
+  intentClarityResult?: IntentClarityResult | null  // 新增
+  nextStepRecommendation?: NextStepRecommendation | null  // 新增
+}</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 4: 运行 typecheck 验证</strong></li>
+</ul>
+
+<p>运行:<code>npx tsc --noEmit</code><br>
+预期:无新增错误(可能出现测试文件缺失的导入错误,后续任务会解决)</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 5: 提交</strong></p>
+
+<pre><code>git add src/lib/novel/outline-section-configs.ts src/lib/novel/outline-workflow-state.ts src/stores/outline-chat-store.ts
+git commit -m "扩展配置outputMode、状态机intent阶段、消息类型字段"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 5 ==================== -->
+
+<div class="task-block">
+<h3>Task 5: 渐进式阶段渲染组件</h3>
+
+<p><span class="tag tag-create">Create</span> <code>src/components/sources/outline-workflow-stages.tsx</code></p>
+
+<p><strong>目标:</strong> 实现渐进式阶段渲染组件,hidden 阶段不渲染,done 阶段折叠为单行摘要,active 阶段展开显示详情。新阶段出现时有过渡动画。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 实现渲染组件</strong></p>
+</ul>
+
+<pre><code>// src/components/sources/outline-workflow-stages.tsx
+import React, { useState } from "react"
+import { CheckCircle2, ChevronRight, Loader2 } from "lucide-react"
+import { cn } from "@/lib/utils"
+import { buildOutlineStages, type OutlineStageInput, type OutlineStage } from "@/lib/novel/outline-stage-trace"
+
+interface OutlineWorkflowStagesProps extends OutlineStageInput {}
+
+function StageRow({ stage, open, onToggle }: {
+  stage: OutlineStage
+  open: boolean
+  onToggle: () =&gt; void
+}) {
+  return (
+    &lt;div
+      className={cn(
+        "rounded-md border transition-all duration-250 ease-out",
+        stage.status === "active" &amp;&amp; "border-sky-500/40 bg-sky-50/30 dark:bg-sky-950/10",
+        stage.status === "done" &amp;&amp; "border-emerald-500/20 bg-emerald-50/10 dark:bg-emerald-950/5",
+        "opacity-0 translate-y-1 animate-[fadeIn_250ms_ease-out_forwards]"
+      )}
+    &gt;
+      &lt;button
+        type="button"
+        onClick={onToggle}
+        className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs"
+      &gt;
+        {stage.status === "done" ? (
+          &lt;CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" /&gt;
+        ) : stage.status === "active" ? (
+          &lt;Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-sky-600 dark:text-sky-400" /&gt;
+        ) : null}
+        &lt;span className="font-medium"&gt;{stage.title}&lt;/span&gt;
+        {stage.summary &amp;&amp; (
+          &lt;span className="text-muted-foreground"&gt;· {stage.summary}&lt;/span&gt;
+        )}
+        &lt;ChevronRight
+          className={cn(
+            "ml-auto h-3 w-3 transition-transform",
+            open &amp;&amp; "rotate-90"
+          )}
+        /&gt;
+      &lt;/button&gt;
+      {open &amp;&amp; stage.details.length &gt; 0 &amp;&amp; (
+        &lt;div className="border-t border-border/50 px-3 py-2 text-xs text-muted-foreground space-y-1"&gt;
+          {stage.details.map((detail, i) =&gt; (
+            &lt;div key={i}&gt;{detail}&lt;/div&gt;
+          ))}
+        &lt;/div&gt;
+      )}
+    &lt;/div&gt;
+  )
+}
+
+export function OutlineWorkflowStages(props: OutlineWorkflowStagesProps) {
+  const stages = buildOutlineStages(props)
+  const visibleStages = stages.filter((s) =&gt; s.status !== "hidden")
+
+  // 默认展开 active 阶段
+  const defaultOpenId = stages.find((s) =&gt; s.status === "active")?.kind ?? null
+  const [openMap, setOpenMap] = useState&lt;Record&lt;string, boolean&gt;&gt;({})
+
+  if (visibleStages.length === 0) return null
+
+  return (
+    &lt;div className="mb-2 space-y-1"&gt;
+      &lt;div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground mb-1"&gt;
+        &lt;span&gt;思考过程&lt;/span&gt;
+      &lt;/div&gt;
+      {visibleStages.map((stage) =&gt; {
+        const isOpen = openMap[stage.kind] ?? (stage.status === "active")
+        return (
+          &lt;StageRow
+            key={stage.kind}
+            stage={stage}
+            open={isOpen}
+            onToggle={() =&gt; setOpenMap((prev) =&gt; ({ ...prev, [stage.kind]: !isOpen }))}
+          /&gt;
+        )
+      })}
+    &lt;/div&gt;
+  )
+}
+</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 在全局 CSS 中添加 fadeIn keyframe</strong></p>
+
+<p>检查项目是否已有 fadeIn 动画。如果没有,在 <code>src/index.css</code> 或全局样式文件中添加:</p>
+
+<pre><code>@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(4px); }
+  to { opacity: 1; transform: translateY(0); }
+}</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 运行 typecheck 验证</strong></li>
+</ul>
+
+<p>运行:<code>npx tsc --noEmit</code><br>
+预期:无新增错误</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 4: 提交</strong></p>
+
+<pre><code>git add src/components/sources/outline-workflow-stages.tsx
+git commit -m "新增渐进式阶段渲染组件"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 6 ==================== -->
+
+<div class="task-block">
+<h3>Task 6: 批量保存确认 — 支持部分取消</h3>
+
+<p><span class="tag tag-modify">Modify</span> <code>src/components/sources/outline-save-confirm-dialog.tsx</code></p>
+
+<p><strong>目标:</strong> 在 normal 模式下,为每个文件增加勾选框,允许用户取消部分文件,只保存勾选的文件。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 修改 OutlineSaveConfirmDialog,normal 模式增加勾选功能</strong></p>
+</ul>
+
+<p>在 <code>outline-save-confirm-dialog.tsx</code> 的 normal 模式渲染部分,为每个文件添加 checkbox:</p>
+
+<pre><code>// 在 normalRequests 的渲染部分,每个 request 卡片内增加勾选框
+// 新增状态:记录被取消勾选的 fileName 集合
+const [deselectedFiles, setDeselectedFiles] = useState&lt;Set&lt;string&gt;&gt;(new Set())
+
+// 在每个 request 卡片的顶部增加 checkbox
+&lt;input
+  type="checkbox"
+  checked={!deselectedFiles.has(request.fileName)}
+  onChange={(e) =&gt; {
+    setDeselectedFiles((prev) =&gt; {
+      const next = new Set(prev)
+      if (e.target.checked) {
+        next.delete(request.fileName)
+      } else {
+        next.add(request.fileName)
+      }
+      return next
+    })
+  }}
+  className="h-4 w-4 accent-primary"
+/&gt;
+
+// 确认按钮的 onClick 中过滤掉被取消的文件
+onClick={() =&gt; {
+  const filteredRequests = normalRequests.filter(
+    (r) =&gt; !deselectedFiles.has(r.fileName)
+  )
+  onConfirm({
+    requests: mode === "character" ? requests : filteredRequests,
+    characterDrafts: selectedDrafts,
+  })
+}}
+
+// 禁用条件:如果所有文件都被取消则禁用确认按钮
+disabled={
+  (mode === "character" &amp;&amp; selectedDrafts.length === 0) ||
+  (mode !== "character" &amp;&amp; normalRequests.length - deselectedFiles.size === 0)
+}
+</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 运行 typecheck 验证</strong></li>
+</ul>
+
+<p>运行:<code>npx tsc --noEmit</code><br>
+预期:无新增错误</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 提交</strong></p>
+
+<pre><code>git add src/components/sources/outline-save-confirm-dialog.tsx
+git commit -m "批量保存确认支持部分取消"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 7 ==================== -->
+
+<div class="task-block">
+<h3>Task 7: 核心改造 — outline-chat-panel.tsx 接入两阶段流程</h3>
+
+<p><span class="tag tag-modify">Modify</span> <code>src/components/sources/outline-chat-panel.tsx</code></p>
+
+<p><strong>目标:</strong> 改造 handleGenerateSection 为两阶段流程(意图分析→生成),替换 OutlineThinkingBlock 为 OutlineWorkflowStages,新增意图解析、推荐选项渲染和下一步推荐渲染。</p>
+
+<p><strong>注意:</strong> 这是最大改动任务,分为多个子步骤。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 添加新模块的 import</strong></p>
+</ul>
+
+<pre><code>// 在 outline-chat-panel.tsx 顶部 import 区域添加
+import {
+  buildIntentAnalysisPrompt,
+  parseIntentClarity,
+  type IntentClarityResult,
+} from "@/lib/novel/outline-intent-clarity"
+import {
+  parseNextStep,
+  buildNextStepPromptSuffix,
+  type NextStepRecommendation,
+} from "@/lib/novel/outline-next-step"
+import { OutlineWorkflowStages } from "@/components/sources/outline-workflow-stages"</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 改造 buildOutlineSectionGenerationPrompt,拆分为两个函数</strong></p>
+</ul>
+
+<pre><code>// 保留原 buildOutlineSectionGenerationPrompt 改名为 buildGenerationPrompt
+// 生成阶段的 prompt,附加 next_step 输出要求
+function buildGenerationPrompt(
+  title: string,
+  requestHint: string,
+  scope?: string,
+): string {
+  return [
+    `请按「AI大纲生成工作流」生成「${title}」。`,
+    scope ? `\n## 已确认范围\n${scope}\n` : "",
+    "## PRD 3.1 主流程要求",
+    "1. 提取请求关键词。2. 识别用户意图。3. 读取资料。4. 提取关键内容。",
+    "5. 结合 skill + soul.md 生成可直接保存的大纲正文。6. 结果强约束收敛。",
+    "",
+    "## 本分项内容要求",
+    requestHint,
+    getOutlineSectionOutputRules(title),
+    "",
+    "## 文件输出要求",
+    "每个章节/每个项必须输出独立的 outlineSaveRequest,每个对应一个独立文件。",
+    "文件名格式:章节细纲/第N章-章节标题.md 或 人物小传/角色名.md",
+    "",
+    "如果资料足够,直接输出完整正文。",
+    buildNextStepPromptSuffix(),
+  ].join("\n")
+}</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 改造 handleGenerateSection 为两阶段流程</strong></p>
+</ul>
+
+<pre><code>// 新增模块级状态:暂存上次意图分析结果
+const [lastIntentResult, setLastIntentResult] = useState&lt;IntentClarityResult | null&gt;(null)
+const [isIntentPhase, setIsIntentPhase] = useState(false)
+
+// 改造 handleGenerateSection
+const handleGenerateSection = useCallback(
+  (title: string, requestHint: string) =&gt; {
+    // Phase 1: 意图分析
+    setIsIntentPhase(true)
+    const intentPrompt = buildIntentAnalysisPrompt(title, requestHint)
+    void handleSend(intentPrompt, undefined, { intentPhase: "intent_analysis" })
+  },
+  [handleSend],
+)
+
+// 新增:Agent 流结束后检查意图分析结果
+// 在 handleSend 的流结束回调中(onFinish 或 streamEnd 处理)
+// 添加意图解析逻辑:
+//
+// if (isIntentPhase) {
+//   const lastMessage = getCurrentLastMessage()
+//   if (lastMessage) {
+//     const result = parseIntentClarity(lastMessage.content)
+//     if (result) {
+//       setLastIntentResult(result)
+//       if (result.clarity === "clear") {
+//         // 自动注入生成 prompt
+//         const scope = result.detectedScope
+//         setIsIntentPhase(false)
+//         void handleSend(
+//           buildGenerationPrompt(title, requestHint, scope),
+//           undefined,
+//           { intentPhase: "generation" }
+//         )
+//       } else {
+//         // needs_input: 渲染推荐选项,等待用户输入
+//         // 设置消息 intentClarityResult 供 UI 渲染
+//         setIsIntentPhase(false)
+//       }
+//     }
+//   }
+// }</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 4: 新增推荐选项渲染组件(内联在 OutlineAssistantMessage 中)</strong></p>
+</ul>
+
+<pre><code>// 在 OutlineAssistantMessage 组件中,当 msg.intentClarityResult?.clarity === "needs_input"
+// 时渲染推荐选项卡片
+//
+// {msg.intentClarityResult?.clarity === "needs_input" &amp;&amp; (
+//   &lt;div className="mt-3 rounded-md border border-amber-500/30 bg-amber-50/30 p-3 dark:bg-amber-950/10"&gt;
+//     &lt;div className="text-sm font-medium mb-2"&gt;{msg.intentClarityResult.question || "请选择生成范围:"}&lt;/div&gt;
+//     &lt;div className="space-y-2"&gt;
+//       {msg.intentClarityResult.options.map((option) =&gt; (
+//         &lt;button
+//           key={option.id}
+//           className="w-full rounded-md border px-3 py-2 text-left text-sm hover:bg-accent transition-colors"
+//           onClick={() =&gt; {
+//             if (option.id === "D") {
+//               // 聚焦输入框等待用户输入
+//               inputRef.current?.focus()
+//             } else {
+//               // 直接注入用户选择作为生成范围
+//               void handleSend(
+//                 buildGenerationPrompt(
+//                   msg.intentClarityResult.module,
+//                   "", // requestHint 从配置重新获取
+//                   option.description,
+//                 ),
+//                 undefined,
+//                 { intentPhase: "generation" }
+//               )
+//             }
+//           }}
+//         &gt;
+//           &lt;div className="font-medium"&gt;{option.label}&lt;/div&gt;
+//           &lt;div className="text-xs text-muted-foreground"&gt;{option.description}&lt;/div&gt;
+//         &lt;/button&gt;
+//       ))}
+//     &lt;/div&gt;
+//   &lt;/div&gt;
+// )}</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 5: 新增下一步推荐渲染组件(内联在消息渲染末尾)</strong></p>
+</ul>
+
+<pre><code>// 在消息渲染中,当 msg.nextStepRecommendation 存在时渲染推荐卡片
+//
+// {msg.nextStepRecommendation &amp;&amp; msg.nextStepRecommendation.recommendations.length &gt; 0 &amp;&amp; (
+//   &lt;div className="mt-3 rounded-md border border-sky-500/30 bg-sky-50/30 p-3 dark:bg-sky-950/10"&gt;
+//     &lt;div className="text-sm font-medium mb-2"&gt;接下来想做什么?&lt;/div&gt;
+//     &lt;div className="space-y-2"&gt;
+//       {msg.nextStepRecommendation.recommendations.map((rec) =&gt; (
+//         &lt;button
+//           key={rec.id}
+//           className="w-full rounded-md border px-3 py-2 text-left text-sm hover:bg-accent transition-colors"
+//           onClick={() =&gt; {
+//             if (rec.id === "D") {
+//               inputRef.current?.focus()
+//             } else {
+//               // 查找对应模块配置并触发生成
+//               const config = OUTLINE_SECTION_GENERATION_CONFIGS.find(c =&gt; c.title === rec.label)
+//               if (config) {
+//                 handleGenerateSection(config.title, config.requestHint)
+//               }
+//             }
+//           }}
+//         &gt;
+//           &lt;div className="font-medium"&gt;{rec.label}&lt;/div&gt;
+//           &lt;div className="text-xs text-muted-foreground"&gt;{rec.reason}&lt;/div&gt;
+//         &lt;/button&gt;
+//       ))}
+//     &lt;/div&gt;
+//   &lt;/div&gt;
+// )}</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 6: 替换 OutlineThinkingBlock 为 OutlineWorkflowStages</strong></p>
+</ul>
+
+<p>在 [OutlineAssistantMessage](file:///c:/QMAI_C/QMAI-main/src/components/sources/outline-chat-panel.tsx#L632) 渲染中,将原来的 <code>&lt;OutlineThinkingBlock content={visibleThinking} open={isStreaming} /&gt;</code> 替换为:</p>
+
+<pre><code>// 替换 OutlineThinkingBlock
+&lt;OutlineWorkflowStages
+  toolCalls={msg.agentToolCalls ?? []}
+  content={displayContent}
+  isStreaming={isStreaming &amp;&amp; index === activeMessagesLength - 1}
+/&gt;</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 7: 在流结束回调中解析 next_step,存入消息</strong></p>
+</ul>
+
+<pre><code>// 在 Agent 流结束的处理逻辑中(streamEnd / onFinish)
+// 添加 next_step 解析:
+//
+// const nextStep = parseNextStep(finalContent)
+// if (nextStep) {
+//   // 更新消息的 nextStepRecommendation 字段
+//   updateMessage(convId, msgId, { nextStepRecommendation: nextStep })
+// }</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 8: 运行 typecheck 验证</strong></p>
+</ul>
+
+<p>运行:<code>npx tsc --noEmit</code><br>
+预期:无新增错误</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 9: 运行已有大纲面板测试验证无回归</strong></p>
+</ul>
+
+<p>运行:<code>npx vitest run src/components/sources/outline-chat-panel.spec.tsx</code><br>
+预期:原有测试 PASS(如果测试因函数签名变更而失败,需同步更新测试)</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 10: 提交</strong></p>
+
+<pre><code>git add src/components/sources/outline-chat-panel.tsx
+git commit -m "核心改造:两阶段流程、渐进式面板、意图解析、推荐渲染"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 8 ==================== -->
+
+<div class="task-block">
+<h3>Task 8: 系统提示词改造</h3>
+
+<p><span class="tag tag-modify">Modify</span> <code>src/components/sources/outline-chat-panel.tsx</code> — <code>buildOutlineAgentSystemPrompt</code> 函数</p>
+
+<p><strong>目标:</strong> 在系统提示词中新增意图分析阶段约束和 intent_clarity 输出格式要求。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 在 buildOutlineAgentSystemPrompt 中新增意图分析约束段落</strong></p>
+</ul>
+
+<p>在 <code>buildOutlineAgentSystemPrompt</code> 函数(约 L253-L312)的返回数组中,在 "## AI大纲生成工作流" 段落之后新增:</p>
+
+<pre><code>// 在 buildOutlineAgentSystemPrompt 返回数组中新增
+"## 意图清晰度分析阶段",
+"当用户请求生成大纲分项时,必须先进行意图清晰度分析:",
+"1. 调用 list_outlines、list_chapters、read_outline 读取已有资料",
+"2. 判断用户意图是否清晰(能否确定具体生成范围)",
+"3. 输出 &lt;!-- intent_clarity --&gt; JSON 标记块",
+"4. clear 时:只输出 JSON,不生成正文,等待系统自动注入生成指令",
+"5. needs_input 时:输出 JSON 后用自然语言提出澄清问题 + 4个推荐选项",
+"推荐选项必须包含:A.全部缺失项 B.基于已有内容推断 C.最近范围 D.自定义",
+"用户选择或回复后,直接进入生成流程,不再二次分析。",
+"",
+"## 下一步推荐输出",
+"生成完成后,在回复末尾附加 &lt;!-- next_step --&gt; JSON 标记块。",
+"推荐方向仅限大纲体系内(人物小传、组织势力、力量体系等),严禁推荐正文生成。",
+"必须包含一个 id 为 D 的自定义选项。",</code></pre>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 运行 typecheck 验证</strong></li>
+</ul>
+
+<p>运行:<code>npx tsc --noEmit</code><br>
+预期:无新增错误</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 提交</strong></p>
+
+<pre><code>git add src/components/sources/outline-chat-panel.tsx
+git commit -m "系统提示词新增意图分析和下一步推荐输出约束"</code></pre>
+</ul>
+
+</div>
+
+<!-- ==================== Task 9 ==================== -->
+
+<div class="task-block">
+<h3>Task 9: 回归测试与最终验证</h3>
+
+<p><strong>目标:</strong> 全面验证改动不影响其他功能,所有测试通过。</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 1: 运行全部新增模块测试</strong></p>
+
+<pre><code>npx vitest run src/lib/novel/outline-intent-clarity.spec.ts src/lib/novel/outline-stage-trace.spec.ts src/lib/novel/outline-next-step.spec.ts</code></pre>
+
+<p>预期:全部 PASS</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 2: 运行大纲面板已有测试</strong></p>
+
+<pre><code>npx vitest run src/components/sources/outline-chat-panel.spec.tsx</code></pre>
+
+<p>预期:PASS(如有失败需修复)</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 3: 运行 AI 会话面板测试验证无回归</strong></p>
+
+<pre><code>npx vitest run src/components/chat/agent-tool-call-message.spec.tsx</code></pre>
+
+<p>预期:PASS — 验证共享文件未被修改</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 4: 运行 TypeScript 全量编译检查</strong></p>
+
+<pre><code>npx tsc --noEmit</code></pre>
+
+<p>预期:无新增错误</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 5: 运行项目构建</strong></p>
+
+<pre><code>npm run build</code></pre>
+
+<p>预期:构建成功</p>
+
+<ul>
+  <li class="step"><span class="checkbox"></span> <strong>Step 6: 验证 git 状态</strong></p>
+
+<pre><code>git log --oneline -10</code></pre>
+
+<p>预期:看到 8 个提交(Task 1-8 各一个)</p>
+
+</div>
+
+<hr>
+
+<h2>实施后检查清单</h2>
+
+<ul>
+  <li><span class="checkbox"></span> 点击章节细纲后,AI 先输出 intent_clarity JSON</li>
+  <li><span class="checkbox"></span> 意图 clear 时自动进入生成流程</li>
+  <li><span class="checkbox"></span> 意图 needs_input 时显示推荐选项卡片</li>
+  <li><span class="checkbox"></span> 用户选择推荐选项后自动注入生成 prompt</li>
+  <li><span class="checkbox"></span> 用户选择「自定义」时聚焦输入框</li>
+  <li><span class="checkbox"></span> 思考过程按事件驱动渐进式出现,未激活阶段不显示</li>
+  <li><span class="checkbox"></span> 已完成阶段折叠为单行摘要,可点击展开</li>
+  <li><span class="checkbox"></span> 每章/每项生成独立文件</li>
+  <li><span class="checkbox"></span> 批量保存确认支持部分取消</li>
+  <li><span class="checkbox"></span> 生成完成后显示下一步推荐卡片</li>
+  <li><span class="checkbox"></span> 推荐中不含正文生成选项(被过滤)</li>
+  <li><span class="checkbox"></span> AI 会话面板的思考过程展示不受影响</li>
+</ul>
+
+</body>
+</html>