Files
bilin/docs/superpowers/plans/2026-07-09-bilin-p41-auto-wizard.md
T
2026-07-06 20:57:31 +08:00

34 KiB
Raw Blame History

笔临 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 (- [x]) syntax for tracking.

Goal: 交付 v0.5.0:实装自动写作(§6.5.2)与向导式写作(§6.5.3),统一写作流引擎扩展 P4;补齐三模式成章 AI 快照。

Architecture: schema v5 扩展 interactive_flowsflow_mode / mode_config_json);SceneGenerationCore 从 P4 抽取共享流式生成;AutoWritingService + WizardWritingService 步骤机;handoff 至 InteractiveWritingServiceIPC auto:* / wizard:*;渲染 AutoPanel / WizardPanel。测试 禁止 MockAI 集成/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:1234model=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

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
npm run test -- tests/main/migrate-v5.test.ts

Expected: FAILversion 仍为 4 或列不存在)

  • Step 3: 创建 schema-v5.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
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 追加类型
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

export interface InteractiveFlow {
  // ...existing fields
  flowMode: WritingFlowMode
  modeConfig: AutoWritingConfig | WizardWritingConfig | Record<string, never>
  step: WritingStep
}

扩展流式事件:

export interface InteractiveStreamChunkEvent {
  flowId: string
  flowMode?: WritingFlowMode
  phase: 'scene' | 'naming' | 'refine' | 'plan'
  delta: string
  done: boolean
}
  • Step 6: ipc-channels.ts 追加
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
npm run test -- tests/main/migrate-v5.test.ts
  • Step 8: Commit
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

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
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 读写
getModeConfig<T extends AutoWritingConfig | WizardWritingConfig>(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
private mapFlow(row: Record<string, unknown>): InteractiveFlow {
  // ...existing namingPending parse
  let modeConfig: AutoWritingConfig | WizardWritingConfig | Record<string, never> = {}
  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 测试
npm run test -- tests/main/migrate-v4.test.ts tests/main/interactive-gate.test.ts

Expected: PASS

  • Step 6: Commit
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

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
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<string, unknown>
    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 风格):

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 返回 HTMLpreview 返回一段中文 HTML <p>…</p>

  • Step 4: 运行 PASS + Commit
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

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, '<p>场景</p>', '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
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
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 抽取:

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<string> {
    // 流式 + tryParseNamingFromBuffer → naming_pause 逻辑,同 P4
    // 返回最终 HTML
  }

  normalizeSceneHtml(raw: string): string {
    // 同 P4 normalizeSceneHtml
  }
}
  • Step 5: 重构 interactive-writing.service.ts

finishChapter 改为调用 ChapterFinishService.finishFromFlow(..., 'interactive', '交互写作成章')

  • Step 6: 运行测试 PASS
npm run test -- tests/main/chapter-finish-snapshot.test.ts tests/main/interactive-finish.test.ts
  • Step 7: Commit
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.ts240s,真实 LM Studio):

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

export class AutoWritingService {
  start(sessionId: string): InteractiveFlow {
    return this.repo.resetInProgress(sessionId, 'auto')
  }

  setGoal(flowId: string, partial: Partial<AutoWritingConfig>): 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<ScenePlanResult> {
    // buildScenePlanMessages → chat → parseScenePlanJson(失败重试一次)
    // 写入 config.scenePlan, currentPlanIndex=0, step=auto_generate
  }

  async generateNext(flowId, onChunk, signal): Promise<InteractiveFlow> {
    // 取 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.tsgenerateNext 写入 scenepause/resume step 断言。
auto-handoff.test.tshandoff 后 flowMode==='interactive'(无 AI)。

  • Step 4: 运行 PASS + Commit
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.ts240s):

it('IT-WIZ-01: buildPreview returns non-empty HTML', async () => {
  // wizard flow 填完 goal/rhythm/pov/context → buildPreview
  expect(preview).toMatch(/<p>/)
}, 240_000)
  • Step 2: 实现 WizardWritingService
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<string> {
    // buildWizardPreviewMessages → chat → 写入 strategyPreview, 返回 HTML
  }

  async startGenerate(flowId, onChunk, signal): Promise<InteractiveFlow> {
    // 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
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 注册

import { registerAutoHandlers } from './handlers/auto.handler'
import { registerWizardHandlers } from './handlers/wizard.handler'
// registerInteractiveHandlers 之后:
registerAutoHandlers(books, settings, aiClient)
registerWizardHandlers(books, settings, aiClient)
  • Step 4: preload 暴露命名空间
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
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

interface AutoState {
  flow: InteractiveFlow | null
  streamBuffer: string
  streaming: boolean
  sceneDraft: string
  gateEligible: boolean
  checkGate: (bookId: string) => Promise<void>
  loadFlow: (bookId: string, sessionId: string) => Promise<void>
  start: (bookId: string, sessionId: string) => Promise<void>
  setGoal: (bookId: string, config: Partial<AutoWritingConfig>) => Promise<void>
  planScenes: (bookId: string) => Promise<void>
  generateNext: (bookId: string) => Promise<void>
  pause: (bookId: string) => Promise<void>
  resume: (bookId: string) => Promise<void>
  handoffInteractive: (bookId: string) => Promise<void>
  finishChapter: (bookId: string, title?: string) => Promise<void>
  mount / unmount  // 复用 onInteractiveStreamChunk,过滤 flowMode==='auto'
}
  • Step 2: useWizardStore
// 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 的 comingSoonhandleModeClick 仿 interactive

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
}

渲染:

{writingMode === 'auto' && <AutoPanel bookId={bookId} sessionId={activeSessionId} disabled={disabled} />}
{writingMode === 'wizard' && <WizardPanel ... />}
{writingMode === 'interactive' && <InteractivePanel ... />}

Tab disabledmode.id === 'auto' && !useAutoStore.gateEligiblewizard 同理)。

  • Step 6: CSS

.auto-panel.wizard-panel.wizard-step-indicator.auto-step-bar(复用 .interactive-step-bar 模式)。

  • Step 7: Commit
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 示例:

"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
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

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
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
test('E2E-AUTO-02: pause and handoff to interactive', async () => {
  // generate 开始后 pause → handoff-interactive → interactive-accept-scene 可见
})
  • Step 4: E2E-WIZ-01(向导 300s
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
npm run test:e2e -- e2e/auto-writing.spec.ts e2e/wizard-writing.spec.ts
  • Step 6: Commit
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

- 自动写作:章节目标、场景规划、连续生成、暂停转交互(P4.1)
- 向导式写作:5 步引导、策略预览、生成后交互微调(P4.1)
  • Step 3: 全量测试
npm run test
npm run build
npm run test:e2e

Expected: 单元全部 PASSauto/wizard E2E PASSLM Studio 运行中)

  • Step 4: Commit
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 6Task 6 依赖 Task 5 AutoWritingService
         → Task 7 → Task 8 → Task 9 → Task 10 → Task 11