news 2026/9/21 2:46:08

ccusage Droid 适配器深度解析:从 Factory Droid 会话文件到用量报告

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ccusage Droid 适配器深度解析:从 Factory Droid 会话文件到用量报告

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 inccusage-coreorccusage-adapter-commoninstead.(凡不属于该数据源特有的逻辑,都应放进ccusage-coreccusage-adapter-common。)

这意味着整个 crate 只有 4 个源文件,各自承担单一职责:

模块职责
loader.rs读取数据源、按 session 去重、日期过滤
parser.rs原始记录解析、Token 字段映射、模型命名归一化
paths.rs环境变量、默认目录、文件发现
report.rs与共享形状不同的 JSON 与表格输出

目录结构也印证了这一点:src/lib.rs只做模块声明与run入口编排,通用能力(文件遍历、并行读取、日期过滤、表格渲染)全部复用ccusage-adapter-commonccusage-coreCargo.toml(见 rust/adapters/droid/Cargo.toml)声明的运行时依赖仅有ccusage-adapter-commonccusage-corejiff(时区/时间处理)与serde_json四项。

数据源与文件发现机制

README 给出的数据源定义是:

${DROID_SESSIONS_DIR:-~/.factory/sessions}/**/*.json

即:默认读取~/.factory/sessions下的所有 JSON 文件;若设置了环境变量DROID_SESSIONS_DIR,则改用它指定的目录。源码 paths.rs 在此基础上做了三层细化:

  1. 多路径支持DROID_SESSIONS_DIR支持用逗号分隔多个目录,每个目录都会被依次扫描;
  2. 后缀过滤:虽然遍历时收集所有*.json文件,但最终只保留文件名以.settings.json结尾的文件(discover_settings_files),避免把同一会话目录下无关的 JSON 配置混入统计;
  3. 目录去重:对解析出的路径做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 内部字段含义
inputTokensinput_tokens输入 token 数
outputTokensoutput_tokens输出 token 数
cacheCreationTokenscache_creation_input_tokens缓存创建 token 数
cacheReadTokenscache_read_input_tokens缓存读取 token 数
thinkingTokensextra_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会按顺序处理:

  1. 剥离custom:前缀;
  2. 删除方括号[...]包裹的片段(如[Anthropic]);
  3. 转小写,并把.、空白、-统一折叠为单个-,同时修剪首尾与连续连字符。

结果custom:Claude-Opus-4.5-Thinking-[Anthropic]-0claude-opus-4-5-thinking-0gemini-2.5-progemini-2-5-pro(见 loader.rs 测试normalizes_droid_model_names)。

模型的来源按优先级有三路:

  1. model字段:存在则直接归一化;
  2. sidecar JSONLextract_model_from_sidecar_jsonl查找与<session>.settings.json同名的<session>.jsonl,在前 500 行中扫描形如Model: xxx的行提取模型名(对应测试falls_back_to_sidecar_jsonl_model);
  3. Provider 默认名:都拿不到时,按 provider 回退为claude-unknowngpt-unknowngemini-unknowngrok-unknownunknown

Provider 同样有两级推断:先看providerLock字段(normalize_droid_provider会把claude/anthropicgoogle_ai/gemini/vertex_aix_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_timestampNone
  • 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的去重策略是:

  1. 所有条目按时间戳升序排序;
  2. 最新到最旧遍历,用HashSet记录已见过的session_id
  3. 每个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.jsonsession-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 中注册了droiddroid dailydroid monthlydroid session等子命令。last_window.rstimezone.rs也将 Droid 纳入--last-window与时区推导的适用命令列表。

run(lib.rs)的完整流程是:

  1. PricingMap::load_with_overrides加载价格表(支持--offline、日志级别与--pricing-overrides自定义);
  2. load_entries并行读取、解析并去重;
  3. filter_loaded_entries_by_date--since/--until过滤日期;
  4. summarize_entriesdaily/weekly/monthly/session聚合;
  5. sort_summaries--order排序;
  6. --json(含--jq--no-cost)则输出 JSON,否则渲染标题为 "Droid Token Usage Report" 的表格。

公共 API 与 README 声明完全一致:loader::load_entriesreport::report_from_rowsreport::summarize_entriesrun

依赖与构建层

Cargo.toml的依赖全部走 workspace 版本:ccusage-adapter-common(文件遍历与并行读取)、ccusage-coreLoadedEntryPricingMap、汇总/输出通用逻辑)、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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/21 2:45:47

STM32+WiFi+云平台的光感智能台灯闭环控制系统

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/21 2:44:24

React高频面试题核心考点解析:从虚拟DOM到Hooks与性能优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/21 2:43:56

深入理解 Secondary NameNode:Checkpoint 机制与 HDFS 元数据安全

很多第一次看到Secondary NameNode这个名词的人&#xff0c;都容易把它当成 NameNode 的“备胎”&#xff0c;觉得它是用来故障转移的热备节点。我在刚开始接触 HDFS 的时候也这么想过&#xff0c;直到有一次真把 NameNode 重启了&#xff0c;才意识到自己的想法错得有多离谱。…

作者头像 李华
网站建设 2026/9/21 2:42:16

软考系统架构师论文写作全攻略:范文拆解与备考实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/21 2:41:40

Raycast 2.0 深度体验:AI 启动器重构与高效工作流配置指南

1. 从启动器到指令中心&#xff1a;Raycast 2.0 到底改了什么用了三年 Raycast&#xff0c;从最早那个只能搜应用、算汇率的小工具&#xff0c;到如今把 AI、剪贴板历史、窗口管理、脚本命令全塞进一个输入框里&#xff0c;我对它的感情挺复杂。一方面它确实把我 Mac 上原本要装…

作者头像 李华