a06038035b
定义 Electron+React 地基与写作核心的架构、IPC、数据模型与验收标准,作为全量实现的第一阶段依据。 Co-authored-by: Cursor <cursoragent@cursor.com>
18 KiB
18 KiB
笔临 P0/P1 设计规格
版本:1.0
日期:2026-07-05
范围:Electron + React 全量实现的第一阶段(地基 + 写作核心)
参考:design/readme.md v1.1、design/ui_pc.html
1. 背景与目标
1.1 产品定位
笔临是面向长篇连载作者的桌面写作平台,以「人本创作」为核心:AI 辅助可选,作者保留绝对控制权。本项目从设计文档与 UI 原型出发,实现完整桌面应用;交付采用分阶段策略,最终覆盖设计文档全部 20 章功能。
1.2 本规格范围(P0 + P1)
| 阶段 | 目标 | 预估周期 |
|---|---|---|
| P0 地基 | 可启动、可持久化设置、主题/i18n/Onboarding/快捷键 | ~1 周 |
| P1 写作核心 | 多书管理、分卷章节、TipTap 编辑、自动保存、字数统计 | ~2 周 |
P1 验收标准:用户能完成「首次引导 → 创建书籍 → 分卷分章 → 富文本写作 → 自动保存 → 重启后内容恢复」。AI 与高级面板为 UI 占位,不实现业务逻辑。
1.3 已确认决策
| 维度 | 决策 |
|---|---|
| 总体目标 | 全量实现,分 P0–P7 阶段交付 |
| P1 演示重点 | 纯写作体验,AI/高级功能占位 |
| UI 策略 | 混合方案 C:布局/主题/编辑器区沿用 ui_pc.html CSS;模态/表单等用 Radix UI |
| 目标平台 | Windows 优先(Win 10/11),macOS/Linux 在 P7 适配 |
| 状态管理 | Zustand(全局域)+ Jotai(编辑器高频局部状态) |
| 外围体验 | 与设计文档对齐:7 主题 + 双语 i18n + Onboarding + 可自定义快捷键 |
| 工程结构 | electron-vite 单体仓库(方案 A) |
1.4 后续阶段预览(本规格不实现)
| 阶段 | 范围 |
|---|---|
| P2 | 大纲/设定/灵感持久化、引用面板、全局搜索、版本历史 |
| P3 | AI 会话、对话模式、上下文注入、离线降级 |
| P4 | 交互/自动/向导写作、知识抽取、文风学习 |
| P5 | 驾驶舱、地标、章间衔接、更新计划、章节模板 |
| P6 | 关系图谱、时间线、用词分析、连续阅读 |
| P7 | 导入导出、云同步、自动更新、崩溃报告、跨平台打包 |
2. 架构
2.1 进程模型
┌─────────────────────────────────────────────────────────┐
│ 主进程 (Node.js) │
│ · better-sqlite3(每书一个 .sqlite) │
│ · global_settings.json / books_registry.json │
│ · IPC Handlers(IpcResult 信封) │
│ · globalShortcut 快捷键 │
└───────────────────────────┬─────────────────────────────┘
│ contextBridge
┌───────────────────────────▼─────────────────────────────┐
│ 渲染进程 (React 18 + TypeScript) │
│ · Zustand Stores(书籍/Tab/设置) │
│ · Jotai Atoms(编辑器 dirty/字数/光标) │
│ · TipTap 编辑器 │
│ · i18next + 7 主题 CSS 变量 │
└─────────────────────────────────────────────────────────┘
约束:
- 禁止
nodeIntegration;所有 DB/FS 操作经 IPC - 所有 IPC 返回值使用
IpcResult<T>(见 §6) better-sqlite3仅在主进程使用同步 API
2.2 技术栈
| 层次 | 技术 |
|---|---|
| 桌面框架 | Electron(electron-vite 脚手架) |
| 前端 | React 18、TypeScript strict |
| 编辑器 | TipTap (ProseMirror) |
| 数据库 | better-sqlite3 |
| 全局状态 | Zustand |
| 编辑器状态 | Jotai |
| 交互组件 | Radix UI(Dialog、Select、Tabs、Checkbox、Toast) |
| 国际化 | i18next + react-i18next |
| 单元/集成测试 | Vitest |
| E2E | Playwright + Electron |
| 图标 | Font Awesome 6(与原型一致) |
3. 项目目录结构
bilin2/
├── design/ # 设计文档(只读参考)
├── docs/superpowers/
│ ├── specs/ # 本文件
│ └── plans/ # 实现计划(writing-plans 产出)
├── public/locales/
│ ├── zh-CN/translation.json
│ └── en/translation.json
├── src/
│ ├── main/
│ │ ├── index.ts
│ │ ├── db/
│ │ │ ├── connection.ts
│ │ │ ├── migrations/
│ │ │ ├── schema-v1.sql
│ │ │ └── repositories/
│ │ │ ├── volume.repo.ts
│ │ │ └── chapter.repo.ts
│ │ ├── ipc/
│ │ │ ├── register.ts
│ │ │ ├── result.ts
│ │ │ └── handlers/
│ │ ├── services/
│ │ │ ├── book-registry.ts
│ │ │ ├── global-settings.ts
│ │ │ └── word-count.ts
│ │ └── shortcuts/
│ │ └── manager.ts
│ ├── preload/
│ │ ├── index.ts
│ │ └── api.d.ts
│ └── renderer/
│ ├── styles/ # themes.css, layout.css(从 ui_pc.html 提取)
│ ├── components/
│ │ ├── layout/
│ │ ├── home/
│ │ ├── editor/
│ │ ├── settings/
│ │ ├── onboarding/
│ │ └── ui/ # Radix 封装
│ ├── stores/ # Zustand
│ ├── atoms/ # Jotai
│ ├── hooks/
│ ├── i18n/
│ └── lib/
├── electron.vite.config.ts
├── package.json
└── tsconfig.json
分层约定:Repository 负责 SQL;IPC Handler 负责校验 + 调用 Repository + 返回 IpcResult;渲染进程禁止直接访问数据库。
4. P0 交付清单
| 模块 | 内容 | 设计文档对应 |
|---|---|---|
| 工程脚手架 | electron-vite + React + TS strict + ESLint | §1.4 |
| 自定义标题栏 | drag 区域、最小化/最大化/关闭 | ui_pc.html #topbar |
| 主题系统 | 7 套主题 CSS 变量,data-theme 切换,持久化 |
§3.1 |
| i18n | zh-CN / en,所有 UI 文本走 t() |
§3.2 |
| 全局设置 | global_settings.json 读写 |
§19 |
| IPC 基础 | IpcResult 信封 + 通道注册 |
§16 |
| 数据库 | Schema v1 迁移框架 | §12.1 |
| Onboarding | 5 步向导,笔名必填,可选示例书 | §3.3 |
| 快捷键 | 默认绑定 + 设置页录制 + 冲突提示 | §3.4 |
| 三视图骨架 | 主页 / 编辑器 / 设置页 + Tab 管理 | ui_pc.html |
5. P1 交付清单
5.1 完整实现
| 模块 | 内容 |
|---|---|
| 主页 | 书籍网格、新建书籍(Radix Dialog)、最近编辑、连更徽章占位 |
| 多书 Tab | 打开/切换/关闭,Tab 标题为书名 |
| 分卷 | 创建、重命名、折叠/展开、删除(章节移入未分类或跟随删除) |
| 章节树 | 树状展示、选中、双击改标题、按当前卷新建章、状态徽章(草稿/定稿等) |
| TipTap 编辑器 | 加粗/斜体/下划线/分割线/缩进;默认 Ctrl+V 纯文本 |
| 自动保存 | 输入停顿 2 秒 debounce 写入 SQLite |
| 字数统计 | 本章/本卷/全书增量更新,状态栏展示 |
| 章节切换 | 切换前强制保存;加载 content + cursor_offset |
| Undo | 每章独立内存 undo 堆栈(TipTap 内置);磁盘持久化放 P2 |
5.2 UI 占位(可见、不可用时 Toast)
| 功能 | 占位行为 |
|---|---|
| 右栏 AI 助手 | 布局完整,输入框 disabled,文案 t('feature.comingSoon') |
| 右栏知识库/用词/书籍设置 | Tab 可见,点击内容区显示占位 |
| 引用面板 | 工具栏按钮可见,点击 Toast |
| 顶栏驾驶舱/图谱/时间线/专注/音效 | 按钮可见,点击 Toast 或空 modal 壳 |
| 侧栏大纲/设定/灵感 | Tab 可见,mock 静态列表,切换编辑器标签但不持久化 |
| 批量章节操作 | P2 |
| 导入导出 | P7 |
6. IPC 协议
6.1 IpcResult 信封
type IpcResult<T> =
| { ok: true; data: T }
| { ok: false; error: IpcError };
interface IpcError {
code: ErrorCode;
message: string;
recoverable: boolean;
}
enum ErrorCode {
DB_READ_FAILED = 'DB_READ_FAILED',
DB_WRITE_FAILED = 'DB_WRITE_FAILED',
FILE_NOT_FOUND = 'FILE_NOT_FOUND',
IMPORT_INVALID = 'IMPORT_INVALID',
UNKNOWN = 'UNKNOWN',
}
6.2 通道列表(P0/P1)
设置
| 通道 | 参数 | 返回 |
|---|---|---|
settings:get |
— | GlobalSettings |
settings:update |
Partial<GlobalSettings> |
GlobalSettings |
书籍
| 通道 | 参数 | 返回 |
|---|---|---|
book:list |
— | BookMeta[] |
book:create |
CreateBookParams |
BookMeta |
book:delete |
{ bookId } |
void |
book:open |
{ bookId } |
{ meta, volumes, chapters } |
分卷
| 通道 | 参数 | 返回 |
|---|---|---|
volume:create |
{ bookId, name } |
Volume |
volume:update |
{ bookId, volumeId, ... } |
Volume |
volume:delete |
{ bookId, volumeId, moveChaptersTo? } |
void |
volume:reorder |
{ bookId, orderedIds } |
void |
章节
| 通道 | 参数 | 返回 |
|---|---|---|
chapter:create |
{ bookId, volumeId, title } |
Chapter |
chapter:get |
{ bookId, chapterId } |
Chapter |
chapter:update |
{ bookId, chapterId, title?, content?, status?, cursorOffset?, wordCount? } |
Chapter |
chapter:delete |
{ bookId, chapterId } |
void |
chapter:reorder |
{ bookId, volumeId, orderedIds } |
void |
快捷键
| 通道 | 参数 | 返回 |
|---|---|---|
shortcut:getAll |
— | Record<ActionId, string> |
shortcut:register |
{ action, accelerator } |
void |
主→渲染事件
| 事件 | 用途 |
|---|---|
shortcut:triggered |
{ action: ActionId } |
6.3 Preload API 形状
interface ElectronAPI {
settings: {
get(): Promise<IpcResult<GlobalSettings>>;
update(p: Partial<GlobalSettings>): Promise<IpcResult<GlobalSettings>>;
};
book: {
list(): Promise<IpcResult<BookMeta[]>>;
create(p: CreateBookParams): Promise<IpcResult<BookMeta>>;
delete(p: { bookId: string }): Promise<IpcResult<void>>;
open(p: { bookId: string }): Promise<IpcResult<BookOpenResult>>;
};
volume: { /* create, update, delete, reorder */ };
chapter: { /* create, get, update, delete, reorder */ };
shortcut: {
getAll(): Promise<IpcResult<Record<ActionId, string>>>;
register(p: { action: ActionId; accelerator: string }): Promise<IpcResult<void>>;
};
onShortcutTriggered(callback: (action: ActionId) => void): () => void;
}
7. 数据模型
7.1 文件布局
{userData}/
├── global_settings.json
├── books_registry.json
├── books/
│ └── {bookId}.sqlite
└── logs/bilin.log
7.2 global_settings.json
interface GlobalSettings {
penName: string;
onboardingCompleted: boolean;
theme: 'default' | 'bamboo' | 'moonlit' | 'ricepaper' | 'mist' | 'teagarden' | 'twilight';
language: 'zh-CN' | 'en';
dailyWordGoal: number;
shortcuts: Record<ActionId, string>;
}
P0/P1 不实现:aiBackend、syncConfig、chapterTemplates(留字段或 P2+ 扩展)。
7.3 books_registry.json
interface BookMeta {
id: string;
name: string;
category: string;
targetWordCount: number | null;
dbPath: string;
status: 'draft' | 'ongoing' | 'done';
lastOpenedAt: string;
lastChapterId: string | null;
createdAt: string;
}
7.4 SQLite Schema v1(每书)
CREATE TABLE schema_version (
version INTEGER PRIMARY KEY,
applied_at TEXT NOT NULL DEFAULT (datetime('now')),
description TEXT
);
CREATE TABLE volumes (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE chapters (
id TEXT PRIMARY KEY,
volume_id TEXT,
title TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'draft',
word_count INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0,
cursor_offset INTEGER DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (volume_id) REFERENCES volumes(id) ON DELETE SET NULL
);
P1 不含:outline_items、settings、inspiration、snapshots、undo_stack、bookmarks 等(P2+ 通过迁移追加)。
7.5 创建书籍流程
- 生成 UUID
- 在
books/下创建{id}.sqlite,执行 schema v1 迁移 - 写入
books_registry.json - 默认创建分卷「第一卷」
- 若 Onboarding 勾选示例书,额外创建 1 章示例正文
8. 状态管理
8.1 Zustand Stores
| Store | 职责 |
|---|---|
useAppStore |
当前视图(home/editor/settings)、侧栏激活 panel |
useTabStore |
openTabs、activeTabId |
useBookStore |
书籍列表、当前书、分卷章节树、选中章节 ID |
useSettingsStore |
theme、language、penName、shortcuts |
useOnboardingStore |
向导步骤、完成状态 |
写操作:调用 IPC → 成功后更新 Store。
8.2 Jotai Atoms(编辑器域)
| Atom | 用途 |
|---|---|
currentChapterIdAtom |
当前编辑章节 |
editorContentAtom |
TipTap 文档内容 |
editorDirtyAtom |
未保存标记 |
editorSavingAtom |
保存中状态 |
wordCountAtom |
{ chapter, volume, book } |
cursorOffsetAtom |
光标字符偏移 |
lastSavedAtAtom |
上次保存时间 |
8.3 章节切换序列
- 若
editorDirtyAtom→ 立即chapter:update chapter:get加载新章节- TipTap 设置 content,恢复 cursor_offset
- 重置 dirty
8.4 自动保存
- 监听 TipTap
onUpdate - debounce 2000ms
- 调用
chapter:update(content、cursorOffset、wordCount) - 主进程
word-count.ts增量更新章节字数;卷/全书字数由 chapters 聚合或缓存
9. UI 规范
9.1 混合方案落地
| 区域 | 实现 |
|---|---|
| 顶栏、侧栏、编辑器区、状态栏 | 从 ui_pc.html 提取 CSS(变量 + 布局类) |
| 模态框、Select、Checkbox、Toast | Radix UI 封装于 components/ui/ |
| 主题 | [data-theme="xxx"] 覆盖 CSS 变量,与原型 7 主题 ID 一致 |
9.2 三视图
- 主页
#home-page:Logo、操作按钮、书籍网格、最近编辑 - 编辑器
#editor-layout:左栏四 Tab + 中央编辑器 + 右栏占位 - 设置
#settings-page:侧栏导航 + 通用/编辑器/AI(占位)/快捷键/备份(占位)/关于
9.3 默认快捷键(P0/P1 实现子集)
| 功能 | 默认 | P0/P1 |
|---|---|---|
| 保存章节 | Ctrl+S | ✅ |
| 新建章节 | Ctrl+Shift+N | ✅ |
| 切换专注模式 | Ctrl+Alt+F | 占位 Toast |
| 全局搜索 | Ctrl+Shift+F | 占位 Toast |
| 关闭 Tab | Ctrl+W | ✅ |
| 打开设置 | — | 顶栏齿轮 |
其余快捷键在设置页展示,P2+ 绑定实际功能。
9.4 i18n 要求
- 所有用户可见字符串使用
t() - zh-CN 与 en 同步维护,不得混用语言
- 数字/日期格式随 locale 变化
10. 核心流程
10.1 启动
- 主进程读取
global_settings.json,注册快捷键 - 渲染进程
settings:get→ 应用 theme + language - 若
onboardingCompleted === false→ 显示 Onboarding - 否则 → 主页 +
book:list
10.2 打开书籍
book:open→ 填充 useBookStore- 新建 Tab,
lastChapterId或第一卷第一章 chapter:get→ TipTap 加载
10.3 错误处理
| 场景 | code | recoverable | UI |
|---|---|---|---|
| SQLite 写失败 | DB_WRITE_FAILED | true | Toast + 重试 |
| DB 文件丢失 | FILE_NOT_FOUND | false | 错误对话框 |
| IPC 异常 | UNKNOWN | true | Toast |
渲染进程统一 ipcCall() 封装处理。
11. 测试
11.1 工具
- Vitest:主进程 Repository、word-count、IPC handler(内存 SQLite)
- Playwright + Electron:E2E
11.2 P0/P1 必过用例
| ID | 场景 |
|---|---|
| E2E-THEME-01 | 依次切换 7 主题,UI 颜色正确 |
| E2E-I18N-03 | 中英文切换,无语言混杂 |
| E2E-ONBOARD-02 | 完成引导,笔名持久化 |
| E2E-KEY-02 | Ctrl+Shift+N 新建章节 |
| E2E-P1-SAVE | 写作 → 等 2s 自动保存 → 重启 → 内容完整 |
| E2E-P1-CURSOR | 切换章节再回来,光标偏移恢复 |
12. 非目标(P0/P1 明确排除)
- AI 任何后端集成
- 版本历史、undo 磁盘持久化
- 大纲/设定/灵感数据持久化
- 导入导出、云同步、PDF
- 驾驶舱、图谱、时间线业务逻辑
- macOS/Linux 打包与签名
- 无障碍完整 E2E(P7 补全)
13. 风险与缓解
| 风险 | 缓解 |
|---|---|
| better-sqlite3 原生模块 Electron 编译 | electron-vite 官方模板 + rebuild 脚本 |
| TipTap 大文档性能 | P1 单章编辑;全书虚拟滚动在 P2 |
| ui_pc.html CSS 迁移工作量大 | 先迁移 layout + themes,组件级样式按需提取 |
| P1 范围含完整 i18n/主题/Onboarding 导致工期膨胀 | 严格区分 P1 完整 vs 占位清单(§5.2) |
14. 完成定义(Definition of Done)
P0/P1 合并发布为 v0.1.0 时:
- Windows 下
npm run dev可启动,无控制台 error - 新用户走完 Onboarding,可创建书籍并写作
- 7 主题、双语、快捷键自定义可用
- 自动保存与重启恢复通过 E2E
- §5.2 占位功能行为符合规范(可见 + Toast,无崩溃)
- Vitest 集成测试覆盖 chapter CRUD + settings 读写
本规格为笔临全量实现的 Phase 0/1 子项目。后续阶段各自独立 spec 或在本文档增量修订。