# 笔临 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 } 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> => 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 { 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 = { 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 && (
...
)} {summary.writingStats && summary.writingStats.streakDays > 0 && (
{t('cockpit.streak', { days: summary.writingStats.streakDays })}
)} ``` - [x] **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)。 - [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` 底部按钮:「确认更新」→ `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 3–8, 11 | | v0.8.0 DoD | Task 11 |