mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
fix(usage): count Claude Code Workflow sub-agent token usage
collect_jsonl_files only walked <project>/<session>/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.
This commit is contained in:
@@ -109,9 +109,15 @@ pub fn sync_claude_session_logs(db: &Database) -> Result<SessionSyncResult, AppE
|
||||
|
||||
/// 收集目录下所有 .jsonl 文件(含子 agent 文件)
|
||||
///
|
||||
/// 扫描三层固定深度,不使用递归,避免死循环:
|
||||
/// projects_dir/项目目录/*.jsonl (主会话)
|
||||
/// projects_dir/项目目录/SESSION_ID/subagents/*.jsonl (子 agent)
|
||||
/// 扫描固定深度,不使用递归,避免死循环:
|
||||
/// projects_dir/项目目录/*.jsonl (主会话)
|
||||
/// projects_dir/项目目录/SESSION_ID/subagents/*.jsonl (Task/Agent 子 agent)
|
||||
/// projects_dir/项目目录/SESSION_ID/subagents/workflows/wf_*/*.jsonl (Workflow 子 agent)
|
||||
///
|
||||
/// 最后一层是 Claude Code Workflow 功能产生的子 agent transcript,比普通子
|
||||
/// agent 多嵌套一层 `workflows/wf_<ID>/`。漏掉这一层会让 Workflow 的 token
|
||||
/// 用量完全不计入统计;`journal.jsonl` 不含 `type=="assistant"` 行,解析时
|
||||
/// 会被 `sync_single_file` 天然跳过,因此这里无需按文件名过滤。
|
||||
fn collect_jsonl_files(projects_dir: &Path) -> Vec<PathBuf> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
@@ -136,12 +142,18 @@ fn collect_jsonl_files(projects_dir: &Path) -> Vec<PathBuf> {
|
||||
// 扫描子 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_<ID>/*.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<PathBuf> {
|
||||
files
|
||||
}
|
||||
|
||||
/// 将 `dir` 下直接子层的所有 `.jsonl` 文件追加到 `files`(不递归)。
|
||||
fn push_jsonl_children(dir: &Path, files: &mut Vec<PathBuf>) {
|
||||
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_<ID>/ 下,比普通子 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<String> = 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user