Files
HomeAgent/internal/plugins/webui/dashboard.css
JianFeeeee 1e58af79d3 feat(webui): 通用反向代理 —— 插件声明服务,HomeAgent 按子域反代出去
用户要求:外部只装 HomeAgent 即可使用自带反代能力;用户只需穿透一个
webui 端口就能访问所有内部插件服务;认证与 WebSocket 支持都作为插件
的可声明项;插件 UI 要有可直接点击的入口。

实测 huawei_smarthome 插件的前端用**根绝对路径**(api('/api/status') →
fetch('/api/status'))。挂在 /p/<name>/ 这类路径前缀下,这些请求会打到
HomeAgent 自己的 /api/status —— 静默错路由;做 HTML/JS 内容重写对拼进
JS 字符串的绝对路径只是"按概率能用",会产生"页面能开、某个按钮就坏"的
静默故障。子域路由下根路径天然正确,**插件前端零改动**。

且它天然匹配"只穿透一个端口":webui 监听 0.0.0.0:8080 按 Host 分发,
外层 frp 单端口 TCP 隧道**一行都不用改**。

默认基座 localhost:RFC 6761 规定 *.localhost 强制解析到 loopback,
现代浏览器原生支持 ⇒ <标签>.localhost:8080 **零配置可用**,不需要 DNS、
证书、/etc/hosts。远程部署改 base_domain 即可。

- 外部插件 → plugin.json 的 proxies(静态可发现:插件没起来也能报
  "声明了 ui 但目标不可达",而不是静默 404)
- 内置插件 → s.DeclareProxy()(remotedevice 是内置的、没有 plugin.json,
  却最需要被反代出去)

反代层在 webui 侧读清单:webui 已能拿到插件目录(PluginManager.PluginDir),
因此**无需给内核接口加方法**。manifest 解析忽略未知字段,加 proxies 对
"旧内核读新插件"与"新内核读旧插件"都无害。

新增 sdk/ProxyDecl 与配套校验(ValidProxyAuth / ValidProxyHostLabel /
NormalizeProxyHost / ValidateProxyDecl);新增运行期 ProxyDeclarer 通道。
hmapdev 的 writePluginJSON 是**白名单 map 重建**——不同步加字段会让声明
被打包静默丢弃(插件作者本地正常、装上去失效),因此 PlgConfig 与
writePluginJSON 同时加,并在打包前校验声明(插件作者本地就能发现写错)。

auth=homeagent(默认,安全的默认):门户会话 / X-API-Key / ?__token=;
auth=none:信任上游自身鉴权,供设备与嵌入式客户端使用——它们不可能持有
浏览器会话,强制走门户鉴权会把设备链路挡死。remotedevice 声明 none,
因为它自身用 ws_token 强制校验。

未声明时升级请求**明确拒绝**(400 + 原因),而不是静默降级成普通请求
(后者表现为前端不断重连、日志看不出原因)。

1. 不跟随上游 3xx:旧实现用 http.DefaultClient(默认跟最多 10 跳),
   上游 302 到内网地址时反代自己跟过去、失败回 502 并把内网 URL 泄给
   客户端。httputil.ReverseProxy 默认不跟随,3xx 原样透传。
2. 逐帧 flush:旧实现 io.Copy 导致上游流式响应被缓冲到上游关闭才下发
   (实测 3 帧 200ms 间隔的流,客户端在 +600ms 一次性收到全部)。
   设 FlushInterval=-1。

另补齐 X-Forwarded-For/Host/Proto(旧实现完全不注入,上游无法判断真实
来源),并剥掉上游 Set-Cookie 的 Domain(防止插件 cookie 打到主门户域)。

插件页新增「服务入口」卡片:列出全部被反代的插件服务(含被拒条目与
不可达原因),点「打开」直接访问。链接带 ?__token=<api_key>,因为子域
与门户不同源、浏览器不会自动带会话 cookie。

webui +35 条、SDK +4 条、工具链 +4 条。关键几条:
- 根绝对路径必须原样到上游(选 Host 路由的核心理由)
- 上游 302 必须原样透传、且反代不得跟随(旧缺陷)
- 已知 Content-Length 的慢速响应必须逐帧到达(**这条经过变异验证**:
  把 FlushInterval 改回 0 后判据挂死 → FAIL,还原后回绿。
  说明:最初写的 SSE/chunked 版本是假判据——ReverseProxy 对
  text/event-stream 与 ContentLength=-1 会自动立即 flush,与
  FlushInterval 无关,变异抓不到,已改正)
- 子域标签冲突不得静默覆盖(后者保留可见并带原因)
- 非法声明不进路由但必须可见(配置页要能看到原因)
- 未声明 websocket 的升级请求必须 400
- auth 逐条生效:none 放行匿名、homeagent 与默认档 401 且给可操作提示
- 自动发现:显式 host 不得被自动编号覆盖(**测试抓到的真 bug**:
  remotedevice 声明的 "devices" 会被改成 "devices-2" 而静默失效)
- 真实端到端:生产实例 huawei_smarthome 的 UI(9444 字节)与其
  /api/status 经反代正确透传

go build ./... 通过;相关包全量测试通过。
internal/plugin/proc 的 TestStreaming_PublishLatencyFlatAcrossSubscribers
是**预存在的不稳定测试**(同一份代码 10 次跑 9 过 1 败,且本改动完全
未触及该包),非本次引入。
2026-09-26 13:49:59 +08:00

3005 lines
87 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

