docs: add P5.2 writing logs and AI extraction implementation plan for v0.8.0

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 17:34:14 +08:00
parent 72c8e13210
commit f331ddae86
@@ -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<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
```
- [ ] **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<IpcResult<WritingStats>> =>
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<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 }
}
}
```
- [ ] **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<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)
}
```
- [ ] **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 && (
<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 ?? []} />
```
- [ ] **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)。
- [ ] **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<CreateKnowledgeInput>`
底部按钮:「确认更新」→ `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 38, 11 |
| v0.8.0 DoD | Task 11 |