news 2026/9/10 15:28:55

ECC `/cost-report` 命令实战:从本地成本追踪数据生成 Claude Code 支出报告

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ECC `/cost-report` 命令实战:从本地成本追踪数据生成 Claude Code 支出报告

ECC/cost-report命令实战:从本地成本追踪数据生成 Claude Code 支出报告

【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC

导读

ECC(Everything Claude Code)提供了一条cost-report命令,用于从本地成本追踪数据中汇总 Claude Code 的使用支出,按日、按模型/项目、按会话维度生成紧凑报告,并支持一键导出 CSV。本文以仓库内docs/ja-JP/commands/cost-report.md(日文版命令文档)为主体脉络,结合当前仓库中命令文档、stop:cost-tracker钩子源码与测试用例,完整讲解该命令的查询逻辑、数据来源、每条查询语句的含义与底层实现原理。读完本文,你将掌握如何独立复现这条命令的所有聚合查询,并能理解追踪器写入的每一条数据的语义。

命令定位与数据来源

cost-report是 ECC 的命令体系之一,在 docs/COMMAND-REGISTRY.json 中注册,其描述为「从 ECC cost-tracker 指标日志生成本地 Claude Code 成本报告」,类型标记为testing。命令文档本体位于 commands/cost-report.md。

需要特别注意:这条命令存在两个历史版本,数据存储形态不同

  • 旧版(社区 PR 复活版):即日文文档docs/ja-JP/commands/cost-report.md所描述的形态。它假设一个成本追踪钩子或插件已经把使用记录写入 SQLite 数据库~/.claude-cost-tracker/usage.db,命令通过sqlite3查询usage表。日文文档末尾注明,该命令源自MayurBhavsar的旧社区 PR #1304。
  • 当前仓库实现(v2):英文命令文档 commands/cost-report.md 描述的是演进后的形态。ECC 的stop:cost-tracker钩子(scripts/hooks/cost-tracker.js)在每个会话结束时向~/.claude/metrics/costs.jsonl追加一行 JSON,命令改用node读取该 JSONL 日志做聚合。改用node而非sqlite3/jq的原因在文档中写得很明确:保证在 macOS、Linux、Windows 三平台上行为一致

当前仓库中commands/cost-report.md、钩子源码与测试均为 JSONL 版本,因此本文以 JSONL 版本为准讲解实际行为,同时完整保留日文文档中 SQLite 版本的查询骨架(汇总、分组、最近 7 天、CSV 导出),两者聚合逻辑一一对应,方便读者理解演进脉络。

命令执行流程

无论哪个版本,cost-report的工作流程都是四步:

  1. 检查查询工具是否可用(SQLite 版本检查sqlite3;JSONL 版本使用node,天然跨平台);
  2. 检查数据文件是否存在(~/.claude-cost-tracker/usage.db~/.claude/metrics/costs.jsonl);
  3. 对数据执行聚合查询;
  4. 输出紧凑报告;若参数为csv,则导出最近的使用行。

日文文档给出了数据文件存在性检查的等价命令:

test -f ~/.claude-cost-tracker/usage.db && echo "Database found" || echo "Database not found"

对应到当前仓库的 JSONL 实现,检查命令为:

node -e 'const fs=require("fs"),os=require("os"),p=require("path");const f=p.join(os.homedir(),"claude","metrics","costs.jsonl");console.log(fs.existsSync(f)?"cost log found":"cost log not found: "+f)'

前提条件:数据文件必须由本地成本追踪器写入。文件不存在时,命令会告知用户追踪器尚未启用,并提示先安装/启用可信的 Claude Code 成本追踪钩子或插件。对当前仓库而言,就是启用stop:cost-tracker钩子并完成至少一个会话——钩子只在会话结束时(Stop 事件)写入首行数据。

数据模型:每行的语义

理解报告的聚合逻辑,必须先理解数据行的语义。scripts/hooks/cost-tracker.js的头部注释与 skills/cost-tracking/SKILL.md 给出了行结构:

