Skip to content

Commit 6ba187a

Browse files
committed
fix(usage): repair legacy v15 rollup schema
1 parent 831e5dc commit 6ba187a

8 files changed

Lines changed: 171 additions & 32 deletions

docs/architecture/PERSONAL_CENTER_USAGE_ANALYTICS_ARCHITECTURE.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
> **受众:** 产品、设计、React、Rust、Python Sidecar、测试与后续维护者。
55
> **最后审阅 / Last reviewed:** 2026-08-13
66
> **规划基线:** `main@5569c45`(Schema v14,React 19 / Tauri 2 / Rust 2021 / Python 3.11)。
7-
> **状态:** P0–P8 已完成并通过交付门禁(含档案编辑、用量生命周期、ExportData v2、可重建 rollup、权限基线与全量 QA),阶段提交和证据见 [实施计划](../planning/PERSONAL_CENTER_USAGE_ANALYTICS_IMPLEMENTATION_PLAN.md)
7+
> **状态:** P0–P8 已完成并通过交付门禁;Schema v16 已补充旧 v15 安装的 rollup 表修复迁移。阶段提交和证据见 [实施计划](../planning/PERSONAL_CENTER_USAGE_ANALYTICS_IMPLEMENTATION_PLAN.md)
88
99
---
1010

@@ -426,14 +426,21 @@ UnifiedTopBar:返回 + “个人中心”
426426
- Sidecar 过去未上报的 Token 无法精确重建;默认不对完整历史上下文做误导性估算。
427427
- migration 必须幂等;坏 JSON 计入诊断计数,不阻塞整个数据库升级。
428428

429-
### 10.2 导入导出
429+
### 10.2 v15 → v16 派生表修复
430+
431+
- v15 引入 `user_profiles``llm_usage_events` 事实表;v16 仅创建/重建四张可丢弃 rollup 表,不删除或改写 profile、账本、消息与会话。
432+
- 修复背景:部分本地数据库在 rollup 优化加入前已记录 `_schema_version=15`,不会重放后来扩充的 `migrate_v15`;新结构必须使用独立版本号,禁止修改已发布迁移后假设其会重跑。
433+
- 升级前以 `VACUUM INTO` 创建 `.pre-v16.sqlite3`;迁移事务完成后首次 dashboard 查询从账本惰性重建读模型。
434+
- 回归 fixture 必须覆盖“v15 + 有账本数据 + 四张 rollup 表缺失”,并验证升级幂等、Dashboard 读数和备份可恢复。
435+
436+
### 10.3 导入导出
430437

431438
- `ExportData.version = 2` 携带可选 usage events 与安全 profile display metadata;v1 仍可导入,未知未来版本拒绝。普通 JSON 不携带 avatar 二进制、avatar key/hash 或原始 usage metadata。
432439
- 本机生成并校验稳定 installation UUID;事件导入使用原始 `source_installation_id + source_event_id` 唯一索引去重,本地 operation/measurement key 由长度分隔输入的 SHA-256 命名以避免分隔符碰撞;再导出时继续保留来源对。
433440
- 导入仅重建确有对应记录的 session/message/provider 弱引用,不能接受文件中的悬空或跨库引用。
434441
- 旧导出按 legacy 规则迁移 session 投影残差;导入 metadata 只保留来源版本、计量质量、legacy 标记和“历史时区未知”,不透传任意 JSON。
435442

436-
### 10.3 隐私与安全
443+
### 10.4 隐私与安全
437444

