首页
/ OpenCode Stats 统计服务解构:app / core / server 三层架构与本地运行实操

OpenCode Stats 统计服务解构:app / core / server 三层架构与本地运行实操

2026-09-06 14:05:18作者:董斯意

Stats 是 opencode 仓库中一个独立于 console 的统计站点,用于沉淀和展示推理用量、模型/供应商排名等统计数据。本文以 packages/stats/README.md 为骨架,结合 coreserverapp 三个子包的源码与部署配置,说明 Stats 的分层结构、数据库设计、数据同步链路,以及如何在本地启动和类型检查整套服务。读完后你可以独立完成 Stats 站点的本地开发、理解其 Effect + Drizzle + SolidStart 的技术组合,并定位各层职责对应的具体源码。

Stats 是什么:独立于 console 的统计站点

packages/stats/README.md 给出的第一句定位是:Stats 是一个独立于 console 的站点(Stats is a separate site from the console)。运行时、数据库和域名服务放在 core,SolidStart 站点放在 app,可部署的服务入口放在独立子包中。

README 声明的子包职责如下:

  • app:SolidStart 前端站点;
  • core:Effect 服务、应用配置、Drizzle schema/migrations 以及统计领域服务;
  • function:调用 core 服务的 Lambda 处理器。

对照当前仓库的实际目录结构(packages/stats/ 下为 appcoreserver 三个子包),从源码结构看,README 中提到的可部署入口包现在对应的是 server 子包:它不再是 Lambda,而是一个基于 Effect NodeHttpServer 的 Bun/Node HTTP 服务(入口文件),承担数据摄入与同步职责。README 的核心分层思想(站点 / 领域核心 / 可部署服务)在现状中仍然完整保留。

本地运行与类型检查:从仓库根目录出发的完整命令

启动站点

packages/stats/AGENTS.md 与 README 一致地指出,从仓库根目录执行 bun run dev:stats 即可启动 SolidStart 站点。根 package.json 中该脚本的真实定义是:

"dev:stats": "bun sst shell --stage=production -- bun run --cwd packages/stats/app dev"

也就是说,它并不是直接起一个 Vite 进程,而是先进入 sst shell --stage=production 环境(加载 SST 生成的环境变量与资源信息),再在 packages/stats/app 目录下执行 vite dev。这解释了为什么 Stats 的本地开发必须“从仓库根目录”执行——子包自身无法获取 SST 注入的环境上下文。

app 子包的 package.json 中脚本为:

  • devvite dev --host 0.0.0.0
  • buildvite build
  • startvite start
  • typechecktsgo --noEmit

类型检查

README 列出的三条 typecheck 命令:

bun run --cwd packages/stats/app typecheck
bun run --cwd packages/stats/core typecheck
bun run --cwd packages/stats/function typecheck

appcore 两个子包当前均可直接使用(两者 typecheck 脚本均为 tsgo --noEmit)。由于可部署入口包已由 function 演进为 server,实际执行时对应命令为:

bun run --cwd packages/stats/server typecheck

三个子包的依赖都声明了 engines.node >= 22,使用 Node/Bun 22+ 是运行前提。

core 子包:Effect 服务 + Drizzle 数据库的核心层

