Files
bilin/docs/superpowers/plans/2026-07-07-bilin-p53-p61-goals-injection.md
T
bing 255f1a99a0 docs: add P5.3 and P6.1 goals and injection history implementation plan for v0.9.0
Break down pomodoro, milestones, notifications, and knowledge injection logging into 11 tasks.

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

21 KiB
Raw Blame History

笔临 P5.3 + P6.1 写作目标 + 知识注入历史 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.9.0:§8 番茄钟(15/25/45)、连更里程碑徽章(7/30/100)、日更/番茄/里程碑通知;P6.1 写作流三模式知识注入历史记录与知识库反向链接 UI。

Architecture: 主进程 PomodoroService + AchievementService + GoalNotificationService 操作全局 writing_logs.sqlite 扩展表;每书 schema v8 增加 knowledge_injection_logsinteractive/auto/wizardconfirmContext 成功后异步写 injection log;渲染进程状态栏番茄控件 + 驾驶舱成就区 + 知识库注入预览。番茄 tick 采用主进程 push GOAL_NOTIFICATION / POMODORO_TICK 事件。

Tech Stack: Electron 36、electron-vite、React 18、TypeScript、node:sqlite、Zustand、i18next、Vitest、Playwright

Spec: docs/superpowers/specs/2026-07-07-bilin-p53-p61-goals-injection-design.md

E2E 番茄加速: BILIN_E2E=1POMODORO_TEST_SEC=3 时覆盖倒计时(仅测试,不暴露生产 UI)


File Map

