# Analytics 逻辑梳理

## 核心原则

| 项 | 核心逻辑 |
| --- | --- |
| AE | 保存实时/短期明细，通用字段包含 Worker 侧读取的真实公网 IP |
| ANALYTICS_DB | `openbidkit-analytics` 只保存简化后的新版 `stats_*` 长期统计表 |
| RESOURCE_DB | `openbidkit-resources` 保存资源元数据和历史累计点击量 |
| Queue | 不使用 Queue，不保留 Queue 代码 |
| `/track` | 写 Analytics Engine；从 Cloudflare 真实客户端 IP 请求头读取公网 IP；仅对 `client_created_at` 距当前业务日期不超过 1 天的新客户端尝试实时插入 D1；授权字段可按快照覆盖既有客户端记录 |
| Cron | 北京时间每天 01:00 起每 30 分钟一个触发点，共 5 个 Cron；每个触发点顺序执行一个或多个已幂等阶段，避免单次 Worker invocation 子请求过多 |
| 历史范围 | 直接读 D1，忽略 AE |
| 今天/7天/30天 | 直接读 AE，忽略 D1 |
| 最近事件/客户端详情 | 只读 AE，不持久化 |
| 留存 | Cron 计算并写固定 30 天 D1 快照，页面读取 D1，忽略当天数据 |
| 公告/授权配置 | 不属于埋点统计明细，使用 KV 保存每项目最新配置 |

## 存储与绑定

| 绑定 | 类型 | 用途 |
| --- | --- | --- |
| `ANALYTICS` | Analytics Engine dataset `agnet_analytics` | 详细埋点事件、实时查询、Cron 汇总来源 |
| `ANALYTICS_DB` | D1 `openbidkit-analytics` | 长期埋点统计新版 `stats_*` 表 |
| `RESOURCE_DB` | D1 `openbidkit-resources` | 资源元数据和累计点击量 |
| `RESOURCE_BUCKET` | R2 `openbidkit` | 资源图片 |
| `NOTICE_STORE` | KV | 公告、授权配置、GitHub stats 缓存 |

## Analytics Engine 字段映射

