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

24 KiB
Raw Permalink Blame History

笔临 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.0writing_logs 日更追踪(保存差分 + 成章补计)、驾驶舱热力图/连更、状态栏日更进度,以及 AI 章节知识自动抽取 + 合并审核增强,与 P6 注入形成上下游闭环。

Architecture: 全局 userData/writing_logs.sqlite(date, bookId) 日字数;每书 schema v7 增加 chapter_writing_snapshotsknowledge_entries 抽取元数据列;ChapterWritingTrackerCHAPTER_UPDATE / finishChapterKnowledgeExtractionService 异步调用 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:1234model=gemma-4-e4b-it


File Map

文件 职责
src/shared/types.ts WritingStatsExtractedKnowledgeItemKnowledgeExtractionResult;扩展 KnowledgeEntryCockpitSummaryGlobalSettings
src/shared/ipc-channels.ts WRITING_GET_STATSKNOWLEDGE_EXTRACT_CHAPTERKNOWLEDGE_APPROVE_MERGEKNOWLEDGE_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 抽取列读写、createExtractedbatchApproveHighConfidence
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 approveMergeclearMergeTarget
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 追加:

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

extractionConfidence?: number
mergeTargetId?: string
extractionBatchId?: string

扩展 CockpitSummary

writingStats?: WritingStats

扩展 GlobalSettings

knowledgeAutoExtract?: boolean
knowledgeExtractConfidenceThreshold?: number
  • Step 2: IPC 通道
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 默认值
knowledgeAutoExtract: true,
knowledgeExtractConfidenceThreshold: 0.8,

get() merge 补默认。

  • Step 4: useSettingsStore 同步默认

  • Step 5: build

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

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
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 读取三列;新增:

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 测试
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: 运行
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() 建表;方法:

addToday(bookId: string, delta: number, date?: string): number
getToday(bookId: string, date?: string): number
listRange(bookId: string, fromDate: string, toDate: string): WritingDayLog[]

addTodayINSERT ... ON CONFLICT DO UPDATE SET word_count = MAX(0, word_count + excluded.word_count)

日期工具:

export function localDateString(d = new Date()): string {
  return d.toLocaleDateString('sv-SE') // YYYY-MM-DD
}
  • Step 2: WritingLogService
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
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: 运行
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

get(chapterId: string): { loggedWordCount: number; finishSupplemented: boolean } | null
upsert(chapterId: string, loggedWordCount: number, finishSupplemented?: boolean): void
  • Step 2: ChapterWritingTracker
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: 运行测试

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.tsIT-LOG-01

  • Step 1: register.ts 创建 WritingLogService 单例

const writingLogs = new WritingLogService(new WritingLogRepository(userData), settings)
registerChapterHandlers(books, writingLogs)
registerInteractiveHandlers(registry, settings, aiClient, books, writingLogs, ...)

工厂函数:

function trackerFor(registry: BookRegistryService, bookId: string, writingLogs: WritingLogService) {
  return new ChapterWritingTracker(registry.getDb(bookId), bookId, writingLogs)
}
  • Step 2: chapter.handler CHAPTER_UPDATE 后
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 } 后:

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: 运行
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

getSummary(activeVolumeId?: string, bookId?: string): CockpitSummary {
  const base = { /* existing */ }
  const writingStats = bookId ? this.writingLogs.getStats(bookId) : undefined
  return { ...base, writingStats }
}

更新 cockpit.handler.tsbookId

  • Step 2: writing.handler
ipcMain.handle(IPC.WRITING_GET_STATS, (_e, { bookId }: { bookId: string }) =>
  wrap(() => writingLogs.getStats(bookId))
)
  • Step 3: preload
writing: {
  getStats: (bookId: string): Promise<IpcResult<WritingStats>> =>
    ipcRenderer.invoke(IPC.WRITING_GET_STATS, { bookId })
},
  • Step 4: build
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

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
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: 运行
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

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
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-02240s

  • 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 组件

Propsheatmap: WritingDayLog[]Canvas 13×7 格(52 周近似 365 天);四档颜色;data-testid="cockpit-heatmap"

  • Step 2: CockpitModal 扩展

cockpit-grid 下方:

{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 状态栏
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 验证
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 合并模式

propsmergeTarget?: KnowledgeEntryinitialPatch?: 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.tsxabout 版本)

  • 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-03300s timeout on AI tests

  • Step 3: 全量测试

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.jsonREADME.md 追加:

- 写作日志与热力图、AI 章节知识自动抽取(P5.2)
  • Step 5: Commit
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