feat: opencode zen adapter + first-run config generation, fix stats/stream bugs

- adapters/opencode.lua: opencode.ai zen free pool adapter — sends the
  opencode client User-Agent (zen fingerprints clients by UA; non-official
  clients hit FreeUsageLimitError); pairs with api_key: public
- config: no config file ships in the repo; first run generates a default
  config at the -config path with a random admin key, loopback listen and a
  keyless zen source (config.EnsureDefault); remove config.example.yaml
- lua: seed bundled adapters from the embedded FS instead of a hardcoded
  name list
- ui: widen model kind select (chat was clipped to 'cha')
- phase 5 bugfixes: stats ms/s bucket mixing, cleanScopes nil, ctx.Err
  guards, direct-path ModelAvailable, empty stream body failure,
  bestImageModel rewrite, transform failure recording, Core.mu, timer,
  effective model for tool-calls
This commit is contained in:
JianFeeeee
2026-08-13 12:25:07 +08:00
parent d06210204b
commit 2bc1d0e67a
22 changed files with 910 additions and 148 deletions

36
plan.md
View File

@ -246,4 +246,38 @@ p==nil → 400/404!TryAcquire → 429 busy快速失败
- [x] **P4-72026-08-11AUTO 链 tier 序 + 审计导出修复 + UI 密钥视图(今日会话)**:① `BuildChain` 排序由降序改升序tier1=最高优先级先试,修 P10-5 中的序颠倒),单测同步;② 统计/CSV 全量limit 20000、`Stats.AuditRecords` 读磁盘含 `*.old`、key_names 掩码键、非 admin 过滤掩码);③ UI 卡片滚动区修正 + 密钥视图 toggle再点同一密钥回全局+ `rec-exit` 退出入口;④ WebUI 测试"所有档位失败"观察AUTO 链降级正常opus 必死→deepseek 撞 zen 间歇故障→frank key 失效→nemotron 404 全灭 503
- [x] **P4-82026-08-11调度延滞排查结论**homeagent AUTO 请求"看起来停在 opus"实为调度正常降级——完整 ChainErr 列出 tier1→2→3→4 全部尝试,审计只记 `ce.Tiers[0]`(首档)故 UI 显 opuszen 源 `opencode.ai/zen/v1` 直连即 403 `[server_error] Upstream response was not valid JSON`(间歇)/ nemotron 404确认为上游源自身问题非网关透传或 homeagent 适配器。
- [x] **P4-92026-08-11WebUI 统计可视化**(多轮迭代定稿):① 模型用量%改为占总请求的比例(`r.reqs/total`,替代原"相对最大模型"的误导性 100%/97%);② 顶部五宫格统一为同构卡片——**标题 + 大数值 + 副标题 + 86px 图表区**:活跃请求=折线 sparkline实时、总请求=按天柱状图、tokens=各模型占比堆叠条(大数值示总用量)、平均延迟=折线 sparkline实时、状态码=按码值堆叠条 + chip 副标题;全部 canvas 2D 手绘(无第三方库),去掉灰色空壳占位与独立全宽状态卡/冗余副标题。
- [x] **P4-102026-08-11claude "Third-party apps now draw from your extra usage" 400 结论**:该报错为 **Anthropic 官方配额提示**(账号第三方 app 用量额度耗尽,提示到 Anthropic usage 设置充值),**非流量特征被识破**;伪装流量/换 adaptive 适配器解决不了(请求已被 Anthropic 接受并按账号用量计费)。且 qijiar+openai 适配器下 claude 模型本就能 200 成功(审计 16 次,含 claude-opus-4-8/claude-sonnet-5AUTO 链/直连的 claude 失败多为上游 `model_not_found`(无 distributor 渠道)、`Concurrency limit exceeded``extra usage` 配额——均为上游账号/渠道级问题,非网关。若确需走 Anthropic 原生 `/v1/messages`,可为其源切换内置 `anthropic` 适配器(仅改变量 wire 格式,不影响配额)。
- [x] **P4-102026-08-11claude "Third-party apps now draw from your extra usage" 400 结论**:该报错为 **Anthropic 官方配额提示**(账号第三方 app 用量额度耗尽,提示到 Anthropic usage 设置充值),**非流量特征被识破**;伪装流量/换 adaptive 适配器解决不了(请求已被 Anthropic 接受并按账号用量计费)。且 qijiar+openai 适配器下 claude 模型本就能 200 成功(审计 16 次,含 claude-opus-4-8/claude-sonnet-5AUTO 链/直连的 claude 失败多为上游 `model_not_found`(无 distributor 渠道)、`Concurrency limit exceeded``extra usage` 配额——均为上游账号/渠道级问题,非网关。若确需走 Anthropic 原生 `/v1/messages`,可为其源切换内置 `anthropic` 适配器(仅改变量 wire 格式,不影响配额)。
### Phase 5 — 2026-08-13 全量复查核实 & 修复(本轮会话)
> 三路并行审查provider/registry/scheduler、gateway、config/store/core+ 逐项对照源码核实,全部条目经本人亲自读取代码确认。
#### 5.1 已核实的未修正 bug均属实
| 编号 | 级别 | 问题 | 证据 |
|---|---|---|---|
| H1 | 高 | **stats 窗口单位错乱**`Req.Time` 为毫秒chat.go:572 `UnixMilli``stats.go:220` `h := r.Time/hourSec`hourSec=3600 秒)→ 桶宽实为 3.6s`WindowTokens`stats.go:352`h*hourSec >= cut`cut 为秒)毫秒恒 ≥ 秒 → **任何 sec>0 窗口返回全部记录**;剪枝 960 桶 ≈ 仅保留 57.6minstats.go:227-233。槽配额chat.go:326/key 配额scopeTokens/UI 周月曲线全部失真LoadAudit 回放同路径;`stats_test.go:125` 断言 372=全计,固化错误行为 | 逐行确认 |
| H2 | 高 | **空 scope 语义分裂**`cleanScopes`core.go:276-288返回非 nil 空切片 → CreateKey/UpdateKey 后 `allowedModels` 非 nil → 空 scope 无匹配 → **403 锁死**config 序列化 `models,omitempty`config.go:275→ 重启后 nil → **全部放行**。另 `UpdateKey`core.go:245对未传 models 的 PUT **静默清空已有 scope** | 逐行确认 |
| H3 | 高 | **客户端取消/断连被记失败**Chatprovider.go:611-613/Stream 首块前(:666-669/Image:751-753`err != nil` 无条件 `RecordFailure`,无 `ctx.Err()==nil` 守卫 → SDK 超时/关页把健康源反复误冷却、退避翻倍至 30min。流式中段:724已有守卫 → "断开≠成败"只实现一半 | 逐行确认 |
| H4 | 高 | **直连路径无视冷却P3 残留)**AUTO 走 `chainDrive``ModelAvailable`scheduler.go:242直连 Chat/ChatStream/Imagescheduler.go:334-398不查 → 401 打满 30min 后直连照打,`RecordFailure(auth)`provider.go:79-101每次重写冷却截止 → **无限续期**"无永久黑名单"被持续直连流量打破 | 逐行确认 |
| M1 | 中 | **AUTO 槽模型名大小写不一致**`buildAutoChain` 保留规则原文core.go:415Image 过滤用精确匹配 :406`ProviderForSlot` 用 EqualFold`runTier:194` 原样下发 → `ModelFor` 精确匹配失败回退 `bestChatModel`provider.go:214-222**静默发错模型**`ModelAvailable` 未知模型恒 true:303-310→ 冷却失效配额键chat.go:326 vs 实际记账模型)错位 → 配额永不生效 | 逐行确认 |
| M2 | 中 | **200-空流记为成功**ChatStream 无 chunk 计数provider.go:678-727`:724` 对"0 chunk 干净 EOF"照记 `RecordSuccess`→ 空成功流不降级 | 逐行确认 |
| M3 | 中 | **AUTO 生图错模型(仅混合源触发)**`Image`provider.go:738`ModelFor``bestChatModel`(跳过 image 模型)→ 混合源chat+image把 chat 模型发往 /images/generations`effectiveImageModel`chat.go:447-459只用于配额/scope 检查不修正请求体 | 逐行确认 |
| M4 | 中 | **transform/unmarshal 失败不上账**Chatprovider.go:619-626/Image:761-771适配器产出坏数据直接 return → **适配器故障源永不退避**,每请求全额重打 | 逐行确认 |
| M5 | 中 | **Core cfg 并发写无锁(潜伏)**`Core``autoChain` 原子core.go:24-31`c.cfg.Keys/Sources/Auto` 裸写CreateKey:228/UpdateKey:237-251/SaveAutoRules:295/AddSource:516-525与请求路径 `FindKey:201-208`/`mergedSources` 并发 → racecore 包零单测,`go test -race` 全绿属"无证据"非"安全" | 确认+race 全绿 |
| L1 | 低 | busyWait 每轮 `time.After` 不 Stopscheduler.go:273 | 确认 |
| L6 | 低 | pinned`src:model`tool-call 请求:`resolveCands` 工具分支chat.go:95-107用原始带前缀串 `ModelFor`(不解前缀)→ 回退 bestChatModel | 确认 |
#### 5.2 修复计划(按序推进,每项完成即勾选)
- [x] **P5-1H1stats 窗口单位修正**`aggregateLocked`stats.go:220毫秒转秒后再分桶`h := r.Time/1000/hourSec`窗口语义0=全量;>0 滑动秒窗整桶计引入截断误差≤1h注释说明剪枝阈值 960 小时桶 ≈ 40 天保留(注释注明);重写 `TestLoadAuditFullReplay`(相对时间戳 + 全量/3h/24h=372、1h ∈ [2,372)、m2=10 边界断言,旧断言 372=全计恰固化错误行为);新增 `TestStatsByStatus` 不受影响。
- [x] **P5-2H2空 scope 归一化**`cleanScopes` 空输入返回 nil进程内=重启后语义一致);`UpdateKey` 仅当 `models != nil` 才覆盖 scope未传保留、显式 `[]` 清空);单测 `TestCleanScopesNilForEmpty`/`TestCreateKeyWithoutScopeUnrestricted`/`TestUpdateKeyPreservesScopeWhenOmitted`
- [x] **P5-3H3断开不记失败**Chat/ChatStream 首块前/Image 三处 `err != nil` 分支改为 `err != nil && ctx.Err() == nil``RecordFailure`;新增 `TestChatClientCancelNotRecorded`(注意:测试 handler 不能依赖服务端 ctx 取消传播——HTTP/2 下客户端取消不会触发服务端 ctx.Done用显式 release 通道)。
- [x] **P5-4H4直连冷却检查**`Scheduler.Chat/ChatStream/Image` 调用前查 `ModelAvailable`,冷却中记 "cooling down" 错误顺延下一候选;全部冷却即失败返回——与 AUTO 一致"冷却=唯一硬跳过"auth 401 冷却不再被直连流量续期;新增 `TestDirectSkipsCooledCandidate`(冷却候选零命中 + 全冷报错。已知取舍AUTO 生图直连的可用性检查用 `ModelFor`chat 模型)近似,精确 image 模型冷却检查待后续。
- [x] **P5-5M1槽模型名规范化**`Provider.ModelIDFold`(大小写无关匹配)+ `buildAutoChain` 用其回填真实模型 ID 到 `r.Model`image-kind 过滤也基于规范化后的精确 ID冷却/配额/上游模型三者键一致;单测 `TestSaveAutoRulesNormalizesModelCase`GPT-4O → gpt-4o
- [x] **P5-6M2空流不记成功**ChatStream 统计有效 chunk 与 `[DONE]`;干净 EOF 且 0 chunk 且无 `[DONE]``RecordFailure`,有 `[DONE]` 或 ≥1 chunk → `RecordSuccess`;新增 `TestChatStreamEmptyBodyNotSuccess`
- [x] **P5-7M3AUTO 生图选 image 模型**`bestImageModel()`Kind==image 最高 priority兜底 Models[0]`Image` 对 AUTO/未知模型**改写请求体**(克隆 req 回填 image 模型 ID非仅记账新增 `TestImageAutoUsesImageModel`(混合源断言上游收到 img 模型而非 chat 模型——初版只改记账导致 body 仍为 AUTO测试当场抓出
- [x] **P5-8M4transform 失败记账**Chat/Image 的 transform_response/unmarshal 失败分支补 `RecordFailure(model,0)`(组合 ctx.Err 守卫)。
- [x] **P5-9M5Core 加锁**`Core.mu sync.Mutex` 包裹全部 cfg 读写公开方法ListKeys/FindKey/CreateKey/UpdateKey/DeleteKey/AutoRules/SaveAutoRules/Sources/AddSource/RemoveSource/Reload私有内部函数不持锁防重入`Config()` 仅测试读 Path 无需锁;新增 `core_test.go`(含 `TestConcurrentKeyMutation` 16 并发增删查)——`-race` 全绿。
- [x] **P5-10L1/L6**busyWait 改 `time.NewTimer`+`Reset`/defer Stop修 20+ 计时器泄漏);`resolveCands` 工具分支改用剥前缀后的 `effective``ModelFor`pinned tool-call 不再回退 bestChatModel
- [x] **P5-11**`go build ./...``go vet ./...``go test ./...`(含 e2e全绿`go test -race`core/scheduler/gateway全绿。待推送 + 生产机 `-tags luajit` 回归部署P3-7 流程)。