Files
bilin/docs/superpowers/specs/2026-07-07-bilin-p21-p3-design.md
T
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

19 KiB
Raw Blame History

笔临 P2.1 + P3 设计规格

版本1.0
日期2026-07-07
范围:P2 遗留补完 + P3 AI 基础(严格 P3-A:对话模式实装,写作模式 Tab 占位)
前置P2 已交付(v0.2.0aac51bf
参考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 StudioOpenAI 兼容 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
  • 超时默认 60sGlobalSettings.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[]
}

全局 userDataglobal_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 流程

  1. 持久化 user message
  2. AiContextBuilder 组装 messages 数组(system + 历史 + user
  3. 调用 AiClientService.chatStream(...)
  4. 流式推送 ai:streamChunk;完成后持久化 assistant message

5. P2.1 功能规格

5.1 章节拖拽排序

UI:左栏章节列表项 draggable;拖拽指示线(同卷内)。

逻辑

  • 同卷:重算 sort_order0..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 底部替换区。

UISearchModal 扩展):

  • 折叠区「替换」:替换文本输入 + 「预览变更」+ 「执行替换」
  • 预览调用 search:replace(dryRun: true) → 列表展示受影响条目
  • 确认后 search:replace(dryRun: false) → toast 显示替换数量
  • 替换后刷新 useBookStore 章节列表;若当前章被替换则刷新编辑器

范围:P2 已支持全书;本阶段补 UI,不扩展 scope select(可后续加)。

5.3 灵感快速捕获

UI#inspirationModal(对齐 ui_pc.html

  • 标题、内容、标签(逗号分隔)
  • 语音按钮:webkitSpeechRecognitionzh-CN),实时填入内容框
  • 保存 → inspiration:create + 可选 switchTarget
  • 不打断编辑器:关闭后焦点回 .ProseMirror

快捷键Ctrl+Shift+IcaptureInspiration 已有 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 === truesetOpen(false) 不隐藏面板
  • 与 AI 右栏互斥:引用打开时仍隐藏 RightPanel(延续 P2);钉住引用 + 需 AI 时用户手动关闭引用

6. P3 功能规格

6.1 AI 面板 UIAiPanel.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() + 定时探测(可选 HEADhttps://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 顶部按钮「命名一致性检查」

流程

  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
  • i18nai.*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 实装