mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-10-03 23:54:06 +00:00
fix(billing): 插件页不可达(真·空白根因)+ i18n + 溢出 + 状态页 tile
## ★ 用户报告「Billing 页还是空白」—— 上一轮的验证有漏洞
上一轮我用 goTab('billing') 直接调用验证,显示"有数据、无错误"就下了结论。
但用户是**点侧栏按钮**。真实点击路径走 goTab,而 goTab 只遍历硬编码的 TABS
常量来切换 `hidden` 类 —— 插件页不在 TABS 里,所以 #tab-billing 的 hidden
**永远不会被移除**。内容一直躺在 DOM 里(KPI/表格都填好了),只是不可见。
这个 bug 没有任何报错:注入正确、数据正确、API 200,唯一的问题是宿主页的
路由逻辑没把插件页纳入。而它是上一轮「TABS 收敛为单一常量」时留下的:
收敛让三处共用一个常量,却没让插件页进入它。
修法:goTab 同时遍历 PLUGIN_PAGES。PLUGIN_PAGES 从 const 改为 var 并**提前到
goTab 之前声明** —— const 在文件后部声明的话,goTab 的读取落在 TDZ 里,
第一次点击插件页就会抛 ReferenceError(同类问题这个文件里已是第二次)。
判据 TestPluginPagesAreReachableByGoTab 锁两件事:goTab 遍历插件页集合 +
声明在使用之前。变异验证:删掉遍历 → 红;var 改回 const → 红。
## 插件 UI 不跟随多语言
宿主的 applyI18n/data-i 只覆盖**宿主渲染的标记**;插件注入的 HTML 对它不可见,
所以整个 UI 切中文时 Billing 页还是英文。
pluginAPI 增加 lang(getter,实时值)与 onLangChange(切换回调)。
billing 页所有文案改走双语字典:KPI、表头(fresh/cache/cache%)、区块标题
(占位后由脚本填)、空态、状态页 tile 标签。切换时立即重渲染标题,
不用等下一次 fetch。
## 部分页面超出 UI 区域
#main 只有 overflow-y,插件页内容(8 列表格 min-width、长字符串)会横向撑破。
两层修:插件 pane 统一 min-width:0/max-width:100%/overflow-x:auto(第三方
任意 HTML 的兜底,与原生 pane 一致);billing 的宽表格在自身容器内滚动。
## 状态页 tile 的 TypeError(每次重绘都报)
tile 的 tick() 在 await 之后直接 getElementById(...).textContent = ...,
但状态页每次刷新都整体重建 pane,元素可能已不存在 → null 属性赋值。
await 之后重新取元素并判空。
## 顺手补的缺口
上一轮加了缓存表格列,但 KPI 卡片漏了(那次替换 assert 失败后重试只重做了
表格)—— 缓存命中率在表格里有、KPI 里没有。本次补上。
## 验证
真实浏览器(禁缓存、真实点击侧栏按钮):pane 可见、KPI 9 项、表头双语、
语言双向切换正确(Per source ⇄ 按源)、无水平溢出、无 billing 控制台错误。
生产数据:Total USD 0.566798 / 732 请求 / 降级 231 / 2.09 亿 prompt tokens。
387+ 测试全绿。
## DSL(进行中,未完)
config.BillingDSL(active + profiles + rules,rule 按 url 匹配 mode=free/
token/subscription/unpriced)与 internal/billing.Compile(url 规则 → 插件
prices 表,含峰谷窗口的形状编译 —— 之前手写 JSON 两次弄错的正是这个形状)
已落地并通过校验/编译;core 启动接线已写。profile 切换 API 与 WebUI 选择器
未做,生产 config.yaml 也尚未写 billing 段 —— 下一轮继续。
This commit is contained in:
@ -315,3 +315,54 @@ func TestPluginElementsSurviveHostRebuild(t *testing.T) {
|
||||
t.Error("element mounting is not guarded by a per-pane marker; re-mounting would re-run plugin scripts")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPluginPagesAreReachableByGoTab guards a bug that only shows up in a
|
||||
// browser: goTab() iterated the hardcoded TABS list to toggle `hidden`, and a
|
||||
// plugin-contributed page is not in that list. The pane existed, the plugin had
|
||||
// filled it with real data, and clicking the sidebar entry changed nothing —
|
||||
// the `hidden` class was never removed.
|
||||
//
|
||||
// It reads exactly like "the page is blank" while the content sits in the DOM,
|
||||
// and no static check catches it: the injection is correct, the data is correct,
|
||||
// and the API returns 200.
|
||||
//
|
||||
// So this asserts the two structural facts that make it reachable: goTab
|
||||
// consults the plugin page set, and that set is declared before goTab runs (a
|
||||
// `const` further down the file would be a temporal-dead-zone ReferenceError on
|
||||
// the first click).
|
||||
func TestPluginPagesAreReachableByGoTab(t *testing.T) {
|
||||
ui := uiSource(t)
|
||||
|
||||
goTab := strings.Index(ui, "function goTab(")
|
||||
if goTab < 0 {
|
||||
t.Fatal("goTab() is gone")
|
||||
}
|
||||
end := strings.Index(ui[goTab:], "\n }")
|
||||
if end < 0 {
|
||||
t.Fatal("could not isolate goTab()")
|
||||
}
|
||||
body := ui[goTab : goTab+end]
|
||||
|
||||
if !strings.Contains(body, "PLUGIN_PAGES.forEach") {
|
||||
t.Error("★ goTab() does not toggle plugin pages — a plugin page keeps its " +
|
||||
"`hidden` class forever, so the sidebar entry does nothing")
|
||||
}
|
||||
|
||||
// Declaration must precede the use, or the first click throws.
|
||||
decl := strings.Index(ui, "var PLUGIN_PAGES = new Set()")
|
||||
if decl < 0 {
|
||||
// A const later in the file would also "work" only if nothing reads it
|
||||
// first — make that explicit rather than silent.
|
||||
if strings.Contains(ui, "const PLUGIN_PAGES = new Set()") {
|
||||
t.Error("PLUGIN_PAGES is a `const` declared after goTab() reads it — " +
|
||||
"temporal dead zone: the first click on a plugin page throws")
|
||||
} else {
|
||||
t.Error("PLUGIN_PAGES is not declared anywhere")
|
||||
}
|
||||
return
|
||||
}
|
||||
if decl > goTab {
|
||||
t.Errorf("PLUGIN_PAGES is declared at %d but goTab() at %d reads it — "+
|
||||
"declaration must come first", decl, goTab)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user