From e64353f608083f4b876dda9f07dd3cea93a9b6ce Mon Sep 17 00:00:00 2001 From: kun1h Date: Wed, 8 Jul 2026 10:04:56 +0800 Subject: [PATCH] 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 --- ...7-08-bilin-p7-graph-p9-analytics-design.md | 444 ++++++++++++++++++ 1 file changed, 444 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-08-bilin-p7-graph-p9-analytics-design.md diff --git a/docs/superpowers/specs/2026-07-08-bilin-p7-graph-p9-analytics-design.md b/docs/superpowers/specs/2026-07-08-bilin-p7-graph-p9-analytics-design.md new file mode 100644 index 0000000..f13e905 --- /dev/null +++ b/docs/superpowers/specs/2026-07-08-bilin-p7-graph-p9-analytics-design.md @@ -0,0 +1,444 @@ +# 笔临 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 │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ +┌──────────────────────────────▼──────────────────────────────────────┐ +│ 主进程(每书 .sqlite,schema v8) │ +│ · CharacterGraphService(读 settings,写 properties.relationships) │ +│ · SettingRepository(已有) │ +│ · ChapterRepository.pov_character_id(已有) │ +└──────────────────────────────┬──────────────────────────────────────┘ + │ IPC +┌──────────────────────────────▼──────────────────────────────────────┐ +│ 渲染进程 │ +│ · CharacterGraphModal(Cytoscape + 侧栏) │ +│ · SettingRelationshipsEditor(角色设定页关系 CRUD) │ +│ · CockpitModal 扩展 analytics 区块 │ +│ · ChapterHeader / ChapterList:POV 下拉 │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### 2.2 模块边界 + +| 模块 | 职责 | 依赖 | +|------|------|------| +| `WritingSessionService` | 活跃 session 状态机;5 分钟 idle 切分;`recordActivity` | `WritingSessionRepository` | +| `WritingAnalyticsService` | `getSummary(bookId)` 聚合分析指标 | sessions, logs, pomodoro, chapters, settings | +| `CharacterGraphService` | 构建 nodes/edges;CRUD 关系;边去重 | `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` | +| `graph:upsertRelationship` | `{ bookId, sourceId, relationship }` | `IpcResult` | +| `graph:deleteRelationship` | `{ bookId, sourceId, relationshipId }` | `IpcResult` | +| `analytics:summary` | `{ bookId }` | `IpcResult` | + +`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 1–5 +- 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 | 同一 session,word_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 Coverage(design/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 |