Files
bilin/docs/superpowers/plans/2026-07-07-bilin-p52-writing-logs-extraction.md
bing a39e06ff07 feat: ship v0.8.0 with writing logs and AI knowledge extraction
Deliver P5.2 writing day logs, cockpit heatmap, streak tracking, and chapter knowledge auto-extraction with merge review.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 18:26:29 +08:00

765 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 笔临 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 (`- [x]`) 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`
- [x] **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<CreateKnowledgeInput>
}
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
```
- [x] **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',
```
- [x] **Step 3: global-settings 默认值**
```typescript
knowledgeAutoExtract: true,
knowledgeExtractConfidenceThreshold: 0.8,
```
`get()` merge 补默认。
- [x] **Step 4: useSettingsStore 同步默认**
- [x] **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`
- [x] **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
);
```
- [x] **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')
}
```
- [x] **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)!
}
```
- [x] **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()
})
```
- [x] **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`
- [x] **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
}
```
- [x] **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 { /* 从昨日向前 */ }
}
```
- [x] **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 */ })
```
- [x] **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`
- [x] **Step 1: ChapterWritingSnapshotRepository**
```typescript
get(chapterId: string): { loggedWordCount: number; finishSupplemented: boolean } | null
upsert(chapterId: string, loggedWordCount: number, finishSupplemented?: boolean): void
```
- [x] **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)
}
}
```
- [x] **Step 3: UT-LOG-03 supplementOnFinish**
- [x] **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
- [x] **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)
}
```
- [x] **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
})
```
- [x] **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 膨胀)。
- [x] **Step 4: IT-LOG-01**
两次 `CHAPTER_UPDATE` 模拟,断言 `writing_logs` 累计。
- [x] **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`
- [x] **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`
- [x] **Step 2: writing.handler**
```typescript
ipcMain.handle(IPC.WRITING_GET_STATS, (_e, { bookId }: { bookId: string }) =>
wrap(() => writingLogs.getStats(bookId))
)
```
- [x] **Step 3: preload**
```typescript
writing: {
getStats: (bookId: string): Promise<IpcResult<WritingStats>> =>
ipcRenderer.invoke(IPC.WRITING_GET_STATS, { bookId })
},
```
- [x] **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`
- [x] **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)
}
```
- [x] **Step 2: KnowledgeExtractionService**
```typescript
export class KnowledgeExtractionService {
async extractForChapter(chapterId: string): Promise<KnowledgeExtractionResult> {
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 }
}
}
```
- [x] **Step 3: UT-EXT-01 / UT-EXT-02**
`parseExtractionJson` 测合法 JSON 与 `suggestedMergeId` 字段。
- [x] **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`
- [x] **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<CreateKnowledgeInput> = {
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)
}
```
- [x] **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)
})
)
```
- [x] **Step 3: IT-EXT-01 / IT-EXT-02**240s
- [x] **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`
- [x] **Step 1: WritingHeatmap 组件**
Props`heatmap: WritingDayLog[]`Canvas 13×7 格(52 周近似 365 天);四档颜色;`data-testid="cockpit-heatmap"`
- [x] **Step 2: CockpitModal 扩展**
`cockpit-grid` 下方:
```tsx
{summary.writingStats && summary.writingStats.dailyGoal > 0 && (
<div data-testid="cockpit-today-progress">...</div>
)}
{summary.writingStats && summary.writingStats.streakDays > 0 && (
<div data-testid="cockpit-streak">{t('cockpit.streak', { days: summary.writingStats.streakDays })}</div>
)}
<WritingHeatmap heatmap={summary.writingStats?.heatmap ?? []} />
```
- [x] **Step 3: EditorLayout 状态栏**
```tsx
const writingStats = useWritingStatsStore() // 或 cockpit summary 缓存
{dailyGoal > 0 && (
<span className={`status-daily-goal ${progress >= 1 ? 'goal-met' : progress >= 0.8 ? 'goal-near' : ''}`} data-testid="status-daily-goal">
{t('status.dailyGoal', { current: todayWords, goal: dailyGoal })}
</span>
)}
```
章节保存后 `writing.getStats` 刷新(监听 save 或轮询 `useBookStore` wordCount)。
- [x] **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`
- [x] **Step 1: KnowledgePanel 待审核增强**
- 卡片显示 `extractionConfidence` 百分比徽章
- `mergeTargetId` 时显示 `t('knowledge.mergeSuggest')` + `data-testid="knowledge-merge-review-{id}"`
- 工具栏按钮 `knowledge-batch-high-confidence`
- 工具栏「从当前章抽取」`knowledge-extract-current`(需 `selectedChapterId`
- [x] **Step 2: KnowledgeEditorDialog 合并模式**
props`mergeTarget?: KnowledgeEntry``initialPatch?: Partial<CreateKnowledgeInput>`
底部按钮:「确认更新」→ `approveMerge`;「作为新条目」→ `saveMergeAsNew` 后常规编辑。
- [x] **Step 3: SettingsPage**
`settings-knowledge-auto-extract` 开关;`settings-extract-threshold` 数字输入。
- [x] **Step 4: 章节抽取入口**
章节列表项或编辑器右键:`data-testid="chapter-extract-knowledge"``knowledge.extractChapter`
- [x] **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`(勾选完成项)
- [x] **Step 1: E2E-LOG-01 / E2E-LOG-02**
写 100 字保存 → 打开驾驶舱 → `cockpit-today-progress` 更新;`cockpit-heatmap` 可见。
- [x] **Step 2: E2E-EXT-01 / E2E-EXT-02 / E2E-EXT-03**300s timeout on AI tests
- [x] **Step 3: 全量测试**
```bash
npm run build
npm run test
npm run test:e2e -- e2e/writing-logs.spec.ts e2e/knowledge-extraction.spec.ts
```
- [x] **Step 4: 版本 0.8.0**
`package.json``README.md` 追加:
```markdown
- 写作日志与热力图、AI 章节知识自动抽取(P5.2)
```
- [x] **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 38, 11 |
| v0.8.0 DoD | Task 11 |