ccusage Droid 适配器深度解析:从 Factory Droid 会话文件到用量报告
【免费下载链接】ccusagenpx ccusage项目地址: https://gitcode.com/gh_mirrors/cc/ccusage
本指南以ccusage-adapter-droid(位于 rust/adapters/droid/README.md)为骨架,完整讲解该适配器如何将 Factory Droid 写入磁盘的*.settings.json会话文件转换为 ccusage 报告渲染所需的用量条目(usage entries),覆盖文件发现、并行读取、Token 解析、模型归一化、会话去重、定价与报告生成的全链路。读完本文,你将掌握ccusage droid命令的数据来源、配置方式与实现原理,能够理解甚至自定义该适配器的行为边界。
适配器职责:一份"只做转换、不做通用"的代码
ccusage-adapter-droid的定位非常明确:把 Factory Droid 的 session JSON 文件变成报告可渲染的用量条目。它是一个"源专属"(source-specific)适配器,其设计哲学在 README 中被一句话点破:
Anything that is not specific to this source belongs in
ccusage-coreorccusage-adapter-commoninstead.(凡不属于该数据源特有的逻辑,都应放进ccusage-core或ccusage-adapter-common。)
这意味着整个 crate 只有 4 个源文件,各自承担单一职责:
| 模块 | 职责 |
|---|---|
| loader.rs | 读取数据源、按 session 去重、日期过滤 |
| parser.rs | 原始记录解析、Token 字段映射、模型命名归一化 |
| paths.rs | 环境变量、默认目录、文件发现 |
| report.rs | 与共享形状不同的 JSON 与表格输出 |
目录结构也印证了这一点:src/lib.rs只做模块声明与run入口编排,通用能力(文件遍历、并行读取、日期过滤、表格渲染)全部复用ccusage-adapter-common与ccusage-core,Cargo.toml(见 rust/adapters/droid/Cargo.toml)声明的运行时依赖仅有ccusage-adapter-common、ccusage-core、jiff(时区/时间处理)与serde_json四项。
数据源与文件发现机制
README 给出的数据源定义是:
${DROID_SESSIONS_DIR:-~/.factory/sessions}/**/*.json即:默认读取~/.factory/sessions下的所有 JSON 文件;若设置了环境变量DROID_SESSIONS_DIR,则改用它指定的目录。源码 paths.rs 在此基础上做了三层细化:
- 多路径支持:
DROID_SESSIONS_DIR支持用逗号分隔多个目录,每个目录都会被依次扫描; - 后缀过滤:虽然遍历时收集所有
*.json文件,但最终只保留文件名以.settings.json结尾的文件(discover_settings_files),避免把同一会话目录下无关的 JSON 配置混入统计; - 目录去重:对解析出的路径做
HashSet去重,重复或不存在(非目录)的路径会被跳过。
在没有设置环境变量且HOME不可用时,droid_session_paths会返回"home directory is not set"错误(见 paths.rs 第 31-33 行)。
文件读取:大小均衡分块 + 有序并行
README 特别强调,文件读取"通过ccusage-adapter-common完成,它负责 walking、size-balanced chunking 与 ordered parallel reads"。对应实现位于 rust/adapters/common/src/lib.rs:
collect_files_with_extension递归遍历目录收集指定扩展名的文件;chunk_file_indexes_by_size按文件字节大小做加权排序,再用贪心算法把索引分配到各 chunk,使每个 worker 处理的字节总量尽量均衡,避免一个大文件拖慢整体;read_files_parallel依据available_parallelism决定 worker 数量(--single-thread时降为 1),通过thread::scope并行读取,并按原始文件顺序重组结果——droid 的load_entries_inner在注释中明确说明:并行读回后必须保持排序后的文件顺序,才能保证随后的稳定排序与"最新快照优先"去重结果与单线程读取一致(见 loader.rs 第 26-28 行)。
每个文件由load_settings_file解析;解析失败(如 JSON 语法错误)不会中断整体流程,而是记入 debug 日志后跳过该文件(返回None)。
Token 解析:tokenUsage字段映射与兜底
load_settings_file的核心是读取会话文件 JSON 中的tokenUsage对象,parser.rs 中parse_token_usage按如下字段映射到 ccusage 的通用TokenUsageRaw:
| Factory Droid 字段 | ccusage 内部字段 | 含义 |
|---|---|---|
inputTokens | input_tokens | 输入 token 数 |
outputTokens | output_tokens | 输出 token 数 |
cacheCreationTokens | cache_creation_input_tokens | 缓存创建 token 数 |
cacheReadTokens | cache_read_input_tokens | 缓存读取 token 数 |
thinkingTokens | extra_total_tokens(经reasoning_tokens) | 思考 token 数,单独计入总量 |
totalTokens | — | 兜底字段,见下 |
关键兜底逻辑:当上述分项缺失时,apply_total_token_fallback会尝试用totalTokens补齐(对应测试falls_back_to_total_tokens_when_droid_parts_are_missing中,仅给{"totalTokens": 456}时,output_tokens被置为 456)。若五类 token 之和为 0,则该文件被判定为无有效用量而跳过。
thinkingTokens被单独保存在reasoning_tokens,最终写入LoadedEntry.extra_total_tokens(见 loader.rs 第 83 行),并在报告统计时计入总 token 数——测试report_total_includes_thinking_tokens验证了这一点:输入 100 + 输出 50 + 缓存创建 20 + 缓存读取 10 + 思考 5 = 总 token 185。
模型与 Provider 归一化:三路取模
Droid 的模型名字符串很"脏"(如custom:Claude-Opus-4.5-Thinking-[Anthropic]-0),normalize_droid_model_name会按顺序处理:
- 剥离
custom:前缀; - 删除方括号
[...]包裹的片段(如[Anthropic]); - 转小写,并把
.、空白、-统一折叠为单个-,同时修剪首尾与连续连字符。
结果custom:Claude-Opus-4.5-Thinking-[Anthropic]-0→claude-opus-4-5-thinking-0,gemini-2.5-pro→gemini-2-5-pro(见 loader.rs 测试normalizes_droid_model_names)。
模型的来源按优先级有三路:
model字段:存在则直接归一化;- sidecar JSONL:
extract_model_from_sidecar_jsonl查找与<session>.settings.json同名的<session>.jsonl,在前 500 行中扫描形如Model: xxx的行提取模型名(对应测试falls_back_to_sidecar_jsonl_model); - Provider 默认名:都拿不到时,按 provider 回退为
claude-unknown、gpt-unknown、gemini-unknown、grok-unknown或unknown。
Provider 同样有两级推断:先看providerLock字段(normalize_droid_provider会把claude/anthropic、google_ai/gemini/vertex_ai、x_ai/grok等别名归一到anthropic/google/xai);若为unknown,再由模型名特征反推(含claude/opus/sonnet/haiku判为 anthropic,gpt-/chatgpt/o+数字 判为 openai,含gemini判为 google,含grok判为 xai)。
时间戳与定价:以providerLockTimestamp为准
会话条目需要一个时间戳用于日期分组与排序。settings_timestamp优先使用providerLockTimestamp(RFC 3339 格式,经parse_ts_timestamp解析并统一序列化为毫秒精度);字段缺失时兜底使用文件系统 mtime。
这一选择对定价有直接影响:calculate_droid_cost会带上pricing_timestamp(即providerLockTimestamp)调用calculate_cost_for_usage_at,让费用按锁定 provider 时的价格计算,而非按报告生成时的最新价。对应测试分别验证了:
preserves_provider_lock_timestamp_for_pricing:设置文件带providerLockTimestamp时,pricing_timestamp等于该时刻;leaves_pricing_timestamp_empty_when_only_file_metadata_is_available:仅能拿到 mtime 时pricing_timestamp为None;does_not_use_display_timestamp_for_droid_pricing:deepseek 模型按 providerLock 时刻计价(100 万输入 token × 0.00000014 = 0.14 美元)。
定价的模型候选由droid_model_candidates生成:先尝试裸模型名,再按 provider 加前缀(如 anthropic 会依次尝试anthropic/<model>、openrouter/anthropic/<model>,openai 尝试openai/、openrouter/openai/,google 尝试google/、vertex_ai/、openrouter/google/,xai 尝试xai/、openrouter/x-ai/),取第一个能算出正费用的候选。注意calculate_droid_cost会把reasoning_tokens并入output_tokens参与计费,且成本模式固定为CostMode::Calculate。
会话去重:最新快照优先(latest-wins)
Factory Droid 的会话可能被多次写入快照(例如archive/session-c.settings.json与根目录下的session-c.settings.json并存)。load_entries_inner的去重策略是:
- 所有条目按时间戳升序排序;
- 从最新到最旧遍历,用
HashSet记录已见过的session_id; - 每个
session_id只保留时间戳最新的一条。
测试keeps_latest_snapshot_for_duplicate_session_ids验证了这一点:两个session-c快照(05-01 与 05-02)最终只产出 1 条,且取 05-02 的用量(输入 100、输出 200)。
session_id的取值来自文件名:去掉.settings.json后缀即得(如session-a.settings.json→session-a),无法识别时回退为"unknown"。每个条目的 message id 统一写成droid:<session_id>格式,项目名固定为droid、项目路径显示为Droid(见to_loaded_entry)。
报告形状:四类聚合与 JSON 输出
report.rs 的summarize_entries按报告类型聚合:
- Daily:按
entry.date分组; - Weekly / Monthly:先按天聚合,再通过
summarize_summaries_by_bucket以周日为一周起点重新分桶; - Session:按
session_id分组,并把分组键放入session_id字段。
report_from_rows生成的 JSON 形状为{ "daily" | "weekly" | "monthly" | "sessions": [...], "totals": {...} },行内数据复用共享的agent_summary_json,总额由totals_json计算——因此 droid 在 JSON 层面几乎没有重复代码,这正是 README 所述"只在与共享形状不同处做覆盖"的体现。
CLI 集成与运行流程
Droid 适配器通过Command::Droid接入 ccusage 主程序(rust/crates/ccusage/src/main.rs 第 39 行Some(Command::Droid(args)) => adapter::droid::run(args)),并在 rust/crates/ccusage-cli-parser/src/cli-commands.json 中注册了droid、droid daily、droid monthly、droid session等子命令。last_window.rs与timezone.rs也将 Droid 纳入--last-window与时区推导的适用命令列表。
run(lib.rs)的完整流程是:
- 用
PricingMap::load_with_overrides加载价格表(支持--offline、日志级别与--pricing-overrides自定义); load_entries并行读取、解析并去重;filter_loaded_entries_by_date按--since/--until过滤日期;summarize_entries按daily/weekly/monthly/session聚合;sort_summaries按--order排序;- 若
--json(含--jq、--no-cost)则输出 JSON,否则渲染标题为 "Droid Token Usage Report" 的表格。
公共 API 与 README 声明完全一致:loader::load_entries、report::report_from_rows、report::summarize_entries与run。
依赖与构建层
Cargo.toml的依赖全部走 workspace 版本:ccusage-adapter-common(文件遍历与并行读取)、ccusage-core(LoadedEntry、PricingMap、汇总/输出通用逻辑)、jiff(时区换算)与serde_json;开发依赖ccusage-test-support提供fs_fixture!与EnvVarGuard等测试工具。构建上,droid 属于adaptersCrane artifact 层,该层在一次 Cargo 调用中同时编译全部适配器,因此各适配器可并发构建。
测试矩阵:行为即契约
droid 适配器的测试全部内联在 loader.rs 与 parser.rs 的#[cfg(test)]模块中,覆盖了适配器全部关键行为:
- 模型名归一化(
normalizes_droid_model_names); totalTokens兜底(falls_back_to_total_tokens_when_droid_parts_are_missing);- 从 settings 文件加载用量(
loads_usage_from_droid_settings_files); - sidecar JSONL 取模型(
falls_back_to_sidecar_jsonl_model); - 重复 session 取最新快照(
keeps_latest_snapshot_for_duplicate_session_ids); - thinking tokens 计入报告总量(
report_total_includes_thinking_tokens); - 定价时间戳的三个分支(providerLock 优先、mtime 兜底、deepseek 按锁定时刻计价)。
这些测试同时充当了"可运行的行为契约":任何对解析、去重或定价逻辑的改动,都必须保持上述语义不变。如果你要基于 Factory Droid 的会话文件做自定义统计,这套映射与兜底规则就是最可靠的参考蓝本。
【免费下载链接】ccusagenpx ccusage项目地址: https://gitcode.com/gh_mirrors/cc/ccusage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考