Files
bilin/docs/superpowers/plans/2026-07-07-bilin-p53-p61-goals-injection.md
T
bing d4122c8f95 feat: ship v0.9.0 with pomodoro, achievements, and injection history
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>
2026-07-07 19:15:52 +08:00

686 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 笔临 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` | 扩展 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`
- [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 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 流程