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>
19 KiB
笔临 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 优先(延续 P0–P2) |
1.3 验收标准
用户能完成:
- 左栏章节同卷/跨卷拖拽排序,重启后顺序保持
- 全局搜索:输入关键词 → 预览替换影响范围 → 确认后批量替换章节内容
Ctrl+Shift+I打开灵感捕获模态,文字/语音输入后保存到灵感列表- 点击正文中
@设定名跳转到对应设定编辑 - 引用面板钉住后保持可见,可与 AI 右栏互斥规则共存
- 设置页配置 LM Studio 地址与模型;右栏 AI 面板新建会话、发送消息、流式收到回复
- 勾选章节/大纲/设定/灵感作为上下文,点击「更新上下文」后下一轮对话携带最新摘要
- 使用
/续写等快捷命令触发预设 prompt - 云端后端配置下断网时 AI 输入禁用并显示离线横幅;LM Studio 配置下断网仍可用
- 知识库 Tab 运行命名一致性检查,逐条确认后可批量替换
- 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(每书 .sqlite,schema v3) │
│ · AiClientService → fetch LM Studio /v1/chat/completions│
│ · AiSessionRepository / AiContextBuilder │
│ · NetworkMonitor → 推送 online/offline │
│ · 既有 FTS / Search / Snapshot / Chapter 服务 │
│ · IPC Handlers(IpcResult 信封 + ai:stream-chunk 事件) │
└───────────────────────────┬─────────────────────────────┘
│ contextBridge
┌───────────────────────────▼─────────────────────────────┐
│ 渲染进程 │
│ · AiPanel(替换 RightPanel AI 占位) │
│ · ContextEditorModal / NamingCheckModal │
│ · InspirationModal(全局快捷键) │
│ · useAiStore(会话、流式缓冲、disabled) │
│ · SearchModal 替换区 / 章节 drag-and-drop │
└─────────────────────────────────────────────────────────┘
约束(延续 P0–P2):
- 禁止渲染进程直接
fetchAI 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;解析 SSEdata: {...}行 AbortController支持ai:abort- 超时默认 60s(
GlobalSettings.aiConfig.timeoutMs) - 错误映射为
IpcError,含recoverable: true(超时/连接拒绝)
默认配置(global_settings.json):
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 扩展。
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 结构:
interface AiContextBinding {
chapterIds: string[]
outlineIds: string[]
settingIds: string[]
inspirationIds: string[]
}
全局 userData(global_settings.json 扩展,非书籍 DB):
interface GlobalSettings {
// ...existing P2 fields
aiConfig: AiConfig
namingIgnoreWords: string[]
}
3.2 共享类型扩展(src/shared/types.ts)
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)
// 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 流程:
- 持久化 user message
AiContextBuilder组装 messages 数组(system + 历史 + user)- 调用
AiClientService.chatStream(...) - 流式推送
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:
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 扩展:
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
渲染进程规则:
const disabled =
settings.aiConfig.backend !== 'lmstudio' && !networkOnline
disabled时:输入框disabled,快捷命令灰显,横幅可见lmstudio后端:永不因离线禁用- 网络恢复:toast
ai.networkRestored
6.5 命名一致性检查(§5.6.2)
入口:知识库 Tab 顶部按钮「命名一致性检查」
流程:
- 收集
settings[].name+ 全书章节 plainText - 构造 prompt,要求模型返回 纯 JSON 数组
NamingConflict[] NamingCheckModal列表:termA / termB / 置信度 / 原因- 操作:忽略(加入
namingIgnoreWords)、替换为 canonical(调search:replace) - 解析失败: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 |
| 服务地址 | text(LM 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 手动验收
- LM Studio 运行
gemma-4-e4b-it,笔临设置页「测试连接」成功 - 右栏多轮对话,重启后会话与消息保留
- 上下文勾选后 AI 回答能引用设定名称
- 命名检查对「林远/林悦」类冲突给出建议
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行 1203–2076 结构与 class 命名 - 灵感模态对齐
#inspirationModal - i18n:
ai.*、naming.*、inspiration.voice*等键
9. 风险与缓解
| 风险 | 缓解 |
|---|---|
| LM Studio 未启动导致 CI 失败 | 文档明确前置条件;本地开发脚本检测端口 |
| 流式 IPC 消息乱序 | messageId 关联;渲染进程按 id 缓冲 |
| 大上下文超 token | 硬限制 16KB + 截断策略 |
| 命名检查 JSON 解析失败 | 重试一次;失败 toast + 日志 |
| Web Speech API 不可用 | 语音按钮 disabled + tooltip;E2E 不测语音 |
gemma-4-e4b-it 模型名变更 |
环境变量 BILIN_AI_MODEL 覆盖 |
10. Definition of Done(v0.3.0)
- P2.1a–c + P3.0–3 全部里程碑完成
- §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 plan;P2.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 实装 |