CodexBar Codex Workspaces 本地索引:项目级 Codex 用量归因的架构与实现解析
【免费下载链接】CodexBarShow usage stats for OpenAI Codex and Claude Code, without having to login.项目地址: https://gitcode.com/GitHub_Trending/co/CodexBar
Codex Workspaces 是 CodexBar 中一套完全本地运行的 Codex 用量索引能力:它把已有的本地 Codex 成本扫描(cost scan)结果按项目(workspace)、会话(session)、模型(model)和天(day)四个维度重新归因,形成可持久化、可查询的项目级用量快照。它不引入任何远程 API、不做 provider 登录鉴权、不提供计费接口,也没有对外承诺公开的 CLI JSON 契约。本文基于仓库中的设计文档 docs/codex-workspaces.md,并结合 Sources/CodexBarCore/CodexLocalProjectUsageIndexer.swift、Sources/CodexBarCore/CodexWorkspaceUsageSidecar.swift、Sources/CodexBarCore/CostUsageFetcher.swift 等源码,从数据流、持久化契约、目录完整性、成本语义到隐私边界,逐层拆解这套索引的工程实现。读完本文,你将理解 CodexBar 如何在零登录的前提下把散落在本地 JSONL rollout 与 Codex 线程目录中的使用记录,聚合成一套可回滚、可失效、可隐私脱敏的 SQLite 项目级用量索引,并了解如何通过CODEXBAR_ENABLE_WORKSPACES_MENU在 Debug 构建中预览该功能的导航雏形。
Workspaces 索引的定位与边界
从 docs/codex-workspaces.md 的定义看,Codex Workspaces 的定位非常明确:它是“对现有本地 Codex 成本扫描的归因增强层(attribution layer)”,而不是一个新的数据采集通道。其能力边界包括:
- 归因维度:项目(projects)、会话(sessions)、模型(models)、天(days);
- 本地优先:索引、扫描、目录读取、缓存、sidecar、分析、CSV 序列化全部在本地完成,不向任何远端上传 rollout 内容或目录记录;
- 明确不做的事:不新增远程 API、不引入 provider 认证流程、不做计费界面、不提供公开的 CLI JSON 契约。
这一边界在源码中得到印证:整个 Workspaces 功能位于 Sources/CodexBarCore 下的CodexLocalProjectUsage*、CodexWorkspaceUsageSidecar.swift、CodexThreadCatalogReader.swift、CodexLocalProjectRootResolver.swift等纯本地组件中,呈现层则通过 CostUsageFetcher.swift 暴露的四个库级 API 与上层交互。
内部导航脚手架:Debug 专属的 Workspaces 菜单
在设计文档中,Workspaces 的 UI 目前只有“内部导航脚手架”(internal navigation scaffold):Debug 构建可以通过环境变量CODEXBAR_ENABLE_WORKSPACES_MENU=1暴露一个 Codex 专属的Workspaces菜单动作,打开一个可复用的原生窗口,窗口内容目前只是No data yet占位。该导航切片不会加载索引、启动扫描、读取账户数据或持久化窗口几何信息;Release 构建即使设置了该环境变量也始终禁用此开关。
源码实现与文档完全一致:
- 门控逻辑在 CodexWorkspacesMenuAvailability.swift:
environmentKey = "CODEXBAR_ENABLE_WORKSPACES_MENU",且#if DEBUG下才检查环境变量是否为"1",#else分支直接返回false; - 窗口呈现由 CodexWorkspacesNavigation.swift 的
CodexWorkspacesPresenter与CodexWorkspacesWindowController完成:默认尺寸 1380×780、最小内容尺寸 980×640、禁用 tabbing 与窗口恢复(isRestorable = false),根视图是一个ContentUnavailableView("No data yet", systemImage: "folder"); - 菜单动作通过 StatusItemController 的
openCodexWorkspaces(_:)触发,并在 MenuContent.swift 的.openCodexWorkspaces分支中被调用。
也就是说,数据驱动的检查器(data-backed inspector)与明确的用户 opt-in 仍是后续的独立工作项,当前阶段只是验证了导航路径的可用性。
数据流:扫描缓存 × 线程目录的归一化管道
文档给出的 Workspaces 数据流共五步,核心思想是以CostUsageScanner为唯一权威(authoritative),配合只读的 Codex 线程目录(thread catalog)做归因补充:
- 将本地 rollout JSONL 扫描进 Codex v11 成本缓存(cost cache);
- 只读地读取目录元数据,绝不修改Codex 的 catalog;
- 规范化 workspace 归因,并将范围限定到选定的 Codex home;
- 在单个 SQLite 事务中发布完整的源状态(source state)与派生快照(derived snapshot);
- 向展示层消费者暴露项目、会话、模型、每日、源状态、进度与 CSV 库模型。
代码层面的对应关系如下:
- 第 1 步:
CostUsageScanner.loadDailyReportCancellable(provider: .codex, ...)在 CodexLocalProjectUsageIndexer.swift 中被调用,随后读取CostUsageStoreAccess.read(...)得到CostUsageCache; - 第 2 步:
CodexThreadCatalogReader.loadResult(options:)以只读方式(sqlite3_open_v2(..., SQLITE_OPEN_READONLY, nil)加PRAGMA query_only = ON)读取目录,见 CodexThreadCatalogReader.swift; - 第 3 步:
CodexLocalProjectRootResolver.projectIdentity(for: cwd)将 cwd 解析为稳定的项目身份(id / displayName / path); - 第 4 步:
CodexWorkspaceUsageSidecar.synchronizeSources(...)与synchronize(...)分别在BEGIN IMMEDIATE ... COMMIT(失败则ROLLBACK)事务中写入,见 CodexWorkspaceUsageSidecar.swift; - 第 5 步:
buildSnapshotFromCostCache(...)产出CodexLocalProjectUsageSnapshot,其字段包括total、projects、sessions、modelBreakdowns、daily、sourceStatus、modelsAnalytics,见 CodexLocalProjectUsageModels.swift。
值得注意的工程细节:sidecar 的导入只吸收扫描器派生的增量(deltas),随后从 sidecar 的规范化行重新聚合,原始 cost cache 仍是游标(cursor)与累计 token 的权威,但不再是聚合来源——这一设计在 CodexLocalProjectUsageIndexer.swift 的注释中写得很清楚,保证了索引与扫描器的职责分离。
官方支持的内部呈现边界
文档明确了五个受支持的内部呈现接口,全部集中在 CostUsageFetcher.swift:
| API | 语义 |
|---|---|
loadCachedCodexLocalProjectUsageSnapshot | 纯读缓存快照,不触发扫描(内部走CodexLocalProjectUsageIndexer.cachedSnapshot) |
loadCodexLocalProjectUsageSnapshot | 可能触发扫描重建(forceRefresh时把refreshMinIntervalSeconds置 0),返回新快照并发布 |
clearCachedCodexLocalProjectUsageSnapshot | 清除指定 scope 的缓存快照(对应 sidecar 的clear(),同时删除-wal与-shm文件) |
CodexLocalProjectUsageSnapshot | 快照数据模型(Sendable & Codable & Equatable) |
CodexLocalProjectUsageIndexProgress | 进度模型:scanningLogs/indexingProjects/saving三阶段,附带 processed/total/indexed/skipped 文件计数 |
其中CodexLocalProjectUsageIndexProgress.Phase的完整定义在 CodexLocalProjectUsageModels.swift。索引器在每处理 1、25 的倍数或最后一个文件时上报进度(reportProgressIfNeeded),并在扫描与聚合阶段埋点CodexModelsTelemetry(IndexRefresh/SnapshotAggregation两个 signpost)。
持久化契约:v11 成本缓存与 v1 sidecar
两个文件路径
文档给出的持久化路径(macOS 下)如下:
# Codex 成本缓存(CostUsageScanner 的游标与累计 token 权威) ~/Library/Caches/CodexBar/cost-usage/codex-v11.json # Workspaces sidecar(项目级归因索引) ~/Library/Caches/CodexBar/local-usage/codex-workspaces-v1.sqlitesidecar 的路径拼接逻辑在 CodexWorkspaceUsageSidecar.swift:cacheRoot/local-usage/codex-workspaces-v1.sqlite,其中cacheRoot默认是用户 Caches 目录,也可通过CostUsageScanner.Options.cacheRoot注入。
版本契约:schema v5 + payload v3
文档明确:sidecar 当前使用SQLite schema 版本 5和快照 payload 格式版本 3,这是首个发布的 Workspaces schema。数据库如果带有任何其他PRAGMA user_version,会被判定为不兼容并直接拒绝、绝不修改。源码中schemaVersion = 5、snapshotPayloadFormatVersion = 3(CodexWorkspaceUsageSidecar.swift),ensureSchema只在user_version == 0(全新库)或== 5时继续,否则抛出SidecarError.incompatibleSchema(CodexWorkspaceUsageSidecar.swift)。
schema 由六张业务表组成,从建表语句可见其设计(CodexWorkspaceUsageSidecar.swift):
catalog_threads:目录线程元数据(id、rollout_path、cwd、title、preview、model、reasoning_effort、时间戳、archived、source_fingerprint、last_seen_generation);usage_rollouts:rollout 文件级归因(session_id、cwd、title、project_path、canonical_project_path、forked_from_id、源文件 mtime/size/parsed_bytes、producer_key、pricing_key、content_fingerprint、event_detail_complete、is_present);usage_daily:(rollout_path, day, model)主键的每日用量(input/cached/output tokens、cost_nanos,以及被保留但“读写双方故意忽略”的 standard/priority 列);usage_events:(rollout_path, event_index)主键的事件级明细(timestamp、canonical/raw model、turn_id、tokens、known_cost_nanos、unpriced_tokens、pricing_model/mode、reasoning_tokens);snapshot_payloads:以(scope_signature, history_days)为主键的 BLOB 快照(payload_format_version、is_complete、updated_at_ms);index_state:scope_signature主键的索引状态(roots_fingerprint、catalog_fingerprint、cache_producer_key、pricing_key、cache_fingerprint、last_success_ms)。
同时为usage_daily、usage_events、usage_rollouts、catalog_threads建立了若干索引(按 rollout/day、时间戳、模型时间戳、turn、session 等)。库以 WAL 模式打开并设置PRAGMA synchronous = NORMAL、busy_timeout = 250ms。
v10 → v11 缓存升级语义
文档特别强调:codex-v10.json不会原地迁移,也不被接受为 v11 的增量游标。原因是“parser 与归因语义发生了变化”,因此升级后的首次扫描会执行一次性重建,并写出独立的 v11 产物:
- v10 文件保持原样、可恢复(untouched and recoverable);
- v11 文件仅在扫描成功后被原子替换;后续 v11 刷新可正常使用增量游标。
这一“写新文件、保留旧文件”的策略,配合“成功后才替换”的原子发布语义,是避免升级过程中数据丢失的关键。
sidecar 回滚
文档指出发布是事务性的:一次失败的同步或快照写入会整体回滚,保留上一个完整快照可用。这正是上面BEGIN IMMEDIATE / COMMIT / ROLLBACK结构的价值——读取侧loadLatestSnapshot也只选择is_complete = 1且与当前 scope、history_days 完全匹配的行,因此读到的一定是完整发布过的快照。
目录完整性与 last-good 数据
Codex 的线程目录是只读外部状态,可能出现缺失、锁定、损坏或不兼容等情况。Workspaces 索引将目录访问区分为complete、missing、locked、corrupt、incompatible五种状态,对应 CodexThreadCatalogReader.swift 中的CodexThreadCatalogCompleteness.complete与unavailable(CodexThreadCatalogFailure)(failure 枚举含 missing / locked / corrupt / incompatible / unreadable)。打开失败时按 SQLite 错误码映射:SQLITE_BUSY/SQLITE_LOCKED→ locked,SQLITE_CORRUPT/SQLITE_NOTADB→ corrupt,其余 → unreadable。
目录完整性影响侧车更新的关键分支(见 CodexWorkspaceUsageSidecar.swift):
- 完整读取(complete)会替换该 Codex home scope 的目录元数据,并在同步后执行
pruneCatalog,清理本代(generation)中已不存在的目录线程; - 同一 scope 稍后的不完整读取(incomplete)会报告 partial 或 stale 的源状态,但保留 last-good 的目录归因与用量快照——
pruneCatalog只在catalogIsComplete时执行,避免用不完整目录误删有效元数据; - 稀疏 rollout 更新(sparse updates)不会抹掉已保留的标题、workspace 路径等目录元数据——
usage_rollouts的 upsert 对 session_id/cwd/title 等字段使用COALESCE(excluded.xxx, usage_rollouts.xxx),即新值优先、缺省保留旧值。
作用域标识与失效指纹
文档强调:scope 标识符和失效指纹都是哈希,原始的 Codex home 路径不会被持久化为 scope 标识。源码中:
stableScopeSignature(options:)由CodexLocalDataScope.resolve(...).identifier与timeZone组成(CodexLocalProjectUsageIndexer.swift);rootsFingerprint是codexRootsFingerprint(各 sessions 根目录的哈希字典)的排序字典;cacheFingerprint是对所有缓存文件逐项做 FNV 式滚动哈希得到的十六进制串(CodexWorkspaceUsageSidecar.swift)。
失效(invalidation)是按受影响部分精准触发的:changed/deleted rollout 文件、parser 或定价变化、history-window 变化、目录变化,都只使对应受影响缓存状态失效,而不是整体重建。加载快照时会逐一校验index_state中的 roots_fingerprint、cache_producer_key、pricing_key、cache_fingerprint 与 catalog_fingerprint,任一不匹配即视为缓存未命中。
成本与展示语义
已知成本与未知成本分离
Workspaces 索引在费用处理上有一条硬性规则:未知定价永远不会变成合成的零成本声明(unknown pricing never becomes a synthetic zero-cost claim)。从readTimePricingTotals(CodexLocalProjectUsageIndexer.swift)可见,逐事件用codexResolvedCostNanos解析定价:
- 能解析出
costNanos的事件计入coveredTokens与hasAuthoritativeCost; - 解析不到定价的事件 token 归入
unknownCostTokens。
因此最终快照中每个项目/会话同时携带knownUSD与unknownTokens(见CodexLocalCostEstimate),展示层可以如实呈现“已知费用 + 未定价 token 量”两个独立指标,而不是把未知部分当作 0 元。模型分析(models analytics)、奇偶校验(parity checks)、性能遥测与 CSV 序列化全部基于同一份持久化快照运行,保证口径一致。
展示投影是瞬态的
文档列出的三条“展示不落盘”约束,在代码中体现为快照投影(projection)发生在读取/返回阶段,而非写入阶段:
- 隐藏估算成本不会改写 sidecar——估算只在
usd(fromNanos:)之类读时转换中产生; - 包含/排除缓存输入不会触发重扫或改写 sidecar;
- 隐藏个人信息(
hidePersonalInfo)会在快照到达展示代码之前移除持久化的 workspace 路径、工作目录、workspace 名称与会话标题,同样不改写本地 sidecar。
此外,排名、合计、图表与明细必须使用同一份投影——CodexLocalProjectUsageIndexer.projecting在源状态变化时重建快照但保留其余字段,sortProjects/sortSessions在总 token、成本、会话数、最近活动等维度上给出确定性的排序规则(CodexLocalProjectUsageIndexer.swift),避免展示层各组件因投影不一致而出现排名打架。
隐私边界:纯本地处理与运行时脱敏
这是设计文档的收尾部分,也是 Workspaces 索引的底线约束:
- 数据不离机:scanner、catalog reader、cache、sidecar、analytics 与 CSV serializer 全部在本地运行,不上传 rollout 内容或目录记录;
- 本地索引中确实可能包含敏感字段:本地 workspace 路径、会话元数据、token 总量、派生成本估算等;
- 发布前必须脱敏:从共享日志与截图中移除路径、标题、会话 ID、身份信息、token、密钥、网络地址与数据库行内容。
这一点与hidePersonalInfo投影、以及PersonalInfoRedactor等仓库内既有隐私组件(Sources/CodexBar/PersonalInfoRedactor.swift)的设计意图一致:本地可完整存储与计算,但任何“对外呈现”都先经过脱敏投影。
总结:一套可增量、可回滚、可脱敏的本地用量索引
回顾 docs/codex-workspaces.md 与源码实现,CodexBar 的 Codex Workspaces 本地索引在工程上呈现出一组非常清晰的取舍:
- 单一权威:
CostUsageScanner继续负责 JSONL 解析、累计 token 增量、fork/subagent 记账、定价与增量游标,Workspaces 只在其上做归因与聚合; - 只读外部依赖:Codex 线程目录以
SQLITE_OPEN_READONLY + query_only打开,索引绝不修改 Codex 自己的状态库; - 事务化发布:sidecar 通过 WAL +
BEGIN IMMEDIATE事务同时写入源状态与派生快照,失败即回滚,读取永远拿到上一个完整快照; - 精准失效:scope 与指纹全部哈希化,parser/定价/目录/文件变化只使受影响缓存失效,保留 last-good 数据;
- 诚实的成本口径:已知成本与未知成本分离存储与呈现,绝不把未定价 token 伪装成 0 元;
- 隐私默认安全:数据全程本地,任何对外呈现(日志、截图、脱敏投影)都在发布前移除个人可识别信息。
对于希望继续深入研究的读者,可以沿以下路径阅读实现:数据入口 CostUsageFetcher.swift → 索引核心 CodexLocalProjectUsageIndexer.swift → 持久化 CodexWorkspaceUsageSidecar.swift → 目录读取 CodexThreadCatalogReader.swift,再到 UI 导航 CodexWorkspacesNavigation.swift 与门控 CodexWorkspacesMenuAvailability.swift。当前阶段的导航切片只服务于内部验证,数据驱动的检查器与用户 opt-in 是明确的后续工作项——这也提醒使用者在自己的构建中不要依赖尚未开放的功能路径。
【免费下载链接】CodexBarShow usage stats for OpenAI Codex and Claude Code, without having to login.项目地址: https://gitcode.com/GitHub_Trending/co/CodexBar
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考