438445
- 用量表不保存 prompt、回复正文、API key、完整 base URL、工具参数/结果、工作区路径或附件内容。
439446
- profile avatar 只读写 app-data 受管目录;文件名由后端生成,拒绝 traversal、超限和不支持 MIME。
@@ -447,7 +454,7 @@ UnifiedTopBar:返回 + “个人中心”
447454
- 年度热力图固定最多 400 个点;30 天趋势最多 8×90 点,DTO 有硬上限。
448455
- 1k/10k/100k 确定性 fixture 在预热后各采样 7 次;未聚合 100k 组合查询 P95 约 490ms,超过 <100ms 门槛后才启用可重建 read model,热查询实测约 64.8ms。
449456
- `usage_operation_rollups``usage_profile_rollups``usage_daily_rollups``usage_rollup_state` 都是派生缓存:账本是唯一事实源;终结只追加账本,不双写 rollup。查询前比较 ledger count/max rowid,新增事件增量刷新,删除、状态缺失或迁移重放则从账本全量重建。
450-
- rollup 表可被安全删除;清空当前 profile 时与账本/session projection 同事务清理。若未来允许原地修改统计字段,必须扩展失效策略,不能绕过追加式仓储。
457+
- rollup 内容可被安全清空并从账本重建;表结构由版本化 migration 保证。清空当前 profile 时与账本/session projection 同事务清理。若未来允许原地修改统计字段,必须扩展失效策略,不能绕过追加式仓储。
451458
- `EXPLAIN QUERY PLAN` 测试固定验证 profile/date 与 provider/model/date 索引;性能门禁失败应先保留证据,再调整索引或派生读模型。
452459
- ECharts 仅在 TrendCard 进入 DOM 后动态加载;页面离开时 dispose,ResizeObserver 必须 disconnect。
453460
- 记录脱敏日志:capture source、operation kind、是否估算、query duration、event insert conflict;不记录用户内容。
@@ -498,7 +505,7 @@ UnifiedTopBar:返回 + “个人中心”
498505

499506
| 门禁 | 结果 |
500507
|---|---|
501-
| 数据与迁移 | Schema v15 幂等迁移/备份、Profile/Usage repository、legacy backfill、ExportData v1/v2 与 rollup 重建通过 Rust 回归 |
508+
| 数据与迁移 | Schema v15 事实表、Schema v16 旧安装修复/备份、Profile/Usage repository、legacy backfill、ExportData v1/v2 与 rollup 重建通过 Rust 回归 |
502509
| 采集与生命周期 | Rig/MCP、Sidecar run-id 去重、provider total、abort 三态、重复 finalize、regenerate、弱引用删除与清空统计通过确定性测试 |
503510
| 查询与 UI | 365/30 天、streak/闰日/DST、同模型跨 provider、Top 5 + others、BigInt、活动日历/趋势降级及可访问语义通过回归 |
504511
| 性能 | 100k 未聚合组合查询 P95 约 490ms;启用按需重建/增量 rollup 后热查询 P95 64.8ms,低于 100ms 门槛 |

docs/planning/PERSONAL_CENTER_USAGE_ANALYTICS_IMPLEMENTATION_PLAN.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
> **用途:** 将个人中心、活动日历、按模型 Token 趋势与可靠用量计量拆成可验证、可独立审查的实施阶段和 Todo。
44
> **受众:** React、Rust、Python Sidecar、测试、设计与发布维护者。
55
> **最后审阅 / Last reviewed:** 2026-08-13
6-
> **状态:** P0–P8 已完成并通过交付门禁;阶段提交与验证证据见下文
6+
> **状态:** P0–P8 已完成并通过交付门禁;交付后发现的旧 v15 rollup 缺表问题已由 Schema v16 修复迁移关闭
77
> **规划基线:** `main@5569c45`,Schema v14。
88
> **实施分支:** `codex/personal-center-usage-analytics`
99
> **关联架构:** [个人中心与 Token 用量统计功能架构](../architecture/PERSONAL_CENTER_USAGE_ANALYTICS_ARCHITECTURE.md)
@@ -25,6 +25,7 @@
2525
| P6 最近 30 天按模型趋势 | ✅ 完成 | `63cef42` | 49 个前端测试文件、308 项测试及生产构建通过;ECharts 独立懒加载 chunk,数据表可切换并自动降级 |
2626
| P7 档案编辑、数据生命周期与性能 | ✅ 完成 | `cbb13cc` | 21 项迁移测试 + 36 项 Rust 定向测试、前端 50 文件/312 项测试、全特性检查及生产构建通过;100k 热查询 P95 64.8ms |
2727
| P8 QA、规范同步与交付 | ✅ 完成 | 本阶段提交 `test(usage): complete P8 delivery gates` | 前端 50 文件/313 项、Python 41 项、Rust 全特性 510 项通过;生产构建、格式、静态检查、ACL 基线与文档同步通过 |
28+
| 交付后修复:Schema v16 | ✅ 完成 | 本次提交 `fix(usage): repair legacy v15 rollup schema` | 复现真实旧 v15 缺表数据库;22 项迁移、Dashboard 查询、`.pre-v16.sqlite3` 恢复及全特性 511 项回归通过 |
2829

