mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-12 20:27:23 +00:00
## 问题(生产实测,非猜测)
用户问「为什么 WebUI 默认显示插件全部日志」。用 CDP 打开真实 UI + 读
`/channels/status` 实测,发现是**三个问题叠加**,而表层症状是"看到的全是噪音":
### ① 归属错误:全局日志渲染在「单个渠道详情」里
`plugin.state.log` 是**单一全局数组**(`logEvent` 写入的条目只有 `{at, msg}`,
**连 platform 字段都没有**),但这个 `<ul id="ch-log">` 的 DOM 位于
`#ch-detail`(点开某渠道才显示)⇒ 用户点开 buddy,看到的却是
trae/minimax/qoder 的日志混在一起,**无从区分**。
### ② 50 条里 48 条(96%)是周期任务噪音
实测分类:`已同步源…` 11×、`模型刷新失败…` 9×、`模型目录已刷新` 9×、
`能力元数据未刷新` 9×、`自动领取…` 9× —— **唯一模式只有 8 种**。
原因:`refresh_models`(2h) + `auto_claim`(2h) + `refresh_creds`(0.5h) 三个
`first_due_now` 任务每次跑写 3~5 条 ⇒ **约 2 小时就把 50 条环形缓冲冲满**,
于是真正的用户动作(如「账号删除」)被挤到看不见。
### ③ 无级别、无去重、无折叠 → 噪音与错误同权重并排
## 修法(用户选 C+A)
**C —— 修正归属**:把「运行日志」从 `#ch-detail` 移出,成为**常驻的独立卡片**
(`#ch-log-card`)。判据断言它与渠道详情是**兄弟节点**(div depth=0),
不是父子 —— 否则"点 buddy 看到 trae"会以另一种形式回来。
**A —— 加级别与过滤**:
- `logEvent(msg, level, platform)`:level ∈ info|task|warn|error。
定时任务的**常规**结果记 `task`(前端默认折叠),只有异常升为 warn/error。
- **紧邻同文折叠**:与 `log[1]` 完全同文时不新增条目,只 `n=n+1` 并刷新时间。
⚠️ 只对**紧邻**条目折叠 —— 否则一条每小时出现的消息会不断"跳"到最前,
时间线失去意义。前端显示成 `×N`,既保留"重复了多少次"又不刷屏。
- 三个视图开关:级别门槛(全部/警告及以上/仅错误)、显示例行任务、
按当前选中渠道过滤(不带 platform 的跨平台汇总只在「全部渠道」出现)。
- `refresh_models` 的失败改为**逐平台各记一条**(原先 N 个挤成一条汇总,
既不属于 trae 也不属于 minimax ⇒ 点进任何单渠道都看不到自己的问题)。
## 判据(3 条 + 变异 2/2)
- `TestChannelsLogEventCarriesLevelAndPlatform`:签名三参 / 条目带两字段 /
紧邻折叠 / **折叠对象必须是 log[1]**
- `TestChannelsPeriodicTaskLogsAreDowngraded`:逐条咬住**具体调用点**被降级
(而不是"存在 task 这个词"——那样一处就为真,测不出漏降级的噪音源)
- `TestChannelsLogPanelHasFilters`:日志卡片与渠道详情是**兄弟**(div depth=0)
- 变异:去掉折叠 / 把卡片放回详情内 → 均报红
## 顺带修好的环境事故(重要)
排查中全量测试突然大批失败(`loopback 未就绪(未取得端口)`)。我一度以为是
自己的改动,**`git stash` 后在干净版本上复现 → 预先存在**,继续追到根因:
**`/tmp/bisect.py`(另一个 agent 的临时脚本)遮蔽了 Python 标准库的 `bisect`**
—— 而 `tempfile` → `random` → `bisect`,于是**任何 `import tempfile` 都炸**,
回环接收器(python3)起不来。`sys.path` 含 `/tmp` 就是这个后果。
已移走(`/tmp/_shadowed-stdlib-conflicts/`,不删除以免打断那个 agent),
Python 与全部测试立刻恢复。
⇒ **教训**:临时脚本**绝不能**在 `/tmp` 下用 stdlib 模块名命名;
它会让全机 Python 静默失效,而症状("回环未就绪")与原因毫无关联。
141 lines
5.8 KiB
Go
141 lines
5.8 KiB
Go
package core
|
||
|
||
import (
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// ★ 插件日志面板:级别 + 归属 + 同文折叠(否则用户动作被 96% 的周期噪音淹没)。
|
||
//
|
||
// 真实问题(生产实测):50 条日志里 **48 条(96%)** 是三个定时任务
|
||
// (refresh_models 2h / auto_claim 2h / refresh_creds 0.5h,全为 first_due_now)
|
||
// 的重复回音,唯一模式只有 8 种 ⇒ 约 2 小时就把 50 条环形缓冲冲满一轮,
|
||
// **真正的用户动作**(如「账号删除」)被挤到看不见。
|
||
//
|
||
// 三个结构性修法,每个都在源码里可断言:
|
||
//
|
||
// ① logEvent 支持 level(task/warn/error),前端默认折叠 task
|
||
// ② logEvent 支持 platform,前端按选中渠道过滤(修"点 buddy 看到 trae"的归属错误)
|
||
// ③ 同文折叠:紧邻同文不新增条目,只 n=n+1
|
||
func TestChannelsLogEventCarriesLevelAndPlatform(t *testing.T) {
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
|
||
// ① 签名必须收 level 与 platform(三参)
|
||
if !regexp.MustCompile(`local function logEvent\(msg, level, platform\)`).MatchString(s) {
|
||
t.Error("logEvent 必须接收 (msg, level, platform) —— 否则级别与渠道归属都无从表达")
|
||
}
|
||
// ② 写入的条目必须带上这两个字段
|
||
for _, field := range []string{"level = level or", "platform = platform"} {
|
||
if !strings.Contains(s, field) {
|
||
t.Errorf("日志条目必须写入 %s", field)
|
||
}
|
||
}
|
||
// ③ 同文折叠:紧邻同文时只加计数
|
||
if !strings.Contains(s, "top.n = (top.n or 1) + 1") {
|
||
t.Error("必须实现紧邻同文折叠(否则周期任务的重复回音会把 50 条缓冲冲满)")
|
||
}
|
||
// ④ 折叠必须只针对**紧邻**条目 —— 否则一条每小时出现的消息会不断跳到最前,
|
||
// 时间线失去意义。断言比较对象是 log[1](栈顶)而不是任意条目。
|
||
if !strings.Contains(s, "local top = log[1]") {
|
||
t.Error("折叠必须只比较**紧邻**的上一条(log[1]);对任意条目折叠会让时间线错乱")
|
||
}
|
||
t.Log("✓ logEvent 具备 level / platform / 紧邻同文折叠")
|
||
}
|
||
|
||
// ★ 周期任务的**常规**结果必须是 task 级(默认折叠),异常必须是 warn/error。
|
||
//
|
||
// 判据咬的是"这些具体调用点被降级了",而不是"存在 task 这个词" ——
|
||
// 后者只要有一处就为真,测不出"某个噪音源忘了降级"。
|
||
func TestChannelsPeriodicTaskLogsAreDowngraded(t *testing.T) {
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
|
||
// 这些是「每次跑都成功」的常规回音 —— 必须 task 级
|
||
routine := []string{
|
||
`logEvent("模型目录已刷新(" .. updated .. " 个平台)", "task")`,
|
||
`logEvent("模型能力元数据已刷新(" .. tostring(cerr or 0) .. " 条)", "task")`,
|
||
`logEvent("模型刷新:跳过 " .. cooled .. " 个冷却中的平台", "task")`,
|
||
}
|
||
for _, r := range routine {
|
||
if !strings.Contains(s, r) {
|
||
t.Errorf("周期任务的常规结果必须降级为 task(否则会淹没用户动作):\n %s", r)
|
||
}
|
||
}
|
||
// 这些是异常 —— 必须 warn(不能被一起折叠掉)
|
||
for _, pat := range []string{
|
||
`logEvent("能力元数据未刷新: " .. tostring(cerr):sub(1, 120), "warn")`,
|
||
`logEvent("能力元数据刷新异常: " .. tostring(cres):sub(1, 120), "warn")`,
|
||
`logEvent("凭据续期失败 " .. #failed .. " 个:`,
|
||
} {
|
||
if !strings.Contains(s, pat) {
|
||
t.Errorf("异常必须保持 warn(不能被当常规噪音折叠):%s", pat)
|
||
}
|
||
}
|
||
// 「已同步源」是周期重同步的常规回音 —— 降级为 task
|
||
if !strings.Contains(s, `logEvent("已同步源 " .. name .. "(" .. #models .. " 模型)", "task", provider)`) {
|
||
t.Error("「已同步源」是周期重同步的常规回音,必须 task 级且带平台归属")
|
||
}
|
||
t.Log("✓ 周期噪音已降级,异常保持 warn")
|
||
}
|
||
|
||
// ★ 前端的三个过滤维度必须都在(级别门槛 / 例行折叠 / 按渠道过滤)。
|
||
func TestChannelsLogPanelHasFilters(t *testing.T) {
|
||
src, err := readBundledChannels(t)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := string(src)
|
||
|
||
// ① 日志卡片必须**独立于渠道详情**(C 的核心:修正归属)
|
||
// 判据:日志卡片的 DOM 不是 ch-detail 的子元素。
|
||
// 做法:取 ch-detail 开始到 ch-log-card 之间的片段,要求中间出现过
|
||
// ch-detail 的闭合(即两者是兄弟而非父子)。
|
||
iDetail := strings.Index(s, `id="ch-detail"`)
|
||
iLogCard := strings.Index(s, `id="ch-log-card"`)
|
||
if iDetail < 0 || iLogCard < 0 {
|
||
t.Fatal("找不到 ch-detail 或 ch-log-card(判据需更新)")
|
||
}
|
||
if iLogCard < iDetail {
|
||
t.Fatal("运行日志卡片应在渠道详情**之后**(作为兄弟节点)")
|
||
}
|
||
// 统计 ch-detail 打开到日志卡片之间未闭合的 div 层数
|
||
seg := s[iDetail:iLogCard]
|
||
depth := 0
|
||
for _, m := range regexp.MustCompile(`</?div`).FindAllString(seg, -1) {
|
||
if strings.HasPrefix(m, "</") {
|
||
depth--
|
||
} else {
|
||
depth++
|
||
}
|
||
}
|
||
if depth > 0 {
|
||
t.Errorf("运行日志卡片仍嵌在渠道详情内部(depth=%d)⇒ 点开某渠道会看到别的渠道的日志(归属错误)", depth)
|
||
}
|
||
|
||
// ② 三个过滤维度
|
||
for _, id := range []string{`id="ch-log-verbose"`, `id="ch-log-level"`} {
|
||
if !strings.Contains(s, id) {
|
||
t.Errorf("缺少日志视图控件 %s", id)
|
||
}
|
||
}
|
||
if !strings.Contains(s, "function chLogRender()") {
|
||
t.Error("必须实现 chLogRender()(否则过滤逻辑无处落地)")
|
||
}
|
||
// ③ 按渠道过滤 + 切渠道重渲染
|
||
if !strings.Contains(s, "if (selPlat) return e.platform === selPlat;") {
|
||
t.Error("必须按当前选中渠道过滤日志(否则归属错误以另一种形式回来)")
|
||
}
|
||
if !strings.Contains(s, "chLogRender();\n };\n") {
|
||
t.Error("chSelect 切换渠道后必须重渲染日志")
|
||
}
|
||
t.Log("✓ 日志面板独立成卡片,三个过滤维度齐备")
|
||
}
|