From 0396cd5491b81f7c8ca9a30ffc5aa966dab3ff4f Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 9 Jun 2026 11:35:38 +0800 Subject: [PATCH] fix(usage): count Claude Code Workflow sub-agent token usage collect_jsonl_files only walked //subagents/*.jsonl, so it missed Workflow sub-agent transcripts which live one level deeper at subagents/workflows/wf_*/agent-*.jsonl. As a result all Workflow token usage was invisible to the no-proxy session-log accounting. Descend into subagents/workflows/wf_*/ as well, via a new push_jsonl_children helper that keeps the fixed-depth, no-recursion design. journal.jsonl carries no assistant rows so it is skipped at parse time and needs no filename special-casing. Existing dedup (request_id PK + INSERT OR IGNORE + should_skip_session_insert) keeps the next sync's backfill idempotent. Add test_collect_jsonl_files_includes_workflow_subagents. --- src-tauri/src/services/session_usage.rs | 77 ++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/services/session_usage.rs b/src-tauri/src/services/session_usage.rs index fae5a8e50..002b34d27 100644 --- a/src-tauri/src/services/session_usage.rs +++ b/src-tauri/src/services/session_usage.rs @@ -109,9 +109,15 @@ pub fn sync_claude_session_logs(db: &Database) -> Result/`。漏掉这一层会让 Workflow 的 token +/// 用量完全不计入统计;`journal.jsonl` 不含 `type=="assistant"` 行,解析时 +/// 会被 `sync_single_file` 天然跳过,因此这里无需按文件名过滤。 fn collect_jsonl_files(projects_dir: &Path) -> Vec { let mut files = Vec::new(); @@ -136,12 +142,18 @@ fn collect_jsonl_files(projects_dir: &Path) -> Vec { // 扫描子 agent 目录: 项目/SESSION_ID/subagents/*.jsonl let subagents_dir = sub_path.join("subagents"); if subagents_dir.is_dir() { - if let Ok(agent_entries) = fs::read_dir(&subagents_dir) { - for agent_entry in agent_entries.flatten() { - let agent_path = agent_entry.path(); - if agent_path.extension().and_then(|e| e.to_str()) == Some("jsonl") - { - files.push(agent_path); + push_jsonl_children(&subagents_dir, &mut files); + + // 额外下探 Workflow 子 agent: + // 项目/SESSION_ID/subagents/workflows/wf_/*.jsonl + let workflows_dir = subagents_dir.join("workflows"); + if workflows_dir.is_dir() { + if let Ok(wf_entries) = fs::read_dir(&workflows_dir) { + for wf_entry in wf_entries.flatten() { + let wf_path = wf_entry.path(); + if wf_path.is_dir() { + push_jsonl_children(&wf_path, &mut files); + } } } } @@ -154,6 +166,18 @@ fn collect_jsonl_files(projects_dir: &Path) -> Vec { files } +/// 将 `dir` 下直接子层的所有 `.jsonl` 文件追加到 `files`(不递归)。 +fn push_jsonl_children(dir: &Path, files: &mut Vec) { + if let Ok(entries) = fs::read_dir(dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().and_then(|e| e.to_str()) == Some("jsonl") { + files.push(path); + } + } + } +} + /// 同步单个 JSONL 文件,返回 (imported, skipped) fn sync_single_file(db: &Database, file_path: &Path) -> Result<(u32, u32), AppError> { let file_path_str = file_path.to_string_lossy().to_string(); @@ -685,4 +709,39 @@ mod tests { fs::remove_dir_all(&tmp).ok(); } + + #[test] + fn test_collect_jsonl_files_includes_workflow_subagents() { + // Claude Code Workflow 把子 agent transcript 嵌在 + // 项目/SESSION_ID/subagents/workflows/wf_/ 下,比普通子 agent 深一层。 + let tmp = std::env::temp_dir().join(format!("cc-switch-test-{}", uuid::Uuid::new_v4())); + let project = tmp.join("project"); + let session_dir = project.join("test-session"); + let subagents_dir = session_dir.join("subagents"); + let wf_dir = subagents_dir.join("workflows").join("wf_test123"); + fs::create_dir_all(&wf_dir).unwrap(); + + fs::write(project.join("main.jsonl"), "{}").unwrap(); + fs::write(subagents_dir.join("agent-plain.jsonl"), "{}").unwrap(); + fs::write(wf_dir.join("agent-wf.jsonl"), "{}").unwrap(); + // journal.jsonl 也会被收集,但解析时因无 assistant 行而产出 0 条 + fs::write(wf_dir.join("journal.jsonl"), "{}").unwrap(); + + let files = collect_jsonl_files(&tmp); + let paths: Vec = files + .iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + + // 主会话 + 普通子 agent + Workflow 子 agent(agent-wf + journal) = 4 + assert_eq!(files.len(), 4); + assert!(paths.iter().any(|p| p.contains("main.jsonl"))); + assert!(paths.iter().any(|p| p.contains("agent-plain.jsonl"))); + assert!( + paths.iter().any(|p| p.contains("agent-wf.jsonl")), + "Workflow 子 agent transcript 必须被收集" + ); + + fs::remove_dir_all(&tmp).ok(); + } }