2930
### 0.2 工作日志
3031

@@ -42,6 +43,7 @@
4243
| 2026-08-13 16:26 | P7 完成 | 派生 rollup 支持缺失/删除后全量重建与新事件增量刷新,账本仍为唯一事实源且终结链路不双写;迁移重放会先清理派生缓存。21 项迁移测试、36 项 Rust 定向测试、前端 50 文件/312 项测试、`cargo check --all-features` 与生产构建全部通过。 |
4344
| 2026-08-13 16:52 | P8 QA | 补齐 provider total 权威值、abort 三态、model probe、regenerate 弱引用/DST、同模型跨 provider、头像可访问名称、Dialog focus return 与 chart 源数据不变性回归。全量验证发现并修复 estimator ASCII 分段 fixture 的期望值,以及 7 个新增 Profile/Usage commands 未进入 AppManifest/`main-commands` 的权限集成缺口。 |
4445
| 2026-08-13 17:01 | P8 交付 | `cargo nextest` 因 Windows 页文件不足(OS 1455)在编译阶段中止,未执行 `cargo clean`;按仓库指南以 `cargo test --all-features -j1` 串行回退并通过 510 项。前端 50 文件/313 项、Python 指定 41 项、生产构建、格式/静态检查与文档同步共同组成最终交付门禁。 |
46+
| 2026-08-13 17:22 | v16 修复 | 实机数据库确认 `_schema_version=15`、3 条 legacy usage event 存在但四张 rollup 表缺失;新增独立 v16 事务迁移与 `.pre-v16.sqlite3` 备份,重建的仅是派生表。新增 fixture 精确模拟旧 v15,验证账本保留、首次 dashboard 返回 321 legacy tokens、迁移幂等和备份可恢复。 |
4547

4648
### 0.3 首版 LLM operation 计入口径
4749

