From 86b66a311ac05f8766c24e57ccf08615cc6a63c0 Mon Sep 17 00:00:00 2001 From: kun1h Date: Mon, 6 Jul 2026 20:47:50 +0800 Subject: [PATCH] docs: add P4.1 auto and wizard writing implementation plan for v0.5.0 Co-authored-by: Cursor --- .../plans/2026-07-09-bilin-p41-auto-wizard.md | 1079 +++++++++++++++++ 1 file changed, 1079 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-09-bilin-p41-auto-wizard.md diff --git a/docs/superpowers/plans/2026-07-09-bilin-p41-auto-wizard.md b/docs/superpowers/plans/2026-07-09-bilin-p41-auto-wizard.md new file mode 100644 index 0000000..e898cb4 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-bilin-p41-auto-wizard.md @@ -0,0 +1,1079 @@ +# 笔临 P4.1 自动写作 + 向导式写作 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 交付 v0.5.0:实装自动写作(§6.5.2)与向导式写作(§6.5.3),统一写作流引擎扩展 P4;补齐三模式成章 AI 快照。 + +**Architecture:** schema v5 扩展 `interactive_flows`(`flow_mode` / `mode_config_json`);`SceneGenerationCore` 从 P4 抽取共享流式生成;`AutoWritingService` + `WizardWritingService` 步骤机;handoff 至 `InteractiveWritingService`;IPC `auto:*` / `wizard:*`;渲染 `AutoPanel` / `WizardPanel`。测试 **禁止 Mock**,AI 集成/E2E 打真实 LM Studio。 + +**Tech Stack:** Electron 36、electron-vite、React 18、TypeScript、node:sqlite、Zustand、i18next、Vitest、Playwright + +**Spec:** `docs/superpowers/specs/2026-07-09-bilin-p41-auto-wizard-design.md` + +**LM Studio 默认:** `baseUrl=http://127.0.0.1:1234`,`model=gemma-4-e4b-it` + +--- + +## File Map + +| 文件 | 职责 | +|------|------| +| `src/main/db/schema-v5.sql` | v5 DDL | +| `src/main/db/migrate.ts` | v4→v5 | +| `src/main/db/repositories/interactive.repo.ts` | flow_mode / mode_config CRUD | +| `src/main/services/auto-parse.service.ts` | 场景规划 JSON 解析 | +| `src/main/services/auto-prompt-builder.service.ts` | plan / scene / preview prompt | +| `src/main/services/scene-generation-core.service.ts` | 共享流式场景生成 + 命名暂停 | +| `src/main/services/writing-flow-handoff.service.ts` | auto/wizard → interactive handoff | +| `src/main/services/chapter-finish.service.ts` | 合并 scenes + 建章 + AI 快照 | +| `src/main/services/auto-writing.service.ts` | 自动写作步骤机 | +| `src/main/services/wizard-writing.service.ts` | 向导步骤机 | +| `src/main/services/interactive-writing.service.ts` | 改用 ChapterFinishService + SceneGenerationCore | +| `src/main/ipc/handlers/auto.handler.ts` | auto IPC + 流式 | +| `src/main/ipc/handlers/wizard.handler.ts` | wizard IPC + 流式 | +| `src/main/ipc/register.ts` | 注册 handler | +| `src/shared/types.ts` | WritingFlowMode / AutoStep / WizardStep / Config | +| `src/shared/ipc-channels.ts` | auto / wizard 通道 | +| `src/preload/index.ts` | auto / wizard API | +| `src/shared/electron-api.d.ts` | 类型 | +| `src/renderer/stores/useAutoStore.ts` | auto 状态 | +| `src/renderer/stores/useWizardStore.ts` | wizard 状态 | +| `src/renderer/components/ai/AutoPanel.tsx` | 自动 UI | +| `src/renderer/components/ai/WizardPanel.tsx` | 向导 UI | +| `src/renderer/components/ai/AiPanel.tsx` | 模式 Tab 实装 | +| `src/renderer/styles/layout.css` | auto / wizard 样式 | +| `public/locales/zh-CN/translation.json` | 文案 | +| `public/locales/en/translation.json` | 文案 | +| `tests/main/migrate-v5.test.ts` | UT-MIG-05 | +| `tests/main/auto-plan.test.ts` | IT-AUTO-01 | +| `tests/main/auto-generate.test.ts` | IT-AUTO-02/03 | +| `tests/main/auto-handoff.test.ts` | IT-AUTO-04 | +| `tests/main/wizard-preview.test.ts` | IT-WIZ-01 | +| `tests/main/wizard-generate.test.ts` | IT-WIZ-02 | +| `tests/main/chapter-finish-snapshot.test.ts` | IT-FIN-01/02 | +| `e2e/auto-writing.spec.ts` | E2E-AUTO-* | +| `e2e/wizard-writing.spec.ts` | E2E-WIZ-01 | + +--- + +## Task 1: Schema v5 + 共享类型 + IPC 通道 + +**Files:** +- Create: `src/main/db/schema-v5.sql` +- Modify: `src/main/db/migrate.ts` +- Modify: `src/shared/types.ts` +- Modify: `src/shared/ipc-channels.ts` +- Create: `tests/main/migrate-v5.test.ts` + +- [ ] **Step 1: 写迁移失败测试** + +`tests/main/migrate-v5.test.ts`: + +```typescript +import { describe, it, expect, afterEach } from 'vitest' +import { DatabaseSync } from 'node:sqlite' +import { migrate } from '../../src/main/db/migrate' +import schemaV1 from '../../src/main/db/schema-v1.sql?raw' +import schemaV2 from '../../src/main/db/schema-v2.sql?raw' +import schemaV3 from '../../src/main/db/schema-v3.sql?raw' +import schemaV4 from '../../src/main/db/schema-v4.sql?raw' +import type { SqliteDb } from '../../src/main/db/types' + +describe('migrate v5', () => { + let db: SqliteDb + afterEach(() => db?.close()) + + it('UT-MIG-05: applies v5 on fresh database', () => { + db = new DatabaseSync(':memory:') + migrate(db) + const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number } + expect(version.v).toBe(5) + const cols = db.prepare('PRAGMA table_info(interactive_flows)').all() as { name: string }[] + expect(cols.some((c) => c.name === 'flow_mode')).toBe(true) + expect(cols.some((c) => c.name === 'mode_config_json')).toBe(true) + }) + + it('migrates v4 database to v5', () => { + db = new DatabaseSync(':memory:') + db.exec(`CREATE TABLE IF NOT EXISTS schema_version ( + version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL DEFAULT (datetime('now')), description TEXT + )`) + db.exec(schemaV1) + db.exec(schemaV2) + db.exec(schemaV3) + db.prepare('INSERT INTO schema_version (version) VALUES (1),(2),(3)').run() + try { + db.exec("ALTER TABLE chapters ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual'") + } catch { /* duplicate ok */ } + db.exec(schemaV4) + db.prepare('INSERT INTO schema_version (version) VALUES (4)').run() + migrate(db) + const version = db.prepare('SELECT MAX(version) AS v FROM schema_version').get() as { v: number } + expect(version.v).toBe(5) + }) +}) +``` + +- [ ] **Step 2: 运行确认 FAIL** + +```bash +npm run test -- tests/main/migrate-v5.test.ts +``` + +Expected: FAIL(version 仍为 4 或列不存在) + +- [ ] **Step 3: 创建 `schema-v5.sql`** + +```sql +ALTER TABLE interactive_flows ADD COLUMN flow_mode TEXT NOT NULL DEFAULT 'interactive'; +ALTER TABLE interactive_flows ADD COLUMN mode_config_json TEXT NOT NULL DEFAULT '{}'; +``` + +- [ ] **Step 4: 更新 `migrate.ts`** + +```typescript +import schemaV5 from './schema-v5.sql?raw' + +const CURRENT_VERSION = 5 + +function applyV5(db: SqliteDb): void { + tryAlter(db, "ALTER TABLE interactive_flows ADD COLUMN flow_mode TEXT NOT NULL DEFAULT 'interactive'") + tryAlter(db, "ALTER TABLE interactive_flows ADD COLUMN mode_config_json TEXT NOT NULL DEFAULT '{}'") +} + +// migrate() 末尾追加: +if (current < 5) { + applyV5(db) + db.prepare('INSERT INTO schema_version (version, description) VALUES (?, ?)').run(5, 'P4.1 auto/wizard schema') +} +``` + +- [ ] **Step 5: `types.ts` 追加类型** + +```typescript +export type WritingFlowMode = 'interactive' | 'auto' | 'wizard' +export type AutoRhythm = 'relaxed' | 'tense' | 'mixed' + +export type AutoStep = + | 'goal_setup' + | 'scene_plan' + | 'auto_generate' + | 'paused' + | 'completed' + +export type WizardStep = + | 'wizard_goal' + | 'wizard_rhythm' + | 'wizard_pov' + | 'wizard_context' + | 'wizard_preview' + | 'wizard_generating' + +export type WritingStep = InteractiveStep | AutoStep | WizardStep + +export interface ScenePlanItem { + label: string + summary: string +} + +export interface AutoWritingConfig { + chapterGoal: string + outlineItemIds?: string[] + wordMin: number + wordMax: number + scenePlan?: ScenePlanItem[] + currentPlanIndex?: number +} + +export interface WizardWritingConfig extends AutoWritingConfig { + rhythm: AutoRhythm + styleNote?: string + povSettingId?: string + strategyPreview?: string + contextBinding?: AiContextBinding +} + +export type ChapterOrigin = 'manual' | 'interactive' | 'auto' | 'wizard' +``` + +扩展 `InteractiveFlow`: + +```typescript +export interface InteractiveFlow { + // ...existing fields + flowMode: WritingFlowMode + modeConfig: AutoWritingConfig | WizardWritingConfig | Record + step: WritingStep +} +``` + +扩展流式事件: + +```typescript +export interface InteractiveStreamChunkEvent { + flowId: string + flowMode?: WritingFlowMode + phase: 'scene' | 'naming' | 'refine' | 'plan' + delta: string + done: boolean +} +``` + +- [ ] **Step 6: `ipc-channels.ts` 追加** + +```typescript +AUTO_CHECK_GATE: 'auto:checkGate', +AUTO_GET_FLOW: 'auto:getFlow', +AUTO_START: 'auto:start', +AUTO_SET_GOAL: 'auto:setGoal', +AUTO_PLAN_SCENES: 'auto:planScenes', +AUTO_GENERATE: 'auto:generate', +AUTO_PAUSE: 'auto:pause', +AUTO_RESUME: 'auto:resume', +AUTO_HANDOFF_INTERACTIVE: 'auto:handoffInteractive', +AUTO_FINISH_CHAPTER: 'auto:finishChapter', +AUTO_GET_SCENE_DRAFT: 'auto:getSceneDraft', +AUTO_ABORT: 'auto:abort', + +WIZARD_GET_FLOW: 'wizard:getFlow', +WIZARD_START: 'wizard:start', +WIZARD_SET_GOAL: 'wizard:setGoal', +WIZARD_SET_RHYTHM: 'wizard:setRhythm', +WIZARD_SET_POV: 'wizard:setPov', +WIZARD_CONFIRM_CONTEXT: 'wizard:confirmContext', +WIZARD_BUILD_PREVIEW: 'wizard:buildPreview', +WIZARD_START_GENERATE: 'wizard:startGenerate', +WIZARD_HANDOFF_INTERACTIVE: 'wizard:handoffInteractive', +WIZARD_ABORT: 'wizard:abort', +``` + +- [ ] **Step 7: 运行测试 PASS** + +```bash +npm run test -- tests/main/migrate-v5.test.ts +``` + +- [ ] **Step 8: Commit** + +```bash +git add src/main/db/schema-v5.sql src/main/db/migrate.ts src/shared/types.ts src/shared/ipc-channels.ts tests/main/migrate-v5.test.ts +git commit -m "feat: add schema v5 and auto/wizard shared types" +``` + +--- + +## Task 2: 扩展 InteractiveRepository + +**Files:** +- Modify: `src/main/db/repositories/interactive.repo.ts` + +- [ ] **Step 1: 扩展 `create` 支持 flow_mode** + +```typescript +create(sessionId: string, flowMode: WritingFlowMode = 'interactive', initialStep?: WritingStep): InteractiveFlow { + const id = randomUUID() + const step = initialStep ?? (flowMode === 'auto' ? 'goal_setup' : flowMode === 'wizard' ? 'wizard_goal' : 'context_confirm') + this.db.prepare( + `INSERT INTO interactive_flows (id, session_id, step, status, context_json, flow_mode, mode_config_json) + VALUES (?, ?, ?, 'in_progress', '{}', ?, '{}')` + ).run(id, sessionId, step, flowMode) + return this.get(id)! +} + +resetInProgress(sessionId: string, flowMode: WritingFlowMode = 'interactive'): InteractiveFlow { + this.db.prepare( + `UPDATE interactive_flows SET status = 'abandoned', updated_at = datetime('now') WHERE session_id = ? AND status = 'in_progress'` + ).run(sessionId) + return this.create(sessionId, flowMode) +} +``` + +- [ ] **Step 2: 泛化 `updateStep`** + +```typescript +updateStep( + flowId: string, + step: WritingStep, + patch: { + contextJson?: string + plotSelectionJson?: string | null + namingPendingJson?: string | null + sceneDraftHtml?: string | null + modeConfigJson?: string + flowMode?: WritingFlowMode + status?: InteractiveFlowStatus + } = {} +): InteractiveFlow +``` + +在 patch 中若 `modeConfigJson` / `flowMode` 有值则追加 SET。 + +- [ ] **Step 3: mode_config 读写** + +```typescript +getModeConfig(flowId: string): T { + const row = this.db.prepare('SELECT mode_config_json FROM interactive_flows WHERE id = ?').get(flowId) as + | { mode_config_json: string } + | undefined + try { + return JSON.parse(row?.mode_config_json ?? '{}') as T + } catch { + return {} as T + } +} + +setModeConfig(flowId: string, config: AutoWritingConfig | WizardWritingConfig): void { + this.db.prepare( + `UPDATE interactive_flows SET mode_config_json = ?, updated_at = datetime('now') WHERE id = ?` + ).run(JSON.stringify(config), flowId) +} +``` + +- [ ] **Step 4: 更新 `mapFlow`** + +```typescript +private mapFlow(row: Record): InteractiveFlow { + // ...existing namingPending parse + let modeConfig: AutoWritingConfig | WizardWritingConfig | Record = {} + try { + modeConfig = JSON.parse((row.mode_config_json as string) ?? '{}') + } catch { /* empty */ } + return { + id: flowId, + sessionId: row.session_id as string, + flowMode: (row.flow_mode as WritingFlowMode) ?? 'interactive', + step: row.step as WritingStep, + status: row.status as InteractiveFlowStatus, + modeConfig, + scenes: this.listScenes(flowId), + namingPending, + contextSummary: undefined + } +} +``` + +- [ ] **Step 5: 运行现有 migrate + gate 测试** + +```bash +npm run test -- tests/main/migrate-v4.test.ts tests/main/interactive-gate.test.ts +``` + +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/main/db/repositories/interactive.repo.ts +git commit -m "feat: extend InteractiveRepository for flow_mode and mode_config" +``` + +--- + +## Task 3: Auto 解析 + Prompt 构建 + +**Files:** +- Create: `src/main/services/auto-parse.service.ts` +- Create: `src/main/services/auto-prompt-builder.service.ts` +- Create: `tests/main/auto-parse.test.ts` + +- [ ] **Step 1: 写解析单元测试(无 AI)** + +`tests/main/auto-parse.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest' +import { parseScenePlanJson } from '../../src/main/services/auto-parse.service' + +describe('parseScenePlanJson', () => { + it('parses valid scene plan', () => { + const json = JSON.stringify({ + scenes: [ + { label: '开场', summary: '林远步入演武场,众人围观,测灵石前气氛紧张,他感受到体内灵力涌动。' }, + { label: '异变', summary: '测灵石突然碎裂,长老震惊,同门哗然,林远意识到自己的天赋远超预期。' } + ], + estimatedWords: 3000 + }) + const plan = parseScenePlanJson(json) + expect(plan.scenes).toHaveLength(2) + expect(plan.estimatedWords).toBe(3000) + }) + + it('throws on invalid JSON', () => { + expect(() => parseScenePlanJson('not json')).toThrow() + }) +}) +``` + +- [ ] **Step 2: 实现 `auto-parse.service.ts`** + +```typescript +import type { ScenePlanItem } from '../../shared/types' + +export interface ScenePlanResult { + scenes: ScenePlanItem[] + estimatedWords: number +} + +export function parseScenePlanJson(response: string): ScenePlanResult { + const trimmed = response.trim() + const start = trimmed.indexOf('{') + const end = trimmed.lastIndexOf('}') + if (start < 0 || end <= start) throw new Error('scene plan JSON not found') + const parsed = JSON.parse(trimmed.slice(start, end + 1)) as { + scenes?: unknown + estimatedWords?: unknown + } + if (!Array.isArray(parsed.scenes) || parsed.scenes.length < 2) { + throw new Error('scenes array invalid') + } + const scenes = parsed.scenes.map((s, i) => { + const row = s as Record + return { + label: String(row.label ?? `场景${i + 1}`), + summary: String(row.summary ?? '') + } + }) + return { + scenes, + estimatedWords: Number(parsed.estimatedWords ?? 3000) + } +} +``` + +- [ ] **Step 3: 实现 `auto-prompt-builder.service.ts`** + +导出三个函数(参考 P4 `interactive-prompt-builder.service.ts` 风格): + +```typescript +export function buildScenePlanMessages( + chapterGoal: string, + outlineSummaries: string[], + contextText: string, + wordMin: number, + wordMax: number +): ChatMessage[] + +export function buildAutoSceneMessages( + config: AutoWritingConfig, + planItem: ScenePlanItem, + priorSceneSummaries: string[], + contextText: string +): ChatMessage[] + +export function buildWizardPreviewMessages( + config: WizardWritingConfig, + contextText: string, + povName: string +): ChatMessage[] +``` + +system prompt 约束:plan 返回 JSON `{ scenes, estimatedWords }`;scene 返回 HTML;preview 返回一段中文 HTML `

