Files
bilin/docs/superpowers/plans/2026-07-06-bilin-p2.md
T
bing 91c93954df docs: 添加笔临 P2 实现计划
18 个 Task 覆盖主题修复、schema v2、统一编辑、引用/搜索/版本/地标/用词分析至 v0.2.0。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-06 10:50:52 +08:00

21 KiB
Raw Blame History

笔临 P2 Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 交付 v0.2.0:大纲/设定/灵感持久化、统一中央编辑、引用面板、版本历史、FTS 全局搜索、地标、用词分析,并修复 3 套浅色主题对比度问题。

Architecture: 延续 electron-vite 单体仓库;主进程 node:sqlite schema v2 + FTS5 + Snapshot/Search/WordFreq 服务;渲染进程新增 useEditStoreEditTarget)统一 TipTap 编辑;IPC 延续 IpcResult 信封;UI 对齐 design/ui_pc.html 模态与引用面板。

Tech Stack: Electron 36、electron-vite、React 18、TypeScript、TipTap、node:sqlite、Zustand、Jotai、Radix UI、i18next、diff-match-patch、Vitest、Playwright

Spec: docs/superpowers/specs/2026-07-06-bilin-p2-design.md


File Map(P2 新增/修改核心文件)

文件 职责
src/renderer/styles/themes.css 补全浅色 token、--text-on-accent--input-*
src/main/db/schema-v2.sql v2 表 + FTS5
src/main/db/migrate.ts version 1→2 增量迁移
src/main/db/repositories/outline.repo.ts 大纲 CRUD/树/move
src/main/db/repositories/setting.repo.ts 设定 CRUD + chapterRefs
src/main/db/repositories/inspiration.repo.ts 灵感 CRUD/convert
src/main/db/repositories/snapshot.repo.ts 快照 CRUD/FIFO
src/main/db/repositories/bookmark.repo.ts 地标 CRUD
src/main/services/fts-sync.service.ts FTS5 增量同步
src/main/services/search.service.ts 查询 + replace dryRun
src/main/services/snapshot.service.ts 定时/退出快照
src/main/services/wordfreq.service.ts 词频统计
src/shared/types.ts P2 类型扩展
src/shared/ipc-channels.ts P2 IPC 常量
src/renderer/stores/useEditStore.ts EditTarget 切换
src/renderer/components/outline/OutlineTree.tsx 大纲树 UI
src/renderer/components/reference/ReferencePanel.tsx 引用面板
src/renderer/components/search/SearchModal.tsx 全局搜索
src/renderer/components/version/VersionModal.tsx 版本历史 + diff
src/renderer/components/landmark/LandmarkModal.tsx 地标清单
src/renderer/components/wordfreq/WordFreqPanel.tsx 用词分析
e2e/themes-contrast.spec.ts 浅色主题对比度 E2E

Task 1: P2.0 主题 Token 修复

Files:

  • Modify: src/renderer/styles/themes.css

  • Modify: src/renderer/styles/layout.css

  • Modify: src/renderer/styles/globals.css

  • Modify: src/renderer/components/settings/SettingsPage.tsx

  • Create: e2e/themes-contrast.spec.ts

  • Step 1: 在 :root 新增语义 token

/* themes.css :root 追加 */
--text-on-accent: #ffffff;
--input-text: var(--text-primary);
--input-bg: var(--bg-surface);
--input-border: var(--border);

[data-theme='ricepaper']mistteagarden 各补全:--bg-surface--bg-active--text-dim--accent-glow--border-light--green--orange--red

  • Step 2: 替换硬编码 #fff

layout.css 中将 .btn-large.primary.tab.activecolor: #fff 改为 color: var(--text-on-accent)

浅色主题覆盖:

[data-theme='ricepaper'],
[data-theme='mist'],
[data-theme='teagarden'] {
  --text-on-accent: #ffffff;
}
  • Step 3: 统一表单控件样式

globals.css 追加:

input, select, textarea {
  color: var(--input-text);
  background: var(--input-bg);
  border: 1px solid var(--input-border);
}

删除 SettingsPage.tsx 内联 color: inherit / 硬编码 border,改用 class form-control(在 globals 定义)。

  • Step 4: 写 contrast E2E

e2e/themes-contrast.spec.ts:对 ricepaper/mist/teagarden,在主页、设置页、编辑器断言 #apph1,h2,button,.setting-item 可见(toBeVisible)且 computed color 非透明/非背景同色(用 page.evaluate 比较 RGB)。

  • Step 5: 运行验证
npm run build
npm run test:e2e -- e2e/themes-contrast.spec.ts

