ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深度解析 codeburn 的 Cursor Agent 用量解析器:transcript 目录、双格式解析与去重原理

深度解析 codeburn 的 Cursor Agent 用量解析器:transcript 目录、双格式解析与去重原理 深度解析 codeburn 的 Cursor Agent 用量解析器transcript 目录、双格式解析与去重原理【免费下载链接】codeburnFree, local tool to track AI coding token usage and cost across 37 tools and agents (Claude Code, Cursor, Codex, Gemini and more), by model, project, and task. npx codeburn项目地址: https://gitcode.com/gh_mirrors/co/codeburn本指南围绕 codeburn 对 Cursor AgentCursor 后台 Agent 模式区别于普通聊天的接入实现展开讲解其数据读取位置、两种 transcript 存储格式Legacy 纯文本与 Composer 2 JSONL的解析策略、会话元数据与去重机制以及修复相关解析 bug 的完整排查清单。读完本文你将理解 codeburn 如何把 Cursor Agent 的本地 transcript 转成可计费、可归因、可去重的 AI 用量记录并能快速定位“解析结果不对、项目归错、token 为 0”这类问题的根因。Cursor Agent 与常规 Cursor 聊天两条完全不同的数据链路codeburn 对 Cursor 生态同时维护了两个独立的 providerCursor常规聊天读取 Cursor IDE 的聊天历史Bubbles / agentKv数据源是各平台的state.vscdbSQLite 库见 src/providers/cursor.tsCursor Agent后台 Agent读取 Cursor 后台 Agent 模式的 transcript 文件与普通聊天分开存储即本文主题 src/providers/cursor-agent.ts。两者在 src/providers/index.ts 中都是懒加载lazyproviderloadCursorAgent()在首次被调用时才通过动态import(./cursor-agent.js)载入src/providers/index.tslazyProviderNames列表中登记了cursor-agentsrc/providers/index.ts其显示名Cursor Agent也单独维护在lazyProviderDisplayNamessrc/providers/index.ts。懒加载的动机与 Cursor provider 相同node:sqlite这类重依赖不应拖慢 codeburn 的冷启动。Provider 文档索引docs/providers/README.md将 Cursor Agent 归类为“Lazyloaded on first call”且存储格式为text / JSONL与以 SQLite 为主的其它 Cursor 链路形成对比。数据来源~/.cursor/projects/projectId/agent-transcripts/Cursor Agent 的 transcript 全部位于用户主目录下的 Cursor 数据目录中默认路径getCursorAgentBaseDir~/.cursor/projects/projectId/agent-transcripts/其中~/.cursor是基础目录源码中通过join(homedir(), .cursor)计算测试可通过createCursorAgentProvider(baseDirOverride)传入自定义基础目录以便隔离测试。基础目录下再拼接projects得到项目目录getProjectsDir。discoverSessions()src/providers/cursor-agent.ts的扫描逻辑是遍历projects/下的每个子目录子目录名即项目标识projectId如果子目录本身就是agent-transcripts无项目层的 workspace-less 布局直接进入该目录扫描否则检查projectId/agent-transcripts/是否存在存在才扫描把命中的每个 transcript 文件登记为一个SessionSource含path、project、provider: cursor-agent。probeRoots()src/providers/cursor-agent.ts向诊断工具暴露了两个探测根projects目录与下方提到的归因数据库文件。两种布局共存在每个项目的agent-transcripts/内Cursor 会同时维护两种 transcript 布局appendTranscriptSources布局形态说明Legacy*.txt扁平文件直接散落在目录下自由格式文本 transcriptComposer 2UUID 命名的子目录目录内是.jsonl文件结构化 JSONL每行一条消息扫描时会同时拾取两种布局目录项是.txt文件则直接登记目录项符合 UUID 形状正则UUID_LIKE则深入子目录收集.jsonl/.txt。若同一会话同一 stem 文件名同时存在.jsonl与.txt则优先选择.jsonlsrc/providers/cursor-agent.ts这一行为在测试 tests/providers/cursor-agent.test.tsprefers jsonl over same-session txt inside UUID transcript dirs中有明确验证。子代理Subagents也会被拾取委托给子代理delegated runs的 transcript 存放在父级会话目录下的subagents/子目录中扫描逻辑会递归进入该目录并把其中的.jsonl/.txt一并登记为独立的SessionSourcesrc/providers/cursor-agent.ts确保子代理的 token 消耗同样进入统计不会被遗漏。存储格式与解析策略Legacy 纯文本格式行级启发式解析Legacy*.txttranscript 是自由格式文本codeburn 用基于行的正则启发式解析parseTranscript。核心标记如下均为源码中定义的行首正则标记含义user:用户消息块的开始A:助手消息块的开始[Thinking]推理reasoning内容计入reasoningTokens[Tool call] name工具调用记为cursor:name[Tool result]工具返回结果整行跳过不计user_query…/user_query用户查询的正文标记解析状态机在none / user / assistant三种状态间切换遇到user:或A:时刷新上一个块助手块内的每一行按优先级判定——[Tool result]跳过、[Thinking]归入 reasoning、[Tool call]解析工具名、其余行拼接为正文输出src/providers/cursor-agent.ts。用户消息会经过extractUserQuery()src/providers/cursor-agent.ts处理提取所有user_query与/user_query之间的文本片段、合并去空白并截断到MAX_USER_TEXT_LENGTH 500字符src/providers/cursor-agent.ts。工具名经 parseToolName 规范化小写化、空白替换为连字符最终以cursor:tool-name形式记录。Composer 2 JSONL 格式结构化逐行解析Composer 2 的.jsonl每行是一条 JSON 消息parseJsonlTranscriptrole: user行把message.content中所有text块拼成当前用户消息同样经extractUserQuery与 500 字符截断role: assistant行遍历message.content块text块拼接为正文、tool_use块记录为cursor:name工具调用名字小写化并消费掉上一个用户消息形成一对 turn。内容块解析复用了 normalizeContentBlocks 统一处理多形态的 content 字段。可参考仓库中的真实 fixturetests/fixtures/cursor-agent/workspace-less/projects/agent-transcripts/1031d227-0c67-4e17-8954-0b6e2b3322f0/1031d227-0c67-4e17-8954-0b6e2b3322f0.jsonl其内容只有两行一条user_queryRun a quick smoke test/user_query用户消息和一条Smoke test passed.助手回复。未识别格式的降级行为如果解析后recognized为 false一行标记都没匹配上该 transcript 会被跳过并在 stderr 输出一条警告codeburn: skipped file: unrecognized cursor-agent transcript format同一文件只警告一次warnedUnrecognizedTranscripts集合去重src/providers/cursor-agent.ts、src/providers/cursor-agent.ts。对应测试 tests/providers/cursor-agent.test.ts 验证了“跳过”与“只警告一次”两个行为。会话元数据conversation_summaries与时间戳兜底每个会话的模型、标题、时间戳并非从 transcript 文件本身获得而是查询 Cursor 的 SQLite 归因库中conversation_summaries表。当前实现的库路径由 getAttributionDbPath 计算为~/.cursor/ai-tracking/ai-code-tracking.db文档早期版本记录为 Cursor 的state.vscdb无论库文件如何定位查询目标都是同一张conversation_summaries表SQL 定义见 src/providers/cursor-agent.ts按conversationId取conversationId / model / title / updatedAt。查询过程src/providers/cursor-agent.ts先查内存中的summariesByConversationId缓存每个 provider 实例共享避免重复打开数据库未命中且库文件存在时用openDatabase打开并执行查询成功则写入内存缓存查询失败或没有对应行则保持summary为空。时间戳兜底规则优先使用summary.updatedAt经 normalizeTimestamp 归一化为 ISO 字符串纯数字值若 1e12按秒处理并乘以 1000否则按毫秒若元数据缺失则回退到 transcript 文件的mtimesrc/providers/cursor-agent.ts。这意味着没有 conversation_summaries 行时时间精度取决于文件修改时间排障时需先确认是否存在对应元数据行见下文排障清单第 3 条。缓存策略Provider 层零缓存与 Cursor 常规聊天使用src/cursor-cache.ts写~/.cache/codeburn/cursor-results.vn.json不同Cursor Agent provider 在 Provider 层不设任何缓存。每次运行都直接重读 transcript 文件与归因库因此解析结果始终反映磁盘上的最新内容代价是 transcript 量大时重复扫描的开销。若观察到“改了 transcript 但结果没变”应从其它层如会话缓存/聚合缓存排查而非 Provider 本身。去重provider:conversationId:turnIndex每一条产出的用量记录ParsedProviderCall都携带去重键cursor-agent:conversationId:turnIndex拼接逻辑在 src/providers/cursor-agent.tsturnIndex为该 transcript 内 turn 的顺序下标从 0 开始配合跨 provider 共享的seenKeys集合实现幂等——同一会话的同一轮对话无论被扫描多少次例如多次运行、或与其它 provider 的扫描交叉都只会计入一次。对应测试 tests/providers/cursor-agent.test.ts 验证了对同一 transcript 两次收集会得到完全相同的sessionId与deduplicationKey。对话 ID 与项目 ID 的派生规则conversationId的确定方式toConversationId文件名去掉扩展名后的 stem长度恰好为 36 且匹配 UUID 形状UUID_LIKE正则→直接用文件名作为对话 ID其它文件名 → 对 transcript 的完整路径做 SHA1 哈希并截取前 16 位十六进制作为稳定 ID。这与文档早期记录“从父目录派生”略有差异当前实现保证了对路径的确定性。项目 ID 则经过 prettifyProjectId 美化纯数字且长度 ≥ 13形如 epoch 毫秒时间戳→ 转成 ISO 日期字符串并以cursor-agent:为前缀例如cursor-agent:2025-01-01T00:00:00.000Z否则去掉-Users-前缀后取按-切分的最后一个段作为项目名。例如 workspace-less 布局中projects/agent-transcripts/这个目录名会被美化展示为transcripts测试 tests/providers/cursor-agent.test.ts 断言sources[0].project transcripts。Token 与成本估算4 字符 1 tokenCursor Agent 的 transcript从不报告真实 token 数因此 codeburn 一律用字符数估算换算常量来自 src/token-estimate.tsexport const CHARS_PER_TOKEN 4 export function estimateTokensFromChars(chars: number): number { return Math.ceil(chars / CHARS_PER_TOKEN) }估算在 estimateTokens 中调用按 turn 分别计算inputTokens estimateTokens(userMessage.length)用户消息字符数 / 4 向上取整outputTokens estimateTokens(assistant.body.length)reasoningTokens estimateTokens(assistant.reasoning.length)Legacy 格式中[Thinking]行的内容或 JSONL 中未单独拆分时为空。测试 tests/providers/cursor-agent.test.ts 直接断言inputTokens/outputTokens/reasoningTokens与estimateTokensFromChars的结果一致例如[Thinking]private\nvisible\n会得到 reasoning2、output2。模型名解析与成本模型元数据中的模型名为空或为default时统一归为cursor-agent-autoresolveModel成本计算时cursor-agent-auto被映射到固定成本模型claude-sonnet-4-5CURSOR_AGENT_COST_MODELsrc/providers/cursor-agent.ts、costModel即“自动模型按 Sonnet 估算”具体价格由calculateCost来自 src/models.ts按输入/输出/reasoning token 计算输出 token 为outputTokens reasoningTokenssrc/providers/cursor-agent.ts展示名由modelDisplayName处理src/providers/cursor-agent.tscursor-agent-auto→Cursor (auto)已知模型如claude-4.5-opus-high-thinking→Opus 4.5 (Thinking)走 modelDisplayNames 映射未知模型回退到getShortModelName最终统一追加(est.)后缀以表明是估算值。这一系列映射在测试 tests/providers/cursor-agent.test.ts 中有完整断言。每次产出的ParsedProviderCall还包含tools本 turn 的工具调用列表、sessionId即 conversationId、userMessage、timestamp等字段供报表、项目归因与模型分解使用。已知怪癖Quirks汇总UUID 形状文件名的双重含义既是会话 ID 的来源也是 Composer 2 子目录的识别依据非 UUID 文件名回退为路径 SHA1 前 16 位保证稳定但不可读Token 永远是估算值Legacy 文本格式从不报告真实 token 数CHARS_PER_TOKEN 4是粗粒度近似字符密度高的语言如中文误差会偏大文本解析器脆弱Legacy 解析完全依赖user:/A:/[Thinking]等行首正则任何 Cursor 侧的格式微调都可能静默破坏识别表现为“skipped: unrecognized transcript format”JSONL 解析基于结构化字段修复成本更低——修 Composer 2 的 bug 比修 Legacy 文本的 bug 容易得多时间戳依赖元数据没有conversation_summaries行时会退化为文件 mtime批量拷贝/同步文件可能造成时间漂移项目 ID 美化有损prettifyProjectId只取连字符切分的最后一段目录名含-时可能丢失层级信息如-Users-前缀被剥离。排障指南修复 cursor-agent 解析 bug 的检查清单按文档 docs/providers/cursor-agent.md 与源码实现处理该 provider 的 bug 时依次检查先确认出问题 transcript 的格式*.txt还是*.jsonl再动手两种格式走完全不同的解析路径parseTranscript vs parseJsonlTranscript文本格式的 bug先把脱敏后的 transcript 原样拷入tests/fixtures/cursor-agent/让正则改动可以被回归测试保护现有 fixture 见 tests/fixtures/cursor-agent/workspace-less/如果症状是“项目归错 / 会话归错”优先检查conversation_summaries表是否存在对应conversationId的行查询 SQL 见 src/providers/cursor-agent.ts以及该 conversationId 是否来自 UUID 文件名还是路径 SHA1——归因错误大概率出在这两处token 为 0 或明显偏小检查MAX_USER_TEXT_LENGTH 500截断src/providers/cursor-agent.ts是否把长用户消息截没了以及估算公式Math.ceil(chars / 4)重复计数检查cursor-agent:conversationId:turnIndex去重键与跨 provider 的seenKeyssrc/providers/cursor-agent.ts。测试覆盖测试文件约 330 行Vitest覆盖了该 provider 的主要契约注册与展示名getAllProviders()中可找到cursor-agentdisplayName为Cursor Agent模型展示名映射与(est.)后缀发现扫描单项目、多项目、无agent-transcripts时跳过、workspace-less 布局顶层agent-transcripts目录、同一会话.jsonl优先于.txt解析用户/助手配对、[Thinking]推理 token 拆分、估算 token 数断言、非 UUID 文件名回退 SHA1 且幂等稳定降级行为未识别格式跳过并写 stderr 警告、同一文件只警告一次SQLite 元数据node:sqlite可用时才运行conversation_summaries行存在时采用其中的模型名与updatedAt时间戳。总结Cursor Agent 在 codeburn 中是一条纯本地、Provider 层零缓存的解析链路从~/.cursor/projects/projectId/agent-transcripts/同时读取 Legacy 纯文本与 Composer 2 JSONL 两种布局含subagents/子代理用启发式/结构化两种解析器提取 turn再结合 Cursor 归因库conversation_summaries补充模型与时间戳最终按cursor-agent:conversationId:turnIndex去重、按CHARS_PER_TOKEN 4估算 token、并以claude-sonnet-4-5作为自动模型的成本基准完成计费。它的核心设计取舍——不缓存、粗粒度估算、对文本格式的低容忍——决定了排查问题时需要优先确认 transcript 格式、归因元数据行与去重键三件事这正是上文排障清单想要固化下来的经验。【免费下载链接】codeburnFree, local tool to track AI coding token usage and cost across 37 tools and agents (Claude Code, Cursor, Codex, Gemini and more), by model, project, and task. npx codeburn项目地址: https://gitcode.com/gh_mirrors/co/codeburn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表