`。 + +- [ ] **Step 4: 运行 PASS + Commit** + +```bash +npm run test -- tests/main/auto-parse.test.ts +git add src/main/services/auto-parse.service.ts src/main/services/auto-prompt-builder.service.ts tests/main/auto-parse.test.ts +git commit -m "feat: add auto scene plan parse and prompt builder" +``` + +--- + +## Task 4: SceneGenerationCore + ChapterFinishService + +**Files:** +- Create: `src/main/services/scene-generation-core.service.ts` +- Create: `src/main/services/writing-flow-handoff.service.ts` +- Create: `src/main/services/chapter-finish.service.ts` +- Modify: `src/main/services/interactive-writing.service.ts` +- Create: `tests/main/chapter-finish-snapshot.test.ts` + +- [ ] **Step 1: 写快照失败测试** + +`tests/main/chapter-finish-snapshot.test.ts`: + +```typescript +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { DatabaseSync } from 'node:sqlite' +import { migrate } from '../../src/main/db/migrate' +import { VolumeRepository } from '../../src/main/db/repositories/volume.repo' +import { AiSessionRepository } from '../../src/main/db/repositories/ai-session.repo' +import { InteractiveRepository } from '../../src/main/db/repositories/interactive.repo' +import { SnapshotRepository } from '../../src/main/db/repositories/snapshot.repo' +import { ChapterFinishService } from '../../src/main/services/chapter-finish.service' +import type { SqliteDb } from '../../src/main/db/types' + +describe('ChapterFinishService', () => { + let db: SqliteDb + let volId: string + let flowId: string + + beforeEach(() => { + db = new DatabaseSync(':memory:') + migrate(db) + volId = new VolumeRepository(db).create('卷一', 0).id + const session = new AiSessionRepository(db).create('t', 'm') + flowId = new InteractiveRepository(db).create(session.id).id + new InteractiveRepository(db).addScene(flowId, '

场景

', 'A') + }) + + afterEach(() => db.close()) + + it('IT-FIN-02: creates ai snapshot on interactive finish', () => { + const service = new ChapterFinishService(db) + const { chapterId } = service.finishFromFlow(flowId, volId, '交互初稿', 'interactive', '交互写作成章') + const snaps = new SnapshotRepository(db).list(chapterId) + expect(snaps.some((s) => s.type === 'ai' && s.name.includes('交互'))).toBe(true) + }) + + it('IT-FIN-01: auto origin and snapshot label', () => { + const service = new ChapterFinishService(db) + const { chapterId } = service.finishFromFlow(flowId, volId, '自动初稿', 'auto', '自动写作成章') + const ch = new ChapterRepository(db).get(chapterId)! + expect(ch.origin).toBe('auto') + }) +}) +``` + +- [ ] **Step 2: 实现 `chapter-finish.service.ts`** + +```typescript +export class ChapterFinishService { + constructor(private db: SqliteDb) {} + + finishFromFlow( + flowId: string, + volumeId: string, + title: string, + origin: ChapterOrigin, + snapshotLabel: string + ): { chapterId: string } { + const repo = new InteractiveRepository(this.db) + const draft = repo.getSceneDraft(flowId).trim() + if (draft) repo.addScene(flowId, draft, 'draft') + const scenes = repo.listScenes(flowId) + if (scenes.length === 0) throw new Error('no scenes to merge') + const merged = scenes.map((s) => s.contentHtml).join('\n') + const chapters = new ChapterRepository(this.db) + const sortOrder = chapters.listByVolume(volumeId).length + const ch = chapters.create(volumeId, title, sortOrder, merged, origin) + new SnapshotRepository(this.db).create(ch.id, 'ai', merged, snapshotLabel) + repo.updateStep(flowId, 'completed', { status: 'completed', sceneDraftHtml: '', namingPendingJson: null }) + return { chapterId: ch.id } + } +} +``` + +- [ ] **Step 3: 实现 `writing-flow-handoff.service.ts`** + +```typescript +export function handoffToInteractive(repo: InteractiveRepository, flowId: string): InteractiveFlow { + const draft = repo.getSceneDraft(flowId) + const scenes = repo.listScenes(flowId) + let step: WritingStep = 'context_confirm' + if (draft.trim()) step = 'scene_review' + else if (scenes.length > 0) step = 'plot_suggest' + return repo.updateStep(flowId, step, { flowMode: 'interactive' }) +} +``` + +- [ ] **Step 4: 实现 `scene-generation-core.service.ts`** + +从 `InteractiveWritingService.generateScene` / `resolveNaming` 抽取: + +```typescript +export class SceneGenerationCore { + constructor( + private repo: InteractiveRepository, + private aiClient: AiClientService + ) {} + + async generateStream( + flowId: string, + messages: ChatMessage[], + onChunk: (delta: string, done: boolean) => void, + abortSignal?: AbortSignal + ): Promise { + // 流式 + tryParseNamingFromBuffer → naming_pause 逻辑,同 P4 + // 返回最终 HTML + } + + normalizeSceneHtml(raw: string): string { + // 同 P4 normalizeSceneHtml + } +} +``` + +- [ ] **Step 5: 重构 `interactive-writing.service.ts`** + +`finishChapter` 改为调用 `ChapterFinishService.finishFromFlow(..., 'interactive', '交互写作成章')`。 + +- [ ] **Step 6: 运行测试 PASS** + +```bash +npm run test -- tests/main/chapter-finish-snapshot.test.ts tests/main/interactive-finish.test.ts +``` + +- [ ] **Step 7: Commit** + +```bash +git add src/main/services/scene-generation-core.service.ts src/main/services/writing-flow-handoff.service.ts src/main/services/chapter-finish.service.ts src/main/services/interactive-writing.service.ts tests/main/chapter-finish-snapshot.test.ts +git commit -m "feat: add chapter finish with ai snapshot and scene generation core" +``` + +--- + +## Task 5: AutoWritingService + +**Files:** +- Create: `src/main/services/auto-writing.service.ts` +- Create: `tests/main/auto-plan.test.ts` +- Create: `tests/main/auto-generate.test.ts` +- Create: `tests/main/auto-handoff.test.ts` + +- [ ] **Step 1: IT-AUTO-01 集成测试** + +`tests/main/auto-plan.test.ts`(240s,真实 LM Studio): + +```typescript +it('IT-AUTO-01: planScenes returns >=2 scenes', async () => { + // migrate → session → create auto flow → setGoal → planScenes + expect(plan.scenes.length).toBeGreaterThanOrEqual(2) +}, 240_000) +``` + +- [ ] **Step 2: 实现 `AutoWritingService`** + +核心方法按 spec §4.2: + +```typescript +export class AutoWritingService { + start(sessionId: string): InteractiveFlow { + return this.repo.resetInProgress(sessionId, 'auto') + } + + setGoal(flowId: string, partial: Partial): InteractiveFlow { + const config: AutoWritingConfig = { + chapterGoal: partial.chapterGoal ?? '', + outlineItemIds: partial.outlineItemIds, + wordMin: partial.wordMin ?? 2000, + wordMax: partial.wordMax ?? 4000, + ...this.repo.getModeConfig(flowId) + } + this.repo.setModeConfig(flowId, config) + return this.repo.updateStep(flowId, 'scene_plan') + } + + async planScenes(flowId: string): Promise { + // buildScenePlanMessages → chat → parseScenePlanJson(失败重试一次) + // 写入 config.scenePlan, currentPlanIndex=0, step=auto_generate + } + + async generateNext(flowId, onChunk, signal): Promise { + // 取 currentPlanIndex 对应 planItem → buildAutoSceneMessages → SceneGenerationCore + // 完成后 addScene, index++, 若 index>=plan.length 或 shouldStop → 保持 auto_generate 待 finish + } + + pause(flowId): InteractiveFlow { + return this.repo.updateStep(flowId, 'paused') + } + + resume(flowId): InteractiveFlow { + return this.repo.updateStep(flowId, 'auto_generate') + } + + handoffToInteractive(flowId): InteractiveFlow { + return handoffToInteractive(this.repo, flowId) + } + + finishChapter(flowId, volumeId, title?): { chapterId: string } { + return new ChapterFinishService(this.db).finishFromFlow(flowId, volumeId, title ?? '自动初稿', 'auto', '自动写作成章') + } +} +``` + +- [ ] **Step 3: IT-AUTO-02/03/04 测试** + +`auto-generate.test.ts`:generateNext 写入 scene;pause/resume step 断言。 +`auto-handoff.test.ts`:handoff 后 `flowMode==='interactive'`(无 AI)。 + +- [ ] **Step 4: 运行 PASS + Commit** + +```bash +npm run test -- tests/main/auto-plan.test.ts tests/main/auto-generate.test.ts tests/main/auto-handoff.test.ts +git commit -m "feat: add AutoWritingService with plan and generate" +``` + +--- + +## Task 6: WizardWritingService + +**Files:** +- Create: `src/main/services/wizard-writing.service.ts` +- Create: `tests/main/wizard-preview.test.ts` +- Create: `tests/main/wizard-generate.test.ts` + +- [ ] **Step 1: IT-WIZ-01 集成测试** + +`wizard-preview.test.ts`(240s): + +```typescript +it('IT-WIZ-01: buildPreview returns non-empty HTML', async () => { + // wizard flow 填完 goal/rhythm/pov/context → buildPreview + expect(preview).toMatch(/

/) +}, 240_000) +``` + +- [ ] **Step 2: 实现 `WizardWritingService`** + +```typescript +export class WizardWritingService { + start(sessionId: string): InteractiveFlow { + return this.repo.resetInProgress(sessionId, 'wizard') + } + + setGoal(flowId, partial): InteractiveFlow { + // merge WizardWritingConfig, step=wizard_rhythm + } + + setRhythm(flowId, rhythm, styleNote?): InteractiveFlow { + // step=wizard_pov + } + + setPov(flowId, povSettingId): InteractiveFlow { + // step=wizard_context + } + + confirmContext(flowId, binding: AiContextBinding): InteractiveFlow { + // config.contextBinding = binding, step=wizard_preview + } + + async buildPreview(flowId: string): Promise { + // buildWizardPreviewMessages → chat → 写入 strategyPreview, 返回 HTML + } + + async startGenerate(flowId, onChunk, signal): Promise { + // step=wizard_generating + // 从 WizardWritingConfig 构建 AutoWritingConfig → autoService.planScenes + loop generateNext 直到 plan 完成 + // 自动 handoffToInteractive + } +} +``` + +- [ ] **Step 3: IT-WIZ-02 集成测试**(300s) + +生成完成后 `flow.flowMode === 'interactive'` 且 `scenes.length >= 1`。 + +- [ ] **Step 4: 运行 PASS + Commit** + +```bash +npm run test -- tests/main/wizard-preview.test.ts tests/main/wizard-generate.test.ts +git commit -m "feat: add WizardWritingService with preview and generate handoff" +``` + +--- + +## Task 7: IPC Handler + Preload + +**Files:** +- Create: `src/main/ipc/handlers/auto.handler.ts` +- Create: `src/main/ipc/handlers/wizard.handler.ts` +- Modify: `src/main/ipc/register.ts` +- Modify: `src/preload/index.ts` +- Modify: `src/shared/electron-api.d.ts` + +- [ ] **Step 1: 实现 `registerAutoHandlers`** + +参照 `interactive.handler.ts`: + +- `activeAuto` AbortController Map +- `AUTO_GENERATE`:流式推送 `{ flowId, flowMode: 'auto', phase: 'scene', delta, done }` +- `AUTO_CHECK_GATE`:调用 `checkInteractiveGate` +- `AUTO_HANDOFF_INTERACTIVE`:返回 enrich 后 flow + +- [ ] **Step 2: 实现 `registerWizardHandlers`** + +- `WIZARD_START_GENERATE`:流式 + 完成后 flow 已 handoff +- 其余为同步 invoke + +- [ ] **Step 3: `register.ts` 注册** + +```typescript +import { registerAutoHandlers } from './handlers/auto.handler' +import { registerWizardHandlers } from './handlers/wizard.handler' +// registerInteractiveHandlers 之后: +registerAutoHandlers(books, settings, aiClient) +registerWizardHandlers(books, settings, aiClient) +``` + +- [ ] **Step 4: preload 暴露命名空间** + +```typescript +auto: { + checkGate: (bookId) => ipcRenderer.invoke(IPC.AUTO_CHECK_GATE, { bookId }), + getFlow: (bookId, sessionId) => ipcRenderer.invoke(IPC.AUTO_GET_FLOW, { bookId, sessionId }), + start: (bookId, sessionId) => ipcRenderer.invoke(IPC.AUTO_START, { bookId, sessionId }), + // ...其余方法 +}, +wizard: { /* 对称 */ }, +onInteractiveStreamChunk: (callback) => { /* 已有,payload 含 flowMode */ } +``` + +- [ ] **Step 5: Commit** + +```bash +git add src/main/ipc/handlers/auto.handler.ts src/main/ipc/handlers/wizard.handler.ts src/main/ipc/register.ts src/preload/index.ts src/shared/electron-api.d.ts +git commit -m "feat: add auto and wizard IPC handlers and preload API" +``` + +--- + +## Task 8: AutoPanel + WizardPanel + Stores + +**Files:** +- Create: `src/renderer/stores/useAutoStore.ts` +- Create: `src/renderer/stores/useWizardStore.ts` +- Create: `src/renderer/components/ai/AutoPanel.tsx` +- Create: `src/renderer/components/ai/WizardPanel.tsx` +- Modify: `src/renderer/components/ai/AiPanel.tsx` +- Modify: `src/renderer/styles/layout.css` + +- [ ] **Step 1: `useAutoStore`** + +参照 `useInteractiveStore.ts`: + +```typescript +interface AutoState { + flow: InteractiveFlow | null + streamBuffer: string + streaming: boolean + sceneDraft: string + gateEligible: boolean + checkGate: (bookId: string) => Promise + loadFlow: (bookId: string, sessionId: string) => Promise + start: (bookId: string, sessionId: string) => Promise + setGoal: (bookId: string, config: Partial) => Promise + planScenes: (bookId: string) => Promise + generateNext: (bookId: string) => Promise + pause: (bookId: string) => Promise + resume: (bookId: string) => Promise + handoffInteractive: (bookId: string) => Promise + finishChapter: (bookId: string, title?: string) => Promise + mount / unmount // 复用 onInteractiveStreamChunk,过滤 flowMode==='auto' +} +``` + +- [ ] **Step 2: `useWizardStore`** + +```typescript +// wizardGoal / setRhythm / setPov / confirmContext / buildPreview / startGenerate +// startGenerate 完成后:useAiStore.setWritingMode('interactive') + showToast +``` + +- [ ] **Step 3: `AutoPanel.tsx`** + +按 spec §7.2 实现全部 `data-testid`;步骤条;`goal_setup` / `scene_plan` / `auto_generate` / `paused` 分支 UI。 + +- [ ] **Step 4: `WizardPanel.tsx`** + +5 步 wizard UI;步骤 4 按钮 `onClick={() => setContextOpen(true)}` 打开 `ContextEditorModal`;步骤 5 展示 `strategyPreview` HTML。 + +- [ ] **Step 5: 修改 `AiPanel.tsx`** + +移除 auto/wizard 的 `comingSoon`;`handleModeClick` 仿 interactive: + +```typescript +if (mode === 'auto' || mode === 'wizard') { + void (async () => { + if (bookId) await (mode === 'auto' ? useAutoStore : useWizardStore).getState().checkGate(bookId) + if (!gateEligible) { showToast(...); return } + setWritingMode(mode) + // loadFlow → start if no flow + })() + return +} +``` + +渲染: + +```typescript +{writingMode === 'auto' && } +{writingMode === 'wizard' && } +{writingMode === 'interactive' && } +``` + +Tab disabled:`mode.id === 'auto' && !useAutoStore.gateEligible`(wizard 同理)。 + +- [ ] **Step 6: CSS** + +`.auto-panel`、`.wizard-panel`、`.wizard-step-indicator`、`.auto-step-bar`(复用 `.interactive-step-bar` 模式)。 + +- [ ] **Step 7: Commit** + +```bash +git add src/renderer/stores/useAutoStore.ts src/renderer/stores/useWizardStore.ts src/renderer/components/ai/AutoPanel.tsx src/renderer/components/ai/WizardPanel.tsx src/renderer/components/ai/AiPanel.tsx src/renderer/styles/layout.css +git commit -m "feat: add AutoPanel and WizardPanel UI" +``` + +--- + +## Task 9: i18n + +**Files:** +- Modify: `public/locales/zh-CN/translation.json` +- Modify: `public/locales/en/translation.json` + +- [ ] **Step 1: 追加文案键** + +zh-CN 示例: + +```json +"auto": { + "gateBlocked": "请先创建章节正文或添加大纲/设定", + "gateTooltip": "需要至少一章正文或大纲/设定内容", + "step": { "goal": "目标", "plan": "规划", "generate": "生成", "done": "完成" }, + "goalPlaceholder": "本章需达成的剧情点…", + "planScenes": "规划场景", + "startGenerate": "开始生成", + "pause": "暂停", + "resume": "继续自动", + "handoffInteractive": "转入交互微调", + "finishChapter": "结束本章", + "chapterCreated": "自动写作章节已创建" +}, +"wizard": { + "step": { "goal": "目标", "rhythm": "节奏", "pov": "视角", "context": "上下文", "preview": "预览" }, + "rhythm": { "relaxed": "舒缓", "tense": "紧张", "mixed": "混合" }, + "styleNotePlaceholder": "可选:本章风格说明", + "contextHint": "确认与本章相关的上下文(含大纲/设定中的伏笔线索)", + "previewTitle": "生成策略预览", + "startGenerate": "开始生成", + "handoffToast": "已进入交互微调" +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add public/locales/zh-CN/translation.json public/locales/en/translation.json +git commit -m "feat: add auto and wizard i18n strings" +``` + +--- + +## Task 10: E2E 测试 + +**Files:** +- Create: `e2e/auto-writing.spec.ts` +- Create: `e2e/wizard-writing.spec.ts` + +- [ ] **Step 1: E2E-AUTO-03(门槛,无 AI)** + +```typescript +test('E2E-AUTO-03: auto tab disabled on empty book', async () => { + // 同 E2E-INT-02,断言 ai-mode-tab-auto disabled +}) +``` + +- [ ] **Step 2: E2E-AUTO-01(完整流程,300s)** + +```typescript +test('E2E-AUTO-01: auto flow creates chapter', async () => { + test.setTimeout(300_000) + // openBookWithContent → auto tab → fill goal → plan → generate → finish → ProseMirror 非空 +}) +``` + +- [ ] **Step 3: E2E-AUTO-02(暂停转交互,300s)** + +```typescript +test('E2E-AUTO-02: pause and handoff to interactive', async () => { + // generate 开始后 pause → handoff-interactive → interactive-accept-scene 可见 +}) +``` + +- [ ] **Step 4: E2E-WIZ-01(向导 300s)** + +```typescript +test('E2E-WIZ-01: wizard flow reaches interactive panel', async () => { + test.setTimeout(300_000) + // wizard 5 步:goal → rhythm → pov(选第一项)→ context(打开 modal 确认)→ preview → start + // expect interactive-panel + interactive-accept-scene or plot-option-A 可见 +}) +``` + +- [ ] **Step 5: 运行 E2E** + +```bash +npm run test:e2e -- e2e/auto-writing.spec.ts e2e/wizard-writing.spec.ts +``` + +- [ ] **Step 6: Commit** + +```bash +git add e2e/auto-writing.spec.ts e2e/wizard-writing.spec.ts +git commit -m "test: add auto and wizard writing E2E specs" +``` + +--- + +## Task 11: v0.5.0 交付 + +**Files:** +- Modify: `package.json` +- Modify: `README.md` +- Modify: `src/renderer/components/settings/SettingsPage.tsx` +- Modify: `docs/superpowers/plans/2026-07-09-bilin-p41-auto-wizard.md`(勾选完成项) + +- [ ] **Step 1: 版本号 → `0.5.0`** + +- [ ] **Step 2: README 功能概览追加 P4.1** + +```markdown +- 自动写作:章节目标、场景规划、连续生成、暂停转交互(P4.1) +- 向导式写作:5 步引导、策略预览、生成后交互微调(P4.1) +``` + +- [ ] **Step 3: 全量测试** + +```bash +npm run test +npm run build +npm run test:e2e +``` + +Expected: 单元全部 PASS;auto/wizard E2E PASS(LM Studio 运行中) + +- [ ] **Step 4: Commit** + +```bash +git commit -m "chore: release v0.5.0 with auto and wizard writing modes" +``` + +--- + +## Spec 覆盖自检 + +| Spec 章节 | 对应 Task | +|-----------|-----------| +| §3 Schema v5 | Task 1 | +| §4.1 SceneGenerationCore | Task 4 | +| §4.2 AutoWritingService | Task 5 | +| §4.3 WizardWritingService | Task 6 | +| §4.4 成章 + AI 快照 | Task 4 | +| §2.2 Handoff | Task 4 | +| §5 IPC | Task 7 | +| §7 UI + testid | Task 8 | +| §7.4 i18n | Task 9 | +| §9 测试矩阵 | Task 1/5/6/10 | +| 向导第 4 步 ContextEditor | Task 8 WizardPanel | +| P4 快照补齐 | Task 4 | + +--- + +## 依赖顺序 + +``` +Task 1 → Task 2 → Task 3 → Task 4 → Task 5 ∥ Task 6(Task 6 依赖 Task 5 AutoWritingService) + → Task 7 → Task 8 → Task 9 → Task 10 → Task 11 +```