mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-20 08:57:57 +00:00
229 lines
22 KiB
Markdown
229 lines
22 KiB
Markdown
# ModelRouter 调度层重构方案(基于线上实测)
|
||
|
||
> 调研对象:192.168.2.60 生产实例(`/usr/local/bin/llmsproxy -config /etc/llmsproxy/config.yaml`,systemd 托管)
|
||
> 调研时间:2026-08-10,审计文件 `/etc/llmsproxy/runtime.json.audit.jsonl`(3978 行 / 1042 条请求记录)
|
||
|
||
---
|
||
|
||
## 一、线上实测问题(证据链)
|
||
|
||
### P1(核心,用户实测)编辑 AUTO 优先级不重载退避状态
|
||
- 证据:`/api/status` 实时返回 `qijiar available=false, live_available=true`、`zen available=false, live_available=true`。
|
||
- 根因:`Core.SaveAutoRules`(core.go:232)只写 runtime.json,不重建 Provider;退避状态存在 `Provider.health`(provider.go:24-51),只有 `rebuildRegistry`(core.go:290)会重建。**用户在 WebUI 改完优先级后,被退避锁死的源依旧被跳过。**
|
||
- 线上复现:AUTO 链 4 槽 `[qijiar/gpt-5.5→zen/flash-free→frank/gpt-5.6-sol→zen/nemotron]`,实时请求 `POST /v1/chat/completions {"model":"AUTO"}` → `{"error":{"message":"no provider available","type":"upstream_error"}}`,0ms 返回;其中 2 个源探活确认在线。
|
||
|
||
### P2 源级黑名单(粒度错)
|
||
- 证据:`zen` 因 `429 FreeUsage` 退避(审计 ×4)→ 其 9 个配置模型全部不可调度;`gpt-5.5` 的 qijiar 一次失败 → 源整体跳过。
|
||
- 根因:健康标志是 `(源)` 级而非 `(源, 模型)` 级(provider.go:24);一个模型失败黑掉全源。
|
||
|
||
### P3 永久黑名单(无法自愈)
|
||
- 证据:`frank` 收到一次 `401 API_KEY_DISABLED` → `markPermanent()`(provider.go:48-51, 281-283);此后 AUTO 永远跳过它,且有 30 分钟周期的显式探测持续打它产生 502(502 又推高退避)。
|
||
- 根因:`permanent` 无过期、无重置入口;显式请求路径不检查 `Available()`,照打不误。
|
||
|
||
### P4 并发打满 = 排队 60s,而非立即切换
|
||
- 证据/源码:`Provider.Acquire`(provider.go:306-324)满则等待 `QueueTimeout`(默认 60s,config.go:154);AUTO 逐槽串行,打满的档位拖死整条链。
|
||
- 无 `TryAcquire` 语义,路由层无法区分"忙"与"故障"。
|
||
|
||
### P5 探活与调度状态脱节
|
||
- 探活(GET /models)不污染调度状态是正确设计,但**探活结果没有任何一条通道能恢复调度健康**——线上 qijiar/zen "探活通、调度死" 的矛盾即因此产生。
|
||
|
||
### P6 流式成功不清退避
|
||
- 证据/源码:`ChatStream`(provider.go:406-490)全程无 `reportOK()`;流式源一旦退避,后续成功流也不复位。
|
||
|
||
### P7 两套 AUTO 逻辑并存,配置互相矛盾
|
||
- 线上:config.yaml 里 qijiar priority 95/90/85、deepseek 40/30;runtime AUTO 链却是 `tier1 gpt-5.5 → tier2 zen/flash-free → tier3 frank/gpt-5.6-sol → tier4 zen/nemotron`。
|
||
- `Registry.Resolve("AUTO")`(registry.go:83-121,按"源的最大模型 priority + 健康优先")与 `autoPlans`(chat.go:630,按 runtime 槽位)行为不一致;deepseek 全部模型不在 AUTO 链中,但运维以为"priority 高"会被 AUTO 选中。
|
||
|
||
### P8 错误码策略粗糙
|
||
- `deepseek` 402 欠费 ×17:走 `ReportStatus` 不触发任何退避也不在 UI 提示(402 不属于 401/403/429/5xx 分支),欠费源被无限重试。
|
||
- 400 Invalid schema ×19(客户端工具定义问题)同样无区分。
|
||
- 链全灭时只返回 `no provider available`(12 条),无分槽错误汇总,无法定位是谁挂了。
|
||
|
||
### P9 其他(顺带)
|
||
- 种子 admin key `sk-gw-local-0001` 未更换(README 明确要求换);`listen: 0.0.0.0:8081` 全端口暴露。
|
||
- AUTO 链只有 4 槽,13+ 配置模型不参与 AUTO,且 webui 编辑链后无任何"健康复位"提示。
|
||
- 审计文件 jsonl 无限增长(当前 547KB);`AppendAudit` 每事件一次文件 open。
|
||
|
||
---
|
||
|
||
## 二、目标架构(定稿)
|
||
|
||
### 2.1 分层(横向三层 + 状态基座横切)
|
||
|
||
```
|
||
┌────────────────────────────────────────────────┐
|
||
│ 直连调度 AUTO 调度 │ ← 同级,共享底座
|
||
│ source:model 直派 链快照 + 偏好表 │ 直连失败即返回(不重试)
|
||
├────────────────────────────────────────────────┤
|
||
│ 源抽象层(每源) │
|
||
│ · 信号量 = max_concurrent,TryAcquire 非阻塞 │ ← 唯一"忙/闲"判定
|
||
│ · Chat / ChatStream / Image / 事件上报 │
|
||
├────────────────────────────────────────────────┤
|
||
│ adapter 池层(每适配器,启动预热) │
|
||
│ worker = Σ(使用该适配器的源 × max_concurrent) │
|
||
└────────────────────────────────────────────────┘
|
||
▲ 状态基座(不属任何层)
|
||
ModelState 表 (source,model) → {pref, failCount, cooldownUntil}
|
||
tier 游标表 tier → atomic next(记录上次分配位置)
|
||
```
|
||
|
||
事件流:所有成功/失败/冷却由**源抽象层**上报到状态基座;auto 与直连都只读。直连失败同样写入状态。
|
||
|
||
### 2.2 数据结构
|
||
|
||
```go
|
||
// 调度链:保存时整体新建、原子替换;调度期只读快照
|
||
type Chain struct { Tiers []TierNode } // 按 tier 降序
|
||
type TierNode struct { Tier int; Slots []*Slot }
|
||
type Slot struct { // 静态配置,链构建时冻结配额
|
||
Model, Source string
|
||
Kind string
|
||
Quota int64; Period string; Hours int64
|
||
state *ModelState // 指针 → 跨链存活
|
||
}
|
||
|
||
// 状态基座(key=(source,model),配置移除后回收)
|
||
type ModelState struct {
|
||
pref atomic.Int64 // +1/-5,clamp[-20,+20]
|
||
failCount atomic.Int64
|
||
cooldownUntil atomic.Int64 // 惰性指数退避,无定时器
|
||
}
|
||
```
|
||
|
||
### 2.3 调度算法(AUTO)
|
||
|
||
```
|
||
snap := chain.Snapshot() // O(n) 拷指针,调度期链只读
|
||
for tier := snap.Tiers 降序 {
|
||
初筛 := 剔除 [冷却中 | 配额超限(slot.Quota>0 && windowTokens>=Quota)] 的槽
|
||
if 初筛为空 → 该 tier 顺延,记录原因
|
||
base := cursor[tier].Add(1) // per-tier 原子游标(记录上次位置)
|
||
ordered := 稳定排序(初筛, 按 pref 降序) // 负面模型沉底
|
||
for i := base; i < base+len(ordered); i++ {
|
||
slot := ordered[i % n]
|
||
if slot.state 冷却中 → continue // 硬跳过(复验)
|
||
if !src.TryAcquire() → continue // 忙 = 软跳过(不记分)
|
||
resp, err := src.Chat(slot.model)
|
||
if err != nil {
|
||
src.Release()
|
||
state.RecordFailure() // failCount++, pref-5, 冷却 5s·2^n(≤30min)
|
||
continue // 单请求内不重试已失败槽
|
||
}
|
||
state.RecordSuccess() // pref+1, failCount/cooldown 清零
|
||
return resp, slot.model, slot.source
|
||
}
|
||
}
|
||
return 503 { 各 tier 错误汇总 } // 全部档位失败才报错,可定位
|
||
```
|
||
|
||
关键语义:
|
||
- **主序 = 游标轮转(均衡),偏好只做同起点排序(自适应)**;负偏好"沉底不跳过"——仅当同 tier 无非负候选时才尝试负面模型,成功 +1 自愈。**无永久黑名单、无定时器**。
|
||
- **忙 ≠ 失败**:不扣分、不计冷却;同 tier 全忙 → 有界等待(≤2s 轮询 TryAcquire,尊重 ctx)再顺延,避免高 QPS 时全线降级。
|
||
- **冷却是唯一硬跳过**:`cooldownUntil = lastFail + min(5s·2^failCount, 30min)`,必然到期 → 自愈。
|
||
- 401/403:failCount 一次打高(不设永久位),可被冷却到期/手动重置恢复。
|
||
- 流式:失败→切换仅限首 chunk 前;首块后固定。首块前失败 -5,正常结束 +1。
|
||
- 生图:不进 AUTO 链,直连 `kind:image` 模型,同一状态机制。
|
||
|
||
### 2.4 生命周期(修 P1)
|
||
|
||
| 事件 | 动作 |
|
||
|---|---|
|
||
| WebUI 保存 AUTO 链 | 构造新 `Chain` → 写锁原子替换 → **链内全部 ModelState 冷却清零(偏好保留)** |
|
||
| 增删/编辑源 | `rebuildRegistry` 重建 Provider,ModelState 复用/回收 |
|
||
| (source,model) 移出配置 | 回收其 ModelState(偏好一并丢弃) |
|
||
| 进行中请求 | 不受 swap 影响(入口 Snapshot 已拷贝引用) |
|
||
|
||
### 2.5 直连
|
||
|
||
```
|
||
p := resolve(source, model) // 唯一归属,无 AUTO 逻辑
|
||
p==nil → 400/404;!TryAcquire → 429 busy(快速失败)
|
||
成功/失败 → RecordSuccess/RecordFailure(同样写状态基座),返回,不重试
|
||
```
|
||
|
||
---
|
||
|
||
## 三、实施计划(分阶段上线)
|
||
|
||
### Phase 0 — 止血热修(改动最小,当天可上)
|
||
1. `core.go`:`SaveAutoRules` 成功后调用 `rebuildRegistry()`;给 `Provider` 增加 `ResetHealth()`(清 failCount/permanent/cooldown),rebuild 时顺带重置。
|
||
2. `provider.go`:`ChatStream` 成功(收到 `[DONE]` 或正常结束)调用 `reportOK()`。
|
||
3. 状态页:`/api/status` 增加 `last_backoff`、`permanent` 展示 + admin 可"重置源健康"按钮(调用 ResetHealth)。
|
||
- 验证:改优先级 → AUTO 立即按新链调度,qijiar/zen 场景恢复;回归 `go test`。
|
||
|
||
### Phase 1 — 状态基座落地(provider.go 重构)
|
||
1. 引入 `ModelState`(每 (source,model):pref/failCount/cooldownUntil,原子)。
|
||
2. `Provider` 增加 `TryAcquire(ctx) error`(非阻塞)与 `RecordFailure(model, code)` / `RecordSuccess(model)`;`ReportStatus` 改为按模型记账与 401/403 不再永久化。
|
||
3. 删除 `permanent` 语义(由有界冷却 + 重置通道取代)。
|
||
- 验证:单测(退避按模型隔离、429/401 行为、TryAcquire 满即返)。
|
||
|
||
### Phase 2 — 调度层重写(scheduler.go + chat.go)
|
||
1. 新增 `Chain/TierNode/Slot` 与 Snapshot/原子替换;`autoPlans`、`rotateSameTier` 删除,`singleChatAuto`/`streamChatAuto` 按 2.3 伪码重写。
|
||
2. `Registry.Resolve` 移除 AUTO 排序/健康优先职责(只留归属 + pin 解析;`AUTOChain`/`Default` 不再用于调度)。
|
||
3. 503 响应携带分槽错误汇总(哪个 tier 哪个源什么错)。
|
||
4. 同 tier 全忙时 ≤2s 有界等待再降级。
|
||
- 验证:scheduler 单测(tier 分桶、游标均衡、偏好沉底自愈、配额初筛、忙不记分);e2e 增加"上游 429 → 切同 tier → 再切下 tier"、"编辑优先级后退避清零"用例。
|
||
|
||
### Phase 3 — 收尾
|
||
1. UI:优先级页展示链上模型的冷却/偏好状态;状态页按模型展示健康。
|
||
2. WebUI 保存链时前端提示"健康状态已复位"。
|
||
3. 审计:错误码分类(402 欠费、400 schema 计入独立统计),jsonl 轮转(按天/大小)。
|
||
4. 运维:更换 admin key;`listen` 收敛到内网地址(0.0.0.0:8081 → 127.0.0.1 或内网 IP + 反向代理)。
|
||
|
||
### 回归与上线
|
||
- 构建:`go build -tags luajit`(生产机 Debian amd64 已具备工具链,可直接在 60 上编译);本地 Windows 需先补 LuaJIT 库才能链接。
|
||
- 测试:`go test -tags luajit ./...` 全绿后按 Phase 0→3 灰度,每个 Phase 观察审计中 `no provider available` 计数与 502 分布。
|
||
- 观察指标:`no provider available` 计数归零;qijiar/zen 由 `available=false` 恢复;`zen 429` 触发时仅 zen 对应模型短冷却,deepseek/qijiar 不受牵连。
|
||
|
||
---
|
||
|
||
## 四、与既有代码的对应改动文件
|
||
|
||
| 文件 | 改动 |
|
||
|---|---|
|
||
| internal/provider/provider.go | health→ModelState 表;TryAcquire;ReportStatus/ResetHealth;ChatStream reportOK |
|
||
| internal/provider/registry.go | 删除 Resolve(AUTO) 排序/健康逻辑,保留归属/pin |
|
||
| internal/scheduler/scheduler.go | 新增 Chain/Slot/游标/偏好排序迭代器 |
|
||
| internal/gateway/chat.go | 删除 autoPlans/rotateSameTier,重写单次/流式 AUTO;错误汇总 |
|
||
| internal/core/core.go | SaveAutoRules 原子换链 + 冷却清零;ModelState 生命周期 |
|
||
| internal/gateway/api.go / keys.go | 状态页/优先级页 UI 展示与重置接口 |
|
||
| e2e/e2e_test.go 等 | 新场景用例 |
|
||
|
||
---
|
||
|
||
## 五、实施状态追踪(每项改动后更新)
|
||
|
||
### Phase 0 — 止血热修
|
||
|
||
- [x] **P0-1(2026-08-10)`core.go`**:`SaveAutoRules` 持久化后调用 `rebuildRegistry()`——新建 Provider 即退避归零,**编辑优先级立即生效**(修 P1 主诉);新增 `Core.ResetHealth()` 供管理端手动清退避。
|
||
- [x] **P0-2(2026-08-10)`provider.go`**:新增 `Provider.ResetHealth()` / `Provider.HealthInfo()`(failCount/cooldownUntil/permanent 只读暴露);`ChatStream` 流式正常结束(`[DONE]`/EOF、非客户端断开、非读错误)调用 `reportOK()`——流式成功可恢复退避(修 P6)。
|
||
- [x] **P0-3(2026-08-10)`registry.go` + `server.go`**:`SourceStatus` 增加 `fail_count`/`backoff_until`/`permanent` 字段(状态页可分辨"探活通但调度退避");新增 `POST /api/status/reset`(admin 专属,写审计)。
|
||
- [x] **P0-4(2026-08-10)验证**:`go build ./internal/...` 通过;`go test ./internal/config/...` 通过(Windows 缺 LuaJIT 库,带 cgo 的包无法本地链接,待生产机验证)。
|
||
|
||
> 说明:P0-1 采用"重建 Provider"实现退避归零(非目标架构的 ModelState 粒度),属临时止血;Phase 1 引入 per-(源,模型) ModelState 后,`ResetHealth` 语义将迁移到模型级。
|
||
|
||
### Phase 1 — 状态基座落地
|
||
- [x] **P1-1(2026-08-10)`provider.go`**:`ModelState` 表落地(pref±1/-5、failCount、cooldownUntil 原子,惰性求值无定时器);新增 `ErrBusy`、`TryAcquire`(非阻塞,满即返)、`ModelAvailable(model)`、`RecordFailure(model, code)` / `RecordSuccess(model)`、`Provider.Pref(model)`;`ReportStatus` 改按模型记账(401/403 → 打满档冷却 + 双倍偏好惩罚,**不再永久化**;5xx/429 → 指数退避;400/402 不惩罚);`ResetHealth` 清全源模型状态;`HealthInfo` 源级聚合(permanent 恒 false);`Chat/ChatStream/Image` 全部按命中模型记账 + 内部 `Acquire` 换 `TryAcquire`(忙即 429,不再排队 60s,修 P4);流式干净收尾 `RecordSuccess`(修 P6)。
|
||
- [x] **P1-2(2026-08-10)`chat.go`**:直连/生图路径 `errors.Is(err, ErrBusy)` → HTTP 429(`upstreamErrStatus`);AUTO 槽改用 `ModelAvailable(slot.model)` 模型级冷却跳过(frank 401 后其模型不被 AUTO 反复打,修 P3 的一半——永久黑名单已随 P1-1 移除)。
|
||
- [x] **P1-3(2026-08-10)单测(`provider_test.go`)**:模型级退避隔离(m1 失败 m2 照常)、401 → failCount 打满 capN + 冷却 ~30min + 偏好 -10(非永久,reset 即恢复)、单次失败冷却 ≈5s 且成功复位 +1、TryAcquire 满即返、Chat 忙时快速 ErrBusy、流式成功清理冷却(P6 回归)。
|
||
- [x] **P1-4(2026-08-10)本地验证**:`go build ./internal/...`、`go vet ./internal/...`、`go test ./internal/config/...` 全绿;provider/gateway 测试因 Windows 缺 `-llua` 仅静态检查通过,待生产机 `-tags luajit` 全量跑(同 P0-4 约束)。
|
||
- [ ] 生产机(192.168.2.60)`go test -tags luajit ./internal/provider/...` 回归 — 待 Phase 2/3 完成后一并部署验证。
|
||
|
||
### Phase 2 — 调度层重写
|
||
- [x] **P2-1(2026-08-10)`scheduler.go`**:`Chain/TierNode/Slot/Rule` 落地(tier 降序;槽静态配置,同 tier 保持配置序;per-tier `next atomic.Int64` 游标始 -1,首请求从配置序开始);`BuildChain` 丢弃 provider 解析失败的槽;`runTier` 按 2.3 语义重写——冷却复验硬跳过、busy 软跳过不记分、**硬失败记账后同档继续下一槽**(单请求不重试已失败槽)、整档遍历完才顺延;`chainDrive` 初筛(配额/冷却)→ 同 tier 按 `Pref` 稳定降序排序 → `NextStart()` 起步轮转 → 全忙/全冷却有界等待(`busyWait` 2s/`busyPoll` 100ms,期间刷新冷却)再降级;`ChainErr` 携带各档 `TierError` + 顺延原因,Error() 输出 `all auto tiers failed: tier N src/model: err; ...`;`ChainChat/ChainChatStream` 返回 (resp, source, model, err);直连 `Chat/ChatStream/Image` 保持不变;**scheduler 不再 import provider**(`FromRegistry` 移入 gateway 为 `toScheduler`),scheduler 单测不拉 LuaJIT 链接。
|
||
- [x] **P2-2(2026-08-10)`registry.go` + `core.go`**:`Registry.Resolve` 移除 AUTO 排序/健康优先(AUTO→按配置序全量仅用于生图/tool 锚定;未知模型→nil→gateway 404;`AUTOChain`/`Default` 删除);`Core` 持 `autoChain atomic.Pointer[scheduler.Chain]` + `AutoChain()`;`buildAutoChain`(过滤 image-kind 与已不存在槽,**槽 Source 规范化为所有权源**使汇总/audit/配额窗口键一致)随 `rebuildRegistry` 与 `SaveAutoRules` 重建;`SaveAutoRules` = 持久化 → 原子换链 → **链内每槽 `ResetModelCooldown`(偏好保留,不重建 Provider)**——修 P1 主诉(编辑优先级立即生效)。
|
||
- [x] **P2-3(2026-08-10)`chat.go`**:AUTO 分支改走 `AutoChain()`+`quotaExhausted`(`Quota<=0` 不过滤;`AutoPeriodSeconds`+`stats.WindowTokens` 实时判定);`singleChatAuto`/`streamChatAuto` 按 `ChainChat`/`ChainChatStream` 重写,总失败在**任何 SSE 字节前**写 JSON;`upstreamErrStatus`:`ChainErr`→503(错误消息即分档汇总)、`ErrBusy`→429、其余→502;失败记录取首个 `TierError` 填 audit source/model;直连无候选→404 `model_not_found`。
|
||
- [x] **P2-4(2026-08-10)单测(`scheduler_test.go`)**:tier 分桶/降序、同 tier 游标轮转交替(s1,s2,s1,s2)、负偏好沉底仍可达(硬失败换槽后由负面槽承接)、busy 跳过不记分、整档全忙有界等待(实测 <1s)后降级、配额耗尽槽不调度、全灭 503 汇总(Tiers+Skipped 文本断言)、流式首 chunk 前失败换槽、游标首帧从 0 起。
|
||
- [x] **P2-5(2026-08-10)本地验证(Windows 已补齐 Lua)**:把 golua 自带 Lua 5.1 头对应的源码编成 `liblua.a` 放入 golua 模块目录(golua 官方 Windows 做法),本机 `go build/vet/test ./...` 全绿——**provider/gateway/lua/e2e 全部首次真正跑通**,并借此揪出三处从未被发现的存量问题:① `RecordFailure(auth)` 只把 failCount 上限用于冷却算式、未落盘计数器(已修:auth 时 `failCount.Store(backoffCapN)`);② gateway 测试种子 key 未进 runtime store 导致全 401(已修:测试 cfg 补 `GatewayKeys`);③ e2e failover 用例只有一个 chat 源、AUTO 全灭必然 503(已修:新增第二 chat 源 `fallback`,真故障转移);e2e `buildBinary` Windows 回退无 tag 构建(bundled Lua,透传适配器行为一致),生产机仍优先 `-tags luajit`。
|
||
- [x] **P2-6(2026-08-10)Phase 2 场景测试**:gateway 新增——同 tier 硬失败顺延承接(`TestChatAutoChainTierFailover`)、全灭 503 含 `a/a-m` 分档汇总(`TestChatAutoChain503Summary`)、配额耗尽槽跳过且不再打上游(`TestChatAutoQuotaSkip`)、`PUT /api/auto` 后冷却立即复位并恢复调度(`TestAutoSaveResetsCooldown`,修 P1 回归);e2e 新增 `TestEndToEndAuto503`(真实二进制 503 汇总)。全部本地跑通。
|
||
- [ ] **P2-7**:生产机(192.168.2.60)`go test -tags luajit ./...` 最终回归 + 灰度部署(luajit 与 bundled Lua 的适配器行为差异由生产验证兜底)。
|
||
|
||
### Phase 3 — 收尾
|
||
- [x] **P3-1(2026-08-10)UI 链上健康展示**:`GET /api/auto` 增加 `states`(`Core.AutoSlotStates` 遍历当前 Chain 槽 × `Provider.ModelHealthInfo`:pref/failCount/cooldownUntil/cooling);优先级页每块按 `model|source` 渲染徽标(冷却红/失败橙×N/±偏好蓝,title 说明);状态页新增"状态码分布"卡片。
|
||
- [x] **P3-2(2026-08-10)保存链健康复位提示**:`saveSort` toast 变更为 `排序已保存并热重载 · 链上冷却已复位`(zh/en)。
|
||
- [x] **P3-3(2026-08-10)审计分类与 jsonl 轮转**:`Stats.byStatus map[int]*Stat`(402 欠费/400 schema 等按状态码独立计数,不触发 provider 退避,`Snapshot.by_status` 有序输出);audit 文件超 `auditRotateBytes`(64MB, var 可测) 轮转 `rename <path>.<unix>.old` 并保留最新 `auditKeepOld`(10) 份——`rotateAuditLocked` 持 mu 在 `Record`/`AppendAudit` 内触发。
|
||
- [x] **P3-4(2026-08-10)运维项(代码部分)**:`main.go` 启动告警——gateway_keys 为空 / 命中种子 key(sk-gw-local-0001 等)提示轮换、listen 绑定 0.0.0.0/:: 提示收敛内网。生产实践(换 admin key、内网绑定)随本次上线执行。
|
||
- [x] **P3-5(2026-08-10)测试**:新增 `stats_test.go`(by_status 断言、轮转→保留上限→Record 路径轮转);gateway 新增 `TestAutoStatesReportChainHealth`(states 契约:失败后 fail_count>0+cooling,PUT 复位归零);`go vet ./...` + `go test ./...` 全绿。
|
||
- [x] **P3-6(2026-08-10)WebUI 右键菜单无法关闭(用户实测)**:根因——`showCtx` 创建菜单后从未赋值 `ctxEl`(`hideCtx()` 恒为空操作),任何路径(点菜单项/点外部/二次右键)都关不掉;补 `ctxEl = w`,优先级页与密钥页共用 `showCtx` 一并修复(这也是最初版本就存在的 bug)。
|
||
- [ ] **P3-7**:推送 origin → 生产机 pull → `-tags luajit` 全量回归(含 P2-7)→ 部署 `/usr/local/bin/llmsproxy` + 重启 service → 观察。
|
||
- [x] **P3-8(2026-08-11)审计回放修复(用户实测)**:重启后统计只剩最后 3000 行≈5.3M tokens,历史 273M 消失、且记录里全是 access 脏行——根因 `LoadAudit`:① 只取文件尾 3000 行(而 92% 行是每 3s 的 access 事件);② access/事件行(type 空)被当请求灌入聚合;③ Scanner 默认 64KB 截断风险。修复:回放**全部真实请求行**进聚合(恢复 totals/配额窗口),仅视图环形缓冲限 maxRecs;跳过 type 空的行;`sc.Buffer` 抬到 16MB。新增 `TestLoadAuditFullReplay`(access/坏 JSON/超大行混合回放断言)。 |