From 404ff2ba2d6de4765f0c489a17312f90a7e3e1f8 Mon Sep 17 00:00:00 2001
From: Gaoshou101 <188431084+Gaoshou101@users.noreply.github.com>
Date: Sun, 8 Nov 2026 01:38:24 +0800
Subject: [PATCH] =?UTF-8?q?refactor(ui):=20=E6=8E=A7=E5=88=B6=E5=8F=B0=20c?=
=?UTF-8?q?onsole.js/console.css=20=E6=94=B9=E4=B8=BA=E5=8F=AF=E8=AF=BB?=
=?UTF-8?q?=E6=BA=90=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 以 Prettier 3.7.4 格式化(.prettierrc.json),JS 语法树与原文件一致,CSS 仅写法差异
- 按原样内嵌,不加压缩步骤:页面约增 17 KB
- Node 测试按唯一标记切片,标记缺失时直接失败;CI 检查两文件格式
---
.github/workflows/build.yml | 2 +
.prettierrc.json | 10 +
CHANGELOG.md | 6 +
README.md | 2 +
docs/VALIDATION.md | 9 +
internal/bridge/ui/console.css | 1867 +++++++++++++++++++++++++++-
internal/bridge/ui/console.js | 1821 +++++++++++++++++++++++----
tests/console-diagnostics.test.cjs | 10 +-
tests/console-evidence.test.cjs | 10 +-
tests/console-ui.test.cjs | 20 +-
10 files changed, 3476 insertions(+), 281 deletions(-)
create mode 100644 .prettierrc.json
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 89b82c8..9b0d4a3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -40,6 +40,8 @@ jobs:
- run: go test -race ./...
- run: go vet ./...
- run: node --test tests/*.test.cjs
+ - name: Console sources stay formatted
+ run: npx --yes prettier@3.7.4 --check internal/bridge/ui/console.js internal/bridge/ui/console.css
- name: Native host integration (CPA v8.0.23)
env:
GH_TOKEN: ${{ github.token }}
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..32a2397
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,10 @@
+{
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": true,
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "arrowParens": "always",
+ "endOfLine": "lf"
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2c18b48..5e1a398 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog
+## [Unreleased]
+
+### Changed
+
+- 插件控制台的 `console.js`、`console.css` 改为 Prettier 3.7.4 格式化的可读源码(原为长行手写压缩风格),按原样内嵌;JS 语法树与原文件一致(仅去掉空语句),CSS 仅引号、属性选择器引号和小数前导零写法变化。控制台页面增大约 17 KB,未加入压缩步骤。CI 检查这两个文件的格式;Node 测试改为按唯一标记切片,标记缺失时直接失败。
+
## [0.2.13] - 2026-11-07
### Added
diff --git a/README.md b/README.md
index d236164..36dbafd 100644
--- a/README.md
+++ b/README.md
@@ -128,6 +128,8 @@ CPAMP 套餐名统一显示 **Go、GOAT、Pro、Max**。月额度与 5h 顶部
原生插件构建需要 **Go ≥1.26、CGO 和目标平台 C 编译器**。发行构建先生成 pinned CPAMP 页面,再内嵌到四个平台的动态库。
+控制台脚本与样式(`internal/bridge/ui/console.js`、`console.css`)以格式化后的源码原样内嵌;修改后运行 `npx prettier@3.7.4 --write internal/bridge/ui/console.js internal/bridge/ui/console.css`(配置见 `.prettierrc.json`),CI 会检查格式。
+
GitHub Actions 运行 Go race/vet、前端测试与类型检查、CPA 原生协议及异常恢复联测、CPAMP 页面安装与热更新联测,并校验四平台插件和独立前端包的来源、校验值及内嵌页面一致性。具体步骤与验证边界见以下文档。
| 文档 | 内容 |
diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md
index 600a6aa..e1f7b8d 100644
--- a/docs/VALIDATION.md
+++ b/docs/VALIDATION.md
@@ -1,5 +1,14 @@
# 开发验证记录
+## 控制台源码可读化(2026-11-08,未发布源码)
+
+`internal/bridge/ui/console.js`、`console.css` 原为长行手写压缩风格(JS 266 行、最长 1,709 字符;CSS 16 行、最长 4,024 字符),改为 Prettier 3.7.4 格式化的源码(JS 1,574 行、CSS 1,854 行),配置见 `.prettierrc.json`。
+
+- **等价性**:`prettier --debug-check` 通过;另用 acorn 独立比较语法树,除去空语句与属性键引号写法后 JS **完全一致**;postcss 比较 1,428 个 CSS 节点,仅 11 处为引号、属性选择器引号和小数前导零写法差异。
+- **未加压缩步骤**:格式化后两文件共增加 16,842 B,控制台页面约增 17 KB,动态库从 16,232,403 B 增至 16,249,299 B(本机未裁剪符号的构建,约 0.1%)。控制台经 CPA 管理接口在本机或内网提供且不缓存;在四平台构建(含 manylinux 容器)中加入 Node 压缩工具会让发布内容与测试源码不同并增加构建依赖,收益不足以抵消,因此按原样内嵌。
+- **测试**:三个 Node 测试文件改为用 `between()` 按唯一标记切片,标记缺失或重复时直接失败(原先 `indexOf` 返回 -1 时会静默截取错误片段)。Node **31 项**、Go `go test ./internal/... ./cmd/...` 通过;CI 测试任务新增 `prettier --check`。
+- **宿主与浏览器**:新 Windows 动态库在实际 CPA v8.0.23 与 v8.0.59 上 **18/18 通过**(含以 v0.2.12 正式包为前一版本的热升级)。`console_preview.py` 普通场景中登录并切换五个页面,无全局错误,设置页显示首字节/流空闲超时字段;与格式化前的 v0.2.13 构建逐页截图对比(1265×712),设置页 0 像素差异,其余四页差异仅为夹具时间戳和一次登录提示气泡。
+
## v0.2.13 发版记录与真实上游验收(2026-11-08)
用户已确认合并 PR #16 并发版,并提供真实凭证用于验收。下方“未发布源码”一节保留为当轮验证时的状态。本次未部署或访问任何服务器。
diff --git a/internal/bridge/ui/console.css b/internal/bridge/ui/console.css
index c2956d3..ff087c5 100644
--- a/internal/bridge/ui/console.css
+++ b/internal/bridge/ui/console.css
@@ -1,15 +1,1854 @@
/* Studio overview/detail provide the visual direction; see docs/ROUND3_UI_PLAN.md. */
-:root{color-scheme:light;--page:#fff;--surface:#fff;--sidebar:#fafafa;--inset:#f4f4f5;--ink:#09090b;--muted:#6b6b74;--line:#e4e4e7;--control:#71717a;--primary:#18181b;--primary-ink:#fafafa;--accent:#4932ae;--soft:#f1effb;--good:#18794e;--good-soft:#edf7f0;--bad:#b42330;--bad-soft:#fff1f2;--warn:#80570f;--warn-soft:#fff8e8;--sidebar-width:224px;--mono:"Geist Mono","Cascadia Code",Consolas,monospace}
-:root[data-theme=dark]{color-scheme:dark;--page:#000;--surface:#09090b;--sidebar:#09090b;--inset:#18181b;--ink:#fafafa;--muted:#a1a1aa;--line:#27272a;--control:#71717a;--primary:#fafafa;--primary-ink:#18181b;--accent:#b4a5ff;--soft:#231d3b;--good:#77c89c;--good-soft:#14251b;--bad:#f4848c;--bad-soft:#2c151a;--warn:#e1b45e;--warn-soft:#2a2213}
-*{box-sizing:border-box}body{margin:0;background:var(--page);color:var(--ink);font:14px/22px "Geist","Segoe UI Variable Text","Segoe UI","Microsoft YaHei",sans-serif;-webkit-font-smoothing:antialiased}button,input,select,textarea{font:inherit}button{cursor:pointer;border:1px solid var(--control);border-radius:2px;background:var(--surface);color:var(--ink);padding:6px 12px;font-weight:500;min-height:36px}button:hover{background:var(--inset)}button:disabled{opacity:.45;cursor:not-allowed}button.primary{background:var(--primary);color:var(--primary-ink);border-color:var(--primary)}button.primary:hover{opacity:.86}button.quiet{border-color:transparent;background:transparent}button.quiet:hover{background:var(--inset)}button.danger{color:var(--bad)}.error-text,.form-error{color:var(--bad)}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}:focus-visible{outline:2px solid var(--accent);outline-offset:3px}input,select,textarea{background:var(--surface);color:var(--ink);border:1px solid var(--control);border-radius:2px;padding:6px 10px;min-width:0;min-height:36px;max-width:100%}input::placeholder,textarea::placeholder{color:var(--muted)}textarea{resize:vertical}input[type=checkbox]{accent-color:var(--accent);width:16px;height:16px;min-height:0;flex-shrink:0}label{display:grid;gap:8px;font-size:14px;margin:16px 0}label.inline{display:flex;align-items:center;margin:0;gap:8px}.block{display:block}.small,.muted{color:var(--muted)}.small{font-size:12px;line-height:18px}.actions,.header-actions,.toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}p{margin:0 0 12px}h1{font-size:24px;font-weight:600;letter-spacing:-.6px;line-height:32px;margin:0}h2{font-size:15px;font-weight:600;line-height:22px;margin:0}h3{font-size:14px;font-weight:600;margin:0 0 16px}svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.5;stroke-linejoin:miter;flex-shrink:0}.code{font-family:var(--mono);font-size:12px;line-height:20px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.skip-link{position:fixed;top:-100px;left:16px;z-index:100;background:var(--surface);padding:12px;border:1px solid var(--accent)}.skip-link:focus{top:8px}
-.shell{display:grid;grid-template-columns:var(--sidebar-width) minmax(0,1fr);min-height:100vh}.sidebar{position:sticky;top:0;display:flex;flex-direction:column;height:100dvh;background:var(--sidebar);border-right:1px solid var(--line);padding:20px 12px 16px;min-width:0}.brand{display:flex;align-items:center;gap:10px;padding:0 8px;color:var(--ink);height:40px}.brand:hover{text-decoration:none}.mark{width:28px;height:28px;display:flex;align-items:center;flex-shrink:0;color:#000}.mark svg{width:100%;height:100%;stroke:none}:root[data-theme=dark] .mark{background:#fff;border-radius:2px}.brand-copy{display:grid;gap:0;white-space:nowrap}.brand strong{font-size:16px;font-weight:600;letter-spacing:-.5px}.brand-copy>span{font-size:12px;color:var(--muted)}.workspace-name{display:flex;align-items:center;gap:8px;margin:24px 0;padding:8px;border:1px solid var(--line);background:var(--surface);font-size:12px;white-space:nowrap}.workspace-avatar{display:grid;place-items:center;width:24px;height:24px;background:var(--inset);font-size:12px;border:1px solid var(--line);flex-shrink:0}nav{display:flex;flex-direction:column;flex:1;min-height:0;gap:4px}nav button{display:flex;align-items:center;gap:12px;width:100%;border:0;background:none;text-align:left;padding:8px 12px;min-height:40px;color:var(--muted);font-weight:400;white-space:nowrap}nav button:hover{color:var(--ink)}nav button.active{background:var(--inset);color:var(--ink);font-weight:500;box-shadow:inset 2px 0 var(--accent)}nav button.active svg{color:var(--accent)}.nav-bottom{margin-top:auto;padding:20px 8px 0;font-size:12px;color:var(--muted)}.nav-bottom a{display:flex;justify-content:space-between;gap:8px;color:var(--muted);margin-bottom:12px}.workspace{min-width:0}.topbar{height:56px;padding:0 24px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line)}.breadcrumbs{display:flex;align-items:center;gap:12px;min-width:0;white-space:nowrap}.breadcrumb-root,.breadcrumb-separator{color:var(--muted)}button.icon-button{display:grid;place-items:center;padding:8px;width:36px;height:36px;min-height:36px;border:0;background:transparent;color:var(--muted);flex-shrink:0}button.icon-button:hover{color:var(--ink)}.header-actions{flex-wrap:nowrap}main{padding:32px 32px 64px;max-width:1440px;min-width:0;margin-inline:auto;width:100%}.title{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;margin-bottom:28px}.title .actions{justify-content:flex-end}.page-description{color:var(--muted);margin:8px 0 0;font-size:14px}.sidebar-scrim{display:none}.sidebar-collapsed{--sidebar-width:64px}.sidebar-collapsed .sidebar{padding-inline:8px}.sidebar-collapsed .brand{padding:0 10px}.sidebar-collapsed .brand-copy,.sidebar-collapsed .workspace-name,.sidebar-collapsed nav button>span,.sidebar-collapsed .nav-bottom{display:none}.sidebar-collapsed nav{margin-top:32px}.sidebar-collapsed nav button{padding:10px 14px;justify-content:center}
-.panel{background:var(--surface);border:1px solid var(--line);margin:24px 0;min-width:0}.panel-head{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid var(--line);gap:12px;min-height:56px}.panel-body{padding:20px}.panel-footer{display:flex;gap:8px;flex-wrap:wrap;padding:0 20px 20px}.date-range{display:flex;gap:8px;align-items:center;flex-wrap:wrap}.date-range input{max-width:200px}.date-range select{min-width:128px}.summary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));margin:24px 0 32px;border:1px solid var(--line);background:var(--surface)}.summary>div{padding:20px;border-right:1px solid var(--line);min-width:0}.summary>div:last-child{border:0}.summary>div>span{font-size:12px}.summary b{display:block;font-size:28px;font-weight:500;letter-spacing:-.6px;margin:12px 0 8px;line-height:36px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.summary small{display:block;color:var(--muted);font-size:12px;line-height:18px}.badge{display:inline-flex;align-items:center;gap:6px;font-size:12px;line-height:18px;font-weight:500;border-radius:2px;padding:3px 7px;background:var(--soft);color:var(--accent);white-space:normal}.badge.good{color:var(--good);background:var(--good-soft)}.badge.bad{color:var(--bad);background:var(--bad-soft)}.badge.warn{color:var(--warn);background:var(--warn-soft)}.badge.neutral{color:var(--muted);background:var(--inset)}.status-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:currentColor;flex-shrink:0}.empty{padding:40px 24px;text-align:center;color:var(--muted)}.empty strong{display:block;color:var(--ink);margin-bottom:8px;font-size:15px;font-weight:500}.empty button{margin-top:16px}table{width:100%;border-collapse:collapse;text-align:left;font-variant-numeric:tabular-nums;font-size:14px}th{color:var(--muted);font-size:12px;font-weight:500;background:var(--sidebar)}th,td{padding:16px;border-bottom:1px solid var(--line);vertical-align:top}tr:last-child td{border-bottom:0}.table-wrap{overflow:auto}#modelStats table{min-width:600px}#modelStats td:not(:first-child),#modelStats th:not(:first-child){text-align:right}td .small{margin-top:4px}tbody tr:hover{background:var(--sidebar)}button.compact{padding:4px 10px;min-height:32px;font-size:12px}.notice{padding:12px 16px;border:1px solid var(--bad);background:var(--bad-soft);color:var(--bad);margin-bottom:20px;overflow-wrap:anywhere}.list-status{padding:12px 20px;margin:0;background:var(--warn-soft);color:var(--warn)}#statisticsPrecision{padding:12px 20px;margin:0;border-bottom:1px solid var(--line)}
-.health-layout{display:grid;grid-template-columns:180px minmax(0,1fr)}.health-score{padding:20px;border-right:1px solid var(--line);background:var(--sidebar)}.health-score>span{font-size:12px;color:var(--muted)}.health-ratio{font-size:32px;line-height:40px;font-weight:500;letter-spacing:-1px;margin:12px 0 20px;font-variant-numeric:tabular-nums}.health-ratio small{font-size:16px;color:var(--muted);font-weight:400;letter-spacing:0}.health-bar{height:4px;display:flex;background:var(--line);overflow:hidden;gap:2px}.health-bar span{background:var(--line)}.health-bar .healthy{background:var(--good)}.health-bar .warning,.health-bar .cooldown{background:var(--warn)}.health-bar .invalid{background:var(--bad)}.health-legend{display:flex;flex-wrap:wrap;gap:8px 12px;margin-top:12px;font-size:12px;color:var(--muted)}.health-list{max-height:320px;overflow:auto}.health-row{display:grid;grid-template-columns:minmax(120px,1fr) minmax(130px,1fr) auto;align-items:center;gap:16px;padding:16px 20px;border:0;border-bottom:1px solid var(--line);border-radius:0;width:100%;text-align:left;font-weight:400;min-height:88px}.health-row:last-child{border-bottom:0}.health-name{font-size:14px;font-weight:500;overflow-wrap:anywhere}.health-row .small{margin-top:4px}.health-row .badge{justify-self:end}.health-meta{color:var(--muted);font-size:12px}
-.diagnostic-panel{margin-bottom:24px}.diagnostic-overview{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:16px 20px}.diagnostic-overview .small{margin-top:6px}.diagnostic-panel>details>summary{padding:12px 20px;border-top:1px solid var(--line);cursor:pointer;color:var(--muted);font-size:12px}.diagnostic-row{padding:16px 0;border-bottom:1px solid var(--line);overflow-wrap:anywhere}.diagnostic-row:last-child{border:0}.diagnostic-row p{margin:8px 0}.diagnostic-row .badge{margin-left:8px}.diagnostic-row .evidence-source{border-left:2px solid var(--line);padding-left:12px;margin-top:8px}.credential-workspace{display:grid;grid-template-columns:180px minmax(0,1fr);gap:24px;align-items:start}.account-selector{display:grid;gap:4px;min-width:0;max-height:480px;overflow:auto}.selector-heading{display:flex;align-items:center;justify-content:space-between;padding:0 8px 12px;font-size:12px;color:var(--muted)}button.account-choice{display:flex;align-items:center;gap:10px;text-align:left;background:transparent;border-color:transparent;min-width:0;padding:12px 8px}button.account-choice:hover,button.account-choice.selected{background:var(--inset)}button.account-choice.selected{box-shadow:inset 2px 0 var(--accent)}.account-choice-copy{display:grid;gap:4px;min-width:0}.account-choice-copy strong{font-size:14px;font-weight:500;overflow-wrap:anywhere}.account-choice-copy small{font-size:12px;color:var(--muted);font-weight:400}.account-choice .status-dot{color:var(--muted)}.account-choice .healthy{color:var(--good)}.account-choice .invalid{color:var(--bad)}.account-choice .warning,.account-choice .cooldown{color:var(--warn)}.account{background:var(--surface);border:1px solid var(--line);min-width:0}.account-top{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px;border-bottom:1px solid var(--line)}.account-title{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.account-title strong{font-size:16px;font-weight:500;overflow-wrap:anywhere}.account-top .small{margin-top:8px}.account-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));padding:24px}.balance-cell{grid-column:1/-1;border-bottom:1px solid var(--line);padding-bottom:24px;margin-bottom:24px;display:flex;align-items:center;justify-content:space-between;gap:24px}.balance{font-size:32px;font-weight:500;letter-spacing:-1px;font-variant-numeric:tabular-nums;line-height:40px;margin:8px 0}.balance small{font-size:12px;color:var(--muted);letter-spacing:0;margin-left:6px}.balance-parts{display:flex;gap:24px}.balance-parts span{display:block;font-size:12px;color:var(--muted);margin-bottom:6px}.balance-parts b{font-size:14px;font-weight:400;font-variant-numeric:tabular-nums}.quota-window{padding:0 16px;min-width:0;display:flex;flex-direction:column;border-right:1px solid var(--line)}.quota-window:nth-child(2){padding-left:0}.quota-window:last-child{border:0;padding-right:0}.meter-label{display:flex;justify-content:space-between;gap:6px;flex-wrap:wrap;font-size:12px;font-weight:500;margin-bottom:12px}.quota-value{font-size:24px;font-weight:500;font-variant-numeric:tabular-nums;margin-bottom:16px;line-height:32px;overflow-wrap:anywhere}.quota-value small{font-size:12px;font-weight:400;color:var(--muted)}.meter{height:4px;background:var(--line);overflow:hidden;margin-top:auto}.meter span{display:block;height:100%;background:var(--accent)}.meter.over span{background:var(--bad)}.quota-note{font-size:12px;color:var(--muted);margin-top:12px;line-height:20px}.account-foot{display:flex;justify-content:space-between;gap:8px 16px;flex-wrap:wrap;font-size:12px;padding:16px 20px;border-top:1px solid var(--line);color:var(--muted)}.account-foot .error-text,.account-foot .usage-warning{width:100%}.usage-warning{color:var(--warn)}
-.model-toolbar{display:flex;align-items:end;gap:16px;flex-wrap:wrap;padding:20px;background:var(--surface);border:1px solid var(--line);margin-bottom:16px}.model-toolbar label{margin:0;min-width:160px;flex:1}.model-toolbar .model-search{flex:1.6}#modelRows table{table-layout:fixed}#modelRows th:first-child{width:30%}#modelRows th:nth-child(2){width:13%}#modelRows th:nth-child(3){width:12%}#modelRows th:nth-child(4){width:28%}#modelRows td{overflow-wrap:anywhere}#modelRows td .small{max-width:32em}.model-mobile-list{display:none}.evidence-summary .small{margin-top:6px}.settings-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px 24px;max-width:780px;margin-bottom:20px}.settings-grid label{margin:0}.setting-note{margin:8px 0 0}.section-actions{margin-top:20px}.advanced-settings{border-block:1px solid var(--line);padding:12px 0;margin:20px 0}.advanced-settings>summary{cursor:pointer}.advanced-settings .settings-grid{margin-top:20px}.status-rows{margin:16px 0 0}.status-rows>div{display:grid;grid-template-columns:140px minmax(0,1fr);gap:16px;padding:12px 0;border-bottom:1px solid var(--line)}.status-rows>div:last-child{border:0}.status-rows dt{color:var(--muted);font-size:12px}.status-rows dd{margin:0;overflow-wrap:anywhere}.path-value{margin-top:8px}.api-panel>summary{cursor:pointer;padding:16px 20px;font-weight:500}.api-example{padding:20px;margin:0;border-top:1px solid var(--line);background:var(--sidebar);font:12px/20px var(--mono)}
-.toolbar input{flex:1;min-width:140px}.toolbar{margin:16px 0}.log-toolbar{padding:16px 20px;margin:0;border-bottom:1px solid var(--line)}.log-toolbar input{flex:1}.log-toolbar .small{margin-left:auto}#logs{--log-columns:72px minmax(110px,1.5fr) 80px minmax(100px,1fr) 100px minmax(90px,1fr) 14px}.log-head,.log-main{display:grid;grid-template-columns:var(--log-columns);gap:12px;align-items:center}.log-head{font-size:12px;color:var(--muted);padding:12px 20px;background:var(--sidebar);border-bottom:1px solid var(--line)}.log-main{min-height:96px;padding:16px 20px;font-size:14px;line-height:22px;width:100%;border:0;border-bottom:1px solid var(--line);border-radius:0;text-align:left;font-weight:400}.log-main:hover{background:var(--inset)}.log-main .small{margin-top:4px}.log-cell{min-width:0;overflow-wrap:anywhere}.cell-label{display:none;color:var(--muted);font-size:12px}.log-model{font-weight:500}.log-latency{display:grid;gap:4px}.latency-line{display:flex;justify-content:space-between;gap:6px;font-size:12px;font-variant-numeric:tabular-nums}.latency-line>span{color:var(--muted)}.latency-line strong{font-weight:400;white-space:nowrap}.latency-line.good strong{color:var(--good)}.latency-line.warn strong{color:var(--warn)}.latency-line.bad strong{color:var(--bad)}.chevron{color:var(--muted);font-size:20px}.detail-section{padding:24px 0;border-bottom:1px solid var(--line)}.detail-section:first-child{padding-top:0}.detail-section:last-child{border-bottom:0}.detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:20px;margin:0}.detail-grid dt{color:var(--muted);font-size:12px}.detail-grid dd{margin:6px 0 0;font-size:14px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.log-error{margin:16px 0 0;padding:12px;border:1px solid var(--bad);background:var(--bad-soft);color:var(--bad);overflow-wrap:anywhere}.log-error pre{margin:8px 0 0;font:12px/20px var(--mono);max-height:260px;overflow:auto}.pager{display:flex;gap:8px;align-items:center;justify-content:flex-end;padding:16px 20px}.pager>span{margin-right:auto}
-dialog{border:1px solid var(--line);border-radius:4px;background:var(--surface);color:var(--ink);padding:24px;width:min(520px,calc(100vw - 32px));max-height:90dvh;overflow:auto;box-shadow:0 16px 70px #0003}dialog::backdrop{background:rgba(0,0,0,.5)}dialog h2{font-size:20px;line-height:28px;font-weight:500;margin-bottom:20px}dialog .actions{justify-content:flex-end;margin-top:24px}dialog label input,dialog label select,dialog textarea{width:100%}dialog label.inline input{width:auto}.dialog-wide{width:min(760px,calc(100vw - 32px))}.catalog-list{max-height:390px;overflow:auto;border:1px solid var(--line);margin-top:16px}.catalog-list label{display:flex;align-items:center;gap:12px;padding:12px 16px;margin:0;border-bottom:1px solid var(--line)}.catalog-list label input{width:auto}.catalog-list label:last-child{border:0}.catalog-list label>span{flex:1;min-width:0}.catalog-list label>span:last-child{flex:0 0 auto}.result{white-space:pre-wrap;max-height:320px;overflow:auto;background:var(--inset);padding:16px;font-size:12px}.toast{position:fixed;bottom:24px;right:24px;max-width:min(420px,calc(100vw - 32px));background:var(--ink);color:var(--page);border-radius:2px;padding:12px 16px;z-index:60;font-size:14px}.form-error{margin-top:12px;white-space:pre-wrap;font-size:14px;min-height:22px}.form-error:empty{margin-top:0;min-height:0}pre{white-space:pre-wrap;word-break:break-word}#confirmText{white-space:pre-wrap}.detail-drawer{position:fixed;inset:0 0 0 auto;margin:0;width:min(480px,100vw);max-width:100vw;height:100dvh;max-height:100dvh;padding:0;border-radius:0;border-width:0 0 0 1px;overflow:hidden}.detail-drawer[open]{display:flex;flex-direction:column}.drawer-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;padding:24px;border-bottom:1px solid var(--line);flex-shrink:0}.drawer-head>div{min-width:0}.drawer-head h2{overflow-wrap:anywhere;margin:4px 0 0;font-size:20px}.drawer-head p{margin:0}.drawer-body{padding:24px;overflow:auto;overscroll-behavior:contain;flex:1}.drawer-body .actions{justify-content:flex-start}.drawer-body .code{font-size:12px}.evidence-source h3{display:flex;align-items:center;gap:12px;flex-wrap:wrap}[hidden],.view[hidden]{display:none!important}
-@media(max-width:1180px){.credential-workspace{grid-template-columns:1fr}.account-selector{display:flex;overflow:auto;gap:8px;padding-bottom:4px}.selector-heading{display:none}button.account-choice{min-width:180px;max-width:260px;flex-shrink:0;border-color:var(--line)}button.account-choice.selected{border-color:var(--accent);background:var(--soft);box-shadow:none}.log-head{display:none}.log-main{grid-template-columns:90px minmax(0,1fr) 95px;gap:16px}.log-cell:nth-child(1){grid-column:1;grid-row:1}.log-cell:nth-child(2){grid-column:2;grid-row:1}.log-cell:nth-child(3){grid-column:3;grid-row:1}.log-cell:nth-child(4){grid-column:1;grid-row:2}.log-cell:nth-child(5){grid-column:2;grid-row:2;max-width:160px}.log-cell:nth-child(6){grid-column:3;grid-row:2}.log-main>.chevron{display:none}.cell-label{display:block;margin-bottom:4px}.summary>div{padding:16px}.summary b{font-size:25px}.topbar{padding-inline:20px}}
-@media(max-width:960px){.breadcrumb-root,.breadcrumb-separator{display:none}.title{flex-wrap:wrap;gap:16px}.title .actions{justify-content:flex-start}.summary{grid-template-columns:1fr 1fr}.summary>div:nth-child(2){border-right:0}.summary>div:nth-child(-n+2){border-bottom:1px solid var(--line)}.health-layout{grid-template-columns:1fr}.health-score{border-right:0;border-bottom:1px solid var(--line);display:grid;grid-template-columns:1fr 1fr;gap:0 20px}.health-ratio{margin:8px 0 0;grid-column:1}.health-bar{grid-column:2;grid-row:1;align-self:center}.health-legend{grid-column:2;grid-row:2;align-self:center;margin:0}.health-row{grid-template-columns:minmax(0,1fr) auto}.health-meta{grid-column:1;grid-row:2}.health-row .badge{grid-column:2;grid-row:1/3}.balance-cell{align-items:flex-start;gap:16px}.balance-parts{gap:16px}.settings-grid{grid-template-columns:1fr}#busy{display:none}}
-@media(max-width:767px){.shell,.sidebar-collapsed{display:block;--sidebar-width:224px}.sidebar{position:fixed;inset:0 auto 0 0;width:256px;height:100dvh;z-index:50;padding:24px 16px;visibility:hidden;transform:translateX(-100%)}.sidebar-open .sidebar{visibility:visible;transform:none}.sidebar-open .sidebar-scrim{display:block;position:fixed;inset:0;border:0;border-radius:0;background:#0008;z-index:40;padding:0}.sidebar-collapsed .brand-copy,.sidebar-collapsed .workspace-name,.sidebar-collapsed nav button>span,.sidebar-collapsed .nav-bottom{display:revert}.sidebar-collapsed .brand-copy{display:grid}.sidebar-collapsed .workspace-name{display:flex}.sidebar-collapsed nav{margin:0}.sidebar-collapsed nav button{justify-content:flex-start;padding:10px 12px}.topbar{padding:0 12px;height:64px;gap:8px}.breadcrumbs{gap:8px;font-size:12px}.header-actions{gap:4px}.header-actions #authButton{font-size:12px;padding-inline:10px}button,input,select{min-height:44px}button.icon-button{width:44px;height:44px;min-height:44px}button.compact{min-height:44px;font-size:12px}main{padding:24px 16px 40px}.title{margin-bottom:24px}.title .actions{width:100%}.title .actions>button{flex:1;padding-inline:8px}.page-description{font-size:14px}h1{font-size:24px}.panel{margin:20px 0}.panel-head,.panel-body{padding:16px}.panel-head{flex-wrap:wrap}.date-range label{flex:1}.date-range input{width:100%;max-width:none}.summary{margin-block:20px}.summary>div{padding:16px}.summary b{font-size:24px;line-height:32px}.health-row{padding:16px;gap:8px 12px}.health-score{padding:16px}.account-selector{gap:8px}.account-top{padding:16px;flex-wrap:wrap}.account-top>.actions{width:100%;justify-content:flex-start}.account-grid{padding:20px 16px;grid-template-columns:1fr}.balance-cell{flex-wrap:wrap;gap:16px;padding-bottom:20px;margin-bottom:0}.balance-parts{width:100%;justify-content:space-between}.quota-window{padding:20px 0!important;border:0;border-bottom:1px solid var(--line)}.quota-window:last-child{padding-bottom:0!important}.quota-value{font-size:24px;margin-bottom:12px}.meter-label{margin-bottom:8px}.quota-note{margin-top:8px}.account-foot{padding:16px}.diagnostic-overview{padding:16px;align-items:flex-start}.diagnostic-overview>button{flex-shrink:0;font-size:12px;padding-inline:8px}.diagnostic-panel>details>summary{padding:14px 16px;min-height:44px}.panel-footer{padding:0 16px 16px}.model-toolbar{padding:16px;gap:16px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr)}.model-toolbar label{min-width:0}.model-toolbar label:first-child{grid-column:1/-1}#modelRows table{display:none}.model-mobile-list{display:block}.model-item{padding:16px;border-bottom:1px solid var(--line)}.model-item:last-child{border-bottom:0}.model-item .code{font-size:14px;line-height:22px}.model-item .small{margin-top:6px}.model-item .evidence-summary{margin-top:12px}.model-item>.actions{margin-top:16px}.log-toolbar{padding:16px;gap:8px}.log-toolbar input{flex-basis:100%}.log-toolbar .small{width:100%;margin:4px 0 0}.log-main{padding:16px;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:16px}.log-cell:nth-child(1){grid-column:1;grid-row:1}.log-cell:nth-child(2){grid-column:1/3;grid-row:2}.log-cell:nth-child(3){grid-column:2;grid-row:1;justify-self:end}.log-cell:nth-child(4){grid-column:1;grid-row:3}.log-cell:nth-child(5){grid-column:2;grid-row:3}.log-cell:nth-child(6){grid-column:1/3;grid-row:4}.log-cell:nth-child(2)>.small{display:none}.pager{padding:16px;flex-wrap:wrap}.pager>span{width:100%;margin:0 0 4px}.status-rows>div{grid-template-columns:1fr;gap:6px}.settings-grid{gap:20px}.detail-drawer{width:100vw;border:0}.drawer-head{padding:20px 16px}.drawer-body{padding:20px 16px}.detail-grid{gap:20px 16px}.dialog-wide,dialog{padding:20px 16px;width:calc(100vw - 24px);max-height:calc(100dvh - 24px)}.detail-drawer{width:100vw;max-height:100dvh;padding:0}.catalog-list label{flex-wrap:wrap}.catalog-list label>span:last-child{flex:1 1 100%;padding-left:28px}.toast{right:16px;bottom:16px}.api-example{padding:16px}}
-@media(prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
+:root {
+ color-scheme: light;
+ --page: #fff;
+ --surface: #fff;
+ --sidebar: #fafafa;
+ --inset: #f4f4f5;
+ --ink: #09090b;
+ --muted: #6b6b74;
+ --line: #e4e4e7;
+ --control: #71717a;
+ --primary: #18181b;
+ --primary-ink: #fafafa;
+ --accent: #4932ae;
+ --soft: #f1effb;
+ --good: #18794e;
+ --good-soft: #edf7f0;
+ --bad: #b42330;
+ --bad-soft: #fff1f2;
+ --warn: #80570f;
+ --warn-soft: #fff8e8;
+ --sidebar-width: 224px;
+ --mono: 'Geist Mono', 'Cascadia Code', Consolas, monospace;
+}
+:root[data-theme='dark'] {
+ color-scheme: dark;
+ --page: #000;
+ --surface: #09090b;
+ --sidebar: #09090b;
+ --inset: #18181b;
+ --ink: #fafafa;
+ --muted: #a1a1aa;
+ --line: #27272a;
+ --control: #71717a;
+ --primary: #fafafa;
+ --primary-ink: #18181b;
+ --accent: #b4a5ff;
+ --soft: #231d3b;
+ --good: #77c89c;
+ --good-soft: #14251b;
+ --bad: #f4848c;
+ --bad-soft: #2c151a;
+ --warn: #e1b45e;
+ --warn-soft: #2a2213;
+}
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+ background: var(--page);
+ color: var(--ink);
+ font:
+ 14px/22px 'Geist',
+ 'Segoe UI Variable Text',
+ 'Segoe UI',
+ 'Microsoft YaHei',
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+}
+button,
+input,
+select,
+textarea {
+ font: inherit;
+}
+button {
+ cursor: pointer;
+ border: 1px solid var(--control);
+ border-radius: 2px;
+ background: var(--surface);
+ color: var(--ink);
+ padding: 6px 12px;
+ font-weight: 500;
+ min-height: 36px;
+}
+button:hover {
+ background: var(--inset);
+}
+button:disabled {
+ opacity: 0.45;
+ cursor: not-allowed;
+}
+button.primary {
+ background: var(--primary);
+ color: var(--primary-ink);
+ border-color: var(--primary);
+}
+button.primary:hover {
+ opacity: 0.86;
+}
+button.quiet {
+ border-color: transparent;
+ background: transparent;
+}
+button.quiet:hover {
+ background: var(--inset);
+}
+button.danger {
+ color: var(--bad);
+}
+.error-text,
+.form-error {
+ color: var(--bad);
+}
+a {
+ color: var(--accent);
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 3px;
+}
+input,
+select,
+textarea {
+ background: var(--surface);
+ color: var(--ink);
+ border: 1px solid var(--control);
+ border-radius: 2px;
+ padding: 6px 10px;
+ min-width: 0;
+ min-height: 36px;
+ max-width: 100%;
+}
+input::placeholder,
+textarea::placeholder {
+ color: var(--muted);
+}
+textarea {
+ resize: vertical;
+}
+input[type='checkbox'] {
+ accent-color: var(--accent);
+ width: 16px;
+ height: 16px;
+ min-height: 0;
+ flex-shrink: 0;
+}
+label {
+ display: grid;
+ gap: 8px;
+ font-size: 14px;
+ margin: 16px 0;
+}
+label.inline {
+ display: flex;
+ align-items: center;
+ margin: 0;
+ gap: 8px;
+}
+.block {
+ display: block;
+}
+.small,
+.muted {
+ color: var(--muted);
+}
+.small {
+ font-size: 12px;
+ line-height: 18px;
+}
+.actions,
+.header-actions,
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+p {
+ margin: 0 0 12px;
+}
+h1 {
+ font-size: 24px;
+ font-weight: 600;
+ letter-spacing: -0.6px;
+ line-height: 32px;
+ margin: 0;
+}
+h2 {
+ font-size: 15px;
+ font-weight: 600;
+ line-height: 22px;
+ margin: 0;
+}
+h3 {
+ font-size: 14px;
+ font-weight: 600;
+ margin: 0 0 16px;
+}
+svg {
+ width: 20px;
+ height: 20px;
+ fill: none;
+ stroke: currentColor;
+ stroke-width: 1.5;
+ stroke-linejoin: miter;
+ flex-shrink: 0;
+}
+.code {
+ font-family: var(--mono);
+ font-size: 12px;
+ line-height: 20px;
+ overflow-wrap: anywhere;
+ font-variant-numeric: tabular-nums;
+}
+.skip-link {
+ position: fixed;
+ top: -100px;
+ left: 16px;
+ z-index: 100;
+ background: var(--surface);
+ padding: 12px;
+ border: 1px solid var(--accent);
+}
+.skip-link:focus {
+ top: 8px;
+}
+.shell {
+ display: grid;
+ grid-template-columns: var(--sidebar-width) minmax(0, 1fr);
+ min-height: 100vh;
+}
+.sidebar {
+ position: sticky;
+ top: 0;
+ display: flex;
+ flex-direction: column;
+ height: 100dvh;
+ background: var(--sidebar);
+ border-right: 1px solid var(--line);
+ padding: 20px 12px 16px;
+ min-width: 0;
+}
+.brand {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 0 8px;
+ color: var(--ink);
+ height: 40px;
+}
+.brand:hover {
+ text-decoration: none;
+}
+.mark {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ flex-shrink: 0;
+ color: #000;
+}
+.mark svg {
+ width: 100%;
+ height: 100%;
+ stroke: none;
+}
+:root[data-theme='dark'] .mark {
+ background: #fff;
+ border-radius: 2px;
+}
+.brand-copy {
+ display: grid;
+ gap: 0;
+ white-space: nowrap;
+}
+.brand strong {
+ font-size: 16px;
+ font-weight: 600;
+ letter-spacing: -0.5px;
+}
+.brand-copy > span {
+ font-size: 12px;
+ color: var(--muted);
+}
+.workspace-name {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin: 24px 0;
+ padding: 8px;
+ border: 1px solid var(--line);
+ background: var(--surface);
+ font-size: 12px;
+ white-space: nowrap;
+}
+.workspace-avatar {
+ display: grid;
+ place-items: center;
+ width: 24px;
+ height: 24px;
+ background: var(--inset);
+ font-size: 12px;
+ border: 1px solid var(--line);
+ flex-shrink: 0;
+}
+nav {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ gap: 4px;
+}
+nav button {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ width: 100%;
+ border: 0;
+ background: none;
+ text-align: left;
+ padding: 8px 12px;
+ min-height: 40px;
+ color: var(--muted);
+ font-weight: 400;
+ white-space: nowrap;
+}
+nav button:hover {
+ color: var(--ink);
+}
+nav button.active {
+ background: var(--inset);
+ color: var(--ink);
+ font-weight: 500;
+ box-shadow: inset 2px 0 var(--accent);
+}
+nav button.active svg {
+ color: var(--accent);
+}
+.nav-bottom {
+ margin-top: auto;
+ padding: 20px 8px 0;
+ font-size: 12px;
+ color: var(--muted);
+}
+.nav-bottom a {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ color: var(--muted);
+ margin-bottom: 12px;
+}
+.workspace {
+ min-width: 0;
+}
+.topbar {
+ height: 56px;
+ padding: 0 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ border-bottom: 1px solid var(--line);
+}
+.breadcrumbs {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ min-width: 0;
+ white-space: nowrap;
+}
+.breadcrumb-root,
+.breadcrumb-separator {
+ color: var(--muted);
+}
+button.icon-button {
+ display: grid;
+ place-items: center;
+ padding: 8px;
+ width: 36px;
+ height: 36px;
+ min-height: 36px;
+ border: 0;
+ background: transparent;
+ color: var(--muted);
+ flex-shrink: 0;
+}
+button.icon-button:hover {
+ color: var(--ink);
+}
+.header-actions {
+ flex-wrap: nowrap;
+}
+main {
+ padding: 32px 32px 64px;
+ max-width: 1440px;
+ min-width: 0;
+ margin-inline: auto;
+ width: 100%;
+}
+.title {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 24px;
+ margin-bottom: 28px;
+}
+.title .actions {
+ justify-content: flex-end;
+}
+.page-description {
+ color: var(--muted);
+ margin: 8px 0 0;
+ font-size: 14px;
+}
+.sidebar-scrim {
+ display: none;
+}
+.sidebar-collapsed {
+ --sidebar-width: 64px;
+}
+.sidebar-collapsed .sidebar {
+ padding-inline: 8px;
+}
+.sidebar-collapsed .brand {
+ padding: 0 10px;
+}
+.sidebar-collapsed .brand-copy,
+.sidebar-collapsed .workspace-name,
+.sidebar-collapsed nav button > span,
+.sidebar-collapsed .nav-bottom {
+ display: none;
+}
+.sidebar-collapsed nav {
+ margin-top: 32px;
+}
+.sidebar-collapsed nav button {
+ padding: 10px 14px;
+ justify-content: center;
+}
+.panel {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ margin: 24px 0;
+ min-width: 0;
+}
+.panel-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--line);
+ gap: 12px;
+ min-height: 56px;
+}
+.panel-body {
+ padding: 20px;
+}
+.panel-footer {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ padding: 0 20px 20px;
+}
+.date-range {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ flex-wrap: wrap;
+}
+.date-range input {
+ max-width: 200px;
+}
+.date-range select {
+ min-width: 128px;
+}
+.summary {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ margin: 24px 0 32px;
+ border: 1px solid var(--line);
+ background: var(--surface);
+}
+.summary > div {
+ padding: 20px;
+ border-right: 1px solid var(--line);
+ min-width: 0;
+}
+.summary > div:last-child {
+ border: 0;
+}
+.summary > div > span {
+ font-size: 12px;
+}
+.summary b {
+ display: block;
+ font-size: 28px;
+ font-weight: 500;
+ letter-spacing: -0.6px;
+ margin: 12px 0 8px;
+ line-height: 36px;
+ font-variant-numeric: tabular-nums;
+ overflow-wrap: anywhere;
+}
+.summary small {
+ display: block;
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 18px;
+}
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+ line-height: 18px;
+ font-weight: 500;
+ border-radius: 2px;
+ padding: 3px 7px;
+ background: var(--soft);
+ color: var(--accent);
+ white-space: normal;
+}
+.badge.good {
+ color: var(--good);
+ background: var(--good-soft);
+}
+.badge.bad {
+ color: var(--bad);
+ background: var(--bad-soft);
+}
+.badge.warn {
+ color: var(--warn);
+ background: var(--warn-soft);
+}
+.badge.neutral {
+ color: var(--muted);
+ background: var(--inset);
+}
+.status-dot {
+ display: inline-block;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: currentColor;
+ flex-shrink: 0;
+}
+.empty {
+ padding: 40px 24px;
+ text-align: center;
+ color: var(--muted);
+}
+.empty strong {
+ display: block;
+ color: var(--ink);
+ margin-bottom: 8px;
+ font-size: 15px;
+ font-weight: 500;
+}
+.empty button {
+ margin-top: 16px;
+}
+table {
+ width: 100%;
+ border-collapse: collapse;
+ text-align: left;
+ font-variant-numeric: tabular-nums;
+ font-size: 14px;
+}
+th {
+ color: var(--muted);
+ font-size: 12px;
+ font-weight: 500;
+ background: var(--sidebar);
+}
+th,
+td {
+ padding: 16px;
+ border-bottom: 1px solid var(--line);
+ vertical-align: top;
+}
+tr:last-child td {
+ border-bottom: 0;
+}
+.table-wrap {
+ overflow: auto;
+}
+#modelStats table {
+ min-width: 600px;
+}
+#modelStats td:not(:first-child),
+#modelStats th:not(:first-child) {
+ text-align: right;
+}
+td .small {
+ margin-top: 4px;
+}
+tbody tr:hover {
+ background: var(--sidebar);
+}
+button.compact {
+ padding: 4px 10px;
+ min-height: 32px;
+ font-size: 12px;
+}
+.notice {
+ padding: 12px 16px;
+ border: 1px solid var(--bad);
+ background: var(--bad-soft);
+ color: var(--bad);
+ margin-bottom: 20px;
+ overflow-wrap: anywhere;
+}
+.list-status {
+ padding: 12px 20px;
+ margin: 0;
+ background: var(--warn-soft);
+ color: var(--warn);
+}
+#statisticsPrecision {
+ padding: 12px 20px;
+ margin: 0;
+ border-bottom: 1px solid var(--line);
+}
+.health-layout {
+ display: grid;
+ grid-template-columns: 180px minmax(0, 1fr);
+}
+.health-score {
+ padding: 20px;
+ border-right: 1px solid var(--line);
+ background: var(--sidebar);
+}
+.health-score > span {
+ font-size: 12px;
+ color: var(--muted);
+}
+.health-ratio {
+ font-size: 32px;
+ line-height: 40px;
+ font-weight: 500;
+ letter-spacing: -1px;
+ margin: 12px 0 20px;
+ font-variant-numeric: tabular-nums;
+}
+.health-ratio small {
+ font-size: 16px;
+ color: var(--muted);
+ font-weight: 400;
+ letter-spacing: 0;
+}
+.health-bar {
+ height: 4px;
+ display: flex;
+ background: var(--line);
+ overflow: hidden;
+ gap: 2px;
+}
+.health-bar span {
+ background: var(--line);
+}
+.health-bar .healthy {
+ background: var(--good);
+}
+.health-bar .warning,
+.health-bar .cooldown {
+ background: var(--warn);
+}
+.health-bar .invalid {
+ background: var(--bad);
+}
+.health-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 12px;
+ margin-top: 12px;
+ font-size: 12px;
+ color: var(--muted);
+}
+.health-list {
+ max-height: 320px;
+ overflow: auto;
+}
+.health-row {
+ display: grid;
+ grid-template-columns: minmax(120px, 1fr) minmax(130px, 1fr) auto;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 20px;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ border-radius: 0;
+ width: 100%;
+ text-align: left;
+ font-weight: 400;
+ min-height: 88px;
+}
+.health-row:last-child {
+ border-bottom: 0;
+}
+.health-name {
+ font-size: 14px;
+ font-weight: 500;
+ overflow-wrap: anywhere;
+}
+.health-row .small {
+ margin-top: 4px;
+}
+.health-row .badge {
+ justify-self: end;
+}
+.health-meta {
+ color: var(--muted);
+ font-size: 12px;
+}
+.diagnostic-panel {
+ margin-bottom: 24px;
+}
+.diagnostic-overview {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 16px 20px;
+}
+.diagnostic-overview .small {
+ margin-top: 6px;
+}
+.diagnostic-panel > details > summary {
+ padding: 12px 20px;
+ border-top: 1px solid var(--line);
+ cursor: pointer;
+ color: var(--muted);
+ font-size: 12px;
+}
+.diagnostic-row {
+ padding: 16px 0;
+ border-bottom: 1px solid var(--line);
+ overflow-wrap: anywhere;
+}
+.diagnostic-row:last-child {
+ border: 0;
+}
+.diagnostic-row p {
+ margin: 8px 0;
+}
+.diagnostic-row .badge {
+ margin-left: 8px;
+}
+.diagnostic-row .evidence-source {
+ border-left: 2px solid var(--line);
+ padding-left: 12px;
+ margin-top: 8px;
+}
+.credential-workspace {
+ display: grid;
+ grid-template-columns: 180px minmax(0, 1fr);
+ gap: 24px;
+ align-items: start;
+}
+.account-selector {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+ max-height: 480px;
+ overflow: auto;
+}
+.selector-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 8px 12px;
+ font-size: 12px;
+ color: var(--muted);
+}
+button.account-choice {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ text-align: left;
+ background: transparent;
+ border-color: transparent;
+ min-width: 0;
+ padding: 12px 8px;
+}
+button.account-choice:hover,
+button.account-choice.selected {
+ background: var(--inset);
+}
+button.account-choice.selected {
+ box-shadow: inset 2px 0 var(--accent);
+}
+.account-choice-copy {
+ display: grid;
+ gap: 4px;
+ min-width: 0;
+}
+.account-choice-copy strong {
+ font-size: 14px;
+ font-weight: 500;
+ overflow-wrap: anywhere;
+}
+.account-choice-copy small {
+ font-size: 12px;
+ color: var(--muted);
+ font-weight: 400;
+}
+.account-choice .status-dot {
+ color: var(--muted);
+}
+.account-choice .healthy {
+ color: var(--good);
+}
+.account-choice .invalid {
+ color: var(--bad);
+}
+.account-choice .warning,
+.account-choice .cooldown {
+ color: var(--warn);
+}
+.account {
+ background: var(--surface);
+ border: 1px solid var(--line);
+ min-width: 0;
+}
+.account-top {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 20px;
+ border-bottom: 1px solid var(--line);
+}
+.account-title {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+.account-title strong {
+ font-size: 16px;
+ font-weight: 500;
+ overflow-wrap: anywhere;
+}
+.account-top .small {
+ margin-top: 8px;
+}
+.account-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ padding: 24px;
+}
+.balance-cell {
+ grid-column: 1/-1;
+ border-bottom: 1px solid var(--line);
+ padding-bottom: 24px;
+ margin-bottom: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+}
+.balance {
+ font-size: 32px;
+ font-weight: 500;
+ letter-spacing: -1px;
+ font-variant-numeric: tabular-nums;
+ line-height: 40px;
+ margin: 8px 0;
+}
+.balance small {
+ font-size: 12px;
+ color: var(--muted);
+ letter-spacing: 0;
+ margin-left: 6px;
+}
+.balance-parts {
+ display: flex;
+ gap: 24px;
+}
+.balance-parts span {
+ display: block;
+ font-size: 12px;
+ color: var(--muted);
+ margin-bottom: 6px;
+}
+.balance-parts b {
+ font-size: 14px;
+ font-weight: 400;
+ font-variant-numeric: tabular-nums;
+}
+.quota-window {
+ padding: 0 16px;
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid var(--line);
+}
+.quota-window:nth-child(2) {
+ padding-left: 0;
+}
+.quota-window:last-child {
+ border: 0;
+ padding-right: 0;
+}
+.meter-label {
+ display: flex;
+ justify-content: space-between;
+ gap: 6px;
+ flex-wrap: wrap;
+ font-size: 12px;
+ font-weight: 500;
+ margin-bottom: 12px;
+}
+.quota-value {
+ font-size: 24px;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+ margin-bottom: 16px;
+ line-height: 32px;
+ overflow-wrap: anywhere;
+}
+.quota-value small {
+ font-size: 12px;
+ font-weight: 400;
+ color: var(--muted);
+}
+.meter {
+ height: 4px;
+ background: var(--line);
+ overflow: hidden;
+ margin-top: auto;
+}
+.meter span {
+ display: block;
+ height: 100%;
+ background: var(--accent);
+}
+.meter.over span {
+ background: var(--bad);
+}
+.quota-note {
+ font-size: 12px;
+ color: var(--muted);
+ margin-top: 12px;
+ line-height: 20px;
+}
+.account-foot {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px 16px;
+ flex-wrap: wrap;
+ font-size: 12px;
+ padding: 16px 20px;
+ border-top: 1px solid var(--line);
+ color: var(--muted);
+}
+.account-foot .error-text,
+.account-foot .usage-warning {
+ width: 100%;
+}
+.usage-warning {
+ color: var(--warn);
+}
+.model-toolbar {
+ display: flex;
+ align-items: end;
+ gap: 16px;
+ flex-wrap: wrap;
+ padding: 20px;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ margin-bottom: 16px;
+}
+.model-toolbar label {
+ margin: 0;
+ min-width: 160px;
+ flex: 1;
+}
+.model-toolbar .model-search {
+ flex: 1.6;
+}
+#modelRows table {
+ table-layout: fixed;
+}
+#modelRows th:first-child {
+ width: 30%;
+}
+#modelRows th:nth-child(2) {
+ width: 13%;
+}
+#modelRows th:nth-child(3) {
+ width: 12%;
+}
+#modelRows th:nth-child(4) {
+ width: 28%;
+}
+#modelRows td {
+ overflow-wrap: anywhere;
+}
+#modelRows td .small {
+ max-width: 32em;
+}
+.model-mobile-list {
+ display: none;
+}
+.evidence-summary .small {
+ margin-top: 6px;
+}
+.settings-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 20px 24px;
+ max-width: 780px;
+ margin-bottom: 20px;
+}
+.settings-grid label {
+ margin: 0;
+}
+.setting-note {
+ margin: 8px 0 0;
+}
+.section-actions {
+ margin-top: 20px;
+}
+.advanced-settings {
+ border-block: 1px solid var(--line);
+ padding: 12px 0;
+ margin: 20px 0;
+}
+.advanced-settings > summary {
+ cursor: pointer;
+}
+.advanced-settings .settings-grid {
+ margin-top: 20px;
+}
+.status-rows {
+ margin: 16px 0 0;
+}
+.status-rows > div {
+ display: grid;
+ grid-template-columns: 140px minmax(0, 1fr);
+ gap: 16px;
+ padding: 12px 0;
+ border-bottom: 1px solid var(--line);
+}
+.status-rows > div:last-child {
+ border: 0;
+}
+.status-rows dt {
+ color: var(--muted);
+ font-size: 12px;
+}
+.status-rows dd {
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+.path-value {
+ margin-top: 8px;
+}
+.api-panel > summary {
+ cursor: pointer;
+ padding: 16px 20px;
+ font-weight: 500;
+}
+.api-example {
+ padding: 20px;
+ margin: 0;
+ border-top: 1px solid var(--line);
+ background: var(--sidebar);
+ font: 12px/20px var(--mono);
+}
+.toolbar input {
+ flex: 1;
+ min-width: 140px;
+}
+.toolbar {
+ margin: 16px 0;
+}
+.log-toolbar {
+ padding: 16px 20px;
+ margin: 0;
+ border-bottom: 1px solid var(--line);
+}
+.log-toolbar input {
+ flex: 1;
+}
+.log-toolbar .small {
+ margin-left: auto;
+}
+#logs {
+ --log-columns: 72px minmax(110px, 1.5fr) 80px minmax(100px, 1fr) 100px minmax(90px, 1fr) 14px;
+}
+.log-head,
+.log-main {
+ display: grid;
+ grid-template-columns: var(--log-columns);
+ gap: 12px;
+ align-items: center;
+}
+.log-head {
+ font-size: 12px;
+ color: var(--muted);
+ padding: 12px 20px;
+ background: var(--sidebar);
+ border-bottom: 1px solid var(--line);
+}
+.log-main {
+ min-height: 96px;
+ padding: 16px 20px;
+ font-size: 14px;
+ line-height: 22px;
+ width: 100%;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ border-radius: 0;
+ text-align: left;
+ font-weight: 400;
+}
+.log-main:hover {
+ background: var(--inset);
+}
+.log-main .small {
+ margin-top: 4px;
+}
+.log-cell {
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+.cell-label {
+ display: none;
+ color: var(--muted);
+ font-size: 12px;
+}
+.log-model {
+ font-weight: 500;
+}
+.log-latency {
+ display: grid;
+ gap: 4px;
+}
+.latency-line {
+ display: flex;
+ justify-content: space-between;
+ gap: 6px;
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+}
+.latency-line > span {
+ color: var(--muted);
+}
+.latency-line strong {
+ font-weight: 400;
+ white-space: nowrap;
+}
+.latency-line.good strong {
+ color: var(--good);
+}
+.latency-line.warn strong {
+ color: var(--warn);
+}
+.latency-line.bad strong {
+ color: var(--bad);
+}
+.chevron {
+ color: var(--muted);
+ font-size: 20px;
+}
+.detail-section {
+ padding: 24px 0;
+ border-bottom: 1px solid var(--line);
+}
+.detail-section:first-child {
+ padding-top: 0;
+}
+.detail-section:last-child {
+ border-bottom: 0;
+}
+.detail-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 20px;
+ margin: 0;
+}
+.detail-grid dt {
+ color: var(--muted);
+ font-size: 12px;
+}
+.detail-grid dd {
+ margin: 6px 0 0;
+ font-size: 14px;
+ overflow-wrap: anywhere;
+ font-variant-numeric: tabular-nums;
+}
+.log-error {
+ margin: 16px 0 0;
+ padding: 12px;
+ border: 1px solid var(--bad);
+ background: var(--bad-soft);
+ color: var(--bad);
+ overflow-wrap: anywhere;
+}
+.log-error pre {
+ margin: 8px 0 0;
+ font: 12px/20px var(--mono);
+ max-height: 260px;
+ overflow: auto;
+}
+.pager {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ justify-content: flex-end;
+ padding: 16px 20px;
+}
+.pager > span {
+ margin-right: auto;
+}
+dialog {
+ border: 1px solid var(--line);
+ border-radius: 4px;
+ background: var(--surface);
+ color: var(--ink);
+ padding: 24px;
+ width: min(520px, calc(100vw - 32px));
+ max-height: 90dvh;
+ overflow: auto;
+ box-shadow: 0 16px 70px #0003;
+}
+dialog::backdrop {
+ background: rgba(0, 0, 0, 0.5);
+}
+dialog h2 {
+ font-size: 20px;
+ line-height: 28px;
+ font-weight: 500;
+ margin-bottom: 20px;
+}
+dialog .actions {
+ justify-content: flex-end;
+ margin-top: 24px;
+}
+dialog label input,
+dialog label select,
+dialog textarea {
+ width: 100%;
+}
+dialog label.inline input {
+ width: auto;
+}
+.dialog-wide {
+ width: min(760px, calc(100vw - 32px));
+}
+.catalog-list {
+ max-height: 390px;
+ overflow: auto;
+ border: 1px solid var(--line);
+ margin-top: 16px;
+}
+.catalog-list label {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ margin: 0;
+ border-bottom: 1px solid var(--line);
+}
+.catalog-list label input {
+ width: auto;
+}
+.catalog-list label:last-child {
+ border: 0;
+}
+.catalog-list label > span {
+ flex: 1;
+ min-width: 0;
+}
+.catalog-list label > span:last-child {
+ flex: 0 0 auto;
+}
+.result {
+ white-space: pre-wrap;
+ max-height: 320px;
+ overflow: auto;
+ background: var(--inset);
+ padding: 16px;
+ font-size: 12px;
+}
+.toast {
+ position: fixed;
+ bottom: 24px;
+ right: 24px;
+ max-width: min(420px, calc(100vw - 32px));
+ background: var(--ink);
+ color: var(--page);
+ border-radius: 2px;
+ padding: 12px 16px;
+ z-index: 60;
+ font-size: 14px;
+}
+.form-error {
+ margin-top: 12px;
+ white-space: pre-wrap;
+ font-size: 14px;
+ min-height: 22px;
+}
+.form-error:empty {
+ margin-top: 0;
+ min-height: 0;
+}
+pre {
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+#confirmText {
+ white-space: pre-wrap;
+}
+.detail-drawer {
+ position: fixed;
+ inset: 0 0 0 auto;
+ margin: 0;
+ width: min(480px, 100vw);
+ max-width: 100vw;
+ height: 100dvh;
+ max-height: 100dvh;
+ padding: 0;
+ border-radius: 0;
+ border-width: 0 0 0 1px;
+ overflow: hidden;
+}
+.detail-drawer[open] {
+ display: flex;
+ flex-direction: column;
+}
+.drawer-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 24px;
+ border-bottom: 1px solid var(--line);
+ flex-shrink: 0;
+}
+.drawer-head > div {
+ min-width: 0;
+}
+.drawer-head h2 {
+ overflow-wrap: anywhere;
+ margin: 4px 0 0;
+ font-size: 20px;
+}
+.drawer-head p {
+ margin: 0;
+}
+.drawer-body {
+ padding: 24px;
+ overflow: auto;
+ overscroll-behavior: contain;
+ flex: 1;
+}
+.drawer-body .actions {
+ justify-content: flex-start;
+}
+.drawer-body .code {
+ font-size: 12px;
+}
+.evidence-source h3 {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+[hidden],
+.view[hidden] {
+ display: none !important;
+}
+@media (max-width: 1180px) {
+ .credential-workspace {
+ grid-template-columns: 1fr;
+ }
+ .account-selector {
+ display: flex;
+ overflow: auto;
+ gap: 8px;
+ padding-bottom: 4px;
+ }
+ .selector-heading {
+ display: none;
+ }
+ button.account-choice {
+ min-width: 180px;
+ max-width: 260px;
+ flex-shrink: 0;
+ border-color: var(--line);
+ }
+ button.account-choice.selected {
+ border-color: var(--accent);
+ background: var(--soft);
+ box-shadow: none;
+ }
+ .log-head {
+ display: none;
+ }
+ .log-main {
+ grid-template-columns: 90px minmax(0, 1fr) 95px;
+ gap: 16px;
+ }
+ .log-cell:nth-child(1) {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ .log-cell:nth-child(2) {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ .log-cell:nth-child(3) {
+ grid-column: 3;
+ grid-row: 1;
+ }
+ .log-cell:nth-child(4) {
+ grid-column: 1;
+ grid-row: 2;
+ }
+ .log-cell:nth-child(5) {
+ grid-column: 2;
+ grid-row: 2;
+ max-width: 160px;
+ }
+ .log-cell:nth-child(6) {
+ grid-column: 3;
+ grid-row: 2;
+ }
+ .log-main > .chevron {
+ display: none;
+ }
+ .cell-label {
+ display: block;
+ margin-bottom: 4px;
+ }
+ .summary > div {
+ padding: 16px;
+ }
+ .summary b {
+ font-size: 25px;
+ }
+ .topbar {
+ padding-inline: 20px;
+ }
+}
+@media (max-width: 960px) {
+ .breadcrumb-root,
+ .breadcrumb-separator {
+ display: none;
+ }
+ .title {
+ flex-wrap: wrap;
+ gap: 16px;
+ }
+ .title .actions {
+ justify-content: flex-start;
+ }
+ .summary {
+ grid-template-columns: 1fr 1fr;
+ }
+ .summary > div:nth-child(2) {
+ border-right: 0;
+ }
+ .summary > div:nth-child(-n + 2) {
+ border-bottom: 1px solid var(--line);
+ }
+ .health-layout {
+ grid-template-columns: 1fr;
+ }
+ .health-score {
+ border-right: 0;
+ border-bottom: 1px solid var(--line);
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0 20px;
+ }
+ .health-ratio {
+ margin: 8px 0 0;
+ grid-column: 1;
+ }
+ .health-bar {
+ grid-column: 2;
+ grid-row: 1;
+ align-self: center;
+ }
+ .health-legend {
+ grid-column: 2;
+ grid-row: 2;
+ align-self: center;
+ margin: 0;
+ }
+ .health-row {
+ grid-template-columns: minmax(0, 1fr) auto;
+ }
+ .health-meta {
+ grid-column: 1;
+ grid-row: 2;
+ }
+ .health-row .badge {
+ grid-column: 2;
+ grid-row: 1/3;
+ }
+ .balance-cell {
+ align-items: flex-start;
+ gap: 16px;
+ }
+ .balance-parts {
+ gap: 16px;
+ }
+ .settings-grid {
+ grid-template-columns: 1fr;
+ }
+ #busy {
+ display: none;
+ }
+}
+@media (max-width: 767px) {
+ .shell,
+ .sidebar-collapsed {
+ display: block;
+ --sidebar-width: 224px;
+ }
+ .sidebar {
+ position: fixed;
+ inset: 0 auto 0 0;
+ width: 256px;
+ height: 100dvh;
+ z-index: 50;
+ padding: 24px 16px;
+ visibility: hidden;
+ transform: translateX(-100%);
+ }
+ .sidebar-open .sidebar {
+ visibility: visible;
+ transform: none;
+ }
+ .sidebar-open .sidebar-scrim {
+ display: block;
+ position: fixed;
+ inset: 0;
+ border: 0;
+ border-radius: 0;
+ background: #0008;
+ z-index: 40;
+ padding: 0;
+ }
+ .sidebar-collapsed .brand-copy,
+ .sidebar-collapsed .workspace-name,
+ .sidebar-collapsed nav button > span,
+ .sidebar-collapsed .nav-bottom {
+ display: revert;
+ }
+ .sidebar-collapsed .brand-copy {
+ display: grid;
+ }
+ .sidebar-collapsed .workspace-name {
+ display: flex;
+ }
+ .sidebar-collapsed nav {
+ margin: 0;
+ }
+ .sidebar-collapsed nav button {
+ justify-content: flex-start;
+ padding: 10px 12px;
+ }
+ .topbar {
+ padding: 0 12px;
+ height: 64px;
+ gap: 8px;
+ }
+ .breadcrumbs {
+ gap: 8px;
+ font-size: 12px;
+ }
+ .header-actions {
+ gap: 4px;
+ }
+ .header-actions #authButton {
+ font-size: 12px;
+ padding-inline: 10px;
+ }
+ button,
+ input,
+ select {
+ min-height: 44px;
+ }
+ button.icon-button {
+ width: 44px;
+ height: 44px;
+ min-height: 44px;
+ }
+ button.compact {
+ min-height: 44px;
+ font-size: 12px;
+ }
+ main {
+ padding: 24px 16px 40px;
+ }
+ .title {
+ margin-bottom: 24px;
+ }
+ .title .actions {
+ width: 100%;
+ }
+ .title .actions > button {
+ flex: 1;
+ padding-inline: 8px;
+ }
+ .page-description {
+ font-size: 14px;
+ }
+ h1 {
+ font-size: 24px;
+ }
+ .panel {
+ margin: 20px 0;
+ }
+ .panel-head,
+ .panel-body {
+ padding: 16px;
+ }
+ .panel-head {
+ flex-wrap: wrap;
+ }
+ .date-range label {
+ flex: 1;
+ }
+ .date-range input {
+ width: 100%;
+ max-width: none;
+ }
+ .summary {
+ margin-block: 20px;
+ }
+ .summary > div {
+ padding: 16px;
+ }
+ .summary b {
+ font-size: 24px;
+ line-height: 32px;
+ }
+ .health-row {
+ padding: 16px;
+ gap: 8px 12px;
+ }
+ .health-score {
+ padding: 16px;
+ }
+ .account-selector {
+ gap: 8px;
+ }
+ .account-top {
+ padding: 16px;
+ flex-wrap: wrap;
+ }
+ .account-top > .actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
+ .account-grid {
+ padding: 20px 16px;
+ grid-template-columns: 1fr;
+ }
+ .balance-cell {
+ flex-wrap: wrap;
+ gap: 16px;
+ padding-bottom: 20px;
+ margin-bottom: 0;
+ }
+ .balance-parts {
+ width: 100%;
+ justify-content: space-between;
+ }
+ .quota-window {
+ padding: 20px 0 !important;
+ border: 0;
+ border-bottom: 1px solid var(--line);
+ }
+ .quota-window:last-child {
+ padding-bottom: 0 !important;
+ }
+ .quota-value {
+ font-size: 24px;
+ margin-bottom: 12px;
+ }
+ .meter-label {
+ margin-bottom: 8px;
+ }
+ .quota-note {
+ margin-top: 8px;
+ }
+ .account-foot {
+ padding: 16px;
+ }
+ .diagnostic-overview {
+ padding: 16px;
+ align-items: flex-start;
+ }
+ .diagnostic-overview > button {
+ flex-shrink: 0;
+ font-size: 12px;
+ padding-inline: 8px;
+ }
+ .diagnostic-panel > details > summary {
+ padding: 14px 16px;
+ min-height: 44px;
+ }
+ .panel-footer {
+ padding: 0 16px 16px;
+ }
+ .model-toolbar {
+ padding: 16px;
+ gap: 16px;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ }
+ .model-toolbar label {
+ min-width: 0;
+ }
+ .model-toolbar label:first-child {
+ grid-column: 1/-1;
+ }
+ #modelRows table {
+ display: none;
+ }
+ .model-mobile-list {
+ display: block;
+ }
+ .model-item {
+ padding: 16px;
+ border-bottom: 1px solid var(--line);
+ }
+ .model-item:last-child {
+ border-bottom: 0;
+ }
+ .model-item .code {
+ font-size: 14px;
+ line-height: 22px;
+ }
+ .model-item .small {
+ margin-top: 6px;
+ }
+ .model-item .evidence-summary {
+ margin-top: 12px;
+ }
+ .model-item > .actions {
+ margin-top: 16px;
+ }
+ .log-toolbar {
+ padding: 16px;
+ gap: 8px;
+ }
+ .log-toolbar input {
+ flex-basis: 100%;
+ }
+ .log-toolbar .small {
+ width: 100%;
+ margin: 4px 0 0;
+ }
+ .log-main {
+ padding: 16px;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: 16px;
+ }
+ .log-cell:nth-child(1) {
+ grid-column: 1;
+ grid-row: 1;
+ }
+ .log-cell:nth-child(2) {
+ grid-column: 1/3;
+ grid-row: 2;
+ }
+ .log-cell:nth-child(3) {
+ grid-column: 2;
+ grid-row: 1;
+ justify-self: end;
+ }
+ .log-cell:nth-child(4) {
+ grid-column: 1;
+ grid-row: 3;
+ }
+ .log-cell:nth-child(5) {
+ grid-column: 2;
+ grid-row: 3;
+ }
+ .log-cell:nth-child(6) {
+ grid-column: 1/3;
+ grid-row: 4;
+ }
+ .log-cell:nth-child(2) > .small {
+ display: none;
+ }
+ .pager {
+ padding: 16px;
+ flex-wrap: wrap;
+ }
+ .pager > span {
+ width: 100%;
+ margin: 0 0 4px;
+ }
+ .status-rows > div {
+ grid-template-columns: 1fr;
+ gap: 6px;
+ }
+ .settings-grid {
+ gap: 20px;
+ }
+ .detail-drawer {
+ width: 100vw;
+ border: 0;
+ }
+ .drawer-head {
+ padding: 20px 16px;
+ }
+ .drawer-body {
+ padding: 20px 16px;
+ }
+ .detail-grid {
+ gap: 20px 16px;
+ }
+ .dialog-wide,
+ dialog {
+ padding: 20px 16px;
+ width: calc(100vw - 24px);
+ max-height: calc(100dvh - 24px);
+ }
+ .detail-drawer {
+ width: 100vw;
+ max-height: 100dvh;
+ padding: 0;
+ }
+ .catalog-list label {
+ flex-wrap: wrap;
+ }
+ .catalog-list label > span:last-child {
+ flex: 1 1 100%;
+ padding-left: 28px;
+ }
+ .toast {
+ right: 16px;
+ bottom: 16px;
+ }
+ .api-example {
+ padding: 16px;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation: none !important;
+ transition: none !important;
+ scroll-behavior: auto !important;
+ }
+}
diff --git a/internal/bridge/ui/console.js b/internal/bridge/ui/console.js
index 7b91f22..392c372 100644
--- a/internal/bridge/ui/console.js
+++ b/internal/bridge/ui/console.js
@@ -1,265 +1,1574 @@
'use strict';
-const $=id=>document.getElementById(id), esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
-const state={view:'overview',config:null,credentials:[],activeCredential:null,catalog:[],selected:new Set(),offset:0,total:0,key:'',authType:'bearer',authenticated:false,blocked:false,busy:0};
-const API_BASE=document.querySelector('meta[name="cpa-api-base"]').content;
-const compactNumber=v=>v==null?'—':Number(v).toLocaleString('en',{notation:'compact',maximumFractionDigits:1});
-const shortWhen=v=>v?new Date(v).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hour12:false}):'未知';
-const fmt=(v,d=0)=>v===null||v===undefined?'—':Number(v).toLocaleString('zh-CN',{maximumFractionDigits:d}), when=v=>v?new Date(v).toLocaleString('zh-CN'):'未知', percent=(a,b)=>a!=null&&b>0?Math.max(0,Math.min(100,a/b*100)):null;
-function toast(text){$('toast').textContent=text;$('toast').hidden=false;clearTimeout(toast.timer);toast.timer=setTimeout(()=>$('toast').hidden=true,3500)}
-function globalError(e){$('globalError').textContent=e.message||String(e);$('globalError').hidden=false}
-function dialogError(id,e){$(id).querySelector('.form-error').textContent=e.message||String(e)}
-const dialogOrigins=new Map();
-function openDialog(id){const dialog=$(id);dialog.querySelectorAll('.form-error').forEach(x=>x.textContent='');if(!dialog.open){const node=document.activeElement;dialogOrigins.set(id,{node,data:{...node?.dataset},view:state.view});dialog.showModal()}}
-function restoreDialogFocus(id){const origin=dialogOrigins.get(id);dialogOrigins.delete(id);if(!origin||document.querySelector('dialog[open]'))return;if(origin.node?.isConnected&&(!origin.node.getClientRects||origin.node.getClientRects().length)&&!origin.node.closest?.('dialog:not([open])')){origin.node.focus();return}for(const key of ['logDetail','modelDetail','modelEdit','modelTest','accountEdit','accountSelect']){if(origin.data[key]!==undefined){const node=[...document.querySelectorAll('button')].find(x=>x.dataset[key]===origin.data[key]&&(!x.getClientRects||x.getClientRects().length)&&!x.closest?.('dialog:not([open])'));if(node){node.focus();return}}}const modelIndex=origin.data.modelEdit??origin.data.modelTest;const modelButton=[...document.querySelectorAll('[data-model-detail]')].find(x=>x.dataset.modelDetail===modelIndex&&x.getClientRects().length);if(modelButton){modelButton.focus();return}if(origin.view===state.view)$('mainContent').focus()}
-function showDetail(title,subtitle,html){$('detailTitle').textContent=title;$('detailSubtitle').textContent=subtitle;$('detailContent').innerHTML=html;openDialog('detailDialog');$('detailTitle').focus()}
-function statusRows(rows){return '
'+rows.filter(([,value])=>value!==undefined&&value!==null&&value!=='').map(([label,value])=>`- ${esc(label)}
- ${esc(value)}
`).join('')+'
'}
-function apiURL(path){return new URL(API_BASE+path,location.origin).href}
-async function api(path,options={}){if(state.blocked&&!options.verify)throw new Error('管理访问已暂停,请重新验证登录');const headers={...(window.CommandCodeBridgeAuthHeaders?.()||{}),...options.headers};if(state.key){delete headers.Authorization;delete headers['X-Management-Key'];headers[state.authType==='x-management-key'?'X-Management-Key':'Authorization']=state.authType==='x-management-key'?state.key:'Bearer '+state.key}if(options.body!==undefined)headers['Content-Type']='application/json';state.busy++;$('busy').textContent='正在更新';try{const response=await fetch(apiURL(path),{method:options.method||'GET',headers,body:options.body===undefined?undefined:JSON.stringify(options.body),signal:AbortSignal.timeout(options.timeout||650000)});const text=await response.text();let data;try{data=JSON.parse(text)}catch{data={error:text.slice(0,200)}}if(!response.ok){if((response.status===401||response.status===403)&&path!=='/models/test'){state.authenticated=false;state.blocked=true;resetDiagnosticViews();if(response.status===401){window.CommandCodeBridgeClearAuth?.(headers);state.key=''}openDialog('authDialog')}throw new Error(data.error||`HTTP ${response.status}`)}return data}finally{state.busy--;$('busy').textContent=state.busy?'正在更新':''}}
-async function confirmed(title,text){$('confirmTitle').textContent=title;$('confirmText').textContent=text;openDialog('confirmDialog');return new Promise(resolve=>{const dialog=$('confirmDialog');let settled=false;const onclose=()=>end(false);const end=value=>{if(settled)return;settled=true;dialog.removeEventListener('close',onclose);if(dialog.open)dialog.close();resolve(value)};dialog.addEventListener('close',onclose);$('confirmCancel').onclick=()=>end(false);$('confirmYes').onclick=()=>end(true);dialog.oncancel=e=>{e.preventDefault();end(false)}})}
-function period(){const range=$('statsRange').value;let from='',to='';if(range==='today'){const date=new Date();date.setHours(0,0,0,0);from=date.toISOString()}else if(range==='24h')from=new Date(Date.now()-86400000).toISOString();else if(range==='custom'){if($('statsFrom').value)from=new Date($('statsFrom').value).toISOString();if($('statsTo').value)to=new Date($('statsTo').value).toISOString()}const q=new URLSearchParams();if(from)q.set('from',from);if(to)q.set('to',to);return q.size?'?'+q.toString():''}
-async function loadConfig(){const identity=diagnosticIdentity(),config=await api('/config');if(!state.authenticated||identity!==diagnosticIdentity())return;state.config=config;renderModels();const f=$('settingsForm');for(const k of ['timeout_seconds','first_byte_timeout_seconds','stream_idle_timeout_seconds','log_retention','usage_refresh_seconds','statistics_retention_days'])f.elements[k].value=state.config[k]??0;for(const k of ['cpamp_install_dir','cpamp_config_path','cpamp_panel_path'])f.elements[k].value=state.config[k]||'';f.elements.max_response_mb.value=state.config.max_response_bytes/1048576;f.elements.zdr.checked=state.config.zdr;$('dataDir').textContent=state.config.data_dir;const model=state.config.models[0]?.id||'你的模型名称';$('apiExample').textContent=JSON.stringify({model,messages:[{role:'user',content:'你好'}],stream:true},null,2);if(state.view==='settings')await Promise.all([loadCPAMPPanel(),loadCompatibility(),loadStorage()])}
-function showCPAMPPanel(result){
- state.cpampPanel=result;
- const served=result.served_page;
- const rows=[['检测结果',result.message],['目标目录',result.install_dir||result.default_install_dir],['运行实例',result.pid?'PID '+result.pid:'未检测到'],['配置文件',result.config_path],['配置方式',result.configuration_mode==='environment'?'环境变量(尚无配置文件)':result.configuration_mode],['面板文件',result.panel_path],['当前服务页面',served?.checked?(served.native_quota?'含原生额度组件':'缺少原生额度组件')+(served.matches?',与内置版本一致':''):'尚未完成 HTTP 核验'],['预计变化',(result.planned_changes||[]).join('\n')||'无'],['可选配置',result.candidates?.join('\n')],['备份文件',result.backups?.join('\n')],['生效条件',result.hot_update_ready?'原路径更新,刷新浏览器': '首次启用后重启 Manager Server']];
- if(!result.available)rows.push(['安装不可用','当前构建未包含面板,请使用官方发布包']);
- $('cpampPanelStatus').innerHTML=statusRows(rows);
- $('installCPAMPPanel').textContent=result.hot_update_ready?'更新原生面板':'安装原生面板';$('installCPAMPPanel').disabled=!result.available||result.installable===false||result.active;
-}
-async function loadCPAMPPanel(){const identity=diagnosticIdentity(),result=await api('/cpamp/panel');if(state.authenticated&&identity===diagnosticIdentity())showCPAMPPanel(result)}
-async function saveCPAMPPaths(){const f=$('settingsForm'),paths={};for(const name of ['cpamp_install_dir','cpamp_config_path','cpamp_panel_path'])paths[name]=f.elements[name].value.trim();if(Object.entries(paths).some(([name,value])=>value!==(state.config?.[name]||'')))state.config=await api('/config',{method:'PUT',body:paths})}
-async function loadCredentials(){const identity=diagnosticIdentity(),result=await api('/credentials');if(!state.authenticated||identity!==diagnosticIdentity())return;state.credentials=result.credentials;renderCredentials();renderHealth()}
-const diagnosticStatus={confirmed:'登记一致',disabled:'停用已确认',missing:'未登记',duplicate:'重复登记',routing_mismatch:'路由不一致',disabled_mismatch:'停用未同步',pending:'等待宿主确认',unknown:'未确认',ok:'通过',mismatch:'版本不匹配'};
-const diagnosticSources={account_query:'账户查询',plugin_test:'插件直测',cpa_request:'CPA 请求'};
-function diagnosticTone(status){return ['ok','confirmed','disabled','success'].includes(status)?'good':['pending','unknown','partial'].includes(status)?'warn':'bad'}
-function diagnosticCounts(report){return {errors:report.credentials.filter(x=>!['confirmed','disabled','unknown','pending'].includes(x.status)).length+(report.unmatched_registrations||0),pending:report.credentials.filter(x=>['unknown','pending'].includes(x.status)).length}}
-function renderDiagnostics(report){
- const rows=[...report.credentials].sort((a,b)=>({bad:0,warn:1,good:2})[diagnosticTone(a.status)]-({bad:0,warn:1,good:2})[diagnosticTone(b.status)]).map(row=>`${esc(row.label||'凭证')}${esc(diagnosticStatus[row.status]||row.status)}${row.cooldown?' 部分模型冷却中':''}
${esc(row.message)}
${esc(row.action)}
${row.evidence.map(item=>`${esc(diagnosticSources[item.source]||item.source)}:${esc(({unknown:'暂无可确认记录',success:'成功',error:'失败',ok:'正常',partial:'部分成功',disabled:'已停用'})[item.status]||item.status)}${item.checked_at?' · '+esc(when(item.checked_at)):''}
`).join('')}`).join('');
- return `检查时间:${esc(when(report.checked_at))} · 映射模型 ${esc(report.models)} 个
${esc(report.host_message)}
${report.models===0?'尚未配置模型映射,请在模型映射页导入。
':''}${report.unmatched_registrations?`另有 ${esc(report.unmatched_registrations)} 个宿主登记无法匹配当前凭证,请检查 CPA 加载日志。
`:''}${rows||'暂无凭证,请先添加凭证。
'}`;
-}
-function renderCompatibility(report){return `检查时间:${esc(when(report.checked_at))} · 插件 ${esc(report.version)}
${report.checks.map(check=>`${esc(({plugin:'插件接口',host_version:'CPA 版本',page:'实际服务页面',page_quota:'额度页面',page_recent_requests:'最近请求页面',page_windows:'周期历史页面',snapshot_backend:'快照后端',browser_cache:'浏览器缓存'})[check.name]||check.name)}${esc(diagnosticStatus[check.status]||({missing:'缺少能力'})[check.status]||check.status)}${esc(check.message)}
${check.action?`
${esc(check.action)}
`:''}
`).join('')}`}
-function diagnosticIdentity(){return JSON.stringify([state.sessionRevision||0,state.key,state.authType,window.CommandCodeBridgeAuthHeaders?.()||{}])}
-function resetDiagnosticViews(){
- state.sessionRevision=(state.sessionRevision||0)+1;state.diagnosticRevision=(state.diagnosticRevision||0)+1;state.compatibilityRevision=(state.compatibilityRevision||0)+1;state.modelEvidenceRevision=(state.modelEvidenceRevision||0)+1;state.logRevision=(state.logRevision||0)+1;
- state.modelEvidence=null;state.modelCredential='';state.config=null;state.credentials=[];state.logs=[];state.total=0;state.offset=0;state.cpampPanel=null;state.detail=null;state.catalog=[];state.selected?.clear();
- for(const id of ['credentialDialog','modelDialog','catalogDialog','testDialog','confirmDialog'])$(id).close?.();
- for(const id of ['credentialForm','modelForm','testForm'])$(id).reset?.();
- $('testResult').textContent='';$('catalogRows').innerHTML='';
- $('detailDialog').close?.();$('detailContent').innerHTML='';
- for(const id of ['modelCredential','modelRows','credentialRows','credentialHealth','summary','modelStats','logRows','logCount','logPage','cpampPanelStatus','dataDir','apiExample'])$(id).innerHTML='';
- $('authButton').textContent='管理登录';$('modelEvidenceStatus').textContent='登录后重新读取模型证据';$('storageStatus').textContent='登录后重新读取存储状态';$('diagnosticSummary').textContent='登录后重新诊断';$('diagnosticResults').textContent='登录后重新诊断';$('compatibilityResults').textContent='登录后重新检查';
- $('installCPAMPPanel').disabled=true;$('prevLogs').disabled=true;$('nextLogs').disabled=true;
-}
-async function loadDiagnostics(){
- const revision=state.diagnosticRevision=(state.diagnosticRevision||0)+1,identity=diagnosticIdentity();
- try{const report=await api('/diagnostics',{timeout:15000});if(state.authenticated&&revision===state.diagnosticRevision&&identity===diagnosticIdentity()){$('diagnosticResults').innerHTML=renderDiagnostics(report);const n=diagnosticCounts(report);$('diagnosticSummary').textContent=`${n.errors} 项异常 · ${n.pending} 项待确认 · ${when(report.checked_at)}`}}catch(e){if(revision===state.diagnosticRevision&&identity===diagnosticIdentity()){$('diagnosticSummary').textContent='诊断刷新失败,详情保留上次结果';if(!$('diagnosticResults').innerHTML)$('diagnosticResults').textContent='诊断失败,请重新登录或重试。'};throw e}
-}
-async function loadCompatibility(){
- const revision=state.compatibilityRevision=(state.compatibilityRevision||0)+1,identity=diagnosticIdentity();
- try{const report=await api('/compatibility',{timeout:15000});if(state.authenticated&&revision===state.compatibilityRevision&&identity===diagnosticIdentity())$('compatibilityResults').innerHTML=renderCompatibility(report)}catch(e){if(revision===state.compatibilityRevision&&identity===diagnosticIdentity())$('compatibilityResults').textContent='兼容检查失败,请重新登录或重试。';throw e}
-}
-$('runDiagnostics').onclick=()=>loadDiagnostics().catch(globalError);
-$('checkCompatibility').onclick=()=>loadCompatibility().catch(globalError);
-$('copyDiagnostics').onclick=async()=>{
- const identity=diagnosticIdentity();
- try{const summary=await api('/diagnostics/summary',{timeout:15000});if(!state.authenticated||identity!==diagnosticIdentity())return;const text=JSON.stringify(summary,null,2);try{await navigator.clipboard.writeText(text);toast('脱敏诊断摘要已复制')}catch{const url=URL.createObjectURL(new Blob([text],{type:'application/json'})),link=document.createElement('a');link.href=url;link.download='commandcodebridge-diagnostics.json';link.click();setTimeout(()=>URL.revokeObjectURL(url),1000);toast('脱敏诊断摘要已导出')}}catch(e){globalError(e)}
-};
-async function loadStats(){const identity=diagnosticIdentity(),d=await api('/statistics'+period()),t=d.totals;if(!state.authenticated||identity!==diagnosticIdentity())return;const metrics=[['请求数',fmt(t.requests),`${fmt(t.success)} 成功 · ${fmt(t.failed)} 失败`],['输入 / 输出 tokens',compactNumber(t.input_tokens)+' / '+compactNumber(t.output_tokens),`${fmt(t.with_usage)} 次有用量记录`],['缓存命中',d.cache_hit_percent==null?'—':fmt(d.cache_hit_percent,1)+'%',`${fmt(t.cached_tokens)} tokens 读取`],['输出速率',d.output_tokens_per_second==null?'—':fmt(d.output_tokens_per_second,1)+' tok/s',d.average_ttft_ms==null?'首字时间未知':'首字 '+fmt(d.average_ttft_ms/1000,2)+'s']];$('summary').innerHTML=metrics.map(([name,value,hint],i)=>`${name}${value}${hint}
`).join('');const rows=Object.values(d.models).sort((a,b)=>b.requests-a.requests);$('modelStats').innerHTML=rows.length?`| 模型 | 请求 | 成功 | 输入 tokens | 输出 tokens | 缓存 tokens |
${rows.map(x=>`| ${esc(x.model)} | ${fmt(x.requests)} | ${fmt(x.success)} | ${fmt(x.input_tokens)} | ${fmt(x.output_tokens)} | ${fmt(x.cached_tokens)} |
`).join('')}
`:'暂无请求发起请求后显示用量
';$('statisticsPrecision').textContent=(d.bucket_seconds>60?d.range_note:'当前范围为分钟级统计')+((d.storage?.recovery_warnings||[]).some(w=>w.file==='statistics.json')?' · 发生过统计恢复,历史可能不完整,请查看设置中的恢复记录':'');renderStorage(d.storage);if(d.storage_error)globalError(new Error(d.storage_error))}
-const healthLabels={healthy:['正常','good'],warning:['需关注','warn'],invalid:['鉴权异常','bad'],cooldown:['冷却中','warn'],disabled:['已停用','neutral'],unknown:['待验证','neutral']};
-function healthBadge(h){const [label,tone]=healthLabels[h?.status]||healthLabels.unknown;return `${label}`}
-function renderHealth(){
- const rows=state.credentials;
- if(!rows.length){$('credentialHealth').innerHTML='暂无凭据
';return}
- const healthy=rows.filter(c=>c.health?.status==='healthy').length,enabled=rows.filter(c=>!c.disabled).length;
- const counts=Object.fromEntries(Object.keys(healthLabels).map(k=>[k,rows.filter(c=>(c.health?.status||'unknown')===k).length]));
- const bar=Object.entries(counts).filter(([,n])=>n).map(([k,n])=>``).join('');
- const legend=Object.entries(counts).filter(([,n])=>n).map(([k,n])=>`${healthLabels[k][0]} ${n}`).join('');
- $('credentialHealth').innerHTML=`正常 / 已启用${healthy} / ${enabled}
${bar}
${legend}
${rows.map(c=>{
- const h=c.health||{},recent=h.recent_requests||0;
- const detail=h.cooldown_until?'重试 '+shortWhen(h.cooldown_until):['invalid','warning'].includes(h.status)?h.reason||'查询异常':h.checked_at?'验证于 '+shortWhen(h.checked_at):h.status==='disabled'?'不参与调度':'尚未验证';
- const request=h.last_request_at?`最近请求 ${h.last_status} · ${shortWhen(h.last_request_at)}`:'暂无近期请求';
- return `
`
- }).join('')}
`;
-}
-function windowHTML(w,name){
- const p=percent(w?.used,w?.cap),unknown=p==null;
- return `${name}${unknown?'未知':fmt(p,1)+'%'}
${fmt(w?.used,2)} / ${fmt(w?.cap,2)} credits
${w?'':'窗口未知 · '}重置 ${esc(shortWhen(w?.reset_at))}
`;
-}
-function monthlyHTML(u){
- const w=(u.windows||[]).find(w=>w.name==='monthly');
- if(w)return windowHTML(w,'月额度窗口');
- return `月额度窗口剩余
${fmt(u.monthly_credits,3)} credits
已用 ${fmt(u.monthly_used_credits,3)} · 上限未知
重置 ${esc(shortWhen(u.subscription_end))}
`;
-}
-function renderCredentials(){
- const rows=state.credentials;
- if(!rows.length){state.activeCredential=null;$('credentialRows').innerHTML='暂无凭据
';return}
- if(!rows.some(c=>c.id===state.activeCredential))state.activeCredential=(rows.find(c=>!c.disabled)||rows[0]).id;
- const c=rows.find(c=>c.id===state.activeCredential),u=c.usage||{},windows=u.windows||[],plan=u.plan||'套餐未知';
- const unavailable=(u.unavailable||[]).map(x=>({credits:'余额',subscription:'订阅',summary:'本期用量'}[x]||x)).join('、');
- const selector=rows.map(c=>``).join('');
- $('credentialRows').innerHTML=`凭据${rows.length}
${selector}
${esc(c.label)}${healthBadge(c.health)}
${esc(plan)} · 优先级 ${c.priority}${c.shared_credentials>1?' · '+c.shared_credentials+' 把凭据共享额度':''}
剩余额度
${fmt(u.remaining_credits,3)}credits
月度${fmt(u.monthly_credits,3)}
购买${fmt(u.purchased_credits,3)}
免费${fmt(u.free_credits,3)}
${monthlyHTML(u)}${windowHTML(windows.find(w=>w.name==='fiveHour'),'5 小时窗口')}${windowHTML(windows.find(w=>w.name==='weekly'),'每周窗口')}
`;
-}
-const modelEvidenceLabels={unknown:'套餐权限未知',available:'最近实测成功',not_in_plan:'明确不在套餐',permission_unknown:'权限拒绝,原因未知',temporary_limit:'临时限流',temporary_error:'临时故障',authentication_error:'鉴权失败',expired:'证据已过期',disabled:'凭据已停用'};
-function evidenceTone(status){return status==='available'?'good':['not_in_plan','authentication_error'].includes(status)?'bad':['temporary_limit','temporary_error','permission_unknown','expired'].includes(status)?'warn':'neutral'}
-function modelEvidenceHTML(model){
- const row=state.modelEvidence?.items.find(x=>x.model===model),status=row?.status||'unknown';
- const latest=row?.evidence.filter(e=>e.checked_at).sort((a,b)=>new Date(b.checked_at)-new Date(a.checked_at))[0];
- return `${esc(modelEvidenceLabels[status]||'未知')}${latest?esc(diagnosticSources[latest.source]||latest.source)+' · '+esc(shortWhen(latest.checked_at)):'暂无可确认记录'}
`;
-}
-function modelEvidenceDetailHTML(model){
- const row=state.modelEvidence?.items.find(x=>x.model===model);
- return `${modelEvidenceHTML(model)}仅表示当前凭据与映射下的最近观察,不保证下一次调用成功。
`+(row?.evidence.length?row.evidence.map(e=>`${esc(diagnosticSources[e.source]||e.source)}${esc(modelEvidenceLabels[e.status]||'未知')}
- 观察时间
- ${esc(when(e.checked_at))}
- 重试时间
- ${esc(e.retry_at?when(e.retry_at):'未提供')}
- 客户端协议
- ${esc(e.client_protocol||'未知')}
- 上游协议
- ${esc(e.upstream_protocol||'未知')}
- HTTP 状态
- ${esc(e.http_status||'未提供')}
- 错误类型
- ${esc(e.error_kind||'无记录')}
- ZDR 请求
- ${e.zdr_requested?'已请求,保留策略待核实':'未请求'}
${e.status==='expired'?'此证据已超过 24 小时,当前权限待重新验证。
':''}`).join(''):'暂无插件直测或 CPA 请求记录。目录可见不代表套餐有权限。
');
-}
-async function loadModelEvidence(){
- if(!state.authenticated)return;
- if(state.detail==='model'){$('detailDialog').close();state.detail=null}
- const selector=$('modelCredential'),available=state.credentials;
- if(!available.some(c=>c.id===state.modelCredential))state.modelCredential=available.find(c=>!c.disabled)?.id||available[0]?.id||'';
- selector.innerHTML=available.map(c=>``).join('');selector.value=state.modelCredential;
- const revision=state.modelEvidenceRevision=(state.modelEvidenceRevision||0)+1,identity=diagnosticIdentity(),cid=state.modelCredential;
- state.modelEvidence=null;renderModels();$('modelEvidenceStatus').textContent=cid?'正在读取证据…':'请先添加凭据';
- if(!cid)return;
- try{const report=await api('/models/evidence?credential_id='+encodeURIComponent(cid));if(!state.authenticated||revision!==state.modelEvidenceRevision||identity!==diagnosticIdentity()||cid!==state.modelCredential)return;state.modelEvidence=report;renderModels();$('modelEvidenceStatus').textContent='读取于 '+when(report.checked_at)+' · 来源为保留日志,缺少证据不代表不可用'}catch(e){if(revision===state.modelEvidenceRevision){$('modelEvidenceStatus').textContent='读取失败,请刷新重试';state.modelEvidence=null;renderModels()}throw e}
-}
-function renderStorage(storage){if(!storage)return;$('storageStatus').innerHTML=statusRows([['统计精度',storage.precision],['最近成功写入',when(storage.last_success_at)],['待写文件',storage.pending_files?.join('、')||'无'],['待写开始',storage.pending_since?when(storage.pending_since):'无'],['统计桶数',fmt(storage.statistics_buckets)]])+(storage.recovery_warnings||[]).map(w=>`恢复记录${esc(w.file)}
${esc(w.message)}
${esc(when(w.checked_at))} · 恢复后的历史可能不完整
`).join('')}
-async function loadStorage(){const identity=diagnosticIdentity(),report=await api('/status');if(state.authenticated&&identity===diagnosticIdentity())renderStorage(report.storage)}
-function filteredModels(models,search,status){
- const text=search.trim().toLocaleLowerCase();return models.map((model,index)=>({model,index})).filter(({model})=>{const evidence=state.modelEvidence?.items.find(x=>x.model===model.id)?.status||'unknown';return (!text||[model.id,model.upstream_id,model.name].join(' ').toLocaleLowerCase().includes(text))&&(status==='all'||status==='available'&&evidence==='available'||status==='unknown'&&['unknown','expired','disabled'].includes(evidence)||status==='attention'&&!['available','unknown','expired','disabled'].includes(evidence))})
-}
-function renderModels(){
- const models=state.config?.models||[],rows=filteredModels(models,$('modelSearch').value,$('modelStatus').value);
- if(!models.length){$('modelRows').innerHTML='暂无模型映射从上游目录导入或手动添加模型。
';return}
- if(!rows.length){$('modelRows').innerHTML='没有匹配的模型调整名称或证据筛选,不会发起生成。
';return}
- const protocol=m=>m.supported_endpoints.includes('/chat/completions')?'OpenAI':'Anthropic';
- const actions=i=>``;
- $('modelRows').innerHTML=`| 客户端名称 / 上游模型 | 协议 | 思考映射 | 当前凭据证据 | 操作 |
${rows.map(({model:m,index:i})=>`${esc(m.id)} ${esc(m.upstream_id)} | ${protocol(m)} ${m.context_length?fmt(m.context_length)+' 上下文':''} | ${m.reasoning_mapping.enabled?'已启用':'透传'} | ${modelEvidenceHTML(m.id)} | ${actions(i)} |
`).join('')}
${rows.map(({model:m,index:i})=>`
${esc(m.id)}
${esc(m.upstream_id)}
${protocol(m)} · 思考${m.reasoning_mapping.enabled?'映射已启用':'透传'}
${modelEvidenceHTML(m.id)}${actions(i)}
`).join('')}
`;
-}
-function showModelDetail(index){
- const m=state.config?.models[index];if(!m)return;state.detail='model';
- showDetail(m.id,'模型详情 · '+(state.credentials.find(c=>c.id===state.modelCredential)?.label||'未选择凭据'),`模型路由
${statusRows([['上游模型',m.upstream_id],['协议',m.supported_endpoints.join('、')],['思考映射',m.reasoning_mapping.enabled?Object.entries(m.reasoning_mapping.rules||{}).map(([a,b])=>a+' → '+b).join(',')||'全部透传':'未启用,原样透传']])}`+modelEvidenceDetailHTML(m.id));
-}
-function latencyHTML(label,ms,local,threshold){
- const tone=ms==null||local?'':ms>=threshold*3?'bad':ms>=threshold?'warn':'good';
- return `${label}${ms==null||local?'—':fmt(ms/1000,2)+'s'}
`;
-}
-function logPresentation(x){const local=x.upstream_skipped,success=x.status>=200&&x.status<400;return {local,status:local?'本地退避':x.status===429?'限流':x.status===401?'鉴权失败':x.status===403?'权限拒绝':success?'成功':'失败',tone:local||x.status===429?'warn':success?'good':'bad',provider:local?'未调用上游':!x.provider||x.provider==='unknown'?'上游未报告':x.provider,usage:x.usage_reported&&!local,reasoning:x.reasoning_from===x.reasoning_to?(x.reasoning_from||'unset'):(x.reasoning_from||'unset')+' → '+(x.reasoning_to||'unset'),speed:x.output_tokens_per_second==null||local?'—':fmt(x.output_tokens_per_second,1)+' tok/s'}}
-function logHTML(x){
- const {local,status,tone,provider,usage}=logPresentation(x),date=x.time?new Date(x.time):null;
- return ``;
-}
-function logDetailHTML(x){
- const {local,status,tone,provider,usage,reasoning,speed}=logPresentation(x);
- const route=[['请求 ID',x.id],['客户端模型',x.model],['上游模型',x.upstream_model],['实际凭据',x.credential||x.credential_id],['实际上游',provider],['客户端 / 上游协议',(x.client_protocol||'未知')+' / '+(x.upstream_protocol||'未知')],['ZDR 请求',x.zdr_requested?'已请求,保留策略待官方核实':'未请求'],['思考映射',reasoning],['响应状态',x.status],['流终态',x.stream_end||'—'],['重试时间',x.retry_at?when(x.retry_at):'—']];
- const timing=[['输入 tokens',usage?fmt(x.prompt_tokens):'—'],['输出 tokens',usage?fmt(x.completion_tokens):'—'],['缓存读取 tokens',usage?fmt(x.cached_tokens):'—'],['缓存写入 tokens',usage?fmt(x.cache_write_tokens):'—'],['思考 tokens',usage?fmt(x.reasoning_tokens):'—'],['首字耗时',x.ttft_ms==null||local?'—':fmt(x.ttft_ms/1000,2)+'s'],['总耗时',local?'—':fmt(x.duration_ms/1000,2)+'s'],['输出速率',speed]];
- const fields=rows=>''+rows.map(([k,v])=>`- ${k}
- ${esc(v??'—')}
`).join('')+'
';
- return `${status}${esc(when(x.time))}
${local?'本地冷却,未调用上游。
':''}${x.error?`错误
${esc(x.error_kind||'请求错误')}${esc(x.error)} `:''}`;
-}
-async function loadLogs(){
- const revision=state.logRevision=(state.logRevision||0)+1,identity=diagnosticIdentity();
- const q=new URLSearchParams({offset:state.offset,limit:50,search:$('logSearch').value.trim(),status:$('logStatus').value});
- $('logRows').setAttribute('aria-busy','true');
- try{
- const d=await api('/logs?'+q);if(!state.authenticated||revision!==state.logRevision||identity!==diagnosticIdentity())return;
- const rows=d.items;state.logs=rows;state.total=d.total;$('logLoadStatus').hidden=true;
- if(state.offset>=d.total&&state.offset>0){state.offset=Math.max(0,Math.ceil(d.total/50)-1)*50;return loadLogs()}
- $('logCount').textContent=`${fmt(d.total)} 条记录`;
- $('logPage').textContent=`第 ${Math.floor(state.offset/50)+1} / ${Math.max(1,Math.ceil(d.total/50))} 页`;
- $('prevLogs').disabled=state.offset===0;$('nextLogs').disabled=state.offset+50>=d.total;
- $('logRows').innerHTML=rows.length?rows.map(logHTML).join(''):(q.get('search')||q.get('status')!=='all'?'暂无匹配记录调整筛选条件后重试
':'暂无请求记录发起请求后可在此查看详情。
');
- }catch(e){if(revision===state.logRevision&&identity===diagnosticIdentity()){$('logLoadStatus').hidden=false;$('logLoadStatus').textContent=state.logs?.length?'刷新失败,保留上次结果;请重试。':'日志读取失败,请重试。';throw e}}
- finally{if(revision===state.logRevision)$('logRows').removeAttribute('aria-busy')}
-}
-async function refresh(){
- if(!state.authenticated)return;
- try{
- $('globalError').hidden=true;
- if(state.view==='overview')await Promise.all([loadStats(),loadCredentials()]);
- if(state.view==='credentials')await Promise.all([loadCredentials(),loadDiagnostics()]);
- if(state.view==='models'){await Promise.all([loadConfig(),loadCredentials()]);await loadModelEvidence()}
- if(state.view==='settings')await loadConfig();
- if(state.view==='logs')await loadLogs();
- }catch(e){globalError(e)}
-}
-const viewNames={overview:'运行概览',credentials:'凭据与用量',models:'模型映射',logs:'请求日志',settings:'设置'};
-const mobileNav=matchMedia('(max-width: 767px)');
-function syncSidebar(){
- const opened=mobileNav.matches?document.body.classList.contains('sidebar-open'):!document.body.classList.contains('sidebar-collapsed');
- $('sidebarToggle').setAttribute('aria-expanded',String(opened));
- $('sidebarToggle').setAttribute('aria-label',opened?'收起导航':'展开导航');
- $('sidebar').inert=mobileNav.matches&&!opened;
- $('sidebarScrim').hidden=!mobileNav.matches||!opened;
-}
-function closeSidebar(){document.body.classList.remove('sidebar-open');syncSidebar()}
-$('sidebarToggle').onclick=()=>{
- document.body.classList.toggle(mobileNav.matches?'sidebar-open':'sidebar-collapsed');syncSidebar();
- if(mobileNav.matches&&document.body.classList.contains('sidebar-open'))$('sidebar').querySelector('button.active').focus();
-};
-$('sidebarScrim').onclick=()=>{closeSidebar();$('sidebarToggle').focus()};
-mobileNav.addEventListener('change',()=>{closeSidebar()});
-document.addEventListener('keydown',e=>{
- const modal=document.activeElement?.closest('dialog[open]')||[...document.querySelectorAll('dialog[open]')].at(-1);
- if(modal){
- if(e.key==='Tab'){
- const nodes=[...modal.querySelectorAll('button:not(:disabled),a[href],input:not(:disabled):not([type=hidden]),select:not(:disabled),textarea:not(:disabled),[tabindex="0"]')].filter(x=>x.getClientRects().length),first=nodes[0],last=nodes.at(-1);
- if(first&&e.shiftKey&&(document.activeElement===first||!nodes.includes(document.activeElement))){e.preventDefault();last.focus()}
- else if(first&&!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus()}
+const $ = (id) => document.getElementById(id),
+ esc = (v) =>
+ String(v ?? '').replace(
+ /[&<>"']/g,
+ (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]
+ );
+const state = {
+ view: 'overview',
+ config: null,
+ credentials: [],
+ activeCredential: null,
+ catalog: [],
+ selected: new Set(),
+ offset: 0,
+ total: 0,
+ key: '',
+ authType: 'bearer',
+ authenticated: false,
+ blocked: false,
+ busy: 0,
+};
+const API_BASE = document.querySelector('meta[name="cpa-api-base"]').content;
+const compactNumber = (v) =>
+ v == null
+ ? '—'
+ : Number(v).toLocaleString('en', { notation: 'compact', maximumFractionDigits: 1 });
+const shortWhen = (v) =>
+ v
+ ? new Date(v).toLocaleString('zh-CN', {
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: false,
+ })
+ : '未知';
+const fmt = (v, d = 0) =>
+ v === null || v === undefined
+ ? '—'
+ : Number(v).toLocaleString('zh-CN', { maximumFractionDigits: d }),
+ when = (v) => (v ? new Date(v).toLocaleString('zh-CN') : '未知'),
+ percent = (a, b) => (a != null && b > 0 ? Math.max(0, Math.min(100, (a / b) * 100)) : null);
+function toast(text) {
+ $('toast').textContent = text;
+ $('toast').hidden = false;
+ clearTimeout(toast.timer);
+ toast.timer = setTimeout(() => ($('toast').hidden = true), 3500);
+}
+function globalError(e) {
+ $('globalError').textContent = e.message || String(e);
+ $('globalError').hidden = false;
+}
+function dialogError(id, e) {
+ $(id).querySelector('.form-error').textContent = e.message || String(e);
+}
+const dialogOrigins = new Map();
+function openDialog(id) {
+ const dialog = $(id);
+ dialog.querySelectorAll('.form-error').forEach((x) => (x.textContent = ''));
+ if (!dialog.open) {
+ const node = document.activeElement;
+ dialogOrigins.set(id, { node, data: { ...node?.dataset }, view: state.view });
+ dialog.showModal();
+ }
+}
+function restoreDialogFocus(id) {
+ const origin = dialogOrigins.get(id);
+ dialogOrigins.delete(id);
+ if (!origin || document.querySelector('dialog[open]')) return;
+ if (
+ origin.node?.isConnected &&
+ (!origin.node.getClientRects || origin.node.getClientRects().length) &&
+ !origin.node.closest?.('dialog:not([open])')
+ ) {
+ origin.node.focus();
+ return;
+ }
+ for (const key of [
+ 'logDetail',
+ 'modelDetail',
+ 'modelEdit',
+ 'modelTest',
+ 'accountEdit',
+ 'accountSelect',
+ ]) {
+ if (origin.data[key] !== undefined) {
+ const node = [...document.querySelectorAll('button')].find(
+ (x) =>
+ x.dataset[key] === origin.data[key] &&
+ (!x.getClientRects || x.getClientRects().length) &&
+ !x.closest?.('dialog:not([open])')
+ );
+ if (node) {
+ node.focus();
+ return;
+ }
}
+ }
+ const modelIndex = origin.data.modelEdit ?? origin.data.modelTest;
+ const modelButton = [...document.querySelectorAll('[data-model-detail]')].find(
+ (x) => x.dataset.modelDetail === modelIndex && x.getClientRects().length
+ );
+ if (modelButton) {
+ modelButton.focus();
return;
}
- if(!mobileNav.matches||!document.body.classList.contains('sidebar-open'))return;
- if(e.key==='Escape'){e.preventDefault();closeSidebar();$('sidebarToggle').focus()}
- if(e.key==='Tab'){
- const focusable=[...$('sidebar').querySelectorAll('a,button')],first=focusable[0],last=focusable.at(-1);
- if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus()}
- else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus()}
+ if (origin.view === state.view) $('mainContent').focus();
+}
+function showDetail(title, subtitle, html) {
+ $('detailTitle').textContent = title;
+ $('detailSubtitle').textContent = subtitle;
+ $('detailContent').innerHTML = html;
+ openDialog('detailDialog');
+ $('detailTitle').focus();
+}
+function statusRows(rows) {
+ return (
+ '' +
+ rows
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
+ .map(([label, value]) => `- ${esc(label)}
- ${esc(value)}
`)
+ .join('') +
+ '
'
+ );
+}
+function apiURL(path) {
+ return new URL(API_BASE + path, location.origin).href;
+}
+async function api(path, options = {}) {
+ if (state.blocked && !options.verify) throw new Error('管理访问已暂停,请重新验证登录');
+ const headers = { ...(window.CommandCodeBridgeAuthHeaders?.() || {}), ...options.headers };
+ if (state.key) {
+ delete headers.Authorization;
+ delete headers['X-Management-Key'];
+ headers[state.authType === 'x-management-key' ? 'X-Management-Key' : 'Authorization'] =
+ state.authType === 'x-management-key' ? state.key : 'Bearer ' + state.key;
+ }
+ if (options.body !== undefined) headers['Content-Type'] = 'application/json';
+ state.busy++;
+ $('busy').textContent = '正在更新';
+ try {
+ const response = await fetch(apiURL(path), {
+ method: options.method || 'GET',
+ headers,
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
+ signal: AbortSignal.timeout(options.timeout || 650000),
+ });
+ const text = await response.text();
+ let data;
+ try {
+ data = JSON.parse(text);
+ } catch {
+ data = { error: text.slice(0, 200) };
+ }
+ if (!response.ok) {
+ if ((response.status === 401 || response.status === 403) && path !== '/models/test') {
+ state.authenticated = false;
+ state.blocked = true;
+ resetDiagnosticViews();
+ if (response.status === 401) {
+ window.CommandCodeBridgeClearAuth?.(headers);
+ state.key = '';
+ }
+ openDialog('authDialog');
+ }
+ throw new Error(data.error || `HTTP ${response.status}`);
+ }
+ return data;
+ } finally {
+ state.busy--;
+ $('busy').textContent = state.busy ? '正在更新' : '';
+ }
+}
+async function confirmed(title, text) {
+ $('confirmTitle').textContent = title;
+ $('confirmText').textContent = text;
+ openDialog('confirmDialog');
+ return new Promise((resolve) => {
+ const dialog = $('confirmDialog');
+ let settled = false;
+ const onclose = () => end(false);
+ const end = (value) => {
+ if (settled) return;
+ settled = true;
+ dialog.removeEventListener('close', onclose);
+ if (dialog.open) dialog.close();
+ resolve(value);
+ };
+ dialog.addEventListener('close', onclose);
+ $('confirmCancel').onclick = () => end(false);
+ $('confirmYes').onclick = () => end(true);
+ dialog.oncancel = (e) => {
+ e.preventDefault();
+ end(false);
+ };
+ });
+}
+function period() {
+ const range = $('statsRange').value;
+ let from = '',
+ to = '';
+ if (range === 'today') {
+ const date = new Date();
+ date.setHours(0, 0, 0, 0);
+ from = date.toISOString();
+ } else if (range === '24h') from = new Date(Date.now() - 86400000).toISOString();
+ else if (range === 'custom') {
+ if ($('statsFrom').value) from = new Date($('statsFrom').value).toISOString();
+ if ($('statsTo').value) to = new Date($('statsTo').value).toISOString();
+ }
+ const q = new URLSearchParams();
+ if (from) q.set('from', from);
+ if (to) q.set('to', to);
+ return q.size ? '?' + q.toString() : '';
+}
+async function loadConfig() {
+ const identity = diagnosticIdentity(),
+ config = await api('/config');
+ if (!state.authenticated || identity !== diagnosticIdentity()) return;
+ state.config = config;
+ renderModels();
+ const f = $('settingsForm');
+ for (const k of [
+ 'timeout_seconds',
+ 'first_byte_timeout_seconds',
+ 'stream_idle_timeout_seconds',
+ 'log_retention',
+ 'usage_refresh_seconds',
+ 'statistics_retention_days',
+ ])
+ f.elements[k].value = state.config[k] ?? 0;
+ for (const k of ['cpamp_install_dir', 'cpamp_config_path', 'cpamp_panel_path'])
+ f.elements[k].value = state.config[k] || '';
+ f.elements.max_response_mb.value = state.config.max_response_bytes / 1048576;
+ f.elements.zdr.checked = state.config.zdr;
+ $('dataDir').textContent = state.config.data_dir;
+ const model = state.config.models[0]?.id || '你的模型名称';
+ $('apiExample').textContent = JSON.stringify(
+ { model, messages: [{ role: 'user', content: '你好' }], stream: true },
+ null,
+ 2
+ );
+ if (state.view === 'settings')
+ await Promise.all([loadCPAMPPanel(), loadCompatibility(), loadStorage()]);
+}
+function showCPAMPPanel(result) {
+ state.cpampPanel = result;
+ const served = result.served_page;
+ const rows = [
+ ['检测结果', result.message],
+ ['目标目录', result.install_dir || result.default_install_dir],
+ ['运行实例', result.pid ? 'PID ' + result.pid : '未检测到'],
+ ['配置文件', result.config_path],
+ [
+ '配置方式',
+ result.configuration_mode === 'environment'
+ ? '环境变量(尚无配置文件)'
+ : result.configuration_mode,
+ ],
+ ['面板文件', result.panel_path],
+ [
+ '当前服务页面',
+ served?.checked
+ ? (served.native_quota ? '含原生额度组件' : '缺少原生额度组件') +
+ (served.matches ? ',与内置版本一致' : '')
+ : '尚未完成 HTTP 核验',
+ ],
+ ['预计变化', (result.planned_changes || []).join('\n') || '无'],
+ ['可选配置', result.candidates?.join('\n')],
+ ['备份文件', result.backups?.join('\n')],
+ [
+ '生效条件',
+ result.hot_update_ready ? '原路径更新,刷新浏览器' : '首次启用后重启 Manager Server',
+ ],
+ ];
+ if (!result.available) rows.push(['安装不可用', '当前构建未包含面板,请使用官方发布包']);
+ $('cpampPanelStatus').innerHTML = statusRows(rows);
+ $('installCPAMPPanel').textContent = result.hot_update_ready ? '更新原生面板' : '安装原生面板';
+ $('installCPAMPPanel').disabled =
+ !result.available || result.installable === false || result.active;
+}
+async function loadCPAMPPanel() {
+ const identity = diagnosticIdentity(),
+ result = await api('/cpamp/panel');
+ if (state.authenticated && identity === diagnosticIdentity()) showCPAMPPanel(result);
+}
+async function saveCPAMPPaths() {
+ const f = $('settingsForm'),
+ paths = {};
+ for (const name of ['cpamp_install_dir', 'cpamp_config_path', 'cpamp_panel_path'])
+ paths[name] = f.elements[name].value.trim();
+ if (Object.entries(paths).some(([name, value]) => value !== (state.config?.[name] || '')))
+ state.config = await api('/config', { method: 'PUT', body: paths });
+}
+async function loadCredentials() {
+ const identity = diagnosticIdentity(),
+ result = await api('/credentials');
+ if (!state.authenticated || identity !== diagnosticIdentity()) return;
+ state.credentials = result.credentials;
+ renderCredentials();
+ renderHealth();
+}
+const diagnosticStatus = {
+ confirmed: '登记一致',
+ disabled: '停用已确认',
+ missing: '未登记',
+ duplicate: '重复登记',
+ routing_mismatch: '路由不一致',
+ disabled_mismatch: '停用未同步',
+ pending: '等待宿主确认',
+ unknown: '未确认',
+ ok: '通过',
+ mismatch: '版本不匹配',
+};
+const diagnosticSources = {
+ account_query: '账户查询',
+ plugin_test: '插件直测',
+ cpa_request: 'CPA 请求',
+};
+function diagnosticTone(status) {
+ return ['ok', 'confirmed', 'disabled', 'success'].includes(status)
+ ? 'good'
+ : ['pending', 'unknown', 'partial'].includes(status)
+ ? 'warn'
+ : 'bad';
+}
+function diagnosticCounts(report) {
+ return {
+ errors:
+ report.credentials.filter(
+ (x) => !['confirmed', 'disabled', 'unknown', 'pending'].includes(x.status)
+ ).length + (report.unmatched_registrations || 0),
+ pending: report.credentials.filter((x) => ['unknown', 'pending'].includes(x.status)).length,
+ };
+}
+function renderDiagnostics(report) {
+ const rows = [...report.credentials]
+ .sort(
+ (a, b) =>
+ ({ bad: 0, warn: 1, good: 2 })[diagnosticTone(a.status)] -
+ { bad: 0, warn: 1, good: 2 }[diagnosticTone(b.status)]
+ )
+ .map(
+ (row) =>
+ `${esc(row.label || '凭证')}${esc(diagnosticStatus[row.status] || row.status)}${row.cooldown ? ' 部分模型冷却中' : ''}
${esc(row.message)}
${esc(row.action)}
${row.evidence.map((item) => `${esc(diagnosticSources[item.source] || item.source)}:${esc({ unknown: '暂无可确认记录', success: '成功', error: '失败', ok: '正常', partial: '部分成功', disabled: '已停用' }[item.status] || item.status)}${item.checked_at ? ' · ' + esc(when(item.checked_at)) : ''}
`).join('')}`
+ )
+ .join('');
+ return `检查时间:${esc(when(report.checked_at))} · 映射模型 ${esc(report.models)} 个
${esc(report.host_message)}
${report.models === 0 ? '尚未配置模型映射,请在模型映射页导入。
' : ''}${report.unmatched_registrations ? `另有 ${esc(report.unmatched_registrations)} 个宿主登记无法匹配当前凭证,请检查 CPA 加载日志。
` : ''}${rows || '暂无凭证,请先添加凭证。
'}`;
+}
+function renderCompatibility(report) {
+ return `检查时间:${esc(when(report.checked_at))} · 插件 ${esc(report.version)}
${report.checks.map((check) => `${esc({ plugin: '插件接口', host_version: 'CPA 版本', page: '实际服务页面', page_quota: '额度页面', page_recent_requests: '最近请求页面', page_windows: '周期历史页面', snapshot_backend: '快照后端', browser_cache: '浏览器缓存' }[check.name] || check.name)}${esc(diagnosticStatus[check.status] || { missing: '缺少能力' }[check.status] || check.status)}${esc(check.message)}
${check.action ? `
${esc(check.action)}
` : ''}
`).join('')}`;
+}
+function diagnosticIdentity() {
+ return JSON.stringify([
+ state.sessionRevision || 0,
+ state.key,
+ state.authType,
+ window.CommandCodeBridgeAuthHeaders?.() || {},
+ ]);
+}
+function resetDiagnosticViews() {
+ state.sessionRevision = (state.sessionRevision || 0) + 1;
+ state.diagnosticRevision = (state.diagnosticRevision || 0) + 1;
+ state.compatibilityRevision = (state.compatibilityRevision || 0) + 1;
+ state.modelEvidenceRevision = (state.modelEvidenceRevision || 0) + 1;
+ state.logRevision = (state.logRevision || 0) + 1;
+ state.modelEvidence = null;
+ state.modelCredential = '';
+ state.config = null;
+ state.credentials = [];
+ state.logs = [];
+ state.total = 0;
+ state.offset = 0;
+ state.cpampPanel = null;
+ state.detail = null;
+ state.catalog = [];
+ state.selected?.clear();
+ for (const id of [
+ 'credentialDialog',
+ 'modelDialog',
+ 'catalogDialog',
+ 'testDialog',
+ 'confirmDialog',
+ ])
+ $(id).close?.();
+ for (const id of ['credentialForm', 'modelForm', 'testForm']) $(id).reset?.();
+ $('testResult').textContent = '';
+ $('catalogRows').innerHTML = '';
+ $('detailDialog').close?.();
+ $('detailContent').innerHTML = '';
+ for (const id of [
+ 'modelCredential',
+ 'modelRows',
+ 'credentialRows',
+ 'credentialHealth',
+ 'summary',
+ 'modelStats',
+ 'logRows',
+ 'logCount',
+ 'logPage',
+ 'cpampPanelStatus',
+ 'dataDir',
+ 'apiExample',
+ ])
+ $(id).innerHTML = '';
+ $('authButton').textContent = '管理登录';
+ $('modelEvidenceStatus').textContent = '登录后重新读取模型证据';
+ $('storageStatus').textContent = '登录后重新读取存储状态';
+ $('diagnosticSummary').textContent = '登录后重新诊断';
+ $('diagnosticResults').textContent = '登录后重新诊断';
+ $('compatibilityResults').textContent = '登录后重新检查';
+ $('installCPAMPPanel').disabled = true;
+ $('prevLogs').disabled = true;
+ $('nextLogs').disabled = true;
+}
+async function loadDiagnostics() {
+ const revision = (state.diagnosticRevision = (state.diagnosticRevision || 0) + 1),
+ identity = diagnosticIdentity();
+ try {
+ const report = await api('/diagnostics', { timeout: 15000 });
+ if (
+ state.authenticated &&
+ revision === state.diagnosticRevision &&
+ identity === diagnosticIdentity()
+ ) {
+ $('diagnosticResults').innerHTML = renderDiagnostics(report);
+ const n = diagnosticCounts(report);
+ $('diagnosticSummary').textContent =
+ `${n.errors} 项异常 · ${n.pending} 项待确认 · ${when(report.checked_at)}`;
+ }
+ } catch (e) {
+ if (revision === state.diagnosticRevision && identity === diagnosticIdentity()) {
+ $('diagnosticSummary').textContent = '诊断刷新失败,详情保留上次结果';
+ if (!$('diagnosticResults').innerHTML)
+ $('diagnosticResults').textContent = '诊断失败,请重新登录或重试。';
+ }
+ throw e;
+ }
+}
+async function loadCompatibility() {
+ const revision = (state.compatibilityRevision = (state.compatibilityRevision || 0) + 1),
+ identity = diagnosticIdentity();
+ try {
+ const report = await api('/compatibility', { timeout: 15000 });
+ if (
+ state.authenticated &&
+ revision === state.compatibilityRevision &&
+ identity === diagnosticIdentity()
+ )
+ $('compatibilityResults').innerHTML = renderCompatibility(report);
+ } catch (e) {
+ if (revision === state.compatibilityRevision && identity === diagnosticIdentity())
+ $('compatibilityResults').textContent = '兼容检查失败,请重新登录或重试。';
+ throw e;
}
+}
+$('runDiagnostics').onclick = () => loadDiagnostics().catch(globalError);
+$('checkCompatibility').onclick = () => loadCompatibility().catch(globalError);
+$('copyDiagnostics').onclick = async () => {
+ const identity = diagnosticIdentity();
+ try {
+ const summary = await api('/diagnostics/summary', { timeout: 15000 });
+ if (!state.authenticated || identity !== diagnosticIdentity()) return;
+ const text = JSON.stringify(summary, null, 2);
+ try {
+ await navigator.clipboard.writeText(text);
+ toast('脱敏诊断摘要已复制');
+ } catch {
+ const url = URL.createObjectURL(new Blob([text], { type: 'application/json' })),
+ link = document.createElement('a');
+ link.href = url;
+ link.download = 'commandcodebridge-diagnostics.json';
+ link.click();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ toast('脱敏诊断摘要已导出');
+ }
+ } catch (e) {
+ globalError(e);
+ }
+};
+async function loadStats() {
+ const identity = diagnosticIdentity(),
+ d = await api('/statistics' + period()),
+ t = d.totals;
+ if (!state.authenticated || identity !== diagnosticIdentity()) return;
+ const metrics = [
+ ['请求数', fmt(t.requests), `${fmt(t.success)} 成功 · ${fmt(t.failed)} 失败`],
+ [
+ '输入 / 输出 tokens',
+ compactNumber(t.input_tokens) + ' / ' + compactNumber(t.output_tokens),
+ `${fmt(t.with_usage)} 次有用量记录`,
+ ],
+ [
+ '缓存命中',
+ d.cache_hit_percent == null ? '—' : fmt(d.cache_hit_percent, 1) + '%',
+ `${fmt(t.cached_tokens)} tokens 读取`,
+ ],
+ [
+ '输出速率',
+ d.output_tokens_per_second == null ? '—' : fmt(d.output_tokens_per_second, 1) + ' tok/s',
+ d.average_ttft_ms == null ? '首字时间未知' : '首字 ' + fmt(d.average_ttft_ms / 1000, 2) + 's',
+ ],
+ ];
+ $('summary').innerHTML = metrics
+ .map(
+ ([name, value, hint], i) =>
+ `${name}${value}${hint}
`
+ )
+ .join('');
+ const rows = Object.values(d.models).sort((a, b) => b.requests - a.requests);
+ $('modelStats').innerHTML = rows.length
+ ? `| 模型 | 请求 | 成功 | 输入 tokens | 输出 tokens | 缓存 tokens |
${rows.map((x) => `| ${esc(x.model)} | ${fmt(x.requests)} | ${fmt(x.success)} | ${fmt(x.input_tokens)} | ${fmt(x.output_tokens)} | ${fmt(x.cached_tokens)} |
`).join('')}
`
+ : '暂无请求发起请求后显示用量
';
+ $('statisticsPrecision').textContent =
+ (d.bucket_seconds > 60 ? d.range_note : '当前范围为分钟级统计') +
+ ((d.storage?.recovery_warnings || []).some((w) => w.file === 'statistics.json')
+ ? ' · 发生过统计恢复,历史可能不完整,请查看设置中的恢复记录'
+ : '');
+ renderStorage(d.storage);
+ if (d.storage_error) globalError(new Error(d.storage_error));
+}
+const healthLabels = {
+ healthy: ['正常', 'good'],
+ warning: ['需关注', 'warn'],
+ invalid: ['鉴权异常', 'bad'],
+ cooldown: ['冷却中', 'warn'],
+ disabled: ['已停用', 'neutral'],
+ unknown: ['待验证', 'neutral'],
+};
+function healthBadge(h) {
+ const [label, tone] = healthLabels[h?.status] || healthLabels.unknown;
+ return `${label}`;
+}
+function renderHealth() {
+ const rows = state.credentials;
+ if (!rows.length) {
+ $('credentialHealth').innerHTML = '暂无凭据
';
+ return;
+ }
+ const healthy = rows.filter((c) => c.health?.status === 'healthy').length,
+ enabled = rows.filter((c) => !c.disabled).length;
+ const counts = Object.fromEntries(
+ Object.keys(healthLabels).map((k) => [
+ k,
+ rows.filter((c) => (c.health?.status || 'unknown') === k).length,
+ ])
+ );
+ const bar = Object.entries(counts)
+ .filter(([, n]) => n)
+ .map(
+ ([k, n]) =>
+ ``
+ )
+ .join('');
+ const legend = Object.entries(counts)
+ .filter(([, n]) => n)
+ .map(([k, n]) => `${healthLabels[k][0]} ${n}`)
+ .join('');
+ $('credentialHealth').innerHTML =
+ `正常 / 已启用${healthy} / ${enabled}
${bar}
${legend}
${rows
+ .map((c) => {
+ const h = c.health || {},
+ recent = h.recent_requests || 0;
+ const detail = h.cooldown_until
+ ? '重试 ' + shortWhen(h.cooldown_until)
+ : ['invalid', 'warning'].includes(h.status)
+ ? h.reason || '查询异常'
+ : h.checked_at
+ ? '验证于 ' + shortWhen(h.checked_at)
+ : h.status === 'disabled'
+ ? '不参与调度'
+ : '尚未验证';
+ const request = h.last_request_at
+ ? `最近请求 ${h.last_status} · ${shortWhen(h.last_request_at)}`
+ : '暂无近期请求';
+ return `
`;
+ })
+ .join('')}
`;
+}
+function windowHTML(w, name) {
+ const p = percent(w?.used, w?.cap),
+ unknown = p == null;
+ return `${name}${unknown ? '未知' : fmt(p, 1) + '%'}
${fmt(w?.used, 2)} / ${fmt(w?.cap, 2)} credits
${w ? '' : '窗口未知 · '}重置 ${esc(shortWhen(w?.reset_at))}
`;
+}
+function monthlyHTML(u) {
+ const w = (u.windows || []).find((w) => w.name === 'monthly');
+ if (w) return windowHTML(w, '月额度窗口');
+ return `月额度窗口剩余
${fmt(u.monthly_credits, 3)} credits
已用 ${fmt(u.monthly_used_credits, 3)} · 上限未知
重置 ${esc(shortWhen(u.subscription_end))}
`;
+}
+function renderCredentials() {
+ const rows = state.credentials;
+ if (!rows.length) {
+ state.activeCredential = null;
+ $('credentialRows').innerHTML =
+ '暂无凭据
';
+ return;
+ }
+ if (!rows.some((c) => c.id === state.activeCredential))
+ state.activeCredential = (rows.find((c) => !c.disabled) || rows[0]).id;
+ const c = rows.find((c) => c.id === state.activeCredential),
+ u = c.usage || {},
+ windows = u.windows || [],
+ plan = u.plan || '套餐未知';
+ const unavailable = (u.unavailable || [])
+ .map((x) => ({ credits: '余额', subscription: '订阅', summary: '本期用量' })[x] || x)
+ .join('、');
+ const selector = rows
+ .map(
+ (c) =>
+ ``
+ )
+ .join('');
+ $('credentialRows').innerHTML =
+ `凭据${rows.length}
${selector}
${esc(c.label)}${healthBadge(c.health)}
${esc(plan)} · 优先级 ${c.priority}${c.shared_credentials > 1 ? ' · ' + c.shared_credentials + ' 把凭据共享额度' : ''}
剩余额度
${fmt(u.remaining_credits, 3)}credits
月度${fmt(u.monthly_credits, 3)}
购买${fmt(u.purchased_credits, 3)}
免费${fmt(u.free_credits, 3)}
${monthlyHTML(u)}${windowHTML(
+ windows.find((w) => w.name === 'fiveHour'),
+ '5 小时窗口'
+ )}${windowHTML(
+ windows.find((w) => w.name === 'weekly'),
+ '每周窗口'
+ )}
`;
+}
+const modelEvidenceLabels = {
+ unknown: '套餐权限未知',
+ available: '最近实测成功',
+ not_in_plan: '明确不在套餐',
+ permission_unknown: '权限拒绝,原因未知',
+ temporary_limit: '临时限流',
+ temporary_error: '临时故障',
+ authentication_error: '鉴权失败',
+ expired: '证据已过期',
+ disabled: '凭据已停用',
+};
+function evidenceTone(status) {
+ return status === 'available'
+ ? 'good'
+ : ['not_in_plan', 'authentication_error'].includes(status)
+ ? 'bad'
+ : ['temporary_limit', 'temporary_error', 'permission_unknown', 'expired'].includes(status)
+ ? 'warn'
+ : 'neutral';
+}
+function modelEvidenceHTML(model) {
+ const row = state.modelEvidence?.items.find((x) => x.model === model),
+ status = row?.status || 'unknown';
+ const latest = row?.evidence
+ .filter((e) => e.checked_at)
+ .sort((a, b) => new Date(b.checked_at) - new Date(a.checked_at))[0];
+ return `${esc(modelEvidenceLabels[status] || '未知')}${latest ? esc(diagnosticSources[latest.source] || latest.source) + ' · ' + esc(shortWhen(latest.checked_at)) : '暂无可确认记录'}
`;
+}
+function modelEvidenceDetailHTML(model) {
+ const row = state.modelEvidence?.items.find((x) => x.model === model);
+ return (
+ `${modelEvidenceHTML(model)}仅表示当前凭据与映射下的最近观察,不保证下一次调用成功。
` +
+ (row?.evidence.length
+ ? row.evidence
+ .map(
+ (e) =>
+ `${esc(diagnosticSources[e.source] || e.source)}${esc(modelEvidenceLabels[e.status] || '未知')}
- 观察时间
- ${esc(when(e.checked_at))}
- 重试时间
- ${esc(e.retry_at ? when(e.retry_at) : '未提供')}
- 客户端协议
- ${esc(e.client_protocol || '未知')}
- 上游协议
- ${esc(e.upstream_protocol || '未知')}
- HTTP 状态
- ${esc(e.http_status || '未提供')}
- 错误类型
- ${esc(e.error_kind || '无记录')}
- ZDR 请求
- ${e.zdr_requested ? '已请求,保留策略待核实' : '未请求'}
${e.status === 'expired' ? '此证据已超过 24 小时,当前权限待重新验证。
' : ''}`
+ )
+ .join('')
+ : '暂无插件直测或 CPA 请求记录。目录可见不代表套餐有权限。
')
+ );
+}
+async function loadModelEvidence() {
+ if (!state.authenticated) return;
+ if (state.detail === 'model') {
+ $('detailDialog').close();
+ state.detail = null;
+ }
+ const selector = $('modelCredential'),
+ available = state.credentials;
+ if (!available.some((c) => c.id === state.modelCredential))
+ state.modelCredential = available.find((c) => !c.disabled)?.id || available[0]?.id || '';
+ selector.innerHTML = available
+ .map(
+ (c) =>
+ ``
+ )
+ .join('');
+ selector.value = state.modelCredential;
+ const revision = (state.modelEvidenceRevision = (state.modelEvidenceRevision || 0) + 1),
+ identity = diagnosticIdentity(),
+ cid = state.modelCredential;
+ state.modelEvidence = null;
+ renderModels();
+ $('modelEvidenceStatus').textContent = cid ? '正在读取证据…' : '请先添加凭据';
+ if (!cid) return;
+ try {
+ const report = await api('/models/evidence?credential_id=' + encodeURIComponent(cid));
+ if (
+ !state.authenticated ||
+ revision !== state.modelEvidenceRevision ||
+ identity !== diagnosticIdentity() ||
+ cid !== state.modelCredential
+ )
+ return;
+ state.modelEvidence = report;
+ renderModels();
+ $('modelEvidenceStatus').textContent =
+ '读取于 ' + when(report.checked_at) + ' · 来源为保留日志,缺少证据不代表不可用';
+ } catch (e) {
+ if (revision === state.modelEvidenceRevision) {
+ $('modelEvidenceStatus').textContent = '读取失败,请刷新重试';
+ state.modelEvidence = null;
+ renderModels();
+ }
+ throw e;
+ }
+}
+function renderStorage(storage) {
+ if (!storage) return;
+ $('storageStatus').innerHTML =
+ statusRows([
+ ['统计精度', storage.precision],
+ ['最近成功写入', when(storage.last_success_at)],
+ ['待写文件', storage.pending_files?.join('、') || '无'],
+ ['待写开始', storage.pending_since ? when(storage.pending_since) : '无'],
+ ['统计桶数', fmt(storage.statistics_buckets)],
+ ]) +
+ (storage.recovery_warnings || [])
+ .map(
+ (w) =>
+ `恢复记录${esc(w.file)}
${esc(w.message)}
${esc(when(w.checked_at))} · 恢复后的历史可能不完整
`
+ )
+ .join('');
+}
+async function loadStorage() {
+ const identity = diagnosticIdentity(),
+ report = await api('/status');
+ if (state.authenticated && identity === diagnosticIdentity()) renderStorage(report.storage);
+}
+function filteredModels(models, search, status) {
+ const text = search.trim().toLocaleLowerCase();
+ return models
+ .map((model, index) => ({ model, index }))
+ .filter(({ model }) => {
+ const evidence =
+ state.modelEvidence?.items.find((x) => x.model === model.id)?.status || 'unknown';
+ return (
+ (!text ||
+ [model.id, model.upstream_id, model.name].join(' ').toLocaleLowerCase().includes(text)) &&
+ (status === 'all' ||
+ (status === 'available' && evidence === 'available') ||
+ (status === 'unknown' && ['unknown', 'expired', 'disabled'].includes(evidence)) ||
+ (status === 'attention' &&
+ !['available', 'unknown', 'expired', 'disabled'].includes(evidence)))
+ );
+ });
+}
+function renderModels() {
+ const models = state.config?.models || [],
+ rows = filteredModels(models, $('modelSearch').value, $('modelStatus').value);
+ if (!models.length) {
+ $('modelRows').innerHTML =
+ '暂无模型映射从上游目录导入或手动添加模型。
';
+ return;
+ }
+ if (!rows.length) {
+ $('modelRows').innerHTML =
+ '没有匹配的模型调整名称或证据筛选,不会发起生成。
';
+ return;
+ }
+ const protocol = (m) =>
+ m.supported_endpoints.includes('/chat/completions') ? 'OpenAI' : 'Anthropic';
+ const actions = (i) =>
+ ``;
+ $('modelRows').innerHTML =
+ `| 客户端名称 / 上游模型 | 协议 | 思考映射 | 当前凭据证据 | 操作 |
${rows.map(({ model: m, index: i }) => `${esc(m.id)} ${esc(m.upstream_id)} | ${protocol(m)} ${m.context_length ? fmt(m.context_length) + ' 上下文' : ''} | ${m.reasoning_mapping.enabled ? '已启用' : '透传'} | ${modelEvidenceHTML(m.id)} | ${actions(i)} |
`).join('')}
${rows.map(({ model: m, index: i }) => `
${esc(m.id)}
${esc(m.upstream_id)}
${protocol(m)} · 思考${m.reasoning_mapping.enabled ? '映射已启用' : '透传'}
${modelEvidenceHTML(m.id)}${actions(i)}
`).join('')}
`;
+}
+function showModelDetail(index) {
+ const m = state.config?.models[index];
+ if (!m) return;
+ state.detail = 'model';
+ showDetail(
+ m.id,
+ '模型详情 · ' +
+ (state.credentials.find((c) => c.id === state.modelCredential)?.label || '未选择凭据'),
+ `模型路由
${statusRows([
+ ['上游模型', m.upstream_id],
+ ['协议', m.supported_endpoints.join('、')],
+ [
+ '思考映射',
+ m.reasoning_mapping.enabled
+ ? Object.entries(m.reasoning_mapping.rules || {})
+ .map(([a, b]) => a + ' → ' + b)
+ .join(',') || '全部透传'
+ : '未启用,原样透传',
+ ],
+ ])}` +
+ modelEvidenceDetailHTML(m.id)
+ );
+}
+function latencyHTML(label, ms, local, threshold) {
+ const tone =
+ ms == null || local ? '' : ms >= threshold * 3 ? 'bad' : ms >= threshold ? 'warn' : 'good';
+ return `${label}${ms == null || local ? '—' : fmt(ms / 1000, 2) + 's'}
`;
+}
+function logPresentation(x) {
+ const local = x.upstream_skipped,
+ success = x.status >= 200 && x.status < 400;
+ return {
+ local,
+ status: local
+ ? '本地退避'
+ : x.status === 429
+ ? '限流'
+ : x.status === 401
+ ? '鉴权失败'
+ : x.status === 403
+ ? '权限拒绝'
+ : success
+ ? '成功'
+ : '失败',
+ tone: local || x.status === 429 ? 'warn' : success ? 'good' : 'bad',
+ provider: local
+ ? '未调用上游'
+ : !x.provider || x.provider === 'unknown'
+ ? '上游未报告'
+ : x.provider,
+ usage: x.usage_reported && !local,
+ reasoning:
+ x.reasoning_from === x.reasoning_to
+ ? x.reasoning_from || 'unset'
+ : (x.reasoning_from || 'unset') + ' → ' + (x.reasoning_to || 'unset'),
+ speed:
+ x.output_tokens_per_second == null || local
+ ? '—'
+ : fmt(x.output_tokens_per_second, 1) + ' tok/s',
+ };
+}
+function logHTML(x) {
+ const { local, status, tone, provider, usage } = logPresentation(x),
+ date = x.time ? new Date(x.time) : null;
+ return ``;
+}
+function logDetailHTML(x) {
+ const { local, status, tone, provider, usage, reasoning, speed } = logPresentation(x);
+ const route = [
+ ['请求 ID', x.id],
+ ['客户端模型', x.model],
+ ['上游模型', x.upstream_model],
+ ['实际凭据', x.credential || x.credential_id],
+ ['实际上游', provider],
+ ['客户端 / 上游协议', (x.client_protocol || '未知') + ' / ' + (x.upstream_protocol || '未知')],
+ ['ZDR 请求', x.zdr_requested ? '已请求,保留策略待官方核实' : '未请求'],
+ ['思考映射', reasoning],
+ ['响应状态', x.status],
+ ['流终态', x.stream_end || '—'],
+ ['重试时间', x.retry_at ? when(x.retry_at) : '—'],
+ ];
+ const timing = [
+ ['输入 tokens', usage ? fmt(x.prompt_tokens) : '—'],
+ ['输出 tokens', usage ? fmt(x.completion_tokens) : '—'],
+ ['缓存读取 tokens', usage ? fmt(x.cached_tokens) : '—'],
+ ['缓存写入 tokens', usage ? fmt(x.cache_write_tokens) : '—'],
+ ['思考 tokens', usage ? fmt(x.reasoning_tokens) : '—'],
+ ['首字耗时', x.ttft_ms == null || local ? '—' : fmt(x.ttft_ms / 1000, 2) + 's'],
+ ['总耗时', local ? '—' : fmt(x.duration_ms / 1000, 2) + 's'],
+ ['输出速率', speed],
+ ];
+ const fields = (rows) =>
+ '' +
+ rows.map(([k, v]) => `- ${k}
- ${esc(v ?? '—')}
`).join('') +
+ '
';
+ return `${status}${esc(when(x.time))}
${local ? '本地冷却,未调用上游。
' : ''}${x.error ? `错误
${esc(x.error_kind || '请求错误')}${esc(x.error)} ` : ''}`;
+}
+async function loadLogs() {
+ const revision = (state.logRevision = (state.logRevision || 0) + 1),
+ identity = diagnosticIdentity();
+ const q = new URLSearchParams({
+ offset: state.offset,
+ limit: 50,
+ search: $('logSearch').value.trim(),
+ status: $('logStatus').value,
+ });
+ $('logRows').setAttribute('aria-busy', 'true');
+ try {
+ const d = await api('/logs?' + q);
+ if (!state.authenticated || revision !== state.logRevision || identity !== diagnosticIdentity())
+ return;
+ const rows = d.items;
+ state.logs = rows;
+ state.total = d.total;
+ $('logLoadStatus').hidden = true;
+ if (state.offset >= d.total && state.offset > 0) {
+ state.offset = Math.max(0, Math.ceil(d.total / 50) - 1) * 50;
+ return loadLogs();
+ }
+ $('logCount').textContent = `${fmt(d.total)} 条记录`;
+ $('logPage').textContent =
+ `第 ${Math.floor(state.offset / 50) + 1} / ${Math.max(1, Math.ceil(d.total / 50))} 页`;
+ $('prevLogs').disabled = state.offset === 0;
+ $('nextLogs').disabled = state.offset + 50 >= d.total;
+ $('logRows').innerHTML = rows.length
+ ? rows.map(logHTML).join('')
+ : q.get('search') || q.get('status') !== 'all'
+ ? '暂无匹配记录调整筛选条件后重试
'
+ : '暂无请求记录发起请求后可在此查看详情。
';
+ } catch (e) {
+ if (revision === state.logRevision && identity === diagnosticIdentity()) {
+ $('logLoadStatus').hidden = false;
+ $('logLoadStatus').textContent = state.logs?.length
+ ? '刷新失败,保留上次结果;请重试。'
+ : '日志读取失败,请重试。';
+ throw e;
+ }
+ } finally {
+ if (revision === state.logRevision) $('logRows').removeAttribute('aria-busy');
+ }
+}
+async function refresh() {
+ if (!state.authenticated) return;
+ try {
+ $('globalError').hidden = true;
+ if (state.view === 'overview') await Promise.all([loadStats(), loadCredentials()]);
+ if (state.view === 'credentials') await Promise.all([loadCredentials(), loadDiagnostics()]);
+ if (state.view === 'models') {
+ await Promise.all([loadConfig(), loadCredentials()]);
+ await loadModelEvidence();
+ }
+ if (state.view === 'settings') await loadConfig();
+ if (state.view === 'logs') await loadLogs();
+ } catch (e) {
+ globalError(e);
+ }
+}
+const viewNames = {
+ overview: '运行概览',
+ credentials: '凭据与用量',
+ models: '模型映射',
+ logs: '请求日志',
+ settings: '设置',
+};
+const mobileNav = matchMedia('(max-width: 767px)');
+function syncSidebar() {
+ const opened = mobileNav.matches
+ ? document.body.classList.contains('sidebar-open')
+ : !document.body.classList.contains('sidebar-collapsed');
+ $('sidebarToggle').setAttribute('aria-expanded', String(opened));
+ $('sidebarToggle').setAttribute('aria-label', opened ? '收起导航' : '展开导航');
+ $('sidebar').inert = mobileNav.matches && !opened;
+ $('sidebarScrim').hidden = !mobileNav.matches || !opened;
+}
+function closeSidebar() {
+ document.body.classList.remove('sidebar-open');
+ syncSidebar();
+}
+$('sidebarToggle').onclick = () => {
+ document.body.classList.toggle(mobileNav.matches ? 'sidebar-open' : 'sidebar-collapsed');
+ syncSidebar();
+ if (mobileNav.matches && document.body.classList.contains('sidebar-open'))
+ $('sidebar').querySelector('button.active').focus();
+};
+$('sidebarScrim').onclick = () => {
+ closeSidebar();
+ $('sidebarToggle').focus();
+};
+mobileNav.addEventListener('change', () => {
+ closeSidebar();
});
-function navigate(view){
- if(!viewNames[view])return;
- state.view=view;document.querySelectorAll('.view').forEach(x=>x.hidden=x.id!==view);
- document.querySelectorAll('nav button').forEach(x=>{x.classList.toggle('active',x.dataset.view===view);if(x.dataset.view===view)x.setAttribute('aria-current','page');else x.removeAttribute('aria-current')});
- $('currentView').textContent=viewNames[view];
- if(mobileNav.matches&&document.body.classList.contains('sidebar-open')){closeSidebar();$('sidebarToggle').focus()}
+document.addEventListener('keydown', (e) => {
+ const modal =
+ document.activeElement?.closest('dialog[open]') ||
+ [...document.querySelectorAll('dialog[open]')].at(-1);
+ if (modal) {
+ if (e.key === 'Tab') {
+ const nodes = [
+ ...modal.querySelectorAll(
+ 'button:not(:disabled),a[href],input:not(:disabled):not([type=hidden]),select:not(:disabled),textarea:not(:disabled),[tabindex="0"]'
+ ),
+ ].filter((x) => x.getClientRects().length),
+ first = nodes[0],
+ last = nodes.at(-1);
+ if (
+ first &&
+ e.shiftKey &&
+ (document.activeElement === first || !nodes.includes(document.activeElement))
+ ) {
+ e.preventDefault();
+ last.focus();
+ } else if (first && !e.shiftKey && document.activeElement === last) {
+ e.preventDefault();
+ first.focus();
+ }
+ }
+ return;
+ }
+ if (!mobileNav.matches || !document.body.classList.contains('sidebar-open')) return;
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ closeSidebar();
+ $('sidebarToggle').focus();
+ }
+ if (e.key === 'Tab') {
+ const focusable = [...$('sidebar').querySelectorAll('a,button')],
+ first = focusable[0],
+ last = focusable.at(-1);
+ if (e.shiftKey && document.activeElement === first) {
+ e.preventDefault();
+ last.focus();
+ } else if (!e.shiftKey && document.activeElement === last) {
+ e.preventDefault();
+ first.focus();
+ }
+ }
+});
+function navigate(view) {
+ if (!viewNames[view]) return;
+ state.view = view;
+ document.querySelectorAll('.view').forEach((x) => (x.hidden = x.id !== view));
+ document.querySelectorAll('nav button').forEach((x) => {
+ x.classList.toggle('active', x.dataset.view === view);
+ if (x.dataset.view === view) x.setAttribute('aria-current', 'page');
+ else x.removeAttribute('aria-current');
+ });
+ $('currentView').textContent = viewNames[view];
+ if (mobileNav.matches && document.body.classList.contains('sidebar-open')) {
+ closeSidebar();
+ $('sidebarToggle').focus();
+ }
refresh();
}
-$('sidebar').querySelector('.brand').onclick=e=>{e.preventDefault();navigate('overview')};
-document.querySelectorAll('nav button').forEach(b=>{b.title=viewNames[b.dataset.view];b.setAttribute('aria-label',viewNames[b.dataset.view])});
+$('sidebar').querySelector('.brand').onclick = (e) => {
+ e.preventDefault();
+ navigate('overview');
+};
+document.querySelectorAll('nav button').forEach((b) => {
+ b.title = viewNames[b.dataset.view];
+ b.setAttribute('aria-label', viewNames[b.dataset.view]);
+});
syncSidebar();
-function editAccount(cid){const f=$('credentialForm');f.reset();const c=state.credentials.find(c=>c.id===cid);f.elements.id.value=cid||'';f.elements.api_key.required=!c;if(c){f.elements.label.value=c.label;f.elements.priority.value=c.priority;f.elements.disabled.checked=c.disabled}$('credentialTitle').textContent=c?'编辑凭据':'添加凭据';openDialog('credentialDialog')}
-const defaultAlias=upstream=>upstream.trim().split('/').pop();
-let lastSuggestedAlias='';
-$('modelForm').elements.upstream_id.oninput=e=>{const f=$('modelForm');if(f.elements.index.value!==''||f.elements.id.value&&f.elements.id.value!==lastSuggestedAlias)return;lastSuggestedAlias=defaultAlias(e.target.value);f.elements.id.value=lastSuggestedAlias};
-function editModel(index){const f=$('modelForm');f.reset();lastSuggestedAlias='';const m=state.config?.models[index];f.elements.index.value=m?index:'';if(m){f.elements.id.value=m.id;f.elements.upstream_id.value=m.upstream_id;f.elements.name.value=m.name||'';f.elements.endpoint.value=m.supported_endpoints.includes('/chat/completions')?'/chat/completions':'/messages';f.elements.thinking_mode.value=m.thinking_mode||"adaptive";f.elements.reasoning_enabled.checked=m.reasoning_mapping.enabled;f.elements.rules.value=JSON.stringify(m.reasoning_mapping.rules||{},null,2)}$('modelTitle').textContent=m?'编辑模型':'添加模型';openDialog('modelDialog')}
-async function saveModels(models){const aliases=new Set();for(const m of models){if(aliases.has(m.id))throw new Error('客户端名称重复:'+m.id+'。请减少所选模型,或手动添加并设置不同名称。');aliases.add(m.id)}await api('/models',{method:'PUT',body:{models}});await loadConfig();await loadModelEvidence()}
-async function showCatalog(){const identity=diagnosticIdentity();try{const result=await api('/models/refresh',{method:'POST',body:{},timeout:20000});if(!state.authenticated||identity!==diagnosticIdentity())return;state.catalog=result.models;state.selected.clear();$('catalogSearch').value='';renderCatalog();openDialog('catalogDialog')}catch(e){if(state.authenticated&&identity===diagnosticIdentity())globalError(e)}}
-function catalogFiltered(){const text=$('catalogSearch').value.toLowerCase();const existing=new Set((state.config?.models||[]).map(m=>m.upstream_id));return state.catalog.filter(m=>!existing.has(m.upstream_id)&&(!text||(m.name+' '+m.upstream_id).toLowerCase().includes(text)))}
-function renderCatalog(){const rows=catalogFiltered();$('catalogRows').innerHTML=rows.length?rows.map(m=>``).join(''):'没有可导入的模型。
';$('importCatalog').textContent=`导入所选${state.selected.size?'('+state.selected.size+')':''}`}
-async function showTest(index){const identity=diagnosticIdentity();await loadCredentials();if(!state.authenticated||identity!==diagnosticIdentity())return;const m=state.config.models[index],f=$('testForm');f.reset();f.elements.model.value=m.id;$('testModelName').textContent=m.id;f.elements.credential_id.innerHTML=state.credentials.filter(c=>!c.disabled).map(c=>``).join('');if(state.modelCredential)f.elements.credential_id.value=state.modelCredential;$('testResult').hidden=true;openDialog('testDialog')}
-document.querySelectorAll('dialog').forEach(d=>{d.addEventListener('close',()=>restoreDialogFocus(d.id));d.setAttribute('aria-labelledby',d.querySelector('h2').id||(d.querySelector('h2').id=d.id+'Title'))});
-document.querySelectorAll('[data-view]').forEach(b=>b.onclick=()=>navigate(b.dataset.view));document.querySelectorAll('[data-jump]').forEach(b=>b.onclick=()=>navigate(b.dataset.jump));document.querySelectorAll('[data-close]').forEach(b=>b.onclick=()=>$(b.dataset.close).close());document.querySelectorAll('[data-action="refresh"]').forEach(b=>b.onclick=refresh);
-$('authButton').onclick=()=>{ $('rememberAuth').checked=window.CommandCodeBridgeRememberAuth?.()||false;openDialog('authDialog')};$('clearAuth').onclick=()=>{window.CommandCodeBridgeClearAuth?.();resetDiagnosticViews();state.key='';state.authenticated=false;state.blocked=true;$('managementKey').value='';toast('已退出登录')};
-$('authForm').onsubmit=async e=>{e.preventDefault();resetDiagnosticViews();state.authenticated=false;state.key=$('managementKey').value.trim();state.authType=$('authType').value;try{const status=await api('/status',{verify:true,timeout:15000});window.CommandCodeBridgeSaveAuth?.(state.key,state.authType,$('rememberAuth').checked);state.authenticated=true;$('authButton').textContent='管理身份';state.blocked=false;$('managementKey').value='';$('authDialog').close();$('version').textContent='v'+status.version;await loadConfig();await loadCredentials();await refresh();toast('已登录')}catch(e){dialogError('authDialog',e)}};
-$('credentialForm').onsubmit=async e=>{e.preventDefault();const f=e.currentTarget,cid=f.elements.id.value,body={label:f.elements.label.value,priority:Number(f.elements.priority.value),disabled:f.elements.disabled.checked};if(f.elements.api_key.value.trim())body.api_key=f.elements.api_key.value.trim();try{const saved=await api('/credentials'+(cid?'?id='+encodeURIComponent(cid):''),{method:cid?'PUT':'POST',body});f.elements.api_key.value='';$('credentialDialog').close();toast('凭据已保存 · '+(saved.registration?.message||'宿主登记待确认'));await Promise.all([loadCredentials(),loadDiagnostics()]).catch(globalError)}catch(e){dialogError('credentialDialog',e)}};
-$('modelForm').onsubmit=async e=>{e.preventDefault();const f=e.currentTarget;try{const rules=JSON.parse(f.elements.rules.value);if(!rules||Array.isArray(rules)||typeof rules!=='object')throw new Error('映射规则必须是 JSON 对象');const index=f.elements.index.value,old=index===''?{}:state.config.models[Number(index)],m={...old,id:f.elements.id.value.trim(),upstream_id:f.elements.upstream_id.value.trim(),name:f.elements.name.value.trim(),supported_endpoints:[f.elements.endpoint.value],thinking_mode:f.elements.endpoint.value==="/messages"?f.elements.thinking_mode.value:"",reasoning_mapping:{enabled:f.elements.reasoning_enabled.checked,rules}};const models=structuredClone(state.config.models);if(index==='')models.push(m);else models[Number(index)]=m;await saveModels(models);$('modelDialog').close();toast('模型已保存')}catch(e){dialogError('modelDialog',e)}};
-$('testForm').onsubmit=async e=>{e.preventDefault();const identity=diagnosticIdentity(),f=e.currentTarget,b=f.querySelector('[type=submit]');b.disabled=true;$('testResult').hidden=false;$('testResult').textContent='等待模型回复…';try{const result=await api('/models/test',{method:'POST',body:Object.fromEntries(new FormData(f))});if(!state.authenticated||identity!==diagnosticIdentity())return;$('testResult').textContent=JSON.stringify(result.response,null,2);toast('测试完成')}catch(e){if(state.authenticated&&identity===diagnosticIdentity()){$('testResult').textContent='';dialogError('testDialog',e)}}finally{b.disabled=false;if(state.authenticated&&identity===diagnosticIdentity())await loadModelEvidence().catch(globalError)}};
-$('settingsForm').onsubmit=async e=>{e.preventDefault();const f=e.currentTarget;try{const retention=Number(f.elements.statistics_retention_days.value);if(retention>0&&retention!==state.config.statistics_retention_days&&!await confirmed('设置历史自动过期','超过 '+retention+' 天的统计将被删除,之后延长保留期也无法恢复。继续保存?'))return;await api('/config',{method:'PUT',body:{statistics_retention_days:retention,timeout_seconds:Number(f.elements.timeout_seconds.value),first_byte_timeout_seconds:Number(f.elements.first_byte_timeout_seconds.value),stream_idle_timeout_seconds:Number(f.elements.stream_idle_timeout_seconds.value),log_retention:Number(f.elements.log_retention.value),usage_refresh_seconds:Number(f.elements.usage_refresh_seconds.value),max_response_bytes:Number(f.elements.max_response_mb.value)*1048576,zdr:f.elements.zdr.checked,cpamp_install_dir:f.elements.cpamp_install_dir.value.trim(),cpamp_config_path:f.elements.cpamp_config_path.value.trim(),cpamp_panel_path:f.elements.cpamp_panel_path.value.trim()}});await loadConfig();toast('设置已保存并生效,无需重启')}catch(e){globalError(e)}};
-$('detectCPAMPPanel').onclick=async()=>{try{await saveCPAMPPaths();await Promise.all([loadCPAMPPanel(),loadCompatibility()])}catch(e){globalError(e)}};
-$('installCPAMPPanel').onclick=async()=>{const button=$('installCPAMPPanel');button.disabled=true;try{await saveCPAMPPaths();await loadCPAMPPanel();const plan=state.cpampPanel;if(!plan?.installable)throw new Error(plan?.message||'尚未确认安装目标');if(!await confirmed(plan.hot_update_ready?'更新原生面板':'安装原生面板',[...(plan.planned_changes||[]),'操作完成后将检查实际服务页面。'].join('\n'))){button.disabled=false;return}const result=await api('/cpamp/panel/install',{method:'POST',body:{}});showCPAMPPanel(result);toast(result.message);await loadCompatibility().catch(globalError)}catch(e){globalError(e);button.disabled=false}};
-for(const name of ['cpamp_install_dir','cpamp_config_path','cpamp_panel_path'])$('settingsForm').elements[name].oninput=()=>{$('installCPAMPPanel').disabled=!state.cpampPanel?.available};
-$('addCredential').onclick=()=>editAccount();$('addModel').onclick=()=>editModel();$('fetchModels').onclick=showCatalog;$('catalogSearch').oninput=renderCatalog;$('selectCatalog').onclick=()=>{catalogFiltered().forEach(m=>state.selected.add(m.upstream_id));renderCatalog()};$('catalogRows').onchange=e=>{const id=e.target.dataset.catalogId;if(!id)return;if(e.target.checked)state.selected.add(id);else state.selected.delete(id);$('importCatalog').textContent='导入所选('+state.selected.size+')'};
-$('importCatalog').onclick=async()=>{try{if(!state.selected.size)throw new Error('请先选择模型');await saveModels([...state.config.models,...state.catalog.filter(m=>state.selected.has(m.upstream_id))]);$('catalogDialog').close();toast('模型已导入')}catch(e){dialogError('catalogDialog',e)}};
-$('credentialRows').onclick=async e=>{const b=e.target.closest('button');if(!b)return;if(b.hasAttribute('data-add-account'))return editAccount();if(b.dataset.accountSelect){state.activeCredential=b.dataset.accountSelect;renderCredentials();[...$('credentialRows').querySelectorAll('[data-account-select]')].find(x=>x.dataset.accountSelect===state.activeCredential)?.focus();return}if(b.dataset.accountEdit)return editAccount(b.dataset.accountEdit);try{if(b.dataset.accountRefresh){b.disabled=true;await api('/credentials/usage?id='+encodeURIComponent(b.dataset.accountRefresh)+'&refresh=1',{timeout:20000});await loadCredentials()}if(b.dataset.accountDelete&&await confirmed('删除凭据','此凭据会从 CPA 移除,后续请求将使用其他可用账号。')){await api('/credentials?id='+encodeURIComponent(b.dataset.accountDelete),{method:'DELETE'});await loadCredentials();toast('凭据已删除')}}catch(e){globalError(e)}finally{b.disabled=false}};
-$('modelCredential').onchange=()=>{state.modelCredential=$('modelCredential').value;loadModelEvidence().catch(globalError)};
-async function modelAction(e){const b=e.target.closest('button');if(!b)return;if(b.hasAttribute('data-fetch-catalog'))return showCatalog();if(b.hasAttribute('data-clear-model-filter')){$('modelSearch').value='';$('modelStatus').value='all';renderModels();$('modelSearch').focus();return}if(b.dataset.modelDetail!==undefined)return showModelDetail(Number(b.dataset.modelDetail));if(b.dataset.modelEdit!==undefined)return editModel(Number(b.dataset.modelEdit));try{if(b.dataset.modelTest!==undefined)return await showTest(Number(b.dataset.modelTest));if(b.dataset.modelDelete!==undefined&&await confirmed('移除模型映射','客户端将无法再通过此名称调用模型。')){await saveModels(state.config.models.filter((_,i)=>i!==Number(b.dataset.modelDelete)));toast('模型映射已移除')}}catch(e){globalError(e)}};
-$('modelRows').onclick=modelAction;$('detailContent').onclick=modelAction;
-$('modelSearch').oninput=renderModels;$('modelStatus').onchange=renderModels;
-$('credentialHealth').onclick=e=>{const b=e.target.closest('[data-health-credential]');if(!b)return;state.activeCredential=b.dataset.healthCredential;navigate('credentials')};
-$('logRows').onclick=e=>{const b=e.target.closest('[data-log-detail]');if(!b)return;const row=state.logs?.find(x=>x.id===b.dataset.logDetail);if(!row)return;state.detail='log';showDetail(row.model,'请求详情',logDetailHTML(row))};
-$('refreshUsage').onclick=async()=>{const b=$('refreshUsage');b.disabled=true;try{for(const c of state.credentials.filter(c=>!c.disabled))await api('/credentials/usage?id='+encodeURIComponent(c.id)+'&refresh=1',{timeout:20000});await loadCredentials();toast('用量已刷新')}catch(e){globalError(e)}finally{b.disabled=false}};
-$('clearAffinity').onclick=async()=>{if(!await confirmed('重新分配会话','清除本插件账号的旧会话绑定。下一条请求按当前优先级重新选账号,正在生成的回复继续完成。'))return;try{await api('/credentials/clear-affinity',{method:'POST',body:{},timeout:30000});toast('会话已重新分配 · 宿主已确认');await loadDiagnostics().catch(globalError)}catch(e){globalError(e)}};
-$('resetStats').onclick=async()=>{if(!await confirmed('清空统计','统计将从零开始,请求日志保留。插件会保存原统计的恢复副本。'))return;try{await api('/statistics/reset',{method:'POST',body:{}});await loadStats();toast('统计已清空')}catch(e){globalError(e)}};
-$('logStatus').onchange=()=>{state.offset=0;loadLogs().catch(globalError)};
-$('searchLogs').onclick=()=>{state.offset=0;loadLogs().catch(globalError)};$('logSearch').onkeydown=e=>{if(e.key==='Enter')$('searchLogs').click()};$('prevLogs').onclick=()=>{state.offset=Math.max(0,state.offset-50);loadLogs().catch(globalError)};$('nextLogs').onclick=()=>{state.offset+=50;loadLogs().catch(globalError)};$('applyStats').onclick=()=>loadStats().catch(globalError);$('statsRange').onchange=()=>{$('statsFrom').hidden=$('statsTo').hidden=$('statsRange').value!=='custom'};
-function setTheme(theme,save=false){document.documentElement.dataset.theme=theme;if(save)localStorage.setItem('commandcodebridge:theme',theme)}try{let theme=localStorage.getItem('commandcodebridge:theme');if(!theme&&parent!==window&&parent.location.origin===location.origin){const root=parent.document.documentElement;theme=root.classList.contains('dark')||root.dataset.theme==='dark'?'dark':'light'}setTheme(theme||(matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light'))}catch{setTheme('light')}$('themeButton').onclick=()=>setTheme(document.documentElement.dataset.theme==='dark'?'light':'dark',true);
-setInterval(()=>{if(state.authenticated&&!state.blocked&&!state.busy&&!document.hidden&&!document.querySelector('dialog[open]')&&!document.activeElement?.closest('#credentialRows,#credentialHealth')&&['credentials','overview'].includes(state.view))loadCredentials().catch(globalError)},15000);
-(async()=>{try{const h=window.CommandCodeBridgeAuthHeaders?.()||{};if(!Object.keys(h).length){openDialog('authDialog');return}const status=await api('/status',{verify:true,timeout:15000});state.authenticated=true;$('authButton').textContent='管理身份';$('version').textContent='v'+status.version;await loadConfig();await loadCredentials();await refresh()}catch(e){globalError(e)}})();
+function editAccount(cid) {
+ const f = $('credentialForm');
+ f.reset();
+ const c = state.credentials.find((c) => c.id === cid);
+ f.elements.id.value = cid || '';
+ f.elements.api_key.required = !c;
+ if (c) {
+ f.elements.label.value = c.label;
+ f.elements.priority.value = c.priority;
+ f.elements.disabled.checked = c.disabled;
+ }
+ $('credentialTitle').textContent = c ? '编辑凭据' : '添加凭据';
+ openDialog('credentialDialog');
+}
+const defaultAlias = (upstream) => upstream.trim().split('/').pop();
+let lastSuggestedAlias = '';
+$('modelForm').elements.upstream_id.oninput = (e) => {
+ const f = $('modelForm');
+ if (
+ f.elements.index.value !== '' ||
+ (f.elements.id.value && f.elements.id.value !== lastSuggestedAlias)
+ )
+ return;
+ lastSuggestedAlias = defaultAlias(e.target.value);
+ f.elements.id.value = lastSuggestedAlias;
+};
+function editModel(index) {
+ const f = $('modelForm');
+ f.reset();
+ lastSuggestedAlias = '';
+ const m = state.config?.models[index];
+ f.elements.index.value = m ? index : '';
+ if (m) {
+ f.elements.id.value = m.id;
+ f.elements.upstream_id.value = m.upstream_id;
+ f.elements.name.value = m.name || '';
+ f.elements.endpoint.value = m.supported_endpoints.includes('/chat/completions')
+ ? '/chat/completions'
+ : '/messages';
+ f.elements.thinking_mode.value = m.thinking_mode || 'adaptive';
+ f.elements.reasoning_enabled.checked = m.reasoning_mapping.enabled;
+ f.elements.rules.value = JSON.stringify(m.reasoning_mapping.rules || {}, null, 2);
+ }
+ $('modelTitle').textContent = m ? '编辑模型' : '添加模型';
+ openDialog('modelDialog');
+}
+async function saveModels(models) {
+ const aliases = new Set();
+ for (const m of models) {
+ if (aliases.has(m.id))
+ throw new Error('客户端名称重复:' + m.id + '。请减少所选模型,或手动添加并设置不同名称。');
+ aliases.add(m.id);
+ }
+ await api('/models', { method: 'PUT', body: { models } });
+ await loadConfig();
+ await loadModelEvidence();
+}
+async function showCatalog() {
+ const identity = diagnosticIdentity();
+ try {
+ const result = await api('/models/refresh', { method: 'POST', body: {}, timeout: 20000 });
+ if (!state.authenticated || identity !== diagnosticIdentity()) return;
+ state.catalog = result.models;
+ state.selected.clear();
+ $('catalogSearch').value = '';
+ renderCatalog();
+ openDialog('catalogDialog');
+ } catch (e) {
+ if (state.authenticated && identity === diagnosticIdentity()) globalError(e);
+ }
+}
+function catalogFiltered() {
+ const text = $('catalogSearch').value.toLowerCase();
+ const existing = new Set((state.config?.models || []).map((m) => m.upstream_id));
+ return state.catalog.filter(
+ (m) =>
+ !existing.has(m.upstream_id) &&
+ (!text || (m.name + ' ' + m.upstream_id).toLowerCase().includes(text))
+ );
+}
+function renderCatalog() {
+ const rows = catalogFiltered();
+ $('catalogRows').innerHTML = rows.length
+ ? rows
+ .map(
+ (m) =>
+ ``
+ )
+ .join('')
+ : '没有可导入的模型。
';
+ $('importCatalog').textContent =
+ `导入所选${state.selected.size ? '(' + state.selected.size + ')' : ''}`;
+}
+async function showTest(index) {
+ const identity = diagnosticIdentity();
+ await loadCredentials();
+ if (!state.authenticated || identity !== diagnosticIdentity()) return;
+ const m = state.config.models[index],
+ f = $('testForm');
+ f.reset();
+ f.elements.model.value = m.id;
+ $('testModelName').textContent = m.id;
+ f.elements.credential_id.innerHTML = state.credentials
+ .filter((c) => !c.disabled)
+ .map((c) => ``)
+ .join('');
+ if (state.modelCredential) f.elements.credential_id.value = state.modelCredential;
+ $('testResult').hidden = true;
+ openDialog('testDialog');
+}
+document.querySelectorAll('dialog').forEach((d) => {
+ d.addEventListener('close', () => restoreDialogFocus(d.id));
+ d.setAttribute(
+ 'aria-labelledby',
+ d.querySelector('h2').id || (d.querySelector('h2').id = d.id + 'Title')
+ );
+});
+document
+ .querySelectorAll('[data-view]')
+ .forEach((b) => (b.onclick = () => navigate(b.dataset.view)));
+document
+ .querySelectorAll('[data-jump]')
+ .forEach((b) => (b.onclick = () => navigate(b.dataset.jump)));
+document
+ .querySelectorAll('[data-close]')
+ .forEach((b) => (b.onclick = () => $(b.dataset.close).close()));
+document.querySelectorAll('[data-action="refresh"]').forEach((b) => (b.onclick = refresh));
+$('authButton').onclick = () => {
+ $('rememberAuth').checked = window.CommandCodeBridgeRememberAuth?.() || false;
+ openDialog('authDialog');
+};
+$('clearAuth').onclick = () => {
+ window.CommandCodeBridgeClearAuth?.();
+ resetDiagnosticViews();
+ state.key = '';
+ state.authenticated = false;
+ state.blocked = true;
+ $('managementKey').value = '';
+ toast('已退出登录');
+};
+$('authForm').onsubmit = async (e) => {
+ e.preventDefault();
+ resetDiagnosticViews();
+ state.authenticated = false;
+ state.key = $('managementKey').value.trim();
+ state.authType = $('authType').value;
+ try {
+ const status = await api('/status', { verify: true, timeout: 15000 });
+ window.CommandCodeBridgeSaveAuth?.(state.key, state.authType, $('rememberAuth').checked);
+ state.authenticated = true;
+ $('authButton').textContent = '管理身份';
+ state.blocked = false;
+ $('managementKey').value = '';
+ $('authDialog').close();
+ $('version').textContent = 'v' + status.version;
+ await loadConfig();
+ await loadCredentials();
+ await refresh();
+ toast('已登录');
+ } catch (e) {
+ dialogError('authDialog', e);
+ }
+};
+$('credentialForm').onsubmit = async (e) => {
+ e.preventDefault();
+ const f = e.currentTarget,
+ cid = f.elements.id.value,
+ body = {
+ label: f.elements.label.value,
+ priority: Number(f.elements.priority.value),
+ disabled: f.elements.disabled.checked,
+ };
+ if (f.elements.api_key.value.trim()) body.api_key = f.elements.api_key.value.trim();
+ try {
+ const saved = await api('/credentials' + (cid ? '?id=' + encodeURIComponent(cid) : ''), {
+ method: cid ? 'PUT' : 'POST',
+ body,
+ });
+ f.elements.api_key.value = '';
+ $('credentialDialog').close();
+ toast('凭据已保存 · ' + (saved.registration?.message || '宿主登记待确认'));
+ await Promise.all([loadCredentials(), loadDiagnostics()]).catch(globalError);
+ } catch (e) {
+ dialogError('credentialDialog', e);
+ }
+};
+$('modelForm').onsubmit = async (e) => {
+ e.preventDefault();
+ const f = e.currentTarget;
+ try {
+ const rules = JSON.parse(f.elements.rules.value);
+ if (!rules || Array.isArray(rules) || typeof rules !== 'object')
+ throw new Error('映射规则必须是 JSON 对象');
+ const index = f.elements.index.value,
+ old = index === '' ? {} : state.config.models[Number(index)],
+ m = {
+ ...old,
+ id: f.elements.id.value.trim(),
+ upstream_id: f.elements.upstream_id.value.trim(),
+ name: f.elements.name.value.trim(),
+ supported_endpoints: [f.elements.endpoint.value],
+ thinking_mode:
+ f.elements.endpoint.value === '/messages' ? f.elements.thinking_mode.value : '',
+ reasoning_mapping: { enabled: f.elements.reasoning_enabled.checked, rules },
+ };
+ const models = structuredClone(state.config.models);
+ if (index === '') models.push(m);
+ else models[Number(index)] = m;
+ await saveModels(models);
+ $('modelDialog').close();
+ toast('模型已保存');
+ } catch (e) {
+ dialogError('modelDialog', e);
+ }
+};
+$('testForm').onsubmit = async (e) => {
+ e.preventDefault();
+ const identity = diagnosticIdentity(),
+ f = e.currentTarget,
+ b = f.querySelector('[type=submit]');
+ b.disabled = true;
+ $('testResult').hidden = false;
+ $('testResult').textContent = '等待模型回复…';
+ try {
+ const result = await api('/models/test', {
+ method: 'POST',
+ body: Object.fromEntries(new FormData(f)),
+ });
+ if (!state.authenticated || identity !== diagnosticIdentity()) return;
+ $('testResult').textContent = JSON.stringify(result.response, null, 2);
+ toast('测试完成');
+ } catch (e) {
+ if (state.authenticated && identity === diagnosticIdentity()) {
+ $('testResult').textContent = '';
+ dialogError('testDialog', e);
+ }
+ } finally {
+ b.disabled = false;
+ if (state.authenticated && identity === diagnosticIdentity())
+ await loadModelEvidence().catch(globalError);
+ }
+};
+$('settingsForm').onsubmit = async (e) => {
+ e.preventDefault();
+ const f = e.currentTarget;
+ try {
+ const retention = Number(f.elements.statistics_retention_days.value);
+ if (
+ retention > 0 &&
+ retention !== state.config.statistics_retention_days &&
+ !(await confirmed(
+ '设置历史自动过期',
+ '超过 ' + retention + ' 天的统计将被删除,之后延长保留期也无法恢复。继续保存?'
+ ))
+ )
+ return;
+ await api('/config', {
+ method: 'PUT',
+ body: {
+ statistics_retention_days: retention,
+ timeout_seconds: Number(f.elements.timeout_seconds.value),
+ first_byte_timeout_seconds: Number(f.elements.first_byte_timeout_seconds.value),
+ stream_idle_timeout_seconds: Number(f.elements.stream_idle_timeout_seconds.value),
+ log_retention: Number(f.elements.log_retention.value),
+ usage_refresh_seconds: Number(f.elements.usage_refresh_seconds.value),
+ max_response_bytes: Number(f.elements.max_response_mb.value) * 1048576,
+ zdr: f.elements.zdr.checked,
+ cpamp_install_dir: f.elements.cpamp_install_dir.value.trim(),
+ cpamp_config_path: f.elements.cpamp_config_path.value.trim(),
+ cpamp_panel_path: f.elements.cpamp_panel_path.value.trim(),
+ },
+ });
+ await loadConfig();
+ toast('设置已保存并生效,无需重启');
+ } catch (e) {
+ globalError(e);
+ }
+};
+$('detectCPAMPPanel').onclick = async () => {
+ try {
+ await saveCPAMPPaths();
+ await Promise.all([loadCPAMPPanel(), loadCompatibility()]);
+ } catch (e) {
+ globalError(e);
+ }
+};
+$('installCPAMPPanel').onclick = async () => {
+ const button = $('installCPAMPPanel');
+ button.disabled = true;
+ try {
+ await saveCPAMPPaths();
+ await loadCPAMPPanel();
+ const plan = state.cpampPanel;
+ if (!plan?.installable) throw new Error(plan?.message || '尚未确认安装目标');
+ if (
+ !(await confirmed(
+ plan.hot_update_ready ? '更新原生面板' : '安装原生面板',
+ [...(plan.planned_changes || []), '操作完成后将检查实际服务页面。'].join('\n')
+ ))
+ ) {
+ button.disabled = false;
+ return;
+ }
+ const result = await api('/cpamp/panel/install', { method: 'POST', body: {} });
+ showCPAMPPanel(result);
+ toast(result.message);
+ await loadCompatibility().catch(globalError);
+ } catch (e) {
+ globalError(e);
+ button.disabled = false;
+ }
+};
+for (const name of ['cpamp_install_dir', 'cpamp_config_path', 'cpamp_panel_path'])
+ $('settingsForm').elements[name].oninput = () => {
+ $('installCPAMPPanel').disabled = !state.cpampPanel?.available;
+ };
+$('addCredential').onclick = () => editAccount();
+$('addModel').onclick = () => editModel();
+$('fetchModels').onclick = showCatalog;
+$('catalogSearch').oninput = renderCatalog;
+$('selectCatalog').onclick = () => {
+ catalogFiltered().forEach((m) => state.selected.add(m.upstream_id));
+ renderCatalog();
+};
+$('catalogRows').onchange = (e) => {
+ const id = e.target.dataset.catalogId;
+ if (!id) return;
+ if (e.target.checked) state.selected.add(id);
+ else state.selected.delete(id);
+ $('importCatalog').textContent = '导入所选(' + state.selected.size + ')';
+};
+$('importCatalog').onclick = async () => {
+ try {
+ if (!state.selected.size) throw new Error('请先选择模型');
+ await saveModels([
+ ...state.config.models,
+ ...state.catalog.filter((m) => state.selected.has(m.upstream_id)),
+ ]);
+ $('catalogDialog').close();
+ toast('模型已导入');
+ } catch (e) {
+ dialogError('catalogDialog', e);
+ }
+};
+$('credentialRows').onclick = async (e) => {
+ const b = e.target.closest('button');
+ if (!b) return;
+ if (b.hasAttribute('data-add-account')) return editAccount();
+ if (b.dataset.accountSelect) {
+ state.activeCredential = b.dataset.accountSelect;
+ renderCredentials();
+ [...$('credentialRows').querySelectorAll('[data-account-select]')]
+ .find((x) => x.dataset.accountSelect === state.activeCredential)
+ ?.focus();
+ return;
+ }
+ if (b.dataset.accountEdit) return editAccount(b.dataset.accountEdit);
+ try {
+ if (b.dataset.accountRefresh) {
+ b.disabled = true;
+ await api(
+ '/credentials/usage?id=' + encodeURIComponent(b.dataset.accountRefresh) + '&refresh=1',
+ { timeout: 20000 }
+ );
+ await loadCredentials();
+ }
+ if (
+ b.dataset.accountDelete &&
+ (await confirmed('删除凭据', '此凭据会从 CPA 移除,后续请求将使用其他可用账号。'))
+ ) {
+ await api('/credentials?id=' + encodeURIComponent(b.dataset.accountDelete), {
+ method: 'DELETE',
+ });
+ await loadCredentials();
+ toast('凭据已删除');
+ }
+ } catch (e) {
+ globalError(e);
+ } finally {
+ b.disabled = false;
+ }
+};
+$('modelCredential').onchange = () => {
+ state.modelCredential = $('modelCredential').value;
+ loadModelEvidence().catch(globalError);
+};
+async function modelAction(e) {
+ const b = e.target.closest('button');
+ if (!b) return;
+ if (b.hasAttribute('data-fetch-catalog')) return showCatalog();
+ if (b.hasAttribute('data-clear-model-filter')) {
+ $('modelSearch').value = '';
+ $('modelStatus').value = 'all';
+ renderModels();
+ $('modelSearch').focus();
+ return;
+ }
+ if (b.dataset.modelDetail !== undefined) return showModelDetail(Number(b.dataset.modelDetail));
+ if (b.dataset.modelEdit !== undefined) return editModel(Number(b.dataset.modelEdit));
+ try {
+ if (b.dataset.modelTest !== undefined) return await showTest(Number(b.dataset.modelTest));
+ if (
+ b.dataset.modelDelete !== undefined &&
+ (await confirmed('移除模型映射', '客户端将无法再通过此名称调用模型。'))
+ ) {
+ await saveModels(state.config.models.filter((_, i) => i !== Number(b.dataset.modelDelete)));
+ toast('模型映射已移除');
+ }
+ } catch (e) {
+ globalError(e);
+ }
+}
+$('modelRows').onclick = modelAction;
+$('detailContent').onclick = modelAction;
+$('modelSearch').oninput = renderModels;
+$('modelStatus').onchange = renderModels;
+$('credentialHealth').onclick = (e) => {
+ const b = e.target.closest('[data-health-credential]');
+ if (!b) return;
+ state.activeCredential = b.dataset.healthCredential;
+ navigate('credentials');
+};
+$('logRows').onclick = (e) => {
+ const b = e.target.closest('[data-log-detail]');
+ if (!b) return;
+ const row = state.logs?.find((x) => x.id === b.dataset.logDetail);
+ if (!row) return;
+ state.detail = 'log';
+ showDetail(row.model, '请求详情', logDetailHTML(row));
+};
+$('refreshUsage').onclick = async () => {
+ const b = $('refreshUsage');
+ b.disabled = true;
+ try {
+ for (const c of state.credentials.filter((c) => !c.disabled))
+ await api('/credentials/usage?id=' + encodeURIComponent(c.id) + '&refresh=1', {
+ timeout: 20000,
+ });
+ await loadCredentials();
+ toast('用量已刷新');
+ } catch (e) {
+ globalError(e);
+ } finally {
+ b.disabled = false;
+ }
+};
+$('clearAffinity').onclick = async () => {
+ if (
+ !(await confirmed(
+ '重新分配会话',
+ '清除本插件账号的旧会话绑定。下一条请求按当前优先级重新选账号,正在生成的回复继续完成。'
+ ))
+ )
+ return;
+ try {
+ await api('/credentials/clear-affinity', { method: 'POST', body: {}, timeout: 30000 });
+ toast('会话已重新分配 · 宿主已确认');
+ await loadDiagnostics().catch(globalError);
+ } catch (e) {
+ globalError(e);
+ }
+};
+$('resetStats').onclick = async () => {
+ if (!(await confirmed('清空统计', '统计将从零开始,请求日志保留。插件会保存原统计的恢复副本。')))
+ return;
+ try {
+ await api('/statistics/reset', { method: 'POST', body: {} });
+ await loadStats();
+ toast('统计已清空');
+ } catch (e) {
+ globalError(e);
+ }
+};
+$('logStatus').onchange = () => {
+ state.offset = 0;
+ loadLogs().catch(globalError);
+};
+$('searchLogs').onclick = () => {
+ state.offset = 0;
+ loadLogs().catch(globalError);
+};
+$('logSearch').onkeydown = (e) => {
+ if (e.key === 'Enter') $('searchLogs').click();
+};
+$('prevLogs').onclick = () => {
+ state.offset = Math.max(0, state.offset - 50);
+ loadLogs().catch(globalError);
+};
+$('nextLogs').onclick = () => {
+ state.offset += 50;
+ loadLogs().catch(globalError);
+};
+$('applyStats').onclick = () => loadStats().catch(globalError);
+$('statsRange').onchange = () => {
+ $('statsFrom').hidden = $('statsTo').hidden = $('statsRange').value !== 'custom';
+};
+function setTheme(theme, save = false) {
+ document.documentElement.dataset.theme = theme;
+ if (save) localStorage.setItem('commandcodebridge:theme', theme);
+}
+try {
+ let theme = localStorage.getItem('commandcodebridge:theme');
+ if (!theme && parent !== window && parent.location.origin === location.origin) {
+ const root = parent.document.documentElement;
+ theme = root.classList.contains('dark') || root.dataset.theme === 'dark' ? 'dark' : 'light';
+ }
+ setTheme(theme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'));
+} catch {
+ setTheme('light');
+}
+$('themeButton').onclick = () =>
+ setTheme(document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark', true);
+setInterval(() => {
+ if (
+ state.authenticated &&
+ !state.blocked &&
+ !state.busy &&
+ !document.hidden &&
+ !document.querySelector('dialog[open]') &&
+ !document.activeElement?.closest('#credentialRows,#credentialHealth') &&
+ ['credentials', 'overview'].includes(state.view)
+ )
+ loadCredentials().catch(globalError);
+}, 15000);
+(async () => {
+ try {
+ const h = window.CommandCodeBridgeAuthHeaders?.() || {};
+ if (!Object.keys(h).length) {
+ openDialog('authDialog');
+ return;
+ }
+ const status = await api('/status', { verify: true, timeout: 15000 });
+ state.authenticated = true;
+ $('authButton').textContent = '管理身份';
+ $('version').textContent = 'v' + status.version;
+ await loadConfig();
+ await loadCredentials();
+ await refresh();
+ } catch (e) {
+ globalError(e);
+ }
+})();
diff --git a/tests/console-diagnostics.test.cjs b/tests/console-diagnostics.test.cjs
index 80824c9..ef22b4f 100644
--- a/tests/console-diagnostics.test.cjs
+++ b/tests/console-diagnostics.test.cjs
@@ -4,6 +4,12 @@ const path = require('node:path');
const vm = require('node:vm');
const { test } = require('node:test');
const script = fs.readFileSync(path.join(__dirname, '../internal/bridge/ui/console.js'), 'utf8');
+// Slice the formatted console source between two unique markers; fail loudly if either moves.
+function between(text, start, end) {
+ const from = text.indexOf(start), to = text.indexOf(end, from);
+ assert.ok(from >= 0 && to > from && text.indexOf(start, from + 1) < 0, `console.js markers not found: ${start} … ${end}`);
+ return text.slice(from, to);
+}
function fixture(api = async () => ({})) {
const nodes = new Map();
const context = {
@@ -13,7 +19,7 @@ function fixture(api = async () => ({})) {
$: id => { if (!nodes.has(id)) nodes.set(id, {}); return nodes.get(id); },
};
vm.createContext(context);
- vm.runInContext(script.slice(script.indexOf('const diagnosticStatus='), script.indexOf("$('runDiagnostics').onclick")), context);
+ vm.runInContext(between(script, 'const diagnosticStatus =', "$('runDiagnostics').onclick"), context);
return context;
}
function report(label) {
@@ -56,7 +62,7 @@ test('rejected management access immediately clears rendered diagnostics', async
fetch: async () => ({ok: false, status: 401, text: async () => '{"error":"unauthorized"}'}),
apiURL: value => value, AbortSignal, openDialog: () => {},
});
- vm.runInContext(script.slice(script.indexOf('async function api('), script.indexOf('async function confirmed(')), context);
+ vm.runInContext(between(script, 'async function api(', 'async function confirmed('), context);
await assert.rejects(context.api('/diagnostics'), /unauthorized/);
assert.equal(context.state.authenticated, false);
assert.equal(context.$('diagnosticResults').textContent, '登录后重新诊断');
diff --git a/tests/console-evidence.test.cjs b/tests/console-evidence.test.cjs
index 3deef74..342eea9 100644
--- a/tests/console-evidence.test.cjs
+++ b/tests/console-evidence.test.cjs
@@ -3,6 +3,12 @@ const fs = require('node:fs');
const vm = require('node:vm');
const {test} = require('node:test');
const script = fs.readFileSync(require('node:path').join(__dirname,'../internal/bridge/ui/console.js'),'utf8');
+// Slice the formatted console source between two unique markers; fail loudly if either moves.
+function between(text, start, end) {
+ const from = text.indexOf(start), to = text.indexOf(end, from);
+ assert.ok(from >= 0 && to > from && text.indexOf(start, from + 1) < 0, `console.js markers not found: ${start} … ${end}`);
+ return text.slice(from, to);
+}
function fixture(api) {
const nodes = new Map();
const context = {state:{authenticated:true,key:'fixture',credentials:[{id:'one',label:'one'},{id:'two',label:'two'}]},
@@ -11,7 +17,7 @@ function fixture(api) {
esc:x=>String(x??'').replace(/x,when:x=>x,fmt:x=>x,
$:id=>{if(!nodes.has(id))nodes.set(id,{});return nodes.get(id)}};
vm.createContext(context);
- vm.runInContext(script.slice(script.indexOf('const modelEvidenceLabels='),script.indexOf('function renderModels()')),context);
+ vm.runInContext(between(script, 'const modelEvidenceLabels =', 'function renderModels()'),context);
return context;
}
test('evidence selector discards delayed results from another credential',async()=>{
@@ -37,7 +43,7 @@ test('evidence escapes protocol and distinguishes source, expiry and generic den
test('model permission failures do not log out management access',async()=>{
const c=fixture();Object.assign(c,{AbortSignal,apiURL:x=>x,openDialog:()=>{},resetDiagnosticViews:()=>{throw Error('unexpected logout')},
fetch:async()=>({ok:false,status:403,text:async()=>' {"error":"model permission denied"}'})});
- vm.runInContext(script.slice(script.indexOf('async function api('),script.indexOf('async function confirmed(')),c);
+ vm.runInContext(between(script, 'async function api(', 'async function confirmed('),c);
await assert.rejects(c.api('/models/test'),/model permission denied/);
assert.equal(c.state.authenticated,true);
});
diff --git a/tests/console-ui.test.cjs b/tests/console-ui.test.cjs
index 78782d1..99c9537 100644
--- a/tests/console-ui.test.cjs
+++ b/tests/console-ui.test.cjs
@@ -3,10 +3,16 @@ const fs = require('node:fs');
const vm = require('node:vm');
const {test} = require('node:test');
const source = fs.readFileSync(require('node:path').join(__dirname, '../internal/bridge/ui/console.js'), 'utf8');
+// Slice the formatted console source between two unique markers; fail loudly if either moves.
+function between(text, start, end) {
+ const from = text.indexOf(start), to = text.indexOf(end, from);
+ assert.ok(from >= 0 && to > from && text.indexOf(start, from + 1) < 0, `console.js markers not found: ${start} … ${end}`);
+ return text.slice(from, to);
+}
const escape = value => String(value ?? '').replace(/[&<>"']/g, c => `${c.charCodeAt(0)};`);
function functions(start, end, context) {
vm.createContext(context);
- vm.runInContext(source.slice(source.indexOf(start), source.indexOf(end)), context);
+ vm.runInContext(between(source, start, end), context);
return context;
}
test('model filters combine evidence with case-insensitive aliases and retain original action indices', () => {
@@ -46,13 +52,13 @@ test('closing a drawer after list refresh restores focus to its replacement trig
let focused='';const old={isConnected:false,dataset:{logDetail:'request'}},replacement={dataset:{logDetail:'request'},focus(){focused='replacement'}};
const dialog={open:false,querySelectorAll:()=>[],showModal(){this.open=true}};
const document={activeElement:old,querySelector:()=>null,querySelectorAll:()=>[replacement]};
- const c=functions('const dialogOrigins=', 'function apiURL(',{state:{view:'logs'},document,$:()=>dialog});
+ const c=functions('const dialogOrigins =', 'function apiURL(',{state:{view:'logs'},document,$:()=>dialog});
c.openDialog('detailDialog');c.restoreDialogFocus('detailDialog');assert.equal(focused,'replacement');
});
test('closing an older dialog never steals focus from the next modal',()=>{
let focused=false;const dialog={open:false,querySelectorAll:()=>[],showModal(){this.open=true}};
const document={activeElement:{isConnected:true,dataset:{},focus(){focused=true}},querySelector:()=>({open:true})};
- const c=functions('const dialogOrigins=', 'function apiURL(',{state:{view:'models'},document,$:()=>dialog});
+ const c=functions('const dialogOrigins =', 'function apiURL(',{state:{view:'models'},document,$:()=>dialog});
c.openDialog('detailDialog');c.restoreDialogFocus('detailDialog');assert.equal(focused,false);
});
@@ -62,14 +68,14 @@ test('focus restoration skips the hidden desktop trigger after a mobile layout c
const visible={dataset:{modelDetail:'2'},getClientRects:()=>[{}],focus(){focused='visible'}};
const dialog={open:false,querySelectorAll:()=>[],showModal(){this.open=true}};
const document={activeElement:old,querySelector:()=>null,querySelectorAll:()=>[hidden,visible]};
- const c=functions('const dialogOrigins=', 'function apiURL(',{state:{view:'models'},document,$:()=>dialog});
+ const c=functions('const dialogOrigins =', 'function apiURL(',{state:{view:'models'},document,$:()=>dialog});
c.openDialog('detailDialog');c.restoreDialogFocus('detailDialog');assert.equal(focused,'visible');
});
test('pending storage and recovery rows escape filenames and preserve original observation times',()=>{
const nodes=new Map();const c={$:id=>{if(!nodes.has(id))nodes.set(id,{});return nodes.get(id)},esc:escape,fmt:String,when:x=>x||'未知'};
functions('function statusRows(', 'function apiURL(',c);
- vm.runInContext(source.slice(source.indexOf('function renderStorage('),source.indexOf('async function loadStorage(')),c);
+ vm.runInContext(between(source, 'function renderStorage(', 'async function loadStorage('),c);
c.renderStorage({precision:'minute/day',last_success_at:'original time',pending_files:['usage.json'],pending_since:'pending time',statistics_buckets:7,recovery_warnings:[{file:'',message:'',checked_at:'recovery time'}]});
const html=c.$('storageStatus').innerHTML;
for(const value of ['original time','pending time','usage.json','history gap','recovery time','恢复后的历史可能不完整'])assert.ok(html.includes(value));
@@ -80,14 +86,14 @@ test('catalog and test dialogs cannot reopen after management logout during load
let resolve;const state={authenticated:true,key:'old',catalog:[]};const c={state,diagnosticIdentity:()=>state.key,api:()=>new Promise(done=>resolve=done),loadCredentials:()=>new Promise(done=>resolve=done),openDialog:()=>assert.fail('stale dialog opened'),globalError:()=>assert.fail('stale error rendered')};
functions('async function showCatalog(', 'function catalogFiltered(',c);
const catalog=c.showCatalog();state.authenticated=false;state.key='';resolve({models:[{id:'private'}]});await catalog;assert.equal(state.catalog.length,0);
- vm.runInContext(source.slice(source.indexOf('async function showTest('),source.indexOf("document.querySelectorAll('dialog')")),c);
+ vm.runInContext(between(source, 'async function showTest(', "document.querySelectorAll('dialog')"),c);
state.authenticated=true;state.key='new';const test=c.showTest(0);state.authenticated=false;resolve();await test;
});
test('late direct-test responses never restore output or evidence after logout',async()=>{
let resolve;const nodes=new Map(),state={authenticated:true,key:'one'},button={};
const c={state,diagnosticIdentity:()=>state.key,$:id=>{if(!nodes.has(id))nodes.set(id,{});return nodes.get(id)},api:()=>new Promise(done=>resolve=done),FormData:class{*[Symbol.iterator](){yield ['model','mock']}},toast:()=>assert.fail('stale success'),loadModelEvidence:()=>assert.fail('stale refresh'),dialogError:()=>assert.fail('stale error')};
- vm.createContext(c);vm.runInContext(source.slice(source.indexOf("$('testForm').onsubmit="),source.indexOf("$('settingsForm').onsubmit=")),c);
+ vm.createContext(c);vm.runInContext(between(source, "$('testForm').onsubmit =", "$('settingsForm').onsubmit ="),c);
const request=c.$('testForm').onsubmit({preventDefault(){},currentTarget:{querySelector:()=>button}});
state.authenticated=false;state.key='two';c.$('testResult').textContent='';resolve({response:'private'});await request;
assert.equal(c.$('testResult').textContent,'');assert.equal(button.disabled,false);