gcli2api / internal /panel /index.html
a3216's picture
chore: 同步到上游 1.12.0-panel + 凭证同步/独立启动器/保活
6d60378 verified
Raw History Blame Contribute Delete
74 kB
<!DOCTYPE html>
<html lang="zh-CN" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WorkBuddy2API · 控制台</title>
<style>
/* ── 设计令牌:明暗两套独立色板 ─────────────────────────────────────────
冷石墨基调(仪表盘语言,非营销页):底色分层 + 单一靛蓝强调(仅交互),
语义色只给状态。等宽数字承载所有观测值,tabular-nums 对齐。 */
:root {
--bg: #0c0e14; --surface: #14171f; --surface-2: #1a1e28; --raise: #202531;
--line: #262c3a; --line-soft: #1e232e;
--ink: #e8ebf2; --ink-2: #a8b0c2; --ink-3: #6b7488;
--accent: #5b7cfa; --accent-ink: #ffffff; --accent-soft: rgba(91,124,250,.13);
--ok: #3ddc97; --ok-soft: rgba(61,220,151,.12);
--warn: #f5b544; --warn-soft: rgba(245,181,68,.12);
--bad: #f0655f; --bad-soft: rgba(240,101,95,.12);
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px -12px rgba(0,0,0,.5);
--mono: ui-monospace, "Cascadia Mono", "SF Mono", Consolas, monospace;
--sans: system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
[data-theme="light"] {
--bg: #f4f5f8; --surface: #ffffff; --surface-2: #f8f9fb; --raise: #ffffff;
--line: #e2e5ec; --line-soft: #eceef3;
--ink: #14171f; --ink-2: #545c6e; --ink-3: #8b93a5;
--accent: #3d5fe0; --accent-ink: #ffffff; --accent-soft: rgba(61,95,224,.09);
--ok: #0f9d63; --ok-soft: rgba(15,157,99,.1);
--warn: #b47611; --warn-soft: rgba(180,118,17,.11);
--bad: #cf3b34; --bad-soft: rgba(207,59,52,.09);
--shadow: 0 1px 2px rgba(16,20,32,.06), 0 8px 24px -14px rgba(16,20,32,.14);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
background: var(--bg); color: var(--ink); font: 14px/1.55 var(--sans);
-webkit-font-smoothing: antialiased;
}
::selection { background: var(--accent-soft); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
@media (prefers-reduced-motion: reduce) { * { transition: none !important; animation: none !important; } }
/* ── 壳层:左侧导航 + 主区 ─────────────────────────────────────────── */
.shell { display: grid; grid-template-columns: 208px 1fr; min-height: 100vh; }
.nav {
background: var(--surface); border-right: 1px solid var(--line);
display: flex; flex-direction: column; position: sticky; top: 0; height: 100vh;
}
.brand { padding: 20px 18px 16px; border-bottom: 1px solid var(--line-soft); }
.brand .name { font-weight: 650; letter-spacing: -.01em; font-size: 15px; }
.brand .sub { color: var(--ink-3); font-size: 11.5px; font-family: var(--mono); margin-top: 3px; }
.nav ul { list-style: none; padding: 10px 8px; flex: 1; }
.nav a {
display: flex; align-items: center; gap: 10px; padding: 8px 11px; margin-bottom: 2px;
color: var(--ink-2); text-decoration: none; border-radius: 7px; font-size: 13.5px;
}
.nav a:hover { background: var(--surface-2); color: var(--ink); }
.nav a.on { background: var(--accent-soft); color: var(--accent); font-weight: 550; }
.nav a svg { width: 15px; height: 15px; flex: none; opacity: .85; }
.nav .sig { padding: 12px 16px; border-top: 1px solid var(--line-soft); font-size: 11.5px; color: var(--ink-3); }
.nav .sig .row { display: flex; align-items: center; gap: 7px; }
/* 日志频道筛选 chips */
.chips { display: inline-flex; gap: 4px; align-items: center; }
.chips .chip { padding: 2px 10px; border-radius: 999px; }
.chips .chip.on { background: var(--accent-soft); color: var(--accent); border-color: transparent; font-weight: 550; }
.ln .lch { font-style: normal; display: inline-block; font-size: 10px; line-height: 1; padding: 2px 6px; border-radius: 4px; margin-right: 8px; vertical-align: 1px; }
.ln .lch.c-task { background: var(--accent-soft); color: var(--accent); }
.ln .lch.c-chat { background: var(--ok-soft); color: var(--ok); }
.ln .lch.c-sys { background: var(--surface-2); color: var(--ink-3); }
/* 任务中心 */
.tc-empty { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 42px 16px; color: var(--ink-3); }
.tc-empty svg { width: 42px; height: 42px; opacity: .55; margin-bottom: 6px; }
.tc-empty .t { font-weight: 550; color: var(--ink-2); }
.tc-empty .d { font-size: 12.5px; max-width: 34em; text-align: center; }
.sel-note { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); white-space: nowrap; flex: none; }
.sel-note select {
appearance: none; -webkit-appearance: none;
background: var(--surface-2) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E") no-repeat right 8px center;
border: 1px solid var(--line); border-radius: 7px; color: var(--ink);
font: 550 12px var(--sans); padding: 3px 24px 3px 9px; cursor: pointer;
}
.qprog { display: flex; align-items: center; gap: 12px; padding: 12px 16px 0; }
.qprog .qbar { flex: 1; height: 4px; border-radius: 3px; background: var(--surface-2); overflow: hidden; }
.qprog .qbar i { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 3px; transition: width .5s ease; }
.qprog .note { font-variant-numeric: tabular-nums; white-space: nowrap; }
.qgroup { padding: 4px 0 10px; }
.qgroup + .qgroup { border-top: 1px solid var(--line-soft); }
.qgroup header {
display: flex; align-items: baseline; gap: 9px; padding: 10px 16px 6px;
background: none; border: none; box-shadow: none;
}
.qgroup header .nm { font-weight: 600; font-size: 13.5px; }
.qgroup header .cnt { font-size: 12px; color: var(--ink-3); font-variant-numeric: tabular-nums; }
.qrow {
display: grid; grid-template-columns: 118px 1fr 52px 84px minmax(120px, 240px);
gap: 10px; align-items: center; padding: 7.5px 16px; font-size: 13px;
}
.qrow:hover { background: var(--surface-2); }
.qrow .code { font: 11.5px/1.35 var(--mono); color: var(--ink-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qrow .name { display: flex; align-items: center; gap: 7px; min-width: 0; }
.qrow .name .t { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qrow .prog { font-variant-numeric: tabular-nums; color: var(--ink-2); text-align: right; }
.qrow .st { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.qrow .msg { font-size: 12px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.qdot { width: 7px; height: 7px; border-radius: 50%; flex: none; }
.qdot.done { background: var(--ok); }
.qdot.run { background: var(--accent); animation: qpulse 1.2s ease-in-out infinite; }
.qdot.wait { background: none; border: 1.5px solid var(--ink-3); }
.qdot.err { background: var(--bad); }
.qdot.skip { background: var(--ink-3); }
@keyframes qpulse { 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .qdot.run { animation: none; } }
.srow { display: grid; grid-template-columns: minmax(150px, 210px) 1fr auto; gap: 14px; align-items: center; padding: 13px 16px; }
.srow + .srow { border-top: 1px solid var(--line-soft); }
.srow .who .nm { font-weight: 600; font-size: 13.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.srow .who .id { font: 10.5px var(--mono); color: var(--ink-3); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.shead {
display: grid; grid-template-columns: minmax(150px, 210px) 1fr auto;
gap: 14px; align-items: center; padding: 9px 16px 7px;
font-size: 11.5px; color: var(--ink-3); border-bottom: 1px solid var(--line-soft);
}
.shead .stasks span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stasks { display: grid; grid-template-columns: repeat(5, minmax(64px, 1fr)); gap: 6px 10px; }
.stask { display: inline-flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--ink-2); }
.stask .mark { font-size: 13px; line-height: 1; width: 14px; text-align: center; flex: none; }
.stask.ok .mark { color: var(--ok); }
.stask.warn .mark { color: var(--warn); } .stask.warn .fr { color: var(--warn); font-variant-numeric: tabular-nums; }
.stask.todo { color: var(--ink-3); }
.srow .luck { display: flex; align-items: center; gap: 5px; font-size: 12.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.srow .luck svg { width: 14px; height: 14px; opacity: .7; }
.srow .err { grid-column: 1 / -1; font-size: 12px; color: var(--bad); margin-top: -4px; }
.pulse { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); flex: none; }
.pulse.bad { background: var(--bad); }
.pulse.warn { background: var(--warn); }
.main { min-width: 0; display: flex; flex-direction: column; }
.topbar {
display: flex; align-items: center; gap: 14px; padding: 14px 24px;
border-bottom: 1px solid var(--line); background: var(--surface); position: sticky; top: 0; z-index: 20;
}
.topbar h2 { font-size: 15.5px; font-weight: 620; letter-spacing: -.01em; }
.topbar .sep { color: var(--ink-3); }
.topbar .meta { color: var(--ink-3); font-size: 12px; font-family: var(--mono); }
.topbar .grow { flex: 1; }
.view { padding: 22px 24px 60px; max-width: 1560px; margin: 0 auto; width: 100%; }
.view[hidden] { display: none; }
/* ── 控件 ─────────────────────────────────────────────────────────── */
button {
font: inherit; color: var(--ink); background: var(--surface-2);
border: 1px solid var(--line); border-radius: 7px; padding: 7px 13px; cursor: pointer;
transition: border-color .12s, background .12s;
}
button:hover:not(:disabled) { border-color: var(--accent); }
button:disabled { opacity: .5; cursor: default; }
button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 550; }
button.primary:hover:not(:disabled) { filter: brightness(1.08); }
button.ghost { background: transparent; }
button.danger { color: var(--bad); }
button.danger:hover:not(:disabled) { border-color: var(--bad); }
button.xs { padding: 3.5px 9px; font-size: 12.5px; border-radius: 6px; white-space: nowrap; }
/* select.xs:header 里的紧凑选择框。通用 input/select 规则是 width:100%(表单页用),
header 里若不收窄会把同行 note 挤到换行——收成内容宽并压小外观,与 button.xs 同款。 */
select.xs { width: auto; padding: 3.5px 9px; font-size: 12.5px; border-radius: 6px; }
button.icon { padding: 7px 9px; }
input, select {
font: inherit; color: var(--ink); background: var(--surface-2);
border: 1px solid var(--line); border-radius: 7px; padding: 7px 11px; width: 100%;
}
input:focus, select:focus { outline: none; border-color: var(--accent); }
input.invalid { border-color: var(--bad); background: var(--bad-soft); }
input.invalid:focus { border-color: var(--bad); }
input[type=checkbox] { width: auto; accent-color: var(--accent); }
label.fld { display: block; margin-bottom: 14px; }
label.fld > .lb { display: block; font-size: 12.5px; color: var(--ink-2); margin-bottom: 5px; }
label.fld > .hint { display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 4px; }
.inline { display: flex; align-items: center; gap: 9px; }
/* ── 统计条(观测区)──────────────────────────────────────────────── */
.stats {
display: grid; grid-template-columns: repeat(6, 1fr);
border: 1px solid var(--line); border-radius: 11px; background: var(--surface);
overflow: hidden; margin-bottom: 20px; box-shadow: var(--shadow);
}
@media (max-width: 980px) { .stats { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 560px) { .stats { grid-template-columns: repeat(2, 1fr); } }
.stat { padding: 14px 17px; border-right: 1px solid var(--line-soft); border-bottom: 1px solid var(--line-soft); }
.stat .v { font: 600 26px/1.15 var(--mono); font-variant-numeric: tabular-nums; letter-spacing: -.02em; white-space: nowrap; }
.stat .k { color: var(--ink-3); font-size: 11.5px; margin-top: 5px; }
.stat.good .v { color: var(--ok); } .stat.warn .v { color: var(--warn); } .stat.bad .v { color: var(--bad); }
/* ── 面板块 ───────────────────────────────────────────────────────── */
.box { background: var(--surface); border: 1px solid var(--line); border-radius: 11px; box-shadow: var(--shadow); margin-bottom: 20px; overflow: hidden; }
.box > header { display: flex; align-items: center; gap: 12px; padding: 12px 17px; border-bottom: 1px solid var(--line-soft); }
.box > header h3 { font-size: 13.5px; font-weight: 600; }
.box > header h3 .hint { font-size: 11.5px; font-weight: 400; color: var(--ink-3); margin-left: 6px; }
.box > header .grow { flex: 1; }
.box > header .note { color: var(--ink-3); font-size: 12px; white-space: nowrap; }
/* 面板块头部的下拉/输入按内容取宽:全局 input/select 是 width:100%(配置表单要整行),
在 header 里会把标题挤成两行、把"近 3 天"这类短选项拉成一整条。 */
.box > header select, .box > header input { width: auto; }
.box > header h3 { white-space: nowrap; }
.box .pad { padding: 17px; }
/* ── 账号表(签名元素:健康色条 + 积分量条)───────────────────────── */
.tbl-wrap { overflow-x: auto; }
.acc { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 640px; }
.acc th {
text-align: left; font-weight: 500; font-size: 11.5px; color: var(--ink-3);
padding: 9px 11px; border-bottom: 1px solid var(--line); white-space: nowrap;
}
.acc td { padding: 11px; border-bottom: 1px solid var(--line-soft); vertical-align: middle; }
.acc tr:last-child td { border-bottom: none; }
.acc tbody tr:hover { background: var(--surface-2); }
/* 健康色条用真实单元格(td.mark)实现,而不是 tr::before 伪元素:
伪元素在表格行内会被浏览器当作匿名单元格插入行首,整行内容右移一列、
与表头错位——窗口越宽列越宽越明显。 */
.acc .mark { width: 3px; min-width: 3px; padding: 0; }
.acc .mark i { display: block; width: 3px; height: 100%; min-height: 36px; background: var(--ok); }
.acc tbody tr.cool .mark i { background: var(--warn); }
.acc tbody tr.off .mark i { background: var(--bad); }
/* 模型表无色条(健康指示只属于账号表)。 */
.acc.md .mark { display: none; }
/* 操作列用 width:1px + nowrap 的经典技巧:列宽恰为内容宽,其余列按内容自适应 */
.acc .c-acts { width: 1px; white-space: nowrap; }
/* 档位标签列:允许换行在标签之间(不拆字),并给足最小宽度 */
.acc .efs { min-width: 150px; }
.acc .efs .tag { margin: 1px 0; }
/* 模型表:紧凑一些,7 列在常见窗口下尽量免横向滚动 */
.acc.md { min-width: 700px; }
.acc.md th, .acc.md td { padding: 10px 9px; }
.who { min-width: 118px; max-width: 200px; }
#taskTable .who { max-width: 360px; }
#taskTable .who .nm, #taskTable .who .id { white-space: normal; word-break: break-all; }
.who .nm { font-weight: 550; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.who .id { font-family: var(--mono); font-size: 11.5px; color: var(--ink-3); margin-top: 1px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.realm-tag { display: inline-block; margin-left: 6px; padding: 1px 5px; border-radius: 4px; font: 500 10px/1 var(--sans); color: var(--accent); background: var(--accent-soft); border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); vertical-align: 1px; }
.realm-row { display: flex; align-items: center; gap: 14px; padding: 4px 0 10px; }
.realm-opt { display: inline-flex; align-items: center; gap: 5px; font-size: 12.5px; color: var(--ink-2); cursor: pointer; }
.realm-opt input { accent-color: var(--accent); margin: 0; }
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
.usage-cell { min-width: 268px; }
.usage-line {
display: inline-flex; align-items: center; gap: 4px; max-width: 100%;
font: 600 11.5px/1.25 var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap;
}
.usage-item {
display: inline-flex; align-items: baseline; padding: 3px 6px; border: 1px solid transparent;
border-radius: 5px; background: var(--surface-2);
}
.usage-item b { font-weight: 650; }
.usage-item em { margin-left: 2px; font: 500 10px/1 var(--sans); font-style: normal; }
.usage-count { color: var(--accent); background: var(--accent-soft); border-color: color-mix(in srgb, var(--accent) 18%, transparent); }
.usage-total {
color: var(--accent); background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 24%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 6%, transparent);
}
.usage-total b { font-size: 12px; letter-spacing: -.02em; }
.usage-total em { color: var(--ink-2); }
.usage-latency { color: var(--warn); background: var(--warn-soft); border-color: color-mix(in srgb, var(--warn) 18%, transparent); }
.usage-rate { color: var(--ok); background: var(--ok-soft); border-color: color-mix(in srgb, var(--ok) 18%, transparent); }
.cred { min-width: 84px; }
.cred .n { font-family: var(--mono); font-variant-numeric: tabular-nums; font-size: 13px; white-space: nowrap; }
.cred .n .of { color: var(--ink-3); font-size: 11px; }
.bar { height: 3px; background: var(--line); border-radius: 2px; margin-top: 5px; overflow: hidden; max-width: 88px; }
.bar i { display: block; height: 100%; background: var(--accent); border-radius: 2px; }
.tag {
display: inline-flex; align-items: center; gap: 5px; padding: 1px 9px;
border-radius: 999px; font-size: 11.5px; font-weight: 500; white-space: nowrap;
}
.tag.ok { background: var(--ok-soft); color: var(--ok); }
.tag.warn { background: var(--warn-soft); color: var(--warn); }
.tag.bad { background: var(--bad-soft); color: var(--bad); }
.tag.mute { background: var(--surface-2); color: var(--ink-3); border: 1px solid var(--line); }
.rate-limits { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; min-width: 210px; }
.rate-limit {
display: flex; align-items: baseline; gap: 6px; padding: 4px 7px;
border: 1px solid color-mix(in srgb, var(--warn) 24%, var(--line));
border-radius: 6px; background: var(--warn-soft); font-size: 11.5px; line-height: 1.35;
}
.rate-limit.model-unavailable { border-color: var(--line); background: var(--surface-2); color: var(--ink-3); }
.rate-limit b { color: var(--ink); font-family: var(--mono); font-weight: 600; }
.rate-limit span { color: var(--ink-2); }
.acts { display: flex; gap: 5px; justify-content: flex-end; white-space: nowrap; }
.acts button { padding: 3.5px 8px; font-size: 12.5px; white-space: nowrap; flex: none; }
.empty { padding: 40px 20px; text-align: center; color: var(--ink-3); }
.empty .big { font-size: 14px; color: var(--ink-2); margin-bottom: 6px; }
/* ── 日志(终端质感)────────────────────────────────────────────── */
#logBox {
max-height: 420px; overflow: auto; margin: 0; padding: 15px 17px;
font: 12px/1.75 var(--mono); white-space: pre-wrap; word-break: break-all; color: var(--ink-2);
}
#logBox .ln { display: block; }
#logBox .ln.e { color: var(--bad); }
#logBox .ln.w { color: var(--warn); }
#view-logs .box > header { flex-wrap: wrap; }
#reqSummary, #reqNote { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#reqSummary { flex: 1 1 320px; }
#reqNote { flex: 0 1 260px; text-align: right; }
/* ── 配置表单 ─────────────────────────────────────────────────────── */
.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 0 20px; }
.grid3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0 16px; }
@media (max-width: 860px) { .grid2, .grid3 { grid-template-columns: 1fr; } }
fieldset { border: none; margin-bottom: 6px; }
legend { font-size: 12px; color: var(--ink-3); font-weight: 600; padding-bottom: 10px; }
.hr { height: 1px; background: var(--line-soft); margin: 4px 0 16px; }
.switch { display: flex; align-items: center; justify-content: space-between; padding: 9px 0; }
.switch .lb { font-size: 13.5px; }
.savebar {
display: flex; align-items: center; gap: 12px; padding: 13px 17px;
border-top: 1px solid var(--line-soft); background: var(--surface-2);
position: sticky; bottom: 0;
}
.savebar .grow { flex: 1; }
.restart-note { color: var(--warn); font-size: 12.5px; }
/* ── 弹层 ─────────────────────────────────────────────────────────── */
.veil { position: fixed; inset: 0; background: rgba(6,8,13,.62); backdrop-filter: blur(2px); display: none; z-index: 60; }
.veil.on { display: flex; align-items: center; justify-content: center; padding: 20px; }
.dlg {
background: var(--surface); border: 1px solid var(--line); border-radius: 13px;
width: 560px; max-width: 100%; max-height: 86vh; display: flex; flex-direction: column;
box-shadow: 0 24px 64px -24px rgba(0,0,0,.6);
}
.dlg header { padding: 17px 20px 13px; border-bottom: 1px solid var(--line-soft); }
.dlg header h3 { font-size: 15px; font-weight: 620; }
.dlg header .hint { color: var(--ink-3); font-size: 12.5px; margin-top: 5px; }
.dlg .body { padding: 18px 20px; overflow: auto; }
.dlg footer { padding: 13px 20px; border-top: 1px solid var(--line-soft); display: flex; gap: 10px; }
.dlg footer .grow { flex: 1; }
.dlg .tabs { display: inline-flex; gap: 2px; background: var(--surface-2); border-radius: 9px; padding: 3px; margin-left: 10px; vertical-align: middle; }
.dlg .tabs .tab { padding: 4px 14px; border-radius: 7px; border: none; background: transparent; color: var(--ink-3); font: 550 12.5px var(--sans); cursor: pointer; }
.dlg .tabs .tab.on { background: var(--surface); color: var(--ink); box-shadow: 0 1px 3px rgba(0,0,0,.25); }
.tab-panel + .tab-panel { border-top: 1px solid var(--line-soft); margin: 0 -20px; padding: 14px 20px 0; }
.url {
background: var(--surface-2); border: 1px solid var(--line); border-radius: 8px;
padding: 11px 13px; font: 12px var(--mono); word-break: break-all; color: var(--accent);
margin: 8px 0 12px; user-select: all;
}
.state { font-size: 13px; color: var(--ink-2); display: flex; align-items: center; gap: 8px; }
.state.ok { color: var(--ok); } .state.err { color: var(--bad); }
.dots::after { content: "…"; animation: blink 1.2s steps(4) infinite; }
@keyframes blink { 0% { opacity: 1; } 50% { opacity: .3; } 100% { opacity: 1; } }
/* ── 券码弹窗(票券式卡片,类名全部 vc- 前缀防越界)────────────────── */
.vc-acct { display: flex; align-items: baseline; gap: 7px; margin: 18px 2px 8px;
font-size: 12px; color: var(--ink-3); }
.vc-acct:first-child { margin-top: 4px; }
.vc-acct .nm { font-weight: 650; font-size: 13px; color: var(--ink-2); }
.vc { border: 1px solid var(--line); border-radius: 11px; overflow: hidden; margin: 0 0 10px;
background: var(--surface); transition: border-color .15s; }
.vc:hover { border-color: var(--accent); }
.vc .hd { display: flex; align-items: center; gap: 8px; padding: 12px 14px 4px; }
.vc .hd .nm { font-weight: 640; font-size: 14px; }
.vc .hd .tag { margin-left: auto; }
.vc .meta { padding: 2px 14px 10px; font-size: 11.5px; color: var(--ink-3); }
.vc .sep { border-top: 1px dashed var(--line); margin: 0 12px; }
.vc .ft { display: flex; align-items: center; gap: 12px; padding: 10px 14px; }
.vc .ft .lab { font-size: 11.5px; color: var(--ink-3); flex: none; }
.vc .ft code { font: 600 15px/1 var(--mono); letter-spacing: 1.2px; user-select: all;
background: var(--surface-2); border: 1px solid var(--line-soft);
border-radius: 7px; padding: 7px 12px; }
.vc .ft .acts { margin-left: auto; display: flex; gap: 8px; flex: none; }
.vc .vc-qr { border-top: 1px dashed var(--line); padding: 14px; display: flex; justify-content: center; }
.vc .vc-qr svg { border-radius: 6px; }
/* 过期态:只淡标题与元信息(保持券码可读),边框改虚线示意作废 */
.vc.expired { border-style: dashed; }
.vc.expired .hd, .vc.expired .meta { opacity: .55; }
/* ── Toast ───────────────────────────────────────────────────────── */
#toasts { position: fixed; right: 20px; bottom: 20px; z-index: 90; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; }
.tst {
background: var(--raise); border: 1px solid var(--line); border-left: 3px solid var(--ink-3);
border-radius: 8px; padding: 10px 15px; font-size: 13px; box-shadow: var(--shadow); max-width: 380px;
}
.tst.ok { border-left-color: var(--ok); } .tst.err { border-left-color: var(--bad); }
@media (max-width: 760px) {
.shell { grid-template-columns: 1fr; }
.nav { position: static; height: auto; flex-direction: row; align-items: center; overflow-x: auto; }
.nav ul { display: flex; padding: 8px; } .nav .sig { border: none; }
.stat { border-right: none; border-bottom: 1px solid var(--line-soft); }
}
/* ── 用量视图 ─────────────────────────────────────────────────────
这一页一屏要放下「两组 KPI + 图表 + 三张表」,所以视觉语言比账号池更轻:
卡片用左侧色轨区分语义,数字用等宽体对齐,分隔靠留白而不是更多边框。 */
/* KPI 卡:左侧 3px 色轨是唯一的重色,数值大、标签小,扫一眼就能读出量级。 */
.kpis { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 10px; padding: 14px 17px 2px; }
.kpis.k4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 1180px) { .kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 700px) { .kpis, .kpis.k4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.kpi {
position: relative; min-width: 0; overflow: hidden;
background: var(--surface-2); border: 1px solid var(--line-soft);
border-radius: 10px; padding: 11px 14px 12px 16px;
}
.kpi::before {
content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
background: var(--kpi-c, var(--ink-3)); opacity: .9;
}
.kpi .k { font-size: 11.5px; color: var(--ink-3); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; }
.kpi .v { font: 600 23px/1.15 var(--mono); font-variant-numeric: tabular-nums;
letter-spacing: -.02em; margin-top: 5px; white-space: nowrap; }
.kpi .s { font-size: 11px; color: var(--ink-3); margin-top: 4px; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; }
.kpi.c-accent { --kpi-c: var(--accent); } .kpi.c-accent .v { color: var(--accent); }
.kpi.c-ok { --kpi-c: var(--ok); } .kpi.c-ok .v { color: var(--ok); }
.kpi.c-warn { --kpi-c: var(--warn); } .kpi.c-warn .v { color: var(--warn); }
.kpi.c-bad { --kpi-c: var(--bad); } .kpi.c-bad .v { color: var(--bad); }
.kpi.c-mute { --kpi-c: var(--line); }
/* 中性但带一点主色:用于 prompt / 平均延迟这类"要看但不抢眼"的指标——
纯灰轨在深色主题下几乎看不见,纯主色又会让六张卡都在喊。 */
.kpi.c-soft { --kpi-c: color-mix(in srgb, var(--accent) 45%, var(--line)); }
/* KPI 内的构成条(总 token 卡的 prompt/completion 占比) */
.kpi .kbar { display: flex; height: 4px; border-radius: 3px; overflow: hidden;
background: var(--line); margin-top: 7px; }
.kpi .kbar i { display: block; height: 100%; min-width: 2px; }
.uschart { border: 1px solid var(--line-soft); border-radius: 10px; overflow: hidden;
background: var(--surface-2); }
.uschart-hd { display: flex; align-items: center; gap: 10px; padding: 10px 14px;
border-bottom: 1px solid var(--line-soft); font-size: 12px; }
.uschart-hd .lb { color: var(--ink-2); font-weight: 600; }
.uschart-hd .legend { color: var(--ink-3); font-size: 11.5px; display: flex; align-items: center; gap: 6px; }
.uschart-hd .legend .sep { width: 1px; height: 11px; background: var(--line); margin: 0 3px; }
.uschart-hd .sw { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.uschart-hd .sw-p { background: var(--accent); }
.uschart-hd .sw-c { background: var(--ok); }
.uschart-hd .sw-t { background: var(--warn); }
.uschart-body { padding: 8px 10px 4px; }
.uschart-body svg { display: block; width: 100%; height: auto; }
.uschart-body .ax { stroke: var(--line); stroke-width: 1; }
.uschart-body .gl { stroke: var(--line-soft); stroke-width: 1; stroke-dasharray: 2 3; opacity: .75; }
.uschart-body .avg { stroke: var(--warn); stroke-width: 1; stroke-dasharray: 4 4; opacity: .55; }
.uschart-body .tk { fill: var(--ink-3); font: 10px var(--mono); }
.uschart-body .tk-avg { fill: var(--warn); font: 10px var(--mono); opacity: .9; }
.uschart-body .tk-peak { fill: var(--ink-2); font: 600 10px var(--mono); }
.uschart-body rect.usbar { transition: opacity .12s; }
.uschart-body rect.usbar:hover { opacity: .78; }
.uschart-body .us-empty { padding: 30px 0; text-align: center; color: var(--ink-3); font-size: 12.5px; }
/* 用量表内的 prompt/completion 占比条。用 .us-mix 而不是复用账号池的 .bar:
全局 .bar 是块级容器(积分条),同优先级下会互相覆盖(v1.9.0 回归)。 */
.us-mix { display: flex; height: 3px; border-radius: 2px; overflow: hidden;
background: var(--line); margin-top: 5px; min-width: 60px; }
.us-mix i { display: block; height: 100%; }
.us-mix .p { background: var(--accent); }
.us-mix .c { background: var(--ok); }
/* 表内 zebra:三张表行数多,靠极淡的交替底色帮眼睛横向对齐。 */
.acc.zebra tbody tr:nth-child(even) { background: color-mix(in srgb, var(--surface-2) 55%, transparent); }
.acc.zebra tbody tr:hover { background: var(--surface-2); }
/* ── 积分构成 ───────────────────────────────────────────────── */
.pk-grid { display: flex; flex-wrap: wrap; gap: 10px; }
.pk-card { flex: 1 1 220px; min-width: 200px; border: 1px solid var(--line-soft);
border-radius: 8px; padding: 11px 13px; }
.pk-card .who { display: flex; align-items: baseline; gap: 7px; margin-bottom: 7px; }
.pk-card .nm { font-weight: 600; font-size: 13px; }
.pk-card .realm { font-size: 11px; color: var(--ink-3); font-family: var(--mono); }
.pk-card .big { font: 600 24px/1.1 var(--mono); font-variant-numeric: tabular-nums; }
.pk-card .sub { font-size: 11.5px; color: var(--ink-3); margin-top: 3px; }
.pk-card .mixbar { display: flex; height: 7px; border-radius: 4px; overflow: hidden;
margin-top: 8px; background: var(--surface-2); }
.pk-card .mixbar i { display: block; height: 100%; }
.pk-card .err { color: var(--warn); font-size: 12px; }
.pk-legend { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 7px;
font-size: 11px; color: var(--ink-3); }
.pk-legend span { display: flex; align-items: center; gap: 5px; }
.pk-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.pk-group-summary td { padding: 7px 11px; background: var(--surface-2); }
.pk-group-toggle { width: 100%; padding: 4px 0; border: 0; background: transparent;
color: var(--ink-2); text-align: left; }
.pk-group-toggle:hover:not(:disabled) { border: 0; color: var(--accent); }
.pk-hidden-row[hidden] { display: none; }
.pk-hidden-row td { color: var(--ink-3); }
.pk-expiry-chart { max-height: 260px; overflow-y: auto; overflow-x: hidden;
scrollbar-gutter: stable; scrollbar-width: thin; padding-right: 10px;
overscroll-behavior: contain; }
.pk-expiry-hdr { display: grid; grid-template-columns: 64px minmax(80px, 1fr) 72px;
align-items: center; gap: 9px; margin-bottom: 4px; color: var(--ink-3);
font-size: 11px; }
.pk-expiry-hdr > b { text-align: right; font-weight: 600; }
.pk-expiry-row { display: grid; grid-template-columns: 64px minmax(80px, 1fr) 72px;
align-items: center; gap: 9px; min-height: 30px; padding-right: 2px; }
.pk-expiry-row > span { color: var(--ink-3); font-size: 11.5px; }
.pk-expiry-row > b { text-align: right; font: 600 11.5px var(--mono);
font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.pk-expiry-track { display: flex; align-items: stretch; gap: 2px; height: 9px;
border-radius: 5px; background: var(--surface-2); overflow: hidden; }
.pk-expiry-seg { display: block; min-width: 3px; height: 100%; cursor: default;
background: var(--seg-color, var(--accent)); transition: filter .15s, transform .15s; }
.pk-expiry-seg:hover { filter: brightness(1.12); transform: scaleY(1.35); }
.pk-expiry-legend { display: flex; flex-wrap: wrap; gap: 5px 12px; margin-top: 8px;
color: var(--ink-3); font-size: 11px; }
.pk-expiry-legend span { display: inline-flex; align-items: center; gap: 5px; }
.pk-expiry-legend i { width: 9px; height: 9px; border-radius: 3px; flex: 0 0 9px; }
.pk-expiry-foot { margin-top: 9px; padding-top: 8px; border-top: 1px solid var(--line-soft);
color: var(--ink-3); font-size: 11px; }
.pk-expiry-empty { padding: 22px 0; text-align: center; color: var(--ink-3); font-size: 12px; }
.pk-card .expirybar { display: flex; height: 5px; border-radius: 3px; overflow: hidden;
margin-top: 6px; background: var(--surface-2); }
.pk-card .expirybar i { display: block; height: 100%; min-width: 2px; }
/* ── 筛选栏(模型档位 / 运行日志 / 用量共用)─────────────────────────
面板控件默认 input/select 是 width:100%(配置表单要整行),筛选栏里必须显式
收回宽度,否则第一个控件会把整行撑满、后面的控件被挤到下一行。 */
.fbar {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding: 10px 17px; border-bottom: 1px solid var(--line-soft); background: var(--surface-2);
}
.fbar > input, .fbar > select {
width: auto; padding: 5px 10px; font-size: 12.5px; border-radius: 6px;
}
.fbar > input[type=search] { min-width: 200px; flex: 0 1 280px; }
.fbar > input[type=search]::-webkit-search-cancel-button { cursor: pointer; }
.fbar .grow { flex: 1; }
.fbar .flab { color: var(--ink-3); font-size: 12px; white-space: nowrap; }
.fbar select { cursor: pointer; }
/* 用量总览的头部在「自定义」态会多出两个时间输入框:允许换行,宁可整组折到
下一行,也不要把标题挤成两行、把下拉压成窄条。
口径说明(#usNote)单行截断而不是折行——它是长文本,一旦允许折行就会在窄屏
被挤成一列竖排的碎字,把标题和控件一起带歪;完整内容走 title 悬停看。 */
#view-usage .box > header { flex-wrap: wrap; }
#view-usage .box > header > .note {
flex: 0 1 auto; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#view-usage .box > header > button { flex: none; }
/* ── 时间范围控件(用量 / 请求记录共用)────────────────────────────
预设下拉 + 「自定义」时展开的起止时间。整组是 inline-flex 且允许换行:
自定义态下在窄屏会整体折到下一行,而不是把标题挤扁。 */
.trange { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.trange .tr-custom { display: inline-flex; align-items: center; gap: 6px; }
.trange .tr-sep { color: var(--ink-3); font-size: 12px; }
.trange select, .trange input { width: auto; padding: 5px 9px; font-size: 12.5px; border-radius: 6px; }
.trange input[type=datetime-local] { font-family: var(--mono); font-size: 12px; }
/* 原生日期选择器的弹出面板要跟随明暗主题,否则深色下是一块刺眼的白。 */
.trange input[type=datetime-local] { color-scheme: light; }
[data-theme="dark"] .trange input[type=datetime-local] { color-scheme: dark; }
.trange .tr-bad { border-color: var(--bad); background: var(--bad-soft); }
/* ── 视图内维度切换(用量「按账号/按模型/按域」等)────────────────── */
.vtabs { display: inline-flex; gap: 2px; background: var(--surface-2); border-radius: 8px; padding: 3px; }
.vtabs button {
padding: 3.5px 13px; border: none; background: transparent; border-radius: 6px;
color: var(--ink-3); font: 550 12.5px var(--sans); cursor: pointer;
}
.vtabs button:hover:not(:disabled) { border: none; color: var(--ink); }
.vtabs button.on { background: var(--surface); color: var(--ink); box-shadow: 0 1px 3px rgba(0,0,0,.22); }
.vtabs .cnt { color: var(--ink-3); font-weight: 400; margin-left: 4px; font-variant-numeric: tabular-nums; }
.vtabs button.on .cnt { color: var(--ink-2); }
/* ── 请求记录表(运行日志)────────────────────────────────────────── */
.acc.req { min-width: 1080px; }
.acc.req th, .acc.req td { padding: 8px 10px; }
.acc.req tbody tr { cursor: default; }
/* 请求记录最多可读 1000 条,不限高会把下面的「运行日志」推到很远的地方。
限高 + 表头吸顶:滚动区域内始终能看到列名。 */
.req-wrap { max-height: 460px; overflow: auto; }
.req-wrap thead th { position: sticky; top: 0; background: var(--surface); z-index: 1; }
.clip { display: block; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.clip.ip { max-width: 150px; font-family: var(--mono); font-size: 12.5px; }
.clip.rid { max-width: 150px; font-family: var(--mono); font-size: 11.5px; color: var(--ink-3); }
.acc.req .muted { color: var(--ink-3); }
.src-off { color: var(--warn); }
/* 用量统计条已改用 .kpis(见「用量视图」段),此处不再保留 .stats.compact 变体。 */
/* 到期提醒卡片(首页):每账号一行,色点=危险度,正文数字用 mono 对齐。 */
.exp-row { display: flex; align-items: flex-start; gap: 10px; padding: 8px 0;
font-size: 12.5px; }
.exp-row + .exp-row { border-top: 1px solid var(--line-soft); }
.exp-dot { width: 9px; height: 9px; border-radius: 50%; margin-top: 4px; flex: none; }
.exp-nm { font-weight: 600; flex: none; min-width: 9em; }
.exp-main { color: var(--ink-2, inherit); }
.exp-main b { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.exp-main .note { margin-top: 2px; font-size: 11px; color: var(--ink-3); }
.exp-main.err { color: var(--bad); }
</style>
</head>
<body>
<div class="shell">
<nav class="nav">
<div class="brand">
<div class="name">WorkBuddy2API</div>
<div class="sub" id="navSub">控制台</div>
</div>
<ul>
<li><a href="#accounts" data-view="accounts" class="on">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="5.5" r="2.6"/><path d="M2.8 13.5c.6-2.6 2.7-4 5.2-4s4.6 1.4 5.2 4"/></svg>
账号池</a></li>
<li><a href="#usage" data-view="usage">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M2 13.5h12"/><path d="M4.5 13.5V8.2M8 13.5V3.5M11.5 13.5v-3"/></svg>
用量</a></li>
<li><a href="#packages" data-view="packages">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M2.2 5.4 8 2.5l5.8 2.9v5.2L8 13.5l-5.8-2.9z"/><path d="M2.2 5.4 8 8.3l5.8-2.9M8 8.3v5.2"/></svg>
积分构成</a></li>
<li><a href="#taskscenter" data-view="taskscenter">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M3 2.5h10v9H8l-3 3v-3H3z"/><path d="M6 6.5h4M6 9h2.5"/></svg>
任务中心</a></li>
<li><a href="#models" data-view="models">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M8 1.8 14 5v6L8 14.2 2 11V5z"/><path d="M2 5l6 3.2L14 5M8 8.2v6"/></svg>
模型与档位</a></li>
<li><a href="#config" data-view="config">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><circle cx="8" cy="8" r="2.2"/><path d="M8 1.6v1.9M8 12.5v1.9M1.6 8h1.9M12.5 8h1.9M3.5 3.5l1.3 1.3M11.2 11.2l1.3 1.3M12.5 3.5l-1.3 1.3M4.8 11.2l-1.3 1.3"/></svg>
配置</a></li>
<li><a href="#logs" data-view="logs">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M2.5 3.5h11M2.5 8h11M2.5 12.5h7"/></svg>
运行日志</a></li>
</ul>
<div class="sig">
<div class="row"><span class="pulse" id="navPulse"></span><span id="navState">连接中</span></div>
<div class="row" style="margin-top:5px"><span id="navVer">v-</span> · <span id="navRedis">-</span></div>
</div>
</nav>
<div class="main">
<div class="topbar">
<h2 id="ttl">账号池</h2>
<span class="sep">/</span>
<span class="meta" id="subMeta">-</span>
<div class="grow"></div>
<button class="icon ghost" id="btnTheme" title="切换明暗(跟随系统 → 浅色 → 深色)" aria-label="切换主题">
<svg id="icoTheme" viewBox="0 0 16 16" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.4"><path d="M13.2 9.6A5.6 5.6 0 0 1 6.4 2.8a5.6 5.6 0 1 0 6.8 6.8z"/></svg>
</button>
<button id="btnRefresh">刷新</button>
<button class="primary" id="btnAdd">添加账号</button>
</div>
<!-- 账号池 -->
<section class="view" id="view-accounts">
<div class="stats">
<div class="stat"><div class="v" id="sTotal">-</div><div class="k">账号总数</div></div>
<div class="stat good"><div class="v" id="sHealthy">-</div><div class="k">可用</div></div>
<div class="stat warn"><div class="v" id="sCooling">-</div><div class="k">冷却中</div></div>
<div class="stat bad"><div class="v" id="sDisabled">-</div><div class="k">已禁用</div></div>
<div class="stat"><div class="v" id="sCredits">-</div><div class="k">积分剩余 / 总额</div></div>
<div class="stat"><div class="v" id="sSticky">-</div><div class="k">粘性会话</div></div>
</div>
<div class="box" id="expBox" hidden>
<header>
<h3>积分到期提醒<span class="hint">按最近到期批次估算 · 日均需耗 = 批次剩余 ÷ 距到期天数 · 上游按失效时刻先后自动优先扣减(FEFO)</span></h3>
<span class="grow"></span>
<span class="note" id="expNote"></span>
<button class="xs" id="btnExp">检查</button>
</header>
<div class="pad"><div id="expList"></div></div>
</div>
<div class="box">
<header>
<h3>账号池</h3>
<span class="grow"></span>
<span class="note" id="accNote"></span>
<button class="xs" id="btnCheckinAll">全部签到</button>
<button class="xs" id="btnTravelAll">旅行巡检</button>
<button class="xs" id="btnActivityAll">活跃上报</button>
<button class="xs" id="btnKeepaliveAll">全部保活</button>
</header>
<div class="tbl-wrap">
<table class="acc">
<thead><tr>
<th class="mark" aria-hidden="true"></th>
<th>账号</th><th>状态</th><th>积分</th>
<th>成功 / 失败</th><th>在途</th><th>用量</th><th>最近成功</th><th class="c-acts"></th>
</tr></thead>
<tbody id="accBody"></tbody>
</table>
</div>
</div>
<div class="box">
<header>
<h3>模型锁池 <span class="hint">哪些模型不能用、还要锁多久</span></h3>
<span class="grow"></span>
<span class="note" id="mlNote"></span>
</header>
<div class="tbl-wrap">
<table class="acc">
<thead><tr>
<th>模型</th><th>域</th><th>状态</th><th>可选 / 总数</th>
<th>锁定账号</th><th>最早解锁</th><th>全池解锁</th><th>原因</th>
</tr></thead>
<tbody id="mlBody"></tbody>
</table>
</div>
</div>
</section>
<!-- 任务中心 -->
<section class="view" id="view-taskscenter" hidden>
<div class="box">
<header>
<h3>成长任务队列 <span class="hint" id="qcSummary"></span></h3>
<span class="grow"></span>
<button class="xs" id="btnSchoolVouchers" title="开学季抽奖抽中的第三方券码(活动已结束,历史券码仍可查)">查询券码</button>
<label class="sel-note">并发
<select id="qcConc"><option value="1" selected>1</option><option value="2">2</option><option value="3">3</option></select>
</label>
<button class="xs" id="btnScanAll">扫描待办</button>
<button class="xs primary" id="btnRunQueue">执行全部待办</button>
</header>
<div class="qprog" id="qProg" hidden>
<div class="qbar"><i id="qBarFill"></i></div>
<span class="note" id="qProgText"></span>
</div>
<div class="tc-empty" id="tcEmpty">
<svg viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="8" y="6" width="24" height="36" rx="3"/>
<path d="M14 16h12M14 24h12M14 32h7" opacity=".45"/>
<circle cx="35" cy="33" r="8"/><path d="m40 38 5 5"/>
</svg>
<div class="t">还没有扫描过</div>
<div class="d">扫描所有账号的成长任务与开学季待办,把没做的排成一列,一键执行。</div>
</div>
<div id="qcList"></div>
</div>
</section>
<!-- 模型与档位 -->
<section class="view" id="view-models" hidden>
<div class="box">
<header>
<h3>模型能力 <span class="hint">max_tokens bug已修复目前可以正常透传最大输出,但是官方返回的最大输出仅为参考,不保证完全正确
</span></h3>
<span class="grow"></span>
<span class="note" id="mdNote">实时查询上游</span>
<button class="xs" id="btnModels">重新获取</button>
</header>
<div class="fbar" id="mdFilter">
<input id="mdQ" type="search" placeholder="搜索模型 ID / 名称 / 描述 / 厂商" aria-label="搜索模型">
<select id="mdRealm" aria-label="按域筛选">
<option value="">全部域</option>
<option value="cn">国内版 CN</option>
<option value="global">国际版 Global</option>
</select>
<select id="mdCap" aria-label="按能力筛选">
<option value="">全部能力</option>
<option value="tool">支持工具调用</option>
<option value="vision">支持视觉</option>
<option value="reasoning">支持思考</option>
<option value="default">默认模型</option>
</select>
<select id="mdEffort" aria-label="按思考档位筛选">
<option value="">全部档位</option>
<option value="off">可关闭思考</option>
<option value="low">low</option>
<option value="medium">medium</option>
<option value="high">high</option>
<option value="xhigh">xhigh</option>
<option value="max">max</option>
</select>
<select id="mdPromo" aria-label="按价格筛选">
<option value="">全部价格</option>
<option value="promo">有折扣 / 限时免费</option>
<option value="free">限时免费</option>
<option value="discount">打折(非免费)</option>
</select>
<select id="mdSort" aria-label="排序方式">
<option value="default">上游默认顺序</option>
<option value="rate">积分倍率 低 → 高</option>
<option value="context">上下文长度 大 → 小</option>
<option value="output">最大输出 大 → 小</option>
<option value="name">模型 ID A → Z</option>
</select>
<span class="grow"></span>
<span class="note" id="mdCount"></span>
<button class="xs ghost" id="mdReset" title="清空全部筛选条件">重置</button>
</div>
<div class="tbl-wrap">
<table class="acc md">
<thead><tr><th class="mark" aria-hidden="true"></th><th>模型</th><th>积分倍率</th><th>默认档</th><th>支持的思考档位</th><th>上下文长度</th><th>最大输出</th></tr></thead>
<tbody id="mdBody"></tbody>
</table>
</div>
</div>
</section>
<!-- 积分构成 -->
<section class="view" id="view-packages" hidden>
<div class="box">
<header>
<h3>积分到期分布</h3>
<span class="grow"></span>
<span class="note">按批次到期日聚合,颜色区分账号</span>
</header>
<div class="pad"><div id="pkExpiry"></div></div>
</div>
<div class="box">
<header>
<h3>账号对比</h3>
<span class="grow"></span>
<span class="note" id="pkNote">—</span>
<select id="pkSort" class="xs" title="逐包明细的排序规则">
<option value="end_asc" selected>按到期时间</option>
<option value="size_desc">按面额大小</option>
</select>
<button class="xs" id="btnPk">刷新</button>
</header>
<div class="pad"><div class="pk-grid" id="pkSummary"></div></div>
</div>
<div id="pkDetail"></div>
</section>
<!-- 用量 -->
<section class="view" id="view-usage" hidden>
<div class="box">
<header>
<h3>用量总览</h3>
<span class="grow"></span>
<span class="note" id="usNote">—</span>
<span class="trange" id="usRange"></span>
<button class="xs" id="btnUsage">刷新</button>
</header>
<div class="kpis" id="usStats"></div>
<div class="pad">
<div class="uschart">
<div class="uschart-hd">
<span class="lb">Token 时序</span>
<span class="note" id="usChartNote">—</span>
<span class="grow"></span>
<span class="legend">
<i class="sw sw-p"></i>prompt<i class="sw sw-c"></i>completion
<span class="sep"></span><i class="sw sw-t"></i>均值
</span>
</div>
<div id="usChart" class="uschart-body"></div>
</div>
</div>
</div>
<div class="box">
<header>
<h3>用量明细</h3>
<span class="vtabs" id="usDimTabs">
<button data-dim="account" class="on">按账号</button>
<button data-dim="model">按模型</button>
<button data-dim="realm">按域</button>
</span>
<span class="grow"></span>
<span class="note" id="usDimNote">—</span>
<label class="sel-note">排序
<select id="usSort">
<option value="total">合计 Token</option>
<option value="requests">请求数</option>
<option value="errors">失败数</option>
<option value="latency">平均延迟</option>
</select>
</label>
</header>
<div class="tbl-wrap">
<table class="acc zebra">
<thead id="usDimHead"></thead>
<tbody id="usDimBody"></tbody>
</table>
</div>
</div>
<div class="box">
<header>
<h3>积分扣除历史</h3>
<span class="vtabs" id="usCreditTabs">
<button data-dim="account" class="on">按账号</button>
<button data-dim="model">按模型</button>
</span>
<span class="grow"></span>
<span class="note" id="usCreditNote">—</span>
</header>
<div class="kpis k4" id="usCreditStats"></div>
<div class="tbl-wrap">
<table class="acc zebra">
<thead id="usCreditHead"></thead>
<tbody id="usCreditBody"></tbody>
</table>
</div>
</div>
</section>
<!-- 配置 -->
<section class="view" id="view-config" hidden>
<form id="cfgForm">
<div class="box">
<header><h3>服务</h3><span class="grow"></span><span class="note" id="cfgPath"></span></header>
<div class="pad">
<div class="grid2">
<label class="fld"><span class="lb">监听地址</span>
<input name="listen" placeholder=":7863">
<span class="hint">改动需重启进程</span></label>
<label class="fld"><span class="lb">API 密钥</span>
<div class="inline"><input name="api_key" id="cfgKey" type="password" placeholder="留空 = 不鉴权">
<button type="button" class="xs" id="btnEye">显示</button></div>
<span class="hint">立即生效(含面板自身)</span></label>
</div>
<label class="fld"><span class="lb">单账号明细默认展示条数</span>
<input name="package_detail_limit" type="number" min="1" placeholder="5">
<span class="hint">积分构成页按最早到期展示,其余未用完包聚合</span></label>
</div>
</div>
<div class="box">
<header><h3>定时任务</h3></header>
<div class="pad">
<div class="grid2">
<div>
<div class="switch"><span class="lb">自动签到</span>
<input type="checkbox" name="checkin_enabled"></div>
<label class="fld"><span class="lb">签到时点(小时,逗号分隔)</span>
<input name="checkin_hours" placeholder="9, 21"></label>
</div>
<div>
<div class="switch"><span class="lb">Token 保活</span>
<input type="checkbox" name="keepalive_enabled"></div>
<label class="fld"><span class="lb">保活时点(小时,逗号分隔)</span>
<input name="keepalive_hours" placeholder="22"></label>
</div>
</div>
<div class="hr"></div>
<div class="grid2">
<div>
<div class="switch"><span class="lb">猫猫旅行</span>
<input type="checkbox" name="travel_enabled"></div>
<label class="fld"><span class="lb">旅行时点(小时,逗号分隔)</span>
<input name="travel_hours" placeholder="9, 21">
<span class="hint">一趟派出 + 一趟领奖闭环</span></label>
</div>
<div>
<div class="switch"><span class="lb">活跃上报</span>
<input type="checkbox" name="activity_enabled"></div>
<label class="fld"><span class="lb">上报时点(小时,逗号分隔)</span>
<input name="activity_hours" placeholder="10">
<span class="hint">点亮连登 + 解锁领养前置</span></label>
</div>
</div>
<div class="hr"></div>
<div class="grid2">
<div class="switch"><span class="lb">后台刷新余额</span>
<input type="checkbox" name="balance_refresh_enabled"></div>
<label class="fld"><span class="lb">刷新间隔(分钟)</span>
<input name="balance_refresh_minutes" type="number" min="1" placeholder="5"></label>
</div>
<div class="hr"></div>
<div class="grid2">
<div class="switch"><span class="lb">成长任务自动执行</span>
<input type="checkbox" name="growth_enabled" checked></div>
<label class="fld"><span class="lb">执行时点(小时,逗号分隔)</span>
<input name="growth_hours" placeholder="1">
<span class="hint">每日到点自动「扫描 + 执行全部待办」(Sequential 任务链零点解锁后自动推进,建议零点后)</span></label>
</div>
<div class="hr"></div>
<div class="switch"><span class="lb">保号任务覆盖已禁用账号</span>
<input type="checkbox" name="include_disabled_in_tasks"></div>
<div class="hint">禁用只关选号:打开后已禁用的账号仍会签到 / 活跃上报 / 保活 / 刷新余额(依旧不参与选号)。适合「一次只放开一个账号、用禁用做流量开关」的轮换养号用法,默认关闭。<br>若只想让<b>单个</b>账号临时退出选号但保留保号,直接用账号行的「<b>暂停选号</b>」按钮(账号级,无需打开本开关)。</div>
</div>
</div>
<div class="box">
<header><h3>账号池与流量治理</h3></header>
<div class="pad">
<div class="grid3">
<label class="fld"><span class="lb">单账号最大在途</span>
<input name="max_in_flight" type="number" min="0" placeholder="3">
<span class="hint">0 = 不限制</span></label>
<label class="fld"><span class="lb">国际版在途上限</span>
<input name="max_in_flight_global" type="number" min="1" placeholder="2">
<span class="hint">global 域风控更紧,默认 2</span></label>
<label class="fld"><span class="lb">连续失败熔断阈值</span>
<input name="breaker_threshold" type="number" min="1" placeholder="3"></label>
</div>
<div class="grid3">
<label class="fld"><span class="lb">软限流冷却基数</span>
<input name="soft_rate" placeholder="600s"></label>
<label class="fld"><span class="lb">软冷却退避上限</span>
<input name="soft_rate_max" placeholder="2h"></label>
<label class="fld"><span class="lb">熔断基础时长</span>
<input name="breaker_cooldown" placeholder="30m"></label>
</div>
<div class="grid3">
<label class="fld"><span class="lb">熔断退避上限</span>
<input name="breaker_cooldown_max" placeholder="6h"></label>
<label class="fld"><span class="lb">连败降权阈值</span>
<input name="degrade_threshold" type="number" min="1" placeholder="5">
<span class="hint">未知错误连败 N 次临时出池</span></label>
<label class="fld"><span class="lb">连败降权时长</span>
<input name="degrade_cooldown" placeholder="10m"></label>
</div>
<div class="grid3">
<label class="fld"><span class="lb">连败降权上限</span>
<input name="degrade_cooldown_max" placeholder="2h"></label>
<label class="fld"><span class="lb">闲置补偿 / 小时</span>
<input name="idle_weight_per_hour" type="number" step="0.1" placeholder="0.5"></label>
<label class="fld"><span class="lb">闲置补偿上限</span>
<input name="idle_weight_max" type="number" step="0.1" placeholder="5"></label>
</div>
<div class="grid3">
<label class="fld"><span class="lb">成本探索窗口</span>
<input name="cost_explore_interval" placeholder="30m">
<span class="hint">垄断破除:免费层垄断时定期搭车探索未知号;0 关停</span></label>
<label class="fld"><span class="lb">积分保底</span>
<input name="credit_floor" type="number" min="0" placeholder="100">
<span class="hint">余额低于此值不再接实测收费模型(保住免费模型可用);0 关闭</span></label>
<label class="fld"><span class="lb">快过期路由窗口</span>
<input name="expiring_soon" placeholder="168h">
<span class="hint">窗口内有快过期批次的账号选号权重 ×3(不排序、与金额无关);0 关闭</span></label>
<label class="fld"><span class="lb">会话粘性 TTL</span>
<input name="ttl" placeholder="30m">
<span class="hint">改动需重启进程</span></label>
</div>
<div class="switch"><span class="lb">快过期积分优先</span>
<input type="checkbox" name="prefer_expiring"></div>
</div>
</div>
<div class="box">
<header><h3>上游与高级</h3></header>
<div class="pad">
<div class="grid3">
<label class="fld"><span class="lb">短请求超时</span>
<input name="timeout_seconds" type="number" min="1" placeholder="120">
<span class="hint">需重启</span></label>
<label class="fld"><span class="lb">聊天首字节超时</span>
<input name="header_timeout_seconds" type="number" min="1" placeholder="120">
<span class="hint">需重启</span></label>
<label class="fld"><span class="lb">流空闲超时</span>
<input name="idle_timeout_seconds" type="number" min="1" placeholder="300">
<span class="hint">需重启</span></label>
</div>
<div class="hr"></div>
<div class="grid2">
<label class="fld"><span class="lb">出站 User-Agent</span>
<input name="user_agent" placeholder="留空 = CLI/2.63.2 CodeBuddy/2.63.2">
<span class="hint">影响官网积分记录「使用端」显示;需重启</span></label>
</div>
<div class="hr"></div>
<div class="grid2">
<label class="fld"><span class="lb">系统提示词模式</span>
<select name="prompt_mode">
<option value="custom">custom — 网关自有提示词(避免指纹误报)</option>
<option value="append">append — 客户端 system 后插网关提示词(并用)</option>
<option value="passthrough">passthrough — 透传客户端原始 system</option>
</select>
<span class="hint">需重启</span></label>
<label class="fld"><span class="lb">提示词文件路径</span>
<input name="prompt_file" placeholder="留空 = 内置默认提示词">
<span class="hint">需重启</span></label>
</div>
<div class="hr"></div>
<div class="switch"><span class="lb">出站请求指纹脱敏</span>
<input type="checkbox" name="sanitize_blacklist_fingerprints"></div>
<div class="switch"><span class="lb">会话粘性路由</span>
<input type="checkbox" name="session_sticky_enabled"></div>
<div class="hint" style="margin-top:10px">Upstash Redis 镜像、凭证目录与状态文件路径需手工编辑配置文件(判为重启项)。</div>
</div>
</div>
<div class="box">
<header><h3>日志</h3></header>
<div class="pad">
<div class="switch"><span class="lb">记录调用来源(客户端 IP / User-Agent)</span>
<input type="checkbox" name="request_client_info"></div>
<div class="hint">开启后,「运行日志」页的请求记录会显示每次调用来自哪个 IP、用什么客户端;
关闭则归档与面板都不再出现来源信息。改动即时生效,无需重启。</div>
<div class="hint" style="margin-top:8px">归档开关、保留天数与容量上限(<code>logging.request_archive_*</code>)需手工编辑配置文件。</div>
</div>
</div>
<div class="savebar">
<span class="grow"></span>
<span class="restart-note" id="cfgNote"></span>
<button type="button" id="btnCfgReload">放弃修改</button>
<button type="submit" class="primary" id="btnCfgSave">保存配置</button>
</div>
</form>
</section>
<!-- 日志 -->
<section class="view" id="view-logs" hidden>
<div class="box">
<header>
<h3>请求记录 <span class="hint">调用来源:IP 取 X-Forwarded-For 首段,无代理头时回落 TCP 对端</span></h3>
<span class="grow"></span>
<span class="note" id="reqSummary">—</span>
<span class="note" id="reqNote">—</span>
</header>
<div class="fbar" id="reqFilter">
<input id="reqQ" type="search" placeholder="搜索 IP / UA / 模型 / 账号 / 请求 ID" aria-label="搜索请求记录">
<select id="reqOutcome" aria-label="按结果筛选">
<option value="">全部结果</option>
<option value="success">成功</option>
<option value="http_error">HTTP 错误</option>
<option value="stream_error">流错误</option>
<option value="interrupted">中断</option>
</select>
<span class="trange" id="reqRange"></span>
<select id="reqLimit" aria-label="读取条数">
<option value="100" selected>最近 100 条</option>
<option value="300">最近 300 条</option>
<option value="1000">最近 1000 条</option>
</select>
<span class="grow"></span>
<span class="note" id="reqCount">—</span>
<button class="xs" id="btnReqReload">重新读取</button>
</div>
<div class="tbl-wrap req-wrap">
<table class="acc req">
<thead><tr>
<th>时间</th><th>结果</th><th>模型</th><th>账号</th>
<th>来源 IP</th><th>User-Agent</th>
<th class="num">耗时</th><th class="num">Token</th><th class="num">积分</th><th>请求 ID</th>
</tr></thead>
<tbody id="reqBody"></tbody>
</table>
</div>
</div>
<div class="box">
<header>
<h3>运行日志</h3>
<span class="grow"></span>
<span class="chips" id="logChips">
<button class="xs chip on" data-ch="all">全部</button>
<button class="xs chip" data-ch="task">任务</button>
<button class="xs chip" data-ch="chat">对话</button>
<button class="xs chip" data-ch="sys">系统</button>
</span>
<span class="note" id="logNote">最近 500 行</span>
<button class="xs" id="btnLogPin">自动滚动:开</button>
</header>
<pre id="logBox"></pre>
</div>
</section>
</div>
</div>
<!-- 积分任务 -->
<div class="veil" id="taskVeil">
<div class="dlg" style="width:940px">
<header>
<h3>积分任务 <span class="hint" id="taskWho"></span></h3>
<div class="hint">查询上游任务进度;「接受」为报名(幂等),「领取」在进度达标后可用。所有操作走网关,无需外部脚本。</div>
</header>
<div class="body">
<div class="state" id="taskState"><span class="dots">加载中</span></div>
<div class="tbl-wrap">
<table class="acc md" id="taskTable" hidden>
<thead><tr><th class="mark" aria-hidden="true"></th><th>任务</th><th>进度</th><th>奖励</th><th>状态</th><th class="c-acts"></th></tr></thead>
<tbody id="taskBody"></tbody>
</table>
</div>
</div>
<footer>
<span class="grow"></span>
<button id="btnTaskAcceptAll">全部接受</button>
<button id="btnTaskAutoAll" class="primary">一键完成可自动任务</button>
<button id="btnTaskReload">重新查询</button>
<button id="btnCloseTask">关闭</button>
</footer>
</div>
</div>
<!-- 开学季券码 -->
<div class="veil" id="vcVeil">
<div class="dlg">
<header>
<h3>开学季 · 我的券码</h3>
<div class="hint">抽奖抽中的第三方券(KFC / 瑞幸 / 酷狗等),券码到店到对应app/小程序兑换。</div>
</header>
<div class="body" id="vcBody"></div>
<footer>
<span class="hint" id="vcNote"></span>
<span class="grow"></span>
<button class="xs" id="btnVcRefresh">刷新</button>
<button class="xs primary" id="btnVcClose">关闭</button>
</footer>
</div>
</div>
<!-- 添加账号 -->
<div class="veil" id="addVeil">
<div class="dlg">
<header>
<h3>添加账号</h3>
<div class="tabs" id="addTabs">
<button class="tab on" data-tab="login">浏览器登录</button>
<button class="tab" data-tab="import">导入 JSON</button>
</div>
</header>
<div class="body">
<!-- 浏览器登录 -->
<div id="addTabLogin" class="tab-panel">
<div id="addPick">
<div class="realm-row">
<span class="hint" style="font-size:12px">版本:</span>
<label class="realm-opt"><input type="radio" name="addRealm" value="cn" checked> 国内版(CN)</label>
<label class="realm-opt"><input type="radio" name="addRealm" value="global"> 国际版(Global)</label>
</div>
<div class="hint" style="font-size:12px;margin-top:2px">国际版登录后,网关自动完成注册地区、激活与试用额度领取,全程无需手动操作。</div>
</div>
<div id="addLoad" class="state" hidden><span class="dots">正在获取授权链接</span></div>
<div id="addReady" hidden>
<div class="state">在浏览器打开以下链接并登录:</div>
<div class="url" id="addUrl"></div>
<div class="state" id="addPoll"><span class="dots">等待授权完成,自动检测中</span></div>
</div>
<div id="addDone" class="state ok" hidden></div>
<div id="addErr" class="state err" hidden></div>
</div>
<!-- 导入 JSON -->
<div id="addTabImport" class="tab-panel" hidden>
<div class="hint" style="margin-bottom:8px">选择 cockpit tools 导出的 JSON 文件,批量导入账号。文件需为账号数组格式。</div>
<input type="file" id="importFile" accept=".json" style="margin-bottom:8px">
<div id="importDone" class="state ok" hidden></div>
<div id="importErr" class="state err" hidden></div>
</div>
</div>
<footer>
<span class="grow"></span>
<button id="btnStartLogin" class="primary">获取授权链接</button>
<button id="btnCopyUrl" hidden>复制链接</button>
<button id="btnOpenUrl" class="primary" hidden>在浏览器打开</button>
<button id="btnCloseAdd">关闭</button>
</footer>
</div>
</div>
<!-- 密钥 -->
<div class="veil" id="keyVeil" data-lock="1">
<div class="dlg" style="width:420px">
<header>
<h3>需要访问密钥</h3>
<div class="hint">该网关已启用 api_key 鉴权,请输入 config.json 中的密钥。</div>
</header>
<div class="body">
<form style="display:contents" onsubmit="return false"><input type="password" id="keyInput" placeholder="api_key" autocomplete="off"></form>
<div class="state err" id="keyErr" hidden style="margin-top:10px">密钥不正确,请重试。</div>
</div>
<footer>
<span class="grow"></span>
<button class="primary" id="btnKey">进入</button>
</footer>
</div>
</div>
<div id="toasts"></div>
<script src="app.js"></script>
</body>
</html>