feat(usage): add Hermes Agent tracking + fix zero-cost bug + perf

Hermes:
- Parse ~/.hermes/state.db sessions (incl. profiles/*/state.db) into
  proxy_request_logs with data_source='hermes_session', WAL-aware
  incremental sync, Hermes-reported cost preferred over model_pricing
  fallback

Zero-cost bug (dashboard showed \$0 totals):
- GPT-5.5 family default pricing (~83% of affected rows used GPT-5.5)
- find_model_pricing_row: ASCII-lowercase normalization so
  "OpenAI/GPT-5.5@HIGH" matches seeded "gpt-5.5"
- Startup cost backfill in async task: scan rows where total_cost <= 0
  but tokens > 0, recompute via model_pricing in a single transaction

Performance:
- Add (app_type, created_at DESC) covering index for dashboard range
  queries
- Add expression index on COALESCE(data_source, 'proxy') so dedup EXISTS
  subqueries use index lookup instead of full scan; drop superseded
  idx_request_logs_dedup_lookup

Refactor:
- row_to_request_log_detail helper (3-way de-dup; fixes cost_multiplier
  \"1\" vs \"1.0\" drift between callers)
- Promote get_sync_state/update_sync_state to shared session_usage
  module (4 copies -> 1)
- run_step helper in lib.rs replaces 9 if-let-Err blocks
- maybe_backfill_log_costs returns bool to skip duplicate total_cost
  parsing in caller
This commit is contained in:
Jason
2026-04-29 22:38:29 +08:00
parent 2ee7cb4101
commit f061b777b7
16 changed files with 1248 additions and 203 deletions
+44 -42
View File
@@ -924,28 +924,35 @@ pub fn run() {
tauri::async_runtime::spawn(async move {
const SESSION_SYNC_INTERVAL_SECS: u64 = 60;
fn run_step<T>(name: &str, result: Result<T, crate::error::AppError>) {
if let Err(e) = result {
log::warn!("{name} failed: {e}");
}
}
let db = &db_for_session_sync;
// 首次同步
if let Err(e) =
crate::services::session_usage::sync_claude_session_logs(
&db_for_session_sync,
)
{
log::warn!("Session usage initial sync failed: {e}");
}
if let Err(e) =
crate::services::session_usage_codex::sync_codex_usage(
&db_for_session_sync,
)
{
log::warn!("Codex usage initial sync failed: {e}");
}
if let Err(e) =
crate::services::session_usage_gemini::sync_gemini_usage(
&db_for_session_sync,
)
{
log::warn!("Gemini usage initial sync failed: {e}");
}
run_step(
"Usage cost startup backfill",
db.backfill_missing_usage_costs(),
);
run_step(
"Session usage initial sync",
crate::services::session_usage::sync_claude_session_logs(db),
);
run_step(
"Codex usage initial sync",
crate::services::session_usage_codex::sync_codex_usage(db),
);
run_step(
"Gemini usage initial sync",
crate::services::session_usage_gemini::sync_gemini_usage(db),
);
run_step(
"Hermes usage initial sync",
crate::services::session_usage_hermes::sync_hermes_usage(db),
);
// 定期同步
let mut interval = tokio::time::interval(std::time::Duration::from_secs(
@@ -954,27 +961,22 @@ pub fn run() {
interval.tick().await; // skip immediate first tick
loop {
interval.tick().await;
if let Err(e) =
crate::services::session_usage::sync_claude_session_logs(
&db_for_session_sync,
)
{
log::warn!("Session usage periodic sync failed: {e}");
}
if let Err(e) =
crate::services::session_usage_codex::sync_codex_usage(
&db_for_session_sync,
)
{
log::warn!("Codex usage periodic sync failed: {e}");
}
if let Err(e) =
crate::services::session_usage_gemini::sync_gemini_usage(
&db_for_session_sync,
)
{
log::warn!("Gemini usage periodic sync failed: {e}");
}
run_step(
"Session usage periodic sync",
crate::services::session_usage::sync_claude_session_logs(db),
);
run_step(
"Codex usage periodic sync",
crate::services::session_usage_codex::sync_codex_usage(db),
);
run_step(
"Gemini usage periodic sync",
crate::services::session_usage_gemini::sync_gemini_usage(db),
);
run_step(
"Hermes usage periodic sync",
crate::services::session_usage_hermes::sync_hermes_usage(db),
);
}
});
});