Skip to content

Commit fb62fe6

Browse files
committed
feat(stats): 输出速度拆「正文 / 总产出」双口径并下沉到逐请求明细
- 口径:速度 = `token 总量 ÷ 生成时间总和`(非逐条比值平均)。两列**同分母**, 分子分别是 `output_tokens - reasoning_tokens`(正文速度 `tps_visible`)与 `output_tokens`(总产出速度 `tps`)——思考 token 成批快速吐出,与正文混算会 远高于屏幕刷新速度(实测占比 50–67% 时约 190 与 63 tok/s)。 - 样本门槛:只算经本服务路由的**成功**请求且 `output_tokens > 0`(**不设输出量 下限**);保留两道物理下限——生成窗口 < `SPEED_MIN_GEN_MS`(200ms)或单条 > `SPEED_MAX_TPS`(1000 tok/s)按「生成时长不可测」剔除(上游攒批吐出时 首末帧几乎同时到达,分母趋近 0 会放大成天文数字)。 - 主口径无样本时回落估算口径(分母换成端到端耗时、含首字等待),样本门槛与 主口径一致,界面整格前置 `≈`;两口径都无样本显示 `—`。 - 逐请求:`events()` 每行带 `tps` / `tps_visible`(`_row_speed_sql` 一个片段 同时供 SELECT 与 `EVENT_SORT_COLUMNS` 排序键,避免「排序按 A 算、展示按 B 算」);**不回落估算口径**。CSV 导出同列、不可测留空(空不是 0)。 - 实时流:`StatsCollector._notify` 广播前调 `row_speed()` 补两个派生字段(库里 不存),规则与明细端点同源。 - 前端:速度并成一格 `正文 / 总产出`(列头下拉仍可分别排序 `tps_visible` / `tps`);概览卡片给正文速度、总产出写在 hint;分组表与明细表同源。 - 明细表列合并:`时间`拆两行(`HH:mm:ss` 在上、`YYYY-MM-DD` 在下,时刻优先、 日期次要色);`渠道 / 凭证`合成一列(凭证名在上、渠道 icon + 名在下,悬停看 完整凭证 ID);列头下拉子列 `provider` / `credential`。 - 文档:README / TECHNICAL / PROPOSAL 同步口径、门槛与展示。
1 parent 1b4a21c commit fb62fe6

18 files changed

Lines changed: 1052 additions & 84 deletions

