Co-authored-by: Cursor <cursoragent@cursor.com>
34 KiB
笔临 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:
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: FAIL(version 仍为 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 返回 HTML;preview 返回一段中文 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.ts(240s,真实 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.ts:generateNext 写入 scene;pause/resume step 断言。
auto-handoff.test.ts:handoff 后 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.ts(240s):
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:
-
activeAutoAbortController 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 的 comingSoon;handleModeClick 仿 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 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
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: 单元全部 PASS;auto/wizard E2E PASS(LM 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 6(Task 6 依赖 Task 5 AutoWritingService)
→ Task 7 → Task 8 → Task 9 → Task 10 → Task 11