字段含义
timestamp快照的 ISO 时间戳
session_idClaude Code 会话标识
transcript_path会话 transcript 文件路径
model使用的模型
input_tokens/output_tokensToken 计数
cache_write_tokens/cache_read_tokens提示缓存写入/读取的 Token 计数
estimated_cost_usd该会话累计成本的预估美元值(预计算)

两个关键语义约束:

  1. 每行是会话的累计快照(cumulative snapshot):Stop 事件按「每次助手响应」触发而非按会话触发,因此同一会话会产生多行,每行代表截至该时刻的累计总量。汇总时必须取每个session_id的最后一行再跨会话求和——把每一行都加起来会造成重复计数。这正是报告脚本中bySessionMap 去重逻辑存在的根本原因。
  2. 成本取预计算值:报告依赖追踪器写入的estimated_cost_usd绝不从原始 Token 数重新估算价格。因为模型与缓存价格会变化,追踪器才是事实来源。

汇总查询:今日 / 昨日 / 总计

日文文档给出了 SQLite 版本的汇总查询(usage表),完整继承如下:

sqlite3 -header -column ~/.claude-cost-tracker/usage.db " SELECT ROUND(COALESCE(SUM(CASE WHEN date(timestamp) = date('now') THEN cost_usd END), 0), 4) AS today_cost, ROUND(COALESCE(SUM(CASE WHEN date(timestamp) = date('now', '-1 day') THEN cost_usd END), 0), 4) AS yesterday_cost, ROUND(COALESCE(SUM(cost_usd), 0), 4) AS total_cost, COUNT(*) AS total_calls, COUNT(DISTINCT session_id) AS sessions FROM usage; "

对应到当前仓库的 JSONL 版本,命令文档中的等价实现为:

node -e ' const fs=require("fs"),os=require("os"),path=require("path"); const f=path.join(os.homedir(),".claude","metrics","costs.jsonl"); if(!fs.existsSync(f)){console.log("Cost tracker not set up: "+f+" not found. Enable the stop:cost-tracker hook and finish a session first.");process.exit(0);} const rows=fs.readFileSync(f,"utf8").split(/\r?\n/).filter(Boolean).map(l=>{try{return JSON.parse(l)}catch{return null}}).filter(Boolean); const bySession=new Map(); for(const r of rows){const k=r.session_id||r.transcript_path||r.timestamp;const p=bySession.get(k);if(!p||String(r.timestamp)>String(p.timestamp))bySession.set(k,r);} const latest=[...bySession.values()]; const cost=r=>Number(r.estimated_cost_usd)||0; const day=r=>String(r.timestamp||"").slice(0,10); const today=new Date().toISOString().slice(0,10); const d=new Date(Date.now()-864e5).toISOString().slice(0,10); const sum=a=>a.reduce((s,r)=>s+cost(r),0); const f4=n=>"$"+n.toFixed(4); console.log("=== Cost summary ==="); console.log("today: "+f4(sum(latest.filter(r=>day(r)===today)))); console.log("yesterday: "+f4(sum(latest.filter(r=>day(r)===d)))); console.log("total: "+f4(sum(latest))+" ("+latest.length+" sessions)"); '

实现要点逐条对应:

  • 去重bySessionsession_id(缺失时回退到transcript_pathtimestamp)为键,仅保留时间戳最新的一行——对应 SQL 版中「按会话取累计快照」的要求。
  • 日切割day()截取 ISO 时间戳前 10 位得到YYYY-MM-DDtodaynew Date().toISOString().slice(0,10)计算,yesterdayDate.now()-864e5(即 24 小时前)计算——对应 SQL 版中的date('now')date('now','-1 day')
  • 成本归一cost()Number(...) || 0兜底,任何缺失或非法值都按 0 处理,对应 SQL 版中的COALESCE(..., 0)
  • 金额格式f4统一输出四位小数,即文档约定的「1 美元以下金额保留 4 位小数」规则。

按维度分组:项目 / 工具 / 模型

日文文档提供了两个 SQLite 分组查询,分别按项目与工具聚合,均按成本降序排列:

