docs: 添加笔临 P0/P1 设计规格

定义 Electron+React 地基与写作核心的架构、IPC、数据模型与验收标准,作为全量实现的第一阶段依据。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-05 16:47:15 +08:00
parent ca2cd29f01
commit a06038035b
4 changed files with 961 additions and 26 deletions
+2
View File
@@ -1,3 +1,5 @@
.worktrees/
worktrees/
node_modules/ node_modules/
dist/ dist/
out/ out/
+218 -22
View File
@@ -1,9 +1,11 @@
# 笔临——长篇创作智能协作平台 完整设计文档 # 笔临——长篇创作智能协作平台 完整设计文档
**版本**1.0 **版本**1.1
**更新日期**2026年6月25 **更新日期**2026年7月2日
**定位**:跨平台长篇小说写作系统,以“人本创作”为核心,通过深度上下文感知与创作流优化,解决AI辅助写作中“难用、不敢用、割裂”的痛点。 **定位**:跨平台长篇小说写作系统,以“人本创作”为核心,通过深度上下文感知与创作流优化,解决AI辅助写作中“难用、不敢用、割裂”的痛点。
> **v1.1 变更摘要**:以资深连载作者工作流为基准,补全引用面板/全局搜索/AI 会话/地标待办/更新计划等 UI 规格;统一主题命名;修正驾驶舱进入逻辑;新增平台投稿与衔接检查等作家刚需能力。
--- ---
## 1. 项目概述 ## 1. 项目概述
@@ -66,15 +68,17 @@
#### 3.1.2 七套内置主题 #### 3.1.2 七套内置主题
| 主题名称 | 类型 | 主背景色 | 主文字色 | 强调色 | 适用场景 | 主题命名采用文学意象,与 UI 原型(`ui_pc.html`)保持一致:
|----------|------|----------|----------|--------|----------|
| **暗夜黑** | 深色 | `#1a1a2e` | `#e0e0e0` | `#6c63ff` | 夜间长时间写作 | | 主题名称 | 内部 ID | 类型 | 主背景色 | 主文字色 | 强调色 | 适用场景 |
| **深海蓝** | 深色 | `#0d1b2a` | `#c5d5e8` | `#4ea8de` | 沉浸式创作 | |----------|---------|------|----------|----------|--------|----------|
| **石墨灰** | 深色 | `#2d2d2d` | `#cccccc` | `#ff6b6b` | 中性低调偏好 | | **墨砚**(默认) | `default` | 深色 | `#1c1816` | `#e8e0d8` | `#c0504a` | 夜间长时间写作,偏暖深棕 |
| **暖米纸** | 色 | `#f5f0e8` | `#3a3a3a` | `#b8860b` | 模拟纸质书写作 | | **竹语** | `bamboo` | 深色 | `#1a1e18` | `#e0e4d8` | `#7a9a60` | 竹林雨后,舒缓护眼 |
| **冷瓷白** | 色 | `#fafafa` | `#2c2c2c` | `#2563eb` | 日间清爽写作 | | **月夜** | `moonlit` | 深色 | `#1a1e24` | `#d8dce4` | `#8098b8` | 月光静谧,冷调沉浸 |
| **护眼绿** | 护眼 | `#e8f0e0` | `#2d3a2d` | `#4a7c59` | 缓解视疲劳 | | **宣纸** | `ricepaper` | 浅色 | `#f4efe4` | `#3a3028` | `#8b4513` | 模拟古籍书卷写作 |
| **琥珀暮色** | 特殊 | `#2b1f1a` | `#e8d5c0` | `#d4a574` | 温暖怀旧氛围 | | **晨雾** | `mist` | 浅色 | `#eff0f2` | `#2c3038` | `#6878a0` | 日间清爽写作 |
| **茶园** | `teagarden` | 护眼 | `#eef0e4` | `#2d3a28` | `#688848` | 缓解视疲劳 |
| **暮光** | `twilight` | 特殊 | `#1f1a16` | `#e8dcc8` | `#c88850` | 温暖怀旧氛围 |
#### 3.1.3 CSS 变量体系 #### 3.1.3 CSS 变量体系
@@ -181,6 +185,9 @@
| 全局搜索 | `Ctrl+Shift+F` | 是 | | 全局搜索 | `Ctrl+Shift+F` | 是 |
| 快照保存 | `Ctrl+Alt+S` | 是 | | 快照保存 | `Ctrl+Alt+S` | 是 |
| 弹出灵感快速捕获窗 | `Ctrl+Shift+I` | 是 | | 弹出灵感快速捕获窗 | `Ctrl+Shift+I` | 是 |
| 打开引用面板 | `Ctrl+Shift+P` | 是 |
| 插入写作地标 | `Ctrl+Shift+M` | 是 |
| 打开地标清单 | `Ctrl+Shift+L` | 是 |
| 导出当前书籍 | `Ctrl+Shift+E` | 是 | | 导出当前书籍 | `Ctrl+Shift+E` | 是 |
| 关闭当前标签 | `Ctrl+W` | 是 | | 关闭当前标签 | `Ctrl+W` | 是 |
| 朗读(播放/暂停) | `Ctrl+Shift+R` | 是 | | 朗读(播放/暂停) | `Ctrl+Shift+R` | 是 |
@@ -277,15 +284,22 @@
- **项目封面与简介**:可选,用于个人识别。 - **项目封面与简介**:可选,用于个人识别。
- **全书字数目标准**:可选设定总目标字数,用于进度推算。 - **全书字数目标准**:可选设定总目标字数,用于进度推算。
### 4.3 项目驾驶舱(首页) ### 4.3 项目驾驶舱
打开书籍后首先进入仪表盘视图,展示 **进入逻辑**(作家友好优先)
- **首次打开某本书**:展示驾驶舱全屏模态(见 UI 顶栏「驾驶舱」按钮),帮助作者建立全书全局认知。
- **日常再次打开**:默认**直接恢复上次编辑位置**(章节 + 光标偏移),不打断写作心流;驾驶舱随时可通过顶栏按钮唤起。
- 可在书籍设置中切换为「每次打开均显示驾驶舱」(适合开书/planning 阶段)。
驾驶舱展示内容:
- **写作日历热力图**:记录每日写作字数,空白日灰色显示。 - **写作日历热力图**:记录每日写作字数,空白日灰色显示。
- **核心统计卡片**:今日字数、本周字数、全书总字数、距目标字数、预计完本日期(基于近30天日均速度估算)。 - **核心统计卡片**:今日字数、本周字数、全书总字数、距目标字数、预计完本日期(基于近30天日均速度估算)。
- **更新计划摘要**:日更/隔日更目标、存稿缓冲(已定稿未发布章节数)、连续达标天数(见 20.1)。
- **结构速览**:各卷字数占比饼图,可点击下钻。 - **结构速览**:各卷字数占比饼图,可点击下钻。
- **伏笔/角色状态摘要**:已埋伏笔数、已回收数、处于"遗忘"状态的伏笔数(超过20章未提及且未标记已回收)。 - **伏笔/角色状态摘要**:已埋伏笔数、已回收数、处于"遗忘"状态的伏笔数(超过20章未提及且未标记已回收)。
- **快速入口**:"回到上次编辑位置"、"处理被遗忘的伏笔"、"查看近期新增设定"、"今日大纲任务"。 - **快速入口**:"回到上次编辑位置"、"处理被遗忘的伏笔"、"查看近期新增设定"、"今日大纲任务"、"检查章间衔接"(见 20.4
--- ---
@@ -331,6 +345,21 @@
| E2E-BATCH-01 | E2E | Ctrl+点击选择第2、4、7章,批量改为"定稿" | 三章状态同步更新为定稿 | | E2E-BATCH-01 | E2E | Ctrl+点击选择第2、4、7章,批量改为"定稿" | 三章状态同步更新为定稿 |
| E2E-BATCH-02 | E2E | Shift+点击选择5章,批量导出为 .txt | 生成包含5章内容的 .txt 文件 | | E2E-BATCH-02 | E2E | Shift+点击选择5章,批量导出为 .txt | 生成包含5章内容的 .txt 文件 |
#### 5.2.4 章节元数据(作家视角)
每章除正文外,支持以下元数据(在章节右键菜单或章节属性侧栏编辑):
| 字段 | 说明 | 用途 |
|------|------|------|
| `pov_character_id` | 叙事视角人物(可选,关联设定角色) | 视角分布统计、AI 向导式写作默认视角 |
| `scene_count` | 场景数(自动检测 `* * *` 分隔符,可手动校正) | 节奏分析、单章结构把控 |
| `publish_status` | `draft` / `ready` / `published` | 存稿缓冲统计、更新计划追踪 |
| `platform_note` | 作者的话(独立于正文,导出时可选择是否包含) | 网文平台互动、读者说明 |
| `avg_sentence_length` | 本章平均句长(自动计算,只读) | 与全书文风对比 |
- 章节列表可按 `publish_status``pov_character_id`、自定义标签筛选。
- 拖拽重排章节后,若影响全局章节编号,弹出确认:「是否自动重编号?」(可选保留原编号仅改排序)。
--- ---
### 5.3 章节编辑器 ### 5.3 章节编辑器
@@ -399,6 +428,27 @@
| E2E-SPELL-01 | E2E | 输入"他在在看书",开启拼写检查 | "在在"以蓝色波浪线标识 | | E2E-SPELL-01 | E2E | 输入"他在在看书",开启拼写检查 | "在在"以蓝色波浪线标识 |
| E2E-TTS-01 | E2E | 点击朗读按钮 | 从光标位置开始朗读,当前句高亮,再次点击暂停 | | E2E-TTS-01 | E2E | 点击朗读按钮 | 从光标位置开始朗读,当前句高亮,再次点击暂停 |
#### 5.3.8 引用面板(设定/大纲/灵感侧栏)
**入口**:编辑器工具栏「引用」按钮,或快捷键 `Ctrl+Shift+P`Pin 面板,可自定义)。
**布局**:编辑器右侧(可与 AI 面板互斥显示,或窄屏时覆盖 AI 面板):
- 顶部 Tab:设定 / 大纲 / 灵感,支持跨 Tab 搜索。
- 列表项展示:名称 + 摘要前 40 字;点击展开详情。
- **钉住模式**:勾选后,切换章节时保持同一引用条目可见。
- **拖拽插入**:将设定描述、大纲要点拖入正文光标处,插入为纯文本(不带格式块)。
- **反向高亮**:正文中已 `@提及` 的设定名,点击可跳转设定详情(TipTap Mention 扩展)。
#### 5.3.9 写作地标与待办
**插入**:编辑器右键 →「插入地标」,或快捷键 `Ctrl+Shift+M`
- 地标类型:`todo`(待写/待改)、`foreshadow`(伏笔提醒)、`research`(待查证)、`custom`
- 地标在正文以行内标记显示(如 `[TODO: 补写林远与长老对话]`),不进入导出正文(除非用户勾选「包含地标」)。
- **地标清单**:章节侧栏底部「地标」按钮,或全局 `Ctrl+Shift+L` 打开清单面板,按类型/章节分组,点击跳转。
- 完成地标后勾选关闭,可选自动归档到章节摘要。
--- ---
### 5.4 版本历史与内容追溯 ### 5.4 版本历史与内容追溯
@@ -450,14 +500,23 @@
| E2E-COV-01 | E2E | 创建大纲条目不关联章节,查看大纲面板 | 条目显示橙色边框和警告图标 | | E2E-COV-01 | E2E | 创建大纲条目不关联章节,查看大纲面板 | 条目显示橙色边框和警告图标 |
| E2E-COV-02 | E2E | 筛选"未覆盖" | 仅显示未关联章节的大纲条目 | | E2E-COV-02 | E2E | 筛选"未覆盖" | 仅显示未关联章节的大纲条目 |
#### 5.5.4 设定管理 #### 5.5.6 大纲-正文对照视图
**入口**:大纲面板顶部「对照模式」按钮。
- 分屏布局:左 35% 大纲树(当前筛选结果),右 65% 关联章节正文片段。
- 选中大纲条目时,右侧滚动至对应章节段落;无关联时显示「尚未写作」占位。
- 底部对比条:预期字数 vs 实际字数 vs 偏差百分比(>20% 标红)。
- 支持从对照视图直接「生成章节骨架」或「跳转编辑器」。
#### 5.5.7 设定管理
- 分类组织:角色、地点、物品、技能、势力、自定义类别。 - 分类组织:角色、地点、物品、技能、势力、自定义类别。
- 每个设定条目:名称、描述、属性字段(可自定义)、关联章节列表(自动记录提及该设定的章节,点击跳转)。 - 每个设定条目:名称、描述、属性字段(可自定义)、关联章节列表(自动记录提及该设定的章节,点击跳转)。
- **写作时引用**:编辑器侧栏可调出设定面板,快速查看并拖拽内容。 - **写作时引用**:编辑器侧栏可调出设定面板,快速查看并拖拽内容。
- **反向链接**:设定条目页显示"提及章节"和"被AI用作上下文注入的轮次"。 - **反向链接**:设定条目页显示"提及章节"和"被AI用作上下文注入的轮次"。
#### 5.5.5 灵感碎片 #### 5.5.8 灵感碎片
- **快速捕获**:全局快捷键(`Ctrl+Shift+I`)呼出小窗,立即记录想法;支持语音输入转文字。 - **快速捕获**:全局快捷键(`Ctrl+Shift+I`)呼出小窗,立即记录想法;支持语音输入转文字。
- **灵感条目**:标题、内容、创建时间、标签(自定义)。 - **灵感条目**:标题、内容、创建时间、标签(自定义)。
@@ -470,10 +529,19 @@
#### 5.6.1 全局搜索 #### 5.6.1 全局搜索
- 支持跨章节、大纲、设定、灵感全文搜索 **入口**`Ctrl+Shift+F` 打开全局搜索浮层(模态,不离开当前编辑上下文)
- 支持跨章节、大纲、设定、灵感、地标全文搜索。
- 搜索结果:按类型分组,每条结果显示所在位置、前后20字上下文。 - 搜索结果:按类型分组,每条结果显示所在位置、前后20字上下文。
- 高级搜索:正则表达式支持、区分大小写、全词匹配。 - 高级搜索:正则表达式支持、区分大小写、全词匹配、限定分卷/发表状态
- 替换功能:支持单次替换、批量替换,批量替换前展示预览列表并需确认。 - 替换功能:支持单次替换、批量替换,批量替换前展示预览列表并需确认。
- 搜索历史:保留最近 10 条,下拉快速复用。
**UI 规格**(见 `ui_pc.html` `#searchModal`):
- 顶部:搜索框 + 范围筛选(全书/当前卷/当前章)+ 高级选项折叠区。
- 中部:结果列表,支持键盘 ↑↓ 选择、Enter 跳转。
- 底部:替换区(可选展开),含「预览变更」按钮。
#### 5.6.2 命名一致性检查(AI驱动) #### 5.6.2 命名一致性检查(AI驱动)
@@ -562,6 +630,10 @@
- **独立会话**:每本书拥有独立的多个会话,支持新建、重命名、归档、导出(导出为`.txt``.md`对话记录)。 - **独立会话**:每本书拥有独立的多个会话,支持新建、重命名、归档、导出(导出为`.txt``.md`对话记录)。
- **会话列表**:显示会话摘要(模型、创建时间、最后活跃时间),归档会话折叠。 - **会话列表**:显示会话摘要(模型、创建时间、最后活跃时间),归档会话折叠。
- **UI 入口**(见 `ui_pc.html` AI 面板顶部):
- 会话下拉选择器 +「新建会话」按钮。
- 会话名可 inline 重命名;右键归档/导出/删除。
- 当前会话上下文摘要一行展示,点击展开「上下文编辑器」。
- **上下文管理** - **上下文管理**
- **上下文源**:当前书籍的章节(可勾选范围)、大纲(可勾选条目)、设定(可勾选条目)、灵感(可勾选条目)、附件。 - **上下文源**:当前书籍的章节(可勾选范围)、大纲(可勾选条目)、设定(可勾选条目)、灵感(可勾选条目)、附件。
- **上下文注入策略**:创建会话时,根据勾选内容初始化;**提供"更新上下文"按钮**,点击后重新提取截至最新的章节摘要、大纲、设定,注入会话。在每轮对话前,系统自动检索最高重要度的知识条目(角色、伏笔等)注入,而非一次性凝固。 - **上下文注入策略**:创建会话时,根据勾选内容初始化;**提供"更新上下文"按钮**,点击后重新提取截至最新的章节摘要、大纲、设定,注入会话。在每轮对话前,系统自动检索最高重要度的知识条目(角色、伏笔等)注入,而非一次性凝固。
@@ -590,6 +662,17 @@
### 6.5 AI 写作模式 ### 6.5 AI 写作模式
**模式入口**AI 面板输入框上方 Segmented Control):
| 模式 | 适用场景 | 入口标签 |
|------|----------|----------|
| 对话 | 自由问答、快捷命令 | 默认 |
| 交互式 | 逐场景共创、可控性最高 | 「交互写作」 |
| 自动 | 有大纲目标、希望 AI 连续成章 | 「自动写作」 |
| 向导 | 新手第一部作品 | 「向导模式」 |
切换模式时保留当前会话历史,但生成交互流程按所选模式重新初始化。
#### 6.5.1 交互式写作 #### 6.5.1 交互式写作
- **启动条件**:需存在至少1章已完成章节,或有大纲/设定内容,否则引导用户先创建。 - **启动条件**:需存在至少1章已完成章节,或有大纲/设定内容,否则引导用户先创建。
@@ -639,6 +722,10 @@
- **每章生成后自动抽取**:角色状态变化、新埋伏笔、人际关系演变、新出现地点、关键事实。 - **每章生成后自动抽取**:角色状态变化、新埋伏笔、人际关系演变、新出现地点、关键事实。
- **抽取条目人工审核**:在"知识库面板"展示所有抽取项,用户可编辑、合并、删除、打回重抽。 - **抽取条目人工审核**:在"知识库面板"展示所有抽取项,用户可编辑、合并、删除、打回重抽。
- **审核工作流 UI**
- 每条待审核项显示:来源章节、置信度、建议操作(采纳/编辑/忽略/合并到已有条目)。
- 批量操作:「全部采纳高置信度」「忽略本批」。
- 审核完成后条目进入正式知识库,影响后续 AI 注入优先级。
- **重要度标注**:用户可为每条知识打1-5星重要度,影响后续检索注入的优先级。 - **重要度标注**:用户可为每条知识打1-5星重要度,影响后续检索注入的优先级。
- **知识库全局视图** - **知识库全局视图**
- 角色维度:当前状态、装备、关系网络(简易图谱)。 - 角色维度:当前状态、装备、关系网络(简易图谱)。
@@ -747,7 +834,9 @@
- **写作速度仪表盘**:按小时统计平均写作速度,展示效率曲线和每日时段热力图。 - **写作速度仪表盘**:按小时统计平均写作速度,展示效率曲线和每日时段热力图。
- **习惯追踪**:连续写作天数、平均每次写作时长、最喜欢写作的时段。 - **习惯追踪**:连续写作天数、平均每次写作时长、最喜欢写作的时段。
- **字数分布**:按卷、按人物视角章节字数统计。 - **字数分布**:按卷、按人物视角POV章节字数统计。
- **入口**:驾驶舱内嵌卡片 + 书籍设置面板「数据分析」Tab;暂不做独立全屏页(避免功能分散)。
- **角色弧线简报**(轻量):选定主角后,展示「首次出场 → 关键转折 → 最近状态」三节时间线,数据来源于知识库角色状态变更记录(见 20.3)。
--- ---
@@ -1182,9 +1271,10 @@ graph TB
{ {
"penName": "string", "penName": "string",
"onboardingCompleted": false, "onboardingCompleted": false,
"theme": "暗夜黑", "theme": "default",
"language": "zh-CN", "language": "zh-CN",
"dailyWordGoal": 0, "dailyWordGoal": 0,
"updateSchedule": "daily",
"shortcuts": {}, "shortcuts": {},
"aiBackend": "cloud", "aiBackend": "cloud",
"aiTimeout": 60, "aiTimeout": 60,
@@ -1214,13 +1304,18 @@ CREATE TABLE chapters (
title TEXT NOT NULL DEFAULT '', title TEXT NOT NULL DEFAULT '',
content TEXT NOT NULL DEFAULT '', content TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'draft', -- draft/reviewing/final status TEXT NOT NULL DEFAULT 'draft', -- draft/reviewing/final
publish_status TEXT NOT NULL DEFAULT 'draft', -- draft/ready/published
pov_character_id TEXT DEFAULT NULL, -- 叙事视角角色
platform_note TEXT DEFAULT '', -- 作者的话
word_count INTEGER NOT NULL DEFAULT 0, word_count INTEGER NOT NULL DEFAULT 0,
sort_order INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0,
summary TEXT DEFAULT '', summary TEXT DEFAULT '',
story_time TEXT DEFAULT NULL, -- 故事时间标记(V2.0新增) story_time TEXT DEFAULT NULL,
cursor_offset INTEGER DEFAULT 0, -- 上次编辑光标位置
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_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 FOREIGN KEY (volume_id) REFERENCES volumes(id) ON DELETE SET NULL,
FOREIGN KEY (pov_character_id) REFERENCES settings(id) ON DELETE SET NULL
); );
CREATE TABLE chapter_tags ( CREATE TABLE chapter_tags (
@@ -1236,6 +1331,8 @@ CREATE TABLE bookmarks (
chapter_id TEXT NOT NULL, chapter_id TEXT NOT NULL,
position INTEGER NOT NULL, -- 字符偏移 position INTEGER NOT NULL, -- 字符偏移
label TEXT NOT NULL, label TEXT NOT NULL,
landmark_type TEXT NOT NULL DEFAULT 'todo', -- todo/foreshadow/research/custom
resolved INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (chapter_id) REFERENCES chapters(id) ON DELETE CASCADE FOREIGN KEY (chapter_id) REFERENCES chapters(id) ON DELETE CASCADE
); );
@@ -1302,11 +1399,35 @@ CREATE TABLE ai_sessions (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '', name TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '', model TEXT NOT NULL DEFAULT '',
mode TEXT NOT NULL DEFAULT 'chat', -- chat/interactive/auto/wizard
context_config TEXT DEFAULT '{}', -- JSON: 勾选的上下文源
archived INTEGER NOT NULL DEFAULT 0, archived INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
last_active TEXT NOT NULL DEFAULT (datetime('now')) last_active TEXT NOT NULL DEFAULT (datetime('now'))
); );
CREATE TABLE ai_messages (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
role TEXT NOT NULL, -- user/assistant/system
content TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES ai_sessions(id) ON DELETE CASCADE
);
CREATE TABLE knowledge_entries (
id TEXT PRIMARY KEY,
type TEXT NOT NULL, -- character/foreshadow/location/relationship/fact
title TEXT NOT NULL,
content TEXT DEFAULT '',
importance INTEGER NOT NULL DEFAULT 3, -- 1-5
status TEXT NOT NULL DEFAULT 'pending', -- pending/approved/rejected
source_chapter_id TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (source_chapter_id) REFERENCES chapters(id) ON DELETE SET NULL
);
CREATE TABLE writing_logs ( CREATE TABLE writing_logs (
date TEXT NOT NULL, date TEXT NOT NULL,
book_id TEXT NOT NULL, book_id TEXT NOT NULL,
@@ -1330,3 +1451,78 @@ CREATE TABLE timeline_events (
--- ---
## 20. 作家工作流专项设计(v1.1 新增)
> 本章从**日更连载作者**真实工作习惯出发,补全 v1.0 功能清单中「有概念无落地」或「作家强需求但未覆盖」的能力。
### 20.1 更新计划与存稿管理
长篇连载作者的核心焦虑不是「写不出来」,而是「断更」和「存稿不够」。
| 能力 | 说明 |
|------|------|
| **更新频率目标** | 全局设置 `updateSchedule``daily`(日更)/ `alternate`(隔日更)/ `weekly`(周更)/ `none`;驾驶舱显示「距下次应发布还有 X 天」。 |
| **存稿缓冲** | 统计 `publish_status = ready` 的章节数;低于阈值(默认 3 章)时在状态栏软性提醒,不阻断写作。 |
| **发布标记** | 章节右键「标记为已发布」,自动记录发布日期;不影响定稿状态,二者独立。 |
| **日更目标** | 沿用 `dailyWordGoal`;顶栏进度条(UI 已实现)+ 设置页可配置。 |
### 20.2 平台投稿导出预设
- 在导出模态中增加「投稿预设」折叠区:
- **通用网文**:纯文本、章节标题 `# 第N章 标题`、段首空两格、UTF-8。
- **自定义预设**:用户保存标题格式、是否含作者的话、段落间距、敏感词替换规则。
- 导出前可选「复制到剪贴板」以便直接粘贴到平台后台(比下载文件更常用)。
- 首版不做平台 API 直连(YAGNI),仅优化「复制即用」体验。
### 20.3 角色弧线简报
- 从知识库角色状态变更自动聚合,无需作者手动维护时间线。
- 在设定面板角色详情页增加「弧线」Tab:关键节点列表 + 一键跳转对应章节。
- 驾驶舱「角色状态摘要」可下钻至弧线简报。
### 20.4 章间衔接检查
连载作者常见痛点:隔日续写时忘记上一章怎么收的。
- **触发**:打开新章编辑时(或手动点击驾驶舱「检查章间衔接」),若上一章存在且本章字数 < 200,自动展示衔接卡片:
- 上一章末 300 字(高亮)
- 本章已写开头(如有)
- AI 可选建议:「上一章悬念点」+「建议本章开头方向」(需 AI 可用,离线时仅展示文本)
- 卡片可关闭且「本章不再提示」,不强制打断。
### 20.5 敏感词与合规提示(可选开启)
- 设置 → 编辑器 →「合规检查」开关,默认关闭。
- 基于本地词库 + 用户自定义词库,对当前章做软性下划线提示(不阻断、不自动删改)。
- 导出时可选择「导出前扫描并确认」。
### 20.6 灵感快速捕获 UI
- 全局快捷键 `Ctrl+Shift+I` 唤起**全局置顶小窗**(见 `ui_pc.html` `#inspirationModal`):
- 单行/多行输入、可选标签、可选关联当前书籍。
- 支持语音输入转文字(系统 API)。
- 保存后 toast 确认,不打断当前编辑器焦点(保存后自动关闭,焦点回编辑器)。
### 20.7 设计审查清单(作家视角)
| 审查项 | v1.0 状态 | v1.1 处理 |
|--------|-----------|-----------|
| 主题命名 UI/文档不一致 | ❌ | ✅ 统一为七套文学意象主题 |
| 打开书籍强制进驾驶舱 | ⚠️ 打断心流 | ✅ 改为恢复上次编辑位,驾驶舱按需唤起 |
| 引用面板 | 文档有,UI 无 | ✅ 补 UI 规格与原型 |
| 全局搜索 | 快捷键有,UI 无 | ✅ 补 `#searchModal` 原型 |
| AI 多会话 | 文档有,UI 无 | ✅ 补会话选择器规格 |
| AI 写作模式切换 | 文档有,UI 无 | ✅ 补 Segmented Control 规格 |
| 地标/待办 | 文档简略 | ✅ 扩展类型与工作流 |
| 大纲-正文对照 | 文档有,UI 无 | ✅ 补入口与布局规格 |
| 导入书籍 | 主页按钮有,模态无 | ✅ 补 `#importModal` 原型 |
| 更新计划/存稿 | 未覆盖 | ✅ 新增 20.1 |
| 章间衔接 | 未覆盖 | ✅ 新增 20.4 |
| 平台投稿 | 仅通用导出 | ✅ 新增 20.2 |
| 知识库审核流 | 概念有,UI 弱 | ✅ 补审核工作流 |
| POV/发表状态 | 未覆盖 | ✅ 新增章节元数据 5.2.4 |
| i18n 首版范围 | UI 含繁体选项 | ✅ UI 移除繁体,保持 zh-CN / en |
| 笔名编辑 | 仅 onboarding | ✅ 通用设置增加笔名字段 |
---
+191 -4
View File
@@ -1136,6 +1136,30 @@
font-family: var(--font-sans); font-family: var(--font-sans);
} }
/* ============ V1.1 NEW UI ============ */
.ai-session-bar { display:flex;align-items:center;gap:6px;padding:8px 10px;border-bottom:1px solid var(--border);flex-shrink:0; }
.ai-session-bar select { flex:1;font-size:11px;padding:4px 8px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px; }
.ai-mode-tabs { display:flex;gap:2px;padding:6px 8px;border-bottom:1px solid var(--border);flex-shrink:0; }
.ai-mode-tab { flex:1;padding:5px 4px;font-size:10px;text-align:center;border-radius:4px;cursor:pointer;color:var(--text-muted);background:transparent;border:none;transition:var(--transition); }
.ai-mode-tab.active { background:var(--accent);color:#fff;font-weight:600; }
.context-preview { padding:6px 10px;font-size:10px;color:var(--text-muted);border-bottom:1px solid var(--border);cursor:pointer;flex-shrink:0; }
.context-preview:hover { color:var(--accent-light); }
#reference-panel { width:0;overflow:hidden;transition:width var(--transition-slow);border-left:1px solid var(--border);background:var(--bg-secondary);display:flex;flex-direction:column;flex-shrink:0; }
#reference-panel.open { width:240px; }
.ref-tabs { display:flex;border-bottom:1px solid var(--border); }
.ref-tab { flex:1;padding:8px 4px;font-size:10px;text-align:center;cursor:pointer;color:var(--text-muted);border:none;background:transparent; }
.ref-tab.active { color:var(--accent-light);border-bottom:2px solid var(--accent); }
.ref-list { flex:1;overflow-y:auto;padding:6px; }
.ref-item { padding:8px;margin-bottom:4px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:11px;cursor:grab;color:var(--text-secondary); }
.ref-item:hover { background:var(--bg-hover);color:#fff; }
.search-input-row { display:flex;gap:8px;margin-bottom:12px; }
.search-input-row input { flex:1;padding:10px 14px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;font-size:13px; }
.search-result { padding:10px 12px;border-radius:var(--radius-sm);background:var(--bg-tertiary);margin-bottom:6px;cursor:pointer;font-size:12px; }
.search-result:hover { background:var(--bg-hover); }
.search-result .sr-type { font-size:10px;color:var(--accent-light);margin-bottom:4px; }
.search-result .sr-context { color:var(--text-muted);font-size:11px; }
.inspiration-capture textarea { width:100%;min-height:100px;padding:12px;border-radius:var(--radius-sm);border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;font-size:13px;resize:vertical;font-family:var(--font-sans); }
/* ============ MODALS ============ */ /* ============ MODALS ============ */
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
@@ -1764,15 +1788,17 @@
<button onclick="showTemplateModal()" aria-label="模板快速新建"><i class="fa-solid fa-file-lines"></i> 模板新建</button> <button onclick="showTemplateModal()" aria-label="模板快速新建"><i class="fa-solid fa-file-lines"></i> 模板新建</button>
<button onclick="addVolume()" aria-label="新建分卷"><i class="fa-solid fa-folder-plus"></i> 新卷</button> <button onclick="addVolume()" aria-label="新建分卷"><i class="fa-solid fa-folder-plus"></i> 新卷</button>
<button onclick="showVersionModal()" aria-label="版本历史"><i class="fa-solid fa-clock-rotate-left"></i> 版本</button> <button onclick="showVersionModal()" aria-label="版本历史"><i class="fa-solid fa-clock-rotate-left"></i> 版本</button>
<button onclick="showLandmarksModal()" aria-label="写作地标"><i class="fa-solid fa-map-pin"></i> 地标</button>
<button class="accent" onclick="enterReadingMode()" aria-label="阅读模式"><i class="fa-solid fa-book-open"></i> 阅读</button> <button class="accent" onclick="enterReadingMode()" aria-label="阅读模式"><i class="fa-solid fa-book-open"></i> 阅读</button>
</div> </div>
</div> </div>
<!-- 大纲面板 --> <!-- 大纲面板 -->
<div class="sidebar-panel" id="outline-panel" style="padding:4px;"> <div class="sidebar-panel" id="outline-panel" style="padding:4px;">
<div style="padding:4px 8px;display:flex;gap:4px;flex-shrink:0;"> <div style="padding:4px 8px;display:flex;gap:4px;flex-shrink:0;align-items:center;">
<button class="btn" style="font-size:10px;padding:3px 8px;background:var(--accent);color:#fff;">全部</button> <button class="btn" style="font-size:10px;padding:3px 8px;background:var(--accent);color:#fff;">全部</button>
<button class="btn" style="font-size:10px;padding:3px 8px;">未覆盖</button> <button class="btn" style="font-size:10px;padding:3px 8px;">未覆盖</button>
<button class="btn" style="font-size:10px;padding:3px 8px;">已覆盖</button> <button class="btn" style="font-size:10px;padding:3px 8px;">已覆盖</button>
<button class="btn" style="font-size:10px;padding:3px 8px;margin-left:auto;" onclick="showToast('进入大纲-正文对照模式')" title="大纲-正文对照">⇔ 对照</button>
</div> </div>
<div style="padding:4px 8px;font-size:10px;color:var(--text-muted);">大纲树(拖拽排序 · 点击关联章节)</div> <div style="padding:4px 8px;font-size:10px;color:var(--text-muted);">大纲树(拖拽排序 · 点击关联章节)</div>
<div class="outline-item uncovered" title="预期 3000字 vs 实际 0字 — 偏差100%">⚠ 第一卷核心剧情线</div> <div class="outline-item uncovered" title="预期 3000字 vs 实际 0字 — 偏差100%">⚠ 第一卷核心剧情线</div>
@@ -1838,6 +1864,7 @@
<span class="sep"></span> <span class="sep"></span>
<button class="tool-btn" id="btnSpellCheck" title="拼写检查" aria-label="拼写检查" onclick="toggleSpellCheck()"><i class="fa-solid fa-spell-check"></i></button> <button class="tool-btn" id="btnSpellCheck" title="拼写检查" aria-label="拼写检查" onclick="toggleSpellCheck()"><i class="fa-solid fa-spell-check"></i></button>
<button class="tool-btn" id="btnTTS" title="朗读 Ctrl+Shift+R" aria-label="朗读" onclick="toggleTTS()"><i class="fa-solid fa-headphones"></i></button> <button class="tool-btn" id="btnTTS" title="朗读 Ctrl+Shift+R" aria-label="朗读" onclick="toggleTTS()"><i class="fa-solid fa-headphones"></i></button>
<button class="tool-btn" id="btnReference" title="引用面板 Ctrl+Shift+P" aria-label="引用面板" onclick="toggleReferencePanel()"><i class="fa-solid fa-book-bookmark"></i></button>
<span class="sep"></span> <span class="sep"></span>
<span class="ch-label">当前:第3章 · 入门考验</span> <span class="ch-label">当前:第3章 · 入门考验</span>
<span class="word-count-display" id="wordCountDisplay" title="点击查看用词分析" onclick="showWordFreqInPanel()">2,847 字</span> <span class="word-count-display" id="wordCountDisplay" title="点击查看用词分析" onclick="showWordFreqInPanel()">2,847 字</span>
@@ -1869,6 +1896,22 @@
</div> </div>
</div> </div>
<!-- REFERENCE PANEL -->
<div id="reference-panel" aria-label="引用面板">
<div class="ref-tabs">
<button class="ref-tab active" onclick="switchRefTab('setting',this)">设定</button>
<button class="ref-tab" onclick="switchRefTab('outline',this)">大纲</button>
<button class="ref-tab" onclick="switchRefTab('inspiration',this)">灵感</button>
</div>
<div style="padding:6px 8px;border-bottom:1px solid var(--border);"><input type="text" placeholder="搜索引用..." style="width:100%;padding:6px 8px;font-size:11px;border-radius:4px;border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;"></div>
<div class="ref-list" id="refList">
<div class="ref-item" draggable="true" onclick="showToast('展开:林远')">👤 林远 — 三纹灵根,主角</div>
<div class="ref-item" draggable="true" onclick="showToast('展开:测灵石')">📍 测灵石 — 青云宗测试法器</div>
<div class="ref-item" draggable="true" onclick="showToast('展开:入门考验大纲')">🗺 入门考验 — 预期3000字</div>
</div>
<div style="padding:8px;border-top:1px solid var(--border);font-size:10px;color:var(--text-muted);"><label><input type="checkbox"> 钉住当前条目</label></div>
</div>
<!-- RIGHT PANEL --> <!-- RIGHT PANEL -->
<div id="right-panel"> <div id="right-panel">
<div class="panel-tabs"> <div class="panel-tabs">
@@ -1879,6 +1922,21 @@
</div> </div>
<!-- AI Chat Panel --> <!-- AI Chat Panel -->
<div class="panel-content active" id="ai-chat"> <div class="panel-content active" id="ai-chat">
<div class="ai-session-bar">
<select id="aiSessionSelect" aria-label="AI会话选择" onchange="showToast('切换会话:'+this.options[this.selectedIndex].text)">
<option>第3章续写讨论</option>
<option>角色命名 brainstorm</option>
<option>第二卷大纲规划</option>
</select>
<button class="btn" style="font-size:10px;padding:4px 8px;" onclick="showToast('新建AI会话')" aria-label="新建会话">+</button>
</div>
<div class="ai-mode-tabs" role="tablist">
<button class="ai-mode-tab active" onclick="setAiMode('chat',this)">对话</button>
<button class="ai-mode-tab" onclick="setAiMode('interactive',this)">交互写作</button>
<button class="ai-mode-tab" onclick="setAiMode('auto',this)">自动写作</button>
<button class="ai-mode-tab" onclick="setAiMode('wizard',this)">向导</button>
</div>
<div class="context-preview" onclick="showToast('展开上下文编辑器:第1-2章摘要、大纲3条、设定7个')" title="点击编辑上下文">📎 上下文:第1-2章摘要 · 大纲3条 · 设定7个 · <span style="color:var(--accent-light)">更新</span></div>
<div class="offline-banner" id="offlineBanner">⚠ 当前网络不可用,已切换为离线模式。请配置本地模型或连接网络。</div> <div class="offline-banner" id="offlineBanner">⚠ 当前网络不可用,已切换为离线模式。请配置本地模型或连接网络。</div>
<div class="chat-messages" id="chatMessages"> <div class="chat-messages" id="chatMessages">
<div class="chat-msg ai"> <div class="chat-msg ai">
@@ -1983,10 +2041,20 @@
<h2>⚙ 通用设置</h2> <h2>⚙ 通用设置</h2>
<div class="setting-block"> <div class="setting-block">
<h4>语言与外观</h4> <h4>语言与外观</h4>
<div class="setting-item"><span class="setting-label">界面语言</span><select><option>简体中文</option><option>繁體中文</option><option>English</option></select></div> <div class="setting-item"><span class="setting-label">界面语言</span><select aria-label="界面语言"><option>简体中文</option><option>English</option></select></div>
<div class="setting-item"><span class="setting-label">主题</span><select id="themeSelect" onchange="switchTheme(this.value)" aria-label="主题选择"><option value="default">墨砚(默认)</option><option value="bamboo">竹语</option><option value="moonlit">月夜</option><option value="ricepaper">宣纸</option><option value="mist">晨雾</option><option value="teagarden">茶园</option><option value="twilight">暮光</option></select></div> <div class="setting-item"><span class="setting-label">主题</span><select id="themeSelect" onchange="switchTheme(this.value)" aria-label="主题选择"><option value="default">墨砚(默认)</option><option value="bamboo">竹语</option><option value="moonlit">月夜</option><option value="ricepaper">宣纸</option><option value="mist">晨雾</option><option value="teagarden">茶园</option><option value="twilight">暮光</option></select></div>
<div class="setting-item"><span class="setting-label">字体大小</span><select><option></option><option></option><option></option></select></div> <div class="setting-item"><span class="setting-label">字体大小</span><select><option></option><option></option><option></option></select></div>
</div> </div>
<div class="setting-block">
<h4>作者信息</h4>
<div class="setting-item"><span class="setting-label">笔名</span><input type="text" value="山海" placeholder="至少2个字符" style="width:160px;" aria-label="笔名"></div>
</div>
<div class="setting-block">
<h4>写作目标</h4>
<div class="setting-item"><span class="setting-label">日更字数目标</span><input type="number" value="3000" min="0" step="500" aria-label="日更目标"></div>
<div class="setting-item"><span class="setting-label">更新频率</span><select aria-label="更新频率"><option>日更</option><option>隔日更</option><option>周更</option><option>不设定</option></select></div>
<div class="setting-item"><span class="setting-label">存稿预警阈值</span><input type="number" value="3" min="1" max="30" aria-label="存稿阈值"></div>
</div>
<div class="setting-block"> <div class="setting-block">
<h4>默认行为</h4> <h4>默认行为</h4>
<div class="setting-item"><span class="setting-label">启动时打开上次书籍</span><select><option></option><option>否(显示主页)</option></select></div> <div class="setting-item"><span class="setting-label">启动时打开上次书籍</span><select><option></option><option>否(显示主页)</option></select></div>
@@ -2001,6 +2069,7 @@
<div class="setting-item"><span class="setting-label">默认字体</span><select><option>宋体</option><option>楷体</option><option>微软雅黑</option></select></div> <div class="setting-item"><span class="setting-label">默认字体</span><select><option>宋体</option><option>楷体</option><option>微软雅黑</option></select></div>
<div class="setting-item"><span class="setting-label">行距</span><select><option>1.8</option><option>2.0</option><option>2.2</option></select></div> <div class="setting-item"><span class="setting-label">行距</span><select><option>1.8</option><option>2.0</option><option>2.2</option></select></div>
<div class="setting-item"><span class="setting-label">专注模式背景</span><select><option>纯黑</option><option>深灰</option><option>渐变</option></select></div> <div class="setting-item"><span class="setting-label">专注模式背景</span><select><option>纯黑</option><option>深灰</option><option>渐变</option></select></div>
<div class="setting-item"><span class="setting-label">合规检查(敏感词提示)</span><select><option>关闭</option><option>开启</option></select></div>
</div> </div>
</div> </div>
<!-- AI设置 --> <!-- AI设置 -->
@@ -2031,6 +2100,8 @@
<div class="setting-item"><span class="setting-label">全局搜索</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+F</span></div> <div class="setting-item"><span class="setting-label">全局搜索</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+F</span></div>
<div class="setting-item"><span class="setting-label">朗读 播放/暂停</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+R</span></div> <div class="setting-item"><span class="setting-label">朗读 播放/暂停</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+R</span></div>
<div class="setting-item"><span class="setting-label">灵感快速捕获</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+I</span></div> <div class="setting-item"><span class="setting-label">灵感快速捕获</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+I</span></div>
<div class="setting-item"><span class="setting-label">引用面板</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+P</span></div>
<div class="setting-item"><span class="setting-label">写作地标</span><span class="shortcut-key" onclick="recordShortcut(this)">Ctrl+Shift+M</span></div>
</div> </div>
</div> </div>
<!-- 备份与同步 --> <!-- 备份与同步 -->
@@ -2171,6 +2242,11 @@
<div style="margin-top:16px;"><h4 style="font-size:12px;color:var(--text-muted);margin-bottom:8px;">📅 写作热力图(近30天)</h4> <div style="margin-top:16px;"><h4 style="font-size:12px;color:var(--text-muted);margin-bottom:8px;">📅 写作热力图(近30天)</h4>
<div style="display:flex;flex-wrap:wrap;gap:2px;" id="heatmapContainer"></div> <div style="display:flex;flex-wrap:wrap;gap:2px;" id="heatmapContainer"></div>
</div> </div>
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap;">
<button class="btn primary" style="font-size:11px;" onclick="closeModal('dashboardModal');showToast('回到上次编辑位置')">↩ 回到上次编辑</button>
<button class="btn" style="font-size:11px;" onclick="closeModal('dashboardModal');showBridgeModal()">🔗 检查章间衔接</button>
<button class="btn" style="font-size:11px;" onclick="closeModal('dashboardModal');switchRightPanel('knowledge',document.querySelector('.panel-tab:nth-child(2)'))">⚠ 处理遗忘伏笔</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -2222,6 +2298,7 @@
<div class="export-opt" onclick="showToast('导出为 .docx');closeModal('exportModal')"><span class="export-icon">📝</span><div class="export-info"><span class="export-name">Word 文档 .docx</span><span class="export-desc">保留格式,自动生成目录</span></div></div> <div class="export-opt" onclick="showToast('导出为 .docx');closeModal('exportModal')"><span class="export-icon">📝</span><div class="export-info"><span class="export-name">Word 文档 .docx</span><span class="export-desc">保留格式,自动生成目录</span></div></div>
<div class="export-opt" onclick="showToast('导出为 .md');closeModal('exportModal')"><span class="export-icon">📋</span><div class="export-info"><span class="export-name">Markdown .md</span><span class="export-desc">适合发布到网络平台</span></div></div> <div class="export-opt" onclick="showToast('导出为 .md');closeModal('exportModal')"><span class="export-icon">📋</span><div class="export-info"><span class="export-name">Markdown .md</span><span class="export-desc">适合发布到网络平台</span></div></div>
<div class="export-opt" onclick="showToast('导出为 PDF (A4模板)');closeModal('exportModal')"><span class="export-icon">📕</span><div class="export-info"><span class="export-name">PDF 文档 .pdf</span><span class="export-desc">A4/信纸排版,页眉含书名+章节名+页码</span></div></div> <div class="export-opt" onclick="showToast('导出为 PDF (A4模板)');closeModal('exportModal')"><span class="export-icon">📕</span><div class="export-info"><span class="export-name">PDF 文档 .pdf</span><span class="export-desc">A4/信纸排版,页眉含书名+章节名+页码</span></div></div>
<div class="export-opt" onclick="showToast('已复制到剪贴板(网文投稿格式)');closeModal('exportModal')"><span class="export-icon">📋</span><div class="export-info"><span class="export-name">复制到剪贴板</span><span class="export-desc">网文投稿预设:段首空两格、含章节标题</span></div></div>
<div class="export-opt" onclick="showToast('打包为 .novel 项目包');closeModal('exportModal')"><span class="export-icon">📦</span><div class="export-info"><span class="export-name">笔临项目包 .novel</span><span class="export-desc">整书数据打包,含版本历史+设定+大纲</span></div></div> <div class="export-opt" onclick="showToast('打包为 .novel 项目包');closeModal('exportModal')"><span class="export-icon">📦</span><div class="export-info"><span class="export-name">笔临项目包 .novel</span><span class="export-desc">整书数据打包,含版本历史+设定+大纲</span></div></div>
</div></div> </div></div>
</div></div> </div></div>
@@ -2244,6 +2321,83 @@
<div class="modal-footer"><button class="btn" onclick="closeModal('templateModal')">取消</button><button class="btn primary" onclick="applyTemplate('generic');closeModal('templateModal')">创建章节</button></div> <div class="modal-footer"><button class="btn" onclick="closeModal('templateModal')">取消</button><button class="btn primary" onclick="applyTemplate('generic');closeModal('templateModal')">创建章节</button></div>
</div></div> </div></div>
<!-- Global Search -->
<div class="modal-overlay hidden" id="searchModal" role="dialog" aria-modal="true" aria-label="全局搜索">
<div class="modal" style="max-width:680px;">
<div class="modal-header"><h3>🔍 全局搜索</h3><button class="modal-close" onclick="closeModal('searchModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<div class="search-input-row">
<input type="text" id="searchInput" placeholder="搜索章节、大纲、设定、灵感、地标..." autofocus>
<select style="padding:8px;font-size:11px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px;" aria-label="搜索范围"><option>全书</option><option>当前卷</option><option>当前章</option></select>
</div>
<div style="display:flex;gap:8px;margin-bottom:12px;font-size:11px;">
<label><input type="checkbox"> 正则</label><label><input type="checkbox"> 全词匹配</label><label><input type="checkbox"> 区分大小写</label>
</div>
<div id="searchResults">
<div class="search-result" onclick="showToast('跳转到第3章');closeModal('searchModal')"><div class="sr-type">章节 · 第3章 入门考验</div><div>...林远深吸一口气,推开木门走了出去。门外是一个宽阔的<b>演武场</b>...</div></div>
<div class="search-result" onclick="showToast('跳转到设定:测灵石');closeModal('searchModal')"><div class="sr-type">设定 · 测灵石</div><div class="sr-context">青云宗测试弟子灵根的法器,可显现一至五纹...</div></div>
<div class="search-result" onclick="showToast('跳转到地标');closeModal('searchModal')"><div class="sr-type">地标 · 第5章 TODO</div><div class="sr-context">[TODO: 补写林远与长老的私下对话]</div></div>
</div>
</div>
<div class="modal-footer"><button class="btn" onclick="closeModal('searchModal')">关闭</button><button class="btn" onclick="showToast('展开替换预览')">替换...</button></div>
</div>
</div>
<!-- Import Book -->
<div class="modal-overlay hidden" id="importModal" role="dialog" aria-modal="true" aria-label="导入书籍">
<div class="modal" style="max-width:520px;">
<div class="modal-header"><h3>📥 导入书籍</h3><button class="modal-close" onclick="closeModal('importModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<p style="color:var(--text-secondary);font-size:12px;margin-bottom:12px;">支持 .txt、.docx、.md 及笔临专用 .novel 项目包</p>
<div class="form-group"><label>选择文件</label><button class="btn primary" style="width:100%;padding:12px;" onclick="showToast('打开文件选择器...')"><i class="fa-solid fa-folder-open"></i> 浏览文件...</button></div>
<div class="form-group"><label>章节分隔规则</label><select style="width:100%;padding:8px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px;"><option>自动识别(# 第.*章)</option><option>双换行分段</option><option>自定义正则</option></select></div>
<p style="font-size:11px;color:var(--text-muted);margin-top:8px;">大于 20MB 的文件将提示确认,导入过程显示进度条。</p>
</div>
<div class="modal-footer"><button class="btn" onclick="closeModal('importModal')">取消</button><button class="btn primary" onclick="showToast('📥 开始导入...');closeModal('importModal')">开始导入</button></div>
</div>
</div>
<!-- Inspiration Capture -->
<div class="modal-overlay hidden" id="inspirationModal" role="dialog" aria-modal="true" aria-label="灵感快速捕获">
<div class="modal" style="max-width:480px;">
<div class="modal-header"><h3>💡 灵感快速捕获</h3><button class="modal-close" onclick="closeModal('inspirationModal')" aria-label="关闭"></button></div>
<div class="modal-body inspiration-capture">
<textarea id="inspirationInput" placeholder="记录一闪而过的想法..."></textarea>
<div class="form-group" style="margin-top:10px;"><label>标签(可选)</label><input type="text" placeholder="如:伏笔、第三卷、角色" style="width:100%;padding:8px;border-radius:4px;border:1px solid var(--border);background:var(--bg-tertiary);color:#fff;"></div>
<div class="form-group"><label>关联书籍</label><select style="width:100%;padding:8px;background:var(--bg-tertiary);color:#fff;border:1px solid var(--border);border-radius:4px;"><option>仙道长青(当前)</option><option>不关联书籍</option></select></div>
</div>
<div class="modal-footer"><button class="btn" onclick="showToast('🎤 语音输入...')"><i class="fa-solid fa-microphone"></i> 语音</button><button class="btn" onclick="closeModal('inspirationModal')">取消</button><button class="btn primary" onclick="saveInspiration()">保存</button></div>
</div>
</div>
<!-- Landmarks List -->
<div class="modal-overlay hidden" id="landmarksModal" role="dialog" aria-modal="true" aria-label="写作地标清单">
<div class="modal" style="max-width:560px;">
<div class="modal-header"><h3>📌 写作地标与待办</h3><button class="modal-close" onclick="closeModal('landmarksModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<div style="display:flex;gap:6px;margin-bottom:12px;"><button class="btn" style="font-size:10px;background:var(--accent);color:#fff;">全部</button><button class="btn" style="font-size:10px;">待办</button><button class="btn" style="font-size:10px;">伏笔</button><button class="btn" style="font-size:10px;">待查证</button></div>
<div class="search-result" onclick="showToast('跳转到第5章地标');closeModal('landmarksModal')"><div class="sr-type">TODO · 第5章</div><div>补写林远与长老的私下对话</div></div>
<div class="search-result" onclick="showToast('跳转到第3章');closeModal('landmarksModal')"><div class="sr-type">伏笔提醒 · 第3章</div><div>测灵石异常发热 — 需在后续章节回收</div></div>
</div>
<div class="modal-footer"><button class="btn" onclick="closeModal('landmarksModal')">关闭</button><button class="btn primary" onclick="showToast('插入新地标');closeModal('landmarksModal')">+ 插入地标</button></div>
</div>
</div>
<!-- Chapter Bridge (衔接检查) -->
<div class="modal-overlay hidden" id="bridgeModal" role="dialog" aria-modal="true" aria-label="章间衔接检查">
<div class="modal" style="max-width:600px;">
<div class="modal-header"><h3>🔗 章间衔接 — 第2章 → 第3章</h3><button class="modal-close" onclick="closeModal('bridgeModal')" aria-label="关闭"></button></div>
<div class="modal-body">
<h4 style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">上一章末尾(第2章)</h4>
<div style="padding:12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:12px;line-height:1.7;margin-bottom:12px;border-left:3px solid var(--accent);">...三纹灵根觉醒的异象尚未散去,林远站在原地,感受着体内翻涌的灵力。他知道,从这一刻起,一切都将不同。</div>
<h4 style="font-size:12px;color:var(--text-muted);margin-bottom:6px;">本章已写开头(第3章)</h4>
<div style="padding:12px;background:var(--bg-tertiary);border-radius:var(--radius-sm);font-size:12px;line-height:1.7;margin-bottom:12px;">晨光透过稀疏的竹帘,在青石地面上投下细碎的光斑。林远深吸一口气,推开木门走了出去...</div>
<div style="padding:10px;background:var(--accent-glow);border-radius:var(--radius-sm);font-size:11px;color:var(--text-secondary);">💡 AI 建议:上一章以「一切都将不同」收束,本章可从时间跳转(数日后抵达宗门)或接续同一日清晨出发,避免重复「深吸一口气」式开头。</div>
</div>
<div class="modal-footer"><label style="font-size:11px;color:var(--text-muted);margin-right:auto;"><input type="checkbox"> 本章不再提示</label><button class="btn primary" onclick="closeModal('bridgeModal')">开始写作</button></div>
</div>
</div>
<!-- Onboarding Wizard --> <!-- Onboarding Wizard -->
<div class="modal-overlay hidden" id="onboardingModal"><div class="modal" style="max-width:560px;"> <div class="modal-overlay hidden" id="onboardingModal"><div class="modal" style="max-width:560px;">
<div class="modal-header"><h3>✒ 欢迎使用笔临</h3></div> <div class="modal-header"><h3>✒ 欢迎使用笔临</h3></div>
@@ -2430,7 +2584,36 @@
showToast('✅ 书籍已创建:' + name); showToast('✅ 书籍已创建:' + name);
} }
function showImportModal() { showToast('📥 导入功能:支持.txt/.docx/.md及.novel项目包。大于20MB会提示确认。'); } function showImportModal() { document.getElementById('importModal').classList.remove('hidden'); }
function showSearchModal() { document.getElementById('searchModal').classList.remove('hidden'); setTimeout(function() { var i = document.getElementById('searchInput'); if (i) i.focus(); }, 100); }
function showInspirationModal() { document.getElementById('inspirationModal').classList.remove('hidden'); setTimeout(function() { var i = document.getElementById('inspirationInput'); if (i) i.focus(); }, 100); }
function showLandmarksModal() { document.getElementById('landmarksModal').classList.remove('hidden'); }
function showBridgeModal() { document.getElementById('bridgeModal').classList.remove('hidden'); }
function saveInspiration() {
var t = document.getElementById('inspirationInput').value.trim();
if (!t) { showToast('请输入灵感内容'); return; }
closeModal('inspirationModal');
document.getElementById('inspirationInput').value = '';
showToast('💡 灵感已保存');
}
function toggleReferencePanel() {
var panel = document.getElementById('reference-panel');
var btn = document.getElementById('btnReference');
panel.classList.toggle('open');
if (btn) btn.classList.toggle('active', panel.classList.contains('open'));
showToast(panel.classList.contains('open') ? '📎 引用面板已打开' : '引用面板已关闭');
}
function switchRefTab(type, el) {
document.querySelectorAll('.ref-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
showToast('引用:' + (type === 'setting' ? '设定' : type === 'outline' ? '大纲' : '灵感'));
}
function setAiMode(mode, el) {
document.querySelectorAll('.ai-mode-tab').forEach(function(t) { t.classList.remove('active'); });
el.classList.add('active');
var labels = { chat: '对话', interactive: '交互写作', auto: '自动写作', wizard: '向导模式' };
showToast('AI 模式:' + (labels[mode] || mode));
}
// ============ SIDEBAR ============ // ============ SIDEBAR ============
function switchSidebarPanel(panelId, btn) { function switchSidebarPanel(panelId, btn) {
@@ -2604,7 +2787,11 @@
if (e.ctrlKey && e.altKey && e.key === 'f') { e.preventDefault(); toggleFocusMode(); } if (e.ctrlKey && e.altKey && e.key === 'f') { e.preventDefault(); toggleFocusMode(); }
if (e.ctrlKey && e.shiftKey && e.key === 'R') { e.preventDefault(); toggleTTS(); } if (e.ctrlKey && e.shiftKey && e.key === 'R') { e.preventDefault(); toggleTTS(); }
if (e.ctrlKey && e.shiftKey && e.key === 'N') { e.preventDefault(); addChapter(); } if (e.ctrlKey && e.shiftKey && e.key === 'N') { e.preventDefault(); addChapter(); }
if (e.ctrlKey && e.shiftKey && e.key === 'I') { e.preventDefault(); showToast('💡 灵感快速捕获窗已打开'); } if (e.ctrlKey && e.shiftKey && e.key === 'F') { e.preventDefault(); showSearchModal(); }
if (e.ctrlKey && e.shiftKey && e.key === 'I') { e.preventDefault(); showInspirationModal(); }
if (e.ctrlKey && e.shiftKey && e.key === 'P') { e.preventDefault(); toggleReferencePanel(); }
if (e.ctrlKey && e.shiftKey && e.key === 'M') { e.preventDefault(); showLandmarksModal(); }
if (e.ctrlKey && e.shiftKey && e.key === 'L') { e.preventDefault(); showLandmarksModal(); }
}); });
// ============ V2.0 NEW FUNCTIONS ============ // ============ V2.0 NEW FUNCTIONS ============
@@ -0,0 +1,550 @@
# 笔临 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 HandlersIpcResult 信封) │
│ · 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 技术栈
| 层次 | 技术 |
|------|------|
| 桌面框架 | Electronelectron-vite 脚手架) |
| 前端 | React 18、TypeScript strict |
| 编辑器 | TipTap (ProseMirror) |
| 数据库 | better-sqlite3 |
| 全局状态 | Zustand |
| 编辑器状态 | Jotai |
| 交互组件 | Radix UIDialog、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 负责 SQLIPC 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<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 形状
```typescript
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
```typescript
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
```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 或在本文档增量修订。*