docs/project/DEVELOPMENT_STATUS.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ MisakaX 已是可用的**桌面 LLM 对话客户端**(流式对话、工作目
130130
### 3.8 个人中心与 Token 用量统计(P0–P8 已交付)
131131

132132
- 用户菜单进入独立 `profile` route;页面无主导航/会话栏,保留 UnifiedTopBar、80px 本地头像、名称编辑与三张总览卡。
133-
- Schema v15 `llm_usage_events` 是唯一事实源;Rig/MCP 与 Sidecar chat/research 统一为 canonical usage,provider-reported 优先,heuristic、legacy 与 unavailable 明确分级,未知值不伪装为 0。
133+
- Schema v15 引入的 `llm_usage_events` 是唯一事实源;Schema v16 为较早到达 v15 的安装补建可重建 rollup 表。Rig/MCP 与 Sidecar chat/research 统一为 canonical usage,provider-reported 优先,heuristic、legacy 与 unavailable 明确分级,未知值不伪装为 0。
134134
- Dashboard 单快照返回总览、365 天活动与 30 天 Top 5 + others 趋势;同模型跨 provider 不合并,前端以十进制字符串 + BigInt 避免 64 位精度损失。
135135
- 消息、账本、session projection 在同一 finalize transaction;重复 complete、重生成、弱引用删除、清空、v1/v2 导入与启动回填均有确定行为。
136136
- 头像限制为 PNG/JPEG/WebP、5 MiB、40M 像素,规范化为最长边 512px 的托管 WebP;清空与替换不接受任意删除路径。
@@ -145,7 +145,7 @@ MisakaX 已是可用的**桌面 LLM 对话客户端**(流式对话、工作目
145145
| 范围 | 数量 | 运行命令 |
146146
|------|------|----------|
147147
| 前端 Vitest | 50 个测试文件 / 313 tests | `npm test` |
148-
| Rust 全特性 | 45 个集成测试文件 / 合计 510 tests | 日常:`cargo test --test <name>`;提交前:`cargo nextest run --all-features --profile ci`(或 `cargo test --all-features -j1`|
148+
| Rust 全特性 | 45 个集成测试文件 / 合计 511 tests | 日常:`cargo test --test <name>`;提交前:`cargo nextest run --all-features --profile ci`(或 `cargo test --all-features -j1`|
149149
| Python Sidecar 交付门禁 | 指定 3 个文件 / 41 tests | `python -m pytest tests/test_stream_sse.py tests/test_agent.py tests/test_models.py` |
150150

151151
> Rust 日常构建/测试依赖增量编译,**不要**在每次 `cargo test` 前执行 `cargo clean`。日常改代码优先 `cargo check` + 精准 `--test`(映射表见优化指南 §4.2);Cursor hook `.cursor/hooks/post-edit-test.sh` 已按映射自动选择测试。仅在链接异常、切分支后编译诡异失败等情况下按需 `cargo clean`。见 [`docs/guides/rust-build-test-optimization.md`](../guides/rust-build-test-optimization.md)
@@ -252,7 +252,7 @@ npm run tauri dev
252252
| 前端 TS/TSX | 268 个文件 |
253253
| Rust 源码 | 129 个 `.rs``src-tauri/src/`|
254254
| Tauri Commands | 120(见 `src-tauri/build.rs``src-tauri/src/lib.rs`|
255-
| DB Schema | **v15**`src-tauri/src/db/migrations.rs`|
255+
| DB Schema | **v16**v15 事实表 + v16 旧安装 rollup 修复;`src-tauri/src/db/migrations.rs`|
256256
| UI 设计规范 | `docs/design/frontend-ui-guidelines.md` 等 3 份 |
257257

258258
---
@@ -267,7 +267,7 @@ React 前端 ✅
267267
Tauri Rust ✅
268268
├── chat ──▶ use_sidecar ? Python Agent : Rig ──▶ LLM API
269269
├── MCP (rmcp) ──▶ MCP Servers
270-
├── Usage finalize/query ──▶ SQLite v15 ledger + rebuildable rollups
270+
├── Usage finalize/query ──▶ SQLite v16 ledger + rebuildable rollups
271271
└── SidecarManager ──▶ Python :9527
272272
├── /health ✅
273273
└── /agent/* ✅ chat/research + Usage SSE v1

docs/project/PROJECT_STRUCTURE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ MisakaX/
9393
| 文件 / 目录 | 用途 |
9494
|-------------|------|
9595
| `mod.rs` | SQLite 初始化、WAL、sqlite-vec 加载、运行迁移 |
96-
| `migrations.rs` | Schema **v1–v15**;v11 stable SkillId/activation,v12 scan/finding/approval,v13 存量扫描,v14 sandbox audit,v15 local profile追加式 usage ledger 与可重建 rollup |
96+
| `migrations.rs` | Schema **v1–v16**;v11 stable SkillId/activation,v12 scan/finding/approval,v13 存量扫描,v14 sandbox audit,v15 local profile/追加式 usage ledger,v16 为旧 v15 安装补建可重建 rollup |
9797
| `models.rs` | Session / Message / RouterConfig、`UserProfile``UsageEvent` 与写入 DTO 等数据模型 |
9898
| `repository/` | 会话、消息、Provider、MCP、Workspace、Settings、Skills/Sandbox,以及 `profile_repo` / `usage_repo`;UsageRepo 使用参数绑定和稳定 measurement/source key 保证幂等 |
9999

@@ -256,7 +256,7 @@ MisakaX/
256256
~/.misakax/
257257
├── config.yaml # 全局配置(YAML 格式,User-editable)
258258
├── data/
259-
│ ├── misaka.db # SQLite v15 数据库(WAL 模式,含向量索引与 usage ledger)
259+
│ ├── misaka.db # SQLite v16 数据库(WAL 模式,含向量索引与 usage ledger)
260260
│ └── profile-avatars/ # 应用托管、规范化后的本地头像 WebP 副本
261261
├── skills/ # 用户自定义 Skills
262262
├── managed/skills/ # 从市场安装的 Skills

src-tauri/src/db/migrations.rs

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ pub fn run_migrations(conn: &Connection) -> Result<()> {
7878
migrate_v15(conn)?;
7979
}
8080

81+
if current_version < 16 {
82+
migrate_v16(conn)?;
83+
}
84+
8185
Ok(())
8286
}
8387

@@ -704,14 +708,6 @@ fn migrate_v15(conn: &Connection) -> Result<()> {
704708
let tx = conn.unchecked_transaction()?;
705709
tx.execute_batch(
706710
"
707-
-- These tables are disposable read-model caches. Recreate them when
708-
-- v15 is replayed by rollback/recovery tooling, while keeping the
709-
-- ledger and profile table creation strict.
710-
DROP TABLE IF EXISTS usage_rollup_state;
711-
DROP TABLE IF EXISTS usage_operation_rollups;
712-
DROP TABLE IF EXISTS usage_profile_rollups;
713-
DROP TABLE IF EXISTS usage_daily_rollups;
714-
715711
CREATE TABLE user_profiles (
716712
profile_id TEXT PRIMARY KEY,
717713
profile_kind TEXT NOT NULL DEFAULT 'local'
@@ -806,9 +802,34 @@ fn migrate_v15(conn: &Connection) -> Result<()> {
806802
ON llm_usage_events(source_installation_id, source_event_id)
807803
WHERE source_installation_id IS NOT NULL AND source_event_id IS NOT NULL;
808804
809-
-- Rebuildable usage read-model cache. The append-only ledger above is
810-
-- the only fact source; these rows are refreshed lazily from it and
811-
-- may be deleted/rebuilt at any time.
805+
INSERT INTO _schema_version (version) VALUES (15);
806+
",
807+
)?;
808+
tx.commit()?;
809+
tracing::info!("Database migrated to version 15");
810+
Ok(())
811+
}
812+
813+
fn migrate_v16(conn: &Connection) -> Result<()> {
814+
let tx = conn.unchecked_transaction()?;
815+
reset_usage_rollup_tables(&tx)?;
816+
tx.execute("INSERT INTO _schema_version (version) VALUES (16)", [])?;
817+
tx.commit()?;
818+
tracing::info!("Database migrated to version 16");
819+
Ok(())
820+
}
821+
822+
fn reset_usage_rollup_tables(conn: &Connection) -> Result<()> {
823+
conn.execute_batch(
824+
"
825+
-- These tables are disposable read-model caches. Schema v16 repairs
826+
-- installations that reached v15 before the rollup optimization was
827+
-- added. The append-only ledger remains the only fact source.
828+
DROP TABLE IF EXISTS usage_rollup_state;
829+
DROP TABLE IF EXISTS usage_operation_rollups;
830+
DROP TABLE IF EXISTS usage_profile_rollups;
831+
DROP TABLE IF EXISTS usage_daily_rollups;
832+
812833
CREATE TABLE usage_rollup_state (
813834
profile_id TEXT PRIMARY KEY,
814835
last_event_rowid INTEGER NOT NULL DEFAULT 0,
@@ -849,12 +870,8 @@ fn migrate_v15(conn: &Connection) -> Result<()> {
849870
PRIMARY KEY (profile_id, local_date),
850871
FOREIGN KEY (profile_id) REFERENCES user_profiles(profile_id) ON DELETE CASCADE
851872
) WITHOUT ROWID;
852-
853-
INSERT INTO _schema_version (version) VALUES (15);
854873
",
855874
)?;
856-
tx.commit()?;
857-
tracing::info!("Database migrated to version 15");
858875
Ok(())
859876
}
860877

src-tauri/src/db/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ pub fn init_database(db_path: &Path) -> Result<Connection> {
2626
conn.execute_batch("PRAGMA foreign_keys=ON;")?;
2727
conn.execute_batch("PRAGMA busy_timeout=5000;")?;
2828

29-
backup_before_migration(&conn, db_path, 15)?;
29+
backup_before_migration(&conn, db_path, 16)?;
3030

3131
// Load sqlite-vec extension
3232
unsafe {

src-tauri/tests/database_tests.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ fn test_init_database() {
2222
})
2323
.unwrap();
2424
assert!(
25-
version >= 15,
26-
"Expected schema version >= 15, got {}",
25+
version >= 16,
26+
"Expected schema version >= 16, got {}",
2727
version
2828
);
2929

@@ -51,4 +51,8 @@ fn test_init_database() {
5151
assert!(tables.contains(&"mcp_servers".to_string()));
5252
assert!(tables.contains(&"user_profiles".to_string()));
5353
assert!(tables.contains(&"llm_usage_events".to_string()));
54+
assert!(tables.contains(&"usage_rollup_state".to_string()));
55+
assert!(tables.contains(&"usage_operation_rollups".to_string()));
56+
assert!(tables.contains(&"usage_profile_rollups".to_string()));
57+
assert!(tables.contains(&"usage_daily_rollups".to_string()));
5458
}

0 commit comments

Comments
 (0)