CodexBar macOS 小组件实现指南快照管线、六类 Widget 与可见性排障【免费下载链接】CodexBarShow usage stats for OpenAI Codex and Claude Code, without having to login.项目地址: https://gitcode.com/GitHub_Trending/co/CodexBarCodexBar 通过一条「主 App 写快照、Widget 扩展读快照」的 WidgetKit 数据管线把 OpenAI Codex、Claude、Cursor 等十余家 AI 编码服务的用量数据投射到 macOS 桌面上。本文基于仓库中 widgets 文档 与对应源码快照数据模型、Widget 扩展、快照持久化展开读完你可以理解WidgetSnapshotJSON 的字段与写盘/超时/熔断机制掌握六个 Widget 的配置意图与刷新策略并能按文档中的六步流程定位「小组件库找不到 CodexBar Widget」这一类签名/注册/守护进程问题。快照管线主 App 与 Widget 扩展之间只有一份 JSON整个管线只有一条数据通道WidgetSnapshotStore把一份紧凑 JSON 快照写到 app group 共享容器Widget 扩展在 timeline 中读取这份快照并渲染 usage / credits / history 状态。文件与路径的关键事实都可以从源码确认快照文件名固定为widget-snapshot.json见 AppGroupSupport.swift 中的widgetSnapshotFilename快照 URL 由 AppGroupSupport.snapshotURL 解析优先使用当前 teamID 对应的 app group 容器TEAMID.com.steipete.codexbardebug 构建追加.debug后缀见 currentGroupID拿不到容器时退回~/Library/Application Support/CodexBar/本地目录localFallbackDirectory——这一「fallback 路径」正是后文「小组件只显示预览数据」的常见根因teamID 优先从代码签名读取codeSignatureTeamID其次读 Info.plist 的CodexBarTeamID最后回落到默认值Y5PE65HELJ仓库还内置了从旧版固定 group IDgroup.com.steipete.codexbar/...debug迁移快照与共享 UserDefaults 的一次性逻辑migrateLegacyDataIfNeeded。WidgetSnapshot 数据模型WidgetSnapshot.swift 定义了扩展与主 App 共同依赖的 Codable 结构文档强调「保持数据形状与主 App 的WidgetSnapshot同步」即指此处。核心字段字段类型说明entries[ProviderEntry]每个已启用 provider 一条含updatedAt、primary/secondary/tertiary三档RateWindowSession/Weekly/Opus 级别、usageRows明细行、creditsRemaining、codeReviewRemainingPercent、tokenUsage、dailyUsage每日点、providerCost、quotaOwnerKeyenabledProviders[ProviderInstanceID]当前启用的 provider 列表旧快照缺失时解码器回落到entries中的 provider自定义 decodeusageBarsShowUsedBool进度条语义显示已用量还是剩余量旧快照默认falsegeneratedAtDate快照生成时间序列化使用 ISO8601encoder/decoderTokenUsageSummary承载 token 成本行session/30 天成本与 token 数、币种、展示标签、自己的updatedAt。这里有一个值得注意的「双时钟」设计token 成本行的刷新节奏慢于配额行当它落后配额数据超过 10 分钟staleLagThreshold 10 * 60见 staleLagThreshold时widget 会单独披露 token 行自己的时间戳而不是继承ProviderEntry.updatedAtisStale对无updatedAt的旧版快照按「新鲜」处理isStale。有界 I/O 与进程级熔断器快照读写全部走WidgetSnapshotStore的performBounded把文件 I/O 丢到独立线程并用信号量限时读 2 秒、写 10 秒defaultLoadTimeout / defaultSaveTimeout。一旦超时会触发进程级熔断器后续 load/save 全部直接返回 nil并记录一条 “Widget snapshot I/O timed out; disabling container access for this process” 的 warningtripBoundedIOCircuitBreaker。这个设计的目的是防止 app group 容器 I/O 卡死例如 macOS 26 上 TCC 门控导致的open()阻塞拖挂 UI 线程——测试代码中也有注释直接说明了这一背景测试文件。快照何时写入主刷新管线之后主 App 侧的写入口是 UsageStore.persistWidgetSnapshot(reason:)先基于内存中上一次排队的快照合并出新的WidgetSnapshot再经由一个串行 Task等待上一个持久化任务完成写盘生产环境先落盘、成功后才请求 WidgetKit reloadsaveWidgetSnapshot。从源码调用点看写入触发时机覆盖主刷新管线reason: refresh见 UsageStore.swifttoken 用量刷新token-usage、Codex creditscredits、OpenAI dashboarddashboard、Codex 账号状态变化codex-account-refresh/codex-account-invalidate、强制刷新富化、Codex 本地历史 catch-uptoken-usage-catch-up等十余处。文档同时点明两条时序约束较窄的单 provider 刷新路径可能等到下一次快照写入才会反映到 widget定时 provider 刷新会触发常规的 token/cost 刷新token/cost TTL 决定该次刷新是否合格timer 驱动的本地历史刷新有15 分钟下限低功耗模式 30 分钟手动关闭只停掉周期性刷新 timer不阻断全部扫描——启动刷新与挂起的 Codex catch-up 仍可能扫本地历史。这个下限只限制重复的本地历史工作与额外的 WidgetKit reload 请求不改变 provider 用量/状态的新鲜度或用户选择的刷新节奏。Claude 的两个特殊规则无配额数据的账号也能进快照makeWidgetEntry允许 Claude 在完全没有数值 session/weekly 配额snapshot nil时凭本地成本/token 历史storedTokenSnapshot单独进入 widget 快照判定逻辑。模型级 weekly 配额行是 opt-in 的Claude Usage widget 可以在常规 Session / Weekly / Opus 行之后展示每个已知模型的作用域 weekly 配额Claude 暴露 Fable 时包含 Fable。该行为由Preferences → Providers → Claude → Show model-specific weekly usage in widgets开关控制settings.claudeModelScopedWeeklyUsageVisible默认关闭不影响抓取也不影响 CodexBar 其他界面作用域行以claude-weekly-scoped-ID 前缀识别前缀常量。关掉开关后此前快照里保留下来的作用域行也会被移除——即便当前没有新的 Claude 配额数据保留逻辑preservedClaudeWidgetUsage在复用旧快照行时会重新施加该可见性过滤re-apply 过滤。配额归属校验Claude 快照带quotaOwnerKey账号/profile 切换后 key 不匹配的旧配额数据会被丢弃而不是错误沿用保留逻辑见 preservedClaudeWidgetUsage。测试隔离必须显式 opt-in 才落盘文档明确要求「测试必须通过内存 save override 或测试自有的快照 URL 来 opt-in 快照持久化」。源码侧的门是 shouldPersistWidgetSnapshot运行测试时没有 override/injected URL 就完全不落盘两种 opt-in 都不会触发 WidgetKit timeline reload。save/reload 辅助函数接受显式的「是否测试模式」与 reload 回调因此测试可以用临时文件 假回调验证「先保存、后 reload」的顺序而不触碰进程级测试隔离。这一组行为由 WidgetSnapshotTestIsolationTests 逐项钉住另有 UsageStoreWidgetSnapshotTests 覆盖各 provider 的快照行投影。扩展结构与六种 Widget扩展侧的事实如下Sources/CodexBarWidget 目录包含全部 timeline provider 与视图AppIntentTimelineProvider/TimelineProvider实现WidgetExtension/CodexBarWidgetExtension.xcodeproj 把上述源码构建成打包进主 App 的 macOS WidgetKit app extension从 project.yml 可以看到 target 是app-extension类型、部署目标macOS 14.0、源码直接引用../Sources/CodexBarWidget、依赖CodexBarCore产品bundle ID 由构建变量CODEXBAR_WIDGET_BUNDLE_ID注入Info.plist 中NSExtensionPointIdentifier固定为com.apple.widgetkit-extensionUsage / Switcher / History / Metric 四类 widget 使用 WidgetKit 的 content margins视图自身不再叠加第二层 outer inset。CodexBarWidgetBundle注册入口共注册六种 widget显示名Widget structkind / 配置意图支持尺寸CodexBar SwitcherCodexBarSwitcherWidgetStaticConfiguration无配置small / medium / largeCodexBar UsageCodexBarUsageWidgetAppIntentConfigurationProviderSelectionIntentsmall / medium / largeCodexBar HistoryCodexBarHistoryWidgetAppIntentConfigurationProviderSelectionIntentmedium / largeCodexBar MetricCodexBarCompactWidgetAppIntentConfigurationCompactMetricSelectionIntentsmall onlyCodexBar Burn DownCodexBarBurnDownWidgetAppIntentConfigurationBurnDownSelectionIntentmedium onlyCodexBar Burn Down (Combined)CodexBarCombinedBurnDownWidgetAppIntentConfigurationBurnProviderSelectionIntentmedium onlyMetric 的指标枚举CompactMetric提供三项Credits left / Today cost / 30d costCompactMetric。Burn Down 两个 widget 声明了可移除容器背景containerBackgroundRemovable。Switcher 的共享选择 vs Usage 的独立配置所有 Switcher widget 共享同一个记忆中的 provider 选择选择存储在 app group 共享 UserDefaults 的widgetSelectedProvider键中WidgetSelectionStore点击切换会执行 SwitchWidgetProviderIntent——写共享默认值并WidgetCenter.shared.reloadAllTimelines()因此切换一个 Switcher 会联动全部 Switcher。若想同时并排观察 Claude 和 Codex文档给出的做法是添加两个CodexBar Usagewidget 并分别配置各自的ProviderUsage/History 走ProviderSelectionIntent读的是自己配置的 provider不受共享 Switcher 选择影响。Switcher 可选列表也来自快照的enabledProviders并过滤掉没有ProviderChoice的 providersupportedProviders。无快照时的回退timeline 的timeline(for:)里WidgetSnapshotStore.load()返回 nil 时Usage/Switcher/History 回退到WidgetPreviewData.emptySnapshot()空数据placeholder回退到WidgetPreviewData.snapshot()CodexBarTimelineProvider 等。这也解释了「小组件出现了但一直显示预览数据」的症状扩展读到了文件但读到的是空/陈旧快照或主 App 根本写在了 fallback 路径上。Provider Picker 支持范围可配置的 provider widgetUsage / History / Metric / Burn Down 的ProviderChoice目前覆盖17 个选项ProviderChoice 枚举与 DisplayRepresentationrawValue配置界面显示名codexCodexclaudeClaudegeminiGeminialibabaAlibabaalibabatokenplanAlibaba Token PlanqwencloudQwen CloudantigravityAntigravitycursorCursorzaiz.ai / GLMcopilotCopilotdevinDevinminimaxMiniMaxkiloKiloopencodeOpenCodeopencodegoOpenCode GomistralMistralkimiKimi Code文档正文列出的是早期批次Codex、Claude、Cursor、Gemini、Alibaba、Antigravity、z.ai、Copilot、MiniMax、Kilo、OpenCode、OpenCode Go上表是源码中当前完整的可选项。两条实现约束值得注意caseDisplayRepresentations必须是字面量、穷尽的字典因为 AppIntents 会静态抽取这份显示名元数据源码注释新增 provider 时必须同步补齐测试会将其与 descriptor registry 钉在一起没有ProviderChoicecase 的 provider 仍然可以出现在 app 快照里只是暂不可从 widget 配置 UI 选择反向构造ProviderChoice(provider:)还要求对应 descriptor 的widgetSelectable元数据为真init?。Burn-down 类 widget 目前支持 Codex 与 Claude它们使用专属配置意图BurnDownSelectionIntent/BurnProviderSelectionIntent不会改动已有 Usage 与 History widget 的配置意图接线见 CodexBarWidgetBundle.swift。可见性排障macOS 14当 widget 在小组件库里完全看不到时问题几乎总是注册、签名或守护进程缓存问题而不是 SwiftUI 代码。以下六步来自 widgets 文档 原文命令可直接复制执行。1) 确认扩展 bundle 在 macOS 期望的位置APP/Applications/CodexBar.app WAPPEX$APP/Contents/PlugIns/CodexBarWidget.appex WIDGET_IDcom.steipete.codexbar.widget # debug builds use com.steipete.codexbar.debug.widget ls -la $WAPPEX $WAPPEX/Contents $WAPPEX/Contents/MacOS2) PlugInKit 注册状态pkdpluginkit -m -p com.apple.widgetkit-extension -v | grep -i codexbar || true pluginkit -m -p com.apple.widgetkit-extension -i $WIDGET_ID -vv说明输出中表示「被选中使用」、-表示「被忽略」PlugInKit election。若缺失或被忽略强制添加并重新选举pluginkit -a $WAPPEX pluginkit -e use -p com.apple.widgetkit-extension -i $WIDGET_ID检查重复注册旧安装或版本优先级问题pluginkit -m -D -p com.apple.widgetkit-extension -i $WIDGET_ID -vv若出现多个路径删除旧版本安装并提升CFBundleVersion。3) 代码签名与 Gatekeeper 评估widget 由系统守护进程加载任何签名失败都会隐藏 widgetcodesign --verify --deep --strict --verbose4 /Applications/CodexBar.app codesign --verify --strict --verbose4 $WAPPEX codesign --verify --strict --verbose4 $WAPPEX/Contents/MacOS/CodexBarWidget spctl --assess --type execute --verbose4 /Applications/CodexBar.app4) 重启正确的守护进程只重启 NotificationCenter 不够killall -9 pkd || true sudo killall -9 chronod || true killall Dock NotificationCenter || true5) 打开小组件库时盯日志log stream --style compact --predicate (process pkd OR process chronod OR subsystem CONTAINS PlugInKit OR subsystem CONTAINS WidgetKit)6) 打包一致性检查release 构建的 widget bundle ID 应为com.steipete.codexbar.widgetdebug 构建为com.steipete.codexbar.debug.widget与 project.yml 中$(CODEXBAR_WIDGET_BUNDLE_ID)的注入值对应NSExtensionPointIdentifier必须是com.apple.widgetkit-extensionInfo.plist 已固定该值bundle 目录名必须匹配CodexBarWidget.appex。可选很少有效但风险低重新播种 LaunchServices/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -seed出现后仍显示预览数据stale data 排查如果 widget 出现了但永远显示预览/空数据典型原因是写读两侧解析到了不同容器主 App 把快照写进了 fallback 路径Application Support/CodexBar而 widget 读的是 app group 容器。验证方式是确认 app 与 widget 两端解析到同一个 app group 容器——从源码看两端共用 AppGroupSupport.currentContainerURL 与snapshotURL逻辑teamID / debug 后缀不一致例如主 App 是 release、扩展误用了 debug group就会造成这种错位。相关文档UI 指南、打包指南。【免费下载链接】CodexBarShow usage stats for OpenAI Codex and Claude Code, without having to login.项目地址: https://gitcode.com/GitHub_Trending/co/CodexBar创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
