6a949b54ba
Co-authored-by: Cursor <cursoragent@cursor.com>
1080 lines
34 KiB
Markdown
1080 lines
34 KiB
Markdown
# 笔临 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_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`
|
||
|
||
- [x] **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)
|
||
})
|
||
})
|
||
```
|
||
|
||
- [x] **Step 2: 运行确认 FAIL**
|
||
|
||
```bash
|
||
npm run test -- tests/main/migrate-v5.test.ts
|
||
```
|
||
|
||
Expected: FAIL(version 仍为 4 或列不存在)
|
||
|
||
- [x] **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 '{}';
|
||
```
|
||
|
||
- [x] **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')
|
||
}
|
||
```
|
||
|
||
- [x] **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
|
||
}
|
||
```
|
||
|
||
- [x] **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',
|
||
```
|
||
|
||
- [x] **Step 7: 运行测试 PASS**
|
||
|
||
```bash
|
||
npm run test -- tests/main/migrate-v5.test.ts
|
||
```
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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)
|
||
}
|
||
```
|
||
|
||
- [x] **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。
|
||
|
||
- [x] **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)
|
||
}
|
||
```
|
||
|
||
- [x] **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
|
||
}
|
||
}
|
||
```
|
||
|
||
- [x] **Step 5: 运行现有 migrate + gate 测试**
|
||
|
||
```bash
|
||
npm run test -- tests/main/migrate-v4.test.ts tests/main/interactive-gate.test.ts
|
||
```
|
||
|
||
Expected: PASS
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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()
|
||
})
|
||
})
|
||
```
|
||
|
||
- [x] **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)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [x] **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 `<p>…</p>`。
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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')
|
||
})
|
||
})
|
||
```
|
||
|
||
- [x] **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 }
|
||
}
|
||
}
|
||
```
|
||
|
||
- [x] **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' })
|
||
}
|
||
```
|
||
|
||
- [x] **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
|
||
}
|
||
}
|
||
```
|
||
|
||
- [x] **Step 5: 重构 `interactive-writing.service.ts`**
|
||
|
||
`finishChapter` 改为调用 `ChapterFinishService.finishFromFlow(..., 'interactive', '交互写作成章')`。
|
||
|
||
- [x] **Step 6: 运行测试 PASS**
|
||
|
||
```bash
|
||
npm run test -- tests/main/chapter-finish-snapshot.test.ts tests/main/interactive-finish.test.ts
|
||
```
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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)
|
||
```
|
||
|
||
- [x] **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', '自动写作成章')
|
||
}
|
||
}
|
||
```
|
||
|
||
- [x] **Step 3: IT-AUTO-02/03/04 测试**
|
||
|
||
`auto-generate.test.ts`:generateNext 写入 scene;pause/resume step 断言。
|
||
`auto-handoff.test.ts`:handoff 后 `flowMode==='interactive'`(无 AI)。
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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)
|
||
```
|
||
|
||
- [x] **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
|
||
}
|
||
}
|
||
```
|
||
|
||
- [x] **Step 3: IT-WIZ-02 集成测试**(300s)
|
||
|
||
生成完成后 `flow.flowMode === 'interactive'` 且 `scenes.length >= 1`。
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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
|
||
|
||
- [x] **Step 2: 实现 `registerWizardHandlers`**
|
||
|
||
- `WIZARD_START_GENERATE`:流式 + 完成后 flow 已 handoff
|
||
- 其余为同步 invoke
|
||
|
||
- [x] **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)
|
||
```
|
||
|
||
- [x] **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 */ }
|
||
```
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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'
|
||
}
|
||
```
|
||
|
||
- [x] **Step 2: `useWizardStore`**
|
||
|
||
```typescript
|
||
// wizardGoal / setRhythm / setPov / confirmContext / buildPreview / startGenerate
|
||
// startGenerate 完成后:useAiStore.setWritingMode('interactive') + showToast
|
||
```
|
||
|
||
- [x] **Step 3: `AutoPanel.tsx`**
|
||
|
||
按 spec §7.2 实现全部 `data-testid`;步骤条;`goal_setup` / `scene_plan` / `auto_generate` / `paused` 分支 UI。
|
||
|
||
- [x] **Step 4: `WizardPanel.tsx`**
|
||
|
||
5 步 wizard UI;步骤 4 按钮 `onClick={() => setContextOpen(true)}` 打开 `ContextEditorModal`;步骤 5 展示 `strategyPreview` HTML。
|
||
|
||
- [x] **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 同理)。
|
||
|
||
- [x] **Step 6: CSS**
|
||
|
||
`.auto-panel`、`.wizard-panel`、`.wizard-step-indicator`、`.auto-step-bar`(复用 `.interactive-step-bar` 模式)。
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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": "已进入交互微调"
|
||
}
|
||
```
|
||
|
||
- [x] **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`
|
||
|
||
- [x] **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
|
||
})
|
||
```
|
||
|
||
- [x] **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 非空
|
||
})
|
||
```
|
||
|
||
- [x] **Step 3: E2E-AUTO-02(暂停转交互,300s)**
|
||
|
||
```typescript
|
||
test('E2E-AUTO-02: pause and handoff to interactive', async () => {
|
||
// generate 开始后 pause → handoff-interactive → interactive-accept-scene 可见
|
||
})
|
||
```
|
||
|
||
- [x] **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 可见
|
||
})
|
||
```
|
||
|
||
- [x] **Step 5: 运行 E2E**
|
||
|
||
```bash
|
||
npm run test:e2e -- e2e/auto-writing.spec.ts e2e/wizard-writing.spec.ts
|
||
```
|
||
|
||
- [x] **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`(勾选完成项)
|
||
|
||
- [x] **Step 1: 版本号 → `0.5.0`**
|
||
|
||
- [x] **Step 2: README 功能概览追加 P4.1**
|
||
|
||
```markdown
|
||
- 自动写作:章节目标、场景规划、连续生成、暂停转交互(P4.1)
|
||
- 向导式写作:5 步引导、策略预览、生成后交互微调(P4.1)
|
||
```
|
||
|
||
- [x] **Step 3: 全量测试**
|
||
|
||
```bash
|
||
npm run test
|
||
npm run build
|
||
npm run test:e2e
|
||
```
|
||
|
||
Expected: 单元全部 PASS;auto/wizard E2E PASS(LM Studio 运行中)
|
||
|
||
- [x] **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
|
||
```
|