Files
bilin/docs/superpowers/specs/2026-07-07-bilin-p21-p3-design.md
bing fd04ca7ea2 docs: add P2.1 and P3 design spec for v0.3.0
Covers P2 backlog (chapter drag, search replace, inspiration voice, mention jump, reference pin) and strict P3-A (LM Studio chat, context injection, offline fallback, naming check) with no-mock testing requirements.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 14:26:56 +08:00

546 lines
19 KiB
Markdown
Raw Permalink 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.
# 笔临 P2.1 + P3 设计规格
**版本**1.0
**日期**2026-07-07
**范围**:P2 遗留补完 + P3 AI 基础(严格 P3-A:对话模式实装,写作模式 Tab 占位)
**前置**P2 已交付(v0.2.0`aac51bf`
**参考**`design/readme.md` v1.1 §5.3.8 / §5.6 / §6.1–§6.2 / §6.8–§6.9、`design/ui_pc.html`
**目标版本**v0.3.0
---
## 1. 背景与目标
### 1.1 阶段定位
P2.1 补齐 P2 规格中明确延期的交互能力(章节拖拽、搜索替换、灵感语音、`@` 跳转、引用钉住)。P3 在 v0.2.0 信息管理能力之上,交付 **可对话的本地 AI 助手**:会话持久化、上下文注入、快捷命令、离线降级、命名一致性检查,以及 `ai` 类型快照。
### 1.2 已确认决策(Brainstorming 2026-07-07
| 维度 | 决策 |
|------|------|
| 范围 | **P2.1 全量** + **P3 严格(选项 A** |
| P3 写作模式 | 「对话」实装;「交互 / 自动 / 向导」Tab **仅 UI 占位** → P4 |
| AI 后端 | **LM Studio**OpenAI 兼容 API),开发/测试/E2E 统一配置 |
| 服务地址 | `http://127.0.0.1:1234` |
| 模型 | `gemma-4-e4b-it` |
| 测试原则 | **禁止 Mock**;不得以「本地服务未启动」为由 skip 测试 |
| 架构 | **主进程直连**`AiClientService` + IPC 流式推送) |
| 平台 | Windows 优先(延续 P0P2 |
### 1.3 验收标准
用户能完成:
1. 左栏章节同卷/跨卷拖拽排序,重启后顺序保持
2. 全局搜索:输入关键词 → 预览替换影响范围 → 确认后批量替换章节内容
3. `Ctrl+Shift+I` 打开灵感捕获模态,文字/语音输入后保存到灵感列表
4. 点击正文中 `@设定名` 跳转到对应设定编辑
5. 引用面板钉住后保持可见,可与 AI 右栏互斥规则共存
6. 设置页配置 LM Studio 地址与模型;右栏 AI 面板新建会话、发送消息、流式收到回复
7. 勾选章节/大纲/设定/灵感作为上下文,点击「更新上下文」后下一轮对话携带最新摘要
8. 使用 `/续写` 等快捷命令触发预设 prompt
9. 云端后端配置下断网时 AI 输入禁用并显示离线横幅;LM Studio 配置下断网仍可用
10. 知识库 Tab 运行命名一致性检查,逐条确认后可批量替换
11. AI 内容写入章节前自动创建 `type='ai'` 快照
### 1.4 本规格不实现(明确排除)
| 项 | 归属 |
|----|------|
| §6.5 交互 / 自动 / 向导写作全流程 | P4 |
| §6.3 文风学习与风格滑杆 | P4 |
| §6.4 Skill / MCP 扩展 | P4+ |
| §6.6 大纲生成章节骨架 | P4 |
| §6.7 知识自动抽取与审核工作流(列表 UI) | P4 |
| `knowledge_entries` / `timeline_events` 表 | P4/P5/P6 |
| 导入导出(`#importModal` | P7 |
| 驾驶舱、章间衔接、更新计划 | P5 |
---
## 2. 架构
### 2.1 进程模型(延续 P2
```
┌─────────────────────────────────────────────────────────┐
│ 主进程 │
│ · node:sqlite(每书 .sqliteschema v3
│ · AiClientService → fetch LM Studio /v1/chat/completions│
│ · AiSessionRepository / AiContextBuilder │
│ · NetworkMonitor → 推送 online/offline │
│ · 既有 FTS / Search / Snapshot / Chapter 服务 │
│ · IPC HandlersIpcResult 信封 + ai:stream-chunk 事件) │
└───────────────────────────┬─────────────────────────────┘
│ contextBridge
┌───────────────────────────▼─────────────────────────────┐
│ 渲染进程 │
│ · AiPanel(替换 RightPanel AI 占位) │
│ · ContextEditorModal / NamingCheckModal │
│ · InspirationModal(全局快捷键) │
│ · useAiStore(会话、流式缓冲、disabled
│ · SearchModal 替换区 / 章节 drag-and-drop │
└─────────────────────────────────────────────────────────┘
```
**约束**(延续 P0P2):
- 禁止渲染进程直接 `fetch` AI API;所有推理请求经主进程
- API Key 仅存 `global_settings.json`(主进程读写)
- 流式输出通过 `webContents.send(IPC.AI_STREAM_CHUNK, payload)` 推送,不阻塞 IPC invoke
### 2.2 AI 客户端(推荐方案)
主进程 `AiClientService`
- 使用 Node.js 内置 `fetch` 调用 OpenAI Chat Completions 兼容端点
- 支持 `stream: true`;解析 SSE `data: {...}`
- `AbortController` 支持 `ai:abort`
- 超时默认 60s`GlobalSettings.aiConfig.timeoutMs`
- 错误映射为 `IpcError`,含 `recoverable: true`(超时/连接拒绝)
**默认配置**`global_settings.json`):
```typescript
interface AiConfig {
backend: 'lmstudio' | 'openai' | 'anthropic'
baseUrl: string // 默认 'http://127.0.0.1:1234'
model: string // 默认 'gemma-4-e4b-it'
apiKey?: string // openai/anthropic 时使用;lmstudio 可空
timeoutMs: number // 默认 60_000
maxTokens: number // 默认 2048
}
```
**环境变量覆盖**(测试/CI,可选):
| 变量 | 作用 |
|------|------|
| `BILIN_AI_BASE_URL` | 覆盖 `baseUrl` |
| `BILIN_AI_MODEL` | 覆盖 `model` |
未设置时使用上述默认值;**测试不得 skip**。
### 2.3 里程碑
| 阶段 | 交付 | 周期 |
|------|------|------|
| **P2.1a** | 章节拖拽 + `chapter:move` IPC | ~2 天 |
| **P2.1b** | 搜索替换 UI + E2E | ~1 天 |
| **P2.1c** | 灵感模态 + 语音 + `@` 跳转 + 引用钉住 | ~2 天 |
| **P3.0** | Schema v3 + AiClientService + AI 设置页 | ~2 天 |
| **P3.1** | 会话 CRUD + 消息持久化 + 流式 IPC | ~3 天 |
| **P3.2** | 上下文编辑器 + 快捷命令 | ~2 天 |
| **P3.3** | 离线降级 + 命名检查 + AI 快照 | ~2 天 |
| **P3.4** | 全量测试 + v0.3.0 | ~1 天 |
---
## 3. 数据模型
### 3.1 Schema v3 迁移
`schema_version` 2 → 3。增量 `schema-v3.sql` + `migrate.ts` 扩展。
```sql
CREATE TABLE ai_sessions (
id TEXT PRIMARY KEY,
title TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '',
archived INTEGER NOT NULL DEFAULT 0,
context_json TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE ai_messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')),
content TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES ai_sessions(id) ON DELETE CASCADE
);
CREATE INDEX idx_ai_messages_session ON ai_messages(session_id, created_at);
```
**说明**`ai_sessions` 存于**每本书**的 `.sqlite`(与章节等同库),非全局。
**`context_json` 结构**
```typescript
interface AiContextBinding {
chapterIds: string[]
outlineIds: string[]
settingIds: string[]
inspirationIds: string[]
}
```
**全局 userData**`global_settings.json` 扩展,非书籍 DB):
```typescript
interface GlobalSettings {
// ...existing P2 fields
aiConfig: AiConfig
namingIgnoreWords: string[]
}
```
### 3.2 共享类型扩展(`src/shared/types.ts`
```typescript
export type AiBackend = 'lmstudio' | 'openai' | 'anthropic'
export type AiMessageRole = 'user' | 'assistant' | 'system'
export type AiWritingMode = 'chat' | 'interactive' | 'auto' | 'wizard' // UI only; P3 仅 chat 实装
export interface AiSession {
id: string
title: string
model: string
archived: boolean
context: AiContextBinding
createdAt: string
updatedAt: string
}
export interface AiMessage {
id: string
sessionId: string
role: AiMessageRole
content: string
createdAt: string
}
export interface NamingConflict {
termA: string
termB: string
confidence: number
reason: string
suggestedCanonical?: string
}
```
---
## 4. IPC 通道
### 4.1 新增常量(`ipc-channels.ts`
```typescript
// Chapter
CHAPTER_MOVE: 'chapter:move',
// AI
AI_SESSION_LIST: 'ai:sessionList',
AI_SESSION_CREATE: 'ai:sessionCreate',
AI_SESSION_UPDATE: 'ai:sessionUpdate',
AI_SESSION_DELETE: 'ai:sessionDelete',
AI_MESSAGE_LIST: 'ai:messageList',
AI_CHAT: 'ai:chat',
AI_ABORT: 'ai:abort',
AI_BUILD_CONTEXT: 'ai:buildContext',
AI_NAMING_CHECK: 'ai:namingCheck',
AI_STREAM_CHUNK: 'ai:streamChunk', // main → renderer event
AI_NETWORK_STATUS: 'ai:networkStatus', // main → renderer event
```
### 4.2 Handler 契约
| 通道 | 参数 | 返回 |
|------|------|------|
| `chapter:move` | `{ bookId, chapterId, volumeId, sortOrder }` | `Chapter` |
| `ai:sessionList` | `{ bookId, includeArchived? }` | `AiSession[]` |
| `ai:sessionCreate` | `{ bookId, title? }` | `AiSession` |
| `ai:sessionUpdate` | `{ bookId, sessionId, patch }` | `AiSession` |
| `ai:sessionDelete` | `{ bookId, sessionId }` | `void` |
| `ai:messageList` | `{ bookId, sessionId }` | `AiMessage[]` |
| `ai:chat` | `{ bookId, sessionId, content, slashCommand? }` | `{ messageId }` |
| `ai:abort` | `{ messageId }` | `void` |
| `ai:buildContext` | `{ bookId, binding }` | `{ preview: ContextPreviewItem[] }` |
| `ai:namingCheck` | `{ bookId }` | `NamingConflict[]` |
**`ai:chat` 流程**
1. 持久化 user message
2. `AiContextBuilder` 组装 messages 数组(system + 历史 + user
3. 调用 `AiClientService.chatStream(...)`
4. 流式推送 `ai:streamChunk`;完成后持久化 assistant message
---
## 5. P2.1 功能规格
### 5.1 章节拖拽排序
**UI**:左栏章节列表项 `draggable`;拖拽指示线(同卷内)。
**逻辑**
- 同卷:重算 `sort_order`0..n-1),批量 `chapter:move` 或单次 `chapter.repo.move`
- 跨卷:更新 `volume_id` + 目标卷末尾 `sort_order`
- 拖拽结束触发 `flushEditorSave()`(若当前章节被移动)
**Repository**
```typescript
move(chapterId: string, volumeId: string, sortOrder: number): Chapter
```
### 5.2 全局搜索替换
对齐 `design/readme.md` §5.6.1 底部替换区。
**UI**`SearchModal` 扩展):
- 折叠区「替换」:替换文本输入 + 「预览变更」+ 「执行替换」
- 预览调用 `search:replace(dryRun: true)` → 列表展示受影响条目
- 确认后 `search:replace(dryRun: false)` → toast 显示替换数量
- 替换后刷新 `useBookStore` 章节列表;若当前章被替换则刷新编辑器
**范围**:P2 已支持全书;本阶段补 UI,不扩展 scope select(可后续加)。
### 5.3 灵感快速捕获
**UI**`#inspirationModal`(对齐 `ui_pc.html`
- 标题、内容、标签(逗号分隔)
- 语音按钮:`webkitSpeechRecognition``zh-CN`),实时填入内容框
- 保存 → `inspiration:create` + 可选 `switchTarget`
- 不打断编辑器:关闭后焦点回 `.ProseMirror`
**快捷键**`Ctrl+Shift+I``captureInspiration` 已有 handler,改为打开模态而非直接 create)
### 5.4 `@提及` 反向跳转
`SettingMention` Mark 增加 `onClick` / `editorProps` 处理:
- 点击带 `data-setting-mention` 的 span → `switchTarget({ kind: 'setting', id })`
- 光标样式 `pointer`
### 5.5 引用面板钉住
`useReferenceStore` 扩展:
```typescript
pinned: boolean
setPinned(open: boolean): void
```
- 钉住按钮(📌)在 `ref-header`
- `pinned === true``setOpen(false)` 不隐藏面板
- 与 AI 右栏互斥:引用打开时仍隐藏 RightPanel(延续 P2);钉住引用 + 需 AI 时用户手动关闭引用
---
## 6. P3 功能规格
### 6.1 AI 面板 UI`AiPanel.tsx`
对齐 `ui_pc.html` `#ai-chat`
| 区域 | 行为 |
|------|------|
| 会话栏 | `<select>` + 新建按钮;重命名(双击 inline);右键归档/删除 |
| 模式 Tab | 对话 **active**;交互/自动/向导 → `feature.comingSoon` |
| 上下文预览行 | 显示摘要计数;点击打开 `ContextEditorModal` |
| 离线横幅 | `aiStore.disabled` 时显示 `#offlineBanner` |
| 消息区 | user/assistant 气泡;assistant 流式追加 |
| 快捷命令 | `/续写` `/扩写` `/润色` `/总结` `/纠错` chip 点击填入 |
| 输入区 | Enter 发送;生成中禁用 + 「停止」调 `ai:abort` |
| 插入编辑器 | assistant 消息旁「插入」→ 纯文本插入章节光标(触发 AI 快照) |
### 6.2 上下文注入(`AiContextBuilder`
**源数据**
| 类型 | 提取内容 | 单条上限 |
|------|----------|----------|
| chapter | `title` + `stripHtml(content)` 截断 | 2000 字符 |
| outline | `title` + `description` 截断 | 500 字符 |
| setting | `name` + `description` 截断 | 500 字符 |
| inspiration | `title` + `content` 截断 | 500 字符 |
**总量上限**~16KB plain text;超出时按 sort_order 新近优先截断。
**System prompt 模板**i18n):
```
你是笔临 AI 写作助手。作者笔名:{penName}。书籍:{bookName}。
以下是与本书相关的上下文(由作者勾选):
{contextBlocks}
请用简体中文回答,尊重作者的创作主权。
```
**「更新上下文」**:重新 `ai:buildContext` 并更新 `context_json` 勾选不变;仅刷新预览摘要。
### 6.3 快捷命令
| 命令 | 预设意图 |
|------|----------|
| `/续写` | 基于当前章节末尾续写 200–400 字 |
| `/扩写` | 扩展选中或最近一段描写 |
| `/润色` | 改善文笔,保持情节不变 |
| `/总结` | 总结当前章节要点 |
| `/纠错` | 指出语法/标点问题 |
实现:解析以 `/` 开头的输入 → 映射 i18n 模板 → 作为 user message 发送(UI 仍显示原命令)。
### 6.4 离线降级(§6.8
**主进程 `NetworkMonitor`**
- 监听 `net.isOnline()` + 定时探测(可选 `HEAD``https://1.1.1.1`
- 状态变化 → `AI_NETWORK_STATUS`
**渲染进程规则**
```typescript
const disabled =
settings.aiConfig.backend !== 'lmstudio' && !networkOnline
```
- `disabled` 时:输入框 `disabled`,快捷命令灰显,横幅可见
- `lmstudio` 后端:**永不**因离线禁用
- 网络恢复:toast `ai.networkRestored`
### 6.5 命名一致性检查(§5.6.2)
**入口**:知识库 Tab 顶部按钮「命名一致性检查」
**流程**
1. 收集 `settings[].name` + 全书章节 plainText
2. 构造 prompt,要求模型返回 **纯 JSON 数组** `NamingConflict[]`
3. `NamingCheckModal` 列表:termA / termB / 置信度 / 原因
4. 操作:忽略(加入 `namingIgnoreWords`)、替换为 canonical(调 `search:replace`
5. 解析失败:toast 错误,展示原始响应片段(调试用)
### 6.6 AI 快照(§5.4.1
触发点:
- `AiPanel`「插入到编辑器」
- 任意将 AI 生成内容写入 `chapters.content` 的路径
行为:`snapshot:create(chapterId, 'ai', 'AI 生成前', currentContent)` → 再写入新内容。
### 6.7 AI 设置页
**位置**:系统设置 → 新 Tab「AI 配置」
| 字段 | 说明 |
|------|------|
| 后端类型 | select: LM Studio / OpenAI / Anthropic |
| 服务地址 | textLM Studio 默认 `http://127.0.0.1:1234` |
| 模型 | text(默认 `gemma-4-e4b-it` |
| API Key | password(云端后端时显示) |
| 超时 | number,秒 |
**§6.9 许可提示**:选择云端后端时显示服务条款链接文案。
**测试连接**按钮:发送最小 chat 请求,toast 成功/失败。
---
## 7. 测试
### 7.1 原则
- **禁止 Mock** `fetch` / AI 响应
- **禁止** `test.skipIf(!aiAvailable)` 或类似条件跳过
- 集成/E2E 必须对 `http://127.0.0.1:1234` 发起真实请求
- 服务未启动时测试 **失败**,迫使环境就绪
### 7.2 单元 / 集成
| 文件 | 场景 |
|------|------|
| `tests/main/migrate-v3.test.ts` | v2→v3 迁移 |
| `tests/main/ai-session.repo.test.ts` | 会话/消息 CRUD |
| `tests/main/chapter.repo.test.ts` | `move` 排序 |
| `tests/main/ai-client.test.ts` | 真实 LM Studio 请求,响应非空 |
| `tests/main/ai-context-builder.test.ts` | 上下文截断与拼接 |
| `tests/main/naming-check.test.ts` | JSON 解析(mock 仅解析器,不 mock HTTP |
### 7.3 E2E
| ID | 场景 |
|----|------|
| E2E-P21-DRAG-01 | 拖拽章节改变顺序,重启后保持 |
| E2E-P21-REP-01 | 搜索 → 预览替换 → 执行 → 正文更新 |
| E2E-P21-INSP-01 | Ctrl+Shift+I 模态保存灵感 |
| E2E-P3-CHAT-01 | 新建会话 → 发送「你好」→ assistant 非空 |
| E2E-P3-CTX-01 | 勾选设定 → 更新上下文 → 摘要行更新 |
| E2E-P3-CMD-01 | 点击 `/总结` → 发送 → 收到回复 |
| E2E-P3-NAME-01 | 命名检查模态出现 ≥1 行(需种子数据) |
| E2E-P3-OFF-01 | 配置 openai + 模拟离线 → 输入禁用(可用 evaluate 设 `aiStore` |
### 7.4 手动验收
1. LM Studio 运行 `gemma-4-e4b-it`,笔临设置页「测试连接」成功
2. 右栏多轮对话,重启后会话与消息保留
3. 上下文勾选后 AI 回答能引用设定名称
4. 命名检查对「林远/林悦」类冲突给出建议
---
## 8. UI / CSS
- 新增/扩展 `layout.css``.ai-session-bar``.ai-mode-tabs``.chat-messages``.chat-msg``.quick-cmds``.offline-banner``.naming-modal`
- 对齐 `design/ui_pc.html` 行 12032076 结构与 class 命名
- 灵感模态对齐 `#inspirationModal`
- i18n`ai.*``naming.*``inspiration.voice*` 等键
---
## 9. 风险与缓解
| 风险 | 缓解 |
|------|------|
| LM Studio 未启动导致 CI 失败 | 文档明确前置条件;本地开发脚本检测端口 |
| 流式 IPC 消息乱序 | `messageId` 关联;渲染进程按 id 缓冲 |
| 大上下文超 token | 硬限制 16KB + 截断策略 |
| 命名检查 JSON 解析失败 | 重试一次;失败 toast + 日志 |
| Web Speech API 不可用 | 语音按钮 disabled + tooltipE2E 不测语音 |
| `gemma-4-e4b-it` 模型名变更 | 环境变量 `BILIN_AI_MODEL` 覆盖 |
---
## 10. Definition of Donev0.3.0
- [ ] P2.1ac + P3.03 全部里程碑完成
- [ ] §7 测试全部 PASS(含真实 LM Studio 集成测试)
- [ ] `npm run build` 无 error
- [ ] 交互/自动/向导 Tab 为占位,不 crash
- [ ] Windows 手动验收 §7.4 四项通过
---
## Spec Self-Review
| 检查项 | 结果 |
|--------|------|
| Placeholder 扫描 | 交互/自动/向导/知识库列表明确 P4;无 TBD |
| 内部一致性 | AI 主进程直连贯穿 IPC/测试;context_json 与 AiContextBinding 对齐 |
| 范围 | 单规格可支撑一份 implementation planP2.1 与 P3 边界清晰 |
| 歧义 | P3-A 已确认;LM Studio 默认值与环境变量覆盖已写明 |
| 测试 | 禁止 Mock/skip 已作为硬性原则写入 §7.1 |
---
## 附录:与 P2 规格差异
| 项 | P2 规格 | 本规格 |
|----|---------|--------|
| 搜索替换 UI | P2.6 规划 | P2.1b 实装 |
| 灵感语音 | P2.1 占位 | P2.1c 实装 |
| 章节拖拽 | P2.1 | P2.1a 实装 |
| AI 命名检查 | P3 占位 | P3.3 实装 |
| ai 快照 | P3 | P3.3 实装 |
| ai_sessions 表 | P3/P5/P6 | P3.0 实装(每书 DB |
| 写作模式 | — | P3 UI 占位,P4 实装 |