Files
bilin/docs/superpowers/plans/2026-07-09-bilin-p41-auto-wizard.md
T

1080 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 笔临 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: FAILversion 仍为 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<string, never>
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<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`**
```typescript
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 测试**
```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<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` 风格):
```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 返回 HTMLpreview 返回一段中文 HTML `<p>…</p>`
- [ ] **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, '<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`**
```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<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**
```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<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.ts`generateNext 写入 scenepause/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(/<p>/)
}, 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<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**
```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<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`**
```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' && <AutoPanel bookId={bookId} sessionId={activeSessionId} disabled={disabled} />}
{writingMode === 'wizard' && <WizardPanel ... />}
{writingMode === 'interactive' && <InteractivePanel ... />}
```
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: 单元全部 PASSauto/wizard E2E PASSLM 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 6Task 6 依赖 Task 5 AutoWritingService
→ Task 7 → Task 8 → Task 9 → Task 10 → Task 11
```