18 个 Task 覆盖主题修复、schema v2、统一编辑、引用/搜索/版本/地标/用词分析至 v0.2.0。 Co-authored-by: Cursor <cursoragent@cursor.com>
21 KiB
笔临 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
/* 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)。
浅色主题覆盖:
[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,在主页、设置页、编辑器断言 #app 内 h1,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[]
扩展 BookOpenResult:outlines, 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)、get、listFlat、update、delete(级联)、move、listUncovered()。
- 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.ts(app.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
CJK:text.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 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
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
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
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 + 搜索 + 钉住 + 拖拽(
onDragStartsetData 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
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
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.tsx(globalSearch 快捷键) -
Step 1: SearchModal — 对齐
#searchModal;范围 select;高级选项折叠(regex/case) -
Step 2: 结果跳转 — chapter → switchTarget + scroll;bookmark → chapter + position
-
Step 3: 替换预览 — 调用
search:replacedryRun → 确认 → 执行 -
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.json(version 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 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。
两种执行选项:
- Subagent-Driven(推荐) — 每个 Task 派发独立 subagent,任务间 review,迭代快
- Inline Execution — 在本会话按 Task 顺序直接实现,批次间设检查点
你选哪种?