‎PROPOSAL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ DDL 以 [src/db/schema.sql](src/db/schema.sql) 为准,共 **12 张表**,`SCH
194194
- 运行时配置与任务运行态:`GET|PUT /api/settings` + `GET /api/tasks` + `POST /api/tasks/{key}/run`(手动跑一轮)。列表端点接受统一 `sort`(白名单键)+ `order`(`asc`/`desc`);未知键 / 非法值回落到默认,不报错。回调(无鉴权):`GET /authorize`。
195195
- 运维告警:`GET /api/alerts`(倒序回看,分页)+ `POST /api/alerts/{id}/resolve`(人工标记一条已恢复,admin + CSRF + 审计;用于规则已下线 / 误报等状态机无法自动判定的情况)。后台正常路径会在规则不再命中时自动写 `resolved_at`,且 `AlertTask` 启动时从库里未恢复的行重建「告警中」集合,重启后仍能自愈。
196196
- 上游状态聚合:`GET /api/upstream-status`(operator)返回后台任务抓取的内存快照(`summary` + `sources`)与任务的当前 `enabled` / `interval_seconds` / `server_time`;**不落盘**,重启后为「尚未抓取」。四种解析器(Statuspage v2 / OpenCode / Better Stack / Atom)归一化到六档,加源只需在 `SOURCE_SPECS` 加一行。
197-
- 用量统计:`GET /api/stats/{overview,by-provider,by-model,by-user,by-credential,timeline,model-timeline}`(分组表的延迟列可选 P50/P90/P95/P99 四档,默认 P95;`p95_*` 字段与排序键保持兼容;另有输出速度 `tps` / `tps_estimated` 列,排序键 `tps`)、`GET /api/stats/errors`(**失败归因**:窗口内 `ok=0` 明细按 `(provider, model, error_type)` 分组计数 + 最近失败时刻,`limit` ≤ 5000)、`GET /api/stats/events`(rowid 游标 / LIMIT-OFFSET 双模式,`provider`/`model`/`credential_id` 钻取过滤,`ok=0` 只看失败,`?format=csv` 导出)、`GET /api/stats/events/stream`(**SSE 实时明细**:`StatsCollector.record` 事务提交后由进程内 `LiveEventHub` 广播刚写入的行,不轮询数据库;每订阅者有界缓冲 200 条、并发上限 8 超出回 429 + `Retry-After`;15s 注释帧保活,`Cache-Control: no-store` + `X-Accel-Buffering: no`;作用域/过滤在订阅者侧逐条判定,与 `/api/stats/events` 同口径)。
197+
- 用量统计:`GET /api/stats/{overview,by-provider,by-model,by-user,by-credential,timeline,model-timeline}`(分组表的延迟列可选 P50/P90/P95/P99 四档,默认 P95;`p95_*` 字段与排序键保持兼容;另有输出速度 `tps_visible`(正文)/ `tps`(总产出含思考)/ `tps_estimated` 列,排序键 `tps_visible` / `tps`)、`GET /api/stats/errors`(**失败归因**:窗口内 `ok=0` 明细按 `(provider, model, error_type)` 分组计数 + 最近失败时刻,`limit` ≤ 5000)、`GET /api/stats/events`(rowid 游标 / LIMIT-OFFSET 双模式,`provider`/`model`/`credential_id` 钻取过滤,`ok=0` 只看失败,每行带逐请求速度 `tps` / `tps_visible`、排序键同名,`?format=csv` 导出)、`GET /api/stats/events/stream`(**SSE 实时明细**:`StatsCollector.record` 事务提交后由进程内 `LiveEventHub` 广播刚写入的行,不轮询数据库;每订阅者有界缓冲 200 条、并发上限 8 超出回 429 + `Retry-After`;15s 注释帧保活,`Cache-Control: no-store` + `X-Accel-Buffering: no`;作用域/过滤在订阅者侧逐条判定,与 `/api/stats/events` 同口径)。
198198
- 服务端日志:`GET /api/logs?limit=`(默认 200,上限 = 缓冲容量)+ `GET /api/logs/stream`(SSE,先回放缓冲再推实时,按 `seq` 去重)。**只读**、admin-only;`LOG_BUFFER_LINES=0` 时两端点均 404。只收本进程 `logging` 记录,不读文件;每行发出前过脱敏白名单。
199199
- 侧栏数据提醒:`GET /api/nav-badges` 一次返回侧栏各项的待办计数(凭证需重登/耗尽、未恢复告警、上次失败任务、异常上游源、待激活账号、本人将到期 Key、模型目录陈旧)。**按角色裁剪 payload**——缺失的键 = 无权看(admin-only 的告警/任务/用户不下发给 operator/viewer,operator 的上游/目录不下发给 viewer);这是聚合端点,**不做整体 403**(viewer 也要看自己的 API Key)。各计数与对应页面数据**不是同一口径**:只数「待办」(如告警只数 `resolved_at IS NULL`、任务用「上次失败」而非「连续失败达阈值」)。
200200

‎README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
- **公共凭证池**:admin 集中维护、全员共享、加密入库(`APP_SECRET`);设备码登录、多账号切换、额度探测、每日签到、token 预刷新。
1515
- **三角色账号体系**:`admin` / `operator` / `viewer`,用户存 SQLite;一次性激活链接自设密码、首登强制改密、改角色/停用即时吊销会话;登录与写操作留审计。
1616
- **成长中心**(仅 CodeBuddy):自动领 Buddy 旅行礼物、派 Buddy、领任务奖、断登补登、连登兑换、开盲盒;不可逆动作可用 `GROWTH_IRREVERSIBLE_ACTIONS=false` 关停;管理台可手动执行。
17-
- **脱敏统计**:不存对话内容;明细 90 天、小时汇总永久;按人/渠道/模型/凭证可视化;按 OpenRouter 刊例价估算成本(人民币为主);按「总输出 token ÷ 生成时间总和」给出输出速度(tok/s);明细面板带「实时」开关(SSE 推送刚落库的请求)。
17+
- **脱敏统计**:不存对话内容;明细 90 天、小时汇总永久;按人/渠道/模型/凭证可视化;按 OpenRouter 刊例价估算成本(人民币为主);按「正文 token ÷ 生成时间总和」与「总产出 token ÷ 生成时间总和」两口径给出输出速度(tok/s,剔除批到达的不可测样本;概览卡片、分组表与逐请求明细同源);明细面板带「实时」开关(SSE 推送刚落库的请求)。
1818
- **上游状态聚合**:把各模型 / 网关提供方的官方状态页(OpenCode Zen、Kilo、DeepSeek、Together、Moonshot、MiniMax、OpenAI、Anthropic)归一化后并排展示,用于区分「上游整体故障」与「本服务凭证 / 额度问题」。只做解释、不参与选号 / 熔断;快照只存内存、重启归零。
1919
- **侧栏数据提醒**:侧栏对应项上显示待办计数角标(凭证需重登/耗尽、未恢复告警、上次失败任务、异常上游源、待激活账号、本人将到期 Key、模型目录陈旧),不点进去也能看到;只数「待办」、按角色裁剪,见[下文](#侧栏数据提醒导航角标)。
2020
- **管理台安全加固**:登录限流、CSRF 校验、请求体上限、Host 白名单。
@@ -541,17 +541,17 @@ OpenRouter 收录数百条,其中多数本服务没接。每行带 `available`
541541

542542
### 分组维度与重新汇总
543543

544-
**分组统计**面板用 tabs 切四个维度(按渠道 / 按模型 / 按用户 / 按凭证),共用同一张表(渠道 / 请求(请求数 · 成功率,悬停看成功数)/ Credit / 成本 / Token(输入 ÷ 输出 · 命中率,悬停看命中 token)/ 速度(tok/s)/ 平均延迟(耗时 ÷ 首字)/ P95 延迟(耗时 ÷ 首字)),共 8 列。对应端点 `GET /api/stats/by-provider|by-model|by-user|by-credential`。默认按请求数降序,点列头可切换排序;请求 / Token / 平均延迟 / P95 延迟四个合并列点列头弹菜单选子列再排序(`sort=requests|ok_count|input_tokens|output_tokens|cached_tokens|avg_latency_ms|avg_ttfb_ms|p50_latency_ms|p90_latency_ms|p95_latency_ms|p99_latency_ms|p50_ttfb_ms|p90_ttfb_ms|p95_ttfb_ms|p99_ttfb_ms`,速度列另有独立排序键 `sort=tps`),成功率与命中率是派生只读值不参与排序。**延迟分位可切换**:P95 列的表头有 P50 / P90 / P95 / P99 四档切换(默认 P95,不铺开四列以免表头拥挤);四档由同一次窗口函数子查询算出,取整口径一致(nearest-rank,n 为**有值**样本数)。**按用户 / 按凭证**只对 admin 与 operator 展示。**口径差异**:渠道 / 模型 / 用户三维读**小时汇总**(永久保留,最近 5 分钟内的请求尚未计入);**凭证维度读明细表**(小时汇总主键里没有 `credential_id`),故只覆盖**最近 90 天**。
544+
**分组统计**面板用 tabs 切四个维度(按渠道 / 按模型 / 按用户 / 按凭证),共用同一张表(渠道 / 请求(请求数 · 成功率,悬停看成功数)/ Credit / 成本 / Token(输入 ÷ 输出 · 命中率,悬停看命中 token)/ 速度(正文 ÷ 总产出,tok/s,悬停看两口径)/ 平均延迟(耗时 ÷ 首字)/ P95 延迟(耗时 ÷ 首字)),共 8 列。对应端点 `GET /api/stats/by-provider|by-model|by-user|by-credential`。默认按请求数降序,点列头可切换排序;请求 / Token / 速度 / 平均延迟 / P95 延迟五个合并列点列头弹菜单选子列再排序(`sort=requests|ok_count|input_tokens|output_tokens|cached_tokens|avg_latency_ms|avg_ttfb_ms|p50_latency_ms|p90_latency_ms|p95_latency_ms|p99_latency_ms|p50_ttfb_ms|p90_ttfb_ms|p95_ttfb_ms|p99_ttfb_ms|tps_visible|tps`),成功率与命中率是派生只读值不参与排序。**延迟分位可切换**:P95 列的表头有 P50 / P90 / P95 / P99 四档切换(默认 P95,不铺开四列以免表头拥挤);四档由同一次窗口函数子查询算出,取整口径一致(nearest-rank,n 为**有值**样本数)。**按用户 / 按凭证**只对 admin 与 operator 展示。**口径差异**:渠道 / 模型 / 用户三维读**小时汇总**(永久保留,最近 5 分钟内的请求尚未计入);**凭证维度读明细表**(小时汇总主键里没有 `credential_id`),故只覆盖**最近 90 天**。
545545

546546
**延迟两类口径**:**只统计成功请求**(失败请求的耗时会拉偏「典型耗时」)。**平均延迟列** = 平均耗时 / 平均首字 = `latency_sum ÷ 成功数`(读小时汇总,与总览卡片、图表同口径,永久保留);**P95 延迟列** = P95 耗时 / P95 首字 = 第 95 百分位,按 **nearest-rank**(第 `⌈0.95·n⌉` 小的值,n = 该组有值样本数)从**逐请求明细**算出——小时汇总只有和与计数,数学上还原不出分布,故分位列只覆盖**最近 90 天**。P50 / P90 / P99 与 P95 同一套 SQL(一次子查询算完四档),口径完全一致,只是百分位参数不同。`ttfb_ms` 对非流式 / 未产出首字的成功请求为 `NULL`,两列各自独立排名(只数有值样本)。无成功样本 / 未上报延迟时显示 `—`(不拿 0 冒充)。
547547

548548
**失败归因**:分组表下方另有「失败归因」面板,`GET /api/stats/errors`(operator+)按 `(渠道, 模型, 错误类型)` 聚合窗口内 `ok=0` 的明细,给出失败次数与最近失败时刻(按失败数降序,`limit` ≤ 5000)。点任意一行即**钻取**到下方明细并自动加上「只看失败」(`GET /api/stats/events?ok=0`,与 `provider`/`model`/`credential_id` 叠加);键盘 Enter / Space 同样触发。该面板只在 admin/operator 下渲染与请求。`/metrics` 的 `upstream_errors_total` 仍取进程内计数(重启归零),与这里的 90 天明细口径不同,两者不合并。
549549

550550
**点行钻取**:点分组表任意一行,下方「请求明细」即按该行钻取(渠道 / 模型 / 凭证按同名列过滤,用户维度走作用域参数),条件叠加、可多个维度层层收窄,面板上方显示当前过滤标签并可一键清除。`GET /api/stats/events` 因此接受 `provider` / `model` / `credential_id` 三个过滤参数(`AND` 语义),另有 `ok=0` 只看失败。
551551

552-
**明细表列合并**:请求明细把同类列并成一列,默认值一眼可读、悬停展开全部子项:**Token**(默认「输入 / 输出 · 命中率」,悬停含命中 token)、**延迟**(默认「耗时 / 首字」)。列数从 14 压到约 10,窄屏卡片也少一半字段。排序能力不丢:点合并列头弹出下拉选子列(Token → 输入 / 输出 / 命中 / 命中率,延迟 → 耗时 / 首字),选中即按该子列排序;**命中率**子列走后端 `sort=cache_rate`(命中 ÷ 输入,输入为 0 或未上报时为 `NULL`,与其它可空列同排序语义)。
552+
**明细表列合并**:请求明细把同类列并成一列,默认值一眼可读、悬停展开全部子项:**Token**(默认「输入 / 输出 · 命中率」,悬停含命中 token)、**延迟**(默认「耗时 / 首字」)、**速度**(默认「正文 / 总产出」,tok/s,悬停写全两口径)、**渠道 / 凭证**(凭证名在上、渠道 icon + 名在下,悬停看完整凭证 ID——同渠道常挂多个账号,先认账号)。**时间列拆成两行**(`20:17:09` / `2026-10-21`,**具体时刻优先**、日期次要色,等宽数字各自纵向对齐,窄屏不折行),请求 ID 提示仍挂在这一格。列数从 15 压到约 10,窄屏卡片也少一半字段。排序能力不丢:点合并列头弹出下拉选子列(Token → 输入 / 输出 / 命中 / 命中率,延迟 → 耗时 / 首字,速度 → 正文 / 总产出,渠道 / 凭证 → 渠道 / 凭证),选中即按该子列排序;**命中率**子列走后端 `sort=cache_rate`(命中 ÷ 输入,输入为 0 或未上报时为 `NULL`,与其它可空列同排序语义),速度子列走 `sort=tps_visible` / `sort=tps`,渠道子列走 `sort=provider` / `sort=credential`。
553553

554-
**导出 CSV**:明细面板右上「导出 CSV」(仅 admin / operator)按**当前时间范围 + 当前钻取过滤**导出逐请求明细,`GET /api/stats/events?format=csv`。文件带 UTF-8 BOM(Excel 认编码)、`\r\n` 换行、时间列 ISO 8601(UTC),并对以 `= + - @`(及制表符/回车)开头的**文本**单元格加前导单引号防公式注入(数值含负数原样)。单次导出上限 10000 行,超出截断并在响应头 `X-Export-Truncated: 1` 标出,页面提示收窄条件。
554+
**导出 CSV**:明细面板右上「导出 CSV」(仅 admin / operator)按**当前时间范围 + 当前钻取过滤**导出逐请求明细,`GET /api/stats/events?format=csv`。列与页面明细表同口径,含**逐请求速度**(`正文速度 tok/s` / `总产出速度 tok/s`,与页面同规则:只算生成窗口可测的成功请求,不可测的行留空——空不是 0)。文件带 UTF-8 BOM(Excel 认编码)、`\r\n` 换行、时间列 ISO 8601(UTC),并对以 `= + - @`(及制表符/回车)开头的**文本**单元格加前导单引号防公式注入(数值含负数原样)。单次导出上限 10000 行,超出截断并在响应头 `X-Export-Truncated: 1` 标出,页面提示收窄条件。
555555

556556
**实时流(SSE)**:明细面板右上「实时」开关打开后,`EventSource` 订阅 `GET /api/stats/events/stream`,展示**当前筛选范围内刚落库**的请求(与下方翻页的历史明细分开展示,避免混成一张会跳动的表)。事件在 `StatsCollector.record` **事务提交后**由进程内 `LiveEventHub` 广播(不轮询数据库),前端只保留最近 50 条、新→旧排列;关闭开关即断开连接。
557557

0 commit comments

Comments
 (0)