首页
/ opencode STATS.md 深度解析:GitHub / npm 双渠道下载统计的自动化流水线

opencode STATS.md 深度解析:GitHub / npm 双渠道下载统计的自动化流水线

2026-09-06 09:46:24作者:侯霆垣

STATS.md 是 opencode 仓库根目录下的下载量统计档案,按天记录 GitHub Release 与 npm 两个分发渠道的累计下载量及日增量,最新一行(2026-01-29)总下载量已达 10,190,453。本篇围绕这张统计表展开:先讲清表格每一列的口径与读法,再结合 统计脚本定时工作流 还原"数据从哪里来、差值怎么算、文件怎么写回仓库"的完整链路,最后基于表内真实数据解读增长拐点、渠道占比变化以及缺失/重复行背后的原因。

STATS.md 的表格结构与读数口径

统计表是一张四列的 Markdown 表格,自 2025-06-29 起每天追加一行:

含义 口径说明
Date 统计日期 脚本执行当天,取 new Date().toISOString().split("T")[0] 的 UTC 日期
GitHub Downloads GitHub Release 资产累计下载量 所有历史 release 全部 asset 的 download_count 之和
npm Downloads npm 包 opencode-ai 累计下载量 2020-01-01 至"当前年份 + 5 年"区间的逐日下载量求和
Total 两渠道之和 GitHub + npm

每个数字后面括号里的增量(如 58,209 (+0)10,190,453 (+386,234))是相对上一行的日环比变化。首日增量为 +0,因为脚本在文件不存在或解析不到上一行时把基线记为 0。

两个数字都不是"当日新增",而是累计值:GitHub 侧遍历全部历史 release 求和,npm 侧查询的也是全量历史区间,因此两列天然单调递增,日增量只反映"当天采集时点上累计值比上一次采集时点多了多少"。这一点在解读异常行(下文)时很关键。

数据采集流水线:从 API 到一行 Markdown

STATS.md 不是人工维护的,由一条 GitHub Actions 工作流每天自动驱动。整条链路涉及三个文件:工作流定义采集脚本、以及被写入的 STATS.md

定时触发与提交机制

工作流定义在 .github/workflows/stats.yml

  • 触发方式schedule 使用 cron 表达式 0 12 * * *,即每天 12:00 UTC 运行一次;同时开放 workflow_dispatch 允许手动触发。
  • 仓库守卫if: github.repository == 'anomalyco/opencode',只在主仓库生效,fork 中不会执行。
  • 并发控制concurrency: ${{ github.workflow }}-${{ github.ref }},同一分支上上一次运行未完成时新运行会被去重,避免同一天产生多笔重复统计(下文会出现的重复日期行就与此类机制的边界情况有关)。
  • 权限:仅授予 contents: write,用于把新行提交回仓库。
  • 运行环境blacksmith-4vcpu-ubuntu-2404 自建 runner。

核心步骤只有两步:执行 bun script/stats.ts,然后以 GitHub Action 身份提交:

git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add STATS.md
git diff --staged --quiet || git commit -m "ignore: update download stats $(date -I)"
git push

注意提交信息前缀是 ignore:,这类消息通常会被 changelog 工具自动排除,避免"每日更新统计"污染发布日志。另外 POSTHOG_KEY 以 secret 形式注入,用于脚本末尾的指标上报。

数据源一:GitHub Releases API

脚本fetchReleases() 负责拉取 GitHub 侧数据:

  • 分页请求 https://api.github.com/repos/anomalyco/opencode/releases?page=N&per_page=100,每页 100 条;
  • 每页之间 await new Promise((resolve) => setTimeout(resolve, 1000)),人为限速 1 秒,规避 API 频率限制;
  • 直到某一页返回空或不足 100 条为止,把所有 release 汇总返回。

随后 calculate() 遍历每个 release 的全部 assets,累加 asset.download_count 得到该 release 的下载量,再对全部 release 求和,得到 GitHub 侧累计值。这里统计的是 Release 资产下载(各平台二进制安装包等),不含 npm 渠道,也不含源码压缩包以外的其他资产来源。

数据源二:npm Downloads API

fetchNpmDownloads("opencode-ai") 调用 npm 官方统计接口:

https://api.npmjs.org/downloads/range/2020-01-01:{当前年份 + 5 年}-12-31/opencode-ai

脚本注释明确写了这样设计的原因:"Use a range from 2020 to current year + 5 years to ensure it works forever"——把结束日期设为当前年份加 5 年,保证日期范围永远是"过去到未来",脚本无需随年份维护。返回值是逐日 downloads 数组,脚本用 reduce 求和得到 opencode-ai 包的全量累计下载量。失败时仅打印 warning 并返回 0,不会中断整个流程。

写回逻辑:解析上一行、算差值、追加

save(githubTotal, npmDownloads) 是 STATS.md 表格逐行增长的直接来源,逻辑值得逐段看(见 script/stats.ts#L125-L189):

  1. 定位上一行:读取现有 STATS.md从文件末尾向前逐行扫描,用正则匹配第一个数据行:

    /\|\s*[\d-]+\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|\s*([\d,]+)\s*(?:\([^)]*\))?\s*\|/
    

    正则把千分位逗号、可选的括号增量都做了容忍处理,取出 GitHub / npm / Total 三个上一期累计值;文件不存在或没有匹配时三者回退为 0。

  2. 计算增量githubChange = githubTotal - previousGithub,npm 与 total 同理。

  3. 格式化:累计值用 toLocaleString() 加千分位;增量为正时写成 (+1,338),为负时写成 (-x),为 0 时写成 (+0)

  4. 兜底建表:若文件中找不到 # Download Stats 标题,会先补上表头,再追加新行,最后跑 bunx prettier --write 统一表格对齐——这正是 STATS.md 里列宽整齐的原因。

  5. 上报:脚本末尾向 PostHog 发送两条 download 事件(source: "github"source: "npm"distinct_id 固定为 "download"),用于在分析平台侧观察下载趋势。没有 POSTHOG_KEY 时仅打印 warning 并跳过。