Expected: PASS

  • Step 6: Commit
git add src/renderer/styles e2e/themes-contrast.spec.ts src/renderer/components/settings/SettingsPage.tsx
git commit -m "fix: repair light theme tokens and contrast"

Task 2: 共享类型与 IPC 通道

Files:

  • Modify: src/shared/types.ts

  • Modify: src/shared/ipc-channels.ts

  • Modify: src/main/services/global-settings.ts

  • Step 1: 扩展 types.ts

追加(完整定义见 spec §3.2):

export type OutlineStatus = 'pending' | 'writing' | 'done' | 'abandoned'
export type SettingType = 'character' | 'location' | 'item' | 'skill' | 'faction' | 'custom'
export type SnapshotType = 'auto' | 'manual' | 'persist' | 'ai'
export type LandmarkType = 'todo' | 'foreshadow' | 'research' | 'custom'
export type EditTargetKind = 'chapter' | 'outline' | 'setting' | 'inspiration'
export type EditTarget = { kind: EditTargetKind; id: string }
export type SearchScope = 'all' | 'volume' | 'chapter'
export type SearchSourceKind = 'chapter' | 'outline' | 'setting' | 'inspiration' | 'bookmark'

export interface OutlineItem { /* spec §3.2 字段 */ }
export interface SettingEntry { /* ... */ }
export interface InspirationEntry { /* ... */ }
export interface Snapshot { /* ... */ }
export interface Bookmark { /* ... */ }
export interface SearchResult {
  kind: SearchSourceKind
  id: string
  title: string
  snippet: string
  chapterId?: string
  position?: number
}

扩展 GlobalSettings

snapshotIntervalMs: number
snapshotMaxAuto: number
snapshotMaxPersist: number
searchHistory: string[]

扩展 BookOpenResultoutlines, settings, inspirations

  • Step 2: 扩展 ipc-channels.ts
OUTLINE_LIST: 'outline:list',
OUTLINE_CREATE: 'outline:create',
// ... 全部见 spec §4.1
SEARCH_QUERY: 'search:query',
WORDFREQ_ANALYZE: 'wordfreq:analyze',
  • Step 3: global-settings defaults
function defaults(): GlobalSettings {
  return {
    // ...existing
    snapshotIntervalMs: 300_000,
    snapshotMaxAuto: 20,
    snapshotMaxPersist: 3,
    searchHistory: []
  }
}
  • Step 4: Commit
git commit -am "feat: add P2 shared types and IPC channels"

Task 3: Schema v2 迁移

Files:

  • Create: src/main/db/schema-v2.sql

  • Modify: src/main/db/migrate.ts

  • Create: tests/main/migrate-v2.test.ts

  • Step 1: 写 schema-v2.sql

包含 spec §3.1 全部 CREATE TABLE + FTS5

CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
  source_kind, source_id, title, body, tokenize='unicode61'
);
  • Step 2: 更新 migrate.ts
import schemaV2 from './schema-v2.sql?raw'

const CURRENT_VERSION = 2

export function migrate(db: SqliteDb): void {
  // ... v1 逻辑保留
  if (current >= CURRENT_VERSION) return
  if (current < 1) { /* apply schemaV1 */ }
  if (current < 2) {
    db.exec(schemaV2)
    db.prepare('INSERT INTO schema_version (version, description) VALUES (?, ?)').run(2, 'P2 schema')
  }
}

v2 SQL 中对已有库使用 ALTER TABLE chapters ADD COLUMN ... 需单独 schema-v2-alter.sql 或在 migrate 中 try/catch 忽略 duplicate column。

  • Step 3: 写 migrate 测试
// tests/main/migrate-v2.test.ts
it('migrates v1 database to v2 with new tables', () => {
  const db = new DatabaseSync(':memory:')
  // apply v1 only, then migrate full
  expect(db.prepare("SELECT name FROM sqlite_master WHERE name='outline_items'").get()).toBeTruthy()
})
  • Step 4: 运行
npm run test -- tests/main/migrate-v2.test.ts
  • Step 5: Commit
git commit -am "feat: add schema v2 migration"

Task 4: Outline Repository

Files:

  • Create: src/main/db/repositories/outline.repo.ts

  • Create: tests/main/outline.repo.test.ts

  • Step 1: 写失败测试

it('creates nested outline items', () => {
  const parent = repo.create(null, '第一幕', 0)
  const child = repo.create(parent.id, '开端', 0)
  expect(repo.listTree()).toHaveLength(1)
  expect(repo.listTree()[0].children).toHaveLength(1)
})
  • Step 2: 实现 outline.repo.ts