:root {
/* ===== NapCat Design DNA tokens ===== */
--sakura-100: #ffe4e9;
--sakura-200: #ffcdd9;
--sakura-300: #ff9eb5;
--sakura-400: #ff7fac;
--sakura-500: #f33b7c;
--sakura-600: #c92462;
--sakura-700: #991b4b;
--frost-100: #d7e8ee;
--frost-200: #a9d6e3;
--frost-300: #88c0d0;
--frost-400: #5ea8bf;
--frost-500: #3f88a3;
--success: #17a964;
--warning: #d99a2b;
--error: #db3694;
--info: #3f6ef5;
--glass-bg: rgba(13, 18, 34, 0.6);
--glass-bg-strong: rgba(13, 18, 34, 0.82);
--glass-blur: 14px;
--glass-border: rgba(255, 255, 255, 0.08);
--glass-hover: rgba(255, 255, 255, 0.05);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.25);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.28);
--shadow-lg: 0 12px 36px rgba(0, 0, 0, 0.38);
--shadow-glow:
0 0 0 1px rgba(var(--accent-rgb), 0.4),
0 4px 20px rgba(var(--accent-rgb), 0.18);
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-pill: 999px;
--font-sans:
"Segoe UI", "Segoe UI Variable Text", -apple-system,
BlinkMacSystemFont, Roboto, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
--font-mono:
"JetBrains Mono", ui-monospace, "SFMono-Regular", "SF Mono", "Menlo",
"Consolas", monospace;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--dur-micro: 150ms;
--dur-normal: 300ms;
/* ===== Compat tokens (rethemed) ===== */
--bg-primary: #0b1020;
--bg-secondary: rgba(17, 24, 44, 0.72);
--bg-card: rgba(17, 24, 44, 0.6);
--bg-input: rgba(13, 18, 34, 0.75);
--bg-hover: rgba(255, 255, 255, 0.06);
--text-primary: #eef1f8;
--text-secondary: #a7b0c4;
--text-muted: #77809a;
--border-color: rgba(255, 255, 255, 0.09);
/* accent 的 rgb 分量:所有 hardcoded rgba(255,127,172,x) 都改用它,
这样换配色(尤其 monochrome)时连阴影/描边一起换,不会残留粉色。 */
--accent-rgb: 255, 127, 172;
--accent-strong-rgb: 243, 59, 124;
--accent: #ff7fac;
--accent-bg: rgba(var(--accent-rgb), 0.14);
--toast-bg: rgba(23, 169, 100, 0.16);
--toast-color: #6ee7a8;
--toast-error-bg: rgba(219, 54, 148, 0.18);
--toast-error-color: #ff9ec6;
--pre-color: #cdd3f5;
--pre-bg: rgba(10, 14, 28, 0.85);
--chat-bg: rgba(10, 14, 28, 0.7);
--msg-user-bg: rgba(var(--accent-rgb), 0.16);
--msg-user-color: #ffb9d0;
--msg-assistant-bg: rgba(136, 192, 208, 0.14);
--msg-assistant-color: #a9d6e3;
--msg-system-bg: rgba(63, 110, 245, 0.16);
--msg-system-color: #a3b8ff;
--msg-bubble-bg: #161b2e;
--msg-bubble-color: #e9edf6;
--msg-bubble-border: rgba(255, 255, 255, 0.1);
--kv-border: rgba(255, 255, 255, 0.07);
--btn-ghost-border: rgba(255, 255, 255, 0.14);
--btn-ghost-hover-bg: rgba(255, 255, 255, 0.07);
--save-btn-border: #d99a2b;
--loading-border: rgba(255, 255, 255, 0.12);
--loading-top: #ff7fac;
--grad-a: rgba(var(--accent-rgb), 0.14);
--grad-b: rgba(136, 192, 208, 0.12);
--grad-c: rgba(var(--accent-strong-rgb), 0.1);
--c-sakura: #ff7fac;
--c-cyan: #2dd4bf;
--c-violet: #a78bfa;
--c-emerald: #34d399;
--c-amber: #fbbf24;
--c-blue: #60a5fa;
}
[data-theme="light"] {
--glass-bg: rgba(255, 255, 255, 0.66);
--glass-bg-strong: rgba(255, 255, 255, 0.88);
--glass-border: rgba(var(--accent-rgb), 0.22);
--glass-hover: rgba(var(--accent-rgb), 0.07);
--shadow-sm: 0 1px 3px rgba(153, 27, 75, 0.08);
--shadow-md: 0 6px 20px rgba(153, 27, 75, 0.1);
--shadow-lg: 0 16px 40px rgba(153, 27, 75, 0.14);
--shadow-glow:
0 0 0 1px rgba(var(--accent-strong-rgb), 0.3), 0 6px 22px rgba(var(--accent-strong-rgb), 0.14);
--bg-primary: #ffffff;
--bg-secondary: rgba(255, 255, 255, 0.78);
--bg-card: rgba(255, 255, 255, 0.72);
--bg-input: rgba(255, 224, 233, 0.55);
--bg-hover: rgba(var(--accent-rgb), 0.08);
--text-primary: #3b2030;
--text-secondary: #7a5c6b;
--text-muted: #a48a96;
--border-color: rgba(201, 36, 98, 0.14);
--accent: #c92462;
--accent-bg: #ffe4e9;
--toast-bg: rgba(23, 169, 100, 0.14);
--toast-color: #128a52;
--toast-error-bg: rgba(219, 54, 148, 0.12);
--toast-error-color: #c2185b;
--pre-color: #5c4060;
--pre-bg: #fff0f5;
--chat-bg: #fff0f5;
--msg-user-bg: #ffe4e9;
--msg-user-color: #c2185b;
--msg-assistant-bg: #e3f2f6;
--msg-assistant-color: #2f7188;
--msg-system-bg: #e6ecfe;
--msg-system-color: #3f6ef5;
--msg-bubble-bg: #ffffff;
--msg-bubble-color: #262a33;
--msg-bubble-border: rgba(201, 36, 98, 0.14);
--kv-border: rgba(201, 36, 98, 0.1);
--btn-ghost-border: rgba(201, 36, 98, 0.22);
--btn-ghost-hover-bg: #ffe4e9;
--save-btn-border: #d99a2b;
--loading-border: rgba(201, 36, 98, 0.18);
--loading-top: #c92462;
--grad-a: rgba(var(--accent-rgb), 0.16);
--grad-b: rgba(136, 192, 208, 0.12);
--grad-c: rgba(var(--accent-strong-rgb), 0.12);
}
[data-color="sakura"][data-theme="light"] {
--accent-rgb: 201, 36, 98;
--accent-strong-rgb: 153, 27, 75;
--bg-primary: #ffffff;
--bg-input: rgba(255, 224, 233, 0.55);
--bg-hover: rgba(var(--accent-rgb), 0.08);
--pre-bg: #fff0f5;
--chat-bg: #fff0f5;
--msg-bubble-border: rgba(201, 36, 98, 0.14);
--btn-ghost-hover-bg: #ffe4e9;
--grad-a: rgba(var(--accent-rgb), 0.14);
--grad-c: rgba(var(--accent-strong-rgb), 0.09);
}
[data-color="cyan"] {
--accent-rgb: 45, 212, 191;
--accent-strong-rgb: 13, 148, 136;
--accent: #2dd4bf;
--accent-bg: rgba(45, 212, 191, 0.14);
--msg-user-bg: rgba(45, 212, 191, 0.16);
--msg-user-color: #99f6e4;
--loading-top: #2dd4bf;
--grad-a: rgba(45, 212, 191, 0.14);
--grad-c: rgba(45, 212, 191, 0.1);
}
[data-color="cyan"][data-theme="light"] {
--accent-rgb: 15, 118, 110;
--accent-strong-rgb: 15, 118, 110;
--accent: #0f766e;
--accent-bg: #ccfbf1;
--msg-user-bg: #ccfbf1;
--msg-user-color: #0f766e;
--loading-top: #0f766e;
--bg-primary: #f0fdfa;
--bg-input: rgba(204, 251, 241, 0.6);
--bg-hover: rgba(13, 148, 136, 0.08);
--pre-bg: #e7f9f3;
--chat-bg: #e7f9f3;
--msg-bubble-border: rgba(13, 148, 136, 0.14);
--btn-ghost-hover-bg: #ccfbf1;
--grad-a: rgba(13, 148, 136, 0.14);
--grad-c: rgba(13, 148, 136, 0.09);
}
[data-color="violet"] {
--accent-rgb: 167, 139, 250;
--accent-strong-rgb: 124, 58, 237;
--accent: #a78bfa;
--accent-bg: rgba(167, 139, 250, 0.14);
--msg-user-bg: rgba(167, 139, 250, 0.16);
--msg-user-color: #ddd6fe;
--loading-top: #a78bfa;
--grad-a: rgba(167, 139, 250, 0.14);
--grad-c: rgba(167, 139, 250, 0.1);
}
[data-color="violet"][data-theme="light"] {
--accent-rgb: 124, 58, 237;
--accent-strong-rgb: 109, 40, 217;
--accent: #7c3aed;
--accent-bg: #ede9fe;
--msg-user-bg: #ede9fe;
--msg-user-color: #7c3aed;
--loading-top: #7c3aed;
--bg-primary: #f8f6ff;
--bg-input: rgba(237, 233, 254, 0.6);
--bg-hover: rgba(124, 58, 237, 0.08);
--pre-bg: #f3f0ff;
--chat-bg: #f3f0ff;
--msg-bubble-border: rgba(124, 58, 237, 0.14);
--btn-ghost-hover-bg: #ede9fe;
--grad-a: rgba(124, 58, 237, 0.13);
--grad-c: rgba(124, 58, 237, 0.08);
}
[data-color="emerald"] {
--accent-rgb: 52, 211, 153;
--accent-strong-rgb: 5, 150, 105;
--accent: #34d399;
--accent-bg: rgba(52, 211, 153, 0.14);
--msg-user-bg: rgba(52, 211, 153, 0.16);
--msg-user-color: #a7f3d0;
--loading-top: #34d399;
--grad-a: rgba(52, 211, 153, 0.14);
--grad-c: rgba(52, 211, 153, 0.1);
}
[data-color="emerald"][data-theme="light"] {
--accent-rgb: 5, 150, 105;
--accent-strong-rgb: 4, 120, 87;
--accent: #059669;
--accent-bg: #d1fae5;
--msg-user-bg: #d1fae5;
--msg-user-color: #059669;
--loading-top: #059669;
--bg-primary: #f0fdf6;
--bg-input: rgba(209, 250, 229, 0.6);
--bg-hover: rgba(5, 150, 105, 0.08);
--pre-bg: #e9f9f0;
--chat-bg: #e9f9f0;
--msg-bubble-border: rgba(5, 150, 105, 0.14);
--btn-ghost-hover-bg: #d1fae5;
--grad-a: rgba(5, 150, 105, 0.14);
--grad-c: rgba(5, 150, 105, 0.09);
}
[data-color="amber"] {
--accent-rgb: 251, 191, 36;
--accent-strong-rgb: 217, 119, 6;
--accent: #fbbf24;
--accent-bg: rgba(251, 191, 36, 0.14);
--msg-user-bg: rgba(251, 191, 36, 0.16);
--msg-user-color: #fde68a;
--loading-top: #fbbf24;
--grad-a: rgba(251, 191, 36, 0.14);
--grad-c: rgba(251, 191, 36, 0.1);
}
[data-color="amber"][data-theme="light"] {
--accent-rgb: 217, 119, 6;
--accent-strong-rgb: 180, 83, 9;
--accent: #d97706;
--accent-bg: #fef3c7;
--msg-user-bg: #fef3c7;
--msg-user-color: #b45309;
--loading-top: #d97706;
--bg-primary: #fffdf4;
--bg-input: rgba(254, 243, 199, 0.6);
--bg-hover: rgba(217, 119, 6, 0.08);
--pre-bg: #fdf6e1;
--chat-bg: #fdf6e1;
--msg-bubble-border: rgba(217, 119, 6, 0.14);
--btn-ghost-hover-bg: #fef3c7;
--grad-a: rgba(217, 119, 6, 0.13);
--grad-c: rgba(217, 119, 6, 0.08);
}
[data-color="blue"] {
--accent-rgb: 96, 165, 250;
--accent-strong-rgb: 37, 99, 235;
--accent: #60a5fa;
--accent-bg: rgba(96, 165, 250, 0.14);
--msg-user-bg: rgba(96, 165, 250, 0.16);
--msg-user-color: #bfdbfe;
--loading-top: #60a5fa;
--grad-a: rgba(96, 165, 250, 0.14);
--grad-c: rgba(96, 165, 250, 0.1);
}
[data-color="blue"][data-theme="light"] {
--accent-rgb: 37, 99, 235;
--accent-strong-rgb: 29, 78, 216;
--accent: #2563eb;
--accent-bg: #dbeafe;
--msg-user-bg: #dbeafe;
--msg-user-color: #2563eb;
--loading-top: #2563eb;
--bg-primary: #f7faff;
--bg-input: rgba(219, 234, 254, 0.6);
--bg-hover: rgba(37, 99, 235, 0.08);
--pre-bg: #eef4ff;
--chat-bg: #eef4ff;
--msg-bubble-border: rgba(37, 99, 235, 0.14);
--btn-ghost-hover-bg: #dbeafe;
--grad-a: rgba(37, 99, 235, 0.13);
--grad-c: rgba(37, 99, 235, 0.08);
}
[data-color="mono"] {
--accent-rgb: 205, 210, 220;
--accent-strong-rgb: 150, 156, 168;
--accent: #cdd2dc;
--accent-bg: rgba(205, 210, 220, 0.12);
--sakura-300: #b9bfcc;
--sakura-400: #cdd2dc;
--sakura-500: #e6e9ef;
--frost-300: #a8aeba;
--frost-400: #8b919d;
--msg-user-bg: rgba(205, 210, 220, 0.12);
--msg-user-color: #e6e9ef;
--msg-assistant-bg: rgba(205, 210, 220, 0.09);
--msg-assistant-color: #c9ced9;
--loading-top: #cdd2dc;
--grad-a: rgba(205, 210, 220, 0.12);
--grad-b: rgba(205, 210, 220, 0.09);
--grad-c: rgba(205, 210, 220, 0.07);
--c-sakura: #cdd2dc;
--shadow-glow:
0 0 0 1px rgba(205, 210, 220, 0.2), 0 4px 20px rgba(0, 0, 0, 0.3);
}
[data-color="mono"][data-theme="light"] {
--accent-rgb: 60, 64, 72;
--accent-strong-rgb: 30, 32, 38;
--accent: #2f333b;
--accent-bg: #ececf0;
--sakura-300: #6b7280;
--sakura-400: #3f434c;
--sakura-500: #23262c;
--frost-300: #7c828e;
--frost-400: #5f6570;
--text-primary: #1f2126;
--text-secondary: #5b5f66;
--text-muted: #8b8f97;
--border-color: rgba(0, 0, 0, 0.1);
--kv-border: rgba(0, 0, 0, 0.08);
--btn-ghost-border: rgba(0, 0, 0, 0.16);
--glass-border: rgba(0, 0, 0, 0.1);
--glass-hover: rgba(0, 0, 0, 0.04);
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.06);
--shadow-md: 0 6px 20px rgba(0, 0, 0, 0.08);
--shadow-lg: 0 16px 40px rgba(0, 0, 0, 0.1);
--shadow-glow:
0 0 0 1px rgba(0, 0, 0, 0.1), 0 4px 20px rgba(0, 0, 0, 0.08);
--msg-user-bg: #ececf0;
--msg-user-color: #23262c;
--msg-assistant-bg: #f0f0f2;
--msg-assistant-color: #3f434c;
--loading-top: #2f333b;
--bg-primary: #ffffff;
--bg-input: rgba(0, 0, 0, 0.045);
--bg-hover: rgba(0, 0, 0, 0.05);
--pre-bg: #f5f5f7;
--chat-bg: #f5f5f7;
--msg-bubble-border: rgba(0, 0, 0, 0.1);
--btn-ghost-hover-bg: #ececf0;
--grad-a: rgba(0, 0, 0, 0.05);
--grad-b: rgba(0, 0, 0, 0.04);
--grad-c: rgba(0, 0, 0, 0.03);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: var(--font-sans);
}
body {
background:
radial-gradient(
900px 700px at 85% -10%,
var(--grad-a),
transparent 60%
),
radial-gradient(
800px 600px at -10% 20%,
var(--grad-b),
transparent 60%
),
radial-gradient(
700px 500px at 50% 110%,
var(--grad-c),
transparent 60%
),
var(--bg-primary);
background-attachment: fixed;
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
font-size: 14px;
text-rendering: optimizeLegibility;
user-select: none;
-webkit-user-select: none;
transition:
background 0.3s,
color 0.2s;
}
.selectable,
.msg .text,
.msg .tc-args,
.msg .tc-result,
.msg .reasoning-body,
pre,
input,
textarea,
.term-buf {
user-select: text;
-webkit-user-select: text;
}
nav {
background: var(--bg-secondary);
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
width: 16rem;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 100;
display: flex;
flex-direction: column;
border-right: 1px solid var(--glass-border);
padding: 16px 12px;
overflow-y: auto;
overflow-x: hidden;
transition:
transform var(--dur-normal) var(--ease-out),
background 0.2s,
border 0.2s;
}
nav h1 {
font-size: 16.5px;
font-weight: 700;
/* 品牌字用 sakura→frost 渐变:比纯色更像「产品」,也不跟导航选中态抢颜色 */
background: linear-gradient(100deg, var(--sakura-300), var(--frost-300));
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin: 2px 10px 18px;
white-space: nowrap;
letter-spacing: -0.015em;
display: flex;
align-items: center;
gap: 8px;
}
.nav-links {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
nav a {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
color: var(--text-secondary);
text-decoration: none;
font-size: 13.5px;
font-weight: 500;
cursor: pointer;
border-radius: var(--radius-md);
/* 选中条用 inset 阴影而不是 border-left:border 会把整行文字右推 3px,
hover 时图标跟着跳一下 —— 这种「一像素级不稳」正是廉价感的来源。 */
box-shadow: inset 2px 0 0 transparent;
transition:
background var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out),
box-shadow var(--dur-micro) var(--ease-out);
}
.nav-ico {
width: 17px;
height: 17px;
flex-shrink: 0;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0.8;
}
nav a:hover {
color: var(--text-primary);
background: var(--glass-hover);
}
nav a.active {
color: var(--accent);
background: var(--accent-bg);
box-shadow: inset 2px 0 0 var(--accent);
font-weight: 600;
}
nav a.active .nav-ico {
opacity: 1;
}
.nav-footer {
border-top: 1px solid var(--glass-border);
padding: 12px 4px 4px;
display: flex;
justify-content: center;
}
.main-wrap {
margin-left: 16rem;
min-height: 100vh;
display: flex;
flex-direction: column;
transition: margin-left var(--dur-normal) var(--ease-out);
}
body.sidebar-collapsed nav {
transform: translateX(-100%);
}
body.sidebar-collapsed .main-wrap {
margin-left: 0;
}
.topbar {
position: sticky;
top: 0;
z-index: 90;
height: 56px;
display: flex;
align-items: center;
gap: 12px;
padding: 0 24px;
background: var(--bg-secondary);
backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.4);
border-bottom: 1px solid var(--glass-border);
}
.sidebar-toggle {
background: transparent;
border: 1px solid var(--glass-border);
color: var(--text-secondary);
cursor: pointer;
padding: 4px 10px;
border-radius: var(--radius-sm);
font-size: 13px;
line-height: 1.2;
transition:
all var(--dur-micro) var(--ease-out),
transform var(--dur-micro) var(--ease-out);
}
.sidebar-toggle:hover {
color: var(--accent);
border-color: rgba(var(--accent-rgb), 0.5);
transform: scale(1.05);
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
font-size: 12.5px;
color: var(--text-muted);
animation: breadcrumbIn var(--dur-normal) var(--ease-out);
}
/* 当前页在顶栏里当标题用:字号/字重/颜色都要跟「面包屑碎片」拉开,
否则整条顶栏就是一行灰字,页面没有入口感。 */
.breadcrumb .crumb-current {
font-size: 15px;
font-weight: 650;
color: var(--text-primary);
letter-spacing: -0.01em;
}
.breadcrumb .crumb-home,
.breadcrumb .crumb-sep {
font-size: 12.5px;
opacity: 0.7;
}
@keyframes breadcrumbIn {
from {
opacity: 0;
transform: translateX(-6px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.crumb-home {
color: var(--accent);
font-weight: 600;
}
.crumb-sep {
color: var(--text-muted);
opacity: 0.6;
}
.crumb-current {
color: var(--text-primary);
font-weight: 500;
}
.theme-btn {
background: var(--glass-bg-strong);
border: 1px solid var(--glass-border);
backdrop-filter: blur(8px);
color: var(--text-secondary);
cursor: pointer;
padding: 4px 10px;
border-radius: var(--radius-pill);
font-size: 14px;
line-height: 1;
margin-right: 8px;
transition:
all var(--dur-micro) var(--ease-out),
transform var(--dur-micro) var(--ease-out);
}
.theme-btn:hover {
color: var(--accent);
border-color: rgba(var(--accent-rgb), 0.5);
box-shadow: var(--shadow-glow);
transform: scale(1.06);
}
#bg-layer {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image: var(--bg-img, none);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
transform: scale(1.04);
filter: blur(calc(var(--bg-blur, 0) * 1px));
transition: filter 0.25s var(--ease-out);
}
.palette-pop {
position: fixed;
bottom: 66px;
left: 16px;
width: 216px;
max-width: calc(100vw - 24px);
max-height: calc(100vh - 130px);
overflow-y: auto;
padding: 12px;
background: var(--glass-bg-strong);
backdrop-filter: blur(12px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-lg);
display: none;
z-index: 300;
}
.palette-pop.on {
display: block;
animation: viewIn 0.16s var(--ease-out);
}
.palette-pop h4 {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary);
margin: 2px 0 6px;
}
.palette-pop button.cdot {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.18);
cursor: pointer;
margin: 3px;
padding: 0;
vertical-align: middle;
transition:
transform 0.15s,
box-shadow 0.15s;
}
.palette-pop button.cdot:hover {
transform: scale(1.25);
}
.palette-pop button.cdot.on {
border-color: var(--text-primary);
box-shadow: var(--shadow-glow);
transform: scale(1.15);
}
.palette-pop input[type="text"] {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 5px 8px;
color: var(--text-primary);
font-size: 11px;
width: 100%;
margin-bottom: 6px;
outline: none;
}
.palette-pop .pp-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 5px;
margin-top: 4px;
}
.palette-pop .pp-row .btn {
padding: 3px 9px;
font-size: 11px;
white-space: nowrap;
}
.palette-pop .pp-val {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: var(--text-muted);
min-width: 0;
text-align: right;
margin-left: auto;
}
.palette-pop input[type="range"] {
width: 72px;
margin: 0;
accent-color: var(--accent);
vertical-align: middle;
}
.container {
/* 1240px 是「卡片网格 + 图表」都能读的最宽值;再宽就变成一片空旷的留白。
居中而不是靠左铺满 —— 超宽屏上摊满整个屏幕是「后台模板」的典型观感。 */
padding: 22px 24px 56px;
max-width: 1240px;
margin: 0 auto;
width: 100%;
}
.card {
position: relative;
background: var(--bg-card);
backdrop-filter: blur(var(--glass-blur)) saturate(1.3);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.3);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 22px;
margin-bottom: 18px;
box-shadow: var(--shadow-sm);
transition:
background 0.2s,
border 0.2s,
box-shadow var(--dur-normal) var(--ease-out);
}
/* hover 只提亮阴影与描边,**不再 translateY**:
整页十几张卡一起「上下浮」,视线扫过时像在抖,是廉价感最明显的一处。 */
.card:hover {
box-shadow: var(--shadow-md);
border-color: rgba(var(--accent-rgb), 0.22);
}
.card.tilt {
transform-style: preserve-3d;
transition:
box-shadow var(--dur-normal) var(--ease-out),
transform 0.15s ease-out;
}
.card.tilt .tilt-glow {
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease-out;
background: radial-gradient(
240px circle at var(--mx, 50%) var(--my, 50%),
rgba(var(--accent-rgb), 0.12),
transparent 70%
);
mix-blend-mode: normal;
}
.card.tilt:hover .tilt-glow {
opacity: 1;
}
@media (hover: none), (prefers-reduced-motion: reduce) {
.card.tilt {
transform: none !important;
}
.card.tilt .tilt-glow {
display: none;
}
}
.card h2 {
/* 标题左侧加一道 accent 短竖:卡片之间就有了同一套「章节起点」信号,
不必靠加粗/变大去抢,长页面翻起来也不迷路。 */
display: flex;
align-items: center;
gap: 9px;
font-size: 15.5px;
font-weight: 650;
margin-bottom: 14px;
color: var(--text-primary);
letter-spacing: -0.012em;
}
.card h2::before {
content: "";
width: 3px;
height: 14px;
border-radius: 2px;
background: linear-gradient(
180deg,
var(--sakura-300),
var(--frost-300)
);
flex-shrink: 0;
}
.card h3 {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
margin: 16px 0 8px;
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.grid-3 {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
.grid-4 {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.stat-value {
font-size: 26px;
font-weight: 700;
color: var(--accent);
letter-spacing: -0.02em;
background: linear-gradient(
120deg,
var(--sakura-400),
var(--frost-300)
);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.stat-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
letter-spacing: 0.02em;
}
.stat-card {
padding: 16px 20px;
}
.status-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
box-shadow: 0 0 6px currentColor;
}
.dot-green {
background: var(--success);
color: var(--success);
}
.dot-yellow {
background: var(--warning);
color: var(--warning);
}
.dot-red {
background: var(--error);
color: var(--error);
}
.dot-gray {
background: var(--text-muted);
color: var(--text-muted);
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
text-align: left;
padding: 8px 10px;
color: var(--text-muted);
font-weight: 500;
border-bottom: 1px solid var(--border-color);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
td {
padding: 8px 10px;
border-bottom: 1px solid var(--kv-border);
}
tr:hover td {
background: var(--bg-hover);
}
.badge {
display: inline-block;
padding: 2px 10px;
border-radius: var(--radius-pill);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.02em;
}
.badge-green {
background: rgba(23, 169, 100, 0.16);
color: var(--success);
}
.badge-red {
background: rgba(219, 54, 148, 0.16);
color: var(--error);
}
.badge-yellow {
background: rgba(217, 154, 43, 0.16);
color: var(--warning);
}
.badge-blue {
background: var(--accent-bg);
color: var(--accent);
}
.badge-gray {
background: var(--glass-hover);
color: var(--text-secondary);
}
.btn {
/* 药丸形换成与卡片同一套圆角:满屏药丸按钮是「玩具感」的主要来源之一 */
padding: 7px 14px;
border-radius: var(--radius-md);
border: none;
font-size: 12.5px;
cursor: pointer;
font-weight: 600;
letter-spacing: 0.005em;
line-height: 1.35;
transition:
background var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out),
border-color var(--dur-micro) var(--ease-out),
box-shadow var(--dur-micro) var(--ease-out),
transform var(--dur-micro) var(--ease-out);
}
.btn:active {
transform: scale(0.96);
}
.btn-primary {
background: linear-gradient(
120deg,
var(--sakura-400),
var(--sakura-500)
);
color: #fff;
box-shadow: 0 2px 12px rgba(var(--accent-rgb), 0.35);
}
.btn-primary:hover {
filter: brightness(1.08);
box-shadow: 0 4px 18px rgba(var(--accent-rgb), 0.45);
}
.btn-danger {
background: var(--error);
color: #fff;
}
.btn-danger:hover {
filter: brightness(1.1);
}
.btn-sm {
padding: 4px 10px;
font-size: 11.5px;
}
.btn-ghost {
background: transparent;
border: 1px solid var(--btn-ghost-border);
color: var(--text-secondary);
}
.btn-ghost:hover {
background: var(--btn-ghost-hover-bg);
color: var(--text-primary);
border-color: rgba(var(--accent-rgb), 0.4);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
animation: tabIn var(--dur-normal) var(--ease-out);
}
@keyframes tabIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
input,
textarea,
select {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 9px 12px;
color: var(--text-primary);
font-size: 13px;
width: 100%;
margin-bottom: 10px;
outline: none;
transition:
border 0.15s,
background 0.2s,
color 0.2s,
box-shadow 0.15s;
}
input:focus,
textarea:focus,
select:focus {
border-color: var(--accent);
box-shadow: var(--shadow-glow);
background: var(--bg-input);
}
textarea {
resize: vertical;
min-height: 80px;
font-family: var(--font-mono);
font-size: 12px;
}
label {
display: block;
font-size: 11px;
color: var(--text-secondary);
margin-bottom: 3px;
font-weight: 500;
}
pre {
background: var(--pre-bg);
border-radius: var(--radius-sm);
padding: 12px;
font-size: 12px;
overflow-x: auto;
color: var(--pre-color);
font-family: var(--font-mono);
max-height: 400px;
overflow-y: auto;
}
code {
font-family: var(--font-mono);
font-size: 12px;
color: var(--pre-color);
}
.settings-layout {
display: flex;
flex-direction: column;
gap: 16px;
}
.settings-tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
background: var(--bg-card);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 6px;
box-shadow: var(--shadow-sm);
}
.settings-tabs span {
display: inline-block;
padding: 7px 16px;
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
text-decoration: none;
border-radius: var(--radius-pill);
border: 1px solid transparent;
transition: all 0.15s;
}
.settings-tabs span:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
.settings-tabs span.active {
background: var(--accent-bg);
color: var(--accent);
border-color: rgba(var(--accent-rgb), 0.35);
}
.settings-content {
flex: 1;
min-width: 0;
}
.settings-content > .card {
margin-bottom: 16px;
}
.settings-key {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-muted);
margin-bottom: 2px;
}
.reasoning {
border-left: 2px solid var(--frost-400);
padding-left: 12px;
margin: 8px 0;
font-size: 12px;
color: var(--text-muted);
}
.reasoning-title {
cursor: pointer;
font-size: 11px;
color: var(--text-muted);
font-weight: 600;
user-select: none;
margin-bottom: 4px;
}
.reasoning-body {
color: var(--text-muted);
line-height: 1.5;
}
.reasoning-body p {
margin: 4px 0;
}
.msg-content .text h1,
.msg-content .text h2,
.msg-content .text h3 {
font-size: 1em;
margin: 8px 0 4px;
color: var(--text-primary);
}
.msg-content .text p {
margin: 4px 0;
line-height: 1.5;
}
.msg-content .text ul,
.msg-content .text ol {
padding-left: 20px;
margin: 4px 0;
}
.msg-content .text li {
margin: 2px 0;
}
.msg-content .text code {
background: var(--pre-bg);
padding: 1px 4px;
border-radius: 3px;
font-size: 11px;
}
.msg-content .text pre {
background: var(--pre-bg);
border-radius: 6px;
padding: 10px;
margin: 8px 0;
overflow-x: auto;
font-size: 11px;
max-height: 300px;
}
.msg-content .text pre code {
background: none;
padding: 0;
}
.msg-content .text blockquote {
border-left: 3px solid var(--border-color);
padding-left: 10px;
margin: 8px 0;
color: var(--text-secondary);
}
.msg-content .text table {
border-collapse: collapse;
margin: 8px 0;
font-size: 12px;
width: 100%;
}
.msg-content .text th,
.msg-content .text td {
border: 1px solid var(--border-color);
padding: 4px 8px;
text-align: left;
}
.msg-content .text img {
max-width: 100%;
border-radius: 6px;
}
.toast {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--toast-bg);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
color: var(--toast-color);
padding: 10px 22px;
border-radius: var(--radius-md);
font-size: 13px;
display: none;
z-index: 100;
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-md);
animation: toastIn 0.3s var(--ease-out);
}
@keyframes toastIn {
from {
opacity: 0;
transform: translateY(12px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.toast.error {
background: var(--toast-error-bg);
color: var(--toast-error-color);
}
.toast.success {
background: var(--toast-bg);
color: var(--toast-color);
border-color: rgba(23, 169, 100, 0.35);
}
.toast.warn {
background: rgba(217, 154, 43, 0.16);
color: var(--warning);
border-color: rgba(217, 154, 43, 0.35);
}
.confirm-overlay {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: rgba(8, 10, 20, 0.55);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
z-index: 200;
animation: fadeIn 0.2s ease;
}
.confirm-box {
background: var(--glass-bg-strong);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 24px;
max-width: 380px;
width: 90%;
box-shadow: var(--shadow-lg);
animation: tabIn 0.25s var(--ease-out);
}
.confirm-box h3 {
font-size: 15px;
font-weight: 600;
margin-bottom: 10px;
color: var(--text-primary);
}
.confirm-box p {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.55;
margin-bottom: 18px;
word-break: break-all;
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
/* 首启人格向导(复用 confirm-* 弹窗)*/
.persona-ta {
width: 100%;
display: none;
margin-bottom: 12px;
font: 12px/1.5 var(--font-mono, monospace);
padding: 8px;
border-radius: var(--radius-md, 8px);
border: 1px solid var(--glass-border);
background: var(--glass-bg);
color: var(--text-primary);
box-sizing: border-box;
}
.persona-warn {
display: none;
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
margin-bottom: 10px;
}
.persona-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.empty-state {
text-align: center;
padding: 48px 24px;
color: var(--text-muted);
background: var(--glass-bg);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px dashed var(--glass-border);
border-radius: var(--radius-lg);
margin: 8px 0;
}
.empty-state p {
font-size: 14px;
margin-bottom: 8px;
color: var(--text-secondary);
}
.empty-state .icon {
font-size: 40px;
margin-bottom: 12px;
opacity: 0.6;
filter: drop-shadow(0 4px 10px rgba(var(--accent-rgb), 0.3));
}
.chat-layout {
display: flex;
flex-direction: column;
gap: 16px;
height: calc(100vh - 112px);
min-height: 60vh;
}
.chat-tabs {
/* 分段控件(segmented control)而不是一排药丸 chip:
四个页签是同一组互斥选项,「一个容器 + 一个高亮块」比「四个各自带边框的胶囊」
更准确地表达了这层关系,也少四道描边噪声。 */
display: inline-flex;
gap: 3px;
padding: 3px;
flex-wrap: wrap;
align-self: flex-start;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
}
.chat-tabs span {
padding: 6px 14px;
font-size: 12.5px;
font-weight: 500;
cursor: pointer;
color: var(--text-muted);
border-radius: 7px;
border: none;
transition:
background var(--dur-micro) var(--ease-out),
color var(--dur-micro) var(--ease-out);
}
.chat-tabs span:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.chat-tabs span.active {
color: var(--accent);
background: var(--accent-bg);
font-weight: 600;
box-shadow: inset 0 0 0 1px rgba(var(--accent-rgb), 0.28);
}
.chat-panel {
display: none;
flex: 1;
min-height: 0;
flex-direction: column;
}
.chat-panel.active {
display: flex;
}
.chat-main {
flex: 1;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
}
.chat-main .card {
flex: 1;
display: flex;
flex-direction: column;
margin-bottom: 0;
min-height: 0;
background: transparent;
border: none;
box-shadow: none;
backdrop-filter: none;
padding: 0;
}
.chat-main .card h2 {
flex-shrink: 0;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px 20px 28px;
margin-bottom: 0;
display: flex;
flex-direction: column;
/* 消息间距从 6px 拉到 14px:原来每条贴在一起,一屏十几条像一堵墙,
分不清哪句是哪句。 */
gap: 14px;
min-height: 0;
}
.msg {
display: flex;
gap: 10px;
margin-bottom: 0;
align-items: flex-start;
max-width: 100%;
}
.msg-user {
flex-direction: row-reverse;
align-self: flex-end;
}
.msg-assistant {
align-self: flex-start;
}
.msg-system {
align-self: center;
max-width: 90%;
}
.msg-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
flex-shrink: 0;
/* 细描边把头像从同色背景里“拓”出来,否则暗色下就是一团 */
box-shadow: 0 0 0 1px var(--glass-border);
}
.msg-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.msg-assistant .msg-avatar img {
transform: scale(1.5);
}
.msg-avatar svg {
display: block;
width: 16px;
height: 16px;
}
.msg-user .msg-avatar {
background: var(--msg-user-bg);
color: var(--msg-user-color);
}
.msg-assistant .msg-avatar {
background: transparent;
}
.msg-system .msg-avatar {
background: var(--msg-system-bg);
color: var(--msg-system-color);
}
.msg-content {
min-width: 0;
flex: 1 1 auto;
/* 行长上限:整屏宽的一行文字没人读得下去(~78ch 是舒适区),
也让「同一侧连续多条」自然堆成一列而不是横铺开。 */
max-width: min(78ch, 86%);
}
.msg-content .msg-bubble + .msg-bubble {
margin-top: 6px;
}
.msg-channel {
align-self: flex-start;
}
.msg-channel .msg-avatar.chan-avatar {
color: #ffffff;
font-weight: 700;
font-size: 13px;
text-transform: uppercase;
}
.msg-channel .msg-chan-name {
font-size: 10px;
color: var(--text-muted);
opacity: 0.8;
margin-bottom: 2px;
padding-left: 4px;
letter-spacing: 0.5px;
}
.msg-channel .msg-bubble {
background: var(--msg-bubble-bg);
color: var(--msg-bubble-color);
border-bottom-left-radius: 4px;
border-color: var(--msg-bubble-border);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
}
.msg-bubble {
padding: 10px 14px;
border-radius: 12px;
font-size: 14.5px;
line-height: 1.65;
word-break: break-word;
position: relative;
border: 1px solid transparent;
transition: box-shadow var(--dur-normal) var(--ease-out);
}
.msg-user .msg-bubble {
/* 用户与助手必须**看得出不一样**:原来两侧同底色、只差一个圆角,
整段对话读下来分不清谁说的。用户侧用 accent 渲染,助手侧保持中性面。 */
background: var(--msg-user-bg);
color: var(--text-primary);
border-bottom-right-radius: 5px;
border-color: rgba(var(--accent-rgb), 0.26);
}
.msg-assistant .msg-bubble {
background: var(--msg-bubble-bg);
color: var(--msg-bubble-color);
border-bottom-left-radius: 5px;
border-color: var(--msg-bubble-border);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
}
.msg-system .msg-bubble {
background: var(--msg-system-bg);
color: var(--msg-system-color);
text-align: center;
font-size: 12px;
border-color: var(--msg-bubble-border);
}
.msg-bubble .text {
white-space: pre-wrap;
}
.msg-bubble .text p {
margin: 4px 0;
}
.msg-bubble .text h1,
.msg-bubble .text h2,
.msg-bubble .text h3 {
font-size: 1em;
margin: 8px 0 4px;
}
.msg-bubble .text ul,
.msg-bubble .text ol {
padding-left: 20px;
margin: 4px 0;
}
.msg-bubble .text pre {
background: var(--pre-bg);
color: var(--pre-color);
border-radius: 6px;
padding: 8px;
margin: 4px 0;
overflow-x: auto;
font-size: 11.5px;
line-height: 1.5;
border: 1px solid var(--kv-border);
max-height: 200px;
}
.msg-bubble .text code {
background: var(--pre-bg);
color: var(--pre-color);
padding: 1px 4px;
border-radius: 3px;
font-size: 11.5px;
}
.msg-bubble .text pre code {
background: none;
padding: 0;
}
.msg-bubble .text blockquote {
border-left: 3px solid var(--border-color);
padding-left: 8px;
margin: 4px 0;
opacity: 0.8;
}
.msg-bubble .text table {
border-collapse: collapse;
margin: 4px 0;
font-size: 12px;
width: 100%;
}
.msg-bubble .text th,
.msg-bubble .text td {
border: 1px solid var(--border-color);
padding: 3px 6px;
text-align: left;
}
.msg-bubble .text img {
max-width: 100%;
border-radius: 6px;
}
/* ===== 思考卡片(PiDeck 风格)===== */
.reasoning-card {
margin: 6px 0;
border: 1px dashed var(--kv-border);
border-radius: var(--radius-sm);
background: var(--bg-panel, var(--bg-hover));
overflow: hidden;
}
.reasoning-card .reasoning-head {
display: flex;
align-items: center;
gap: 7px;
padding: 6px 10px;
font-size: 11px;
cursor: pointer;
user-select: none;
color: var(--text-secondary);
}
.reasoning-card .reasoning-head:hover {
background: var(--bg-hover);
}
.reasoning-card .rc-ico {
color: var(--accent);
flex-shrink: 0;
}
.reasoning-card .rc-title {
font-weight: 600;
color: var(--text-primary);
}
.reasoning-card .rc-chev {
margin-left: auto;
display: inline-flex;
align-items: center;
color: var(--text-muted);
transition: transform 0.2s var(--ease-out);
}
.reasoning-card.open .rc-chev {
transform: rotate(180deg);
}
.reasoning-card .reasoning-body {
padding: 4px 10px 8px;
}
.reasoning-card .reasoning-content {
line-height: 1.5;
color: var(--text-tertiary);
font-size: 11.5px;
}
.reasoning-card .reasoning-content p {
margin: 4px 0;
}
.reasoning-card .reasoning-sweep {
height: 2px;
border-radius: 2px;
margin-top: 6px;
background: linear-gradient(
90deg,
transparent,
var(--accent),
transparent
);
background-size: 200% 100%;
animation: reasoningSweep 1.2s var(--ease-out) infinite;
opacity: 0.6;
}
@keyframes reasoningSweep {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
/* ===== 工具卡片(PiDeck 风格,扁平不套气泡)===== */
.tool-card {
background: var(--bg-panel, var(--bg-hover));
color: var(--text-secondary);
border-radius: var(--radius-sm);
padding: 7px 10px;
margin: 6px 0;
font-size: 11px;
border: 1px solid var(--kv-border);
border-left: 3px solid var(--accent);
cursor: pointer;
transition:
border-color 0.2s var(--ease-out),
background 0.2s var(--ease-out);
}
.tool-card.tc-running {
border-left-color: var(--accent);
}
.tool-card.tc-done {
border-left-color: rgba(23, 169, 100, 0.8);
}
.tool-card.tc-error {
border-left-color: var(--error, #db3694);
}
.tool-card .tc-line {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
user-select: none;
}
.tool-card .tc-ico {
display: inline-flex;
flex-shrink: 0;
color: var(--accent);
}
.tool-card.tc-error .tc-ico {
color: var(--error, #db3694);
}
.tool-card.tc-done .tc-ico {
color: rgba(23, 169, 100, 0.9);
}
.tool-card .tc-name {
font-weight: 600;
color: var(--text-primary);
}
.tool-card .tc-plugin {
font-size: 9.5px;
color: var(--text-muted);
background: var(--pre-bg);
border-radius: 4px;
padding: 0 4px;
line-height: 1.6;
flex-shrink: 0;
}
.tool-card .tc-state {
margin-left: auto;
font-size: 9.5px;
font-weight: 500;
padding: 1px 6px;
border-radius: 999px;
flex-shrink: 0;
display: inline-flex;
align-items: center;
gap: 4px;
}
.tool-card .tc-run {
background: rgba(63, 110, 245, 0.18);
color: #a3b8ff;
}
.tool-card .tc-done {
background: rgba(23, 169, 100, 0.18);
color: #6ee7a8;
}
.tool-card .tc-deny {
background: rgba(219, 54, 148, 0.18);
color: #ff9ec6;
}
.tool-card .tc-spinner {
width: 11px;
height: 11px;
border-radius: 50%;
border: 2px solid rgba(63, 110, 245, 0.25);
border-top-color: #3f6ef5;
animation: spin 0.7s linear infinite;
display: inline-block;
}
.tool-card .tc-caret {
display: inline-flex;
align-items: center;
color: var(--text-muted);
transition: transform 0.15s var(--ease-out);
}
.tool-card.open .tc-caret {
transform: rotate(180deg);
}
.tool-card .tc-detail {
margin-top: 6px;
}
.tool-card .tc-detail-label {
font-size: 9.5px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.03em;
margin-bottom: 2px;
}
.tool-card .tc-args,
.tool-card .tc-result {
font-family: var(--font-mono);
font-size: 11px;
opacity: 0.85;
white-space: pre-wrap;
word-break: break-all;
margin-top: 5px;
background: var(--pre-bg);
color: var(--pre-color);
border-radius: 4px;
padding: 5px 7px;
}
.tool-card .tc-result {
opacity: 0.72;
max-height: 80px;
overflow-y: auto;
}
[data-theme="light"] .tool-card .tc-run {
color: #3f6ef5;
}
[data-theme="light"] .tool-card .tc-done {
color: #128a52;
}
[data-theme="light"] .tool-card .tc-deny {
color: #c2185b;
}
.tool-card.tool-drip-in {
animation: toolDripIn 0.32s var(--ease-out) both;
}
@keyframes toolDripIn {
0% {
opacity: 0;
transform: translateY(-18px) scale(0.96);
}
70% {
opacity: 1;
transform: translateY(2px) scale(1.005);
}
100% {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.msg-bubble .thinking-tools {
display: flex;
gap: 6px;
align-items: center;
flex-wrap: wrap;
}
.msg-bubble .thinking-tool {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: var(--text-secondary);
background: var(--pre-bg);
border-radius: 999px;
padding: 3px 10px;
}
.msg-bubble .thinking-tool.pill-in {
animation: pillIn 0.25s var(--ease-out) both;
}
@keyframes pillIn {
from {
opacity: 0;
transform: translateX(8px);
}
to {
opacity: 1;
transform: none;
}
}
.msg-bubble .live-spinner {
width: 15px;
height: 15px;
border-radius: 50%;
border: 2px solid var(--msg-user-bg);
border-top-color: var(--accent);
animation: spin 0.7s linear infinite;
flex-shrink: 0;
display: inline-block;
vertical-align: middle;
}
.msg-bubble .live-spinner + .thinking-tools,
.msg-bubble .live-spinner + .text {
margin-left: 12px;
}
.msg-bubble.grow-in {
animation: bubbleGrow 0.5s var(--ease-out) both;
}
.msg-bubble.grow-in .text {
animation: textFadeIn 0.35s ease 0.125s both;
}
@keyframes bubbleGrow {
from {
max-height: 28px;
opacity: 0.4;
}
to {
max-height: 3000px;
opacity: 1;
}
}
@keyframes textFadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.tool-call.tool-drip-in {
animation: toolDripIn 0.3s var(--ease-out) both;
}
@keyframes toolDripIn {
from {
opacity: 0;
transform: translateY(-14px) scale(0.98);
}
to {
opacity: 1;
transform: none;
}
}
/* 实时终端画面:模拟 agent 操控的终端屏幕 */
.term-screen {
background: #0f1115;
border: 1px solid #2a2e35;
border-radius: 8px;
margin: 6px 0 4px 0;
overflow: hidden;
font-size: 11px;
line-height: 1.45;
}
.term-screen .term-head {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: #1a1d23;
color: #9aa0a8;
font-size: 10px;
border-bottom: 1px solid #2a2e35;
font-family: var(--font-mono);
}
.term-screen .term-head .term-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #3fb950;
flex-shrink: 0;
}
.term-screen .term-head .term-dot.stopped {
background: #d29922;
}
.term-screen pre.term-buf {
margin: 0;
padding: 6px 8px;
background: transparent;
color: #d8dee9;
font-family: var(--font-mono);
font-size: 11px;
white-space: pre-wrap;
word-break: break-all;
max-height: 260px;
overflow-y: auto;
scrollbar-width: thin;
}
.term-screen pre.term-buf::-webkit-scrollbar {
width: 6px;
}
.term-screen pre.term-buf::-webkit-scrollbar-thumb {
background: #33373d;
border-radius: 3px;
}
.chat-input-row {
display: flex;
gap: 8px;
flex-shrink: 0;
padding-top: 14px;
margin-top: 4px;
/* 输入区与消息流之间加一道分隔:否则最后一条消息与输入框糊在一起,
看不出“消息到头了”。 */
border-top: 1px solid var(--border-color);
}
.chat-input-row input {
flex: 1;
margin-bottom: 0;
}
.chat-input-row button {
flex-shrink: 0;
margin-bottom: 0;
}
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid var(--loading-border);
border-radius: 50%;
border-top-color: var(--loading-top);
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.monaco-like {
font-family: var(--font-mono);
font-size: 12px;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 6px;
}
.kv-row {
display: grid;
grid-template-columns: minmax(110px, 180px) 1fr;
gap: 12px;
align-items: baseline;
padding: 8px;
/* 负外边距让 hover 高亮能满宽,而不把内容挤窄 */
margin: 0 -8px;
border-bottom: 1px solid var(--kv-border);
font-size: 13px;
border-radius: var(--radius-sm);
transition: background var(--dur-micro) var(--ease-out);
}
.kv-row:hover {
background: var(--glass-hover);
}
.kv-row:last-child {
border-bottom: none;
}
.kv-row .key {
color: var(--text-muted);
/* 键列不再硬编码 180px 宽度:窄屏/长键名时能自己收,避免把值挤成竖排 */
width: auto;
flex-shrink: 0;
font-size: 12.5px;
}
.kv-row .val {
color: var(--text-primary);
word-break: break-all;
/* 数字等宽:端口/计数/内存这类值上下对齐才好扫 */
font-variant-numeric: tabular-nums;
min-width: 0;
}
.tool-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 10px;
background: var(--accent-bg);
color: var(--accent);
margin: 1px;
}
.check-pass {
color: var(--success);
}
.check-fail {
color: var(--error);
}
.check-skip {
color: var(--text-secondary);
}
.memory-graph {
width: 100%;
height: 300px;
background: var(--bg-input);
border-radius: var(--radius-md);
border: 1px solid var(--border-color);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 13px;
}
.health-panel {
display: grid;
gap: 8px;
}
.health-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--bg-input);
border-radius: var(--radius-sm);
font-size: 13px;
border: 1px solid transparent;
transition:
border var(--dur-micro),
background var(--dur-micro);
}
.health-item:hover {
border-color: rgba(var(--accent-rgb), 0.25);
}
.health-item .check-name {
flex: 1;
}
.health-item .check-status {
font-size: 11px;
font-weight: 500;
}
.fade-in {
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
#sm-container-chat {
height: 260px;
background: var(--bg-input);
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
overflow: hidden;
position: relative;
}
#sm-container-chat canvas {
display: block;
}
.toggle-row {
margin-top: 8px;
display: flex;
align-items: center;
gap: 12px;
}
.toggle-row .label-text {
color: var(--text-muted);
font-size: 12px;
}
.toggle-switch {
position: relative;
width: 36px;
height: 20px;
cursor: pointer;
flex-shrink: 0;
}
.toggle-track {
position: absolute;
inset: 0;
background: var(--bg-input);
border-radius: 10px;
transition: all 0.3s;
border: 1px solid var(--border-color);
}
.toggle-track.on {
background: var(--accent-bg);
border-color: var(--accent);
box-shadow: var(--shadow-glow);
}
.toggle-knob {
position: absolute;
width: 16px;
height: 16px;
left: 2px;
top: 2px;
background: var(--text-muted);
border-radius: 50%;
transition: all 0.3s;
}
.toggle-knob.on {
left: 18px;
background: var(--accent);
}
.toggle-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-color);
background: transparent;
color: var(--text-muted);
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
}
.toggle-btn:hover {
background: var(--accent-bg);
color: var(--text-primary);
}
.toggle-btn.on {
background: var(--accent-bg);
color: var(--accent);
border-color: var(--accent);
}
.label-text {
color: var(--text-muted);
font-size: 12px;
}
.loading-spinner {
width: 32px;
height: 32px;
border: 3px solid var(--loading-border);
border-top: 3px solid var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.sidebar-subnav {
display: flex;
gap: 0;
border-bottom: 1px solid var(--border-color);
margin-bottom: 10px;
}
.sidebar-subnav span {
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
color: var(--text-muted);
border-bottom: 2px solid transparent;
transition: all 0.15s;
}
.sidebar-subnav span:hover {
color: var(--text-primary);
}
.sidebar-subnav span.active {
color: var(--accent);
border-bottom-color: var(--accent);
}
@media (max-width: 900px) {
.chat-layout {
height: auto;
}
.msg {
max-width: 100%;
}
}
@media (max-width: 768px) {
nav {
width: 14rem;
box-shadow: var(--shadow-lg);
}
.main-wrap {
margin-left: 0;
}
nav h1 {
font-size: 15px;
}
.topbar {
padding: 0 12px;
}
.breadcrumb .crumb-home {
display: none;
}
.container {
padding: 12px;
}
.card {
padding: 12px;
}
.grid-2,
.grid-3,
.grid-4 {
grid-template-columns: 1fr;
}
.stat-value {
font-size: 20px;
}
.settings-layout {
flex-direction: column;
}
.settings-tabs {
max-height: 200px;
overflow-y: auto;
}
.kv-row {
flex-direction: column;
gap: 2px;
}
.kv-row .key {
width: auto;
}
.msg-avatar {
width: 24px;
height: 24px;
font-size: 11px;
}
.health-item {
flex-wrap: wrap;
gap: 4px;
}
.health-item .check-name {
flex: auto;
width: 100%;
}
}
/* ===== 8.7 Accessibility ===== */
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--radius-sm);
}
::selection {
background: rgba(var(--accent-rgb), 0.35);
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* ===== Themed scrollbar ===== */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(var(--accent-rgb), 0.25);
border-radius: var(--radius-pill);
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(var(--accent-rgb), 0.45);
background-clip: content-box;
border: 2px solid transparent;
}
* {
scrollbar-width: thin;
scrollbar-color: rgba(var(--accent-rgb), 0.35) transparent;
}
/* ===== 运行态面板(图形化总览)=====
目标是"一眼看懂内核现在在干什么":排队/中断/中断栈/驻留子四个数字,
四级中断队列的条形,中断栈的层叠图,以及通道拓扑。全部用纯 CSS + 内联 SVG,
不引入任何依赖(前端刻意没有构建链)。 */
.rt-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
gap: 10px;
margin: 4px 0 14px;
}
.rt-tile {
position: relative;
padding: 12px 14px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
overflow: hidden;
}
.rt-tile .rt-num {
font-size: 28px;
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.5px;
}
.rt-tile .rt-label {
font-size: 11px;
color: var(--text-muted);
margin-top: 2px;
}
.rt-tile .rt-sub {
font-size: 10px;
color: var(--text-muted);
opacity: 0.85;
}
.rt-tile.rt-active {
border-color: var(--sakura-400);
box-shadow: var(--shadow-glow);
}
.rt-tile.rt-warn .rt-num {
color: #ffb86b;
}
/* 底部细条:把"占了多少"用长度表达,比纯数字更快读懂 */
.rt-bar {
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
margin-top: 8px;
overflow: hidden;
}
.rt-bar > i {
display: block;
height: 100%;
background: linear-gradient(90deg, var(--sakura-400), var(--frost-300));
transition: width 0.35s var(--ease-out);
}
/* ===== 队列:五个等大表框 =====
此前是五行「名字 | 进度条 | 元数据」的扁条:四级中断全为 0 时,
四行几乎全是空白,既占高度又难看。改成五个等大的框(与 KPI、阶段管道
同一套视觉语言),深度用大字报出来,下面是可见的格槽。 */
.rt-queues {
display: grid;
/* 100px 而不是 150px:两块都要保证「5 个一行」。
否则容器 540px(小窗口侧栏展开时就是这个宽度)会排成「4 个 + 1 个」——
第二行四个空位,看着就是一块坑。宁可框窄一点,也不留孤行。 */
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
margin-bottom: 8px;
}
.rt-qcell {
display: flex;
flex-direction: column;
gap: 7px;
padding: 11px 12px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
transition:
border-color 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.rt-qhead {
display: flex;
align-items: baseline;
gap: 7px;
min-width: 0;
}
.rt-qhead > b {
font-size: 16px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
}
.rt-qhead > span {
font-size: 10.5px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rt-qnum {
font-size: 26px;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
}
.rt-qmeta {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 4px 8px;
font-size: 10.5px;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* 级别配色:框头与格槽点亮共用同一套色 */
.rt-qcell.rt-lv-4 .rt-qhead > b { color: #ff5c7a; }
.rt-qcell.rt-lv-3 .rt-qhead > b { color: #ffa657; }
.rt-qcell.rt-lv-2 .rt-qhead > b { color: var(--sakura-400); }
.rt-qcell.rt-lv-1 .rt-qhead > b { color: var(--frost-300); }
.rt-qcell.rt-lv-q .rt-qhead > b { color: #a3be8c; }
/* 有积压时整个框描边点亮:一眼看出哪条队列在堵 */
.rt-qcell.rt-lv-4.rt-active { border-color: rgba(255, 92, 122, 0.55); box-shadow: 0 0 0 1px rgba(255, 92, 122, 0.18); }
.rt-qcell.rt-lv-3.rt-active { border-color: rgba(255, 166, 87, 0.55); box-shadow: 0 0 0 1px rgba(255, 166, 87, 0.18); }
.rt-qcell.rt-lv-2.rt-active { border-color: rgba(var(--accent-rgb), 0.55); box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.18); }
.rt-qcell.rt-lv-1.rt-active { border-color: rgba(136, 192, 208, 0.55); box-shadow: 0 0 0 1px rgba(136, 192, 208, 0.18); }
.rt-qcell.rt-lv-q.rt-active { border-color: rgba(163, 190, 140, 0.55); box-shadow: 0 0 0 1px rgba(163, 190, 140, 0.18); }
/* 排队队列无级别:用虚线框与四级中断区分开 */
.rt-qcell.rt-qcell-queued {
border-style: dashed;
}
/* 队列格槽:固定可见的「车位」,占用多少一眼可数。
此前是宽度百分比进度条 —— 队列为 0 时宽度为 0,整行只剩文字,
看上去就是「这块空着」。 */
.rt-slots {
display: flex;
align-items: stretch;
gap: 3px;
height: 16px;
min-width: 0;
}
.rt-slots > i {
flex: 1 1 0;
min-width: 3px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.05);
transition:
background 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out);
}
.rt-slots.rt-lv-4 > i.on { background: #ff5c7a; box-shadow: 0 0 6px rgba(255, 92, 122, 0.45); }
.rt-slots.rt-lv-3 > i.on { background: #ffa657; box-shadow: 0 0 6px rgba(255, 166, 87, 0.4); }
.rt-slots.rt-lv-2 > i.on { background: var(--sakura-400); box-shadow: 0 0 6px rgba(var(--accent-rgb), 0.4); }
.rt-slots.rt-lv-1 > i.on { background: var(--frost-300); box-shadow: 0 0 6px rgba(136, 192, 208, 0.4); }
.rt-slots.rt-lv-q > i.on { background: #a3be8c; box-shadow: 0 0 6px rgba(163, 190, 140, 0.4); }
.rt-slots-more {
flex: 0 0 auto;
align-self: center;
margin-left: 4px;
font-size: 10px;
font-weight: 600;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* 拓扑图:整张图为 SVG,文字降到最少,其余信息用图形编码
(归属=容器/配色、容量=细条、输出能力=圆点、路由=连线)。 */
.rt-svg-wrap {
overflow-x: auto;
padding: 4px 0 8px;
}
.rt-svg {
min-width: 560px;
font-family: inherit;
display: block;
}
.rt-svg text {
fill: var(--text-primary);
}
/* 拓扑图的文字全部走类(不用 font-size/fill 属性):
1) 字号统一可读(原来 9–11px 且低对比度,实机很难看清);
2) 颜色交给主题变量,切主题/配色时不用重算 SVG。
长名截断由 JS 的 rtClip 负责(SVG 没有 text-overflow)。 */
.rt-topo-svg .tp-name { font-size: 12px; fill: var(--text-primary); }
.rt-topo-svg .tp-sub { font-size: 10.5px; fill: var(--text-muted); font-variant-numeric: tabular-nums; }
.rt-topo-svg .tp-val { font-size: 12.5px; font-weight: 700; fill: var(--text-primary); }
.rt-topo-svg .tp-lab { font-size: 11px; font-weight: 600; }
.rt-topo-svg .tp-warn { font-size: 10.5px; fill: #ffb86b; }
.rt-topo-svg .tp-hint { font-size: 10.5px; fill: var(--text-muted); }
.rt-topo-svg .tp-edge { fill: none; }
.rt-topo-svg .tp-edge-in { stroke-opacity: 0.45; stroke-width: 1.4; }
.rt-topo-svg .tp-edge-out { stroke-opacity: 0.35; stroke-width: 1.2; }
/* 排队队列无级别:虚线框已在 .rt-qcell.rt-qcell-queued 处理 */
/* 中断栈:栈顶在上,做出"层叠现场"的观感 */
.rt-stack {
display: flex;
flex-direction: column-reverse;
gap: 4px;
margin: 2px 0 6px;
}
.rt-frame {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 9px;
border-radius: 8px;
font-size: 11px;
background: rgba(255, 255, 255, 0.05);
border-left: 3px solid var(--sakura-400);
}
.rt-frame .rt-frame-top {
margin-left: auto;
font-size: 10px;
color: var(--text-muted);
}
.rt-empty {
font-size: 11px;
color: var(--text-muted);
padding: 4px 0;
}
/* 通道拓扑:输入 → 内核 → 输出 */
.rt-topo {
display: grid;
grid-template-columns: 1fr auto 1fr;
gap: 10px;
align-items: center;
margin-top: 6px;
}
.rt-topo-col {
display: flex;
flex-direction: column;
gap: 6px;
}
.rt-topo-col.rt-right {
text-align: right;
}
.rt-chan {
padding: 6px 9px;
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
font-size: 11px;
cursor: default;
}
.rt-chan .rt-chan-name {
font-weight: 600;
}
.rt-chan .rt-chan-sub {
font-size: 10px;
color: var(--text-muted);
}
.rt-chan .rt-caps {
display: inline-flex;
gap: 3px;
margin-left: 6px;
vertical-align: middle;
}
.rt-cap {
font-size: 9px;
padding: 1px 5px;
border-radius: 999px;
background: rgba(136, 192, 208, 0.16);
color: var(--frost-300);
}
/* —— 通道分配(按归属):容器 + 容量滑块 ——
为什么用容器而不是一排文字:归属是“分组”语义,只有把同一 owner 的
inputch 画进同一个盒子,才能一眼看出“这条输入被划给了哪个驻留子”。 */
.rt-owner {
border: 1px solid var(--border-color);
border-left: 3px solid var(--frost-300);
border-radius: 8px;
padding: 7px 9px;
margin-bottom: 7px;
background: rgba(255, 255, 255, 0.03);
}
.rt-owner.rt-owner-child {
border-left-color: #b48ead;
background: rgba(180, 142, 173, 0.07);
}
.rt-owner-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 4px;
}
.rt-owner-name {
font-size: 11px;
font-weight: 600;
}
.rt-owner-meta {
font-size: 10px;
color: var(--text-muted);
}
.rt-assign {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 0;
border-top: 1px dashed rgba(255, 255, 255, 0.06);
font-size: 11px;
}
.rt-assign .rt-chan-name {
font-weight: 600;
min-width: 84px;
}
.rt-assign .rt-chan-sub {
font-size: 10px;
color: var(--text-muted);
margin-left: auto;
}
.rt-badge-warn {
font-size: 9px;
padding: 1px 6px;
border-radius: 999px;
background: rgba(191, 97, 106, 0.22);
color: #d08770;
}
/* 容量滑块:轨道 + 填充 + 把手 + 读数。 */
.rt-slider {
display: inline-flex;
align-items: center;
gap: 5px;
flex: 0 0 auto;
}
.rt-slider-label {
font-size: 9px;
color: var(--text-muted);
}
.rt-slider-track {
position: relative;
display: inline-block;
width: 64px;
height: 6px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.10);
overflow: visible;
}
.rt-slider-track > i {
position: absolute;
left: 0;
top: 0;
bottom: 0;
border-radius: 999px;
background: var(--frost-300);
}
.rt-slider-track > b {
position: absolute;
top: 50%;
width: 9px;
height: 9px;
margin-left: -4.5px;
margin-top: -4.5px;
border-radius: 50%;
background: #fff;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
}
.rt-slider-val {
font-size: 10px;
min-width: 34px;
color: var(--text-muted);
}
/* 行内迷你条(登记 / 抢占并排) */
.rt-mini {
display: inline-flex;
align-items: center;
gap: 3px;
margin-left: 6px;
font-size: 10px;
}
.rt-mini-track {
display: inline-block;
width: 30px;
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.10);
overflow: hidden;
vertical-align: middle;
}
.rt-mini-track > i {
display: block;
height: 100%;
border-radius: 999px;
background: var(--frost-300);
}
.rt-core {
width: 74px;
height: 74px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 600;
text-align: center;
line-height: 1.2;
background: radial-gradient(circle at 50% 40%, rgba(var(--accent-strong-rgb), 0.35), rgba(24, 24, 38, 0.9));
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glow);
}
.rt-section-title {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.3px;
margin: 14px 0 8px;
text-transform: uppercase;
}
/* ===== 阶段管道:五个等大表框 =====
与中断队列、KPI 同一套视觉语言(等大框 + 大号加粗标题)。
此前是「小圆点 + 一条连接线 + 9px 小字」,在总览里几乎读不出
「现在走到哪一步」,尺寸也远小于旁边的 KPI 框。
现在事件直接落在它所属的框里,不需要靠颜色或图例去猜。 */
.rt-pipe-row {
display: grid;
/* 与 .rt-queues 同一套列宽规则:两块的框宽、换行点保持一致 */
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 10px;
margin: 2px 0 8px;
transition: opacity 0.3s var(--ease-out);
}
.rt-pipe-idle {
opacity: 0.55;
}
.rt-pipe-cell {
display: flex;
flex-direction: column;
gap: 8px;
padding: 11px 12px 10px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
transition:
border-color 0.25s var(--ease-out),
box-shadow 0.25s var(--ease-out),
background 0.25s var(--ease-out);
}
.rt-pipe-cell.active {
border-color: var(--accent);
background: rgba(var(--accent-rgb), 0.08);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.22);
}
.rt-pipe-head {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
}
.rt-pipe-head > b {
font-size: 13.5px;
font-weight: 700;
letter-spacing: 0.01em;
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rt-pipe-head .rt-ico {
width: 15px;
height: 15px;
color: var(--text-secondary);
}
.rt-pipe-cell.active .rt-pipe-head > b,
.rt-pipe-cell.active .rt-pipe-head .rt-ico {
color: var(--accent);
}
.rt-pipe-events {
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 23px;
}
/* 「工具」格的滚动视口:固定一行高,只露最新一条。
工具调用一轮内可能几十次,全部追加会把这一格撑成长条,反而看不出
「现在在调什么」;这里让旧条向上滚出视口,留下最新那条。 */
.rt-pipe-scroll {
flex-wrap: nowrap;
height: 23px;
min-height: 23px;
overflow: hidden;
align-items: center;
}
/* 名称可缩、计数不可缩:窄框里先截断工具名,累计次数始终看得见。 */
.rt-pipe-scroll .rt-chip {
flex: 0 1 auto;
min-width: 0;
max-width: none;
}
/* rtFlashLatestTool 每来一条新调用就重加一次,重置动画时间轴。 */
.rt-chip-enter {
animation: rt-chip-scroll-in 0.3s var(--ease-out);
}
@keyframes rt-chip-scroll-in {
from {
transform: translateY(115%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.rt-scroll-count {
flex: 0 0 auto;
margin-left: auto;
font-style: normal;
font-size: 10.5px;
opacity: 0.6;
font-variant-numeric: tabular-nums;
}
/* 拓扑光点:沿 animateMotion 给的路径跑;不吃鼠标事件,跑完由 JS 摘掉。 */
.rt-spark {
pointer-events: none;
filter: drop-shadow(0 0 4px rgba(255, 255, 255, 0.8));
}
/* ===== 设置页「外观」区 ===== */
.ap-dots {
display: inline-flex;
gap: 8px;
flex-wrap: wrap;
align-items: center;
}
.ap-dot {
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid var(--border-color);
cursor: pointer;
padding: 0;
transition: transform 0.12s var(--ease-out);
}
.ap-dot:hover {
transform: scale(1.12);
}
.ap-dot.on {
border-color: var(--text-primary);
box-shadow: 0 0 0 2px var(--accent-bg);
}
/* ===== 总览 KPI(图标 + 数字,静态骨架)=====
节点只在首帧建一次,刷新只改值与类名 —— 这是总览页不再闪的前提。 */
.ov-kpis {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(118px, 1fr));
gap: 10px;
}
.ov-kpi {
display: flex;
flex-direction: column;
gap: 5px;
padding: 12px 14px 11px;
border-radius: var(--radius-md);
background: var(--bg-input);
border: 1px solid var(--border-color);
min-width: 0;
}
.ov-ico svg {
width: 18px;
height: 18px;
fill: none;
stroke: var(--accent);
stroke-width: 1.7;
stroke-linecap: round;
stroke-linejoin: round;
display: block;
}
.ov-val {
display: flex;
align-items: center;
gap: 6px;
font-size: 18px;
font-weight: 700;
line-height: 1.15;
min-width: 0;
}
.ov-val b {
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 副行:内核名 + commit。总览只给版本号会分不清是哪个内核、哪次构建。 */
.ov-sub {
font-size: 10px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-height: 12px;
font-variant-numeric: tabular-nums;
}
.ov-lab {
font-size: 10px;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--text-muted);
}
.ov-dot {
flex: 0 0 auto;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--text-muted);
}
.ov-dot.ok {
background: var(--success);
box-shadow: 0 0 8px rgba(23, 169, 100, 0.55);
}
.ov-dot.warn {
background: var(--warning);
}
.ov-dot.bad {
background: var(--error);
}
/* 开源许可卡:协议标识 + 协议全文 + 源码仓库。
此前只是 KPI 卡底部一行小链接(.ov-foot),在长页面里基本看不见。 */
.ov-legal h2 {
margin-bottom: 6px;
}
.ov-legal-note {
margin: 10px 0 0;
font-size: 11px;
line-height: 1.55;
color: var(--text-muted);
}
/* 行内小图标:一律 SVG,不使用 emoji/符号字符。
事件 chip 用它区分普通工具与 output_* 输出通道。 */
.rt-ico {
width: 12px;
height: 12px;
flex: 0 0 auto;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
vertical-align: -1.5px;
}
.rt-ico-sm {
width: 11px;
height: 11px;
}
.rt-loop {
display: inline-flex;
color: var(--accent);
font-weight: 700;
}
.rt-chip {
display: inline-flex;
align-items: center;
gap: 3px;
/* min-width:0 + overflow:hidden:框窄时 chip 跟着缩,而不是把框撞破 */
min-width: 0;
max-width: 170px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: var(--text-secondary);
}
.rt-chip-t {
/* font-weight 继承,不因为换成 <b> 而变粗;省略号靠它生效 */
font-weight: inherit;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rt-chip-tool {
border-color: rgba(136, 192, 208, 0.42);
color: var(--frost-300);
}
.rt-chip-output {
border-color: var(--accent);
color: var(--accent);
}
.rt-chip-stage {
opacity: 0.7;
}
.rt-chip-none {
opacity: 0.35;
border-style: dashed;
}
.rt-chip-n {
font-style: normal;
opacity: 0.75;
font-variant-numeric: tabular-nums;
}
/* ===== 服务入口列表(反代插件服务的「点一下直接打开」) ===== */
/* 与 run-* 系列同样的格槽风格:左状态点、中名称、右动作,避免行内文字挤在一起 */
.svc-list{display:flex;flex-direction:column;gap:6px}
.svc-row{display:flex;align-items:center;gap:10px;padding:9px 12px;border-radius:10px;background:var(--bg-input,rgba(13,13,22,.5));border:1px solid var(--border-color,rgba(255,255,255,.09));font-size:13px;transition:border-color .15s,background .15s}
.svc-row:hover{border-color:var(--sakura-400,#ff7fac)}
.svc-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}
.svc-name{font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:180px}
.svc-sub{color:var(--text-muted,#6e7284);font-size:12px;white-space:nowrap}
.svc-tag{font-size:10px;padding:2px 6px;border-radius:6px;background:var(--frost-300,#88c0d0);color:#0d0d16;font-weight:700;letter-spacing:.04em}
.svc-tag-warn{background:var(--danger,#d1383d);color:#fff}
.svc-path{color:var(--text-muted,#6e7284);font-family:ui-monospace,Menlo,Consolas,monospace;font-size:11px;margin-left:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:240px}
.svc-err{color:var(--danger,#d1383d);font-size:11px;margin-left:auto;max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}