core 是整个 Stats 的领域核心,core/package.json 的 exports 划分了清晰的对外模块面:

  • .:总入口,聚合导出 AthenaAppConfigDatabaseGeoStatStatsHomeInferenceModelStatProviderStatRetentionStatStatRuntimeStatSync(见 src/index.ts);
  • ./athena./config./database./r2-sql./runtime./stat-sync:各基础设施模块;
  • ./domain/*:领域服务按文件独立导出。

Effect 运行时装配

runtime.ts 展示了典型的 Effect 依赖注入装配方式:

const repoLayer = Layer.mergeAll(
  ModelStatRepo.layer,
  ProviderStatRepo.layer,
  GeoStatRepo.layer,
  RetentionStatRepo.layer,
).pipe(Layer.provide(databaseLayer))

export const layer = Layer.mergeAll(AppConfig.layer, databaseLayer, repoLayer)
export const runtime = ManagedRuntime.make(layer)

即:合并四个统计仓储层(Model / Provider / Geo / Retention),统一由数据库层提供依赖,再与 AppConfig 合并为 ManagedRuntimeapp(SolidStart 服务端)与 server(摄入服务)都通过这一入口消费同样的服务集,保证两端行为一致。

应用配置:Effect Config + SST 资源

config.ts 定义了 AppConfigValuestage + publicUrl):

const config = Config.all({
  stage: Config.succeed(Resource.App.stage),
  publicUrl: Config.string("PUBLIC_URL").pipe(Config.withDefault("http://localhost:3000")),
}).pipe(Config.map(decodeAppConfigValue))

stage 来自 SST 注入的 Resource.App.stagepublicUrl 读环境变量 PUBLIC_URL 且默认 http://localhost:3000。这说明本地开发时无需额外配置,站点默认按 localhost:3000 处理公开地址。

数据库:PlanetScale(MySQL)+ Drizzle

core 的依赖里同时出现 @planetscale/databasedrizzle-orm,schema 使用 mysqlTable 定义。database/schema.ts 中两张核心表:

  • model_stat:以 provider + model + provider_model 为维度,包含周期列(grain/period_key/dataset/tier/client/source 等)、指标列与 rank_by_tokens / rank_by_requests / rank_by_cost 排名列;
  • provider_stat:以 provider 为维度,额外带市场份额列与 rank_by_sessions 等排名列。

两者均建立以 (grain, period_key, dataset, tier, client, source, provider[, model]) 为键的唯一索引(uniq_model_perioduniq_provider_period),并配有面向排行榜查询的二级索引(如 idx_leaderboard_tokenstotal_tokens 排序)。这与 app 端“模型/供应商排行榜”页面是一一对应的。

migrations 目录 core/migrations 目前包含 8 个迁移,其中 20260620000000_unique_users(去重独立用户统计)与 20260826000000_model_retention(模型留存)两个迁移只包含 migration.sql,说明存在手工 SQL 迁移的历史痕迹;配合 drizzle.config.ts 使用 drizzle-kit 管理。

core 提供的数据库运维脚本(见 core/package.json):

bun run --cwd packages/stats/core db:generate     # drizzle-kit generate 生成迁移
bun run --cwd packages/stats/core db:migrate      # 执行 src/migrate.ts
bun run --cwd packages/stats/core db:push         # drizzle-kit push 直推 schema
bun run --cwd packages/stats/core db:studio        # drizzle-kit studio 交互查看
bun run --cwd packages/stats/core db:check-unique-users      # 校验唯一用户约束
bun run --cwd packages/stats/core db:ensure-unique-users     # 落地唯一用户约束
bun run --cwd packages/stats/core honeycomb:backfill         # 回灌 Honeycomb 数据

领域服务与可观测性

core/src/domain/ 下的服务划分:home(首页统计,含 home.test.ts)、inference(推理用量,含 inference.test.ts)、modelprovidergeo(地域)、retention(留存)、stat(基础统计),另有 model-normalization.ts 处理模型名称归一化。core/src 下还有若干与数据管道直接相关的一等脚本:athena.ts@aws-sdk/client-athena 查询)、r2-sql.ts(Cloudflare R2 SQL 查询)、stat-sync.ts(统计同步)、honeycomb-backfill.tsensure-unique-users.ts

server 子包:数据摄入与同步服务

server/src/server.ts 使用 Effect 的 @effect/platform-node 起一个 Node HTTP 服务:

const ServerLive = NodeHttpServer.layerConfig(
  () => createServer(),
  Config.all({
    port: Config.number("PORT").pipe(Config.withDefault(3000)),
    host: Config.string("HOST").pipe(Config.withDefault("0.0.0.0")),
  }),
)

路由层 Routes 建立在 Ingest.layer 之上(ingest.ts,依赖 @aws-sdk/client-firehose,用于消费/转发 Firehose 事件),并注册了优雅退出信号处理(shutdown.ts)。服务入口脚本为 bun src/server.ts,构建镜像使用 server/Dockerfile

从部署配置 infra/stats.ts 可以确认整条链路的组装方式:

  • 数据库:PlanetScale 数据库 opencode-stats(含开发分支 StatsDatabaseBranch 与独立密码 StatsDatabasePassword),并通过 sst.Linkable 暴露给 app/core 链接使用;
  • 站点:sst.cloudflare.x.SolidStart 部署到 Cloudflare,路径为 packages/stats/app,域名为 stats.${domain}——即 Stats 是独立域名,与 console 完全分离;
  • 同步服务:sst.aws.Service(StatsSyncService)使用 packages/stats/server/Dockerfile 打包,链接数据库、inference 事件、R2 SQL 及其认证配置。infra 中的注释提到该服务以约 5 分钟一次的周期拉取统计查询,说明 Stats 数据是近实时增量同步而非整库重算;
  • 本地辅助:sst.x.DevCommand(StatsStudio)在 packages/stats/core 目录下启动数据库 Studio,方便本地连库。

app 子包:SolidStart 统计站点

app/package.json 显示站点技术栈为 solid-js + @solidjs/start + nitro,并直接依赖 @opencode-ai/stats-core(workspace 引用)与 @opencode-ai/ui,图表缩放使用 d3-scale,国家/地区文案使用 i18n-iso-countries

路由结构(app/src/routes):

  • /(index.tsx):站点首页,配合 stats-runtime.tsstats-cache.ts 在服务端装配核心服务并缓存查询结果;
  • /compare/...:多套对比页面(family 对比、lab+model 跨供应商对比),包含雷达图(compare-radar.tsx)与对比卡片等组件;
  • /[lab]/[model]:单模型详情(lab 为实验室/供应商标识);
  • /api/health/api/newsletter:健康检查与订阅接口。

国际化方面,app/src/i18n 内置 16 个语言文件(ar、br、da、de、es、fr、it、ja、ko、no、pl、ru、th、tr、uk、zh、zht 中的 16 种),并通过 context/i18n.tsxlib/language.ts 在客户端/服务端协同工作。SEO 相关由 sitemap.xml.ts 生成站点地图,构建配置见 app.config.tsvite.config.ts

小结

  • Stats 与 console 完全解耦:独立 SolidStart 站点(app)、独立域名(stats.${domain})、独立 PlanetScale 数据库(opencode-stats);
  • 本地开发只需一条命令:bun run dev:stats(本质是 sst shell + vite dev);三个子包均可用 bun run --cwd <子包> typecheck 做类型检查;
  • core 以 Effect Layer 装配领域仓储,以 Drizzle(MySQL)管理 model_stat / provider_stat 等统计表,数据库脚本集中在 core 的 package.json 中;
  • server 负责 Firehose 数据摄入与周期性 stat-sync,是 Stats 数据链路的服务端入口;
  • 相关源码入口:core 运行时数据库 schemaserver 入口部署配置
登录后查看全文
热门项目推荐
相关项目推荐