@@ -107,7 +107,7 @@ Provider 承担上游协议私有部分:发请求、解析事件、分类错
107107
108108** 健康度归一化** :` known(0-100) | unknown | exhausted ` 。CB 取 ` CycleCapacityRemainPrecise ` /` CycleCapacitySizePrecise ` ,TRAE 取 ` credits_limit - credits_amount ` / ` credits_limit ` 。** 不用响应顶层的 ` TotalDosage ` ** :它 = Σ(size − CapacityUsed),而 ` CapacityUsed ` 是上周期口径,本周期消耗记在 ` CycleCapacityUsed ` /` CycleCapacityRemain ` ,用它会出现 ` remaining == total ` 、健康度恒 100。** 为什么必须三态** :探测失败被算成 0 分会让这类凭证在有健康号时永远轮不到。
109109
110- ** 会话粘性** :识别按可靠性分两级 ——① 显式会话标识 ` conversation_id ` / ` conversationId ` / ` prompt_cache_key ` ;② 回落「消息增量前缀指纹」。TTL(` CONVERSATION_STICKY_SECONDS ` ,默认 1h)内固定复用,避免中途换号触发上游风控并丢提示词缓存。请求体带 ` user_id ` 时** 不派生** 第 2 级兜底键 (同用户并行对话前缀可能相同,会误钉)。手动 pin 优先于粘性;凭证报错仍走正常轮换;条目纯内存。` prompt_cache_key ` (OpenAI 官方顶层参数)与 ` metadata.user_id ` (Anthropic 官方字段)已核实;` conversation_id ` / ` conversationId ` / 顶层 ` user_id ` 非两家标准键,属客户端惯用约定,作为兼容探测接受。
110+ **会话粘性**:识别按可靠性分三级 ——① 请求体显式会话标识 `conversation_id` / `conversationId` / `prompt_cache_key`;② **请求头会话标识** `x-session-id` / `x-opencode-session` / `x-session-affinity` / `conversation-id`(按序取第一个合法值,须匹配 `[A-Za-z0-9._:-]{1,128}`,经 contextvar 传入不污染中间层签名,键带 `header:` 前缀避免与 body 同值撞键);③ 回落「消息增量前缀指纹」。**头为什么夹在中间**:body 显式键最精确;头是客户端主动声明的会话身份,比指纹猜测可靠;而指纹只是启发式兜底。头值与其他键一样掺 `username` 再哈希——头是客户端可写的,不掺等于允许一个用户靠猜头值占用别人的账号。 TTL(`CONVERSATION_STICKY_SECONDS`,默认 1h)内固定复用,避免中途换号触发上游风控并丢提示词缓存。请求体带 `user_id` 时**不派生**第 3 级前缀兜底键 (同用户并行对话前缀可能相同,会误钉;头照常生效 )。手动 pin 优先于粘性;凭证报错仍走正常轮换;条目纯内存。`prompt_cache_key`(OpenAI 官方顶层参数)与 `metadata.user_id`(Anthropic 官方字段)已核实;`conversation_id` / `conversationId` / 顶层 `user_id` 非两家标准键,属客户端惯用约定,作为兼容探测接受。
111111
112112### 3.4 模型名三字段
113113
@@ -165,7 +165,7 @@ DDL 以 [src/db/schema.sql](src/db/schema.sql) 为准,共 **12 张表**,`SCH
165165
166166**外部(API Key 鉴权)**:`POST /v1/chat/completions`(流式 + 非流式,`n` 只接受 1——出口结构只能产生单个 choice,其余值 400 `invalid_request`);`POST /v1/responses`(Responses 子集,Codex CLI);`POST /v1/messages` 与 `POST /v1/messages/count_tokens`(Anthropic Messages 子集,`count_tokens` 本地估算不转发上游)——三者共用同一调度链路;`GET /v1/models`(对外 `id` 为归一键 + `name` + `providers` + `by_provider.{渠道}.{raw_id, credit_rate, benefit}` + 可选 `benchmarks` / `cost`(`input`/`output`/`cache_read`,USD/1M,缺价不带该字段)/ `reasoning_efforts`(该渠道生效档位去重集合,未配置不带);`benefit` 标福利模型,单渠道在顶层;自定义别名条目另带 `custom_alias: true`);`GET /v1/models/{id}`(单模型查询,`:path` 容纳带命名空间的渠道原代号;对外 id / 展示名 / 任一渠道原 id / 自定义别名四种写法、大小写不敏感;不存在**或不在该 Key 模型白名单内**一律 404 + `model_not_found`,不泄露池中模型);`GET /v1/usage?days=1|7|30`(自助用量:只查**该 Key 自己**的本地明细,不触发上游,返回 `requests/ok/failed/token/cost_usd` + `limits`/`remaining`/`rate`/`resets_at`,不含凭证 id、昵称或池级信息);`GET /v1/user/balance`(DeepSeek 兼容余额,读探测缓存聚合,不实时打上游);`GET /health`(纯存活)、`GET /healthz`(存活 + 凭证池五类计数,无鉴权)、`GET /metrics`(Prometheus text 格式指标,默认开、无鉴权,`METRICS_ENABLED=false` 关闭)。
167167
168- ** 管理台(会话 Cookie + RBAC)** :凭证管理、API Key 管理(含 Key 级配额 ` quota ` :日请求 / 日 token / 日成本 / RPM / TPM / 并发,` 0 ` =不限;超限 429 + ` Retry-After ` ,判定异常一律** 放行** 并记 warning)、用量统计(` /api/stats/* ` )、Playground、模型列表(operator,含 ` GET /api/models/availability ` 疑似下线只读视图)、用户管理(admin-only)、审计日志(admin-only)、运维告警(admin-only)、上游状态(operator)、任务与配置(admin-only)。
168+ ** 管理台(会话 Cookie + RBAC)** :凭证管理、API Key 管理(含 Key 级配额 ` quota ` :日请求 / 日 token / 日成本 / RPM / TPM / 并发,` 0 ` =不限;超限 429 + ` Retry-After ` ,判定异常一律** 放行** 并记 warning)、用量统计(` /api/stats/* ` )、Playground、模型列表(operator,含 ` GET /api/models/availability ` 疑似下线只读视图)、用户管理(admin-only)、审计日志(admin-only)、运维告警(admin-only)、上游状态(operator)、任务与配置(admin-only)、 ** 服务端日志(admin-only,只读) ** 。
169169
170170- 账号:` GET|POST /api/users ` 、` PATCH /api/users/{username} ` 、` POST /api/users/{username}/{disable|enable|reset-password} ` (** 不提供 DELETE** );自助改密 ` POST /api/auth/password ` ;无鉴权的一次性激活流 ` GET|POST /api/auth/activate ` ;审计查询 ` GET /api/audit ` 。
171171- 凭证运维:` POST /api/credentials/{id}/checkin|toggle|revive|probe|accounts/select ` 、` PUT /api/credentials/{id}/limits ` (限额护栏整体替换,全关时删行)、` GET|POST /api/credentials/{id}/growth ` 、` GET /api/credentials/{id}/credit-events|accounts ` 。
@@ -174,6 +174,7 @@ DDL 以 [src/db/schema.sql](src/db/schema.sql) 为准,共 **12 张表**,`SCH
174174- 运维告警:` GET /api/alerts ` (倒序回看,分页)+ ` POST /api/alerts/{id}/resolve ` (人工标记一条已恢复,admin + CSRF + 审计;用于规则已下线 / 误报等状态机无法自动判定的情况)。后台正常路径会在规则不再命中时自动写 ` resolved_at ` ,且 ` AlertTask ` 启动时从库里未恢复的行重建「告警中」集合,重启后仍能自愈。
175175- 上游状态聚合:` GET /api/upstream-status ` (operator)返回后台任务抓取的内存快照(` summary ` + ` sources ` )与任务的当前 ` enabled ` / ` interval_seconds ` / ` server_time ` ;** 不落盘** ,重启后为「尚未抓取」。四种解析器(Statuspage v2 / OpenCode / Better Stack / Atom)归一化到六档,加源只需在 ` SOURCE_SPECS ` 加一行。
176176- 用量统计:` GET /api/stats/{overview,by-provider,by-model,by-user,by-credential,timeline,model-timeline} ` (分组表的延迟列可选 P50/P90/P95/P99 四档,默认 P95;` p95_* ` 字段与排序键保持兼容)、` 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 ` 同口径)。
177+ - 服务端日志:` GET /api/logs?limit= ` (默认 200,上限 = 缓冲容量)+ ` GET /api/logs/stream ` (SSE,先回放缓冲再推实时,按 ` seq ` 去重)。** 只读** 、admin-only;` LOG_BUFFER_LINES=0 ` 时两端点均 404。只收本进程 ` logging ` 记录,不读文件;每行发出前过脱敏白名单。
177178- 侧栏数据提醒:` GET /api/nav-badges ` 一次返回侧栏各项的待办计数(凭证需重登/耗尽、未恢复告警、上次失败任务、异常上游源、待激活账号、本人将到期 Key、模型目录陈旧)。** 按角色裁剪 payload** ——缺失的键 = 无权看(admin-only 的告警/任务/用户不下发给 operator/viewer,operator 的上游/目录不下发给 viewer);这是聚合端点,** 不做整体 403** (viewer 也要看自己的 API Key)。各计数与对应页面数据** 不是同一口径** :只数「待办」(如告警只数 ` resolved_at IS NULL ` 、任务用「上次失败」而非「连续失败达阈值」)。
178179
179180实现以代码为准,使用说明见 [ README.md] ( README.md ) 。
@@ -187,6 +188,7 @@ DDL 以 [src/db/schema.sql](src/db/schema.sql) 为准,共 **12 张表**,`SCH
187188- ** 会话** :Cookie ` SameSite=Lax ` + 写操作 CSRF 校验(含 logout);` Secure ` ** 按请求实际协议动态判定** (` X-Forwarded-Proto ` 首个值优先,无该头则仅当请求 Host 命中 https 的 ` PUBLIC_BASE_URL ` 主机才判 Secure)——同一实例 LAN(http) 与隧道(https) 双入口写死任一侧都会打断另一侧;** 无 Cookie 的写端点(登录 / ` /authorize ` )也做跨站防护** :登录按 Origin/Referer 同源校验(防 login CSRF),` /authorize ` 按上游原样回显的 ` loginTraceID ` 绑定到** 发起登录那一次** (TRAE 回调本就跨源,Origin/Referer 既拦不住真攻击也可能误杀真回调);会话与 API Key 除签名 / 摘要外校验用户仍存在、启用且 epoch 一致。
188189- ** 错误响应不回流上游正文** :` /v1 ` 与外层的错误文案只带受控标识(HTTP 状态码 / 上游业务码 / 异常类名),上游错误体摘要只进服务端日志;未匹配的 ` /api ` 、` /v1 ` 路径返回 JSON 404(不落到 SPA 的 200 + HTML)。** 日志脱敏** :不打印 Token、完整请求体;审计写 INFO(含操作人),** 绝不记密码/令牌明文** 。
189190- ** 请求 ID 是客户端可写输入** :入站 ` X-Request-ID ` 按白名单字符集与长度(` [A-Za-z0-9._:-]{1,128} ` )校验后才采信与回显,不合法一律丢弃改用自生成——否则日志与统计列会被人塞进换行/超长串。响应头回显最终 ID(含自生成值),不伪装成上游追踪。
191+ - ** 服务端日志页同样只出不进** :` src/webapp/log_buffer.py ` 的 ` redact_or_drop ` 在行离开进程前整行过滤——独立敏感词(token/secret/authorization/cookie/password/api_key 及复数和连字符形态)、键值形态(` access_token= ` 、` X-Api-Key: ` 、` set-cookie: ` 、JSON 键)、` sk-… ` 形状、` Bearer <值> ` 命中即丢弃;空行也丢。代价是 ` token 已刷新 ` 这类正常行会被一起吞掉,取舍是** 宁可漏一条日志,也不把凭证推进浏览器** 。页面无任何写入/执行入口,订阅者只能读。
190192- ** 请求级结构化日志** (` src/webapp/access_log.py ` ,` REQUEST_LOG_ENABLED ` 可热更):每个业务请求(` /api/* ` 、` /v1/* ` 、` /authorize ` )一行 logfmt,含 ` request_id ` / 路由** 模板** / 状态码 / 耗时。** 不写 query string** (可能被客户端塞 token)、** 不写请求体 / 请求头 / 用户名** (身份靠 ` request_id ` 关联 ` usage_events ` );未匹配路由时回落为清洗并截断的原始路径,防换行注入。静态资源与探针不记(uvicorn 已覆盖)。
191193- ** 租户隔离** :` /api/stats/events ` 与 ` /api/stats/by-credential ` 的** 凭证昵称** 取自全局共享池(常含邮箱/手机),仅 admin/operator 可见;viewer 即便只看自己的记录也不下发该字段。` /api/stats/events?format=csv ` (整份明细导出)同样仅 admin/operator;` /api/stats/events/stream ` 的广播 payload 不含昵称,仅 admin/operator 的连接由服务端逐条补 ` credential_name ` 。
192194- ** 热更配置写入即校验** :float 拒 NaN/±inf;字符串类有长度上限;告警 webhook 只接受 ` http(s) ` 并做 ** SSRF 校验** (写入期拒字面量内网 / 回环 / 链路本地 / 保留 / 多播 / 未指定地址,** 投递前再按 DNS 解析结果复核一次** 、解析失败即阻断),外发载荷对 ` nickname ` 打码,多地址并发投递并加总预算。告警出站 client ` trust_env=False ` ,不吃宿主代理环境变量。
0 commit comments