方法:create(parentId, title, sortOrder)getlistFlatupdatedelete(级联)、movelistUncovered()

  • Step 3: 运行 PASS + Commit
git commit -am "feat: add outline repository"

Task 5: Setting + Inspiration Repository

Files:

  • Create: src/main/db/repositories/setting.repo.ts

  • Create: src/main/db/repositories/inspiration.repo.ts

  • Create: tests/main/setting.repo.test.ts

  • Create: tests/main/inspiration.repo.test.ts

  • Step 1: setting.repo — CRUD + syncChapterRefs(settingId, chapterIds[])

  • Step 2: inspiration.repo — CRUD + convertToOutline(id) / convertToSetting(id, type)

  • Step 3: 测试 PASS + Commit

git commit -am "feat: add setting and inspiration repositories"

Task 6: Snapshot + Bookmark Repository

Files:

  • Create: src/main/db/repositories/snapshot.repo.ts

  • Create: src/main/db/repositories/bookmark.repo.ts

  • Create: tests/main/snapshot.repo.test.ts

  • Create: tests/main/bookmark.repo.test.ts

  • Step 1: snapshot.repo

create(chapterId, type, content, name?): Snapshot
list(chapterId): Snapshot[]
pruneAuto(chapterId, max: number): void  // FIFO 删除最旧 auto
restore(snapshotId): string  // 返回 content
  • Step 2: bookmark.repo — CRUD + listByChapter + resolve(id, boolean)

  • Step 3: 测试 + Commit

git commit -am "feat: add snapshot and bookmark repositories"

Task 7: FTS Sync + Search Service

Files:

  • Create: src/main/services/fts-sync.service.ts

  • Create: src/main/services/search.service.ts

  • Create: tests/main/search.service.test.ts

  • Step 1: fts-sync.service.ts

export class FtsSyncService {
  upsert(db: SqliteDb, kind: SearchSourceKind, id: string, title: string, body: string): void {
    db.prepare('DELETE FROM search_fts WHERE source_kind=? AND source_id=?').run(kind, id)
    db.prepare('INSERT INTO search_fts(source_kind, source_id, title, body) VALUES (?,?,?,?)').run(kind, id, title, body)
  }
  rebuildAll(db: SqliteDb, book: BookRegistryService): void { /* 全量 */ }
}

HTML → plainText 用现有 word-count 同款 strip 或简单 replace(/<[^>]+>/g,'')

  • Step 2: search.service.ts
query(db, { text, scope, regex, caseSensitive }): SearchResult[]
replace(db, { text, replacement, dryRun }): { count: number; previews: SearchResult[] }

regex 模式:FTS 不支持时 fallback 遍历 search_fts body LIKE。

  • Step 3: 测试 + Commit
git commit -am "feat: add FTS sync and search service"

Task 8: Snapshot Service + WordFreq Service

Files:

  • Create: src/main/services/snapshot.service.ts

  • Create: src/main/services/wordfreq.service.ts

  • Modify: src/main/index.tsapp.on('before-quit') 触发 persist 快照)

  • Create: tests/main/wordfreq.service.test.ts

  • Step 1: 安装 diff 库

npm install diff-match-patch
npm install -D @types/diff-match-patch
  • Step 2: snapshot.service.ts
export class SnapshotService {
  startAutoSave(bookId: string, chapterId: string, getContent: () => string): void {
    this.timer = setInterval(() => { /* if changed → auto snapshot + prune */ }, settings.snapshotIntervalMs)
  }
  onPersistSave(bookId, chapterId, content): void { /* type=persist, prune to max 3 */ }
}
  • Step 3: wordfreq.service.ts

CJKtext.match(/[\u4e00-\u9fff]/g) 计数;Latin/\b[a-zA-Z]{2,}\b/g;返回 Top 100。

  • Step 4: 测试 + Commit
git commit -am "feat: add snapshot and word frequency services"

Task 9: IPC Handlers + Preload

