feat: ship v1.0.0 with character graph and writing analytics

Add P7 Cytoscape relationship graph with setting CRUD, P9 cockpit analytics driven by writing_sessions, chapter POV selection, and full test coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-08 10:48:31 +08:00
parent 2c9b034a29
commit 4adafa1936
44 changed files with 2062 additions and 602 deletions
@@ -1,6 +1,6 @@
# 笔临 P7 角色关系图谱 + P9 个人写作分析 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.
> **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:** 交付 v1.0.0:§7 角色关系图谱(Cytoscape + 设定关系 CRUD+ §9 个人写作分析(`writing_sessions` 驱动速度/时段热力 + POV/卷分布),驾驶舱内嵌展示。
@@ -53,7 +53,7 @@
- Modify: `src/shared/types.ts`
- Modify: `src/shared/ipc-channels.ts`
- [ ] **Step 1: 扩展 types**
- [x] **Step 1: 扩展 types**
```typescript
export interface CharacterRelationship {
@@ -97,7 +97,7 @@ export interface WritingAnalyticsSummary {
`SettingEntry.properties` 使用文档约定 `relationships?: CharacterRelationship[]`JSDoc 或 inline 注释即可)。
- [ ] **Step 2: IPC 通道**
- [x] **Step 2: IPC 通道**
```typescript
GRAPH_GET_DATA: 'graph:getData',
@@ -106,7 +106,7 @@ GRAPH_DELETE_RELATIONSHIP: 'graph:deleteRelationship',
ANALYTICS_SUMMARY: 'analytics:summary',
```
- [ ] **Step 3: build 验证**
- [x] **Step 3: build 验证**
```bash
npm run build
@@ -121,7 +121,7 @@ npm run build
- Create: `src/main/db/repositories/writing-session.repo.ts`
- Create: `tests/main/writing-session.test.ts`repo 部分)
- [ ] **Step 1: writing-log.repo migrate 扩展**
- [x] **Step 1: writing-log.repo migrate 扩展**
`migrate()``CREATE TABLE` 块追加:
@@ -139,7 +139,7 @@ CREATE INDEX IF NOT EXISTS idx_sessions_book_date ON writing_sessions (book_id,
CREATE INDEX IF NOT EXISTS idx_sessions_book_started ON writing_sessions (book_id, started_at);
```
- [ ] **Step 2: WritingSessionRepository**
- [x] **Step 2: WritingSessionRepository**
```typescript
export class WritingSessionRepository {
@@ -153,7 +153,7 @@ export class WritingSessionRepository {
}
```
- [ ] **Step 3: UT repo smoke**
- [x] **Step 3: UT repo smoke**
```typescript
it('UT-SESSION-03: insert and updateActivity accumulates word_delta', () => {
@@ -165,7 +165,7 @@ it('UT-SESSION-03: insert and updateActivity accumulates word_delta', () => {
})
```
- [ ] **Step 4: 运行**
- [x] **Step 4: 运行**
```bash
npm run test -- tests/main/writing-session.test.ts
@@ -179,7 +179,7 @@ npm run test -- tests/main/writing-session.test.ts
- Create: `src/main/services/writing-session.service.ts`
- Modify: `tests/main/writing-session.test.ts`
- [ ] **Step 1: 实现**
- [x] **Step 1: 实现**
```typescript
const IDLE_MS = 5 * 60 * 1000
@@ -217,7 +217,7 @@ export class WritingSessionService {
}
```
- [ ] **Step 2: UT-SESSION-01 / UT-SESSION-02**
- [x] **Step 2: UT-SESSION-01 / UT-SESSION-02**
使用 `vi.useFakeTimers()`
@@ -226,7 +226,7 @@ it('UT-SESSION-01: within 5min merges into one session', () => { /* advance 2min
it('UT-SESSION-02: after 6min creates two sessions', () => { /* advance 6min between calls */ })
```
- [ ] **Step 3: 运行**
- [x] **Step 3: 运行**
```bash
npm run test -- tests/main/writing-session.test.ts
@@ -243,7 +243,7 @@ npm run test -- tests/main/writing-session.test.ts
- Modify: `src/shared/types.ts`UpdateChapterParams
- Modify: `src/main/ipc/register.ts`trackerFor 注入 sessions
- [ ] **Step 1: chapter.repo mapRow + update**
- [x] **Step 1: chapter.repo mapRow + update**
```typescript
// mapRow 增加
@@ -253,7 +253,7 @@ povCharacterId: (row.pov_character_id as string) || null,
// UPDATE SQL 增加 pov_character_id = ?
```
- [ ] **Step 2: trackerFor 签名**
- [x] **Step 2: trackerFor 签名**
```typescript
export function trackerFor(
@@ -279,7 +279,7 @@ supplementOnFinish(...) {
}
```
- [ ] **Step 3: register.ts**
- [x] **Step 3: register.ts**
```typescript
const writingSessionRepo = new WritingSessionRepository(writingLogRepo.getDb())
@@ -287,7 +287,7 @@ const writingSessions = new WritingSessionService(writingSessionRepo)
// registerChapterHandlers(books, writingLogs, writingSessions)
```
- [ ] **Step 4: build**
- [x] **Step 4: build**
```bash
npm run build
@@ -301,7 +301,7 @@ npm run build
- Create: `src/main/services/character-graph.service.ts`
- Create: `tests/main/character-graph.test.ts`
- [ ] **Step 1: getData 边去重**
- [x] **Step 1: getData 边去重**
```typescript
export class CharacterGraphService {
@@ -319,7 +319,7 @@ export class CharacterGraphService {
}
```
- [ ] **Step 2: upsertRelationship / deleteRelationship**
- [x] **Step 2: upsertRelationship / deleteRelationship**
```typescript
upsertRelationship(bookId, sourceId, input: Partial<CharacterRelationship> & { targetId: string; label: string; intimacy: number }): CharacterRelationship {
@@ -331,11 +331,11 @@ upsertRelationship(bookId, sourceId, input: Partial<CharacterRelationship> & { t
deleteRelationship(bookId, sourceId, relationshipId): void
```
- [ ] **Step 3: UT-GRAPH-01 / UT-GRAPH-02 / UT-GRAPH-03**
- [x] **Step 3: UT-GRAPH-01 / UT-GRAPH-02 / UT-GRAPH-03**
内存 DB + migrate + 创建 3 character settings + 2 relationships。
- [ ] **Step 4: 运行**
- [x] **Step 4: 运行**
```bash
npm run test -- tests/main/character-graph.test.ts
@@ -349,7 +349,7 @@ npm run test -- tests/main/character-graph.test.ts
- Create: `src/main/services/writing-analytics.service.ts`
- Create: `tests/main/writing-analytics.test.ts`
- [ ] **Step 1: getSummary 实现**
- [x] **Step 1: getSummary 实现**
```typescript
export class WritingAnalyticsService {
@@ -385,9 +385,9 @@ function computeWpm(sessions: { wordDelta: number; startedAt: string; endedAt: s
}
```
- [ ] **Step 2: UT-ANALYTICS-01 / UT-ANALYTICS-02**
- [x] **Step 2: UT-ANALYTICS-01 / UT-ANALYTICS-02**
- [ ] **Step 3: IT-ANALYTICS-01**
- [x] **Step 3: IT-ANALYTICS-01**
```typescript
it('IT-ANALYTICS-01: recordDelta feeds session then getSummary', () => {
@@ -398,7 +398,7 @@ it('IT-ANALYTICS-01: recordDelta feeds session then getSummary', () => {
})
```
- [ ] **Step 4: 运行**
- [x] **Step 4: 运行**
```bash
npm run test -- tests/main/writing-analytics.test.ts
@@ -416,7 +416,7 @@ npm run test -- tests/main/writing-analytics.test.ts
- Modify: `src/preload/index.ts`
- Modify: `src/shared/electron-api.d.ts`
- [ ] **Step 1: graph.handler**
- [x] **Step 1: graph.handler**
```typescript
export function registerGraphHandlers(registry: BookRegistryService): void {
@@ -429,7 +429,7 @@ export function registerGraphHandlers(registry: BookRegistryService): void {
}
```
- [ ] **Step 2: analytics.handler**
- [x] **Step 2: analytics.handler**
```typescript
export function registerAnalyticsHandlers(
@@ -443,7 +443,7 @@ export function registerAnalyticsHandlers(
}
```
- [ ] **Step 3: register.ts 串联**
- [x] **Step 3: register.ts 串联**
```typescript
const writingSessionRepo = new WritingSessionRepository(writingLogRepo.getDb())
@@ -453,7 +453,7 @@ registerAnalyticsHandlers(books, writingLogs, writingSessionRepo, pomodoroDailyR
registerChapterHandlers(books, writingLogs, writingSessions)
```
- [ ] **Step 4: preload**
- [x] **Step 4: preload**
```typescript
graph: {
@@ -466,7 +466,7 @@ analytics: {
}
```
- [ ] **Step 5: build**
- [x] **Step 5: build**
```bash
npm run build
@@ -484,14 +484,14 @@ npm run build
- Modify: `src/renderer/components/layout/EditorLayout.tsx`(挂载模态)
- Modify: `src/renderer/styles/layout.css`
- [ ] **Step 1: 安装依赖**
- [x] **Step 1: 安装依赖**
```bash
npm install cytoscape cytoscape-cose-bilkent --legacy-peer-deps
npm install -D @types/cytoscape --legacy-peer-deps
```
- [ ] **Step 2: useGraphStore**
- [x] **Step 2: useGraphStore**
```typescript
interface GraphStore {
@@ -504,7 +504,7 @@ interface GraphStore {
}
```
- [ ] **Step 3: CharacterGraphModal 骨架**
- [x] **Step 3: CharacterGraphModal 骨架**
```tsx
export function CharacterGraphModal(): React.JSX.Element | null {
@@ -532,9 +532,9 @@ export function CharacterGraphModal(): React.JSX.Element | null {
`initCytoscape` 注册 `cytoscape.use(coseBilkent)`,边颜色按 intimacy 映射 spec §6.1。
- [ ] **Step 4: EditorLayout 挂载 `<CharacterGraphModal />`**
- [x] **Step 4: EditorLayout 挂载 `<CharacterGraphModal />`**
- [ ] **Step 5: build**
- [x] **Step 5: build**
```bash
npm run build
@@ -549,7 +549,7 @@ npm run build
- Modify: `src/renderer/components/editor/TipTapEditor.tsx`character setting 下方嵌入)
- Modify: `src/renderer/components/setting/SettingList.tsx`setting-open-graph 按钮)
- [ ] **Step 1: SettingRelationshipsEditor**
- [x] **Step 1: SettingRelationshipsEditor**
`target.kind === 'setting'` 且 entry.type === 'character'
@@ -566,7 +566,7 @@ npm run build
保存调用 `window.electronAPI.graph.upsertRelationship`
- [ ] **Step 2: SettingList 工具栏**
- [x] **Step 2: SettingList 工具栏**
```tsx
<button data-testid="setting-open-graph" onClick={() => useGraphStore.getState().openModal()}>
@@ -574,7 +574,7 @@ npm run build
</button>
```
- [ ] **Step 3: build**
- [x] **Step 3: build**
```bash
npm run build
@@ -591,7 +591,7 @@ npm run build
- Modify: `src/renderer/components/layout/EditorLayout.tsx`chapter-pov-select
- Modify: `src/renderer/stores/useCockpitStore.ts`(可选:loadAnalytics
- [ ] **Step 1: CockpitModal 并行加载**
- [x] **Step 1: CockpitModal 并行加载**
```typescript
useEffect(() => {
@@ -603,7 +603,7 @@ useEffect(() => {
}, [open, bookId, activeVolumeId])
```
- [ ] **Step 2: CockpitAnalytics 组件**
- [x] **Step 2: CockpitAnalytics 组件**
```tsx
<div className="cockpit-analytics" data-testid="cockpit-analytics">
@@ -618,7 +618,7 @@ useEffect(() => {
</button>
```
- [ ] **Step 3: chapter-pov-select**
- [x] **Step 3: chapter-pov-select**
`#editor-statusbar``isChapterTarget`
@@ -635,7 +635,7 @@ useEffect(() => {
`updatePov``chapter.update(bookId, chapterId, { povCharacterId })`.
- [ ] **Step 4: build**
- [x] **Step 4: build**
```bash
npm run build
@@ -650,9 +650,9 @@ npm run build
- Modify: `public/locales/en/translation.json`
- Modify: `src/renderer/styles/layout.css`
- [ ] **Step 1: 添加 spec §7 全部 i18n 键(graph / relationship / analytics / chapter.pov / cockpit.openGraph**
- [x] **Step 1: 添加 spec §7 全部 i18n 键(graph / relationship / analytics / chapter.pov / cockpit.openGraph**
- [ ] **Step 2: CSS**
- [x] **Step 2: CSS**
```css
.character-graph-modal .modal-content { max-width: 95vw; width: 1100px; }
@@ -664,7 +664,7 @@ npm run build
.pov-bar, .volume-bar { height: 8px; background: var(--accent); border-radius: 4px; }
```
- [ ] **Step 3: build**
- [x] **Step 3: build**
```bash
npm run build
@@ -681,7 +681,7 @@ npm run build
- Modify: `README.md`
- Modify: `src/renderer/components/settings/SettingsPage.tsx`
- [ ] **Step 1: E2E-GRAPH-01~04**
- [x] **Step 1: E2E-GRAPH-01~04**
```typescript
// 创建书 → 新建 character 设定 A/B → SettingRelationshipsEditor 添加关系
@@ -690,14 +690,14 @@ npm run build
// → graph-layout-circle click
```
- [ ] **Step 2: E2E-ANALYTICS-01~02**
- [x] **Step 2: E2E-ANALYTICS-01~02**
```typescript
// E2E-ANALYTICS-01: 写作保存 → topbar-cockpit → cockpit-speed-today 非空
// E2E-ANALYTICS-02: chapter-pov-select 选角色 → 刷新驾驶舱 → cockpit-pov-chart 含角色名
```
- [ ] **Step 3: 全量测试**
- [x] **Step 3: 全量测试**
```bash
npm run build
@@ -705,11 +705,11 @@ npm run test
npm run test:e2e -- e2e/character-graph.spec.ts e2e/writing-analytics.spec.ts
```
- [ ] **Step 4: 版本 1.0.0**
- [x] **Step 4: 版本 1.0.0**
`package.json` / `README.md` / Settings 关于页 → `v1.0.0`
- [ ] **Step 5: 勾选本 plan 全部 checkbox**
- [x] **Step 5: 勾选本 plan 全部 checkbox**
---