| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495 |
- <!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: 1080px;
- margin: 0 auto;
- }
- h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
- h2 { font-size: 1.4rem; 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); }
- h4 { font-size: 1rem; margin-top: 1.2rem; margin-bottom: 0.5rem; color: var(--text); }
- 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.88em;
- 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.85rem;
- line-height: 1.6;
- }
- 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.6rem 0.8rem; 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; }
- .callout { background: var(--surface); border-left: 3px solid var(--accent); padding: 0.8rem 1rem; border-radius: 0 6px 6px 0; margin: 1rem 0; }
- .callout-warn { border-left-color: var(--warning); }
- .callout-danger { border-left-color: var(--danger); }
- .callout-success { border-left-color: var(--success); }
- .phase-tag { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 4px; font-size: 0.8rem; font-weight: 600; margin-right: 0.5rem; }
- .phase-1 { background: rgba(248,113,113,0.15); color: var(--danger); }
- .phase-2 { background: rgba(251,191,36,0.15); color: var(--warning); }
- .phase-3 { background: rgba(107,182,255,0.15); color: var(--accent); }
- .phase-4 { background: rgba(74,222,128,0.15); color: var(--success); }
- .task-block { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 1.2rem 1.5rem; margin: 1.5rem 0; }
- .step-line { padding: 0.3rem 0; }
- </style>
- </head>
- <body>
- <h1>AI大纲交互重构优化 — 实施计划</h1>
- <blockquote><strong>For agentic workers:</strong> REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox-style tracking.</blockquote>
- <p><strong>Goal:</strong> 修复AI大纲会话窗口交互重构中的3个致命问题、4个高优先级问题及11个中等/低优先级优化项,使两阶段流程真正可运行并达到设计规格要求的完成度。</p>
- <p><strong>Architecture:</strong> 通过 props 传递回调解决推荐点击断裂;使用 useRef 替代 useState 消除闭包陷阱;提取独立组件降低面板复杂度;增强 buildOutlineStages 填充运行时摘要数据。</p>
- <p><strong>Tech Stack:</strong> React 18, TypeScript, Zustand, Vitest, Tailwind CSS</p>
- <hr>
- <h2>文件结构</h2>
- <table>
- <tr><th>文件</th><th>操作</th><th>职责</th></tr>
- <tr><td><code>src/lib/novel/outline-intent-clarity.ts</code></td><td>修改</td><td>新增 stripStructuredMarkers 工具函数</td></tr>
- <tr><td><code>src/lib/novel/outline-intent-clarity.spec.ts</code></td><td>修改</td><td>新增 stripStructuredMarkers 测试</td></tr>
- <tr><td><code>src/components/sources/outline-intent-options-card.tsx</code></td><td>创建</td><td>意图推荐选项卡片组件(从面板提取)</td></tr>
- <tr><td><code>src/components/sources/outline-next-step-card.tsx</code></td><td>创建</td><td>下一步推荐卡片组件(从面板提取)</td></tr>
- <tr><td><code>src/components/sources/outline-chat-panel.tsx</code></td><td>修改</td><td>核心改造:props传递、闭包修复、内容过滤、阶段摘要</td></tr>
- <tr><td><code>src/lib/novel/outline-stage-trace.ts</code></td><td>修改</td><td>填充运行时摘要数据</td></tr>
- <tr><td><code>src/lib/novel/outline-stage-trace.spec.ts</code></td><td>修改</td><td>新增摘要填充测试</td></tr>
- <tr><td><code>src/lib/novel/outline-next-step.ts</code></td><td>修改</td><td>优化 isRecommendationForbidden 精确度</td></tr>
- <tr><td><code>src/lib/novel/outline-next-step.spec.ts</code></td><td>修改</td><td>新增误杀测试</td></tr>
- <tr><td><code>src/lib/novel/outline-section-configs.ts</code></td><td>修改</td><td>buildGenerationPrompt 使用 outputMode</td></tr>
- <tr><td><code>src/lib/novel/outline-workflow-state.ts</code></td><td>修改</td><td>面板集成状态机</td></tr>
- </table>
- <hr>
- <h2><span class="phase-tag phase-1">第一阶段</span>修复致命问题(P0)</h2>
- <div class="callout callout-danger">
- <strong>目标</strong>:让两阶段流程真正可运行,推荐点击能触发生成,显示内容干净。预估 4.5h。
- </div>
- <div class="task-block">
- <h3>Task 1: 新增 stripStructuredMarkers 工具函数</h3>
- <p><strong>问题 U1</strong>:intent_clarity / next_step JSON 块在正文显示中未过滤,用户看到原始 JSON。</p>
- **Files:**
- - Modify: `src/lib/novel/outline-intent-clarity.ts`
- - Modify: `src/lib/novel/outline-intent-clarity.spec.ts`
- - [ ] **Step 1: 在 outline-intent-clarity.spec.ts 新增测试**
- ```typescript
- import { stripStructuredMarkers } from "./outline-intent-clarity"
- describe("stripStructuredMarkers", () => {
- it("移除 intent_clarity 标记块", () => {
- const text = `分析结果如下:\n<!-- intent_clarity -->\n{"clarity":"clear"}\n<!-- /intent_clarity -->\n\n这是正文内容。`
- expect(stripStructuredMarkers(text)).toBe("分析结果如下:\n\n这是正文内容。")
- })
- it("移除 next_step 标记块", () => {
- const text = `正文内容\n<!-- next_step -->\n{"recommendations":[]}\n<!-- /next_step -->`
- expect(stripStructuredMarkers(text).trim()).toBe("正文内容")
- })
- it("同时移除两种标记块", () => {
- const text = `<!-- intent_clarity -->\n{"clarity":"clear"}\n<!-- /intent_clarity -->\n正文\n<!-- next_step -->\n{"recommendations":[]}\n<!-- /next_step -->`
- expect(stripStructuredMarkers(text).trim()).toBe("正文")
- })
- it("无标记块时原样返回", () => {
- const text = "纯文本内容"
- expect(stripStructuredMarkers(text)).toBe("纯文本内容")
- })
- })
- ```
- - [ ] **Step 2: 运行测试验证失败**
- Run: `npx vitest run src/lib/novel/outline-intent-clarity.spec.ts`
- Expected: stripStructuredMarkers 相关测试 FAIL
- - [ ] **Step 3: 在 outline-intent-clarity.ts 新增 stripStructuredMarkers**
- 在文件末尾(`buildIntentAnalysisPrompt` 函数之后)添加:
- ```typescript
- export function stripStructuredMarkers(text: string): string {
- return text
- .replace(/<!--\s*intent_clarity\s*-->[\s\S]*?<!--\s*\/intent_clarity\s*-->/gi, "")
- .replace(/<!--\s*next_step\s*-->[\s\S]*?<!--\s*\/next_step\s*-->/gi, "")
- .replace(/\n{3,}/g, "\n\n")
- .trim()
- }
- ```
- - [ ] **Step 4: 运行测试验证通过**
- Run: `npx vitest run src/lib/novel/outline-intent-clarity.spec.ts`
- Expected: 全部 PASS
- - [ ] **Step 5: 在 outline-chat-panel.tsx 的 displayContent 中使用 stripStructuredMarkers**
- 在 `OutlineAssistantMessage` 函数中(约 L732-L736),当前 `renderedMarkdownContent` 的 useMemo:
- ```typescript
- const renderedMarkdownContent = useMemo(() => {
- const rawContent = parsed.textContent || answer;
- const bodyContent = extractBodyContent(rawContent);
- return normalizeOutlineMarkdown(bodyContent || rawContent);
- }, [answer, parsed.textContent]);
- ```
- 修改为:
- ```typescript
- const renderedMarkdownContent = useMemo(() => {
- const rawContent = parsed.textContent || answer;
- const stripped = stripStructuredMarkers(rawContent);
- const bodyContent = extractBodyContent(stripped);
- return normalizeOutlineMarkdown(bodyContent || stripped);
- }, [answer, parsed.textContent]);
- ```
- 同时在文件顶部新增 import:
- ```typescript
- import { stripStructuredMarkers } from "@/lib/novel/outline-intent-clarity"
- ```
- 注意:`stripStructuredMarkers` 需要添加到已有的 `outline-intent-clarity` import 语句中(约 L157)。
- - [ ] **Step 6: 运行 typecheck**
- Run: `npx tsc --noEmit`
- Expected: 无错误
- - [ ] **Step 7: 提交**
- ```
- git add src/lib/novel/outline-intent-clarity.ts src/lib/novel/outline-intent-clarity.spec.ts src/components/sources/outline-chat-panel.tsx
- git commit -m "新增stripStructuredMarkers过滤intent_clarity/next_step显示"
- ```
- </div>
- <div class="task-block">
- <h3>Task 2: 修复推荐选项点击回调(F1+F3+Q1)</h3>
- <p><strong>问题 F1</strong>:推荐选项点击回调未连接 handleGenerateSection。<br>
- <strong>问题 F3</strong>:needs_input 用户选择后未跳过意图分析。<br>
- <strong>问题 Q1</strong>:推荐点击回调直接操作 store,绕过面板流程控制。</p>
- **Files:**
- - Modify: `src/components/sources/outline-chat-panel.tsx`
- - [ ] **Step 1: 在 OutlineAssistantMessage props 新增回调**
- 在 `OutlineAssistantMessage` 函数签名(约 L682-L708)中,在 `onRejectTool` 之后添加三个 props:
- ```typescript
- function OutlineAssistantMessage({
- msg,
- index,
- isStreaming,
- streamingContent,
- activeMessagesLength,
- copied,
- projectPath,
- onSaveAsOutline,
- onCopy,
- onRegenerate,
- onConfirmToolSave,
- onRejectTool,
- onGenerateSection,
- onSendMessage,
- onFocusInput,
- }: {
- msg: import("@/stores/outline-chat-store").OutlineChatMessage;
- index: number;
- isStreaming: boolean;
- streamingContent: string;
- activeMessagesLength: number;
- copied: string | null;
- projectPath: string | null;
- onSaveAsOutline: (content: string) => Promise<void>;
- onCopy: (content: string, id: string) => void;
- onRegenerate: (index: number) => Promise<void>;
- onConfirmToolSave: (call: ToolCallRecord & { preview?: string }) => void;
- onRejectTool: (call: ToolCallRecord & { preview?: string }) => void;
- onGenerateSection: (title: string, requestHint: string) => void;
- onSendMessage: (text: string, options?: { intentPhase?: "intent_analysis" | "generation" | "waiting_user_input"; scope?: string }) => void;
- onFocusInput: () => void;
- }) {
- ```
- - [ ] **Step 2: 替换意图推荐选项的点击回调**
- 在 `OutlineAssistantMessage` 中(约 L910-L943),替换 needs_input 推荐选项的 onClick:
- 将:
- ```tsx
- onClick={() => {
- if (option.id === "D") {
- // 自定义:聚焦输入框(暂不实现,后续 Task 补充)
- } else {
- const outlineChatStore = useOutlineChatStore.getState();
- const convId = outlineChatStore.activeConversationId;
- if (!convId) return;
- useOutlineChatStore.getState().addMessage(convId, {
- id: crypto.randomUUID(),
- role: "user",
- content: option.label + (option.description ? `:${option.description}` : ""),
- });
- }
- }}
- ```
- 替换为:
- ```tsx
- onClick={() => {
- if (option.id === "D") {
- onFocusInput();
- } else {
- const scope = option.label + (option.description ? `:${option.description}` : "");
- onSendMessage(scope, { intentPhase: "generation", scope });
- }
- }}
- ```
- - [ ] **Step 3: 替换下一步推荐的点击回调**
- 在 `OutlineAssistantMessage` 中(约 L944-L980),替换 next_step 推荐的 onClick:
- 将:
- ```tsx
- onClick={() => {
- if (rec.id === "D") {
- // 自定义:聚焦输入框
- } else {
- const config = OUTLINE_SECTION_GENERATION_CONFIGS.find(c => c.title === rec.label);
- if (config) {
- const outlineChatStore = useOutlineChatStore.getState();
- const convId = outlineChatStore.activeConversationId;
- if (!convId) return;
- useOutlineChatStore.getState().addMessage(convId, {
- id: crypto.randomUUID(),
- role: "user",
- content: `生成${rec.label}`,
- });
- }
- }
- }}
- ```
- 替换为:
- ```tsx
- onClick={() => {
- if (rec.id === "D") {
- onFocusInput();
- } else {
- const config = OUTLINE_SECTION_GENERATION_CONFIGS.find(c => c.title === rec.label);
- if (config) {
- onGenerateSection(config.title, config.requestHint);
- }
- }
- }}
- ```
- - [ ] **Step 4: 在渲染 OutlineAssistantMessage 的位置传入新 props**
- 在面板主组件中(约 L2860-L2873),添加三个 props:
- ```tsx
- <OutlineAssistantMessage
- msg={msg}
- index={i}
- isStreaming={isStreaming}
- streamingContent={streamingContent}
- activeMessagesLength={activeMessages.length}
- copied={copied}
- projectPath={project?.path ?? null}
- onSaveAsOutline={handleSaveAsOutline}
- onCopy={handleCopy}
- onRegenerate={handleRegenerate}
- onConfirmToolSave={handleConfirmToolSave}
- onRejectTool={handleRejectTool}
- onGenerateSection={handleGenerateSection}
- onSendMessage={(text, options) => {
- void handleSend(
- buildGenerationPrompt(lastIntentTitle, lastIntentHint, options?.scope),
- [],
- { intentPhase: options?.intentPhase ?? "generation" },
- );
- }}
- onFocusInput={() => {
- insertReferenceTokensRef.current?.focus();
- }}
- />
- ```
- - [ ] **Step 5: 在 handleSend 中处理 scope 参数,确保防无限循环**
- 在 handleSend 中 intentPhase 为 "generation" 时,不需要再做意图分析。当前 handleSend 的 options 已有 intentPhase 字段,流结束回调中已有逻辑只在 intentPhase 为 "intent_analysis" 时解析 intent_clarity。
- 修改流结束回调中的意图解析逻辑(约 L2090-L2107),增加 intentPhase 条件检查:
- 将:
- ```typescript
- // 解析意图清晰度结果
- const intentResult = parseIntentClarity(finalContent);
- ```
- 替换为:
- ```typescript
- // 解析意图清晰度结果(仅意图分析阶段触发)
- const currentIntentPhase = useOutlineChatStore.getState()
- .conversations.find(c => c.id === convId)
- ?.messages.find(m => m.id === assistantId)?.intentPhase;
- if (currentIntentPhase === "intent_analysis") {
- const intentResult = parseIntentClarity(finalContent);
- ```
- 并在该 if 块末尾(约 L2107)添加对应的闭合大括号 `}`。
- - [ ] **Step 6: 运行 typecheck**
- Run: `npx tsc --noEmit`
- Expected: 无错误
- - [ ] **Step 7: 运行大纲面板测试**
- Run: `npx vitest run src/components/sources/outline-chat-panel.spec.tsx`
- Expected: 全部 PASS
- - [ ] **Step 8: 提交**
- ```
- git add src/components/sources/outline-chat-panel.tsx
- git commit -m "修复推荐选项点击回调连接handleGenerateSection,防无限循环"
- ```
- </div>
- <h2><span class="phase-tag phase-2">第二阶段</span>补全交互细节(P1)</h2>
- <div class="callout callout-warn">
- <strong>目标</strong>:4类选项完整可用,阶段信息可读,用户体验可感知。预估 6h。
- </div>
- <div class="task-block">
- <h3>Task 3: 提取推荐选项和下一步推荐为独立组件</h3>
- <p><strong>问题 M1</strong>:outline-chat-panel.tsx 过大,推荐选项和下一步推荐应提取为独立组件。</p>
- **Files:**
- - Create: `src/components/sources/outline-intent-options-card.tsx`
- - Create: `src/components/sources/outline-next-step-card.tsx`
- - Modify: `src/components/sources/outline-chat-panel.tsx`
- - [ ] **Step 1: 创建 outline-intent-options-card.tsx**
- ```tsx
- import React from "react"
- import type { IntentClarityResult } from "@/lib/novel/outline-intent-clarity"
- interface IntentOptionsCardProps {
- result: IntentClarityResult
- onSelectOption: (optionId: string, label: string, description: string) => void
- }
- export function IntentOptionsCard({ result, onSelectOption }: IntentOptionsCardProps) {
- return (
- <div className="mt-3 rounded-md border border-amber-500/30 bg-amber-50/30 p-3 dark:bg-amber-950/10">
- {result.analysis ? (
- <div className="mb-2 text-xs text-muted-foreground">{result.analysis}</div>
- ) : null}
- <div className="text-sm font-medium mb-2">{result.question || "请选择生成范围:"}</div>
- <div className="space-y-2">
- {result.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={() => onSelectOption(option.id, option.label, option.description)}
- >
- <div className="font-medium">{option.label}</div>
- {option.description ? (
- <div className="text-xs text-muted-foreground">{option.description}</div>
- ) : null}
- </button>
- ))}
- </div>
- </div>
- )
- }
- ```
- - [ ] **Step 2: 创建 outline-next-step-card.tsx**
- ```tsx
- import React from "react"
- import type { NextStepRecommendation } from "@/lib/novel/outline-next-step"
- interface NextStepCardProps {
- recommendation: NextStepRecommendation
- onSelectRecommendation: (recId: string, label: string) => void
- }
- export function NextStepCard({ recommendation, onSelectRecommendation }: NextStepCardProps) {
- return (
- <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-xs text-muted-foreground mb-1">
- 已完成:{recommendation.completedModule}
- {recommendation.completedScope ? `(${recommendation.completedScope})` : ""}
- </div>
- <div className="text-sm font-medium mb-2">接下来想做什么?</div>
- <div className="space-y-2">
- {recommendation.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={() => onSelectRecommendation(rec.id, rec.label)}
- >
- <div className="font-medium">{rec.label}</div>
- {rec.reason ? (
- <div className="text-xs text-muted-foreground">{rec.reason}</div>
- ) : null}
- </button>
- ))}
- </div>
- </div>
- )
- }
- ```
- - [ ] **Step 3: 在 outline-chat-panel.tsx 中替换内联渲染为组件调用**
- 在文件顶部添加 import:
- ```typescript
- import { IntentOptionsCard } from "@/components/sources/outline-intent-options-card"
- import { NextStepCard } from "@/components/sources/outline-next-step-card"
- ```
- 在 `OutlineAssistantMessage` 中,替换 needs_input 推荐选项区域(约 L910-L943):
- ```tsx
- {msg.intentClarityResult?.clarity === "needs_input" && !isStreaming ? (
- <IntentOptionsCard
- result={msg.intentClarityResult}
- onSelectOption={(optionId, label, description) => {
- if (optionId === "D") {
- onFocusInput();
- } else {
- const scope = label + (description ? `:${description}` : "");
- onSendMessage(scope, { intentPhase: "generation", scope });
- }
- }}
- />
- ) : null}
- ```
- 替换下一步推荐区域(约 L944-L980):
- ```tsx
- {msg.nextStepRecommendation && msg.nextStepRecommendation.recommendations.length > 0 && !isStreaming ? (
- <NextStepCard
- recommendation={msg.nextStepRecommendation}
- onSelectRecommendation={(recId, label) => {
- if (recId === "D") {
- onFocusInput();
- } else {
- const config = OUTLINE_SECTION_GENERATION_CONFIGS.find(c => c.title === label);
- if (config) {
- onGenerateSection(config.title, config.requestHint);
- }
- }
- }}
- />
- ) : null}
- ```
- - [ ] **Step 4: 运行 typecheck**
- Run: `npx tsc --noEmit`
- Expected: 无错误
- - [ ] **Step 5: 提交**
- ```
- git add src/components/sources/outline-intent-options-card.tsx src/components/sources/outline-next-step-card.tsx src/components/sources/outline-chat-panel.tsx
- git commit -m "提取IntentOptionsCard和NextStepCard独立组件"
- ```
- </div>
- <div class="task-block">
- <h3>Task 4: 填充阶段摘要运行时数据</h3>
- <p><strong>问题 U2</strong>:阶段摘要信息为空,用户只看到"✓ 任务理解"而非"✓ 任务理解 · 识别为生成章节细纲 · 1.2s"。</p>
- **Files:**
- - Modify: `src/lib/novel/outline-stage-trace.ts`
- - Modify: `src/lib/novel/outline-stage-trace.spec.ts`
- - [ ] **Step 1: 在 outline-stage-trace.spec.ts 新增摘要测试**
- ```typescript
- it("任务理解阶段摘要包含工具信息", () => {
- const input: OutlineStageInput = {
- toolCalls: [makeToolCall("route_task", "done")],
- content: "",
- isStreaming: false,
- }
- const stages = buildOutlineStages(input)
- const intentStage = stages.find((s) => s.kind === "intent")
- expect(intentStage?.summary).toBeTruthy()
- expect(intentStage?.summary).toContain("route_task")
- })
- it("范围分析阶段摘要包含模块名", () => {
- const content = `<!-- intent_clarity -->
- {"clarity":"clear","module":"章节细纲","analysis":"3章缺细纲","detectedScope":"第1-3章","missingItems":[],"options":[],"question":""}
- <!-- /intent_clarity -->`
- const input: OutlineStageInput = {
- toolCalls: [makeToolCall("route_task", "done")],
- content,
- isStreaming: false,
- }
- const stages = buildOutlineStages(input)
- const scopeStage = stages.find((s) => s.kind === "scope")
- expect(scopeStage?.summary).toContain("章节细纲")
- })
- it("上下文准备阶段摘要包含读取文件数", () => {
- const input: OutlineStageInput = {
- toolCalls: [
- makeToolCall("route_task", "done"),
- makeToolCall("read_outline", "done"),
- makeToolCall("read_chapter", "done"),
- makeToolCall("list_outlines", "done"),
- ],
- content: "",
- isStreaming: false,
- }
- const stages = buildOutlineStages(input)
- const ctxStage = stages.find((s) => s.kind === "context")
- expect(ctxStage?.summary).toContain("3")
- })
- ```
- - [ ] **Step 2: 运行测试验证失败**
- Run: `npx vitest run src/lib/novel/outline-stage-trace.spec.ts`
- Expected: 新增测试 FAIL(summary 为空)
- - [ ] **Step 3: 修改 buildOutlineStages 填充摘要数据**
- 修改 `buildOutlineStages` 函数,在 stageDefs 构建后、返回前,根据激活条件填充 summary:
- 将 `buildOutlineStages` 中 `stageDefs` 定义后的逻辑改为:
- ```typescript
- export function buildOutlineStages(input: OutlineStageInput): OutlineStage[] {
- const { toolCalls, content, isStreaming } = input
- const routeCalls = toolCalls.filter((c) => ROUTE_TOOLS.has(c.name))
- const skillCalls = toolCalls.filter((c) => SKILL_TOOLS.has(c.name))
- const readCalls = toolCalls.filter((c) => LIST_TOOLS.has(c.name) || READ_TOOLS.has(c.name))
- const writeCalls = toolCalls.filter((c) => WRITE_TOOLS.has(c.name))
- const hasRoute = routeCalls.length > 0
- const hasSkill = skillCalls.length > 0
- const hasRead = readCalls.length > 0
- const hasWrite = writeCalls.length > 0
- const hasClarity = hasIntentClarity(content)
- const hasThinkingBlock = hasThinking(content)
- const hasOutput = hasGenerationStarted(content)
- const activations = [hasRoute, hasClarity, hasSkill, hasRead, hasWrite, hasThinkingBlock, hasOutput]
- // 提取意图分析摘要
- let clarityModule = ""
- let clarityAnalysis = ""
- const clarityMatch = content.match(INTENT_CLARITY_PATTERN)
- if (clarityMatch) {
- try {
- const payload = JSON.parse(clarityMatch[1].trim())
- clarityModule = String(payload.module ?? "")
- clarityAnalysis = String(payload.analysis ?? "")
- } catch {}
- }
- // 构建摘要
- const summaries: string[] = [
- hasRoute ? `路由到${routeCalls.map(c => c.name).join("、")}` : "",
- hasClarity ? (clarityModule ? `${clarityModule}:${clarityAnalysis || "范围已明确"}` : "范围分析完成") : "",
- hasSkill ? `已加载${skillCalls.map(c => c.name).join("、")}` : "",
- hasRead ? `读取了${readCalls.length}个资料` : "",
- hasWrite ? `调用${writeCalls.length}个写入工具` : "",
- hasThinkingBlock ? "角色与情节推理中" : "",
- hasOutput ? "内容生成完成" : "",
- ]
- const detailsList: string[][] = [
- hasRoute ? routeCalls.map((c) => `${c.name}(${c.status})`) : [],
- hasClarity ? [clarityModule, clarityAnalysis].filter(Boolean) : [],
- hasSkill ? skillCalls.map((c) => `${c.name}(${c.status})`) : [],
- hasRead ? readCalls.map((c) => `${c.name}(${c.status})`) : [],
- hasWrite ? writeCalls.map((c) => `${c.name}(${c.status})`) : [],
- [],
- [],
- ]
- const stageDefs: Array<Omit<OutlineStage, "status">> = [
- { kind: "intent", title: "任务理解", summary: summaries[0], details: detailsList[0] },
- { kind: "scope", title: "范围分析", summary: summaries[1], details: detailsList[1] },
- { kind: "skill", title: "技能选择", summary: summaries[2], details: detailsList[2] },
- { kind: "context", title: "上下文准备", summary: summaries[3], details: detailsList[3] },
- { kind: "tool", title: "工具调用", summary: summaries[4], details: detailsList[4] },
- { kind: "thinking", title: "思考与角色", summary: summaries[5], details: detailsList[5] },
- { kind: "generation", title: "生成与校验", summary: summaries[6], details: detailsList[6] },
- ]
- 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"
- }
- }
- if (activated && !isStreaming) {
- status = "done"
- }
- return { ...def, status }
- })
- }
- ```
- - [ ] **Step 4: 运行测试验证通过**
- Run: `npx vitest run src/lib/novel/outline-stage-trace.spec.ts`
- Expected: 全部 PASS
- - [ ] **Step 5: 提交**
- ```
- git add src/lib/novel/outline-stage-trace.ts src/lib/novel/outline-stage-trace.spec.ts
- git commit -m "填充阶段摘要运行时数据"
- ```
- </div>
- <div class="task-block">
- <h3>Task 5: 添加过渡提示和分析摘要</h3>
- <p><strong>问题 U3</strong>:clear 自动注入时无过渡提示。<br>
- <strong>问题 U4</strong>:推荐选项区域缺少分析摘要。</p>
- **Files:**
- - Modify: `src/components/sources/outline-chat-panel.tsx`
- - [ ] **Step 1: 在 clear 自动注入前添加系统消息**
- 在 handleSend 流结束回调中(约 L2098-L2106),clear 自动注入逻辑前添加系统消息:
- 将:
- ```typescript
- if (intentResult.clarity === "clear") {
- // 自动注入生成 prompt
- const scope = intentResult.detectedScope;
- void handleSend(
- buildGenerationPrompt(lastIntentTitle, lastIntentHint, scope),
- [],
- { intentPhase: "generation" },
- );
- }
- ```
- 替换为:
- ```typescript
- if (intentResult.clarity === "clear") {
- // 插入过渡提示
- addMessage(convId, {
- id: crypto.randomUUID(),
- role: "user",
- content: `✓ 意图明确(${intentResult.module}${intentResult.detectedScope ? `:${intentResult.detectedScope}` : ""}),开始生成...`,
- });
- // 自动注入生成 prompt
- const scope = intentResult.detectedScope;
- void handleSend(
- buildGenerationPrompt(lastIntentTitle, lastIntentHint, scope),
- [],
- { intentPhase: "generation" },
- );
- }
- ```
- 注意:U4(分析摘要)已在 Task 3 的 IntentOptionsCard 中通过 `result.analysis` 渲染实现。
- - [ ] **Step 2: 运行 typecheck**
- Run: `npx tsc --noEmit`
- Expected: 无错误
- - [ ] **Step 3: 提交**
- ```
- git add src/components/sources/outline-chat-panel.tsx
- git commit -m "添加clear自动注入过渡提示"
- ```
- </div>
- <div class="task-block">
- <h3>Task 6: 修复闭包陷阱,使用 useRef 保存意图上下文</h3>
- <p><strong>问题 Q2</strong>:lastIntentTitle/lastIntentHint 在 handleSend 闭包中可能过期。</p>
- **Files:**
- - Modify: `src/components/sources/outline-chat-panel.tsx`
- - [ ] **Step 1: 将 lastIntentTitle/lastIntentHint 改为 useRef**
- 将(约 L1168-L1170):
- ```typescript
- const [lastIntentTitle, setLastIntentTitle] = useState("");
- const [lastIntentHint, setLastIntentHint] = useState("");
- ```
- 替换为:
- ```typescript
- const lastIntentTitleRef = useRef("");
- const lastIntentHintRef = useRef("");
- ```
- - [ ] **Step 2: 更新 handleGenerateSection 中的赋值**
- 将(约 L2217-L2224):
- ```typescript
- const handleGenerateSection = useCallback(
- (title: string, requestHint: string) => {
- setLastIntentTitle(title);
- setLastIntentHint(requestHint);
- const intentPrompt = buildIntentAnalysisPrompt(title, requestHint);
- void handleSend(intentPrompt, [], { intentPhase: "intent_analysis" });
- },
- [handleSend],
- );
- ```
- 替换为:
- ```typescript
- const handleGenerateSection = useCallback(
- (title: string, requestHint: string) => {
- lastIntentTitleRef.current = title;
- lastIntentHintRef.current = requestHint;
- const intentPrompt = buildIntentAnalysisPrompt(title, requestHint);
- void handleSend(intentPrompt, [], { intentPhase: "intent_analysis" });
- },
- [handleSend],
- );
- ```
- - [ ] **Step 3: 更新 handleSend 中引用 lastIntentTitle/lastIntentHint 的位置**
- 在流结束回调中(约 L2101-L2102),将:
- ```typescript
- buildGenerationPrompt(lastIntentTitle, lastIntentHint, scope),
- ```
- 替换为:
- ```typescript
- buildGenerationPrompt(lastIntentTitleRef.current, lastIntentHintRef.current, scope),
- ```
- 在 onSendMessage 的渲染位置(约 L2870),将:
- ```tsx
- onSendMessage={(text, options) => {
- void handleSend(
- buildGenerationPrompt(lastIntentTitle, lastIntentHint, options?.scope),
- ```
- 替换为:
- ```tsx
- onSendMessage={(text, options) => {
- void handleSend(
- buildGenerationPrompt(lastIntentTitleRef.current, lastIntentHintRef.current, options?.scope),
- ```
- - [ ] **Step 4: 更新 handleSend 依赖数组**
- 从 handleSend 的依赖数组中移除 `lastIntentTitle`、`lastIntentHint`,添加 `lastIntentTitleRef`、`lastIntentHintRef`。
- 实际上 useRef 不需要在依赖数组中,所以直接移除 `lastIntentTitle` 和 `lastIntentHint` 即可。
- 同时移除 `setLastIntentResult`(因为 lastIntentResult 也可以改用 ref)。
- 将(约 L2211-L2213):
- ```typescript
- lastIntentTitle,
- lastIntentHint,
- setLastIntentResult,
- ```
- 替换为空(直接删除这三行)。
- - [ ] **Step 5: 将 lastIntentResult 也改为 useRef**
- 将(约 L1168):
- ```typescript
- const [lastIntentResult, setLastIntentResult] = useState<IntentClarityResult | null>(null);
- ```
- 替换为:
- ```typescript
- const lastIntentResultRef = useRef<IntentClarityResult | null>(null);
- ```
- 在流结束回调中,将:
- ```typescript
- setLastIntentResult(intentResult);
- ```
- 替换为:
- ```typescript
- lastIntentResultRef.current = intentResult;
- ```
- - [ ] **Step 6: 运行 typecheck + 大纲面板测试**
- Run: `npx tsc --noEmit`
- Run: `npx vitest run src/components/sources/outline-chat-panel.spec.tsx`
- Expected: 无错误,全部 PASS
- - [ ] **Step 7: 提交**
- ```
- git add src/components/sources/outline-chat-panel.tsx
- git commit -m "修复闭包陷阱:lastIntent改用useRef"
- ```
- </div>
- <h2><span class="phase-tag phase-3">第三阶段</span>架构完善与质量提升(P2)</h2>
- <div class="callout">
- <strong>目标</strong>:outputMode 真正生效,状态机驱动流程,推荐过滤优化,测试覆盖完整。预估 9h。
- </div>
- <div class="task-block">
- <h3>Task 7: outputMode 在生成流程中生效</h3>
- <p><strong>问题 F4</strong>:outputMode 字段已定义但生成流程未使用。</p>
- **Files:**
- - Modify: `src/lib/novel/outline-section-configs.ts`
- - Modify: `src/components/sources/outline-chat-panel.tsx`
- - [ ] **Step 1: 在 buildGenerationPrompt 中使用 outputMode**
- 当前 `buildGenerationPrompt`(约 L376-L399)不接收 outputMode 参数。修改为接收并使用:
- 将:
- ```typescript
- function buildGenerationPrompt(
- title: string,
- requestHint: string,
- scope?: string,
- ): string {
- ```
- 替换为:
- ```typescript
- function buildGenerationPrompt(
- title: string,
- requestHint: string,
- scope?: string,
- outputMode?: "per_chapter" | "per_item" | "single",
- ): string {
- ```
- 在 prompt 中添加 outputMode 相关指导:
- 在 `buildGenerationPrompt` 的返回数组中,在 `"## 文件输出要求"` 之后添加:
- ```typescript
- outputMode === "per_chapter"
- ? "每个章节必须输出独立的 outlineSaveRequest,每个对应一个独立 .md 文件,文件名格式:第N章-章节标题.md。禁止将多个章节写入同一文件。"
- : outputMode === "per_item"
- ? "每个角色/势力/体系必须输出独立的 outlineSaveRequest,每个对应一个独立 .md 文件,文件名格式:名称.md。禁止将多个角色/势力写入同一文件。"
- : "按可保存的大纲正文输出:标题清楚、条目完整、能直接指导后续小说写作。",
- ```
- - [ ] **Step 2: 在 handleGenerateSection 中传递 outputMode**
- 修改 `handleGenerateSection`(约 L2217-L2224):
- ```typescript
- const handleGenerateSection = useCallback(
- (title: string, requestHint: string) => {
- lastIntentTitleRef.current = title;
- lastIntentHintRef.current = requestHint;
- const config = OUTLINE_SECTION_GENERATION_CONFIGS.find(c => c.title === title);
- lastIntentOutputModeRef.current = config?.outputMode;
- const intentPrompt = buildIntentAnalysisPrompt(title, requestHint);
- void handleSend(intentPrompt, [], { intentPhase: "intent_analysis" });
- },
- [handleSend],
- );
- ```
- 添加 `lastIntentOutputModeRef`:
- ```typescript
- const lastIntentOutputModeRef = useRef<"per_chapter" | "per_item" | "single" | undefined>(undefined);
- ```
- - [ ] **Step 3: 在 clear 自动注入生成时传递 outputMode**
- 在流结束回调中,修改 clear 分支:
- ```typescript
- buildGenerationPrompt(
- lastIntentTitleRef.current,
- lastIntentHintRef.current,
- scope,
- lastIntentOutputModeRef.current,
- ),
- ```
- 同样修改 onSendMessage 中的调用:
- ```tsx
- onSendMessage={(text, options) => {
- void handleSend(
- buildGenerationPrompt(
- lastIntentTitleRef.current,
- lastIntentHintRef.current,
- options?.scope,
- lastIntentOutputModeRef.current,
- ),
- [],
- { intentPhase: options?.intentPhase ?? "generation" },
- );
- }}
- ```
- - [ ] **Step 4: 运行 typecheck + 大纲面板测试**
- Run: `npx tsc --noEmit`
- Run: `npx vitest run src/components/sources/outline-chat-panel.spec.tsx`
- Expected: 无错误,全部 PASS
- - [ ] **Step 5: 提交**
- ```
- git add src/lib/novel/outline-section-configs.ts src/components/sources/outline-chat-panel.tsx
- git commit -m "outputMode在生成流程中生效"
- ```
- </div>
- <div class="task-block">
- <h3>Task 8: 优化 isRecommendationForbidden 精确度</h3>
- <p><strong>问题 Q4</strong>:isRecommendationForbidden 可能误杀合法推荐。</p>
- **Files:**
- - Modify: `src/lib/novel/outline-next-step.ts`
- - Modify: `src/lib/novel/outline-next-step.spec.ts`
- - [ ] **Step 1: 新增误杀测试**
- ```typescript
- it("不误杀包含'正文'但非正文生成的推荐", () => {
- expect(isRecommendationForbidden("生成组织势力正文说明")).toBe(false)
- expect(isRecommendationForbidden("完善伏笔正文分析")).toBe(false)
- expect(isRecommendationForbidden("生成角色设定文档")).toBe(false)
- })
- it("精确禁止章节正文生成", () => {
- expect(isRecommendationForbidden("生成章节正文")).toBe(true)
- expect(isRecommendationForbidden("生成对应正文")).toBe(true)
- expect(isRecommendationForbidden("写章节正文")).toBe(true)
- expect(isRecommendationForbidden("开始写正文")).toBe(true)
- })
- ```
- - [ ] **Step 2: 优化 isRecommendationForbidden 正则**
- 将:
- ```typescript
- const FORBIDDEN_PATTERNS = [
- /生成.*正文/,
- /写.*正文/,
- /生成.*章节内容/,
- /生成.*正文章节/,
- /章节正文/,
- /写正文/,
- /生成正文/,
- ]
- ```
- 替换为:
- ```typescript
- const FORBIDDEN_PATTERNS = [
- /生成.*(章节?|对应|相关).?正文/,
- /写.*(章节?|对应|相关).?正文/,
- /生成正文/,
- /写正文/,
- /开始写正文/,
- /生成章节内容/,
- ]
- ```
- - [ ] **Step 3: 运行测试**
- Run: `npx vitest run src/lib/novel/outline-next-step.spec.ts`
- Expected: 全部 PASS
- - [ ] **Step 4: 提交**
- ```
- git add src/lib/novel/outline-next-step.ts src/lib/novel/outline-next-step.spec.ts
- git commit -m "优化isRecommendationForbidden精确度避免误杀"
- ```
- </div>
- <div class="task-block">
- <h3>Task 9: 面板集成状态机</h3>
- <p><strong>问题 F5</strong>:状态机定义了 intent_analysis/waiting_user_input 但面板未使用。</p>
- **Files:**
- - Modify: `src/components/sources/outline-chat-panel.tsx`
- - [ ] **Step 1: 引入状态机状态**
- 在面板主组件中(约 L1168 附近),添加:
- ```typescript
- const [outlineWorkflowStage, setOutlineWorkflowStage] = useState<OutlineWorkflowStage>("idle");
- ```
- 添加 import:
- ```typescript
- import {
- type OutlineWorkflowStage,
- createInitialOutlineWorkflowState,
- transitionOutlineWorkflow,
- canTransitionOutlineWorkflow,
- } from "@/lib/novel/outline-workflow-state"
- ```
- - [ ] **Step 2: 在关键流程节点转换状态**
- 在 `handleGenerateSection` 中,发送意图分析前:
- ```typescript
- if (canTransitionOutlineWorkflow(outlineWorkflowStage, "intent_analysis")) {
- setOutlineWorkflowStage("intent_analysis");
- }
- ```
- 在流结束回调中,clear 分支自动注入生成前:
- ```typescript
- if (canTransitionOutlineWorkflow(outlineWorkflowStage, "sufficiency_check")) {
- setOutlineWorkflowStage("sufficiency_check");
- }
- ```
- 在 needs_input 分支:
- ```typescript
- if (canTransitionOutlineWorkflow(outlineWorkflowStage, "waiting_user_input")) {
- setOutlineWorkflowStage("waiting_user_input");
- }
- ```
- 在用户选择推荐后(onSendMessage 回调中):
- ```typescript
- if (canTransitionOutlineWorkflow(outlineWorkflowStage, "sufficiency_check")) {
- setOutlineWorkflowStage("sufficiency_check");
- }
- ```
- 在生成完成(流结束且 intentPhase 为 generation)时:
- ```typescript
- if (canTransitionOutlineWorkflow(outlineWorkflowStage, "saved")) {
- setOutlineWorkflowStage("saved");
- }
- ```
- - [ ] **Step 3: 在面板标题区域显示当前状态**
- 在面板顶栏(约 L2800 附近),在现有标题旁添加状态标签:
- ```tsx
- {outlineWorkflowStage !== "idle" && outlineWorkflowStage !== "saved" ? (
- <span className="ml-2 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">
- {outlineWorkflowStage === "intent_analysis" ? "意图分析中" :
- outlineWorkflowStage === "waiting_user_input" ? "等待选择" :
- outlineWorkflowStage === "sufficiency_check" ? "生成中" :
- "处理中"}
- </span>
- ) : null}
- ```
- - [ ] **Step 4: 运行 typecheck**
- Run: `npx tsc --noEmit`
- Expected: 无错误
- - [ ] **Step 5: 提交**
- ```
- git add src/components/sources/outline-chat-panel.tsx
- git commit -m "面板集成状态机驱动流程"
- ```
- </div>
- <div class="task-block">
- <h3>Task 10: 新增面板集成测试</h3>
- <p><strong>问题 M3</strong>:缺少面板集成测试。</p>
- **Files:**
- - Modify: `src/components/sources/outline-chat-panel.spec.tsx`
- - [ ] **Step 1: 在现有测试文件中新增集成测试**
- 在 `src/components/sources/outline-chat-panel.spec.tsx` 末尾添加:
- ```typescript
- it("handleGenerateSection 发送意图分析 prompt 并设置 intentPhase", () => {
- renderOutlineChatPanel();
- const generateButtons = screen.getAllByRole("button", { name: /章节细纲|人物小传|组织势力/ });
- // 点击第一个生成按钮
- if (generateButtons.length > 0) {
- fireEvent.click(generateButtons[0]);
- // 验证 handleSend 被调用且 intentPhase 为 intent_analysis
- const lastCall = mockHandleSend.mock.calls[mockHandleSend.mock.calls.length - 1];
- expect(lastCall?.[2]?.intentPhase).toBe("intent_analysis");
- }
- });
- it("intent_clarity 解析后 clear 自动注入生成 prompt", () => {
- // 验证 stripStructuredMarkers 被正确调用
- // 验证显示内容中不含 intent_clarity JSON 块
- renderOutlineChatPanel();
- // 模拟一条包含 intent_clarity 的 assistant 消息
- const store = useOutlineChatStore.getState();
- const convId = store.activeConversationId ?? store.createConversation();
- store.addMessage(convId, {
- id: "test-msg-1",
- role: "assistant",
- content: `<!-- intent_clarity -->\n{"clarity":"clear","module":"章节细纲","analysis":"3章缺细纲","detectedScope":"第1-3章","missingItems":[],"options":[],"question":""}\n<!-- /intent_clarity -->`,
- intentPhase: "intent_analysis",
- intentClarityResult: {
- clarity: "clear",
- module: "章节细纲",
- analysis: "3章缺细纲",
- detectedScope: "第1-3章",
- missingItems: [],
- options: [],
- question: "",
- },
- });
- // 重新渲染
- rerenderOutlineChatPanel();
- // 验证显示内容中不包含 intent_clarity 标记
- expect(screen.queryByText(/intent_clarity/)).toBeNull();
- });
- ```
- 注意:需要根据现有测试的 mock 结构调整。如果测试使用的是真实组件渲染,需确保 mock 了 handleSend。
- - [ ] **Step 2: 运行测试**
- Run: `npx vitest run src/components/sources/outline-chat-panel.spec.tsx`
- Expected: 全部 PASS
- - [ ] **Step 3: 提交**
- ```
- git add src/components/sources/outline-chat-panel.spec.tsx
- git commit -m "新增面板集成测试"
- ```
- </div>
- <h2><span class="phase-tag phase-4">第四阶段</span>性能与兼容性打磨(P3)</h2>
- <div class="callout callout-success">
- <strong>目标</strong>:性能优化、兼容性保障、边界场景处理。预估 4.5h。
- </div>
- <div class="task-block">
- <h3>Task 11: 性能优化</h3>
- <p><strong>问题 P1-P3</strong>:buildOutlineStages 频繁重计算、组件未 memo、正则替换效率低。</p>
- **Files:**
- - Modify: `src/components/sources/outline-workflow-stages.tsx`
- - Modify: `src/lib/novel/outline-stage-trace.ts`
- - [ ] **Step 1: 对 OutlineWorkflowStages 添加 React.memo**
- 将 `OutlineWorkflowStages` 导出改为:
- ```typescript
- export const OutlineWorkflowStages = React.memo(function OutlineWorkflowStages(props: OutlineStageInput) {
- const stages = buildOutlineStages(props)
- const visibleStages = stages.filter((s) => s.status !== "hidden")
- 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>
- )
- })
- ```
- - [ ] **Step 2: 优化 hasGenerationStarted 避免全文正则替换**
- 在 `outline-stage-trace.ts` 中,将:
- ```typescript
- function hasGenerationStarted(content: string): boolean {
- const withoutThinking = content.replace(THINKING_PATTERN, "")
- const withoutClarity = withoutThinking.replace(INTENT_CLARITY_PATTERN, "")
- return withoutClarity.trim().length > 0
- }
- ```
- 替换为:
- ```typescript
- function hasGenerationStarted(content: string): boolean {
- if (!content) return false
- let idx = 0
- while (idx < content.length) {
- const thinkStart = content.indexOf("<think", idx)
- const clarityStart = content.indexOf("<!-- intent_clarity", idx)
- if (thinkStart === -1 && clarityStart === -1) {
- return content.slice(idx).trim().length > 0
- }
- const nextMark = Math.min(
- thinkStart === -1 ? Infinity : thinkStart,
- clarityStart === -1 ? Infinity : clarityStart,
- )
- if (nextMark > idx && content.slice(idx, nextMark).trim().length > 0) {
- return true
- }
- const thinkEnd = content.indexOf("</think>", thinkStart)
- const clarityEnd = content.indexOf("<!-- /intent_clarity -->", clarityStart)
- idx = Math.max(
- thinkStart >= 0 && thinkEnd >= 0 ? thinkEnd + 8 : 0,
- clarityStart >= 0 && clarityEnd >= 0 ? clarityEnd + 24 : 0,
- nextMark + 1,
- )
- }
- return false
- }
- ```
- - [ ] **Step 3: 运行测试**
- Run: `npx vitest run src/lib/novel/outline-stage-trace.spec.ts`
- Expected: 全部 PASS
- - [ ] **Step 4: 提交**
- ```
- git add src/components/sources/outline-workflow-stages.tsx src/lib/novel/outline-stage-trace.ts
- git commit -m "性能优化:React.memo和indexOf替代正则替换"
- ```
- </div>
- <div class="task-block">
- <h3>Task 12: 兼容性保障</h3>
- <p><strong>问题 C1-C3</strong>:旧数据兼容、AI格式遵从度fallback、弱模型降级。</p>
- **Files:**
- - Modify: `src/components/sources/outline-chat-panel.tsx`
- - Modify: `src/components/sources/outline-workflow-stages.tsx`
- - [ ] **Step 1: 弱模型降级 — 无工具调用时显示简化状态**
- 在 `OutlineWorkflowStages` 组件中,当 `visibleStages.length === 0` 但 `isStreaming` 为 true 时,显示简化状态:
- 将:
- ```typescript
- if (visibleStages.length === 0) return null
- ```
- 替换为:
- ```typescript
- if (visibleStages.length === 0) {
- if (isStreaming) {
- return (
- <div className="mb-2 flex items-center gap-2 text-xs text-muted-foreground">
- <Loader2 className="h-3 w-3 animate-spin" />
- <span>生成中...</span>
- </div>
- )
- }
- return null
- }
- ```
- - [ ] **Step 2: 验证旧数据兼容 — 检查可选字段安全访问**
- 在 `OutlineAssistantMessage` 中,所有新增字段访问均使用可选链 `?.`:
- - `msg.intentClarityResult?.clarity` — 安全
- - `msg.nextStepRecommendation?.recommendations` — 安全
- - `msg.intentPhase` — 安全(undefined 时不会触发任何分支)
- 确认无需额外修改。
- - [ ] **Step 3: 运行 typecheck + 全量测试**
- Run: `npx tsc --noEmit`
- Run: `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 src/components/sources/outline-chat-panel.spec.tsx`
- Expected: 无错误,全部 PASS
- - [ ] **Step 4: 提交**
- ```
- git add src/components/sources/outline-chat-panel.tsx src/components/sources/outline-workflow-stages.tsx
- git commit -m "兼容性保障:弱模型降级显示、旧数据安全访问"
- ```
- </div>
- <div class="task-block">
- <h3>Task 13: 最终回归验证</h3>
- **Files:** 无新修改
- - [ ] **Step 1: 运行全部新增模块测试**
- Run: `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`
- Expected: 全部 PASS
- - [ ] **Step 2: 运行大纲面板测试**
- Run: `npx vitest run src/components/sources/outline-chat-panel.spec.tsx`
- Expected: 全部 PASS
- - [ ] **Step 3: 运行 AI 会话面板测试(回归验证)**
- Run: `npx vitest run src/components/chat/agent-tool-call-message.spec.tsx`
- Expected: 全部 PASS
- - [ ] **Step 4: 运行 TypeScript 全量编译检查**
- Run: `npx tsc --noEmit`
- Expected: 无错误
- - [ ] **Step 5: 确认 git 状态**
- Run: `git log --oneline -15`
- 确认所有 13 个任务的 commit 都在 dagangshengji 分支上
- - [ ] **Step 6: 标记任务完成**
- 所有优化任务完成,更新 todo 列表。
- </div>
- </body>
- </html>
|