# 笔临 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 ```