# 笔临 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 服务;渲染进程新增 `useEditStore`(EditTarget)统一 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** ```css /* themes.css :root 追加 */ --text-on-accent: #ffffff; --input-text: var(--text-primary); --input-bg: var(--bg-surface); --input-border: var(--border); ``` 为 `[data-theme='ricepaper']`、`mist`、`teagarden` 各补全:`--bg-surface`、`--bg-active`、`--text-dim`、`--accent-glow`、`--border-light`、`--green`、`--orange`、`--red`。 - [ ] **Step 2: 替换硬编码 `#fff`** 在 `layout.css` 中将 `.btn-large.primary`、`.tab.active` 的 `color: #fff` 改为 `color: var(--text-on-accent)`。 浅色主题覆盖: ```css [data-theme='ricepaper'], [data-theme='mist'], [data-theme='teagarden'] { --text-on-accent: #ffffff; } ``` - [ ] **Step 3: 统一表单控件样式** `globals.css` 追加: ```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`,在主页、设置页、编辑器断言 `#app` 内 `h1,h2,button,.setting-item` 可见(`toBeVisible`)且 computed color 非透明/非背景同色(用 `page.evaluate` 比较 RGB)。 - [ ] **Step 5: 运行验证** ```bash npm run build npm run test:e2e -- e2e/themes-contrast.spec.ts ``` Expected: PASS - [ ] **Step 6: Commit** ```bash 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): ```typescript 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`: ```typescript snapshotIntervalMs: number snapshotMaxAuto: number snapshotMaxPersist: number searchHistory: string[] ``` 扩展 `BookOpenResult`:`outlines`, `settings`, `inspirations`。 - [ ] **Step 2: 扩展 ipc-channels.ts** ```typescript OUTLINE_LIST: 'outline:list', OUTLINE_CREATE: 'outline:create', // ... 全部见 spec §4.1 SEARCH_QUERY: 'search:query', WORDFREQ_ANALYZE: 'wordfreq:analyze', ``` - [ ] **Step 3: global-settings defaults** ```typescript function defaults(): GlobalSettings { return { // ...existing snapshotIntervalMs: 300_000, snapshotMaxAuto: 20, snapshotMaxPersist: 3, searchHistory: [] } } ``` - [ ] **Step 4: Commit** ```bash 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: ```sql CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5( source_kind, source_id, title, body, tokenize='unicode61' ); ``` - [ ] **Step 2: 更新 migrate.ts** ```typescript 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 测试** ```typescript // 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: 运行** ```bash npm run test -- tests/main/migrate-v2.test.ts ``` - [ ] **Step 5: Commit** ```bash 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: 写失败测试** ```typescript 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)`、`get`、`listFlat`、`update`、`delete`(级联)、`move`、`listUncovered()`。 - [ ] **Step 3: 运行 PASS + Commit** ```bash 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** ```bash 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** ```typescript 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** ```bash 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** ```typescript 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** ```typescript 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** ```bash 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.ts`(app.on('before-quit') 触发 persist 快照) - Create: `tests/main/wordfreq.service.test.ts` - [ ] **Step 1: 安装 diff 库** ```bash npm install diff-match-patch npm install -D @types/diff-match-patch ``` - [ ] **Step 2: snapshot.service.ts** ```typescript 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** CJK:`text.match(/[\u4e00-\u9fff]/g)` 计数;Latin:`/\b[a-zA-Z]{2,}\b/g`;返回 Top 100。 - [ ] **Step 4: 测试 + Commit** ```bash 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 模式** ```typescript export function registerOutlineHandlers(books: BookRegistryService): void { ipcMain.handle(IPC.OUTLINE_LIST, (_e, { bookId }) => wrap(() => books.getOutlineRepo(bookId).listFlat()) ) } ``` - [ ] **Step 2: BookRegistryService 扩展** ```typescript getOutlineRepo(bookId): OutlineRepository getSettingRepo(bookId): SettingRepository // ... open(bookId): BookOpenResult // 附加 outlines/settings/inspirations ``` - [ ] **Step 3: preload 暴露新 API 命名空间** ```typescript outline: { list, create, update, delete, move }, setting: { list, create, update, delete }, // ... search: { query, replace, getHistory, saveHistory }, wordfreq: { analyze }, ``` - [ ] **Step 4: `npm run build` + Commit** ```bash 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** ```typescript interface EditState { target: EditTarget | null switchTarget: (next: EditTarget) => Promise saveCurrent: () => Promise } 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** ```typescript interface TipTapEditorProps { target: EditTarget | null } ``` `useEditor` extensions:target.kind === 'chapter' ? fullStarterKit : simplifiedKit。 save 回调按 kind 分发 IPC:`chapter.update` / `outline.update` / etc. - [ ] **Step 3: EditorLayout 四 Tab 选中 → switchTarget** 删除 `activePanel !== 'chapters'` 占位,替换为 OutlineTree / SettingList / InspirationList。 - [ ] **Step 4: Commit** ```bash 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 分组;底部新建 Dialog(type select + name) - [ ] **Step 3: InspirationList** — 列表 + 新建;`InspirationModal` 快速捕获(Ctrl+Shift+I) - [ ] **Step 4: E2E outline 持久化** `e2e/outline.spec.ts`:建书 → 新建大纲 → 输入内容 → 重启 → 内容保留。 - [ ] **Step 5: Commit** ```bash 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: useOutlineStore** — `compareMode: boolean`、`filter: 'all'|'uncovered'|'covered'` - [ ] **Step 2: OutlineCompareView** — 35/65 分屏;右侧只读章节 HTML;底部偏差条 - [ ] **Step 3: E2E E2E-COV-01/02**(spec §5.5.3) - [ ] **Step 4: Commit** ```bash 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.tsx`(openReference 快捷键) - [ ] **Step 1: ReferencePanel** — Tab + 搜索 + 钉住 + 拖拽(`onDragStart` setData text/plain) - [ ] **Step 2: 与 AI 右栏互斥** — `useAppStore` 或 layout state:`referenceOpen` 时隐藏 RightPanel - [ ] **Step 3: Mention 扩展(简化)** — 输入 `@` 弹出设定名列表;点击跳转 `switchTarget({kind:'setting',id})` - [ ] **Step 4: E2E E2E-P2-REF-01** - [ ] **Step 5: Commit** ```bash 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.tsx`(Ctrl+S 触发 persist snapshot) - [ ] **Step 1: VersionModal UI** — 列表 + 选中版本;双栏 diff 用 `diff-match-patch` ```typescript 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** ```bash 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.tsx`(globalSearch 快捷键) - [ ] **Step 1: SearchModal** — 对齐 `#searchModal`;范围 select;高级选项折叠(regex/case) - [ ] **Step 2: 结果跳转** — chapter → switchTarget + scroll;bookmark → chapter + position - [ ] **Step 3: 替换预览** — 调用 `search:replace` dryRun → 确认 → 执行 - [ ] **Step 4: searchHistory** — 下拉最近 10 条 - [ ] **Step 5: E2E E2E-P2-SEARCH-01 + Commit** ```bash 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** ```bash 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** ```bash git commit -am "feat: add word frequency panel and overuse highlight" ``` --- ## Task 18: 全量测试与 v0.2.0 收尾 **Files:** - Modify: `package.json`(version 0.2.0) - Modify: `README.md` - [ ] **Step 1: 运行全量测试** ```bash 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** ```bash 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 1–17 各 E2E + Task 18 | | §10 DoD | Task 18 | **Placeholder 扫描:** 无 TBD;章节拖拽明确未含;语音灵感为占位。 **类型一致性:** `EditTarget`、`SearchSourceKind`、`SnapshotType` 在 Task 2 定义,后续任务一致引用。 --- ## 执行方式 Plan 已保存至 `docs/superpowers/plans/2026-07-06-bilin-p2.md`。 **两种执行选项:** 1. **Subagent-Driven(推荐)** — 每个 Task 派发独立 subagent,任务间 review,迭代快 2. **Inline Execution** — 在本会话按 Task 顺序直接实现,批次间设检查点 你选哪种?