Skip to content

Commit 01ee2de

Browse files
committed
docs: PROPOSAL 补 E 批语义(会话头来源、日志页、日志脱敏)
- §3.3 会话粘性由两级改三级,说明头为何夹在 body 显式键与指纹之间、 头值为何要掺 username - §5 管理台端点补服务端日志两条(只读、admin-only、关闭即 404) - §6 安全边界补日志页的整行丢弃口径与取舍
1 parent 2af402f commit 01ee2de

1 file changed

Lines changed: 4 additions & 2 deletions

File tree

‎PROPOSAL.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)