# 笔临 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`(见 §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 信封 ```typescript type IpcResult = | { 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` | **书籍** | 通道 | 参数 | 返回 | |------|------|------| | `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` | | `shortcut:register` | `{ action, accelerator }` | `void` | **主→渲染事件** | 事件 | 用途 | |------|------| | `shortcut:triggered` | `{ action: ActionId }` | ### 6.3 Preload API 形状 ```typescript interface ElectronAPI { settings: { get(): Promise>; update(p: Partial): Promise>; }; book: { list(): Promise>; create(p: CreateBookParams): Promise>; delete(p: { bookId: string }): Promise>; open(p: { bookId: string }): Promise>; }; volume: { /* create, update, delete, reorder */ }; chapter: { /* create, get, update, delete, reorder */ }; shortcut: { getAll(): Promise>>; register(p: { action: ActionId; accelerator: string }): Promise>; }; 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 ```typescript interface GlobalSettings { penName: string; onboardingCompleted: boolean; theme: 'default' | 'bamboo' | 'moonlit' | 'ricepaper' | 'mist' | 'teagarden' | 'twilight'; language: 'zh-CN' | 'en'; dailyWordGoal: number; shortcuts: Record; } ``` P0/P1 不实现:`aiBackend`、`syncConfig`、`chapterTemplates`(留字段或 P2+ 扩展)。 ### 7.3 books_registry.json ```typescript 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(每书) ```sql 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 创建书籍流程 1. 生成 UUID 2. 在 `books/` 下创建 `{id}.sqlite`,执行 schema v1 迁移 3. 写入 `books_registry.json` 4. 默认创建分卷「第一卷」 5. 若 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 章节切换序列 1. 若 `editorDirtyAtom` → 立即 `chapter:update` 2. `chapter:get` 加载新章节 3. TipTap 设置 content,恢复 cursor_offset 4. 重置 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 启动 1. 主进程读取 `global_settings.json`,注册快捷键 2. 渲染进程 `settings:get` → 应用 theme + language 3. 若 `onboardingCompleted === false` → 显示 Onboarding 4. 否则 → 主页 + `book:list` ### 10.2 打开书籍 1. `book:open` → 填充 useBookStore 2. 新建 Tab,`lastChapterId` 或第一卷第一章 3. `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 或在本文档增量修订。*