Files:

  • Create: src/main/ipc/handlers/outline.handler.ts(及 setting/inspiration/snapshot/bookmark/search/wordfreq

  • Modify: src/main/ipc/register.ts

  • Modify: src/preload/index.ts

  • Modify: src/shared/electron-api.d.ts

  • Step 1: 每个 handler 遵循现有 chapter.handler 模式

export function registerOutlineHandlers(books: BookRegistryService): void {
  ipcMain.handle(IPC.OUTLINE_LIST, (_e, { bookId }) =>
    wrap(() => books.getOutlineRepo(bookId).listFlat())
  )
}
  • Step 2: BookRegistryService 扩展
getOutlineRepo(bookId): OutlineRepository
getSettingRepo(bookId): SettingRepository
// ...
open(bookId): BookOpenResult  // 附加 outlines/settings/inspirations
  • Step 3: preload 暴露新 API 命名空间
outline: { list, create, update, delete, move },
setting: { list, create, update, delete },
// ...
search: { query, replace, getHistory, saveHistory },
wordfreq: { analyze },
  • Step 4: npm run build + Commit
git commit -am "feat: add P2 IPC handlers and preload API"

Task 10: useEditStore + 统一 TipTap 编辑

Files:

  • Create: src/renderer/stores/useEditStore.ts

  • Modify: src/renderer/components/editor/TipTapEditor.tsx

  • Modify: src/renderer/components/layout/EditorLayout.tsx

  • Modify: src/renderer/stores/useBookStore.ts

  • Step 1: useEditStore.ts

interface EditState {
  target: EditTarget | null
  switchTarget: (next: EditTarget) => Promise<void>
  saveCurrent: () => Promise<void>
}

switchTarget: async (next) => {
  await flushEditorSave()
  const data = await loadTargetContent(next)  // IPC by kind
  set({ target: next })
  // TipTapEditor reacts via target prop
}
  • Step 2: TipTapEditor 接受 target + contentLoader
interface TipTapEditorProps {
  target: EditTarget | null
}

useEditor extensionstarget.kind === 'chapter' ? fullStarterKit : simplifiedKit。

save 回调按 kind 分发 IPCchapter.update / outline.update / etc.

  • Step 3: EditorLayout 四 Tab 选中 → switchTarget

删除 activePanel !== 'chapters' 占位,替换为 OutlineTree / SettingList / InspirationList。

  • Step 4: Commit
git commit -am "feat: add unified EditSession and TipTap targets"

Task 11: 左栏 CRUD UI(大纲/设定/灵感)

Files:

  • Create: src/renderer/components/outline/OutlineTree.tsx

  • Create: src/renderer/components/setting/SettingList.tsx

  • Create: src/renderer/components/inspiration/InspirationList.tsx

  • Modify: public/locales/zh-CN/translation.json(及 en

  • Step 1: OutlineTree — 递归渲染;双击标题 inline 编辑;底部「+ 新建大纲」;未覆盖橙色边框

  • Step 2: SettingList — 按 type 分组;底部新建 Dialogtype select + name

  • Step 3: InspirationList — 列表 + 新建;InspirationModal 快速捕获(Ctrl+Shift+I

  • Step 4: E2E outline 持久化

e2e/outline.spec.ts:建书 → 新建大纲 → 输入内容 → 重启 → 内容保留。

  • Step 5: Commit
git commit -am "feat: add outline setting inspiration sidebar UI"

Task 12: 大纲对照视图(P2.3

Files:

  • Create: src/renderer/components/outline/OutlineCompareView.tsx

  • Modify: src/renderer/stores/useOutlineStore.ts

  • Step 1: useOutlineStorecompareMode: booleanfilter: 'all'|'uncovered'|'covered'

  • Step 2: OutlineCompareView — 35/65 分屏;右侧只读章节 HTML;底部偏差条

  • Step 3: E2E E2E-COV-01/02spec §5.5.3

  • Step 4: Commit

git commit -am "feat: add outline compare view and coverage filter"

Task 13: 引用面板(P2.4

Files:

  • Create: src/renderer/components/reference/ReferencePanel.tsx

  • Create: src/renderer/stores/useReferenceStore.ts

  • Modify: src/renderer/styles/layout.css#reference-panel

  • Create: src/renderer/extensions/mention.ts

  • Modify: src/renderer/App.tsxopenReference 快捷键)

  • Step 1: ReferencePanel — Tab + 搜索 + 钉住 + 拖拽(onDragStart setData text/plain

  • Step 2: 与 AI 右栏互斥useAppStore 或 layout statereferenceOpen 时隐藏 RightPanel

  • Step 3: Mention 扩展(简化) — 输入 @ 弹出设定名列表;点击跳转 switchTarget({kind:'setting',id})

  • Step 4: E2E E2E-P2-REF-01

  • Step 5: Commit

git commit -am "feat: add reference panel with drag insert and mentions"

Task 14: 版本历史模态(P2.5

Files:

  • Create: src/renderer/components/version/VersionModal.tsx

  • Modify: src/renderer/components/layout/EditorLayout.tsx(工具栏入口)

  • Modify: src/renderer/App.tsxCtrl+S 触发 persist snapshot

  • Step 1: VersionModal UI — 列表 + 选中版本;双栏 diff 用 diff-match-patch

import DiffMatchPatch from 'diff-match-patch'
const dmp = new DiffMatchPatch()
const diffs = dmp.diff_main(oldPlain, newPlain)
dmp.diff_cleanupSemantic(diffs)

渲染:DIFF_INSERT 绿色背景,DIFF_DELETE 红色删除线。

  • Step 2: 回滚 — 调用 snapshot:restore;确认 Dialog

  • Step 3: SnapshotService wired — EditorLayout mount 章节时 startAutoSave

  • Step 4: E2E E2E-P2-SNAP-01

  • Step 5: Commit

git commit -am "feat: add chapter version history with diff and restore"

Task 15: 全局搜索模态(P2.6

Files:

  • Create: src/renderer/components/search/SearchModal.tsx

  • Create: src/renderer/stores/useSearchStore.ts

  • Modify: src/renderer/App.tsxglobalSearch 快捷键)

  • Step 1: SearchModal — 对齐 #searchModal;范围 select;高级选项折叠(regex/case

  • Step 2: 结果跳转 — chapter → switchTarget + scrollbookmark → chapter + position

  • Step 3: 替换预览 — 调用 search:replace dryRun → 确认 → 执行

  • Step 4: searchHistory — 下拉最近 10 条

  • Step 5: E2E E2E-P2-SEARCH-01 + Commit

git commit -am "feat: add global search modal with FTS and replace"

Task 16: 地标全量(P2.6

Files:

  • Create: src/renderer/extensions/landmark-mark.ts

  • Create: src/renderer/components/landmark/LandmarkModal.tsx

  • Modify: src/renderer/components/editor/TipTapEditor.tsx

  • Step 1: TipTap Mark — 行内 [TODO: label] 样式;insertLandmark 快捷键打开输入

  • Step 2: LandmarkModal — 按类型/章节分组;跳转 switchTarget chapter + editor.commands.focus(pos)

  • Step 3: FTS 同步 bookmark label

  • Step 4: E2E E2E-P2-LAND-01 + Commit

git commit -am "feat: add writing landmarks and list modal"

Task 17: 用词分析(P2.7

Files:

  • Modify: src/renderer/components/layout/RightPanel.tsx

  • Create: src/renderer/components/wordfreq/WordFreqPanel.tsx

  • Step 1: WordFreqPanel — 调用 wordfreq:analyze;Top 100 列表;点击 → 编辑器 scrollIntoView + 临时 highlight Mark

  • Step 2: 过度用词 — 阈值:每千字 >5 次;TipTap Decoration 淡黄色

  • Step 3: §5.6.2 占位 — 面板底部按钮 → showToast(t('feature.comingSoon'))

  • Step 4: E2E E2E-P2-WORD-01 + Commit

git commit -am "feat: add word frequency panel and overuse highlight"

Task 18: 全量测试与 v0.2.0 收尾

Files:

  • Modify: package.jsonversion 0.2.0

  • Modify: README.md

  • Step 1: 运行全量测试

npm run test
npm run build
npm run test:e2e

Expected: 全部 PASS

  • Step 2: 手动验收 — spec §1.3 七项 + §10 DoD

  • Step 3: 更新 README — v0.2.0 功能列表、测试命令

  • Step 4: Commit + tag

git commit -am "chore: prepare v0.2.0 release"
git tag v0.2.0

Plan Self-Review

Spec 章节 对应用 Task
§5.9 主题 P2.0 Task 1
§3 Schema v2 Task 3
§4 IPC Task 2, 9
§5.1 四 Tab Task 10, 11
§5.5 大纲对照/覆盖度 Task 12
§5.3.8 引用面板 Task 13
§5.4 版本历史 Task 8, 14
§5.6.1 搜索 Task 7, 15
§5.3.9 地标 Task 16
§5.6.3 用词分析 Task 17
§5.6.2 AI 命名 Task 17 Step 3 占位
§7 测试 Task 117 各 E2E + Task 18
§10 DoD Task 18

Placeholder 扫描: 无 TBD;章节拖拽明确未含;语音灵感为占位。

类型一致性: EditTargetSearchSourceKindSnapshotType 在 Task 2 定义,后续任务一致引用。


执行方式

Plan 已保存至 docs/superpowers/plans/2026-07-06-bilin-p2.md

两种执行选项:

  1. Subagent-Driven(推荐) — 每个 Task 派发独立 subagent,任务间 review,迭代快
  2. Inline Execution — 在本会话按 Task 顺序直接实现,批次间设检查点

你选哪种?