# 按项目 sqlite3 -header -column ~/.claude-cost-tracker/usage.db " SELECT project, ROUND(SUM(cost_usd), 4) AS cost, COUNT(*) AS calls FROM usage GROUP BY project ORDER BY cost DESC; " # 按工具 sqlite3 -header -column ~/.claude-cost-tracker/usage.db " SELECT tool_name, ROUND(SUM(cost_usd), 4) AS cost, COUNT(*) AS calls FROM usage GROUP BY tool_name ORDER BY cost DESC; "

当前仓库的 JSONL 行结构中没有projecttool_name字段,其维度被model取代。命令文档中的按模型分组实现为:

console.log("\n=== By model ==="); for(const [k,v] of by(r=>r.model))console.log(f4(v).padStart(12)+" "+k);

其中by是通用的分组聚合器:

const by=(key)=>{ const m=new Map(); for(const r of latest){ const k=key(r)||"(unknown)"; m.set(k,(m.get(k)||0)+cost(r)); } return [...m.entries()].sort((a,b)=>b[1]-a[1]); };
  • 分组键取r.model,缺失时归入(unknown)组;
  • 求和后按成本降序排列,输出时f4(...).padStart(12)右对齐保证列对齐;
  • 语义与 SQL 版的GROUP BY ... ORDER BY cost DESC完全一致。

最近 7 天趋势

日文文档的 SQLite 版本:

sqlite3 -header -column ~/.claude-cost-tracker/usage.db " SELECT date(timestamp) AS date, ROUND(SUM(cost_usd), 4) AS cost, COUNT(*) AS calls FROM usage GROUP BY date(timestamp) ORDER BY date DESC LIMIT 7; "

JSONL 版本实现:

console.log("\n=== Last 7 days ==="); const days=new Map(); for(const r of latest){const k=day(r);days.set(k,(days.get(k)||0)+cost(r));} [...days.entries()].sort((a,b)=>b[0]<a[0]?-1:1).slice(0,7).forEach(([k,v])=>console.log(k+" "+f4(v)));

按日期分组求和后,按日期倒序取前 7 天,输出格式为日期 + 成本

CSV 导出:/cost-report csv

当用户以/cost-report csv调用时,导出最近的使用行。SQLite 版本使用显式列名:

sqlite3 -csv -header ~/.claude-cost-tracker/usage.db " SELECT timestamp, project, tool_name, input_tokens, output_tokens, cost_usd, session_id, model FROM usage ORDER BY timestamp DESC LIMIT 100; "

JSONL 版本使用与行结构一致的列:

node -e ' const fs=require("fs"),os=require("os"),path=require("path"); const f=path.join(os.homedir(),".claude","metrics","costs.jsonl"); if(!fs.existsSync(f)){console.error("no data");process.exit(0);} const rows=fs.readFileSync(f,"utf8").split(/\r?\n/).filter(Boolean).map(l=>{try{return JSON.parse(l)}catch{return null}}).filter(Boolean).slice(-100); console.log("timestamp,session_id,model,input_tokens,output_tokens,cache_write_tokens,cache_read_tokens,estimated_cost_usd"); for(const r of rows)console.log([r.timestamp,r.session_id,r.model,r.input_tokens,r.output_tokens,r.cache_write_tokens,r.cache_read_tokens,r.estimated_cost_usd].join(",")); '

要点:

  • 取原始行数组的最后 100 行slice(-100),等价于 SQL 的ORDER BY timestamp DESC LIMIT 100的最近行);
  • 首行输出带表头的 CSV 列名;
  • 字段用逗号直接拼接;与 SQL 版相比,字段从cost_usd变为estimated_cost_usd,并增加了cache_write_tokens/cache_read_tokens两个缓存字段,使 CSV 保留了完整的会话成本结构。

报告格式约定

命令将响应整理为固定结构:

  1. 汇总(Summary):今日、昨日、总计、调用次数(会话数);
  2. 按维度分组:按总成本降序排名(SQLite 版为项目/工具,JSONL 版为模型);
  3. 最近 7 天:日期、成本、调用次数。

金额格式化规则:1 美元以下保留 4 位小数($0.0123),更大金额可缩减位数。命令本身不做价格估算——它只信任追踪器写入的预计算成本值。

源码级原理:stop:cost-tracker钩子如何产出数据