文件 职责
src/shared/types.ts PomodoroStateWritingAchievementKnowledgeInjectionLog;扩展 CockpitSummaryGlobalSettings
src/shared/ipc-channels.ts pomodoro / achievement / injection / push 通道
src/main/db/schema-v8.sql knowledge_injection_logs
src/main/db/migrate.ts v8
src/main/db/repositories/writing-log.repo.ts 扩展 migratemilestones + pomodoro_daily
src/main/db/repositories/writing-milestone.repo.ts 成就 CRUD
src/main/db/repositories/pomodoro-daily.repo.ts 番茄日统计
src/main/db/repositories/knowledge-injection.repo.ts 每书 injection logs
src/main/services/pomodoro.service.ts 番茄状态机
src/main/services/achievement.service.ts 里程碑检测
src/main/services/goal-notification.service.ts Toast push + Notification
src/main/services/knowledge-injection.service.ts 注入记录
src/main/services/writing-log.service.ts addToday 后触发 goal-met + milestones
src/main/services/cockpit.service.ts achievements + pomodoroTodayCount
src/main/ipc/handlers/pomodoro.handler.ts 番茄 IPC
src/main/ipc/handlers/achievement.handler.ts 成就 IPC
src/main/ipc/handlers/knowledge.handler.ts listInjections
src/main/ipc/handlers/interactive.handler.ts confirmContext 挂钩
src/main/ipc/handlers/auto.handler.ts 同上
src/main/ipc/handlers/wizard.handler.ts 同上
src/main/ipc/register.ts 注入新服务
src/preload/index.ts + electron-api.d.ts 新 API + push 监听
src/renderer/components/layout/EditorLayout.tsx 番茄控件 + goal 通知
src/renderer/components/cockpit/CockpitModal.tsx 成就区
src/renderer/components/knowledge/KnowledgePanel.tsx 注入预览
src/renderer/components/knowledge/KnowledgeEditorDialog.tsx 完整历史
src/renderer/components/settings/SettingsPage.tsx 番茄/通知设置
src/renderer/stores/usePomodoroStore.ts 番茄 UI 状态
public/locales/*/translation.json P5.3/P6.1 文案
tests/main/pomodoro.test.ts UT-POM-*
tests/main/achievement.test.ts UT-ACH-* / IT-GOAL-01
tests/main/knowledge-injection.test.ts UT/IT-INJ-*
tests/main/migrate-v8.test.ts UT-MIG-08
e2e/writing-goals.spec.ts E2E-GOAL-*
e2e/knowledge-injection-history.spec.ts E2E-INJ-01

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

export type PomodoroDuration = 15 | 25 | 45
export type WritingMilestone = 'streak_7' | 'streak_30' | 'streak_100'
export type InjectionFlowMode = 'interactive' | 'auto' | 'wizard'

export interface PomodoroState {
  running: boolean
  paused: boolean
  remainingSec: number
  bookId: string | null
}

export interface WritingAchievement {
  milestone: WritingMilestone
  unlockedAt: string
}

export interface KnowledgeInjectionLog {
  id: string
  knowledgeEntryId: string
  flowMode: InjectionFlowMode
  flowId: string
  chapterId?: string
  injectedAt: string
}

export interface GoalNotificationPayload {
  kind: 'daily_met' | 'milestone' | 'pomodoro_complete' | 'pomodoro_cancelled_book_switch'
  messageKey: string
  messageParams?: Record<string, string | number>
}

扩展 CockpitSummary

achievements?: WritingAchievement[]
pomodoroTodayCount?: number

扩展 GlobalSettings

pomodoroDurationMinutes?: PomodoroDuration
enableSystemNotifications?: boolean
enableGoalNotifications?: boolean
dailyGoalNotifiedDate?: string  // YYYY-MM-DD,防重复达标 Toast
  • Step 2: IPC 通道
POMODORO_START: 'pomodoro:start',
POMODORO_PAUSE: 'pomodoro:pause',
POMODORO_RESUME: 'pomodoro:resume',
POMODORO_CANCEL: 'pomodoro:cancel',
POMODORO_GET_STATE: 'pomodoro:getState',
ACHIEVEMENT_LIST: 'achievement:list',
KNOWLEDGE_LIST_INJECTIONS: 'knowledge:listInjections',
POMODORO_TICK: 'pomodoro:tick',           // main → renderer push
GOAL_NOTIFICATION: 'goal:notification',     // main → renderer push
  • Step 3: global-settings 默认值
pomodoroDurationMinutes: 25,
enableSystemNotifications: false,
enableGoalNotifications: true,
  • Step 4: useSettingsStore 同步

  • Step 5: build

npm run build

Task 2: 全局 sqlite 扩展 + Milestone/Pomodoro Repos

Files:

  • Modify: src/main/db/repositories/writing-log.repo.ts

  • Create: src/main/db/repositories/writing-milestone.repo.ts

  • Create: src/main/db/repositories/pomodoro-daily.repo.ts

  • Create: tests/main/achievement.test.ts(先写 milestone repo 部分)

  • Step 1: writing-log.repo migrate 扩展

migrate() 追加:

CREATE TABLE IF NOT EXISTS writing_milestones (
  book_id TEXT NOT NULL, milestone TEXT NOT NULL, unlocked_at TEXT NOT NULL DEFAULT (datetime('now')),
  PRIMARY KEY (book_id, milestone)
);
CREATE TABLE IF NOT EXISTS pomodoro_daily (
  date TEXT NOT NULL, book_id TEXT NOT NULL,
  completed_count INTEGER NOT NULL DEFAULT 0, total_words INTEGER NOT NULL DEFAULT 0,
  PRIMARY KEY (date, book_id)
);
  • Step 2: WritingMilestoneRepository
export class WritingMilestoneRepository {
  constructor(private db: DatabaseSync) {}
  list(bookId: string): WritingAchievement[] { /* SELECT */ }
  has(bookId: string, milestone: WritingMilestone): boolean { /* ... */ }
  unlock(bookId: string, milestone: WritingMilestone): WritingAchievement { /* INSERT OR IGNORE */ }
}
  • Step 3: PomodoroDailyRepository
