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>
18 KiB
笔临 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–5),保存后在关系图谱中可见对应边
- 打开 角色关系图谱 全屏模态,看到力导向布局下的角色节点与关系边;可切换 grid / circle 布局
- 点击节点 → 侧栏显示角色名与「编辑设定」跳转;点击边 → 侧栏编辑关系描述与亲密度
- 在章节编辑区设置 POV 角色,驾驶舱 POV 分布反映该角色章节字数
- 在章节编辑器写作并保存后,驾驶舱 今日写作速度(字/分钟)> 0;24 小时时段热力有数据
- 驾驶舱展示: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() 追加:
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)
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.relationshipsdeleteRelationship(sourceId, relationshipId)- 禁止
targetId === sourceId;禁止指向非 character 设定
3.3 POV(已有)
- DB 列:
chapters.pov_character_id(schema v8 已有tryAlter) - UI:章节编辑器工具栏或章节列表元数据区,
select绑定type='character'设定 - IPC:复用
chapter.updatepatch{ povCharacterId }
3.4 类型扩展(src/shared/types.ts)
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
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
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 扩展
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
// 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 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 键(节选)
"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 |