要彻底理解报告,需要知道costs.jsonl的行是怎么算出来的。核心实现在 scripts/hooks/cost-tracker.js,该钩子通过 hooks/hooks.json 注册为stop:cost-tracker,匹配所有会话(matcher: ".*"),由run-with-flags.jsminimal,standard,strict模式运行,异步执行、超时 10 秒。

1. 数据不是从 Stop 载荷直接拿的

Stop 事件的 stdin 载荷形如{ session_id, transcript_path, cwd, hook_event_name, ... }并不直接包含usagemodel字段。钩子文件头部注释记录了一个真实的教训:旧版本期望这些字段存在,结果 52 天内在 2340 行记录中非零 Token 率为 0.0%——全部是零值行。修复方式是改读 Claude Code 已经传入的 transcript 文件

2. Transcript 解析与按 message.id 去重

Claude Code 的 transcript 是 JSONL,每行一个内容块。sumUsageFromTranscript()只处理type === "assistant"且带message.usage的行,并做一次关键去重:

Claude Code 每个内容块写一行 JSONL,因此同一次 API 响应(同一个message.id)会横跨多行 assistant 记录,且每行重复相同的 usage。逐行求和会把总量放大 2.5~3 倍。实测:一个 704 行 assistant 记录的会话只有 286 个唯一message.id——逐行求和得 $867,按 id 去重后仅 $333。

因此钩子以message.id为键、保留每个 id 的最后一行 usage,再累计input_tokensoutput_tokenscache_creation_input_tokenscache_read_input_tokens。对没有message.id的旧格式行,用合成键__line_N维持原有逐行行为。模型名取最后一个非unknown值。

3. 内置费率表

钩子内置了一张近似费率表(每百万 Token 美元),用于把 Token 折算为成本:

档位输入输出缓存写入缓存读取
haiku1.005.01.250.10
sonnet(Sonnet 4.6 等)3.0015.03.750.30
sonnet52.0010.02.500.20
opus(Opus 4.5+)5.0025.06.250.50
opusLegacy(Opus 3 / 4.0 / 4.1)15.0075.018.751.50
fable/mythos10.0050.012.501.00

模型匹配规则由getRates()实现:包含fable/mythos走 Fable 档;包含haiku走 Haiku 档;正则精确匹配sonnet-5(避免把claude-sonnet-50误判);LEGACY_OPUS_RE用于识别带日期快照的 Opus 4.0(如claude-opus-4-20250514)等旧档模型。

4. Harness 权威成本优先

钩子还支持一个可选的权威成本通道:如果用户的 statusline 在每次渲染时把 Claude Code 直接下发的cost.total_cost_usd写入<os.tmpdir()>/harness-cost-<session_id>.json(内容为{ts, cost_usd}),钩子会优先采用该值,前提是缓存新鲜(HARNESS_COST_MAX_AGE_SECONDS = 300,即 5 秒内)。优先的原因写在文件头:

  • 硬编码费率表无法表达 Opus 4.7 的 >200K Token 2x 档位与 1 小时缓存 2x 档位,长会话会低估;
  • 对整个 transcript 求和会在--resume边界重复计算,而cost.total_cost_usd是按进程计的,不会漂移。

缓存缺失或过期则回退到 transcript 求和值。最终行写入~/.claude/metrics/costs.jsonl(实际目录经getClaudeDir()解析,测试中以临时 HOME 覆盖验证)。

5. 失败不阻塞会话

整个处理包裹在 try/catch 中,任何解析错误都不会让 Stop 钩子失败(fail-open);stdin 超过 1MB 时(Stop 载荷携带last_assistant_message时常超旧版 64KB 上限)抑制透传并在 stderr 告警,避免把截断的 JSON 回显到 stdout 被报告为钩子失败。

测试验证:行为被测试用例锁死