recordComplete(bookId: string, wordDelta: number, date?: string): void {
  // INSERT ... ON CONFLICT DO UPDATE completed_count += 1, total_words += wordDelta
}
getTodayCount(bookId: string): number
  • Step 4: UT-ACH repo smoke
it('UT-ACH-01: unlock streak_7 once', () => {
  const repo = new WritingMilestoneRepository(db)
  repo.unlock('b1', 'streak_7')
  expect(repo.has('b1', 'streak_7')).toBe(true)
})
  • Step 5: 运行
npm run test -- tests/main/achievement.test.ts -t "UT-ACH-01"

Task 3: schema v8 + KnowledgeInjectionRepository

Files:

  • Create: src/main/db/schema-v8.sql

  • Modify: src/main/db/migrate.ts

  • Create: src/main/db/repositories/knowledge-injection.repo.ts

  • Create: tests/main/migrate-v8.test.ts

  • Create: tests/main/knowledge-injection.test.ts

  • Step 1: schema-v8.sql(见 spec §3.2

  • Step 2: migrate.ts

import schemaV8 from './schema-v8.sql?raw'
const CURRENT_VERSION = 8

if (current < 8) {
  db.exec(schemaV8)
  db.prepare('INSERT INTO schema_version (version, description) VALUES (?, ?)').run(8, 'P5.3 P6.1 goals injection')
}
  • Step 3: KnowledgeInjectionRepository
insertBatch(rows: Array<Omit<KnowledgeInjectionLog, 'id' | 'injectedAt'>>): void
listForEntry(entryId: string, limit = 50): KnowledgeInjectionLog[]
  • Step 4: UT-MIG-08 + UT-INJ-01
it('UT-MIG-08: v7 database migrates to v8 with injection table', () => { /* ... */ })
it('UT-INJ-01: insertBatch creates 3 rows', () => { /* ... */ })
  • Step 5: 运行
npm run test -- tests/main/migrate-v8.test.ts tests/main/knowledge-injection.test.ts

Task 4: PomodoroService

Files:

  • Create: src/main/services/pomodoro.service.ts

  • Create: tests/main/pomodoro.test.ts

  • Step 1: PomodoroService 实现

export class PomodoroService {
  private state: PomodoroState = { running: false, paused: false, remainingSec: 0, bookId: null }
  private wordSnapshot = 0
  private timer: NodeJS.Timeout | null = null

  constructor(
    private writingLogs: WritingLogService,
    private pomodoroDaily: PomodoroDailyRepository,
    private settings: GlobalSettingsService,
    private notify: GoalNotificationService,
    private pushTick: (state: PomodoroState) => void
  ) {}

  start(bookId: string): PomodoroState {
    if (this.state.running && this.state.bookId !== bookId) this.cancel()
    const duration = this.resolveDurationSec()
    this.wordSnapshot = this.writingLogs.getStats(bookId).todayWords
    this.state = { running: true, paused: false, remainingSec: duration, bookId }
    this.startTimer()
    return this.state
  }

  private resolveDurationSec(): number {
    if (process.env.BILIN_E2E === '1' && process.env.POMODORO_TEST_SEC) {
      return Number(process.env.POMODORO_TEST_SEC) || 3
    }
    const mins = this.settings.get().pomodoroDurationMinutes ?? 25
    return mins * 60
  }

  private complete(): void {
    const bookId = this.state.bookId!
    const delta = Math.max(0, this.writingLogs.getStats(bookId).todayWords - this.wordSnapshot)
    this.pomodoroDaily.recordComplete(bookId, delta)
    this.notify.notifyPomodoroComplete(bookId, delta)
    this.reset()
  }
  // pause, resume, cancel, getState, tick...
}
  • Step 2: UT-POM-01 / UT-POM-02
it('UT-POM-01: complete records word delta', () => { /* mock writingLogs */ })
it('UT-POM-02: start different book cancels previous', () => { /* ... */ })
  • Step 3: 运行
npm run test -- tests/main/pomodoro.test.ts

Task 5: GoalNotificationService + AchievementService

Files:

  • Create: src/main/services/goal-notification.service.ts

  • Create: src/main/services/achievement.service.ts

  • Modify: src/main/services/writing-log.service.ts

  • Modify: tests/main/achievement.test.ts

  • Step 1: GoalNotificationService

export class GoalNotificationService {
  constructor(
    private settings: GlobalSettingsService,
    private getMainWindow: () => BrowserWindow | null
  ) {}

  pushToast(payload: GoalNotificationPayload): void {
    this.getMainWindow()?.webContents.send(IPC.GOAL_NOTIFICATION, payload)
  }

  maybeSystemNotify(title: string, body: string): void {
    if (!this.settings.get().enableSystemNotifications) return
    try {
      new Notification({ title, body }).show()
    } catch { /* ignore permission denied */ }
  }

  notifyDailyGoalMet(bookId: string, current: number, goal: number): void { /* ... */ }
  notifyMilestone(milestone: WritingMilestone): void { /* ... */ }
  notifyPomodoroComplete(bookId: string, words: number): void { /* ... */ }
}
  • Step 2: AchievementService
checkMilestones(bookId: string): WritingAchievement[] {
  const { dailyWordGoal } = this.settings.get()
  if (dailyWordGoal <= 0) return []
  const streak = this.writingLogs.getStats(bookId).streakDays
  const unlocked: WritingAchievement[] = []
  for (const [threshold, m] of [[7,'streak_7'],[30,'streak_30'],[100,'streak_100']] as const) {
    if (streak >= threshold && !this.repo.has(bookId, m)) {
      const a = this.repo.unlock(bookId, m)
      this.notify.notifyMilestone(m)
      unlocked.push(a)
    }
  }
  return unlocked
}
  • Step 3: WritingLogService.addToday 扩展

addToday 返回后:

const stats = this.getStats(bookId)
if (stats.dailyGoal > 0 && stats.todayWords >= stats.dailyGoal) {
  this.goalNotifications.maybeNotifyDailyMet(bookId, stats) // 内部检查 dailyGoalNotifiedDate
}
this.achievementService.checkMilestones(bookId)
  • Step 4: UT-ACH-02 + IT-GOAL-01

  • Step 5: 运行

npm run test -- tests/main/achievement.test.ts

Task 6: KnowledgeInjectionService + confirmContext 挂钩

Files:

  • Create: src/main/services/knowledge-injection.service.ts

  • Modify: src/main/ipc/handlers/interactive.handler.ts

  • Modify: src/main/ipc/handlers/auto.handler.ts

  • Modify: src/main/ipc/handlers/wizard.handler.ts

  • Modify: tests/main/knowledge-injection.test.ts

  • Step 1: KnowledgeInjectionService

export class KnowledgeInjectionService {
  record(bookDb: SqliteDb, input: {
    knowledgeIds: string[]
    flowMode: InjectionFlowMode
    flowId: string
    chapterId?: string
  }): void {
    if (input.knowledgeIds.length === 0) return
    const knowledgeRepo = new KnowledgeRepository(bookDb)
    const injectionRepo = new KnowledgeInjectionRepository(bookDb)
    const rows = input.knowledgeIds
      .filter((id) => knowledgeRepo.get(id)?.status === 'approved')
      .map((id) => ({ knowledgeEntryId: id, flowMode: input.flowMode, flowId: input.flowId, chapterId: input.chapterId }))
    injectionRepo.insertBatch(rows)
  }
}
  • Step 2: interactive.handler 挂钩

INTERACTIVE_CONFIRM_CONTEXTbuildFlowContextPayload 成功后:

try {
  new KnowledgeInjectionService().record(registry.getDb(bookId), {
    knowledgeIds: binding.knowledgeIds,
    flowMode: 'interactive',
    flowId,
    chapterId: registry.getMeta(bookId)?.lastChapterId ?? undefined
  })
} catch (err) {
  console.error('[knowledge-injection]', err)
}
  • Step 3: auto / wizard 同理

  • Step 4: IT-INJ-01

直接调用 handler 或 service + 内存 DB,断言 knowledge_injection_logs 行数。

  • Step 5: 运行
npm run test -- tests/main/knowledge-injection.test.ts

Task 7: IPC + register + preload + Cockpit 扩展

Files:

  • Create: src/main/ipc/handlers/pomodoro.handler.ts

  • Create: src/main/ipc/handlers/achievement.handler.ts

  • Modify: src/main/ipc/handlers/knowledge.handler.ts

  • Modify: src/main/ipc/register.ts

  • Modify: src/main/services/cockpit.service.ts

  • Modify: src/preload/index.ts

  • Modify: src/shared/electron-api.d.ts

  • Step 1: pomodoro.handler + achievement.handler

  • Step 2: knowledge.handler

ipcMain.handle(IPC.KNOWLEDGE_LIST_INJECTIONS, (_e, { bookId, entryId, limit }) =>
  wrap(() => new KnowledgeInjectionService(registry.getDb(bookId)).listForEntry(entryId, limit))
)
  • Step 3: register.ts 注入
const goalNotify = new GoalNotificationService(settings, () => getMainWindow())
const achievementService = new AchievementService(writingLogs, milestoneRepo, settings, goalNotify)
writingLogs.setAchievementHooks(achievementService, goalNotify)
const pomodoro = new PomodoroService(writingLogs, pomodoroDailyRepo, settings, goalNotify, pushTick)
registerPomodoroHandlers(pomodoro)
registerAchievementHandlers(achievementService)
  • Step 4: preload
pomodoro: {
  start: (bookId: string) => ipcRenderer.invoke(IPC.POMODORO_START, { bookId }),
  pause: () => ipcRenderer.invoke(IPC.POMODORO_PAUSE),
  resume: () => ipcRenderer.invoke(IPC.POMODORO_RESUME),
  cancel: () => ipcRenderer.invoke(IPC.POMODORO_CANCEL),
  getState: () => ipcRenderer.invoke(IPC.POMODORO_GET_STATE)
},
onPomodoroTick: (cb) => { ipcRenderer.on(IPC.POMODORO_TICK, cb); return () => ... },
onGoalNotification: (cb) => { ipcRenderer.on(IPC.GOAL_NOTIFICATION, cb); return () => ... },
  • Step 5: CockpitService 扩展
achievements: this.achievementService.listAchievements(bookId),
pomodoroTodayCount: this.pomodoroDaily.getTodayCount(bookId),
  • Step 6: build
npm run build

Task 8: 状态栏番茄 + Goal 通知监听

Files:

  • Create: src/renderer/stores/usePomodoroStore.ts

  • Modify: src/renderer/components/layout/EditorLayout.tsx

  • Modify: src/renderer/App.tsx(注册 onGoalNotification 全局 Toast

  • Modify: src/renderer/styles/layout.css

  • Step 1: usePomodoroStore

订阅 onPomodoroTickactionsstart/pause/resume/cancel

  • Step 2: EditorLayout 状态栏控件
<div className="pomodoro-control" data-testid="pomodoro-control">
  {!running ? (
    <button onClick={() => void start(currentBookId!)}></button>
  ) : (
    <>
      <button onClick={() => void (paused ? resume() : pause())}>{paused ? '▶' : '⏸'}</button>
      <span>{formatMmSs(remainingSec)}</span>
      <button onClick={() => void cancel()}></button>
    </>
  )}
</div>
  • Step 3: App.tsx goal notification
useEffect(() => {
  return window.electronAPI.onGoalNotification((payload) => {
    showToast(t(payload.messageKey, payload.messageParams))
  })
}, [])
  • Step 4: build 验证
npm run build

Task 9: 驾驶舱成就区 + 设置页

Files:

  • Modify: src/renderer/components/cockpit/CockpitModal.tsx

  • Modify: src/renderer/components/settings/SettingsPage.tsx

  • Modify: public/locales/zh-CN/translation.json

  • Modify: public/locales/en/translation.json

  • Step 1: CockpitModal 成就区

{summary.achievements && summary.achievements.length > 0 && (
  <div data-testid="cockpit-achievements">...</div>
)}
<div data-testid="cockpit-pomodoro-today">
  {t('cockpit.pomodoroToday', { count: summary.pomodoroTodayCount ?? 0 })}
</div>
  • Step 2: SettingsPage 三项设置

settings-pomodoro-duration / settings-system-notifications / settings-goal-notifications

  • Step 3: i18nspec §7 全部键,中英)

  • 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

  • Step 1: KnowledgePanel 加载注入预览

条目 render 时调用 knowledge.listInjections(bookId, entry.id, 3)(或 batch 加载 map)。

<div data-testid={`knowledge-injection-preview-${entry.id}`}>
  {injections.map((log) => (
    <div key={log.id}>{t('knowledge.injectionEntry', { date, mode, chapter })}</div>
  ))}
</div>
  • Step 2: KnowledgeEditorDialog 完整历史

data-testid="knowledge-injection-history";章节名点击 switchTarget({ kind:'chapter', id })

  • Step 3: build
npm run build

Task 11: E2E + v0.9.0 交付

Files:

  • Create: e2e/writing-goals.spec.ts

  • Create: e2e/knowledge-injection-history.spec.ts

  • Modify: package.json

  • Modify: README.md

  • Modify: tests/main/migrate-v7.test.ts 等(若 CURRENT_VERSION→8,期望版本改为 8

  • Step 1: E2E-GOAL-01~03

  • E2E-GOAL-01:设 dailyWordGoal=100,打字保存,断言 toast / status-daily-goal

  • E2E-GOAL-02:通过测试 hook 或 seed API 写入 streak_7 milestone,打开驾驶舱断言 cockpit-achievements

  • E2E-GOAL-03POMODORO_TEST_SEC=3,启动番茄,等待完成 Toast

  • Step 2: E2E-INJ-01

向导 confirm 知识上下文(可 stub 空 AI),断言 knowledge-injection-preview-* 可见。

  • Step 3: 全量测试
npm run build
npm run test
npm run test:e2e -- e2e/writing-goals.spec.ts e2e/knowledge-injection-history.spec.ts
  • Step 4: 版本 0.9.0

package.jsonREADME.mdSettingsPage about 版本号。

  • Step 5: 更新 migrate-v2~v7 测试期望版本 → 8(与 P5.2 v7 时同样处理)

Spec Coverage Checklist

Spec 要求 Task
番茄钟 15/25/45 + 状态栏 UI Task 1, 4, 8
番茄完成字数 + pomodoro_daily Task 2, 4
7/30/100 里程碑 + 成就区 Task 2, 5, 9
Toast + 系统通知 + 设置开关 Task 1, 5, 8, 9
日更首次达标通知 Task 5
knowledge_injection_logs v8 Task 3
三模式 confirmContext 挂钩 Task 6
知识库注入预览 + 完整历史 Task 10
UT/IT/E2E Task 26, 11
v0.9.0 DoD Task 11

Execution Notes

  • WritingLogService 需轻量 refactor 接受 AchievementService / GoalNotificationService 回调(constructor 注入或 setter,避免循环依赖:Achievement 依赖 WritingLogWritingLog 回调 Achievement
  • 切换书籍时:useBookStore 订阅 currentBookId 变化 → 若 pomodoro running 且 bookId 不同 → cancel() + toast pomodoro.switchedBook
  • 注入历史 E2E 禁止 Mock AI 指生成类测试;confirmContext 本身不调用 AI,可纯 UI 流程