From f331ddae865604546350fd99dbc7a5576be89e28 Mon Sep 17 00:00:00 2001 From: kun1h Date: Tue, 7 Jul 2026 17:34:14 +0800 Subject: [PATCH] docs: add P5.2 writing logs and AI extraction implementation plan for v0.8.0 Co-authored-by: Cursor --- ...07-07-bilin-p52-writing-logs-extraction.md | 764 ++++++++++++++++++ 1 file changed, 764 insertions(+) create mode 100644 docs/superpowers/plans/2026-07-07-bilin-p52-writing-logs-extraction.md diff --git a/docs/superpowers/plans/2026-07-07-bilin-p52-writing-logs-extraction.md b/docs/superpowers/plans/2026-07-07-bilin-p52-writing-logs-extraction.md new file mode 100644 index 0000000..6eb96f4 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-bilin-p52-writing-logs-extraction.md @@ -0,0 +1,764 @@ +# 笔临 P5.2 写作日志 + AI 知识抽取 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.8.0:`writing_logs` 日更追踪(保存差分 + 成章补计)、驾驶舱热力图/连更、状态栏日更进度,以及 AI 章节知识自动抽取 + 合并审核增强,与 P6 注入形成上下游闭环。 + +**Architecture:** 全局 `userData/writing_logs.sqlite` 存 `(date, bookId)` 日字数;每书 schema v7 增加 `chapter_writing_snapshots` 与 `knowledge_entries` 抽取元数据列;`ChapterWritingTracker` 挂 `CHAPTER_UPDATE` / `finishChapter`;`KnowledgeExtractionService` 异步调用 `AiClientService` 落库 `pending`。测试 **禁止 Mock** AI 集成/E2E。 + +**Tech Stack:** Electron 36、electron-vite、React 18、TypeScript、node:sqlite、Zustand、i18next、Vitest、Playwright + +**Spec:** `docs/superpowers/specs/2026-07-07-bilin-p52-writing-logs-extraction-design.md` + +**LM Studio 默认:** `baseUrl=http://127.0.0.1:1234`,`model=gemma-4-e4b-it` + +--- + +## File Map + +| 文件 | 职责 | +|------|------| +| `src/shared/types.ts` | `WritingStats`、`ExtractedKnowledgeItem`、`KnowledgeExtractionResult`;扩展 `KnowledgeEntry`、`CockpitSummary`、`GlobalSettings` | +| `src/shared/ipc-channels.ts` | `WRITING_GET_STATS`、`KNOWLEDGE_EXTRACT_CHAPTER`、`KNOWLEDGE_APPROVE_MERGE`、`KNOWLEDGE_BATCH_APPROVE_HIGH_CONFIDENCE` | +| `src/main/db/schema-v7.sql` | `chapter_writing_snapshots` + knowledge ALTER | +| `src/main/db/migrate.ts` | v7 迁移 | +| `src/main/db/repositories/writing-log.repo.ts` | 全局 writing_logs | +| `src/main/db/repositories/chapter-writing-snapshot.repo.ts` | 每书 snapshot | +| `src/main/db/repositories/knowledge.repo.ts` | 抽取列读写、`createExtracted`、`batchApproveHighConfidence` | +| `src/main/services/writing-log.service.ts` | 日更、连更、热力图 | +| `src/main/services/chapter-writing-tracker.service.ts` | `recordDelta` / `supplementOnFinish` | +| `src/main/services/knowledge-extraction-prompt.ts` | prompt + `parseExtractionJson` | +| `src/main/services/knowledge-extraction.service.ts` | `extractForChapter` | +| `src/main/services/knowledge.service.ts` | `approveMerge`、`clearMergeTarget` | +| `src/main/services/cockpit.service.ts` | 附加 `writingStats` | +| `src/main/services/global-settings.ts` | 新设置默认值 | +| `src/main/ipc/handlers/writing.handler.ts` | `writing:getStats` | +| `src/main/ipc/handlers/knowledge.handler.ts` | 抽取/合并 IPC | +| `src/main/ipc/handlers/chapter.handler.ts` | 挂钩 tracker | +| `src/main/ipc/handlers/interactive.handler.ts` | finish 后 supplement + 异步抽取 | +| `src/main/ipc/handlers/auto.handler.ts` | 同上 | +| `src/main/ipc/register.ts` | 注入 `WritingLogService` | +| `src/preload/index.ts` + `electron-api.d.ts` | 新 API | +| `src/renderer/components/cockpit/WritingHeatmap.tsx` | Canvas 热力图 | +| `src/renderer/components/cockpit/CockpitModal.tsx` | 今日进度/连更/热力图 | +| `src/renderer/components/layout/EditorLayout.tsx` | 状态栏日更进度 | +| `src/renderer/components/knowledge/KnowledgePanel.tsx` | 批次/合并/批量采纳 | +| `src/renderer/components/knowledge/KnowledgeEditorDialog.tsx` | 合并预填模式 | +| `src/renderer/components/settings/SettingsPage.tsx` | 抽取设置 | +| `src/renderer/components/layout/ChapterList.tsx` 或编辑器菜单 | 抽取本章入口 | +| `public/locales/*/translation.json` | P5.2 文案 | +| `tests/main/writing-log.test.ts` | UT-LOG-* / IT-LOG-01 | +| `tests/main/knowledge-extraction.test.ts` | UT-EXT-* / IT-EXT-* | +| `e2e/writing-logs.spec.ts` | E2E-LOG-* | +| `e2e/knowledge-extraction.spec.ts` | E2E-EXT-* | + +--- + +## Task 1: 类型 + IPC 通道 + GlobalSettings + +**Files:** +- Modify: `src/shared/types.ts` +- Modify: `src/shared/ipc-channels.ts` +- Modify: `src/main/services/global-settings.ts` +- Modify: `src/renderer/stores/useSettingsStore.ts` + +- [ ] **Step 1: 扩展 types** + +`src/shared/types.ts` 追加: + +```typescript +export interface WritingDayLog { + date: string + bookId: string + wordCount: number +} + +export interface WritingStats { + todayWords: number + dailyGoal: number + goalProgress: number + streakDays: number + heatmap: WritingDayLog[] +} + +export interface ExtractedKnowledgeItem { + type: KnowledgeType + title: string + content: string + importance?: number + confidence: number + foreshadowStatus?: ForeshadowStatus + suggestedMergeId?: string + proposedPatch?: Partial +} + +export interface KnowledgeExtractionResult { + batchId: string + chapterId: string + items: ExtractedKnowledgeItem[] +} +``` + +扩展 `KnowledgeEntry`: + +```typescript +extractionConfidence?: number +mergeTargetId?: string +extractionBatchId?: string +``` + +扩展 `CockpitSummary`: + +```typescript +writingStats?: WritingStats +``` + +扩展 `GlobalSettings`: + +```typescript +knowledgeAutoExtract?: boolean +knowledgeExtractConfidenceThreshold?: number +``` + +- [ ] **Step 2: IPC 通道** + +```typescript +WRITING_GET_STATS: 'writing:getStats', +KNOWLEDGE_EXTRACT_CHAPTER: 'knowledge:extractChapter', +KNOWLEDGE_APPROVE_MERGE: 'knowledge:approveMerge', +KNOWLEDGE_BATCH_APPROVE_HIGH_CONFIDENCE: 'knowledge:batchApproveHighConfidence', +``` + +- [ ] **Step 3: global-settings 默认值** + +```typescript +knowledgeAutoExtract: true, +knowledgeExtractConfidenceThreshold: 0.8, +``` + +`get()` merge 补默认。 + +- [ ] **Step 4: useSettingsStore 同步默认** + +- [ ] **Step 5: build** + +```bash +npm run build +``` + +--- + +## Task 2: schema v7 + KnowledgeRepository 扩展 + +**Files:** +- Create: `src/main/db/schema-v7.sql` +- Modify: `src/main/db/migrate.ts` +- Modify: `src/main/db/repositories/knowledge.repo.ts` +- Create: `tests/main/migrate-v7.test.ts` + +- [ ] **Step 1: schema-v7.sql** + +```sql +CREATE TABLE IF NOT EXISTS chapter_writing_snapshots ( + chapter_id TEXT PRIMARY KEY, + logged_word_count INTEGER NOT NULL DEFAULT 0, + finish_supplemented INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (chapter_id) REFERENCES chapters(id) ON DELETE CASCADE +); +``` + +- [ ] **Step 2: migrate.ts** + +```typescript +import schemaV7 from './schema-v7.sql?raw' +const CURRENT_VERSION = 7 + +// in migrate(), after v6: +if (current < 7) { + db.exec(schemaV7) + tryAlter(db, 'ALTER TABLE knowledge_entries ADD COLUMN extraction_confidence REAL') + tryAlter(db, 'ALTER TABLE knowledge_entries ADD COLUMN merge_target_id TEXT') + tryAlter(db, 'ALTER TABLE knowledge_entries ADD COLUMN extraction_batch_id TEXT') + db.prepare('INSERT INTO schema_version (version, description) VALUES (?, ?)').run(7, 'P5.2 writing logs extraction') +} +``` + +- [ ] **Step 3: knowledge.repo mapRow + createExtracted** + +更新 `mapRow` 读取三列;新增: + +```typescript +createExtracted(input: CreateKnowledgeInput & { + extractionConfidence?: number + mergeTargetId?: string + extractionBatchId?: string +}): KnowledgeEntry { + const id = randomUUID() + this.db.prepare( + `INSERT INTO knowledge_entries ( + id, type, title, content, importance, status, foreshadow_status, + source_chapter_id, last_mention_chapter_id, linked_bookmark_id, + extraction_confidence, merge_target_id, extraction_batch_id + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ).run( + id, input.type, input.title, input.content ?? '', input.importance ?? 3, + input.status ?? 'pending', input.foreshadowStatus ?? null, + input.sourceChapterId ?? null, input.lastMentionChapterId ?? null, input.linkedBookmarkId ?? null, + input.extractionConfidence ?? null, input.mergeTargetId ?? null, input.extractionBatchId ?? null + ) + return this.get(id)! +} + +batchApproveHighConfidence(threshold: number): number { + const rows = this.db.prepare( + `SELECT id FROM knowledge_entries + WHERE status = 'pending' AND merge_target_id IS NULL + AND extraction_confidence IS NOT NULL AND extraction_confidence >= ?` + ).all(threshold) as { id: string }[] + let count = 0 + for (const { id } of rows) { + this.update(id, { status: 'approved' }) + count++ + } + return count +} + +clearMergeTarget(id: string): KnowledgeEntry { + this.db.prepare('UPDATE knowledge_entries SET merge_target_id = NULL, updated_at = datetime(\'now\') WHERE id = ?').run(id) + return this.get(id)! +} +``` + +- [ ] **Step 4: migrate-v7 测试** + +```typescript +it('UT-MIG-07: v6 database migrates to v7 with snapshot table and knowledge columns', () => { + const db = new DatabaseSync(':memory:') + migrate(db) // fresh → v7 + const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='chapter_writing_snapshots'").get() + expect(tables).toBeTruthy() + db.close() +}) +``` + +- [ ] **Step 5: 运行** + +```bash +npm run test -- tests/main/migrate-v7.test.ts +``` + +--- + +## Task 3: WritingLogService(全局) + +**Files:** +- Create: `src/main/db/repositories/writing-log.repo.ts` +- Create: `src/main/services/writing-log.service.ts` +- Create: `tests/main/writing-log.test.ts` + +- [ ] **Step 1: WritingLogRepository** + +全局 sqlite 路径 `join(userDataDir, 'writing_logs.sqlite')`;`migrate()` 建表;方法: + +```typescript +addToday(bookId: string, delta: number, date?: string): number +getToday(bookId: string, date?: string): number +listRange(bookId: string, fromDate: string, toDate: string): WritingDayLog[] +``` + +`addToday`:`INSERT ... ON CONFLICT DO UPDATE SET word_count = MAX(0, word_count + excluded.word_count)`。 + +日期工具: + +```typescript +export function localDateString(d = new Date()): string { + return d.toLocaleDateString('sv-SE') // YYYY-MM-DD +} +``` + +- [ ] **Step 2: WritingLogService** + +```typescript +export class WritingLogService { + constructor(private repo: WritingLogRepository, private settings: GlobalSettingsService) {} + + addToday(bookId: string, delta: number): number { + return this.repo.addToday(bookId, delta) + } + + getStats(bookId: string): WritingStats { + const dailyGoal = this.settings.get().dailyWordGoal ?? 0 + const today = localDateString() + const todayWords = this.repo.getToday(bookId, today) + const goalProgress = dailyGoal > 0 ? todayWords / dailyGoal : 0 + const heatmap = this.buildHeatmap(bookId, 365) + const streakDays = dailyGoal > 0 ? this.calcStreak(bookId, dailyGoal) : 0 + return { todayWords, dailyGoal, goalProgress, streakDays, heatmap } + } + + private buildHeatmap(bookId: string, days: number): WritingDayLog[] { /* 填充 0 */ } + private calcStreak(bookId: string, goal: number): number { /* 从昨日向前 */ } +} +``` + +- [ ] **Step 3: UT-LOG-01~04** + +```typescript +it('UT-LOG-01: addToday accumulates +200', () => { /* ... */ }) +it('UT-LOG-02: negative delta clamps at 0', () => { /* ... */ }) +it('UT-LOG-04: streak counts consecutive goal-met days', () => { /* seed 3 days */ }) +``` + +- [ ] **Step 4: 运行** + +```bash +npm run test -- tests/main/writing-log.test.ts +``` + +--- + +## Task 4: ChapterWritingTracker + Snapshot Repo + +**Files:** +- Create: `src/main/db/repositories/chapter-writing-snapshot.repo.ts` +- Create: `src/main/services/chapter-writing-tracker.service.ts` +- Modify: `tests/main/writing-log.test.ts` + +- [ ] **Step 1: ChapterWritingSnapshotRepository** + +```typescript +get(chapterId: string): { loggedWordCount: number; finishSupplemented: boolean } | null +upsert(chapterId: string, loggedWordCount: number, finishSupplemented?: boolean): void +``` + +- [ ] **Step 2: ChapterWritingTracker** + +```typescript +export class ChapterWritingTracker { + constructor( + private bookDb: SqliteDb, + private bookId: string, + private writingLogs: WritingLogService + ) {} + + recordDelta(chapterId: string, newWordCount: number): void { + const snap = this.snapRepo.get(chapterId) + const logged = snap?.loggedWordCount ?? 0 + const delta = newWordCount - logged + if (delta !== 0) this.writingLogs.addToday(this.bookId, delta) + this.snapRepo.upsert(chapterId, newWordCount, snap?.finishSupplemented ?? false) + } + + supplementOnFinish(chapterId: string, wordCount: number): void { + const snap = this.snapRepo.get(chapterId) + const logged = snap?.loggedWordCount ?? 0 + const gap = wordCount - logged + if (gap > 0) this.writingLogs.addToday(this.bookId, gap) + this.snapRepo.upsert(chapterId, wordCount, true) + } +} +``` + +- [ ] **Step 3: UT-LOG-03 supplementOnFinish** + +- [ ] **Step 4: 运行测试** + +```bash +npm run test -- tests/main/writing-log.test.ts +``` + +--- + +## Task 5: 挂钩 CHAPTER_UPDATE + finishChapter + +**Files:** +- Modify: `src/main/ipc/handlers/chapter.handler.ts` +- Modify: `src/main/ipc/handlers/interactive.handler.ts` +- Modify: `src/main/ipc/handlers/auto.handler.ts` +- Modify: `src/main/ipc/register.ts` +- Modify: `tests/main/writing-log.test.ts`(IT-LOG-01) + +- [ ] **Step 1: register.ts 创建 WritingLogService 单例** + +```typescript +const writingLogs = new WritingLogService(new WritingLogRepository(userData), settings) +registerChapterHandlers(books, writingLogs) +registerInteractiveHandlers(registry, settings, aiClient, books, writingLogs, ...) +``` + +工厂函数: + +```typescript +function trackerFor(registry: BookRegistryService, bookId: string, writingLogs: WritingLogService) { + return new ChapterWritingTracker(registry.getDb(bookId), bookId, writingLogs) +} +``` + +- [ ] **Step 2: chapter.handler CHAPTER_UPDATE 后** + +```typescript +wrap(() => { + const chapter = registry.getChapterRepo(bookId).update(...) + ftsSync.upsert(...) + trackerFor(registry, bookId, writingLogs).recordDelta(chapterId, chapter.wordCount) + registry.updateMeta(bookId, { lastChapterId: chapterId }) + return chapter +}) +``` + +- [ ] **Step 3: interactive/auto finishChapter 后** + +`finishChapter` 返回 `{ chapterId }` 后: + +```typescript +const ch = registry.getChapterRepo(bookId).get(chapterId)! +trackerFor(registry, bookId, writingLogs).supplementOnFinish(chapterId, ch.wordCount) +if (settings.get().knowledgeAutoExtract !== false) { + setImmediate(() => void extractKnowledge(registry, bookId, chapterId, aiClient, settings)) +} +return result +``` + +抽取辅助放在 `src/main/services/knowledge-extraction-runner.ts`(避免 handler 膨胀)。 + +- [ ] **Step 4: IT-LOG-01** + +两次 `CHAPTER_UPDATE` 模拟,断言 `writing_logs` 累计。 + +- [ ] **Step 5: 运行** + +```bash +npm run test -- tests/main/writing-log.test.ts +``` + +--- + +## Task 6: Cockpit 扩展 + writing IPC + preload + +**Files:** +- Modify: `src/main/services/cockpit.service.ts` +- Create: `src/main/ipc/handlers/writing.handler.ts` +- Modify: `src/main/ipc/register.ts` +- Modify: `src/preload/index.ts` +- Modify: `src/shared/electron-api.d.ts` + +- [ ] **Step 1: CockpitService 注入 WritingLogService** + +```typescript +getSummary(activeVolumeId?: string, bookId?: string): CockpitSummary { + const base = { /* existing */ } + const writingStats = bookId ? this.writingLogs.getStats(bookId) : undefined + return { ...base, writingStats } +} +``` + +更新 `cockpit.handler.ts` 传 `bookId`。 + +- [ ] **Step 2: writing.handler** + +```typescript +ipcMain.handle(IPC.WRITING_GET_STATS, (_e, { bookId }: { bookId: string }) => + wrap(() => writingLogs.getStats(bookId)) +) +``` + +- [ ] **Step 3: preload** + +```typescript +writing: { + getStats: (bookId: string): Promise> => + ipcRenderer.invoke(IPC.WRITING_GET_STATS, { bookId }) +}, +``` + +- [ ] **Step 4: build** + +```bash +npm run build +``` + +--- + +## Task 7: KnowledgeExtraction prompt + parse + service + +**Files:** +- Create: `src/main/services/knowledge-extraction-prompt.ts` +- Create: `src/main/services/knowledge-extraction.service.ts` +- Create: `tests/main/knowledge-extraction.test.ts` + +- [ ] **Step 1: knowledge-extraction-prompt.ts** + +```typescript +export function buildExtractionPrompt( + chapterTitle: string, + plainText: string, + approvedTitles: Array<{ id: string; title: string }> +): string { /* 中文指令 + JSON schema 说明 */ } + +export function parseExtractionJson(raw: string): ExtractedKnowledgeItem[] { + const cleaned = raw.replace(/```json\n?|\n?```/g, '').trim() + const arr = JSON.parse(cleaned) as ExtractedKnowledgeItem[] + if (!Array.isArray(arr)) throw new Error('expected array') + return arr.map((item) => ({ + type: item.type, + title: String(item.title).trim(), + content: String(item.content ?? '').trim(), + importance: item.importance ?? 3, + confidence: Number(item.confidence) || 0.5, + foreshadowStatus: item.foreshadowStatus, + suggestedMergeId: item.suggestedMergeId ?? undefined, + proposedPatch: item.proposedPatch + })).filter((x) => x.title.length > 0) +} +``` + +- [ ] **Step 2: KnowledgeExtractionService** + +```typescript +export class KnowledgeExtractionService { + async extractForChapter(chapterId: string): Promise { + const chapter = this.chapterRepo.get(chapterId) + if (!chapter) throw new Error('Chapter not found') + const plain = stripHtml(chapter.content).slice(0, 12000) + if (!plain.trim()) throw new Error('empty chapter') + const approved = this.knowledgeRepo.list({ status: 'approved' }).slice(0, 30) + const prompt = buildExtractionPrompt(chapter.title, plain, approved.map((e) => ({ id: e.id, title: e.title }))) + let response = await this.aiClient.chat([{ role: 'user', content: prompt }]) + let items: ExtractedKnowledgeItem[] + try { + items = parseExtractionJson(response) + } catch { + response = await this.aiClient.chat([ + { role: 'user', content: prompt }, + { role: 'user', content: '仅返回 JSON 数组,不要 markdown 或其它说明。' } + ]) + items = parseExtractionJson(response) + } + const batchId = randomUUID() + const saved: ExtractedKnowledgeItem[] = [] + for (const item of items) { + const mergeTargetId = item.suggestedMergeId && this.knowledgeRepo.get(item.suggestedMergeId) ? item.suggestedMergeId : undefined + this.knowledgeRepo.createExtracted({ + type: item.type, + title: item.title, + content: item.content, + importance: item.importance, + status: 'pending', + foreshadowStatus: item.type === 'foreshadow' ? (item.foreshadowStatus ?? 'buried') : undefined, + sourceChapterId: chapterId, + extractionConfidence: item.confidence, + mergeTargetId, + extractionBatchId: batchId + }) + saved.push({ ...item, suggestedMergeId: mergeTargetId }) + } + return { batchId, chapterId, items: saved } + } +} +``` + +- [ ] **Step 3: UT-EXT-01 / UT-EXT-02** + +对 `parseExtractionJson` 测合法 JSON 与 `suggestedMergeId` 字段。 + +- [ ] **Step 4: 运行** + +```bash +npm run test -- tests/main/knowledge-extraction.test.ts -t "UT-EXT" +``` + +--- + +## Task 8: Knowledge 合并 IPC + KnowledgeService + +**Files:** +- Modify: `src/main/services/knowledge.service.ts` +- Modify: `src/main/ipc/handlers/knowledge.handler.ts` +- Modify: `src/preload/index.ts` +- Modify: `tests/main/knowledge-extraction.test.ts` + +- [ ] **Step 1: KnowledgeService.approveMerge** + +```typescript +approveMerge(pendingId: string): KnowledgeEntry { + const pending = this.repo.get(pendingId) + if (!pending?.mergeTargetId) throw new Error('not a merge suggestion') + const target = this.repo.get(pending.mergeTargetId) + if (!target || target.status !== 'approved') throw new Error('merge target missing') + const patch: Partial = { + content: pending.content, + importance: pending.importance, + lastMentionChapterId: pending.lastMentionChapterId ?? pending.sourceChapterId + } + const updated = this.repo.update(target.id, patch) + this.repo.update(pendingId, { status: 'rejected' }) + return updated +} + +saveMergeAsNew(pendingId: string): KnowledgeEntry { + return this.repo.clearMergeTarget(pendingId) +} +``` + +- [ ] **Step 2: knowledge.handler IPC** + +```typescript +ipcMain.handle(IPC.KNOWLEDGE_EXTRACT_CHAPTER, (_e, { bookId, chapterId }) => + wrap(() => new KnowledgeExtractionService(registry.getDb(bookId), aiClient).extractForChapter(chapterId)) +) +ipcMain.handle(IPC.KNOWLEDGE_APPROVE_MERGE, (_e, { bookId, pendingId }) => + wrap(() => new KnowledgeService(registry.getDb(bookId)).approveMerge(pendingId)) +) +ipcMain.handle(IPC.KNOWLEDGE_BATCH_APPROVE_HIGH_CONFIDENCE, (_e, { bookId, threshold }) => + wrap(() => { + const t = threshold ?? settings.get().knowledgeExtractConfidenceThreshold ?? 0.8 + return new KnowledgeRepository(registry.getDb(bookId)).batchApproveHighConfidence(t) + }) +) +``` + +- [ ] **Step 3: IT-EXT-01 / IT-EXT-02**(240s) + +- [ ] **Step 4: IT-EXT-03** — 抽取 → approve → `KnowledgeRetrievalService.suggest` 含新条目 + +--- + +## Task 9: 驾驶舱 UI + 状态栏日更进度 + +**Files:** +- Create: `src/renderer/components/cockpit/WritingHeatmap.tsx` +- Modify: `src/renderer/components/cockpit/CockpitModal.tsx` +- Modify: `src/renderer/components/layout/EditorLayout.tsx` +- Modify: `src/renderer/styles/layout.css` + +- [ ] **Step 1: WritingHeatmap 组件** + +Props:`heatmap: WritingDayLog[]`;Canvas 13×7 格(52 周近似 365 天);四档颜色;`data-testid="cockpit-heatmap"`。 + +- [ ] **Step 2: CockpitModal 扩展** + +在 `cockpit-grid` 下方: + +```tsx +{summary.writingStats && summary.writingStats.dailyGoal > 0 && ( +
...
+)} +{summary.writingStats && summary.writingStats.streakDays > 0 && ( +
{t('cockpit.streak', { days: summary.writingStats.streakDays })}
+)} + +``` + +- [ ] **Step 3: EditorLayout 状态栏** + +```tsx +const writingStats = useWritingStatsStore() // 或 cockpit summary 缓存 +{dailyGoal > 0 && ( + = 1 ? 'goal-met' : progress >= 0.8 ? 'goal-near' : ''}`} data-testid="status-daily-goal"> + {t('status.dailyGoal', { current: todayWords, goal: dailyGoal })} + +)} +``` + +章节保存后 `writing.getStats` 刷新(监听 save 或轮询 `useBookStore` wordCount)。 + +- [ ] **Step 4: build 验证** + +```bash +npm run build +``` + +--- + +## Task 10: 知识库 UI + 设置 + 抽取入口 + +**Files:** +- Modify: `src/renderer/components/knowledge/KnowledgePanel.tsx` +- Modify: `src/renderer/components/knowledge/KnowledgeEditorDialog.tsx` +- Modify: `src/renderer/stores/useKnowledgeStore.ts` +- Modify: `src/renderer/components/settings/SettingsPage.tsx` +- Modify: `src/renderer/components/layout/ChapterList.tsx`(或 `EditorLayout` 章节菜单) +- Modify: `public/locales/zh-CN/translation.json` +- Modify: `public/locales/en/translation.json` + +- [ ] **Step 1: KnowledgePanel 待审核增强** + +- 卡片显示 `extractionConfidence` 百分比徽章 +- `mergeTargetId` 时显示 `t('knowledge.mergeSuggest')` + `data-testid="knowledge-merge-review-{id}"` +- 工具栏按钮 `knowledge-batch-high-confidence` +- 工具栏「从当前章抽取」`knowledge-extract-current`(需 `selectedChapterId`) + +- [ ] **Step 2: KnowledgeEditorDialog 合并模式** + +props:`mergeTarget?: KnowledgeEntry`、`initialPatch?: Partial` + +底部按钮:「确认更新」→ `approveMerge`;「作为新条目」→ `saveMergeAsNew` 后常规编辑。 + +- [ ] **Step 3: SettingsPage** + +`settings-knowledge-auto-extract` 开关;`settings-extract-threshold` 数字输入。 + +- [ ] **Step 4: 章节抽取入口** + +章节列表项或编辑器右键:`data-testid="chapter-extract-knowledge"` → `knowledge.extractChapter`。 + +- [ ] **Step 5: i18n** — spec §9 全部键 + +--- + +## Task 11: E2E + v0.8.0 交付 + +**Files:** +- Create: `e2e/writing-logs.spec.ts` +- Create: `e2e/knowledge-extraction.spec.ts` +- Modify: `package.json` +- Modify: `README.md` +- Modify: `src/renderer/components/settings/SettingsPage.tsx`(about 版本) +- Modify: `docs/superpowers/plans/2026-07-07-bilin-p52-writing-logs-extraction.md`(勾选完成项) + +- [ ] **Step 1: E2E-LOG-01 / E2E-LOG-02** + +写 100 字保存 → 打开驾驶舱 → `cockpit-today-progress` 更新;`cockpit-heatmap` 可见。 + +- [ ] **Step 2: E2E-EXT-01 / E2E-EXT-02 / E2E-EXT-03**(300s timeout on AI tests) + +- [ ] **Step 3: 全量测试** + +```bash +npm run build +npm run test +npm run test:e2e -- e2e/writing-logs.spec.ts e2e/knowledge-extraction.spec.ts +``` + +- [ ] **Step 4: 版本 0.8.0** + +`package.json`、`README.md` 追加: + +```markdown +- 写作日志与热力图、AI 章节知识自动抽取(P5.2) +``` + +- [ ] **Step 5: Commit** + +```bash +git commit -m "feat: ship v0.8.0 with writing logs and AI knowledge extraction" +``` + +--- + +## Spec Coverage Checklist + +| Spec 要求 | Task | +|-----------|------| +| writing_logs 全局表 | Task 3 | +| chapter_writing_snapshots v7 | Task 2, 4 | +| 保存差分 + 成章补计 | Task 4, 5 | +| 连更 + 热力图 | Task 3, 9 | +| 状态栏日更进度 | Task 9 | +| knowledgeAutoExtract 设置 | Task 1, 5, 10 | +| AI 抽取五类 + JSON | Task 7 | +| 合并建议 approveMerge | Task 8, 10 | +| 批量高置信度 | Task 2, 8, 10 | +| P6 suggestContext 联动 | Task 8 IT-EXT-03 | +| UT-LOG / UT-EXT / IT / E2E | Task 3–8, 11 | +| v0.8.0 DoD | Task 11 |