tests/hooks/cost-tracker.test.jsnode tests/hooks/cost-tracker.test.js运行)覆盖了上述全部关键行为,可作为复现与验证的依据:

  • stdin 透传:钩子必须原样回写 stdin,空输入与非法 JSON 均不崩溃(fail-open 契约);
  • Token 汇总:从 transcript 正确累计 input/output/cache 四类 Token,最后出现的 assistant 模型被记录;
  • 按 message.id 去重:同一msg_01AAA出现 3 行只计 1 次,input_tokens精确为 1025 而非 3 倍;
  • 会话 ID 优先级ECC_SESSION_ID优先于CLAUSE_SESSION_ID,其次才用载荷中的session_id——这是与 ECC2 会话关联的约定;
  • Harness 成本优先:新鲜缓存(cost_usd: 1.23)胜过 transcript 估算,且 Token 仍来自 transcript;超过 300 秒的陈旧缓存(999.99)被忽略并回退到 transcript 估算;
  • 费率准确性:Sonnet 5 的 1M/1M Token 精确等于 $12;含缓存写入/读取时 $14.70;Sonnet 4.6 保持 $18 且不被误判为 Sonnet 5;带日期的 Sonnet 5(claude-sonnet-5-20261001)按 $12;claude-sonnet-50近误判回退 $18;带日期的 Opus 4.0 保留 $15/$75 旧档(1M/1M = $90),Opus 4.5 用现行 $5/$25(= $30)。

这些断言直接印证了「报告只信任预计算值」的约定:估算精度由钩子侧统一保证,报告侧不重复定价。

关联资源

  • 命令文档(当前实现):commands/cost-report.md(英文原版,数据为costs.jsonl
  • 日文命令文档(SQLite 旧版脉络):docs/ja-JP/commands/cost-report.md
  • 钩子源码:scripts/hooks/cost-tracker.js
  • 钩子注册:hooks/hooks.json
  • 测试用例:tests/hooks/cost-tracker.test.js
  • 成本追踪技能:skills/cost-tracking/SKILL.md(含行结构表格、反模式清单:不要全量求和、不要拿原始 Token 自行估价、不要假定日志存在、不要在面向用户的回答中硬编码模型价格)
  • 命令注册表:docs/COMMAND-REGISTRY.json

小结

/cost-report的价值在于把「成本数据收集」与「成本数据展示」解耦:追踪侧由stop:cost-tracker钩子在会话停止时按message.id去重汇总 transcript,叠加内置费率表或优先采用 harness 权威成本,产出带estimated_cost_usd的累计快照;报告侧只做「每会话取最新一行 → 去重 → 聚合」这一件事,通过摘要、按模型分组、最近 7 天与 CSV 导出四种视图回答「今天/昨天花了多少、哪个模型最贵、趋势如何」等问题。理解行语义(累计快照而非增量行)是正确解读一切报告的前提,也是本命令最值得记住的设计要点。

【免费下载链接】ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.项目地址: https://gitcode.com/GitHub_Trending/ev/ECC

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

TVBoxOSC 语音控制 5 分钟上手指南:3 步开口即操作

TVBoxOSC 语音控制 5 分钟上手指南&#xff1a;3 步开口即操作 【免费下载链接】TVBoxOSC TVBoxOSC - 一个基于第三方项目的代码库&#xff0c;用于电视盒子的控制和管理。 项目地址: https://gitcode.com/GitHub_Trending/tv/TVBoxOSC 坐在沙发上不用碰遥控器&#xff…

作者头像 李华
网站建设 2026/9/10 15:18:54

电磁仿真软件选型与应用全解析

1. 电磁仿真软件行业现状与核心需求电磁场仿真技术作为现代电子工程设计的基石&#xff0c;已经渗透到通信设备、汽车电子、航空航天等各个领域。根据2023年EDA行业报告显示&#xff0c;全球电磁仿真软件市场规模已突破50亿美元&#xff0c;年复合增长率保持在12%以上。这种快速…

作者头像 李华
网站建设 2026/9/10 15:18:31

微服务架构疫苗预约系统实战:从Spring Boot到云部署全解析

先交代一下这个项目的来源。上半年我帮一个社区接种点做信息化改造&#xff0c;他们当时的预约方式是微信群接龙加现场排队&#xff0c;每天早上八点半放号&#xff0c;手机一响所有人同时点&#xff0c;页面直接卡死。后来我以这个真实场景为蓝本&#xff0c;用 Spring Boot 做…

作者头像 李华