|
|
@@ -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><!-- intent_clarity --></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 = `<!-- intent_clarity -->
|
|
|
+{"clarity":"clear","module":"章节细纲","analysis":"检测到3章缺细纲","detectedScope":"第1-3章","missingItems":["第1章细纲"],"options":[],"question":""}
|
|
|
+<!-- /intent_clarity -->`
|
|
|
+ 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 = `<!-- intent_clarity -->
|
|
|
+{"clarity":"needs_input","module":"章节细纲","analysis":"0章有细纲","detectedScope":"","missingItems":[],"options":[{"id":"A","label":"生成全部缺失细纲","description":"第1-35章"},{"id":"D","label":"自定义","description":"自行描述"}],"question":"请问要生成哪些章节的细纲?"}
|
|
|
+<!-- /intent_clarity -->`
|
|
|
+ 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 = `<!-- intent_clarity -->
|
|
|
+{invalid json}
|
|
|
+<!-- /intent_clarity -->`
|
|
|
+ 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 = /<!--\s*intent_clarity\s*-->([\s\S]*?)<!--\s*\/intent_clarity\s*-->/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<string, unknown>
|
|
|
+ const clarity = String(raw.clarity ?? "")
|
|
|
+ if (clarity !== "clear" && clarity !== "needs_input") return null
|
|
|
+
|
|
|
+ const options: IntentClarityOption[] = Array.isArray(raw.options)
|
|
|
+ ? raw.options
|
|
|
+ .filter((item): item is Record<string, unknown> =>
|
|
|
+ Boolean(item) && typeof item === "object" && !Array.isArray(item))
|
|
|
+ .map((item) => ({
|
|
|
+ id: String(item.id ?? ""),
|
|
|
+ label: String(item.label ?? ""),
|
|
|
+ description: String(item.description ?? ""),
|
|
|
+ }))
|
|
|
+ .filter((item) => item.id && 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 => 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. 自定义(由用户描述要生成的内容范围或故事方向)",
|
|
|
+ "",
|
|
|
+ "## 输出格式(必须严格遵守)",
|
|
|
+ "<!-- intent_clarity -->",
|
|
|
+ '按 JSON 输出,字段:clarity("clear"|"needs_input")、module、analysis、detectedScope(clear时填写)、missingItems(数组)、options(needs_input时填4个选项,clear时为空数组)、question(needs_input时填写自然语言提问)',
|
|
|
+ "<!-- /intent_clarity -->",
|
|
|
+ "",
|
|
|
+ "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) => 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) => s.kind === "intent")
|
|
|
+ expect(intentStage?.status).toBe("active")
|
|
|
+ // 后续阶段仍为 hidden
|
|
|
+ const skillStage = stages.find((s) => s.kind === "skill")
|
|
|
+ expect(skillStage?.status).toBe("hidden")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("检测到 intent_clarity 后范围分析阶段激活", () => {
|
|
|
+ const content = `<!-- intent_clarity -->
|
|
|
+{"clarity":"clear","module":"章节细纲","analysis":"3章缺细纲","detectedScope":"第1-3章","missingItems":[],"options":[],"question":""}
|
|
|
+<!-- /intent_clarity -->`
|
|
|
+ const input: OutlineStageInput = {
|
|
|
+ toolCalls: [makeToolCall("route_task")],
|
|
|
+ content,
|
|
|
+ isStreaming: true,
|
|
|
+ }
|
|
|
+ const stages = buildOutlineStages(input)
|
|
|
+ const scopeStage = stages.find((s) => 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) => s.kind === "intent")
|
|
|
+ const skillStage = stages.find((s) => 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) => s.kind === "context")
|
|
|
+ expect(ctxStage?.status).toBe("active")
|
|
|
+ })
|
|
|
+
|
|
|
+ it("检测到 thinking 块后思考阶段激活", () => {
|
|
|
+ const input: OutlineStageInput = {
|
|
|
+ toolCalls: [],
|
|
|
+ content: "<thinking>正在分析角色动机...</thinking>",
|
|
|
+ isStreaming: true,
|
|
|
+ }
|
|
|
+ const stages = buildOutlineStages(input)
|
|
|
+ const thinkingStage = stages.find((s) => 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) => s.status === "active")
|
|
|
+ const doneStages = stages.filter((s) => 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 = /<!--\s*intent_clarity\s*-->[\s\S]*?<!--\s*\/intent_clarity\s*-->/i
|
|
|
+const THINKING_PATTERN = /<(?:think|thinking)>([\s\S]*?)<\/\1>/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) => ROUTE_TOOLS.has(c.name))
|
|
|
+ const hasSkill = toolCalls.some((c) => SKILL_TOOLS.has(c.name))
|
|
|
+ const hasRead = toolCalls.some((c) => LIST_TOOLS.has(c.name) || READ_TOOLS.has(c.name))
|
|
|
+ const hasWrite = toolCalls.some((c) => 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<Omit<OutlineStage, "status">> = [
|
|
|
+ { 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 >= 0; i--) {
|
|
|
+ if (activations[i]) {
|
|
|
+ lastActiveIndex = i
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return stageDefs.map((def, index) => {
|
|
|
+ const activated = activations[index]
|
|
|
+ let status: OutlineStageStatus = "hidden"
|
|
|
+
|
|
|
+ if (activated) {
|
|
|
+ if (!isStreaming || index < lastActiveIndex) {
|
|
|
+ status = "done"
|
|
|
+ } else {
|
|
|
+ status = "active"
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 流结束时所有已激活阶段为 done
|
|
|
+ if (activated && !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><!-- next_step --></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 = `<!-- next_step -->
|
|
|
+{"completedModule":"章节细纲","completedScope":"第1-3章细纲","recommendations":[{"id":"A","label":"完善人物小传","reason":"提及3个未立传角色"},{"id":"D","label":"自定义","reason":"由你描述"}]}
|
|
|
+<!-- /next_step -->`
|
|
|
+ const result = parseNextStep(text)
|
|
|
+ expect(result).not.toBeNull()
|
|
|
+ expect(result!.completedModule).toBe("章节细纲")
|
|
|
+ expect(result!.recommendations).toHaveLength(2)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("无标记块时返回 null", () => {
|
|
|
+ expect(parseNextStep("普通文本")).toBeNull()
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+describe("isRecommendationForbidden", () => {
|
|
|
+ it("禁止包含正文生成关键词", () => {
|
|
|
+ expect(isRecommendationForbidden("生成对应章节正文")).toBe(true)
|
|
|
+ expect(isRecommendationForbidden("写正文")).toBe(true)
|
|
|
+ expect(isRecommendationForbidden("生成章节内容")).toBe(true)
|
|
|
+ })
|
|
|
+
|
|
|
+ it("允许大纲体系内的推荐", () => {
|
|
|
+ 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 = /<!--\s*next_step\s*-->([\s\S]*?)<!--\s*\/next_step\s*-->/i
|
|
|
+
|
|
|
+// 严禁推荐的正文生成关键词
|
|
|
+const FORBIDDEN_PATTERNS = [
|
|
|
+ /生成.*正文/,
|
|
|
+ /写.*正文/,
|
|
|
+ /生成.*章节内容/,
|
|
|
+ /生成.*正文章节/,
|
|
|
+ /章节正文/,
|
|
|
+ /写正文/,
|
|
|
+ /生成正文/,
|
|
|
+]
|
|
|
+
|
|
|
+export function isRecommendationForbidden(label: string): boolean {
|
|
|
+ return FORBIDDEN_PATTERNS.some((pattern) => 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<string, unknown>
|
|
|
+
|
|
|
+ const recommendations: NextStepRecommendationItem[] = Array.isArray(raw.recommendations)
|
|
|
+ ? raw.recommendations
|
|
|
+ .filter((item): item is Record<string, unknown> =>
|
|
|
+ Boolean(item) && typeof item === "object" && !Array.isArray(item))
|
|
|
+ .map((item) => ({
|
|
|
+ id: String(item.id ?? ""),
|
|
|
+ label: String(item.label ?? ""),
|
|
|
+ reason: String(item.reason ?? ""),
|
|
|
+ }))
|
|
|
+ .filter((item) => item.id && item.label)
|
|
|
+ // 过滤掉被禁止的推荐(正文生成)
|
|
|
+ .filter((item) => !isRecommendationForbidden(item.label))
|
|
|
+ : []
|
|
|
+
|
|
|
+ return {
|
|
|
+ completedModule: String(raw.completedModule ?? ""),
|
|
|
+ completedScope: String(raw.completedScope ?? ""),
|
|
|
+ recommendations,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function buildNextStepPromptSuffix(): string {
|
|
|
+ return [
|
|
|
+ "",
|
|
|
+ "## 下一步推荐输出要求",
|
|
|
+ "生成完成后,在回复末尾附加 <!-- next_step --> 标记块,按 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<OutlineWorkflowStage, OutlineWorkflowStage[]> = {
|
|
|
+ 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: () => void
|
|
|
+}) {
|
|
|
+ return (
|
|
|
+ <div
|
|
|
+ className={cn(
|
|
|
+ "rounded-md border transition-all duration-250 ease-out",
|
|
|
+ stage.status === "active" && "border-sky-500/40 bg-sky-50/30 dark:bg-sky-950/10",
|
|
|
+ stage.status === "done" && "border-emerald-500/20 bg-emerald-50/10 dark:bg-emerald-950/5",
|
|
|
+ "opacity-0 translate-y-1 animate-[fadeIn_250ms_ease-out_forwards]"
|
|
|
+ )}
|
|
|
+ >
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={onToggle}
|
|
|
+ className="flex w-full items-center gap-2 px-3 py-1.5 text-left text-xs"
|
|
|
+ >
|
|
|
+ {stage.status === "done" ? (
|
|
|
+ <CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
|
+ ) : stage.status === "active" ? (
|
|
|
+ <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin text-sky-600 dark:text-sky-400" />
|
|
|
+ ) : null}
|
|
|
+ <span className="font-medium">{stage.title}</span>
|
|
|
+ {stage.summary && (
|
|
|
+ <span className="text-muted-foreground">· {stage.summary}</span>
|
|
|
+ )}
|
|
|
+ <ChevronRight
|
|
|
+ className={cn(
|
|
|
+ "ml-auto h-3 w-3 transition-transform",
|
|
|
+ open && "rotate-90"
|
|
|
+ )}
|
|
|
+ />
|
|
|
+ </button>
|
|
|
+ {open && stage.details.length > 0 && (
|
|
|
+ <div className="border-t border-border/50 px-3 py-2 text-xs text-muted-foreground space-y-1">
|
|
|
+ {stage.details.map((detail, i) => (
|
|
|
+ <div key={i}>{detail}</div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+export function OutlineWorkflowStages(props: OutlineWorkflowStagesProps) {
|
|
|
+ const stages = buildOutlineStages(props)
|
|
|
+ const visibleStages = stages.filter((s) => s.status !== "hidden")
|
|
|
+
|
|
|
+ // 默认展开 active 阶段
|
|
|
+ const defaultOpenId = stages.find((s) => s.status === "active")?.kind ?? null
|
|
|
+ const [openMap, setOpenMap] = useState<Record<string, boolean>>({})
|
|
|
+
|
|
|
+ if (visibleStages.length === 0) return null
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="mb-2 space-y-1">
|
|
|
+ <div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground mb-1">
|
|
|
+ <span>思考过程</span>
|
|
|
+ </div>
|
|
|
+ {visibleStages.map((stage) => {
|
|
|
+ const isOpen = openMap[stage.kind] ?? (stage.status === "active")
|
|
|
+ return (
|
|
|
+ <StageRow
|
|
|
+ key={stage.kind}
|
|
|
+ stage={stage}
|
|
|
+ open={isOpen}
|
|
|
+ onToggle={() => setOpenMap((prev) => ({ ...prev, [stage.kind]: !isOpen }))}
|
|
|
+ />
|
|
|
+ )
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|
|
|
+</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<Set<string>>(new Set())
|
|
|
+
|
|
|
+// 在每个 request 卡片的顶部增加 checkbox
|
|
|
+<input
|
|
|
+ type="checkbox"
|
|
|
+ checked={!deselectedFiles.has(request.fileName)}
|
|
|
+ onChange={(e) => {
|
|
|
+ setDeselectedFiles((prev) => {
|
|
|
+ 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"
|
|
|
+/>
|
|
|
+
|
|
|
+// 确认按钮的 onClick 中过滤掉被取消的文件
|
|
|
+onClick={() => {
|
|
|
+ const filteredRequests = normalRequests.filter(
|
|
|
+ (r) => !deselectedFiles.has(r.fileName)
|
|
|
+ )
|
|
|
+ onConfirm({
|
|
|
+ requests: mode === "character" ? requests : filteredRequests,
|
|
|
+ characterDrafts: selectedDrafts,
|
|
|
+ })
|
|
|
+}}
|
|
|
+
|
|
|
+// 禁用条件:如果所有文件都被取消则禁用确认按钮
|
|
|
+disabled={
|
|
|
+ (mode === "character" && selectedDrafts.length === 0) ||
|
|
|
+ (mode !== "character" && 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<IntentClarityResult | null>(null)
|
|
|
+const [isIntentPhase, setIsIntentPhase] = useState(false)
|
|
|
+
|
|
|
+// 改造 handleGenerateSection
|
|
|
+const handleGenerateSection = useCallback(
|
|
|
+ (title: string, requestHint: string) => {
|
|
|
+ // 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" && (
|
|
|
+// <div className="mt-3 rounded-md border border-amber-500/30 bg-amber-50/30 p-3 dark:bg-amber-950/10">
|
|
|
+// <div className="text-sm font-medium mb-2">{msg.intentClarityResult.question || "请选择生成范围:"}</div>
|
|
|
+// <div className="space-y-2">
|
|
|
+// {msg.intentClarityResult.options.map((option) => (
|
|
|
+// <button
|
|
|
+// key={option.id}
|
|
|
+// className="w-full rounded-md border px-3 py-2 text-left text-sm hover:bg-accent transition-colors"
|
|
|
+// onClick={() => {
|
|
|
+// if (option.id === "D") {
|
|
|
+// // 聚焦输入框等待用户输入
|
|
|
+// inputRef.current?.focus()
|
|
|
+// } else {
|
|
|
+// // 直接注入用户选择作为生成范围
|
|
|
+// void handleSend(
|
|
|
+// buildGenerationPrompt(
|
|
|
+// msg.intentClarityResult.module,
|
|
|
+// "", // requestHint 从配置重新获取
|
|
|
+// option.description,
|
|
|
+// ),
|
|
|
+// undefined,
|
|
|
+// { intentPhase: "generation" }
|
|
|
+// )
|
|
|
+// }
|
|
|
+// }}
|
|
|
+// >
|
|
|
+// <div className="font-medium">{option.label}</div>
|
|
|
+// <div className="text-xs text-muted-foreground">{option.description}</div>
|
|
|
+// </button>
|
|
|
+// ))}
|
|
|
+// </div>
|
|
|
+// </div>
|
|
|
+// )}</code></pre>
|
|
|
+
|
|
|
+<ul>
|
|
|
+ <li class="step"><span class="checkbox"></span> <strong>Step 5: 新增下一步推荐渲染组件(内联在消息渲染末尾)</strong></p>
|
|
|
+</ul>
|
|
|
+
|
|
|
+<pre><code>// 在消息渲染中,当 msg.nextStepRecommendation 存在时渲染推荐卡片
|
|
|
+//
|
|
|
+// {msg.nextStepRecommendation && msg.nextStepRecommendation.recommendations.length > 0 && (
|
|
|
+// <div className="mt-3 rounded-md border border-sky-500/30 bg-sky-50/30 p-3 dark:bg-sky-950/10">
|
|
|
+// <div className="text-sm font-medium mb-2">接下来想做什么?</div>
|
|
|
+// <div className="space-y-2">
|
|
|
+// {msg.nextStepRecommendation.recommendations.map((rec) => (
|
|
|
+// <button
|
|
|
+// key={rec.id}
|
|
|
+// className="w-full rounded-md border px-3 py-2 text-left text-sm hover:bg-accent transition-colors"
|
|
|
+// onClick={() => {
|
|
|
+// if (rec.id === "D") {
|
|
|
+// inputRef.current?.focus()
|
|
|
+// } else {
|
|
|
+// // 查找对应模块配置并触发生成
|
|
|
+// const config = OUTLINE_SECTION_GENERATION_CONFIGS.find(c => c.title === rec.label)
|
|
|
+// if (config) {
|
|
|
+// handleGenerateSection(config.title, config.requestHint)
|
|
|
+// }
|
|
|
+// }
|
|
|
+// }}
|
|
|
+// >
|
|
|
+// <div className="font-medium">{rec.label}</div>
|
|
|
+// <div className="text-xs text-muted-foreground">{rec.reason}</div>
|
|
|
+// </button>
|
|
|
+// ))}
|
|
|
+// </div>
|
|
|
+// </div>
|
|
|
+// )}</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><OutlineThinkingBlock content={visibleThinking} open={isStreaming} /></code> 替换为:</p>
|
|
|
+
|
|
|
+<pre><code>// 替换 OutlineThinkingBlock
|
|
|
+<OutlineWorkflowStages
|
|
|
+ toolCalls={msg.agentToolCalls ?? []}
|
|
|
+ content={displayContent}
|
|
|
+ isStreaming={isStreaming && index === activeMessagesLength - 1}
|
|
|
+/></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. 输出 <!-- intent_clarity --> JSON 标记块",
|
|
|
+"4. clear 时:只输出 JSON,不生成正文,等待系统自动注入生成指令",
|
|
|
+"5. needs_input 时:输出 JSON 后用自然语言提出澄清问题 + 4个推荐选项",
|
|
|
+"推荐选项必须包含:A.全部缺失项 B.基于已有内容推断 C.最近范围 D.自定义",
|
|
|
+"用户选择或回复后,直接进入生成流程,不再二次分析。",
|
|
|
+"",
|
|
|
+"## 下一步推荐输出",
|
|
|
+"生成完成后,在回复末尾附加 <!-- next_step --> 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>
|