d4122c8f95
Add pomodoro timer with goal notifications, writing streak milestones, and knowledge injection logging with UI previews across AI writing flows. Co-authored-by: Cursor <cursoragent@cursor.com>
686 lines
21 KiB
Markdown
686 lines
21 KiB
Markdown
# 笔临 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 (`- [x]`) 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_logs`;`interactive/auto/wizard` 的 `confirmContext` 成功后异步写 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=1` 且 `POMODORO_TEST_SEC=3` 时覆盖倒计时(仅测试,不暴露生产 UI)
|
||
|
||
---
|
||
|
||
## File Map
|
||
|
||
| 文件 | 职责 |
|
||
|------|------|
|
||
| `src/shared/types.ts` | `PomodoroState`、`WritingAchievement`、`KnowledgeInjectionLog`;扩展 `CockpitSummary`、`GlobalSettings` |
|
||
| `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` | 扩展 migrate:milestones + 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`
|
||
|
||
- [x] **Step 1: 扩展 types**
|
||
|
||
```typescript
|
||
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`:
|
||
|
||
```typescript
|
||
achievements?: WritingAchievement[]
|
||
pomodoroTodayCount?: number
|
||
```
|
||
|
||
扩展 `GlobalSettings`:
|
||
|
||
```typescript
|
||
pomodoroDurationMinutes?: PomodoroDuration
|
||
enableSystemNotifications?: boolean
|
||
enableGoalNotifications?: boolean
|
||
dailyGoalNotifiedDate?: string // YYYY-MM-DD,防重复达标 Toast
|
||
```
|
||
|
||
- [x] **Step 2: IPC 通道**
|
||
|
||
```typescript
|
||
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
|
||
```
|
||
|
||
- [x] **Step 3: global-settings 默认值**
|
||
|
||
```typescript
|
||
pomodoroDurationMinutes: 25,
|
||
enableSystemNotifications: false,
|
||
enableGoalNotifications: true,
|
||
```
|
||
|
||
- [x] **Step 4: useSettingsStore 同步**
|
||
|
||
- [x] **Step 5: build**
|
||
|
||
```bash
|
||
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 部分)
|
||
|
||
- [x] **Step 1: writing-log.repo migrate 扩展**
|
||
|
||
在 `migrate()` 追加:
|
||
|
||
```sql
|
||
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)
|
||
);
|
||
```
|
||
|
||
- [x] **Step 2: WritingMilestoneRepository**
|
||
|
||
```typescript
|
||
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 */ }
|
||
}
|
||
```
|
||
|
||
- [x] **Step 3: PomodoroDailyRepository**
|
||
|
||
```typescript
|
||
recordComplete(bookId: string, wordDelta: number, date?: string): void {
|
||
// INSERT ... ON CONFLICT DO UPDATE completed_count += 1, total_words += wordDelta
|
||
}
|
||
getTodayCount(bookId: string): number
|
||
```
|
||
|
||
- [x] **Step 4: UT-ACH repo smoke**
|
||
|
||
```typescript
|
||
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)
|
||
})
|
||
```
|
||
|
||
- [x] **Step 5: 运行**
|
||
|
||
```bash
|
||
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`
|
||
|
||
- [x] **Step 1: schema-v8.sql**(见 spec §3.2)
|
||
|
||
- [x] **Step 2: migrate.ts**
|
||
|
||
```typescript
|
||
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')
|
||
}
|
||
```
|
||
|
||
- [x] **Step 3: KnowledgeInjectionRepository**
|
||
|
||
```typescript
|
||
insertBatch(rows: Array<Omit<KnowledgeInjectionLog, 'id' | 'injectedAt'>>): void
|
||
listForEntry(entryId: string, limit = 50): KnowledgeInjectionLog[]
|
||
```
|
||
|
||
- [x] **Step 4: UT-MIG-08 + UT-INJ-01**
|
||
|
||
```typescript
|
||
it('UT-MIG-08: v7 database migrates to v8 with injection table', () => { /* ... */ })
|
||
it('UT-INJ-01: insertBatch creates 3 rows', () => { /* ... */ })
|
||
```
|
||
|
||
- [x] **Step 5: 运行**
|
||
|
||
```bash
|
||
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`
|
||
|
||
- [x] **Step 1: PomodoroService 实现**
|
||
|
||
```typescript
|
||
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...
|
||
}
|
||
```
|
||
|
||
- [x] **Step 2: UT-POM-01 / UT-POM-02**
|
||
|
||
```typescript
|
||
it('UT-POM-01: complete records word delta', () => { /* mock writingLogs */ })
|
||
it('UT-POM-02: start different book cancels previous', () => { /* ... */ })
|
||
```
|
||
|
||
- [x] **Step 3: 运行**
|
||
|
||
```bash
|
||
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`
|
||
|
||
- [x] **Step 1: GoalNotificationService**
|
||
|
||
```typescript
|
||
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 { /* ... */ }
|
||
}
|
||
```
|
||
|
||
- [x] **Step 2: AchievementService**
|
||
|
||
```typescript
|
||
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
|
||
}
|
||
```
|
||
|
||
- [x] **Step 3: WritingLogService.addToday 扩展**
|
||
|
||
`addToday` 返回后:
|
||
|
||
```typescript
|
||
const stats = this.getStats(bookId)
|
||
if (stats.dailyGoal > 0 && stats.todayWords >= stats.dailyGoal) {
|
||
this.goalNotifications.maybeNotifyDailyMet(bookId, stats) // 内部检查 dailyGoalNotifiedDate
|
||
}
|
||
this.achievementService.checkMilestones(bookId)
|
||
```
|
||
|
||
- [x] **Step 4: UT-ACH-02 + IT-GOAL-01**
|
||
|
||
- [x] **Step 5: 运行**
|
||
|
||
```bash
|
||
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`
|
||
|
||
- [x] **Step 1: KnowledgeInjectionService**
|
||
|
||
```typescript
|
||
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)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [x] **Step 2: interactive.handler 挂钩**
|
||
|
||
在 `INTERACTIVE_CONFIRM_CONTEXT` 的 `buildFlowContextPayload` 成功后:
|
||
|
||
```typescript
|
||
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)
|
||
}
|
||
```
|
||
|
||
- [x] **Step 3: auto / wizard 同理**
|
||
|
||
- [x] **Step 4: IT-INJ-01**
|
||
|
||
直接调用 handler 或 service + 内存 DB,断言 `knowledge_injection_logs` 行数。
|
||
|
||
- [x] **Step 5: 运行**
|
||
|
||
```bash
|
||
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`
|
||
|
||
- [x] **Step 1: pomodoro.handler + achievement.handler**
|
||
|
||
- [x] **Step 2: knowledge.handler**
|
||
|
||
```typescript
|
||
ipcMain.handle(IPC.KNOWLEDGE_LIST_INJECTIONS, (_e, { bookId, entryId, limit }) =>
|
||
wrap(() => new KnowledgeInjectionService(registry.getDb(bookId)).listForEntry(entryId, limit))
|
||
)
|
||
```
|
||
|
||
- [x] **Step 3: register.ts 注入**
|
||
|
||
```typescript
|
||
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)
|
||
```
|
||
|
||
- [x] **Step 4: preload**
|
||
|
||
```typescript
|
||
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 () => ... },
|
||
```
|
||
|
||
- [x] **Step 5: CockpitService 扩展**
|
||
|
||
```typescript
|
||
achievements: this.achievementService.listAchievements(bookId),
|
||
pomodoroTodayCount: this.pomodoroDaily.getTodayCount(bookId),
|
||
```
|
||
|
||
- [x] **Step 6: build**
|
||
|
||
```bash
|
||
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`
|
||
|
||
- [x] **Step 1: usePomodoroStore**
|
||
|
||
订阅 `onPomodoroTick`;actions:`start/pause/resume/cancel`。
|
||
|
||
- [x] **Step 2: EditorLayout 状态栏控件**
|
||
|
||
```tsx
|
||
<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>
|
||
```
|
||
|
||
- [x] **Step 3: App.tsx goal notification**
|
||
|
||
```typescript
|
||
useEffect(() => {
|
||
return window.electronAPI.onGoalNotification((payload) => {
|
||
showToast(t(payload.messageKey, payload.messageParams))
|
||
})
|
||
}, [])
|
||
```
|
||
|
||
- [x] **Step 4: build 验证**
|
||
|
||
```bash
|
||
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`
|
||
|
||
- [x] **Step 1: CockpitModal 成就区**
|
||
|
||
```tsx
|
||
{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>
|
||
```
|
||
|
||
- [x] **Step 2: SettingsPage 三项设置**
|
||
|
||
`settings-pomodoro-duration` / `settings-system-notifications` / `settings-goal-notifications`
|
||
|
||
- [x] **Step 3: i18n**(spec §7 全部键,中英)
|
||
|
||
- [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`
|
||
|
||
- [x] **Step 1: KnowledgePanel 加载注入预览**
|
||
|
||
条目 render 时调用 `knowledge.listInjections(bookId, entry.id, 3)`(或 batch 加载 map)。
|
||
|
||
```tsx
|
||
<div data-testid={`knowledge-injection-preview-${entry.id}`}>
|
||
{injections.map((log) => (
|
||
<div key={log.id}>{t('knowledge.injectionEntry', { date, mode, chapter })}</div>
|
||
))}
|
||
</div>
|
||
```
|
||
|
||
- [x] **Step 2: KnowledgeEditorDialog 完整历史**
|
||
|
||
`data-testid="knowledge-injection-history"`;章节名点击 `switchTarget({ kind:'chapter', id })`。
|
||
|
||
- [x] **Step 3: build**
|
||
|
||
```bash
|
||
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)
|
||
|
||
- [x] **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-03:`POMODORO_TEST_SEC=3`,启动番茄,等待完成 Toast
|
||
|
||
- [x] **Step 2: E2E-INJ-01**
|
||
|
||
向导 confirm 知识上下文(可 stub 空 AI),断言 `knowledge-injection-preview-*` 可见。
|
||
|
||
- [x] **Step 3: 全量测试**
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
npm run test:e2e -- e2e/writing-goals.spec.ts e2e/knowledge-injection-history.spec.ts
|
||
```
|
||
|
||
- [x] **Step 4: 版本 0.9.0**
|
||
|
||
`package.json`、`README.md`、`SettingsPage` about 版本号。
|
||
|
||
- [x] **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 2–6, 11 |
|
||
| v0.9.0 DoD | Task 11 |
|
||
|
||
---
|
||
|
||
## Execution Notes
|
||
|
||
- `WritingLogService` 需轻量 refactor 接受 `AchievementService` / `GoalNotificationService` 回调(constructor 注入或 setter,避免循环依赖:Achievement 依赖 WritingLog,WritingLog 回调 Achievement)
|
||
- 切换书籍时:`useBookStore` 订阅 `currentBookId` 变化 → 若 pomodoro running 且 bookId 不同 → `cancel()` + toast `pomodoro.switchedBook`
|
||
- 注入历史 E2E **禁止 Mock AI** 指生成类测试;confirmContext 本身不调用 AI,可纯 UI 流程
|