5cfc7c10b9
Define state machine, schema v4, IPC, prompts, UI, and LM Studio test timeouts for interactive writing mode only. Co-authored-by: Cursor <cursoragent@cursor.com>
410 lines
16 KiB
Markdown
410 lines
16 KiB
Markdown
# 笔临 P4 交互式写作设计规格
|
||
|
||
**版本**:1.0
|
||
**日期**:2026-07-08
|
||
**范围**:P4 严格(选项 A)— 仅实装「交互式写作」全流程(§6.5.1)
|
||
**前置**:P2.1 + P3 已交付(v0.3.0,`8ce35a1`)
|
||
**参考**:`design/readme.md` v1.1 §6.5.1、`design/ui_pc.html` AI 面板交互写作原型
|
||
|
||
**目标版本**:v0.4.0
|
||
|
||
---
|
||
|
||
## 1. 背景与目标
|
||
|
||
### 1.1 阶段定位
|
||
|
||
v0.3.0 已交付 AI 对话模式;「交互 / 自动 / 向导」写作 Tab 仍为占位。本阶段实装 **交互式写作**:以场景为单位的剧情走向建议 → 场景生成 → 命名暂停 → 微调确认 → 循环 → 成章落盘,全流程状态持久化并可恢复。
|
||
|
||
### 1.2 已确认决策(Brainstorming 2026-07-08)
|
||
|
||
| 维度 | 决策 |
|
||
|------|------|
|
||
| 范围 | **P4 严格**:仅 §6.5.1 交互式写作;自动 / 向导仍占位 |
|
||
| 实现方案 | **方案 1**:专用状态机 + 独立 DB 表 + 逐步 IPC |
|
||
| 成章落盘 | 当前活跃卷 **新建章节**,`origin=interactive`,合并全部场景后自动打开编辑器 |
|
||
| 命名暂停 | **完整实装**:生成中暂停,5 选项 + 自定义输入,选定后续写 |
|
||
| 状态持久化 | **schema v4**,`interactive_flows` / `interactive_scenes`,重启可恢复 |
|
||
| 启动门槛 | **硬拦截**:无章节正文且无大纲/设定时 Tab 禁用 |
|
||
| AI 后端 | **LM Studio**(延续 P3),`http://127.0.0.1:1234`,`gemma-4-e4b-it` |
|
||
| 测试原则 | **禁止 Mock**;集成 / E2E 打真实 LM Studio |
|
||
| 测试超时 | 本地模型慢,Vitest AI 用例 **180–240s**,Playwright 全局 **180s**,交互 E2E **300s** |
|
||
| 工程 | `test-results/` 加入 `.gitignore` |
|
||
|
||
### 1.3 验收标准
|
||
|
||
用户能完成:
|
||
|
||
1. 在有足够上下文的书籍中进入「交互写作」Tab(门槛不满足时 Tab 禁用并提示)
|
||
2. 确认 / 编辑 AI 上下文后,收到 3 条剧情走向建议(每条 ≥50 字说明)
|
||
3. 选择走向(可附补充指令)后,流式生成一个场景(600–1500 字)
|
||
4. 生成中若 AI 要求命名,流程暂停并展示 5 个命名选项;选定后自动续写完成场景
|
||
5. 对场景输入微调指令,AI 针对当前场景修订(不重写全章)
|
||
6. 满意后进入下一场景循环,或点击「结束本章」
|
||
7. 结束时在当前活跃卷创建新章节,内容为全部场景合并 HTML,标记为交互初稿,编辑器自动打开
|
||
8. 关闭应用并重启后,未完成的交互流程恢复到上次步骤
|
||
9. 切换回「对话」模式时,同会话聊天历史仍可见;再次进入交互模式时流程按规格重新初始化
|
||
|
||
### 1.4 本规格不实现(明确排除)
|
||
|
||
| 项 | 归属 |
|
||
|----|------|
|
||
| §6.5.2 自动写作 | P4.1+ |
|
||
| §6.5.3 向导式写作 | P4.1+ |
|
||
| §6.3 文风学习与风格滑杆 | P4.1+ |
|
||
| §6.6 大纲生成章节骨架 | P4.1+ |
|
||
| §6.7 知识抽取与审核工作流 | P4.1+ |
|
||
| §6.4 Skill / MCP | P4+ |
|
||
| 命名结果自动写入设定库 | 本阶段仅写入正文 |
|
||
| 导入导出、驾驶舱等 | P5+ |
|
||
|
||
---
|
||
|
||
## 2. 架构
|
||
|
||
### 2.1 进程模型
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ 主进程 │
|
||
│ · schema v4:interactive_flows / interactive_scenes │
|
||
│ · chapters.origin 列(manual | interactive) │
|
||
│ · InteractiveWritingService(步骤机 + 持久化) │
|
||
│ · InteractivePromptBuilder(各步 prompt / JSON 约束) │
|
||
│ · AiClientService(复用:JSON 步非流式,场景步流式 SSE) │
|
||
│ · AiContextBuilder(复用) │
|
||
│ · IPC:interactive:* + interactive:streamChunk 事件 │
|
||
└────────────────────────────┬─────────────────────────────────┘
|
||
│ contextBridge
|
||
┌────────────────────────────▼─────────────────────────────────┐
|
||
│ 渲染进程 │
|
||
│ · AiPanel:writingMode=interactive 时挂载 InteractivePanel │
|
||
│ · InteractivePanel:步骤 UI(走向卡片、命名暂停、场景预览) │
|
||
│ · useInteractiveStore:IPC + 流式缓冲 │
|
||
│ · ContextEditorModal(复用,上下文确认步) │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**约束**(延续 P0–P3):
|
||
|
||
- 渲染进程禁止直接 `fetch` AI API
|
||
- 流式通过 `webContents.send` 推送,不阻塞 IPC invoke
|
||
- 交互流程绑定 `ai_session_id`;`ai_messages` 仍记录关键轮次作审计
|
||
|
||
### 2.2 步骤机
|
||
|
||
| Step | 名称 | AI | 持久化字段 |
|
||
|------|------|-----|-----------|
|
||
| `context_confirm` | 上下文确认 | 无 | `context_json` |
|
||
| `plot_suggest` | 剧情走向 | JSON ×3 | — |
|
||
| `plot_select` | 用户选择 | 无 | `plot_selection_json` |
|
||
| `scene_generate` | 场景生成 | 流式 | 可能写入 `naming_pending_json` |
|
||
| `naming_pause` | 命名暂停 | 流式续写 | `naming_pending_json` 清空后 |
|
||
| `scene_review` | 场景确认/微调 | 流式(可选) | — |
|
||
| `loop` | 下一场景 | 回到 `plot_suggest` | `interactive_scenes` 追加 |
|
||
| `completed` | 成章完成 | 无 | `status=completed` |
|
||
|
||
切换到交互模式:调用 `interactive:start`,创建或重置 `flow`(`status=in_progress`)。切换离开交互模式不删除 flow;再次 `start` 时重新初始化。
|
||
|
||
### 2.3 Schema v4(每本书 `.sqlite`)
|
||
|
||
```sql
|
||
-- schema-v4.sql
|
||
ALTER TABLE chapters ADD COLUMN origin TEXT NOT NULL DEFAULT 'manual';
|
||
|
||
CREATE TABLE IF NOT EXISTS interactive_flows (
|
||
id TEXT PRIMARY KEY,
|
||
session_id TEXT NOT NULL,
|
||
step TEXT NOT NULL,
|
||
status TEXT NOT NULL DEFAULT 'in_progress',
|
||
context_json TEXT NOT NULL DEFAULT '{}',
|
||
plot_selection_json TEXT,
|
||
naming_pending_json TEXT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
FOREIGN KEY (session_id) REFERENCES ai_sessions(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE TABLE IF NOT EXISTS interactive_scenes (
|
||
id TEXT PRIMARY KEY,
|
||
flow_id TEXT NOT NULL,
|
||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||
content_html TEXT NOT NULL DEFAULT '',
|
||
plot_label TEXT NOT NULL DEFAULT '',
|
||
refined_count INTEGER NOT NULL DEFAULT 0,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
FOREIGN KEY (flow_id) REFERENCES interactive_flows(id) ON DELETE CASCADE
|
||
);
|
||
|
||
CREATE INDEX IF NOT EXISTS idx_interactive_flows_session ON interactive_flows(session_id);
|
||
CREATE INDEX IF NOT EXISTS idx_interactive_scenes_flow ON interactive_scenes(flow_id, sort_order);
|
||
```
|
||
|
||
`migrate.ts`:`version 3 → 4` 应用上述 DDL。
|
||
|
||
---
|
||
|
||
## 3. 共享类型
|
||
|
||
```typescript
|
||
export type InteractiveStep =
|
||
| 'context_confirm'
|
||
| 'plot_suggest'
|
||
| 'plot_select'
|
||
| 'scene_generate'
|
||
| 'naming_pause'
|
||
| 'scene_review'
|
||
| 'completed'
|
||
|
||
export type InteractiveFlowStatus = 'in_progress' | 'completed' | 'abandoned'
|
||
export type ChapterOrigin = 'manual' | 'interactive'
|
||
|
||
export interface PlotOption {
|
||
id: 'A' | 'B' | 'C'
|
||
label: string
|
||
summary: string
|
||
}
|
||
|
||
export interface NamingPause {
|
||
elementType: string
|
||
context: string
|
||
options: string[] // 长度 5
|
||
}
|
||
|
||
export interface PlotSelection {
|
||
optionIds: Array<'A' | 'B' | 'C'>
|
||
userNote?: string
|
||
}
|
||
|
||
export interface InteractiveScene {
|
||
id: string
|
||
sortOrder: number
|
||
contentHtml: string
|
||
plotLabel: string
|
||
refinedCount: number
|
||
}
|
||
|
||
export interface InteractiveFlow {
|
||
id: string
|
||
sessionId: string
|
||
step: InteractiveStep
|
||
status: InteractiveFlowStatus
|
||
scenes: InteractiveScene[]
|
||
namingPending?: NamingPause | null
|
||
contextSummary?: string
|
||
}
|
||
|
||
export interface InteractiveGateResult {
|
||
eligible: boolean
|
||
reason?: 'no_chapter' | 'no_outline_or_setting'
|
||
}
|
||
|
||
export interface InteractiveStreamChunkEvent {
|
||
flowId: string
|
||
phase: 'scene' | 'refine' | 'naming_resume'
|
||
delta: string
|
||
done: boolean
|
||
}
|
||
```
|
||
|
||
`Chapter` 接口增加可选 `origin?: ChapterOrigin`(读取时填充,默认 `manual`)。
|
||
|
||
---
|
||
|
||
## 4. IPC 接口
|
||
|
||
### 4.1 通道列表
|
||
|
||
| 通道 | 说明 |
|
||
|------|------|
|
||
| `interactive:checkGate` | 门槛检查 |
|
||
| `interactive:getFlow` | 按 session 恢复 flow |
|
||
| `interactive:start` | 初始化 / 重置流程 |
|
||
| `interactive:confirmContext` | 保存上下文 binding,进入 plot_suggest |
|
||
| `interactive:suggestPlots` | AI 返回 3 条走向 |
|
||
| `interactive:selectPlot` | 记录用户选择 |
|
||
| `interactive:generateScene` | 开始场景流式生成 |
|
||
| `interactive:resolveNaming` | 命名选择后续写 |
|
||
| `interactive:refineScene` | 场景微调(流式) |
|
||
| `interactive:acceptScene` | 确认场景,写入 scenes 表 |
|
||
| `interactive:finishChapter` | 合并场景 → 新建章节 |
|
||
| `interactive:abort` | 中止当前 AI 请求 |
|
||
|
||
事件:`interactive:streamChunk` → `InteractiveStreamChunkEvent`
|
||
|
||
### 4.2 门槛逻辑(`checkGate`)
|
||
|
||
`eligible = true` 当满足任一:
|
||
|
||
- 存在至少 1 章,`stripHtml(content).trim().length > 0`
|
||
- `outline_items` 表非空
|
||
- `settings` 表非空
|
||
|
||
否则 `eligible = false`,`reason` 区分无章节正文 vs 无大纲设定。
|
||
|
||
### 4.3 成章逻辑(`finishChapter`)
|
||
|
||
1. 校验 `activeVolumeId` 存在(来自调用方或书籍默认第一卷)
|
||
2. 按 `sort_order` 合并 `interactive_scenes.content_html`,段落间插入空行
|
||
3. `chapter.create(volumeId, title ?? '交互初稿')`,`origin='interactive'`,`status='draft'`
|
||
4. `flow.status = 'completed'`,`step = 'completed'`
|
||
5. 创建 `type='ai'` 快照(标签「交互写作成章」)
|
||
6. 返回 `{ chapterId }`
|
||
|
||
---
|
||
|
||
## 5. Prompt 策略
|
||
|
||
`InteractivePromptBuilder` 为各步构建 `ChatMessage[]`:
|
||
|
||
### 5.1 走向建议(`suggestPlots`)
|
||
|
||
- System:你是小说创作助手;基于上下文给出 3 个互斥剧情走向;仅返回 JSON
|
||
- 格式:`{ "plots": [{ "id": "A", "label": "...", "summary": "..." }] }`
|
||
- 约束:恰好 3 条;`summary` ≥50 汉字
|
||
- 非流式;解析失败重试 1 次(追加「仅返回 JSON,不要 markdown」)
|
||
|
||
### 5.2 场景生成(`generateScene`)
|
||
|
||
- 注入:上下文块 + 已确认场景摘要 + 选定走向 + 用户补充
|
||
- 若需新命名:先返回单行 JSON
|
||
`{"type":"naming_required","elementType":"角色","context":"...","options":["名1",...,"名5"]}`
|
||
- 否则流式输出场景 HTML(`<p>` 段落,600–1500 字)
|
||
- 交互步 `max_tokens` 覆盖为 **4096**(不改全局 `aiConfig.maxTokens`)
|
||
|
||
### 5.3 命名续写(`resolveNaming`)
|
||
|
||
- User:用户选定名称 `{chosen}`,请继续完成当前场景
|
||
- 流式输出剩余正文
|
||
|
||
### 5.4 场景微调(`refineScene`)
|
||
|
||
- User:在保持情节连贯前提下,按指令修改当前场景:`{instruction}`
|
||
- 流式输出完整修订后场景(覆盖当前草稿)
|
||
|
||
---
|
||
|
||
## 6. 错误处理
|
||
|
||
| 场景 | 行为 |
|
||
|------|------|
|
||
| AI 超时 | `IpcError` recoverable;toast「AI 响应超时」;留在当前步,可重试 |
|
||
| JSON 解析失败 | 自动重试 1 次;仍失败则 toast + 留在 `plot_suggest` |
|
||
| 流式中断 | 保留已收 delta;提供「重新生成」 |
|
||
| 无活跃卷 | `finishChapter` 返回错误码,提示创建卷 |
|
||
| 切换书籍 | DB 状态保留;`getFlow` 恢复 |
|
||
| 云端后端 + 离线 | 与 P3 一致禁用输入;LM Studio 不受影响 |
|
||
| `interactive:abort` | 中止 fetch;不清除 flow 状态 |
|
||
|
||
---
|
||
|
||
## 7. UI 规格
|
||
|
||
### 7.1 入口
|
||
|
||
- `AiPanel` 模式 Tab:`interactive` 可点击;`auto` / `wizard` 仍 `feature.comingSoon`
|
||
- 挂载时 `checkGate`;`!eligible` → Tab `disabled` + `title` 提示
|
||
- `writingMode === 'interactive'` 时主区渲染 `InteractivePanel`
|
||
|
||
### 7.2 InteractivePanel 布局
|
||
|
||
1. **步骤指示器** — 上下文 → 走向 → 场景 → 确认
|
||
2. **主内容**(按 `flow.step`)
|
||
- `context_confirm`:摘要 +「编辑上下文」+「开始交互」
|
||
- `plot_suggest` / `plot_select`:`PlotOptionCard` ×3,多选 + 补充输入
|
||
- `scene_generate` / `naming_pause`:流式预览;命名时覆盖为选项列表 + 自定义
|
||
- `scene_review`:全文预览 + 微调输入 +「下一场景」/「结束本章」
|
||
3. **底栏** — 主操作按钮;与 `ai-offline-banner` 联动
|
||
|
||
样式参考 `design/ui_pc.html`:`.ai-mode-tab`、`.plot-opt`、`.opt-label`。
|
||
|
||
### 7.3 成章后导航
|
||
|
||
`finishChapter` 成功 → `openBook` → `setSelectedChapter(chapterId)` → `setView('editor')` → toast `interactive.chapterCreated`。
|
||
|
||
---
|
||
|
||
## 8. 工程配套
|
||
|
||
### 8.1 `.gitignore`
|
||
|
||
```
|
||
test-results/
|
||
```
|
||
|
||
### 8.2 测试超时配置
|
||
|
||
| 位置 | 值 |
|
||
|------|-----|
|
||
| `playwright.config.ts` `timeout` | `180_000` |
|
||
| E2E 交互用例 `test.setTimeout` | `300_000` |
|
||
| E2E 助手回复 `expect` | `120_000` |
|
||
| Vitest `IT-INT-01/02/03` | `180_000` / `240_000` / `240_000` |
|
||
| 现有 `IT-AI-01` | 保持 `90_000` 或升至 `180_000` |
|
||
| 现有 `IT-NAME-01` | 保持 `180_000` |
|
||
|
||
### 8.3 LM Studio 前置
|
||
|
||
与 P3 相同:`BILIN_AI_BASE_URL` / `BILIN_AI_MODEL` 环境变量由主进程 `global-settings` 读取;渲染进程不使用 `process.env`。
|
||
|
||
---
|
||
|
||
## 9. 测试要求
|
||
|
||
### 9.1 单元 / 集成(Vitest,禁止 Mock)
|
||
|
||
| 用例 ID | 场景 | 超时 |
|
||
|---------|------|------|
|
||
| IT-INT-01 | `suggestPlots` 返回 3 条合法 JSON | 180s |
|
||
| IT-INT-02 | `generateScene` 流式正文非空 | 240s |
|
||
| IT-INT-03 | naming JSON → `resolveNaming` 后续有内容 | 240s |
|
||
| IT-INT-04 | `acceptScene` ×2 + `finishChapter` 创建 `origin=interactive` 章节 | 60s |
|
||
| IT-INT-05 | `checkGate` 门槛 true/false 分支 | 5s |
|
||
| IT-MIG-04 | schema v3→v4 迁移 | 5s |
|
||
|
||
### 9.2 E2E(Playwright + Electron)
|
||
|
||
| 用例 ID | 场景 | 超时 |
|
||
|---------|------|------|
|
||
| E2E-INT-01 | 选走向 → 生成 → 接受 → 结束本章 → 编辑器有内容 | 300s |
|
||
| E2E-INT-02 | 空书交互 Tab 禁用 | 30s |
|
||
| E2E-INT-03 | 重启后恢复 flow 步骤 | 300s |
|
||
|
||
文件:`e2e/interactive-writing.spec.ts`
|
||
|
||
---
|
||
|
||
## 10. 文件清单(实现参考)
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `src/main/db/schema-v4.sql` | v4 DDL |
|
||
| `src/main/db/migrate.ts` | v3→v4 |
|
||
| `src/main/db/repositories/interactive.repo.ts` | flow / scene CRUD |
|
||
| `src/main/services/interactive-writing.service.ts` | 步骤机 |
|
||
| `src/main/services/interactive-prompt-builder.service.ts` | Prompt |
|
||
| `src/main/ipc/handlers/interactive.handler.ts` | IPC |
|
||
| `src/shared/ipc-channels.ts` | 新通道 |
|
||
| `src/shared/types.ts` | 交互类型 + `ChapterOrigin` |
|
||
| `src/preload/index.ts` | API + 事件 |
|
||
| `src/renderer/components/ai/InteractivePanel.tsx` | 主 UI |
|
||
| `src/renderer/components/ai/PlotOptionCard.tsx` | 走向卡片 |
|
||
| `src/renderer/stores/useInteractiveStore.ts` | 状态 |
|
||
| `e2e/interactive-writing.spec.ts` | E2E |
|
||
| `tests/main/interactive-*.test.ts` | 集成测试 |
|
||
|
||
---
|
||
|
||
## 11. 规格自检
|
||
|
||
| 检查项 | 结果 |
|
||
|--------|------|
|
||
| Placeholder 扫描 | 自动/向导明确排除至 P4.1+;无 TBD |
|
||
| 内部一致性 | 步骤机、IPC、DB、UI 步骤名称一致 |
|
||
| 范围 | 单阶段可交付 v0.4.0 |
|
||
| 歧义 | 成章=新建章节+interactive origin;门槛=硬拦截 |
|
||
|
||
---
|
||
|
||
*本规格为笔临 P4 子项目。自动写作、知识抽取等各自在后续 spec 中增量修订。*
|