Files
bilin/docs/superpowers/specs/2026-07-08-bilin-p7-graph-p9-analytics-design.md
bing e64353f608 docs: add P7 character graph and P9 writing analytics design spec for v1.0.0
Define Cytoscape relationship graph MVP, writing_sessions telemetry, and cockpit analytics cards for the v1.0.0 release.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 10:04:56 +08:00

445 lines
18 KiB
Markdown
Raw Permalink 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.
# 笔临 P7 角色关系图谱 + P9 个人写作分析 设计规格
**版本**1.0
**日期**2026-07-08
**范围**:§7 角色关系图谱 MVP + §9 个人写作分析 MVP(含 `writing_sessions` 精确采集)
**前置**P5.3 + P6.1 已交付(v0.9.0`d4122c8`);P5.2 写作日志 / 热力图 / 番茄(v0.8.0)
**参考**`design/readme.md` v1.1 §7 / §9、`design/ui_pc.html` `#graphModal` / `#dashboardModal`
**目标版本**v1.0.0
---
## 1. 背景与目标
### 1.1 阶段定位
v0.9.0 已完成作家工作流核心链路(驾驶舱、日志、番茄、知识库、AI 上下文、注入历史)。`design/readme.md` §7 **角色关系图谱** 与 §9 **个人写作分析** 仍为概念/原型状态(`#graphModal` 为静态 demo,分析卡片无真实数据源)。
本阶段(v1.0.0)统一 MVP 交付:
- **P7(§7**:基于 `settings` 角色设定 + `properties.relationships` 的可视化关系图谱
- **P9(§9**:基于 `writing_sessions` + 现有 `writing_logs` / `pomodoro_daily` 的驾驶舱分析区
### 1.2 已确认决策(Brainstorming 2026-07-08
| 维度 | 决策 |
|------|------|
| 交付策略 | **方案 3**C + D 统一 MVP,单版 v1.0.0 |
| 分析数据采集 | **方案 A**:新增 `writing_sessions` 表,编辑活跃时精确记录 |
| 关系存储 | **`settings.properties.relationships[]` JSON**,不新增每书关系表 |
| 每书 schema | **保持 v8**,不迁移;`chapters.pov_character_id` 已有,补 UI |
| 全局 sqlite | **扩展 `writing_logs.sqlite`**(与 `writing_logs` / `pomodoro_daily` 同库) |
| 图谱库 | **Cytoscape.js + cose-bilkent**(渲染进程) |
| Session 合并 | 同一书籍连续写作,间隔 **≤ 5 分钟** 合并为同一 session |
| 图谱上限 | **100 角色节点**;超出 toast 提示,仍可按「仅有关系的节点」筛选展示 |
| 边去重 | 单向存储;渲染时 A↔B 去重,取较高 `intimacy` |
| 分析入口 | **仅驾驶舱内嵌卡片**,不做独立全屏分析页 |
| POV | 章节元数据下拉绑定 `pov_character_id` → 分析 POV 字数分布 |
### 1.3 验收标准
用户能完成:
1. 在设定面板为角色添加/编辑/删除关系(目标角色、描述、亲密度 1–5),保存后在关系图谱中可见对应边
2. 打开 **角色关系图谱** 全屏模态,看到力导向布局下的角色节点与关系边;可切换 grid / circle 布局
3. 点击节点 → 侧栏显示角色名与「编辑设定」跳转;点击边 → 侧栏编辑关系描述与亲密度
4. 在章节编辑区设置 **POV 角色**,驾驶舱 POV 分布反映该角色章节字数
5. 在章节编辑器写作并保存后,驾驶舱 **今日写作速度**(字/分钟)> 0;**24 小时时段热力**有数据
6. 驾驶舱展示:7 日字数趋势、番茄今日次数与均字/次、按卷字数占比(与现有字数聚合一致)
### 1.4 本规格不实现(明确排除)
| 项 | 归属 |
|----|------|
| AI 章节分析后自动关系建议(§7.2-3) | v1.1+ |
| 图谱中右键拖拽连线创建关系 | v1.1+ |
| 知识抽取面板「关系建议」一键添加 | v1.1+ |
| 预计完本日期 | §9 后续 |
| 角色弧线简报三节时间线(§20.3) | v1.1+ |
| 独立全屏「数据分析」页、书籍设置分析 Tab | 后续 |
| 导入导出、章节模板(§10 / §11) | 其他阶段 |
| 故事时间线模态(§5.7) | 其他阶段 |
| 设定条目(非 character)进入关系图谱 | 排除 |
---
## 2. 架构
### 2.1 进程模型
```
┌─────────────────────────────────────────────────────────────────────┐
│ 主进程(userData writing_logs.sqlite 扩展) │
│ · WritingSessionRepository / WritingSessionService │
│ · WritingAnalyticsService(聚合 sessions + logs + pomodoro + 章节) │
│ · ChapterWritingTracker.recordDelta → session.recordActivity │
└──────────────────────────────┬──────────────────────────────────────┘
┌──────────────────────────────▼──────────────────────────────────────┐
│ 主进程(每书 .sqliteschema v8
│ · CharacterGraphService(读 settings,写 properties.relationships
│ · SettingRepository(已有) │
│ · ChapterRepository.pov_character_id(已有) │
└──────────────────────────────┬──────────────────────────────────────┘
│ IPC
┌──────────────────────────────▼──────────────────────────────────────┐
│ 渲染进程 │
│ · CharacterGraphModalCytoscape + 侧栏) │
│ · SettingRelationshipsEditor(角色设定页关系 CRUD
│ · CockpitModal 扩展 analytics 区块 │
│ · ChapterHeader / ChapterListPOV 下拉 │
└─────────────────────────────────────────────────────────────────────┘
```
### 2.2 模块边界
| 模块 | 职责 | 依赖 |
|------|------|------|
| `WritingSessionService` | 活跃 session 状态机;5 分钟 idle 切分;`recordActivity` | `WritingSessionRepository` |
| `WritingAnalyticsService` | `getSummary(bookId)` 聚合分析指标 | sessions, logs, pomodoro, chapters, settings |
| `CharacterGraphService` | 构建 nodes/edgesCRUD 关系;边去重 | `SettingRepository` |
| `ChapterWritingTracker` | 已有字数 delta → **扩展**调用 session | `WritingLogService`, `WritingSessionService` |
| `CharacterGraphModal` | Cytoscape 渲染与布局切换 | preload `graph.*` |
| `CockpitModal` | 展示 `WritingAnalyticsSummary` | preload `analytics.getSummary` |
**原则**
- Session 写入与 `writing_logs` 共用 `recordDelta` 路径,**不重复计字**
- 图谱数据以 `settings` 为唯一真相源;Cytoscape 元素由 IPC 返回的 DTO 构建
- 分析服务只读;失败时驾驶舱分析区显示占位/空态,不阻断驾驶舱其他卡片
---
## 3. 数据模型
### 3.1 全局 userData:扩展 `writing_logs.sqlite`
`WritingLogRepository.migrate()` 追加:
```sql
CREATE TABLE IF NOT EXISTS writing_sessions (
id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
chapter_id TEXT,
started_at TEXT NOT NULL,
ended_at TEXT NOT NULL,
word_delta INTEGER NOT NULL DEFAULT 0,
date TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_sessions_book_date
ON writing_sessions (book_id, date);
CREATE INDEX IF NOT EXISTS idx_sessions_book_started
ON writing_sessions (book_id, started_at);
```
**Session 状态机**`WritingSessionService`):
| 事件 | 行为 |
|------|------|
| `recordActivity(bookId, chapterId, delta)` | 若无活跃 session 或距上次 `ended_at` > 5min → `INSERT` 新 session;否则 `UPDATE ended_at, word_delta += delta` |
| `endActive(bookId)` | 书籍切换 / 应用退出前结束活跃 session |
| `delta === 0` | 忽略(不创建空 session |
活跃 session 可在主进程内存缓存 `{ bookId, sessionId, lastEndedAt }`DB 为持久真相。
### 3.2 角色关系 JSON(每书 `settings.properties`
```typescript
interface CharacterRelationship {
id: string
targetId: string
label: string
intimacy: 1 | 2 | 3 | 4 | 5
}
// SettingEntry.properties.relationships?: CharacterRelationship[]
// 仅 type === 'character' 时读写
```
**边去重规则**(渲染):对无序对 `{A,B}` 仅输出一条边;若 A→B 与 B→A 均存在,取 `max(intimacy)``label` 优先取较高亲密度源的 label。
**CRUD**
- `upsertRelationship(sourceId, { id?, targetId, label, intimacy })` — 更新源角色 `properties.relationships`
- `deleteRelationship(sourceId, relationshipId)`
- 禁止 `targetId === sourceId`;禁止指向非 character 设定
### 3.3 POV(已有)
- DB 列:`chapters.pov_character_id`schema v8 已有 `tryAlter`
- UI:章节编辑器工具栏或章节列表元数据区,`select` 绑定 `type='character'` 设定
- IPC:复用 `chapter.update` patch `{ povCharacterId }`
### 3.4 类型扩展(`src/shared/types.ts`
```typescript
export interface CharacterGraphNode {
id: string
label: string
}
export interface CharacterGraphEdge {
id: string
source: string
target: string
label: string
intimacy: 1 | 2 | 3 | 4 | 5
}
export interface CharacterGraphData {
nodes: CharacterGraphNode[]
edges: CharacterGraphEdge[]
truncated: boolean
nodeCount: number
}
export interface WritingAnalyticsSummary {
todayWords: number
todaySpeedWpm: number | null
weekTrend: { date: string; words: number }[]
hourlyHeatmap: number[] // length 24
pomodoroToday: number
pomodoroAvgWords: number | null
povDistribution: { characterId: string; name: string; words: number }[]
volumeDistribution: { volumeId: string; title: string; words: number }[]
}
```
---
## 4. 服务层设计
### 4.1 WritingAnalyticsService
```typescript
getSummary(bookId: string): WritingAnalyticsSummary
```
| 字段 | 计算方式 |
|------|----------|
| `todayWords` | `WritingLogService.getStats(bookId).todayWords` |
| `todaySpeedWpm` | 今日 sessions`sum(word_delta) / sum(minutes(ended_at - started_at))`;无 session 或零时长 → `null` |
| `weekTrend` | 近 7 天 `writing_logs` 按 date |
| `hourlyHeatmap` | 近 30 天 sessions,按 `started_at` 小时 bucket 累加 `word_delta`24 槽) |
| `pomodoroToday` | `PomodoroDailyRepository.getTodayCount` |
| `pomodoroAvgWords` | 今日 `total_words / completed_count`count=0 → `null` |
| `povDistribution` | chapters GROUP BY `pov_character_id` SUM `word_count`JOIN character name |
| `volumeDistribution` | chapters GROUP BY `volume_id` SUM `word_count` |
未设置 POV 的章节计入 `{ characterId: '__none__', name: i18n未指定 }` 或单独过滤(实现时选 **单独「未指定」桶**)。
### 4.2 CharacterGraphService
```typescript
getData(bookId: string, filter?: 'all' | 'connected'): CharacterGraphData
upsertRelationship(bookId, sourceId, input): CharacterRelationship
deleteRelationship(bookId, sourceId, relationshipId): void
```
- `filter: 'connected'`:仅包含有至少一条边的节点(用于超 100 节点时的降级展示)
- `truncated: true` 当 character 总数 > 100
### 4.3 ChapterWritingTracker 扩展
```typescript
recordDelta(chapterId: string, newWordCount: number): void {
// 现有 writing_logs delta 逻辑不变
if (delta !== 0) {
this.writingLogs.addToday(this.bookId, delta)
this.sessions.recordActivity(this.bookId, chapterId, delta)
}
// snapshot upsert 不变
}
```
---
## 5. IPC 与 preload
```typescript
// ipc-channels.ts
GRAPH_GET_DATA: 'graph:getData',
GRAPH_UPSERT_RELATIONSHIP: 'graph:upsertRelationship',
GRAPH_DELETE_RELATIONSHIP: 'graph:deleteRelationship',
ANALYTICS_SUMMARY: 'analytics:summary',
```
| 通道 | 参数 | 返回 |
|------|------|------|
| `graph:getData` | `{ bookId, filter? }` | `IpcResult<CharacterGraphData>` |
| `graph:upsertRelationship` | `{ bookId, sourceId, relationship }` | `IpcResult<CharacterRelationship>` |
| `graph:deleteRelationship` | `{ bookId, sourceId, relationshipId }` | `IpcResult<void>` |
| `analytics:summary` | `{ bookId }` | `IpcResult<WritingAnalyticsSummary>` |
`cockpit:getSummary` **不合并** analytics(保持单一职责);驾驶舱 UI 并行请求 `cockpit.getSummary` + `analytics.getSummary`,或在 renderer 层 `Promise.all`
---
## 6. UI 规格
### 6.1 角色关系图谱(`CharacterGraphModal`
参考 `design/ui_pc.html` `#graphModal`
| 元素 | testid | 说明 |
|------|--------|------|
| 模态 | `character-graph-modal` | 宽屏 `modal-content--wide` |
| Cytoscape 容器 | `character-graph-cy` | 最小高度 480px |
| 布局按钮 | `graph-layout-cose` / `grid` / `circle` | |
| 侧栏 | `character-graph-sidebar` | 节点/边详情 |
| 设定入口按钮 | `setting-open-graph` | 设定面板工具栏 |
**亲密度配色**(边 `line-color`):
| intimacy | 颜色 |
|----------|------|
| 1 | `#4ea8de` 蓝 |
| 2 | `#45b7aa` 青 |
| 3 | `#3fb950` 绿 |
| 4 | `#f0883e` 橙 |
| 5 | `#e0556a` 红 |
**Cytoscape 依赖**(渲染进程):`cytoscape``cytoscape-cose-bilkent`destroy 于 modal 关闭。
### 6.2 设定面板关系编辑(`SettingRelationshipsEditor`
- 嵌入角色设定编辑区(TipTap 下方或侧栏,当 `type === 'character'`
- 列表:目标角色名、label、亲密度、删除
- 添加:目标角色下拉(排除自身)、label 输入、intimacy range 15
- testid`setting-relationships``setting-relationship-add`
### 6.3 驾驶舱分析区(`CockpitModal` 扩展)
| 卡片 | testid | 内容 |
|------|--------|------|
| 分析区容器 | `cockpit-analytics` | |
| 今日速度 | `cockpit-speed-today` | `{{wpm}} 字/分钟``—` |
| 时段热力 | `cockpit-hourly-heatmap` | 24 格横条 |
| POV 分布 | `cockpit-pov-chart` | Top 5 横向条 |
| 按卷占比 | `cockpit-volume-chart` | 卷名字 + 字数 |
| 7 日趋势 | `cockpit-week-trend` | 7 柱 mini chart |
驾驶舱底部增加「打开关系图谱」按钮:`cockpit-open-graph`
### 6.4 章节 POV
- 位置:章节编辑器标题栏或 `#editor-statusbar` 附近
- testid`chapter-pov-select`
- 选项:空(未指定)+ 所有 character 设定
- 变更时 debounce 调用 `chapter.update`
---
## 7. i18n 键(节选)
```json
"graph.title": "角色关系图谱",
"graph.layout.cose": "力导向",
"graph.layout.grid": "网格",
"graph.layout.circle": "圆形",
"graph.truncated": "角色过多,仅显示前 {{count}} 个",
"graph.sidebar.node": "角色:{{name}}",
"graph.sidebar.edge": "关系:{{label}}",
"graph.editSetting": "编辑设定",
"relationship.title": "角色关系",
"relationship.add": "添加关系",
"relationship.target": "目标角色",
"relationship.label": "关系描述",
"relationship.intimacy": "亲密度",
"analytics.title": "写作分析",
"analytics.speedToday": "今日 {{wpm}} 字/分钟",
"analytics.speedUnknown": "暂无速度数据",
"analytics.hourlyHeatmap": "写作时段(近30天)",
"analytics.povDistribution": "视角字数分布",
"analytics.volumeDistribution": "按卷字数",
"analytics.weekTrend": "近 7 日字数",
"analytics.povUnassigned": "未指定视角",
"chapter.pov": "视角角色",
"cockpit.openGraph": "角色关系图谱"
```
---
## 8. 测试要求
### 8.1 单元 / 集成
| 用例ID | 层级 | 场景 | 预期 |
|--------|------|------|------|
| UT-SESSION-01 | 单元 | 5min 内两次 recordActivity | 同一 sessionword_delta 累加 |
| UT-SESSION-02 | 单元 | 间隔 6min 两次 recordActivity | 两个 session |
| UT-SESSION-03 | 单元 | delta=0 | 不创建 session |
| UT-GRAPH-01 | 单元 | 3 角色 2 关系 getData | nodes=3, edges=2 |
| UT-GRAPH-02 | 单元 | A→B 与 B→A 双向存储 | 渲染 1 条边,intimacy 取 max |
| UT-GRAPH-03 | 单元 | 101 个 character | truncated=true |
| UT-ANALYTICS-01 | 单元 | 2 个 session 共 100 字 / 10 分钟 | todaySpeedWpm=10 |
| UT-ANALYTICS-02 | 单元 | hourlyHeatmap | 24 长度,对应小时有值 |
| IT-ANALYTICS-01 | 集成 | chapter update → recordDelta → getSummary | todaySpeedWpm > 0 |
| IT-GRAPH-01 | 集成 | upsertRelationship → getData | 边可见 |
### 8.2 E2E
| 用例ID | 场景 | 预期 |
|--------|------|------|
| E2E-GRAPH-01 | 打开关系图谱 | 力导向图可见节点 |
| E2E-GRAPH-02 | 点击节点 | 侧栏显示角色名 |
| E2E-GRAPH-03 | 切换圆形布局 | 布局变化 |
| E2E-GRAPH-04 | 设定面板添加关系 | 图谱出现新边 |
| E2E-ANALYTICS-01 | 写作保存后打开驾驶舱 | `cockpit-speed-today` 有数值 |
| E2E-ANALYTICS-02 | 设置 POV 后刷新驾驶舱 | POV chart 含该角色 |
**E2E 图谱**:不依赖 AI / LM Studio。
---
## 9. 文件清单(实现阶段参考)
| 路径 | 说明 |
|------|------|
| `src/main/db/repositories/writing-session.repo.ts` | sessions CRUD |
| `src/main/services/writing-session.service.ts` | 状态机 |
| `src/main/services/writing-analytics.service.ts` | 分析聚合 |
| `src/main/services/character-graph.service.ts` | 图谱 DTO + 关系 CRUD |
| `src/main/ipc/handlers/graph.handler.ts` | 图谱 IPC |
| `src/main/ipc/handlers/analytics.handler.ts` | 分析 IPC |
| `src/renderer/components/graph/CharacterGraphModal.tsx` | Cytoscape UI |
| `src/renderer/components/setting/SettingRelationshipsEditor.tsx` | 关系 CRUD |
| `src/renderer/components/cockpit/CockpitAnalytics.tsx` | 分析卡片 |
| `src/renderer/components/cockpit/HourlyHeatmap.tsx` | 24 槽热力 |
| `tests/main/writing-session.test.ts` | UT-SESSION-* |
| `tests/main/character-graph.test.ts` | UT-GRAPH-* |
| `tests/main/writing-analytics.test.ts` | UT-ANALYTICS-* |
| `e2e/character-graph.spec.ts` | E2E-GRAPH-* |
| `e2e/writing-analytics.spec.ts` | E2E-ANALYTICS-* |
---
## 10. Definition of Done
- [ ] `writing_sessions` 表与 Session 状态机落地
- [ ] 角色关系 CRUD + Cytoscape 图谱模态
- [ ] 驾驶舱分析区(速度 / 时段 / POV / 卷 / 7 日)
- [ ] 章节 POV 选择 UI
- [ ] UT / IT / E2E 用例通过
- [ ] i18n zh-CN / en 键齐全
- [ ] `npm run build` + `npm run test` 全绿
- [ ] 版本号 **1.0.0**`package.json` / README / Settings 关于页)
---
## 11. Spec Coveragedesign/readme.md
| 设计章节 | 本 spec 覆盖 |
|----------|-------------|
| §7.1 力导向图、节点/边、布局 | ✅ MVP |
| §7.2-1 设定面板手动添加 | ✅ |
| §7.2-2 图谱拖拽连线 | ❌ v1.1 |
| §7.2-3 AI 关系建议 | ❌ v1.1 |
| §7.3 E2E-GRAPH-01~04 | ✅(04 改为设定面板添加) |
| §9 写作速度仪表盘 | ✅ 今日速度 + 时段热力 |
| §9 习惯追踪 | 部分(连更/番茄已有;session 时长留待后续展示) |
| §9 POV 字数分布 | ✅ |
| §9 角色弧线简报 | ❌ v1.1 |
| §20.3 角色弧线 | ❌ v1.1 |