| AE 字段 | 通用含义 | `config_usage` | `ai_request` | `resource_click` | `agent_runtime` |
| --- | --- | --- | --- | --- | --- |
| `blob1` | `projectName` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob2` | `event` | `config_usage` | `ai_request` | `resource_click` | `agent_runtime` |
| `blob3` | `page` | 空 | 空 | `resources` | 空 |
| `blob4` | `version` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob5` | `platform` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob6` | `arch` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob7` | `client_id` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob8` | `client_created_at` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob9` | 事件扩展字段 | 配置项 key | 模型服务商 provider | 资源统计 key | v3 复合 key：runtime、状态、重试、provider、host、model |
| `blob10` | 事件扩展字段 | 配置项 value | endpoint host | 空 | 空 |
| `blob11` | 事件扩展字段 | 空 | 模型名 | 空 | 空 |
| `blob12` | 事件扩展字段 | 空 | 请求类型 `text/image` | 空 | 空 |
| `blob13` | `client_ip` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob14` | `license_status` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob15` | `license_plan` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob16` | `license_expires_at` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob17` | `source_trusted` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob18` | `untrusted_reason` | 同通用 | 同通用 | 同通用 | 同通用 |
| `blob19`-`blob20` | 预留字段 | 空 | 空 | 空 | 空 |
| `double1` | 固定 `1` | 固定 `1` | 固定 `1` | 固定 `1` | 固定 `1` |
| `double2` | `prompt_tokens` | 0 | Prompt Tokens | 0 | 0 |
| `double3` | `completion_tokens` | 0 | Completion Tokens | 0 | 0 |
| `double4` | `total_tokens` | 0 | Total Tokens | 0 | 0 |

`agent_runtime` 使用通用字段并接收运行时注册表 ID `agent_runtime_kind`。运行时 ID 按 `^[a-z0-9][a-z0-9._-]{0,39}$` 校验，不写死具体运行时白名单。`blob9` 保存单字段 v3 复合统计值，格式为 `v3|<runtime>|<success|failed>|r<0-3>|<provider>|<host>|<model>`，表示运行时、最终状态、本轮重试次数和 Agent 使用的文本模型维度；已有 v2 指标在查询和汇总时归为 `opencode`，不记录 API Key、任务内容、错误详情、Prompt、输出或本地路径。

## 事件类型

| event | 统计用途 |
| --- | --- |
| `app_open` | 打开次数、留存 |
| `page_view` | 页面访问量、页面排行 |
| `config_usage` | 配置使用分布 |
| `ai_request` | 模型使用、累计 AI 请求 |
| `resource_click` | 资源点击统计 |
| `agent_runtime` | Agent 执行成功率、失败率、重试率和重试后成功率 |

`config_usage` 新版每个配置项一条键值对事件，`blob9=config_key`、`blob10=config_value`。D1 `stats_configs` 仍保留 `field_key/value/report_count`，历史已汇总数据不清空；AE 旧格式不再兼容，旧客户端配置上报不会进入新的近期统计或后续 Cron 配置汇总。

授权状态字段只保存低基数状态和值，不上传设备原始指纹；`source_trusted=false` 表示客户端记录为“不可信安装来源”，当前版本只统计不阻断。

## ANALYTICS_DB 新版表

| 表 | 主键 | 关键字段 | 查询用途 |
| --- | --- | --- | --- |
| `stats_totals` | `project_name` | `total_clients`, `total_open`, `total_page_views`, `total_events`, `total_ai_requests`, `total_text_tokens`, `total_generated_images`, `last_rollup_date` | 概览累计总数 |
| `stats_clients` | `project_name + client_id` | `first_seen_at`, `first_seen_date`, `active_days`, `last_active_date`, `last_active_version`, `last_access_ip`, `license_status`, `license_plan`, `license_expires_at`, `source_trusted` | 今日/7日新增、客户端统计列表、IP 汇总、授权状态 |
| `stats_daily` | `project_name + activity_date` | `active_clients`, `app_open_count`, `page_view_count`, `event_count`, `ai_request_count` | 概览每日统计历史部分 |
| `stats_pages` | `project_name + page` | `view_count` | 访问分析历史页面排行 |
| `stats_versions` | `project_name + version` | `event_count`, `client_count` | 访问分析历史版本分布 |
| `stats_configs` | `project_name + field_key + value` | `report_count` | 配置使用历史分布 |
| `stats_models` | `project_name + request_type + provider + endpoint_host + model` | `request_count`, `total_tokens` | 模型使用历史分布、Token 和筛选 |
| `stats_agent_runtime` | `project_name + runtime + provider + endpoint_host + model` | `success_count`, `failed_count`, `total_count`, `retry_count`, `retried_run_count`, `retry_success_count` | Agent 执行、重试和重试后成功历史累计，支持运行时和模型维度分析；迁移前汇总统一写为 `opencode` |
| `stats_client_activity` | `project_name + activity_date + client_id` | `client_created_date` | 留存计算用的每日 `app_open` 活动明细 |
| `stats_retention` | `project_name + snapshot_date + range_days + retention_day` | `cohort_clients`, `retained_clients` | 固定 30 天留存快照，页面直接读取 D1 |
| `stats_rollup_runs` | `project_name + activity_date` | `status`, `started_at`, `completed_at`, `error` | 防止同一天重复汇总 |
| `stats_rollup_stages` | `project_name + activity_date + stage` | `status`, `started_at`, `completed_at`, `error` | 记录分阶段 Cron 和分块写入状态，避免阶段重试重复累加 |

## Cron 写入逻辑

业务日期统一为 `activity_date = 昨日 Asia/Shanghai`。Cloudflare Cron 使用 UTC。免费账号最多 5 个 Cron Triggers，所以部分触发点会顺序执行多个已幂等 stage。

| 北京时间 | UTC Cron | Stage | 查询/操作 | 写入表 |
| --- | --- | --- | --- | --- |
| 01:00 | `0 17 * * *` | `discover`, `daily` | AE 查询昨日有允许事件的 `blob1` 项目列表并初始化当天汇总任务；随后聚合昨日总事件、打开、页面访问、AI 请求和活跃客户端 | `stats_rollup_runs`, `stats_rollup_stages`, `stats_daily`, `stats_totals` |
| 01:30 | `30 17 * * *` | `clients` | AE 按 `projectName + blob7` 聚合客户端当天活跃、最近版本、最后访问 IP、平台、架构和授权状态，并刷新总客户端数；同时写入当天 `app_open` 活动明细 | `stats_clients`, `stats_totals`, `stats_client_activity` |
| 02:00 | `0 18 * * *` | `pages`, `versions` | AE 按 `projectName + blob3` 聚合页面访问；按版本聚合事件量，空版本归 `未知版本`，随后从 `stats_clients.last_active_version` 重算版本客户端数 | `stats_pages`, `stats_versions` |
| 02:30 | `30 18 * * *` | `configs`, `models`, `agents` + 概览 AI 汇总 | AE 按 `projectName + blob9 + blob10` 聚合配置使用；按 `projectName + blob12 + blob9 + blob10 + blob11` 聚合模型请求和 `double4` Total Tokens；按 `projectName + blob9` 解析 Agent 运行时、最终状态、重试次数和模型维度；完成后从 D1 模型历史覆盖刷新概览文本 Token 和生图请求次数 | `stats_configs`, `stats_models`, `stats_agent_runtime`, `stats_totals` |
| 03:00 | `0 19 * * *` | `retention`, `resources` | 从 `stats_client_activity` 计算截至昨日的 D1/D3/D7 留存快照；随后 AE 重算截至昨日的历史资源点击总量，最后把当天项目标记为 success | `stats_retention`, `RESOURCE_DB.resources.click_count`, `stats_rollup_runs`, `stats_rollup_stages` |

阶段执行规则：

| 规则 | 说明 |
| --- | --- |
| 顺序依赖 | 除 `discover` 外，每个 stage 必须等待同项目之前 stage 全部 success，否则跳过等待人工或回填修复 |
| 阶段幂等 | `stats_rollup_stages` 记录每个 `project + date + stage` 的成功状态，已成功 stage 不重复执行 |
| 分块幂等 | 大结果按 JSON 大小分块写 D1，每个块记录为 `stage:chunk:index`，重试时跳过已成功块，避免累计表重复加 |
| 批量写入 | D1 写入使用 `json_each(?)` 批量 `INSERT/UPDATE`，不再按客户端、页面、配置、模型逐行 `run()` |
| 留存快照 | 留存固定使用 `range_days = 30`，忽略当天数据，`snapshot_date = 昨日`；页面读取最新 D1 快照，不再实时扫描 AE 明细 |
| 资源点击 | 资源点击不再按天 `+ clickCount`，而是每次资源 stage 按 AE 历史总量直接设置 `resources.click_count`，避免跨库标记失败后重复累加 |
| 完成状态 | 只有 `resources` stage 完成后，才把 `stats_rollup_runs.status` 标记为 `success`；本地回填跳过资源 stage 时由回填脚本统一重算资源历史总量 |

## 查询逻辑

### 概览

| 指标 | 数据源 | 查询逻辑 |
| --- | --- | --- |
| 总客户端数 | D1 | `stats_totals.total_clients` |
| 总打开次数 | D1 | `stats_totals.total_open` |
| 总页面访问量 | D1 | `stats_totals.total_page_views` |
| 总事件数 | D1 | `stats_totals.total_events` |
| 累计 AI 请求数 | D1 | `stats_totals.total_ai_requests` |
| Token 消耗量 | D1 | `stats_totals.total_text_tokens`，由文本模型 `stats_models.total_tokens` 汇总覆盖 |
| 生成图片数 | D1 | `stats_totals.total_generated_images`，由生图模型 `stats_models.request_count` 汇总覆盖 |
| 今日新增客户端 | D1 | `stats_clients.first_seen_date = 今日 Asia/Shanghai` |
| 7日新增客户端 | D1 | `stats_clients.first_seen_date >= 今日往前6天` |
| 今日活跃客户端 | AE | 今日任意允许事件 `COUNT(DISTINCT blob7)` |
| 每日统计 | D1 + AE | 今天从 AE 查；前 9 天从 `stats_daily` 查 |
| 留存概览 | D1 | 读取 `stats_retention` 最新 30 天快照，忽略当天数据 |
| GitHub stats | GitHub + KV | 保持现有缓存逻辑 |

### 客户端统计

| 视图 | 数据源 | 查询逻辑 |
| --- | --- | --- |
| 客户端列表 | D1 | `stats_clients` 全量，按最近活跃日期和首次访问倒排 |
| 客户端详情 | AE | 按 `projectName + client_id` 查询 7天/30天/全部，按日期和事件分组 |
| IP 统计 | D1 | `stats_clients.last_access_ip` 分组，按客户端数倒排分页；空 IP 不展示 |

### 访问、配置、模型

| 模块 | 历史 | 今天/7天/30天 |
| --- | --- | --- |
| 访问分析 | D1 `stats_pages` / `stats_versions` | AE 实时聚合 |
| 配置使用 | D1 `stats_configs` | AE 实时聚合 |
| 模型使用 | D1 `stats_models` | AE 实时聚合 |
| Agent分析 | D1 `stats_agent_runtime` 按运行时和模型维度汇总 | AE 实时解析 `blob9` 聚合；接口保留总体汇总，并返回 `runtimes` 和带 `runtime` 的 `models` |

版本排序由 Worker JS 做语义版本倒排，保证 `2.10.1 > 2.9.1`，`未知版本` 放最后。历史版本客户端数来自 `stats_clients.last_active_version` 当前分组，不按天累加。

模型使用同时返回请求次数和 Total Tokens。历史 Total Tokens 来自 `stats_models.total_tokens`；今天/7天/30天直接从 AE `double4 * _sample_interval` 聚合。

### 最近事件

| 指标 | 数据源 | 查询逻辑 |
| --- | --- | --- |
| 总数 | AE | `COUNT()`，可按事件过滤 |
| 列表 | AE | `ORDER BY timestamp DESC, blob7 DESC, blob2 DESC, blob3 DESC LIMIT 10 OFFSET pageOffset` |
| 事件筛选 | AE | `blob2 = event`，只允许白名单事件 |

### 资源管理

| 功能 | 数据源 | 查询逻辑 |
| --- | --- | --- |
| 资源公开列表 | `RESOURCE_DB` + AE | `resources.enabled=1`，点击量为 `resources.click_count + AE 今天点击量` |
| 资源后台列表 | `RESOURCE_DB` + AE | `resources` 全量，点击量为 `resources.click_count + AE 今天点击量` |
| 资源图片 | `RESOURCE_BUCKET` | `/resource-image` 读取 R2 |
| 资源历史点击 | `RESOURCE_DB` | Cron 每天按 AE 截至昨日的历史总量重算并设置 `resources.click_count` |

## 历史回填

历史回填脚本复用 `rollupStatsDay()` 按 `discover -> daily -> clients -> pages -> versions -> configs -> models -> agents -> retention` 写 `ANALYTICS_DB` 的统计表。回填前会先补齐回填日期窗口前 30 天到最后回填日的 `stats_client_activity`，保证留存快照可计算。资源点击不按天累加，回填结束时按 AE 中今天之前的总资源点击量直接设置 `RESOURCE_DB.resources.click_count`，避免脚本重跑导致资源累计量翻倍。

如只补指定日期，可在 `analytics/scripts/.env` 配好凭据后设置环境变量 `BACKFILL_DATE=YYYY-MM-DD` 再运行 `npm run backfill:analytics-stats`。例如 PowerShell：`$env:BACKFILL_DATE="2026-06-17"; npm run backfill:analytics-stats; Remove-Item Env:\BACKFILL_DATE`。

新增字段补齐脚本只处理 `stats_versions.client_count` 和 `stats_models.total_tokens`：版本客户端数从 `stats_clients.last_active_version` 分组覆盖，模型 Total Tokens 从 AE 历史数据覆盖写入，不重跑每日统计，不处理资源点击。

## 接口与数据源

| 接口 | 数据源 | 核心逻辑 |
| --- | --- | --- |
| `POST /track` | AE + D1 | 写 AE，实时 upsert 近 1 天新客户端 |
| `GET /api/overview` | D1 + AE + KV | 概览总数、新增、今日活跃、每日统计 |
| `GET /api/clients` | D1 | 客户端列表 |
| `GET /api/client-detail` | AE | 单客户端日期和事件明细 |
| `GET /api/ip-stats` | D1 | 按最后访问 IP 分组统计客户端数，分页返回 |
| `GET /api/traffic` | D1 或 AE | 历史读 D1；今天/7天/30天读 AE |
| `GET /api/config-usage` | D1 或 AE | 历史读 D1；今天/7天/30天读 AE |
| `GET /api/model-usage` | D1 或 AE | 同上，支持服务商/域名/模型筛选 |
| `GET /api/agent-runtime` | D1 或 AE | Agent 总体、运行时和模型维度的成功率、失败率、重试率和重试后成功率，支持 `range=history/today/7/30` |
| `GET /api/latest` | AE | 最近事件分页和事件筛选 |
| `GET /api/retention` | D1 | 留存概览，读取 `stats_retention` 最新 30 天快照；`days` 参数不触发单独快照计算 |
| `GET /api/projects` | D1 优先，AE 兜底 | 项目名列表 |
| `GET/POST/DELETE /api/notice` | KV | 公告后台管理 |
| `GET/POST/DELETE /api/resources` | `RESOURCE_DB` + R2 + AE | 资源管理 |
| `GET /api/github-repo-stats` | GitHub + KV | 仓库统计和缓存 |