这套"末尾扫描 + 正则解析"的设计是刻意的脆弱-鲁权:它不依赖数据库或临时文件,只依赖 STATS.md 自身作为状态存储,即使 workflow 重跑、环境重建也能延续序列。

数据解读:增长曲线、渠道拐点与异常行

基于 STATS.md 现有 200 余行数据(2025-06-29 至 2026-01-29),可以读出几个有依据的事实性结论。

关键里程碑

日期 Total 事件
2025-10-19 1,005,287 总下载量首次破 100 万
2025-12-10 2,017,599 首次破 200 万
2026-01-14 5,214,290 首次破 500 万
2026-01-29 10,190,453 首次破 1000 万(表内最后一行)

从 100 万到 200 万用了约 52 天,而从 500 万到 1000 万只用了 15 天(2026-01-14 到 2026-01-29),增长显著加速。单渠道最大单日增量出现在 2026-01-16:GitHub 单日 +552,622,Total 单日 +661,678,是表内最高峰。

渠道结构:GitHub 在 2026-01 反超 npm

表内两列的相对位置随时间发生了一次明确翻转:

  • 2026-01-05 及以前,npm 列始终大于 GitHub 列(如 2026-01-05:GitHub 1,738,171 < npm 1,353,043 不成立除外,实际 2026-01-05 为 GitHub 1,738,171 / npm 1,353,043,GitHub 已略高)。以 2026-01-06 为表内首个明确反超日:GitHub 1,960,988(单日 +222,817)对比 npm 1,377,377(+24,334),此后再未回落。
  • 到 2026-01-29,GitHub 渠道累计 7,815,471,占比约 77%;npm 渠道 2,374,982,占比约 23%。

从渠道含义看,这反映安装方式向直接下载 Release 二进制(各平台原生安装包)倾斜;具体原因(发布节奏、安装引导变化等)仓库内没有直接记录,此处不下结论。

异常行:缺失日期、重复行与 +0

统计表本身就是"流水线运行日志",几处异常恰好印证了上文机制:

  • 缺失 2025-07-07:表内从 07-06 直接跳到 07-08,说明当天 12:00 UTC 的运行未产生提交(当天未执行或被并发控制丢弃)。
  • 缺失 2025-10-13:10-12 到 10-14 之间无行,且 10-14 的增量是 +13,005/+10,541,接近两个日期的累计量,符合"补记"特征。
  • 2025-08-27 / 08-28 npm 列 +0:GitHub 列照常增长而 npm 列纹丝不动,可推断这两天 npm 统计接口返回的累计值与前一天相同(npm 官方计数存在延迟或波动),并非脚本故障——脚本对接口失败会返回 0 并打 warning,而 0 与前一值相减不会出现 +0 且总量仍在增长的情况。
  • 2025-10-30 出现两行:同日两行分别为 (613,746 / 542,064) 与 (617,846 / 555,026),是当天两次执行(例如手动 workflow_dispatch 叠加定时触发,或重试)各追加了一行;由于脚本总是"追加"而非"覆盖当天行",重复执行会留下双行。这也解释了为什么第二行的增量是相对第一行算的。

这些异常不影响累计值的单调可信性,但提醒读者:STATS.md 的每一行对应"某次采集时点",而非严格的自然日快照。

延伸:与 packages/stats 统计站点的区别

仓库中另有一个同名易混的模块 packages/stats,它是独立的统计站点(SolidStart 前端 + Effect/Drizzle 数据层 + Lambda),面向的是推理请求、token 用量、成本等运行指标数据,由 infra/stats.ts 部署到 stats. 域名下,与本文的"下载量统计"是两套互不相关的体系:

  • 本文的 STATS.md 链路:GitHub Releases API + npm API → script/stats.ts → 根目录 Markdown 表格;
  • packages/stats 站点链路:Iceberg 事件表(inference.event)→ stat-sync 守护进程每小时聚合 → PlanetScale 数据库 → SolidStart 站点。

packages/stats/server 的同步守护进程 可以看到其"每日一次全量刷新 + 每小时增量"的策略,但那属于运行指标域,与下载统计无关,读者如需运行统计站点可参考其 AGENTS.md:在仓库根目录执行 bun dev:stats 即可本地启动。

小结

  • STATS.md 的四个数字口径:GitHub 全 release 资产累计下载、npm opencode-ai 全量累计下载(2020 起)、两者之和、以及相对上一采集时点的日增量。
  • 生成链路完全自动化:.github/workflows/stats.yml 每天 12:00 UTC 触发 → script/stats.ts 分页拉取 GitHub Releases(每页 100 条、间隔 1 秒)并查询 npm range API → 从文件末尾正则解析上一行 → 追加新行 → prettier 格式化 → 以 ignore: 前缀提交并推送,同时向 PostHog 上报两条 download 事件。
  • 数据本身可读性良好:总下载量从 2025-10-19 破百万到 2026-01-29 破一千万;2026 年 1 月中旬 GitHub 渠道日增量多次超过 npm 数个量级,渠道占比翻转至约 77% / 23%;缺失日、+0 日与重复日都是流水线时点行为的直接记录,可按上文机制逐条归因。
  • 复用这套模式时,关键设计点是"用被统计文件自身作为状态存储":只解析最后一行即可恢复基线,无需额外基础设施,代价是异常行(重复/缺失)会留在文件中作为历史痕迹。
登录后查看全文
热门项目推荐
相关项目推荐