2026-03-11 11:37:35 +08:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > AI Team 调度中心< / title >
< style >
*{margin:0;padding:0;box-sizing:border-box}
:root, [data-theme="dark"]{
--bg:#0b0b10;--surface:#13131d;--surface2:#1c1c2e;--surface3:#24243a;
--border:#2a2a44;--border2:#363655;
--text:#e8e8f4;--text2:#a0a0be;--text3:#6a6a88;
--green:#22c55e;--yellow:#eab308;--red:#ef4444;--blue:#3b82f6;
--purple:#a78bfa;--cyan:#4ecdc4;--orange:#ff6b35;
--think-bg:rgba(234,179,8,.06);--think-border:rgba(234,179,8,.15);
--chat-fade:var(--surface);--chat-fade2:rgba(11,11,16,.95);
}
[data-theme="light"]{
--bg:#f5f5f8;--surface:#ffffff;--surface2:#f0f0f5;--surface3:#e8e8f0;
--border:#d8d8e4;--border2:#c8c8d8;
--text:#1a1a2e;--text2:#555570;--text3:#888898;
--green:#16a34a;--yellow:#ca8a04;--red:#dc2626;--blue:#2563eb;
--purple:#7c3aed;--cyan:#0891b2;--orange:#ea580c;
--think-bg:rgba(234,179,8,.08);--think-border:rgba(234,179,8,.25);
--chat-fade:var(--surface);--chat-fade2:rgba(245,245,248,.95);
}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
::-webkit-scrollbar{width:5px;height:5px}
::-webkit-scrollbar-track{background:var(--surface)}
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
::-webkit-scrollbar-thumb:hover{background:var(--text3)}
/* ── Header ── */
.header{padding:10px 28px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:100}
.header h1{font-size:18px;font-weight:700;display:flex;align-items:center;gap:8px;cursor:pointer;flex-shrink:0}
.header h1 span{font-size:22px}
.header-right{display:flex;align-items:center;gap:12px}
.refresh-info{font-size:10px;color:var(--text3)}
.stats-bar{display:flex;gap:5px}
.stat-badge{padding:2px 8px;border-radius:14px;font-size:10px;font-weight:600;background:var(--surface2);border:1px solid var(--border)}
.stat-badge.green{color:var(--green);border-color:rgba(34,197,94,.25)}
.stat-badge.yellow{color:var(--yellow);border-color:rgba(234,179,8,.25)}
.stat-badge.blue{color:var(--blue);border-color:rgba(59,130,246,.25)}
.live-dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 1.5s infinite;display:inline-block}
/* ── Tabs (in header) ── */
.header-tabs{display:flex;gap:0;margin-left:18px}
.tab-btn{padding:6px 14px;font-size:11px;font-weight:600;color:var(--text3);background:transparent;border:1px solid transparent;border-radius:7px;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:4px;white-space:nowrap}
.tab-btn:hover{color:var(--text2);background:var(--surface2)}
.tab-btn.active{color:var(--cyan);background:var(--surface2);border-color:var(--border)}
.tab-content{display:none}
.tab-content.active{display:block}
/* ── Common ── */
.main{padding:16px 28px;display:flex;flex-direction:column;gap:16px}
.panel{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.panel.full{grid-column:1/-1}
.panel-head{padding:8px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--surface2)}
.panel-head h3{font-size:11px;font-weight:700;display:flex;align-items:center;gap:5px}
.panel-head .cnt{font-size:10px;color:var(--text3);font-family:monospace}
.panel-head a,.panel-head .link{font-size:10px;color:var(--cyan);text-decoration:none;cursor:pointer}
.panel-head a:hover,.panel-head .link:hover{text-decoration:underline}
.panel-head .sub{font-size:10px;color:var(--text3)}
.panel-body{max-height:260px;overflow-y:auto}
.panel-body::-webkit-scrollbar{width:5px}
.panel-body::-webkit-scrollbar-track{background:var(--surface2)}
.panel-body::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
.empty-msg{padding:18px;text-align:center;color:var(--text3);font-size:11px}
/* ══════ 骨架屏加载动画 ══════ */
.skeleton{background:linear-gradient(90deg,var(--surface2) 0%,var(--surface3) 50%,var(--surface2) 100%);background-size:200% 100%;animation:skeleton-loading 1.5s ease-in-out infinite;border-radius:6px}
@keyframes skeleton-loading{0%{background-position:200% 0}100%{background-position:-200% 0}}
.skeleton-panel{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:10px}
.skeleton-header{height:40px;margin-bottom:12px;border-radius:10px}
.skeleton-row{height:48px;margin-bottom:8px;border-radius:8px}
.skeleton-card{height:120px;border-radius:10px}
.skeleton-stat{height:80px;border-radius:10px}
/* ══════ TAB 1: 总览 ══════ */
.col-toggles{display:flex;gap:6px;margin-bottom:12px}
.col-toggle{padding:6px 14px;border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--text2);transition:all .15s;display:flex;align-items:center;gap:6px}
.col-toggle:hover{background:var(--surface2)}
.col-toggle.active{border-color:var(--cyan);color:var(--cyan);background:rgba(78,205,196,.08)}
.col-toggle .tog-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.col-toggle .tog-dot.on{background:var(--green);box-shadow:0 0 4px rgba(34,197,94,.4)}
.col-toggle .tog-dot.off{background:var(--text3)}
.bot-columns{display:flex;gap:14px;align-items:flex-start}
.bot-column{flex:1;min-width:0;display:flex;flex-direction:column;gap:10px;transition:flex .25s ease,opacity .2s ease}
.bot-column.hidden{display:none}
.col-header{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--surface);border:1px solid var(--border);border-radius:10px}
.col-header .col-icon{font-size:20px;flex-shrink:0}
.col-header h2{font-size:13px;font-weight:700}
.col-header .col-role{font-size:9px;color:var(--text2);margin-left:2px}
.col-header .col-actions{margin-left:auto;display:flex;align-items:center;gap:6px}
.col-header .col-status{display:flex;align-items:center;gap:4px}
.col-header .col-dot{width:7px;height:7px;border-radius:50%}
.col-header .col-dot.healthy{background:var(--green);box-shadow:0 0 5px rgba(34,197,94,.4)}
.col-header .col-dot.unhealthy{background:var(--red)}
.col-header .col-dot.unknown{background:var(--text3)}
.col-header .col-status-text{font-size:9px;color:var(--text3)}
.restart-btn{border:1px solid rgba(239,68,68,.25);background:rgba(239,68,68,.08);color:var(--red);border-radius:8px;padding:4px 10px;font-size:10px;font-weight:700;cursor:pointer;transition:all .12s;white-space:nowrap}
.restart-btn:hover{background:rgba(239,68,68,.14)}
.restart-btn:disabled{opacity:.65;cursor:wait}
.restart-btn.compact{padding:3px 8px;font-size:9px}
.cap-row{display:flex;flex-wrap:wrap;gap:3px;padding:0 2px}
.cap{padding:2px 7px;border-radius:5px;font-size:9px;background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
.t-row{padding:7px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px;font-size:11px;transition:background .1s;cursor:pointer}
.t-row:hover{background:var(--surface2)}
.t-row:last-child{border-bottom:none}
.t-num{color:var(--text3);font-family:monospace;min-width:20px;font-size:10px}
.t-title{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.t-badge{padding:1px 6px;border-radius:4px;font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap}
.t-badge.pending{background:rgba(234,179,8,.12);color:var(--yellow)}
.t-badge.in-progress{background:rgba(59,130,246,.12);color:var(--blue)}
.t-badge.done{background:rgba(34,197,94,.12);color:var(--green)}
.t-badge.blocked{background:rgba(239,68,68,.12);color:var(--red)}
.t-badge.dispatched{background:rgba(255,107,53,.12);color:var(--orange)}
.t-badge.accepted{background:rgba(78,205,196,.12);color:var(--cyan)}
.t-target{font-size:9px;color:var(--text3);white-space:nowrap}
.t-time{font-size:9px;color:var(--text3);white-space:nowrap}
.c-row{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;font-size:11px}
.c-row:last-child{border-bottom:none}
.c-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.c-dot.on{background:var(--green)}.c-dot.off{background:var(--text3)}
.c-info{flex:1;min-width:0}
.c-name{font-weight:600;font-size:11px}
.c-desc{font-size:9px;color:var(--text3);margin-top:1px}
.c-right{text-align:right;flex-shrink:0}
.c-countdown{font-size:12px;font-weight:800;color:var(--cyan);font-family:'SF Mono',Menlo,monospace;letter-spacing:.5px}
.c-countdown.soon{color:var(--green);animation:pulse 1s infinite}
.c-last{font-size:9px;color:var(--text3)}
.commit-row{padding:7px 12px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;gap:7px;font-size:10px;transition:background .1s}
.commit-row:hover{background:var(--surface2)}
.commit-row:last-child{border-bottom:none}
.commit-sha{font-family:'SF Mono',Menlo,monospace;font-size:9px;color:var(--cyan);background:var(--surface3);padding:1px 4px;border-radius:3px;flex-shrink:0;text-decoration:none}
.commit-sha:hover{color:var(--text);background:var(--border)}
.commit-msg{flex:1;color:var(--text2);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.commit-msg .cm-title{color:var(--text);font-weight:600}
.commit-time{font-size:9px;color:var(--text3);white-space:nowrap;flex-shrink:0}
.skill-row{padding:6px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:7px;font-size:11px;transition:background .1s}
.skill-row:hover{background:var(--surface2)}
.skill-row:last-child{border-bottom:none}
.skill-icon{font-size:13px;flex-shrink:0}
.skill-name{flex:1;color:var(--text);font-weight:500;min-width:0}
.skill-desc{font-size:9px;color:var(--text3);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.skill-link{font-size:9px;color:var(--cyan);text-decoration:none}
.skill-link:hover{text-decoration:underline}
.skill-ver{font-size:8px;color:var(--text3);font-weight:400;margin-left:3px}
.mini-stats{display:flex;gap:4px;padding:0 12px 8px}
.ms{flex:1;text-align:center;padding:5px 0;border-radius:6px;background:var(--surface3)}
.ms .n{font-size:14px;font-weight:800}
.ms .l{font-size:8px;color:var(--text3);margin-top:1px}
.ms.p .n{color:var(--yellow)}.ms.w .n{color:var(--blue)}.ms.d .n{color:var(--green)}.ms.b .n{color:var(--red)}
/* Sub-page */
.sub-page{display:none;padding:16px 28px}
.sub-page.active{display:block}
.main.hidden{display:none}
.back-btn{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--cyan);cursor:pointer;padding:5px 10px;border-radius:7px;background:var(--surface);border:1px solid var(--border);margin-bottom:14px;transition:background .1s}
.back-btn:hover{background:var(--surface2)}
.detail-hero{display:flex;align-items:center;gap:14px;padding:16px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px}
.detail-avatar{width:56px;height:56px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:32px}
.detail-info h2{font-size:20px;font-weight:800}
.detail-info .role{font-size:12px;color:var(--text2);margin-top:2px}
.detail-repos{display:flex;gap:6px;margin-top:6px}
.detail-repos a{font-size:10px;color:var(--cyan);text-decoration:none;padding:2px 7px;border-radius:5px;background:var(--surface3);border:1px solid var(--border)}
.detail-repos a:hover{background:var(--border)}
.detail-status{margin-left:auto;text-align:right}
.detail-status .big-dot{width:12px;height:12px;border-radius:50%;display:inline-block;vertical-align:middle;margin-right:5px}
.detail-status .big-dot.healthy{background:var(--green);box-shadow:0 0 8px rgba(34,197,94,.5)}
.detail-status .big-dot.unhealthy{background:var(--red)}
.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.detail-grid .panel-body{max-height:400px}
/* Modal */
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200;justify-content:center;align-items:center}
.modal-overlay.active{display:flex}
.modal{background:var(--surface);border:1px solid var(--border);border-radius:14px;width:90%;max-width:680px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column}
.modal-head{padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--surface2)}
.modal-head h2{font-size:14px;font-weight:700}
.modal-close{width:26px;height:26px;border:none;background:var(--surface3);border-radius:6px;color:var(--text2);cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center}
.modal-close:hover{background:var(--border);color:var(--text)}
.modal-body{padding:16px;overflow-y:auto;flex:1}
.modal-body h3{font-size:12px;font-weight:700;color:var(--text2);margin:14px 0 6px;padding-top:10px;border-top:1px solid var(--border)}
.modal-body h3:first-child{margin-top:0;padding-top:0;border-top:none}
.modal-body pre{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:10px;font-size:11px;line-height:1.5;overflow-x:auto;white-space:pre-wrap;word-break:break-all;color:var(--text2);margin:6px 0}
.comment-item{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:10px;margin:6px 0}
.comment-meta{font-size:9px;color:var(--text3);margin-bottom:4px}
.comment-body{font-size:11px;color:var(--text2);line-height:1.5;white-space:pre-wrap;word-break:break-all}
/* ══════ TAB 2: 监控 ══════ */
.mon-status-bar{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.status-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px;text-align:center}
.status-card .label{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.8px}
.status-card .value{font-size:24px;font-weight:800;margin-top:3px}
.status-card .sub2{font-size:10px;color:var(--text3);margin-top:1px}
.status-card.ok .value{color:var(--green)}
.status-card.warn .value{color:var(--yellow)}
.status-card.err .value{color:var(--red)}
.status-card.info .value{color:var(--cyan)}
.mon-panels{display:grid;grid-template-columns:1fr 1fr;gap:14px}
/* Chat */
.chat-body{max-height:500px;overflow-y:auto;padding:10px 0}
.chat-msg{padding:7px 14px;margin:3px 0}
.chat-msg.user{border-left:3px solid var(--blue)}
.chat-msg.assistant{border-left:3px solid var(--green)}
.chat-role{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;margin-bottom:2px;display:flex;align-items:center;gap:5px}
.chat-msg.user .chat-role{color:var(--blue)}
.chat-msg.assistant .chat-role{color:var(--green)}
.chat-role .chat-time{font-weight:400;color:var(--text3);letter-spacing:0}
.chat-text{font-size:11px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-word}
.chat-text.collapsed{max-height:100px;overflow:hidden;position:relative}
.chat-text.collapsed::after{content:'';position:absolute;bottom:0;left:0;right:0;height:35px;background:linear-gradient(transparent,var(--chat-fade))}
.chat-expand{font-size:9px;color:var(--cyan);cursor:pointer;margin-top:3px;display:inline-block}
.chat-expand:hover{text-decoration:underline}
.think-block{margin:3px 0 6px;padding:7px 10px;border-radius:7px;background:var(--think-bg);border:1px solid var(--think-border);font-size:10px;line-height:1.5;color:var(--yellow);white-space:pre-wrap;word-break:break-word}
.think-block.collapsed{max-height:70px;overflow:hidden;position:relative}
.think-block.collapsed::after{content:'';position:absolute;bottom:0;left:0;right:0;height:28px;background:linear-gradient(transparent,var(--chat-fade2))}
.think-label{font-size:8px;font-weight:700;color:var(--yellow);opacity:.7;margin-bottom:2px;display:flex;align-items:center;gap:4px}
/* Log */
.log-entry{padding:5px 12px;border-bottom:1px solid var(--border);font-size:10px;font-family:'SF Mono',Menlo,monospace;display:flex;gap:6px;line-height:1.5}
.log-entry:last-child{border-bottom:none}
.log-time{color:var(--text3);flex-shrink:0;min-width:54px}
.log-tag{padding:0 4px;border-radius:3px;font-size:8px;font-weight:700;flex-shrink:0;text-align:center;min-width:30px;line-height:16px}
.log-tag.incoming{background:rgba(59,130,246,.15);color:var(--blue)}
.log-tag.processing{background:rgba(234,179,8,.15);color:var(--yellow)}
.log-tag.complete{background:rgba(34,197,94,.15);color:var(--green)}
.log-tag.streaming{background:rgba(167,139,250,.15);color:var(--purple)}
.log-tag.stream_done{background:rgba(78,205,196,.15);color:var(--cyan)}
.log-tag.warn{background:rgba(234,179,8,.15);color:var(--yellow)}
.log-tag.error{background:rgba(239,68,68,.15);color:var(--red)}
.log-content{flex:1;color:var(--text2);word-break:break-all}
/* Timeline */
.tl-entry{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;font-size:11px}
.tl-entry:last-child{border-bottom:none}
.tl-status{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.tl-status.done{background:var(--green)}
.tl-status.thinking{background:var(--yellow);animation:pulse 1s infinite}
.tl-status.streaming{background:var(--purple);animation:pulse .7s infinite}
.tl-status.queued{background:var(--text3)}
.tl-msg{flex:1;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.tl-dur{font-family:'SF Mono',Menlo,monospace;font-size:10px;font-weight:700;flex-shrink:0;min-width:36px;text-align:right}
.tl-dur.fast{color:var(--green)}.tl-dur.medium{color:var(--yellow)}.tl-dur.slow{color:var(--orange)}
.tl-time{font-size:9px;color:var(--text3);flex-shrink:0}
.tl-entry.active-msg{background:rgba(234,179,8,.06);border-left:2px solid var(--yellow)}
/* Workers / Cron (monitor tab) */
.worker-row{display:flex;gap:10px;padding:8px 12px}
.worker-card{flex:1;background:var(--surface3);border-radius:8px;padding:10px;display:flex;align-items:center;gap:8px}
.worker-card .w-avatar{font-size:20px}
.worker-card .w-info{flex:1}
.worker-card .w-name{font-size:12px;font-weight:700}
.worker-card .w-sub{font-size:9px;color:var(--text3)}
.worker-card .w-dot{width:7px;height:7px;border-radius:50%;display:inline-block}
.worker-card .w-dot.on{background:var(--green)}.worker-card .w-dot.off{background:var(--red)}
.worker-card .w-poll{font-size:9px;color:var(--cyan);font-family:monospace;margin-top:1px}
.mcron-entry{padding:7px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;font-size:11px}
.mcron-entry:last-child{border-bottom:none}
.mcron-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.mcron-dot.on{background:var(--green)}.mcron-dot.off{background:var(--text3)}
.mcron-name{flex:1;font-weight:600}
.mcron-next{font-family:monospace;font-size:10px;color:var(--cyan)}
/* System */
.meter-row{display:flex;gap:14px;padding:12px}
.meter{flex:1;text-align:center}
.meter .m-label{font-size:9px;color:var(--text3);margin-bottom:5px}
.meter .m-bar{height:5px;border-radius:3px;background:var(--surface3);overflow:hidden}
.meter .m-fill{height:100%;border-radius:3px;transition:width .5s}
.meter .m-val{font-size:13px;font-weight:800;margin-top:3px}
/* Turn status badges */
.turn-badge{font-size:9px;padding:1px 6px;border-radius:4px;margin-left:6px;font-weight:600;vertical-align:middle}
.turn-badge.final{background:rgba(34,197,94,.15);color:var(--green)}
.turn-badge.working{background:rgba(234,179,8,.15);color:var(--yellow)}
.turn-badge.text-only{background:rgba(239,68,68,.15);color:var(--red)}
.status-card.err{border-color:rgba(239,68,68,.3)}
.status-card.err .value{color:var(--red)}
/* Theme toggle */
.theme-btn{width:30px;height:30px;border-radius:8px;border:1px solid var(--border);background:var(--surface2);cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:background .15s}
.theme-btn:hover{background:var(--surface3)}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
.loading{animation:pulse 1.2s infinite}
@media(max-width:1100px){.bot-columns{flex-direction:column}.detail-grid,.mon-panels{grid-template-columns:1fr}.mon-status-bar{grid-template-columns:repeat(2,1fr)}}
/* ══════════════════════════════════════════════════════════
App Store 风格 UI —— macOS 桌面端左右布局
通过 body.app-mode 激活
══════════════════════════════════════════════════════════ */
/* ── 布局切换按钮 ── */
.layout-btn{width:30px;height:30px;border-radius:8px;border:1px solid var(--border);background:var(--surface2);cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:background .15s}
.layout-btn:hover{background:var(--surface3)}
/* ── 过渡动画 ── */
body{transition:background .3s}
.tab-content,.main,.header,.panel,.bot-column,.sub-page{transition:all .3s ease}
/* ── App 模式:全局控制 ── */
body.app-mode{background:var(--bg)}
body.app-mode .app-container{display:flex}
body:not(.app-mode) .app-container{display:none}
body.app-mode #tab-overview > .main,
body.app-mode #tab-overview > .sub-page,
body.app-mode #tab-monitor > .main{display:none!important}
/* ── App 模式: Header ── */
body.app-mode .header{
padding:10px 20px;
background:var(--surface);
backdrop-filter:saturate(180%) blur(20px);
-webkit-backdrop-filter:saturate(180%) blur(20px);
border-bottom:1px solid var(--border);
}
/* ══ App 容器:左右双栏 ══ */
.app-container{
display:flex;height:calc(100vh - 51px);overflow:hidden;
}
/* ── 左侧边栏 ── */
.app-sidebar{
width:220px;flex-shrink:0;
background:var(--surface);border-right:1px solid var(--border);
overflow-y:auto;padding:16px 0;
display:flex;flex-direction:column;
}
.app-sidebar-section{padding:0 12px;margin-bottom:16px}
.app-sidebar-label{
font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;
letter-spacing:1px;padding:0 8px;margin-bottom:6px;
}
.app-sidebar-item{
display:flex;align-items:center;gap:10px;
padding:8px 12px;border-radius:8px;cursor:pointer;
font-size:13px;font-weight:500;color:var(--text2);
transition:all .12s;margin-bottom:2px;
}
.app-sidebar-item:hover{background:var(--surface2);color:var(--text)}
.app-sidebar-item.active{background:var(--surface3);color:var(--text);font-weight:600}
.app-sidebar-item .si-icon{font-size:18px;width:24px;text-align:center;flex-shrink:0}
.app-sidebar-item .si-name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.app-sidebar-item .si-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.app-sidebar-item .si-dot.on{background:var(--green);box-shadow:0 0 4px rgba(34,197,94,.5)}
.app-sidebar-item .si-dot.off{background:var(--red)}
.app-sidebar-item .si-badge{
font-size:9px;font-weight:700;padding:1px 6px;border-radius:10px;
background:var(--surface3);color:var(--text3);min-width:18px;text-align:center;
}
.app-sidebar-divider{height:1px;background:var(--border);margin:8px 12px}
/* ── 右侧主内容区 ── */
.app-content{
flex:1;overflow-y:auto;padding:24px 28px;min-width:0;
}
/* ── 区域标题 ── */
.app-section-title{
font-size:22px;font-weight:800;margin:28px 0 14px;
color:var(--text);letter-spacing:-.3px;
}
.app-section-title:first-child{margin-top:0}
.app-section-sub{
font-size:13px;color:var(--text3);font-weight:500;margin:-8px 0 14px;
}
/* ══════ Bot 卡片行( App Store 顶部 App 列表风格) ══════ */
.app-listing-row{
display:flex;align-items:center;gap:14px;
padding:14px 16px;border-radius:12px;
background:var(--surface);border:1px solid var(--border);
cursor:pointer;transition:all .15s;margin-bottom:10px;
}
.app-listing-row:hover{background:var(--surface2);box-shadow:0 2px 12px rgba(0,0,0,.08)}
.app-listing-icon{
width:56px;height:56px;border-radius:14px;
display:flex;align-items:center;justify-content:center;
font-size:30px;flex-shrink:0;
}
.app-listing-info{flex:1;min-width:0}
.app-listing-name{font-size:15px;font-weight:700}
.app-listing-role{font-size:11px;color:var(--text3);margin-top:1px}
.app-listing-caps{display:flex;flex-wrap:wrap;gap:3px;margin-top:4px}
.app-listing-caps .cap{padding:1px 6px;border-radius:6px;font-size:8px;background:var(--surface3);color:var(--text3);border:1px solid var(--border)}
.app-listing-right{display:flex;flex-direction:column;align-items:center;gap:4px;flex-shrink:0}
.app-listing-btn{
padding:5px 16px;border-radius:16px;font-size:12px;font-weight:700;
border:none;cursor:pointer;transition:all .12s;
}
.app-listing-btn.online{background:rgba(34,197,94,.15);color:var(--green)}
.app-listing-btn.offline{background:rgba(239,68,68,.12);color:var(--red)}
.app-listing-btn:hover{filter:brightness(1.1)}
.app-listing-stats{display:flex;gap:8px;font-size:9px;color:var(--text3)}
.app-listing-stats span{display:flex;align-items:center;gap:2px}
/* ══════ 2 列特色大卡片网格 ══════ */
.app-feature-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.app-feature-card{
background:var(--surface);border:1px solid var(--border);border-radius:16px;
overflow:hidden;transition:transform .15s,box-shadow .15s;
}
.app-feature-card:hover{transform:translateY(-2px);box-shadow:0 6px 24px rgba(0,0,0,.12)}
.app-feature-card.full{grid-column:1/-1}
.app-feature-head{
padding:12px 16px;display:flex;align-items:center;justify-content:space-between;
border-bottom:1px solid var(--border);background:var(--surface2);
}
.app-feature-head h3{font-size:13px;font-weight:700;display:flex;align-items:center;gap:6px}
.app-feature-head .af-link{font-size:11px;color:var(--cyan);cursor:pointer;text-decoration:none}
.app-feature-head .af-link:hover{text-decoration:underline}
.app-feature-head .af-cnt{font-size:10px;color:var(--text3);font-family:monospace}
.app-feature-body{padding:12px 16px;max-height:360px;overflow-y:auto}
/* ── 任务行(在特色卡内) ── */
.app-task-row{
padding:8px 0;border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:8px;font-size:12px;
cursor:pointer;transition:background .1s;
}
.app-task-row:last-child{border-bottom:none}
.app-task-row:hover{background:var(--surface2);border-radius:6px;padding-left:6px;margin-left:-6px}
.app-task-num{font-family:'SF Mono',Menlo,monospace;font-size:10px;color:var(--text3);min-width:28px}
.app-task-title{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
.app-task-badge{padding:2px 8px;border-radius:10px;font-size:9px;font-weight:700}
.app-task-badge.pending{background:rgba(234,179,8,.12);color:var(--yellow)}
.app-task-badge.in-progress{background:rgba(59,130,246,.12);color:var(--blue)}
.app-task-badge.done{background:rgba(34,197,94,.12);color:var(--green)}
.app-task-badge.blocked{background:rgba(239,68,68,.12);color:var(--red)}
.app-task-badge.dispatched{background:rgba(255,107,53,.12);color:var(--orange)}
.app-task-badge.accepted{background:rgba(78,205,196,.12);color:var(--cyan)}
.app-task-target{font-size:10px;color:var(--text3);white-space:nowrap}
.app-task-time{font-size:10px;color:var(--text3);white-space:nowrap}
/* ── 提交行 ── */
.app-commit-row{
padding:8px 0;border-bottom:1px solid var(--border);
display:flex;align-items:flex-start;gap:8px;font-size:11px;
}
.app-commit-row:last-child{border-bottom:none}
.app-commit-sha{
font-family:'SF Mono',Menlo,monospace;font-size:10px;
color:var(--cyan);background:var(--surface3);
padding:2px 6px;border-radius:4px;flex-shrink:0;text-decoration:none;
}
.app-commit-sha:hover{color:var(--text);background:var(--border)}
.app-commit-msg{flex:1;color:var(--text2);line-height:1.4;font-weight:500;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.app-commit-time{font-size:10px;color:var(--text3);white-space:nowrap;flex-shrink:0}
/* ── Cron 行 ── */
.app-cron-row{
padding:8px 0;border-bottom:1px solid var(--border);
display:flex;align-items:center;gap:10px;
}
.app-cron-row:last-child{border-bottom:none}
.app-cron-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.app-cron-dot.on{background:var(--green)}.app-cron-dot.off{background:var(--text3)}
.app-cron-info{flex:1;min-width:0}
.app-cron-name{font-size:12px;font-weight:600}
.app-cron-desc{font-size:10px;color:var(--text3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.app-cron-right{text-align:right;flex-shrink:0}
.app-cron-countdown{font-size:14px;font-weight:800;color:var(--cyan);font-family:'SF Mono',Menlo,monospace}
.app-cron-countdown.soon{color:var(--green);animation:pulse 1s infinite}
.app-cron-countdown.paused{color:var(--text3);font-size:11px;font-weight:600}
.app-cron-last{font-size:9px;color:var(--text3);margin-top:1px}
/* ══════ 工具 & 技能网格 ══════ */
.app-tools-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:10px}
.app-tool-card{
background:var(--surface);border:1px solid var(--border);border-radius:14px;
padding:12px 8px;display:flex;flex-direction:column;align-items:center;
gap:5px;text-align:center;transition:transform .15s,box-shadow .15s;
}
.app-tool-card:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.1)}
.app-tool-icon{
width:40px;height:40px;border-radius:10px;
display:flex;align-items:center;justify-content:center;
font-size:20px;background:var(--surface3);
}
.app-tool-name{font-size:10px;font-weight:600;line-height:1.3;word-break:break-all}
.app-tool-ver{font-size:8px;color:var(--text3)}
.app-skills-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
.app-skill-card{
background:var(--surface);border:1px solid var(--border);border-radius:14px;
padding:12px;display:flex;align-items:flex-start;gap:10px;
transition:transform .15s,box-shadow .15s;
}
.app-skill-card:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.1)}
.app-skill-icon{
width:36px;height:36px;border-radius:9px;flex-shrink:0;
display:flex;align-items:center;justify-content:center;
font-size:18px;background:var(--surface3);
}
.app-skill-info{flex:1;min-width:0}
.app-skill-name{font-size:11px;font-weight:700;line-height:1.3}
.app-skill-desc{font-size:9px;color:var(--text3);margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
/* ══════ 监控页 ══════ */
.app-status-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.app-status-card{
background:var(--surface);border:1px solid var(--border);border-radius:14px;
padding:16px;text-align:center;transition:transform .15s;
}
.app-status-card:hover{transform:scale(1.02)}
.app-status-card .as-label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.8px}
.app-status-card .as-value{font-size:28px;font-weight:800;margin-top:4px}
.app-status-card .as-sub{font-size:10px;color:var(--text3);margin-top:2px}
.app-status-card.ok .as-value{color:var(--green)}
.app-status-card.warn .as-value{color:var(--yellow)}
.app-status-card.err .as-value{color:var(--red)}
.app-status-card.info .as-value{color:var(--cyan)}
/* 对话 + 日志 + 时间线 */
.app-feature-body:has(.app-chat-body){max-height:none;overflow:visible}
.app-chat-body{display:flex;flex-direction:column;gap:8px;padding:8px 0;max-height:500px;overflow-y:auto}
.app-chat-bubble{
max-width:75%;padding:10px 14px;border-radius:18px;
font-size:12px;line-height:1.6;word-break:break-word;white-space:pre-wrap;
}
.app-chat-bubble.user{align-self:flex-end;background:var(--blue);color:#fff;border-bottom-right-radius:4px}
.app-chat-bubble.assistant{align-self:flex-start;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px}
.app-chat-sender{font-size:9px;font-weight:700;margin-bottom:3px;opacity:.7}
.app-chat-bubble.user .app-chat-sender{color:rgba(255,255,255,.8)}
.app-chat-bubble.assistant .app-chat-sender{color:var(--text3)}
.app-chat-time-label{font-size:9px;color:var(--text3);text-align:center;margin:6px 0}
.app-chat-bubble .app-think-inline{
margin:4px 0 6px;padding:6px 10px;border-radius:10px;
background:var(--think-bg);border:1px solid var(--think-border);
font-size:10px;color:var(--yellow);line-height:1.5;
}
.app-chat-bubble .app-think-inline.collapsed{max-height:60px;overflow:hidden;position:relative}
.app-chat-bubble .app-think-inline.collapsed::after{
content:'';position:absolute;bottom:0;left:0;right:0;height:24px;
background:linear-gradient(transparent,var(--surface2));
}
.app-chat-turn-badge{display:inline-block;font-size:8px;padding:1px 6px;border-radius:8px;margin-left:4px;font-weight:700;vertical-align:middle}
.app-chat-turn-badge.final{background:rgba(34,197,94,.15);color:var(--green)}
.app-chat-turn-badge.working{background:rgba(234,179,8,.15);color:var(--yellow)}
.app-chat-turn-badge.text-only{background:rgba(239,68,68,.15);color:var(--red)}
.app-log-list{display:flex;flex-direction:column;gap:1px;padding:4px 0}
.app-log-entry{padding:6px 0;display:flex;gap:8px;align-items:flex-start;font-size:11px;font-family:'SF Mono',Menlo,monospace}
.app-log-entry:hover{background:var(--surface2);border-radius:4px}
.app-log-time{color:var(--text3);flex-shrink:0;font-size:10px}
.app-log-tag{padding:1px 5px;border-radius:4px;font-size:9px;font-weight:700;flex-shrink:0;min-width:30px;text-align:center}
.app-log-tag.incoming{background:rgba(59,130,246,.15);color:var(--blue)}
.app-log-tag.processing{background:rgba(234,179,8,.15);color:var(--yellow)}
.app-log-tag.complete{background:rgba(34,197,94,.15);color:var(--green)}
.app-log-tag.streaming{background:rgba(167,139,250,.15);color:var(--purple)}
.app-log-tag.stream_done{background:rgba(78,205,196,.15);color:var(--cyan)}
.app-log-tag.warn{background:rgba(234,179,8,.15);color:var(--yellow)}
.app-log-tag.error{background:rgba(239,68,68,.15);color:var(--red)}
.app-log-content{flex:1;color:var(--text2);word-break:break-all;line-height:1.5}
.app-timeline{display:flex;flex-direction:column;gap:0;padding:4px 0}
.app-tl-entry{padding:8px 0;display:flex;align-items:center;gap:8px;font-size:12px}
.app-tl-entry:hover{background:var(--surface2);border-radius:4px}
.app-tl-entry.active-msg{background:rgba(234,179,8,.06);border-left:3px solid var(--yellow);padding-left:6px}
.app-tl-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.app-tl-dot.done{background:var(--green)}
.app-tl-dot.thinking{background:var(--yellow);animation:pulse 1s infinite}
.app-tl-dot.streaming{background:var(--purple);animation:pulse .7s infinite}
.app-tl-dot.queued{background:var(--text3)}
.app-tl-msg{flex:1;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.app-tl-dur{font-family:'SF Mono',Menlo,monospace;font-size:10px;font-weight:700;flex-shrink:0;min-width:36px;text-align:right}
.app-tl-dur.fast{color:var(--green)}.app-tl-dur.medium{color:var(--yellow)}.app-tl-dur.slow{color:var(--orange)}
.app-tl-time{font-size:10px;color:var(--text3);flex-shrink:0}
/* 圆形进度环 */
.app-resource-row{display:flex;gap:24px;justify-content:flex-start;padding:10px 0}
.app-resource-ring{display:flex;flex-direction:column;align-items:center;gap:6px}
.app-ring-svg{width:72px;height:72px;transform:rotate(-90deg)}
.app-ring-bg{fill:none;stroke:var(--surface3);stroke-width:5}
.app-ring-fill{fill:none;stroke-width:5;stroke-linecap:round;transition:stroke-dashoffset .6s ease}
.app-ring-label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.5px}
.app-ring-val{font-size:16px;font-weight:800}
/* Worker 卡 */
.app-worker-list{display:flex;gap:12px;flex-wrap:wrap}
.app-worker-card{
flex:1;min-width:200px;
background:var(--surface);border:1px solid var(--border);border-radius:14px;
padding:14px 16px;display:flex;align-items:center;gap:12px;
cursor:pointer;transition:all .15s;
}
.app-worker-card:hover{background:var(--surface2);box-shadow:0 2px 12px rgba(0,0,0,.08)}
.app-worker-avatar{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px}
.app-worker-info{flex:1;min-width:0}
.app-worker-name{font-size:13px;font-weight:700}
.app-worker-sub{font-size:10px;color:var(--text3);margin-top:1px}
.app-worker-status{display:flex;align-items:center;gap:5px}
.app-worker-dot{width:7px;height:7px;border-radius:50%}
.app-worker-dot.on{background:var(--green);box-shadow:0 0 4px rgba(34,197,94,.4)}
.app-worker-dot.off{background:var(--red)}
/* ── 空状态 ── */
.app-empty{padding:24px;text-align:center;color:var(--text3);font-size:12px}
/* ── 响应式 ── */
@media(max-width:900px){
.app-sidebar{width:180px}
.app-content{padding:16px 18px}
.app-feature-grid{grid-template-columns:1fr}
.app-status-grid{grid-template-columns:repeat(2,1fr)}
}
@media(max-width:600px){
.app-container{flex-direction:column}
.app-sidebar{width:100%;height:auto;max-height:140px;border-right:none;border-bottom:1px solid var(--border);flex-direction:row;overflow-x:auto;padding:8px}
.app-sidebar-section{display:flex;gap:4px;margin:0}
.app-sidebar-label{display:none}
.app-sidebar-divider{display:none}
.app-content{height:auto;overflow-y:visible}
}
< / style >
< / head >
< body >
< div class = "header" >
< h1 onclick = "goHome()" > < span > 🦞< / span > AI Team 调度中心< / h1 >
< div class = "header-tabs" id = "tabBar" >
< button class = "tab-btn active" onclick = "switchTab('overview')" > 📊 总览面板< / button >
< button class = "tab-btn" onclick = "switchTab('monitor')" > 📡 实时监控< / button >
< button class = "tab-btn" onclick = "switchTab('stats')" > 📈 数据统计< / button >
< button class = "tab-btn" onclick = "switchTab('memory')" > 🧠 每日记忆< / button >
< / div >
< div class = "header-right" >
< div class = "stats-bar" id = "globalStats" > < / div >
< span class = "live-dot" > < / span >
< button class = "layout-btn" onclick = "toggleLayoutMode()" id = "layoutBtn" title = "切换布局" > 💻< / button >
< button class = "theme-btn" onclick = "toggleTheme()" id = "themeBtn" title = "切换主题" > 🌙< / button >
< div class = "refresh-info" id = "refreshInfo" > 加载中...< / div >
< / div >
< / div >
<!-- ═══ TAB 1: 总览 ═══ -->
< div class = "tab-content active" id = "tab-overview" >
< div class = "main" id = "mainView" >
< div class = "col-toggles" id = "colToggles" > < / div >
< div class = "bot-columns" id = "botColumns" > < div class = "empty-msg loading" > 加载数据中...< / div > < / div >
< / div >
< div class = "sub-page" id = "botDetailPage" > < / div >
< div class = "app-container" id = "appOverview" > < / div >
< / div >
<!-- ═══ TAB 2: 监控 ═══ -->
< div class = "tab-content" id = "tab-monitor" >
< div class = "main" >
< div class = "col-toggles" id = "monColToggles" > < / div >
< div class = "bot-columns" id = "monColumns" > < / div >
< div id = "monitorContent" style = "margin-top:14px" > < / div >
< / div >
< div class = "app-container" id = "appMonitor" > < / div >
< / div >
<!-- ═══ TAB 3: 数据统计 ═══ -->
< div class = "tab-content" id = "tab-stats" >
< div class = "main" >
< div style = "margin-bottom:16px" >
< h2 style = "font-size:18px;font-weight:800;margin-bottom:6px" > 📈 API 调用统计 & Token 监控< / h2 >
< p style = "font-size:11px;color:var(--text3)" > 实时监控各 Bot 的 API 调用次数和 Token 使用量< / p >
< / div >
< div id = "statsCards" > < / div >
< div id = "statsContent" > < / div >
< / div >
< div class = "app-container" id = "appStats" > < / div >
< / div >
<!-- ═══ TAB 4: 每日记忆 ═══ -->
< div class = "tab-content" id = "tab-memory" >
< div class = "main" >
< div style = "margin-bottom:16px;display:flex;align-items:center;justify-content:space-between" >
< div >
< h2 style = "font-size:18px;font-weight:800;margin-bottom:6px" > 🧠 每日记忆 & 成长复盘< / h2 >
< p style = "font-size:11px;color:var(--text3)" > 记录每个机器人每天的学习内容、成就和挑战,方便复盘与成长< / p >
< / div >
< div style = "display:flex;gap:8px;align-items:center" >
< label style = "font-size:11px;color:var(--text3)" > 选择日期:< / label >
< input type = "date" id = "memoryDatePicker" style = "padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface2);color:var(--text);font-size:11px;cursor:pointer" / >
< button onclick = "loadMemoryByDate()" style = "padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font-size:11px;cursor:pointer;font-weight:600;transition:background .15s" onmouseover = "this.style.background='var(--surface2)'" onmouseout = "this.style.background='var(--surface)'" > 查看< / button >
< button onclick = "loadTodayMemory()" style = "padding:6px 14px;border:1px solid var(--cyan);border-radius:6px;background:rgba(78,205,196,.08);color:var(--cyan);font-size:11px;cursor:pointer;font-weight:600;transition:all .15s" onmouseover = "this.style.background='rgba(78,205,196,.15)'" onmouseout = "this.style.background='rgba(78,205,196,.08)'" > 今天< / button >
< / div >
< / div >
< div id = "memoryContent" > < / div >
< / div >
< div class = "app-container" id = "appMemory" > < / div >
< / div >
<!-- ═══ TAB 2: 监控 (continued system resources panel) ═══ -->
<!-- This content is part of tab - monitor, rendered dynamically -->
<!-- Modal -->
< div class = "modal-overlay" id = "taskModal" >
< div class = "modal" >
< div class = "modal-head" > < h2 id = "modalTitle" > 任务详情< / h2 > < button class = "modal-close" onclick = "closeModal()" > × < / button > < / div >
< div class = "modal-body" id = "modalBody" > < / div >
< / div >
< / div >
< script >
/* ═══════════ Shared ═══════════ */
const BD = {leader:{name:'大龙虾',avatar:'🦞',color:'#FF6B35'},qianwen:{name:'全栈高手',avatar:'⚡',color:'#4ECDC4'},kimi:{name:'智囊团',avatar:'🔬',color:'#A78BFA'}};
let ovData = null, monData = null, convData = null, statsData = null, memoryData = null;
let workerConvData = {};
let activeTab = 'overview';
let overviewView = 'home';
let expandedThink = {}, expandedReply = {};
let ovTimer = null, monTimer = null, statsTimer = null, memoryTimer = null;
let colVisible = {leader:true, qianwen:true, kimi:true};
let monColVisible = {leader:true, qianwen:true, kimi:true};
let workerLogs = {};
let webBotDetailId = null;
function switchTab(tab) {
activeTab = tab;
document.querySelectorAll('.tab-btn').forEach((b,i) => b.classList.toggle('active', (i===0&&tab==='overview')||(i===1&&tab==='monitor')||(i===2&&tab==='stats')||(i===3&&tab==='memory')));
document.getElementById('tab-overview').classList.toggle('active', tab==='overview');
document.getElementById('tab-monitor').classList.toggle('active', tab==='monitor');
document.getElementById('tab-stats').classList.toggle('active', tab==='stats');
document.getElementById('tab-memory').classList.toggle('active', tab==='memory');
clearInterval(ovTimer); clearInterval(monTimer); clearInterval(statsTimer); clearInterval(memoryTimer);
if (tab==='overview') {
if(!ovData){
if(isAppMode())showAppOverviewFramework();
else showOverviewFramework();
}
fetchOverview();
ovTimer = setInterval(fetchOverview, 30000);
}
else if (tab==='monitor') {
if(!monData){
if(isAppMode())showAppMonitorFramework();
else showMonitorFramework();
}
fetchMonitor();
monTimer = setInterval(fetchMonitor, 5000);
}
else if (tab==='stats') {
fetchStats();
statsTimer = setInterval(fetchStats, 10000);
}
else if (tab==='memory') {
fetchMemory(selectedMemoryDate);
memoryTimer = setInterval(() => fetchMemory(selectedMemoryDate), 30000);
}
}
/* ═══════════ 骨架屏加载动画 ═══════════ */
function showSkeletonOverview(){
const skeletonHTML = `
< div class = "bot-columns" >
${['leader','qianwen','kimi'].map(id=>{
const d=BD[id]||{};
return `< div class = "bot-column" id = "col-${id}" >
< div class = "skeleton skeleton-header" > < / div >
< div class = "skeleton-panel" >
${[1,2,3,4,5].map(()=>'< div class = "skeleton skeleton-row" > < / div > ').join('')}
< / div >
< div class = "skeleton-panel" >
${[1,2,3].map(()=>'< div class = "skeleton skeleton-row" > < / div > ').join('')}
< / div >
< / div > `;
}).join('')}
< / div >
`;
document.getElementById('botColumns').innerHTML = skeletonHTML;
}
function showSkeletonMonitor(){
const skeletonHTML = `
< div class = "mon-status-bar" >
${[1,2,3,4].map(()=>'< div class = "skeleton skeleton-stat" > < / div > ').join('')}
< / div >
< div class = "mon-panels" style = "margin-top:14px" >
< div class = "skeleton-panel" > < div class = "skeleton skeleton-card" > < / div > < / div >
< div class = "skeleton-panel" > < div class = "skeleton skeleton-card" > < / div > < / div >
< / div >
`;
document.getElementById('monitorContent').innerHTML = skeletonHTML;
}
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
function timeAgo(iso){if(!iso)return'--';const d=Date.now()-new Date(iso).getTime();if(d< 0 ) return ' 刚刚 ' ; const s = Math.floor(d/1000);if(s<60)return s + ' 秒前 ' ; const m = Math.floor(s/60);if(m<60)return m + ' 分钟前 ' ; const h = Math.floor(m/60);if(h<24)return h + ' 小时前 ' ; return Math . floor ( h / 24 ) + ' 天前 ' ; }
function fmtCountdown(ms){if(ms< =0)return'即将';const s=Math.floor(ms/1000);const m=Math.floor(s/60);if(m>=60)return Math.floor(m/60)+'时'+String(m%60).padStart(2,'0')+'分';return String(m).padStart(2,'0')+':'+String(s%60).padStart(2,'0');}
function fmtCountdownFull(ms){if(ms< =0)return'即将执行';const t=Math.floor(ms/1000);const m=Math.floor(t/60);const s=t%60;if(m>=60){const h=Math.floor(m/60);return h+'时'+String(m%60).padStart(2,'0')+'分';}return String(m).padStart(2,'0')+'分'+String(s).padStart(2,'0')+'秒';}
function fmtTime(iso){if(!iso)return'--:--';let d;if(typeof iso==='number')d=new Date(iso);else d=new Date(iso);if(isNaN(d.getTime()))return'--:--';return d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit'});}
function botName(id){return (BD[id]&&BD[id].name)||id; }
function restartButtonHtml(id, compact=false){return `< button class = "restart-btn${compact?' compact':''}" onclick = "restartBot('${id}', event)" title = "重启 ${botName(id)}" > 重启< / button > `;}
async function restartBot(id, event){
if(event){event.preventDefault();event.stopPropagation();}
const btn=event&&event.currentTarget?event.currentTarget:null;
if(btn&&btn.disabled)return;
const name=botName(id);
if(!confirm(`确认重启 ${name} 吗?\n当前任务可能会短暂中断。`))return;
const oldText=btn?btn.textContent:'';
if(btn){btn.disabled=true;btn.textContent='重启中...';}
try{
const r=await fetch(`/api/bot/${id}/restart`,{method:'POST'});
const data=await r.json().catch(()=>({}));
if(!r.ok||!data.ok)throw new Error(data.message||data.result?.stderr||'重启失败');
alert(data.message||`${name} 重启命令已发送`);
await new Promise(resolve=>setTimeout(resolve,1500));
await Promise.all([fetchOverview(),fetchMonitor()]);
if(activeTab==='overview'& & !isAppMode()& & overviewView==='detail'& & webBotDetailId===id)await openBotDetail(id,true);
if(isAppMode()& & appBotDetailId===id)await openAppBotDetail(id,true);
if(activeTab==='monitor'&&isAppMode()&&appMonitorBotId===id)renderAppMonitorBotDetail(id);
}catch(err){
alert(`重启失败:${err.message||err}`);
}finally{
if(btn){btn.disabled=false;btn.textContent=oldText||'重启';}
}
}
/* ═══════════ TAB 1: Overview ═══════════ */
2026-03-11 18:37:57 +08:00
async function fetchOverview(lite=false){
2026-03-11 11:37:35 +08:00
// 立即显示框架和骨架屏
if(!ovData) {
showOverviewFramework();
showSkeletonOverview();
}
try{
2026-03-11 18:37:57 +08:00
const r=await fetch(lite?'/api/status?lite=1':'/api/status');
2026-03-11 11:37:35 +08:00
ovData=await r.json();
if(activeTab==='overview'&&overviewView==='home')renderOverview();
}catch(e){
console.error(e);
}
}
function showOverviewFramework(){
// 立即显示3列框架, 让用户知道页面结构
const frameHTML = `
< div class = "bot-columns" >
${['leader','qianwen','kimi'].map(id=>{
const d=BD[id]||{};
const c=d.color||'#888';
const a=d.avatar||'🤖';
const n=d.name||id;
return `< div class = "bot-column" id = "col-${id}" >
< div class = "col-header" style = "border-left:3px solid ${c}" >
< span class = "col-icon" > ${a}< / span >
< div >
< h2 > ${n}< / h2 >
< div class = "col-role" > 加载中...< / div >
< / div >
< div class = "col-actions" >
< div class = "col-status" >
< span class = "col-status-text" > --< / span >
< span class = "col-dot unknown" > < / span >
< / div >
< / div >
< / div >
< div class = "panel" >
< div class = "panel-head" > < h3 > 📋 加载中...< / h3 > < / div >
< div class = "panel-body" > < div class = "empty-msg loading" > 获取数据中...< / div > < / div >
< / div >
< / div > `;
}).join('')}
< / div >
`;
document.getElementById('botColumns').innerHTML = frameHTML;
// 显示切换按钮
document.getElementById('colToggles').innerHTML = ['leader','qianwen','kimi'].map(id=>{
const d=BD[id]||{};
return`< button class = "col-toggle active" onclick = "toggleCol('${id}')" > < span class = "tog-dot unknown" > < / span > ${d.avatar||'🤖'} ${d.name||id}< / button > `;
}).join('');
// 显示顶部统计占位
document.getElementById('globalStats').innerHTML = `< span class = "stat-badge" > 加载中...< / span > `;
}
function toggleCol(id){colVisible[id]=!colVisible[id];renderOverview();}
function renderOverview(){
if(!ovData)return;
// App 模式下只渲染 App 版本
if(isAppMode()){
renderAppOverview();
return;
}
// Web 模式渲染
2026-03-11 18:37:57 +08:00
const isLoading=!!ovData.lite;
2026-03-11 11:37:35 +08:00
const s=ovData.stats;const on=ovData.bots.filter(b=>b.status.running).length;
2026-03-11 18:37:57 +08:00
document.getElementById('globalStats').innerHTML=isLoading
? `< span class = "stat-badge" > 加载中...< / span > `
: `< span class = "stat-badge green" > ${on}/${ovData.bots.length} 在线< / span > < span class = "stat-badge yellow" > ${s.openTasks} 待办< / span > < span class = "stat-badge blue" > ${s.doneTasks} 完成< / span > `;
2026-03-11 11:37:35 +08:00
document.getElementById('refreshInfo').textContent=`${new Date(ovData.timestamp).toLocaleTimeString('zh-CN')}`;
document.getElementById('colToggles').innerHTML=ovData.bots.map(b=>{const d=BD[b.id]||{};const vis=colVisible[b.id]!==false;const hc=b.status.running?'on':'off';return`< button class = "col-toggle${vis?' active':''}" onclick = "toggleCol('${b.id}')" > < span class = "tog-dot ${hc}" > < / span > ${d.avatar||b.avatar} ${d.name||b.name}${vis?'':' (隐藏)'}< / button > `;}).join('');
2026-03-11 18:37:57 +08:00
document.getElementById('botColumns').innerHTML=ovData.bots.map(b=>{const d=BD[b.id]||{};const n=d.name||b.name;const c=d.color||b.color;const a=d.avatar||b.avatar;const vis=colVisible[b.id]!==false;const hc=b.status.health==='healthy'?'healthy':b.status.health==='unhealthy'?'unhealthy':'unknown';const st=b.status.running?b.status.status:'离线';return`< div class = "bot-column${vis?'':' hidden'}" id = "col-${b.id}" > < div class = "col-header" onclick = "openBotDetail('${b.id}')" style = "cursor:pointer;border-left:3px solid ${c}" > < span class = "col-icon" > ${a}< / span > < div > < h2 > ${n}< / h2 > < div class = "col-role" > ${b.role}< / div > < / div > < div class = "col-actions" > ${restartButtonHtml(b.id,true)}< div class = "col-status" > < span class = "col-status-text" > ${st}< / span > < span class = "col-dot ${hc}" > < / span > < / div > < / div > < / div > < div class = "cap-row" > ${b.capabilities.map(cap=>`< span class = "cap" > ${cap}< / span > `).join('')}< / div > ${b.id==='leader'?renderLeaderTasks(b,isLoading):renderWorkerTasks(b,n,isLoading)}${renderBotCron(b,isLoading)}${renderBotCommits(b,isLoading)}${renderBotMcps(b,isLoading)}${renderBotSkills(b,isLoading)}< / div > `;}).join('');
}
function renderLeaderTasks(b,isLoading){const t=b.tasks||{};const all=t.dispatched||[];const num=v=>isLoading?'--':v;const body=isLoading?'< div class = "empty-msg loading" > 加载中...< / div > ':(all.length?all.slice(0,6).map(i=>{const tg=BD[i.assignedTo]||{};let badge,bc;if(i.status==='pending'){badge='待接收';bc='dispatched';}else if(i.status==='in-progress'){badge='已接收';bc='accepted';}else if(i.status==='done'){badge='完成';bc='done';}else{badge='阻塞';bc='blocked';}return`< div class = "t-row" onclick = "openTaskModal(${i.number})" > < span class = "t-num" > #${i.number}< / span > < span class = "t-title" > ${esc(i.title)}< / span > < span class = "t-badge ${bc}" > ${badge}< / span > < span class = "t-target" > →${tg.avatar||''}${tg.name||i.assignedTo}< / span > < span class = "t-time" > ${timeAgo(i.updatedAt)}< / span > < / div > `;}).join(''):'< div class = "empty-msg" > 暂无< / div > ');return`< div class = "panel" > < div class = "panel-head" > < h3 > 📤 已分派< / h3 > < span class = "link" onclick = "openBotDetail('leader')" > 全部 →< / span > < / div > < div class = "mini-stats" > < div class = "ms p" > < div class = "n" > ${num((t.pending||[]).length)}< / div > < div class = "l" > 待接收< / div > < / div > < div class = "ms w" > < div class = "n" > ${num((t.accepted||[]).length)}< / div > < div class = "l" > 已接收< / div > < / div > < div class = "ms d" > < div class = "n" > ${num((t.done||[]).length)}< / div > < div class = "l" > 完成< / div > < / div > < div class = "ms b" > < div class = "n" > ${num((t.blocked||[]).length)}< / div > < div class = "l" > 阻塞< / div > < / div > < / div > < div class = "panel-body" > ${body}< / div > < / div > `;}
function renderWorkerTasks(b,name,isLoading){const t=b.tasks||{};const all=[...((t.blocked)||[]),...((t.inProgress)||[]),...((t.pending)||[]),...((t.done)||[])];const num=v=>isLoading?'--':v;const body=isLoading?'< div class = "empty-msg loading" > 加载中...< / div > ':(all.length?all.slice(0,6).map(i=>{const sl={pending:'待处理','in-progress':'进行中',done:'完成',blocked:'阻塞'}[i.status]||'?';return`< div class = "t-row" onclick = "openTaskModal(${i.number})" > < span class = "t-num" > #${i.number}< / span > < span class = "t-title" > ${esc(i.title)}< / span > < span class = "t-badge ${i.status}" > ${sl}< / span > < span class = "t-time" > ${timeAgo(i.updatedAt)}< / span > < / div > `;}).join(''):'< div class = "empty-msg" > 暂无< / div > ');return`< div class = "panel" > < div class = "panel-head" > < h3 > 📥 任务< / h3 > < span class = "link" onclick = "openBotDetail('${b.id}')" > 全部 →< / span > < / div > < div class = "mini-stats" > < div class = "ms p" > < div class = "n" > ${num((t.pending||[]).length)}< / div > < div class = "l" > 待处理< / div > < / div > < div class = "ms w" > < div class = "n" > ${num(((t.inProgress)||[]).length)}< / div > < div class = "l" > 进行中< / div > < / div > < div class = "ms d" > < div class = "n" > ${num((t.done||[]).length)}< / div > < div class = "l" > 完成< / div > < / div > < div class = "ms b" > < div class = "n" > ${num((t.blocked||[]).length)}< / div > < div class = "l" > 阻塞< / div > < / div > < / div > < div class = "panel-body" > ${body}< / div > < / div > `;}
function renderBotCron(b,isLoading){const jobs=b.cron||[];if(isLoading)return`< div class = "panel" > < div class = "panel-head" > < h3 > ⏰ 定时任务< / h3 > < / div > < div class = "panel-body" > < div class = "empty-msg loading" > 加载中...< / div > < / div > < / div > `;if(!jobs.length)return'';return`< div class = "panel" > < div class = "panel-head" > < h3 > ⏰ 定时任务< / h3 > < span class = "cnt" > ${jobs.length}< / span > < / div > < div class = "panel-body" > ${jobs.map(j=>{const on=j.enabled;const next=j.nextRunAt||null;const rm=next?next-Date.now():null;let cd;if(!on)cd='已暂停';else if(rm!==null)cd=fmtCountdownFull(rm);else cd='计算中...';const sc=(rm!==null& & rm< 60000 & & rm > 0)?' soon':'';const last=j.lastRunAt?timeAgo(new Date(j.lastRunAt).toISOString()):'尚未运行';return`< div class = "c-row" > < span class = "c-dot ${on?'on':'off'}" > < / span > < div class = "c-info" > < div class = "c-name" > ${j.name}< / div > < div class = "c-desc" > ${j.description||''}< / div > < / div > < div class = "c-right" > < div class = "c-countdown${sc}" data-next = "${next||''}" > ${cd}< / div > < div class = "c-last" > 上次: ${last}< / div > < / div > < / div > `;}).join('')}< / div > < / div > `;}
function renderBotCommits(b,isLoading){const c=b.commits||[];const u=`https://github.com/${b.codeRepo}`;const body=isLoading?'< div class = "empty-msg loading" > 加载中...< / div > ':(c.length?c.slice(0,4).map(renderCommitRow).join(''):'< div class = "empty-msg" > 暂无提交< / div > ');return`< div class = "panel" > < div class = "panel-head" > < h3 > 📦 代码< / h3 > < a href = "${u}" target = "_blank" > 仓库→< / a > < / div > < div class = "panel-body" > ${body}< / div > < / div > `;}
function renderBotMcps(b,isLoading){const m=b.mcps||[];if(isLoading)return`< div class = "panel" > < div class = "panel-head" > < h3 > 🔌 MCP / 工具< / h3 > < / div > < div class = "panel-body" > < div class = "empty-msg loading" > 加载中...< / div > < / div > < / div > `;if(!m.length)return'';return`< div class = "panel" > < div class = "panel-head" > < h3 > 🔌 MCP / 工具< / h3 > < span class = "cnt" > ${m.length}< / span > < / div > < div class = "panel-body" > ${m.map(sk=>`< div class = "skill-row" > < span class = "skill-icon" > 🔧< / span > < div style = "flex:1;min-width:0" > < div class = "skill-name" > ${esc(sk.name)}${sk.version?`< span class = "skill-ver" > v${esc(sk.version)}< / span > `:''}< / div > ${sk.description?`< div class = "skill-desc" > ${esc(sk.description)}< / div > `:''}< / div > < / div > `).join('')}< / div > < / div > `;}
function renderBotSkills(b,isLoading){const s=b.installedSkills||[];const body=isLoading?'< div class = "empty-msg loading" > 加载中...< / div > ':(s.length?s.map(sk=>`< div class = "skill-row" > < span class = "skill-icon" > 📘< / span > < div style = "flex:1;min-width:0" > < div class = "skill-name" > ${esc(sk.name)}${sk.version?`< span class = "skill-ver" > v${esc(sk.version)}< / span > `:''}< / div > ${sk.description?`< div class = "skill-desc" > ${esc(sk.description)}< / div > `:''}< / div > < / div > `).join(''):'< div class = "empty-msg" > 暂无技能< / div > ');return`< div class = "panel" > < div class = "panel-head" > < h3 > 🧠 技能< / h3 > < span class = "cnt" > ${isLoading?'--':s.length}< / span > < / div > < div class = "panel-body" > ${body}< / div > < / div > `;}
2026-03-11 11:37:35 +08:00
function renderCommitRow(c){const t=(c.message||'').split('\n')[0]||'';return`< div class = "commit-row" > < a class = "commit-sha" href = "${c.url||'#'}" target = "_blank" > ${c.sha}< / a > < div class = "commit-msg" > < span class = "cm-title" > ${esc(t)}< / span > < / div > < span class = "commit-time" > ${timeAgo(c.date)}< / span > < / div > `;}
/* Sub-page */
async function openBotDetail(id){
webBotDetailId=id;
if(isAppMode()){openAppBotDetail(id);return;}
overviewView='detail';document.getElementById('mainView').classList.add('hidden');const pg=document.getElementById('botDetailPage');pg.classList.add('active');pg.innerHTML='< div class = "empty-msg loading" > 加载中...< / div > ';try{const r=await fetch(`/api/bot/${id}`);renderBotDetailPage(await r.json());}catch{pg.innerHTML='< div class = "empty-msg" > 加载失败< / div > ';}
}
function renderBotDetailPage(b){const d=BD[b.id]||{};const n=d.name||b.name;const c=d.color||b.color;const a=d.avatar||b.avatar;const hc=b.status.health==='healthy'?'healthy':'unhealthy';const tasks=b.tasks||[];const commits=b.commits||[];const mcps=b.mcps||[];const installed=b.installedSkills||[];const cUrl=`https://github.com/${b.codeRepo}`;const sUrl=`https://github.com/${b.skillsRepo}`;document.getElementById('botDetailPage').innerHTML=`< div class = "back-btn" onclick = "goHome()" > ← 返回< / div > < div class = "detail-hero" > < div class = "detail-avatar" style = "background:${c}18;border:2px solid ${c}" > ${a}< / div > < div class = "detail-info" > < h2 > ${n}< / h2 > < div class = "role" > ${b.role}< / div > < div class = "detail-repos" > < a href = "${cUrl}" target = "_blank" > 📦 代码仓< / a > < a href = "${sUrl}" target = "_blank" > 🧠 技能仓< / a > < / div > < / div > < div class = "detail-status" > ${restartButtonHtml(b.id)}< div style = "margin-top:8px" > < span class = "big-dot ${hc}" > < / span > < span style = "font-size:13px;font-weight:600" > ${b.status.running?'运行中':'离线'}< / span > < / div > < / div > < / div > < div class = "cap-row" style = "margin-bottom:14px" > ${b.capabilities.map(cap=>`< span class = "cap" > ${cap}< / span > `).join('')}< / div > < div class = "detail-grid" > < div class = "panel" > < div class = "panel-head" > < h3 > 📋 全部任务< / h3 > < span class = "cnt" > ${tasks.length}< / span > < / div > < div class = "panel-body" > ${tasks.length?tasks.map(i=>{if(b.id==='leader'){const tg=BD[i.assignedTo]||{};const m={pending:['待接收','dispatched'],'in-progress':['已接收','accepted'],done:['完成','done'],blocked:['阻塞','blocked']};const[sl,bc]=m[i.status]||['?','pending'];return`< div class = "t-row" onclick = "openTaskModal(${i.number})" > < span class = "t-num" > #${i.number}< / span > < span class = "t-title" > ${esc(i.title)}< / span > < span class = "t-badge ${bc}" > ${sl}< / span > < span class = "t-target" > →${tg.avatar||''}${tg.name||i.assignedTo}< / span > < span class = "t-time" > ${timeAgo(i.updatedAt)}< / span > < / div > `;}const sl={pending:'待处理','in-progress':'进行中',done:'完成',blocked:'阻塞'}[i.status]||'?';return`< div class = "t-row" onclick = "openTaskModal(${i.number})" > < span class = "t-num" > #${i.number}< / span > < span class = "t-title" > ${esc(i.title)}< / span > < span class = "t-badge ${i.status}" > ${sl}< / span > < span class = "t-time" > ${timeAgo(i.updatedAt)}< / span > < / div > `;}).join(''):'< div class = "empty-msg" > 暂无< / div > '}< / div > < / div > < div class = "panel" > < div class = "panel-head" > < h3 > 📦 提交< / h3 > < a href = "${cUrl}" target = "_blank" > 仓库→< / a > < / div > < div class = "panel-body" > ${commits.length?commits.map(renderCommitRow).join(''):'< div class = "empty-msg" > 暂无< / div > '}< / div > < / div > ${mcps.length?`< div class = "panel" > < div class = "panel-head" > < h3 > 🔌 MCP / 工具< / h3 > < span class = "cnt" > ${mcps.length}< / span > < / div > < div class = "panel-body" > ${mcps.map(s=>`< div class = "skill-row" > < span class = "skill-icon" > 🔧< / span > < div style = "flex:1;min-width:0" > < div class = "skill-name" > ${esc(s.name)}${s.version?`< span class = "skill-ver" > v${esc(s.version)}< / span > `:''}< / div > ${s.description?`< div class = "skill-desc" > ${esc(s.description)}< / div > `:''}< / div > < / div > `).join('')}< / div > < / div > `:''}< div class = "panel" > < div class = "panel-head" > < h3 > 🧠 技能< / h3 > < span class = "cnt" > ${installed.length}< / span > < / div > < div class = "panel-body" > ${installed.length?installed.map(s=>`< div class = "skill-row" > < span class = "skill-icon" > 📘< / span > < div style = "flex:1;min-width:0" > < div class = "skill-name" > ${esc(s.name)}${s.version?`< span class = "skill-ver" > v${esc(s.version)}< / span > `:''}< / div > ${s.description?`< div class = "skill-desc" > ${esc(s.description)}< / div > `:''}< / div > < / div > `).join(''):'< div class = "empty-msg" > 暂无技能< / div > '}< / div > < / div > < / div > `;}
function goHome(){webBotDetailId=null;overviewView='home';document.getElementById('mainView').classList.remove('hidden');document.getElementById('botDetailPage').classList.remove('active');document.getElementById('botDetailPage').innerHTML='';if(ovData)renderOverview();}
/* Modal */
async function openTaskModal(number){const ol=document.getElementById('taskModal');ol.classList.add('active');document.getElementById('modalTitle').textContent=`任务 #${number}`;document.getElementById('modalBody').innerHTML='< div class = "empty-msg loading" > 加载中...< / div > ';try{const r=await fetch(`/api/task/${number}`);const t=await r.json();const sl={pending:'待处理','in-progress':'进行中',done:'已完成',blocked:'阻塞'}[t.status]||'未知';const cm=t.comments||[];document.getElementById('modalTitle').textContent=`#${t.number} ${t.title}`;document.getElementById('modalBody').innerHTML=`< div style = "display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap" > < span class = "t-badge ${t.status}" style = "font-size:10px;padding:3px 8px" > ${sl}< / span > ${t.labels.map(l=>`< span style = "font-size:9px;padding:2px 6px;border-radius:4px;background:var(--surface3);color:var(--text2)" > ${l}< / span > `).join('')}< span style = "font-size:10px;color:var(--text3);margin-left:auto" > 创建:${timeAgo(t.createdAt)} · 更新:${timeAgo(t.updatedAt)}< / span > < / div > < h3 > 描述< / h3 > < pre > ${esc(t.body||'无')}< / pre > < h3 > 评论(${cm.length})< / h3 > ${cm.length?cm.map(c=>`< div class = "comment-item" > < div class = "comment-meta" > ${esc(c.author)} · ${timeAgo(c.createdAt)}< / div > < div class = "comment-body" > ${esc(c.body)}< / div > < / div > `).join(''):'< div class = "empty-msg" > 暂无< / div > '}`;}catch{document.getElementById('modalBody').innerHTML='< div class = "empty-msg" > 加载失败< / div > ';}}
function closeModal(){document.getElementById('taskModal').classList.remove('active');}
document.getElementById('taskModal').addEventListener('click',function(e){if(e.target===this)closeModal();});
/* ═══════════ TAB 3: Stats ═══════════ */
async function fetchStats(){
console.log('[Stats] 开始获取统计数据...');
try{
const r=await fetch('/api/stats');
statsData=await r.json();
console.log('[Stats] 数据获取成功:', statsData);
if(activeTab==='stats'){
console.log('[Stats] 当前在统计页面,开始渲染');
renderStats();
}
}catch(e){
console.error('[Stats] 获取失败:', e);
}
}
function renderStats(){
console.log('[Stats] renderStats 被调用, statsData=', statsData);
if(!statsData){
console.warn('[Stats] statsData 为空,跳过渲染');
return;
}
// App 模式下只渲染 App 版本
if(isAppMode()){
renderAppStats();
return;
}
console.log('[Stats] 开始 Web 模式渲染');
// Web 模式渲染
const total=statsData.total||{};
const bots=statsData.bots||{};
console.log('[Stats] 开始渲染, total=', total, 'bots=', Object.keys(bots));
// 生成顶部卡片( 3列布局)
const avgTokens=total.apiCalls>0?Math.round(total.totalTokens/total.apiCalls):0;
let cardsHTML=`< div style = "display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px" >
< div class = "status-card info" >
< div class = "label" > 总调用< / div >
< div class = "value" > ${formatNumber(total.apiCalls||0)}< / div >
< div class = "sub2" > 次< / div >
< / div >
< div class = "status-card info" >
< div class = "label" > 总 Token< / div >
< div class = "value" > ${formatNumber(total.totalTokens||0)}< / div >
< div class = "sub2" > tokens< / div >
< / div >
< div class = "status-card ok" >
< div class = "label" > 平均/次< / div >
< div class = "value" > ${formatNumber(avgTokens)}< / div >
< div class = "sub2" > tokens< / div >
< / div >
< / div > `;
console.log('[Stats] 准备更新 statsCards');
document.getElementById('statsCards').innerHTML=cardsHTML;
// 渲染各 Bot 的详细统计
let html='< div class = "bot-columns" style = "gap:16px" > ';
for(const botId in bots){
const stats=bots[botId];
const d=BD[botId]||{};
const color=d.color||'#888';
const avatar=d.avatar||'🤖';
const name=d.name||botId;
const inputPct=stats.totalTokens>0?Math.round(stats.inputTokens/stats.totalTokens*100):0;
const outputPct=100-inputPct;
html+=`< div class = "bot-column" style = "flex:1" >
< div class = "panel" >
< div class = "panel-head" style = "border-left:3px solid ${color}" >
< h3 > < span style = "font-size:18px" > ${avatar}< / span > ${name}< / h3 >
< / div >
< div class = "panel-body" style = "max-height:none" >
< div style = "padding:14px" >
< div style = "display:grid;grid-template-columns:1fr;gap:12px;margin-bottom:16px" >
< div style = "text-align:center;padding:12px;background:var(--surface2);border-radius:8px" >
< div style = "font-size:11px;color:var(--text3);margin-bottom:4px" > API 调用< / div >
< div style = "font-size:24px;font-weight:800;color:var(--cyan)" > ${stats.apiCalls||0}< / div >
< div style = "font-size:9px;color:var(--text3)" > 次< / div >
< / div >
< / div >
< div style = "margin-bottom:12px" >
< div style = "display:flex;justify-content:space-between;font-size:10px;color:var(--text2);margin-bottom:4px" >
< span > 📥 Input Tokens< / span >
< span style = "font-family:monospace;font-weight:700" > ${formatNumber(stats.inputTokens||0)} (${inputPct}%)< / span >
< / div >
< div style = "height:6px;background:var(--surface3);border-radius:3px;overflow:hidden" >
< div style = "width:${inputPct}%;height:100%;background:var(--blue);border-radius:3px" > < / div >
< / div >
< / div >
< div style = "margin-bottom:12px" >
< div style = "display:flex;justify-content:space-between;font-size:10px;color:var(--text2);margin-bottom:4px" >
< span > 📤 Output Tokens< / span >
< span style = "font-family:monospace;font-weight:700" > ${formatNumber(stats.outputTokens||0)} (${outputPct}%)< / span >
< / div >
< div style = "height:6px;background:var(--surface3);border-radius:3px;overflow:hidden" >
< div style = "width:${outputPct}%;height:100%;background:var(--green);border-radius:3px" > < / div >
< / div >
< / div >
< div style = "display:flex;justify-content:space-between;padding:8px 0;border-top:1px solid var(--border);font-size:11px" >
< span style = "color:var(--text3)" > 总 Tokens< / span >
< span style = "font-family:monospace;font-weight:700;color:var(--text)" > ${formatNumber(stats.totalTokens||0)}< / span >
< / div >
< div style = "display:flex;justify-content:space-between;padding:8px 0;font-size:11px" >
< span style = "color:var(--text3)" > 平均每次< / span >
< span style = "font-family:monospace;font-weight:700;color:var(--text)" > ${stats.apiCalls>0?formatNumber(Math.round(stats.totalTokens/stats.apiCalls)):0} tokens< / span >
< / div >
< div style = "display:flex;justify-content:space-between;padding:8px 0;border-top:1px solid var(--border);font-size:10px" >
< span style = "color:var(--text3)" > 最后更新< / span >
< span style = "color:var(--text3)" > ${stats.lastUpdated?fmtTime(stats.lastUpdated):'--'}< / span >
< / div >
< / div >
< / div >
< / div >
< / div > `;
}
html+='< / div > ';
console.log('[Stats] 准备更新 statsContent, html长度:', html.length);
document.getElementById('statsContent').innerHTML=html;
console.log('[Stats] 渲染完成');
}
function formatNumber(num){
if(num>=1000000)return(num/1000000).toFixed(2)+'M';
if(num>=1000)return(num/1000).toFixed(1)+'K';
return num.toString();
}
/* ═══════════ TAB 4: Memory ═══════════ */
let selectedMemoryDate = null;
async function fetchMemory(date = null){
console.log('[Memory] 开始获取记忆数据, date=', date);
try{
const url = date ? `/api/memory?date=${date}` : '/api/memory';
const r=await fetch(url);
memoryData=await r.json();
console.log('[Memory] 数据获取成功:', memoryData);
if(activeTab==='memory'){
console.log('[Memory] 当前在记忆页面,开始渲染');
renderMemory();
}
}catch(e){
console.error('[Memory] 获取失败:', e);
}
}
function loadMemoryByDate(){
const datePicker = document.getElementById('memoryDatePicker');
const selectedDate = datePicker.value;
if(selectedDate){
selectedMemoryDate = selectedDate;
fetchMemory(selectedDate);
// 更新定时器,使用新选择的日期
clearInterval(memoryTimer);
memoryTimer = setInterval(() => fetchMemory(selectedDate), 30000);
}
}
function loadTodayMemory(){
selectedMemoryDate = null;
const datePicker = document.getElementById('memoryDatePicker');
datePicker.value = '';
fetchMemory();
// 更新定时器,使用今天的日期
clearInterval(memoryTimer);
memoryTimer = setInterval(() => fetchMemory(null), 30000);
}
function renderMemory(){
console.log('[Memory] renderMemory 被调用, memoryData=', memoryData);
if(!memoryData){
console.warn('[Memory] memoryData 为空,跳过渲染');
return;
}
// App 模式下只渲染 App 版本
if(isAppMode()){
renderAppMemory();
return;
}
console.log('[Memory] 开始 Web 模式渲染');
// Web 模式渲染
const bots=memoryData.bots||{};
console.log('[Memory] 开始渲染, bots=', Object.keys(bots));
const date=memoryData.date||'今天';
// 由于所有 Bot 共享同一份记忆,我们只显示一次
const sharedMemory = bots['leader'] || {};
let html = '< div style = "max-width:1200px;margin:0 auto" > ';
if (sharedMemory.isEmpty) {
html += `< div style = "text-align:center;padding:60px;color:var(--text3)" >
< div style = "font-size:48px;margin-bottom:16px" > 📝< / div >
< div style = "font-size:16px;font-weight:600;margin-bottom:8px" > ${date} 暂无记录< / div >
< div style = "font-size:12px" > OpenClaw 还没有创建今天的记忆文件< / div >
< / div > `;
} else {
// 显示摘要
if (sharedMemory.summary) {
html += `< div style = "padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px" >
< div style = "font-size:13px;font-weight:700;color:var(--text2);margin-bottom:10px" > 📅 ${date}< / div >
< div style = "font-size:11px;color:var(--text2);line-height:1.8;white-space:pre-wrap" > ${esc(sharedMemory.summary)}< / div >
< / div > `;
}
// 显示今日事件
if (sharedMemory.events & & sharedMemory.events.length > 0) {
html += `< div style = "padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px" >
< div style = "font-size:13px;font-weight:700;color:var(--text2);margin-bottom:12px" > 📝 今日事件< / div > `;
sharedMemory.events.forEach(event => {
html += `< div style = "padding:10px;background:rgba(59,130,246,.08);border-left:3px solid var(--blue);border-radius:6px;margin-bottom:8px" >
< div style = "font-size:11px;color:var(--text);font-weight:600" > ${esc(event)}< / div >
< / div > `;
});
html += `< / div > `;
}
// 新增:每日总结
if (sharedMemory.dailySummary & & sharedMemory.dailySummary.trim()) {
html += `< div style = "padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px" >
< div style = "font-size:13px;font-weight:700;color:var(--text2);margin-bottom:12px" > 📌 每日总结< / div >
< div style = "padding:12px;background:linear-gradient(135deg, rgba(59,130,246,.08) 0%, rgba(167,139,250,.08) 100%);border-left:3px solid var(--blue);border-radius:8px" >
< div style = "font-size:11px;color:var(--text);line-height:1.8;white-space:pre-wrap" > ${esc(sharedMemory.dailySummary)}< / div >
< / div >
< / div > `;
}
// 新增:教训与反思
if (sharedMemory.reflections & & sharedMemory.reflections.length > 0) {
html += `< div style = "padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px" >
< div style = "font-size:13px;font-weight:700;color:var(--text2);margin-bottom:12px" > ⚠️ 教训与反思< / div > `;
sharedMemory.reflections.forEach(reflection => {
html += `< div style = "padding:12px;background:rgba(239,68,68,.08);border-left:3px solid var(--red);border-radius:8px;margin-bottom:10px" >
< div style = "font-size:12px;color:var(--text);font-weight:600;margin-bottom:6px" > ${esc(reflection.title)}< / div >
< div style = "font-size:10px;color:var(--text2);line-height:1.6;white-space:pre-wrap" > ${esc(reflection.content)}< / div >
< / div > `;
});
html += `< / div > `;
}
// 显示学习笔记
if (sharedMemory.learnings & & sharedMemory.learnings.length > 0) {
html += `< div style = "padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px" >
< div style = "font-size:13px;font-weight:700;color:var(--text2);margin-bottom:12px" > 🧠 学习笔记< / div > `;
sharedMemory.learnings.forEach(learning => {
html += `< div style = "padding:12px;background:var(--surface2);border-radius:8px;margin-bottom:10px" >
< div style = "font-size:12px;color:var(--text);font-weight:600;margin-bottom:6px" > ${esc(learning.title)}< / div >
< div style = "font-size:10px;color:var(--text2);line-height:1.6;white-space:pre-wrap" > ${esc(learning.content)}< / div >
< / div > `;
});
html += `< / div > `;
}
// 显示待跟进事项
if (sharedMemory.notes & & sharedMemory.notes.length > 0) {
html += `< div style = "padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px" >
< div style = "font-size:13px;font-weight:700;color:var(--text2);margin-bottom:12px" > 🔄 待跟进< / div > `;
sharedMemory.notes.forEach(note => {
html += `< div style = "padding:8px 12px;background:var(--surface2);border-radius:6px;margin-bottom:6px;font-size:11px;color:var(--text2)" >
• ${esc(note)}
< / div > `;
});
html += `< / div > `;
}
// 显示完整内容(可展开)
if (sharedMemory.content) {
html += `< details style = "padding:20px;background:var(--surface);border:1px solid var(--border);border-radius:12px" >
< summary style = "cursor:pointer;font-size:13px;font-weight:700;color:var(--text2);margin-bottom:12px" > 📄 查看完整记忆 (Markdown)< / summary >
< pre style = "margin-top:12px;padding:12px;background:var(--surface2);border-radius:8px;font-size:10px;line-height:1.6;overflow-x:auto;white-space:pre-wrap;color:var(--text2)" > ${esc(sharedMemory.content)}< / pre >
< / details > `;
}
}
html += '< / div > ';
console.log('[Memory] 准备更新 memoryContent, html长度:', html.length);
document.getElementById('memoryContent').innerHTML = html;
console.log('[Memory] 渲染完成');
}
function renderAppStats(){
console.log('[AppStats] renderAppStats 被调用, statsData=', statsData);
if(!statsData){
console.warn('[AppStats] statsData 为空,跳过渲染');
return;
}
console.log('[AppStats] 开始 App 模式渲染');
const el=document.getElementById('appStats');if(!el)return;
const total=statsData.total||{};
const bots=statsData.bots||{};
console.log('[AppStats] 开始渲染, total=', total, 'bots=', Object.keys(bots));
let h='< div class = "app-content" style = "padding:20px" > ';
// 总览卡片
const avgTokens=total.apiCalls>0?Math.round(total.totalTokens/total.apiCalls):0;
h+=`< div class = "app-status-grid" style = "margin-bottom:20px" >
< div class = "app-status-card info" > < div class = "as-label" > 总调用< / div > < div class = "as-value" > ${formatNumber(total.apiCalls||0)}< / div > < div class = "as-sub" > 次< / div > < / div >
< div class = "app-status-card info" > < div class = "as-label" > 总 Token< / div > < div class = "as-value" > ${formatNumber(total.totalTokens||0)}< / div > < div class = "as-sub" > tokens< / div > < / div >
< div class = "app-status-card ok" > < div class = "as-label" > 平均/次< / div > < div class = "as-value" > ${formatNumber(avgTokens)}< / div > < div class = "as-sub" > tokens< / div > < / div >
< / div > `;
// 各 Bot 统计
for(const botId in bots){
const stats=bots[botId];
const d=BD[botId]||{};
const color=d.color||'#888';
const avatar=d.avatar||'🤖';
const name=d.name||botId;
const inputPct=stats.totalTokens>0?Math.round(stats.inputTokens/stats.totalTokens*100):0;
const outputPct=100-inputPct;
h+=`< div class = "app-feature-card" style = "margin-bottom:16px" >
< div class = "app-feature-head" style = "border-left:3px solid ${color}" > < h3 > ${avatar} ${name}< / h3 > < / div >
< div class = "app-feature-body" style = "padding:16px" >
< div style = "display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-bottom:16px" >
< div style = "text-align:center" > < div style = "font-size:11px;color:var(--text3);margin-bottom:4px" > API 调用< / div > < div style = "font-size:24px;font-weight:800;color:var(--cyan)" > ${formatNumber(stats.apiCalls||0)}< / div > < div style = "font-size:10px;color:var(--text3)" > 次< / div > < / div >
< div style = "text-align:center" > < div style = "font-size:11px;color:var(--text3);margin-bottom:4px" > 总 Token< / div > < div style = "font-size:24px;font-weight:800;color:var(--purple)" > ${formatNumber(stats.totalTokens||0)}< / div > < div style = "font-size:10px;color:var(--text3)" > tokens< / div > < / div >
< / div >
< div style = "margin-bottom:12px" > < div style = "font-size:10px;color:var(--text3);margin-bottom:6px" > Token 分布< / div > < div style = "display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--surface3)" > < div style = "width:${inputPct}%;background:var(--blue)" title = "输入: ${inputPct}%" > < / div > < div style = "width:${outputPct}%;background:var(--green)" title = "输出: ${outputPct}%" > < / div > < / div > < div style = "display:flex;justify-content:space-between;margin-top:4px;font-size:10px" > < span style = "color:var(--blue)" > 📥 输入: ${formatNumber(stats.inputTokens||0)} (${inputPct}%)< / span > < span style = "color:var(--green)" > 📤 输出: ${formatNumber(stats.outputTokens||0)} (${outputPct}%)< / span > < / div > < / div >
< / div >
< / div > `;
}
h+='< / div > ';
console.log('[AppStats] 准备更新 appStats, html长度:', h.length);
el.innerHTML=h;
console.log('[AppStats] 渲染完成');
}
function renderAppMemory(){
console.log('[AppMemory] renderAppMemory 被调用, memoryData=', memoryData);
if(!memoryData){
console.warn('[AppMemory] memoryData 为空,跳过渲染');
return;
}
console.log('[AppMemory] 开始 App 模式渲染');
const el=document.getElementById('appMemory');if(!el)return;
const bots=memoryData.bots||{};
const date=memoryData.date||'今天';
const sharedMemory=bots['leader']||{};
console.log('[AppMemory] 开始渲染, date=', date, 'sharedMemory=', sharedMemory);
let h='< div class = "app-content" style = "padding:20px" > ';
if(sharedMemory.isEmpty){
h+=`< div style = "text-align:center;padding:60px;color:var(--text3)" >
< div style = "font-size:48px;margin-bottom:16px" > 📝< / div >
< div style = "font-size:16px;font-weight:600;margin-bottom:8px" > ${date} 暂无记录< / div >
< div style = "font-size:12px" > OpenClaw 还没有创建今天的记忆文件< / div >
< / div > `;
}else{
h+=`< div style = "text-align:center;margin-bottom:24px" > < div style = "font-size:32px;font-weight:800;background:linear-gradient(135deg,var(--cyan),var(--purple));-webkit-background-clip:text;-webkit-text-fill-color:transparent" > ${date}< / div > < / div > `;
// 摘要
if(sharedMemory.summary){
h+=`< div class = "app-feature-card" style = "margin-bottom:16px" > < div class = "app-feature-head" > < h3 > 📅 ${date}< / h3 > < / div > < div class = "app-feature-body" style = "padding:16px" > < div style = "font-size:13px;line-height:1.8;color:var(--text2);white-space:pre-wrap" > ${esc(sharedMemory.summary)}< / div > < / div > < / div > `;
}
// 今日事件
if(sharedMemory.events& & sharedMemory.events.length>0){
h+=`< div class = "app-feature-card" style = "margin-bottom:16px" > < div class = "app-feature-head" > < h3 > 📝 今日事件< / h3 > < span class = "af-cnt" > ${sharedMemory.events.length}< / span > < / div > < div class = "app-feature-body" style = "padding:12px" > `;
sharedMemory.events.forEach(event=>{
h+=`< div style = "padding:10px;margin-bottom:8px;background:rgba(59,130,246,.08);border-left:3px solid var(--blue);border-radius:6px;font-size:12px;line-height:1.6;color:var(--text)" > ${esc(event)}< / div > `;
});
h+=`< / div > < / div > `;
}
// 每日总结(新增)
if(sharedMemory.dailySummary& & sharedMemory.dailySummary.trim()){
h+=`< div class = "app-feature-card" style = "margin-bottom:16px" > < div class = "app-feature-head" > < h3 > 📌 每日总结< / h3 > < / div > < div class = "app-feature-body" style = "padding:16px;background:linear-gradient(135deg,rgba(59,130,246,.05),rgba(167,139,250,.05))" > < div style = "font-size:13px;line-height:1.8;color:var(--text);white-space:pre-wrap" > ${esc(sharedMemory.dailySummary)}< / div > < / div > < / div > `;
}
// 教训与反思(新增)
if(sharedMemory.reflections& & sharedMemory.reflections.length>0){
h+=`< div class = "app-feature-card" style = "margin-bottom:16px;border-left:3px solid var(--red)" > < div class = "app-feature-head" > < h3 > ⚠️ 教训与反思< / h3 > < span class = "af-cnt" > ${sharedMemory.reflections.length}< / span > < / div > < div class = "app-feature-body" style = "padding:16px;background:rgba(239,68,68,.05)" > `;
sharedMemory.reflections.forEach(r=>{
h+=`< div style = "margin-bottom:12px;padding:12px;background:var(--surface);border-radius:8px;border-left:3px solid var(--orange)" > < div style = "font-size:12px;font-weight:600;margin-bottom:6px;color:var(--text)" > ${esc(r.title)}< / div > < div style = "font-size:11px;line-height:1.6;color:var(--text2);white-space:pre-wrap" > ${esc(r.content)}< / div > < / div > `;
});
h+=`< / div > < / div > `;
}
// 学习笔记
if(sharedMemory.learnings& & sharedMemory.learnings.length>0){
h+=`< div class = "app-feature-card" style = "margin-bottom:16px" > < div class = "app-feature-head" > < h3 > 🧠 学习笔记< / h3 > < span class = "af-cnt" > ${sharedMemory.learnings.length}< / span > < / div > < div class = "app-feature-body" style = "padding:12px" > `;
sharedMemory.learnings.forEach(learning=>{
h+=`< div style = "padding:12px;margin-bottom:10px;background:var(--surface2);border-radius:8px" > < div style = "font-size:12px;font-weight:600;margin-bottom:6px;color:var(--text)" > ${esc(learning.title)}< / div > < div style = "font-size:11px;line-height:1.6;color:var(--text2);white-space:pre-wrap" > ${esc(learning.content)}< / div > < / div > `;
});
h+=`< / div > < / div > `;
}
// 待跟进事项
if(sharedMemory.notes& & sharedMemory.notes.length>0){
h+=`< div class = "app-feature-card" style = "margin-bottom:16px" > < div class = "app-feature-head" > < h3 > 🔄 待跟进< / h3 > < span class = "af-cnt" > ${sharedMemory.notes.length}< / span > < / div > < div class = "app-feature-body" style = "padding:12px" > `;
sharedMemory.notes.forEach(note=>{
h+=`< div style = "padding:10px;margin-bottom:8px;background:var(--surface2);border-radius:6px;font-size:12px;line-height:1.6;color:var(--text2)" > • ${esc(note)}< / div > `;
});
h+=`< / div > < / div > `;
}
}
h+='< / div > ';
console.log('[AppMemory] 准备更新 appMemory, html长度:', h.length);
el.innerHTML=h;
console.log('[AppMemory] 渲染完成');
}
/* ═══════════ TAB 2: Monitor ═══════════ */
function toggleMonCol(id){monColVisible[id]=!monColVisible[id];renderMonitor();}
async function fetchMonitor(){
// 立即显示框架
if(!monData) {
showMonitorFramework();
showSkeletonMonitor();
}
2026-03-11 18:37:57 +08:00
const firstLoad=!monData;
if(firstLoad){
workerLogs.qianwen={loading:true,logs:[]};
workerLogs.kimi={loading:true,logs:[]};
workerConvData.qianwen={loading:true,messages:[]};
workerConvData.kimi={loading:true,messages:[]};
}
2026-03-11 11:37:35 +08:00
try{
2026-03-11 18:37:57 +08:00
const[r1,r2]=await Promise.all([
fetch('/api/monitor'),
fetch('/api/monitor/conversation?limit=20& botId=leader')
2026-03-11 11:37:35 +08:00
]);
2026-03-11 18:37:57 +08:00
monData=await r1.json();
convData=await r2.json();
2026-03-11 11:37:35 +08:00
if(activeTab==='monitor')renderMonitor();
2026-03-11 18:37:57 +08:00
(async ()=>{
try{
const[r3,r4,r5,r6,r7,r8]=await Promise.all([
fetch('/api/bot/kimi/logs'),
fetch('/api/bot/qianwen/logs'),
fetch('/api/monitor/conversation?limit=20& botId=qianwen'),
fetch('/api/monitor/conversation?limit=20& botId=kimi'),
fetch('/api/bot/qianwen?lite=1'),
fetch('/api/bot/kimi?lite=1')
]);
workerLogs.kimi=await r3.json();
workerLogs.qianwen=await r4.json();
workerConvData.qianwen=await r5.json();
workerConvData.kimi=await r6.json();
const qianwenData=await r7.json();
const kimiData=await r8.json();
workerLogs.qianwen.mcps=qianwenData.mcps||[];
workerLogs.qianwen.installedSkills=qianwenData.installedSkills||[];
workerLogs.kimi.mcps=kimiData.mcps||[];
workerLogs.kimi.installedSkills=kimiData.installedSkills||[];
workerLogs.qianwen.loading=false;
workerLogs.kimi.loading=false;
workerConvData.qianwen.loading=false;
workerConvData.kimi.loading=false;
if(activeTab==='monitor')renderMonitor();
}catch(err){
workerLogs.qianwen.loading=false;
workerLogs.kimi.loading=false;
workerConvData.qianwen.loading=false;
workerConvData.kimi.loading=false;
if(activeTab==='monitor')renderMonitor();
console.error(err);
}
})();
2026-03-11 11:37:35 +08:00
}catch(e){console.error(e);}
}
function showMonitorFramework(){
// 显示监控页面的3列框架
const bots=[
{id:'leader',name:'大龙虾',avatar:'🦞',color:'#FF6B35'},
{id:'qianwen',name:'全栈高手',avatar:'⚡',color:'#4ECDC4'},
{id:'kimi',name:'智囊团',avatar:'🔬',color:'#A78BFA'}
];
document.getElementById('monColToggles').innerHTML=bots.map(b=>{
return`< button class = "col-toggle active" > < span class = "tog-dot unknown" > < / span > ${b.avatar} ${b.name}< / button > `;
}).join('');
document.getElementById('monColumns').innerHTML=bots.map(b=>{
return`< div class = "bot-column" >
< div class = "col-header" style = "border-left:3px solid ${b.color}" >
< span class = "col-icon" > ${b.avatar}< / span >
< div >
< h2 > ${b.name}< / h2 >
< div class = "col-role" > 加载中...< / div >
< / div >
< div class = "col-actions" >
< div class = "col-status" >
< span class = "col-status-text" > --< / span >
< span class = "col-dot unknown" > < / span >
< / div >
< / div >
< / div >
< div class = "panel" >
< div class = "panel-head" > < h3 > 🧠 思考 & 对话< / h3 > < / div >
< div class = "chat-body" style = "max-height:400px" > < div class = "empty-msg loading" > 加载中...< / div > < / div >
< / div >
< div class = "panel" >
< div class = "panel-head" > < h3 > 📡 日志< / h3 > < / div >
< div class = "panel-body" > < div class = "empty-msg loading" > 加载中...< / div > < / div >
< / div >
< / div > `;
}).join('');
}
function renderMonitor(){
if(!monData)return;
// App 模式下只渲染 App 版本
if(isAppMode()){
renderAppMonitor();
return;
}
// Web 模式渲染 - 保存所有聊天容器的滚动状态
const scrollStates = {};
['chatBody', 'chatBody-qianwen', 'chatBody-kimi'].forEach(id => {
const el = document.getElementById(id);
if (el) {
scrollStates[id] = el.scrollHeight - el.scrollTop - el.clientHeight < 50 ;
}
});
const bots=[
{id:'leader',name:'大龙虾',avatar:'🦞',color:'#FF6B35'},
{id:'qianwen',name:'全栈高手',avatar:'⚡',color:'#4ECDC4'},
{id:'kimi',name:'智囊团',avatar:'🔬',color:'#A78BFA'}
];
document.getElementById('monColToggles').innerHTML=bots.map(b=>{
const vis=monColVisible[b.id]!==false;
let on=false;
if(b.id==='leader')on=monData.leader.gateway.running;
else{const w=monData.workers?.find(x=>x.id===b.id);on=w&&w.running; }
return`< button class = "col-toggle${vis?' active':''}" onclick = "toggleMonCol('${b.id}')" > < span class = "tog-dot ${on?'on':'off'}" > < / span > ${b.avatar} ${b.name}${vis?'':' (隐藏)'}< / button > `;
}).join('');
document.getElementById('monColumns').innerHTML=bots.map(b=>{
const vis=monColVisible[b.id]!==false;
if(b.id==='leader') return renderLeaderMonCol(b,vis);
return renderWorkerMonCol(b,vis);
}).join('');
renderSystem();
document.getElementById('refreshInfo').textContent=`${new Date().toLocaleTimeString('zh-CN')} · 5s`;
// 恢复所有聊天容器的滚动位置
requestAnimationFrame(() => {
Object.keys(scrollStates).forEach(id => {
if (scrollStates[id]) {
const el = document.getElementById(id);
if (el) el.scrollTop = el.scrollHeight;
}
});
});
}
function renderLeaderMonCol(b,vis){
const g=monData.leader.gateway;const st=monData.leader.currentStatus;
const stMap={idle:'空闲',thinking:'思考中',streaming:'输出中',queued:'排队中'};
const ts=monData.leader.turnStatus;
let tsLabel='💤 空闲';
if(ts){if(ts.status==='final')tsLabel='✅ 完成';else if(ts.status==='working')tsLabel='🔧 执行中';else if(ts.status==='text_only')tsLabel='⚠️ 仅口头';}
const logs=monData.logs||[];const tl=monData.timeline||[];
const msgs=(convData&&convData.messages)||[];
const cronJobs=monData.cronJobs||[];
return`< div class = "bot-column${vis?'':' hidden'}" > < div class = "col-header" style = "border-left:3px solid ${b.color}" > < span class = "col-icon" > ${b.avatar}< / span > < div > < h2 > ${b.name}< / h2 > < div class = "col-role" > ${g.running?stMap[st]||'运行中':'离线'} · ${tsLabel}< / div > < / div > < div class = "col-actions" > ${restartButtonHtml('leader',true)}< div class = "col-status" > < span class = "col-status-text" > ${g.running?g.latencyMs+'ms':'停止'}< / span > < span class = "col-dot ${g.running?'healthy':'unhealthy'}" > < / span > < / div > < / div > < / div > < div class = "panel" > < div class = "panel-head" > < h3 > 🧠 思考 & 对话< / h3 > < span class = "sub" > ${msgs.length}条< / span > < / div > < div class = "chat-body" id = "chatBody" style = "max-height:400px" > ${renderChatMsgs(msgs)}< / div > < / div > < div class = "panel" > < div class = "panel-head" > < h3 > 💬 时间线< / h3 > < / div > < div class = "panel-body" > ${renderTimelineHtml(tl)}< / div > < / div > < div class = "panel" > < div class = "panel-head" > < h3 > 📡 日志< / h3 > < / div > < div class = "panel-body" style = "max-height:200px" > ${renderLogHtml(logs)}< / div > < / div > ${cronJobs.length?`< div class = "panel" > < div class = "panel-head" > < h3 > ⏰ 定时任务< / h3 > < span class = "cnt" > ${cronJobs.length}< / span > < / div > < div class = "panel-body" > ${cronJobs.map(j=>{const on=j.enabled;const next=j.nextRunAt?fmtCountdownFull(j.nextRunAt-Date.now()):'--';return`< div class = "c-row" > < span class = "c-dot ${on?'on':'off'}" > < / span > < div class = "c-info" > < div class = "c-name" > ${j.name}< / div > < / div > < div class = "c-right" > < div class = "c-countdown${(j.nextRunAt&&j.nextRunAt-Date.now()<60000&&j.nextRunAt-Date.now()>0)?' soon':''}" data-next = "${j.nextRunAt||''}" > ${on?next:'已暂停'}< / div > < / div > < / div > `;}).join('')}< / div > < / div > `:''}< / div > `;
}
function renderWorkerMonCol(b,vis){
const wl=workerLogs[b.id]||{};const logs=wl.logs||[];const st=wl.status||{};
2026-03-11 18:37:57 +08:00
const loading=!!wl.loading||!!(workerConvData[b.id]&&workerConvData[b.id].loading);
const pm=wl.pollMeta;const on=!loading&&(st.running||st.state==='running');
2026-03-11 11:37:35 +08:00
let nextPoll='--';let nextPollMs=null;
2026-03-11 18:37:57 +08:00
if(pm& & !loading){const lp=pm.lastPollAt;if(lp){nextPollMs=(lp+pm.interval)*1000-Date.now();nextPoll=fmtCountdownFull(nextPollMs);}}
const pollInterval=pm&&!loading?Math.round(pm.interval/60)+'分钟':'--';
const lastPollTime=pm&&pm.lastPollAt&&!loading?fmtTime(pm.lastPollAt*1000):'--';
2026-03-11 11:37:35 +08:00
const tagMap={poll:'轮询',idle:'空闲',processing:'处理',working:'执行',complete:'完成',handoff:'交接',error:'错误',warn:'警告',info:'信息'};
const tagCls={poll:'processing',idle:'stream_done',processing:'incoming',working:'streaming',complete:'complete',handoff:'complete',error:'error',warn:'warn',info:'stream_done'};
const taskLogs=logs.filter(l=>l.type==='processing'||l.type==='working'||l.type==='complete'||l.type==='handoff');
const sysLogs=logs.filter(l=>l.type!=='processing'&&l.type!=='working'&&l.type!=='complete'&&l.type!=='handoff');
const msgs=(workerConvData[b.id]&&workerConvData[b.id].messages)||[];
2026-03-11 18:37:57 +08:00
const msgCount=loading?'--':msgs.length;
const taskCount=loading?'--':taskLogs.length;
const sysCount=loading?'--':sysLogs.length;
2026-03-11 11:37:35 +08:00
const mcps=wl.mcps||[];const skills=wl.installedSkills||[];
2026-03-11 18:37:57 +08:00
let mcpHtml='';
if(loading)mcpHtml=`< div class = "panel" > < div class = "panel-head" > < h3 > 🔌 MCP / 工具< / h3 > < / div > < div class = "panel-body" > < div class = "empty-msg loading" > 加载中...< / div > < / div > < / div > `;
else if(mcps.length)mcpHtml=`< div class = "panel" > < div class = "panel-head" > < h3 > 🔌 MCP / 工具< / h3 > < span class = "cnt" > ${mcps.length}< / span > < / div > < div class = "panel-body" > ${mcps.map(sk=>`< div class = "skill-row" > < span class = "skill-icon" > 🔧< / span > < div style = "flex:1;min-width:0" > < div class = "skill-name" > ${esc(sk.name)}${sk.version?`< span class = "skill-ver" > v${esc(sk.version)}< / span > `:''}< / div > ${sk.description?`< div class = "skill-desc" > ${esc(sk.description)}< / div > `:''}< / div > < / div > `).join('')}< / div > < / div > `;
let skillHtml='';
if(loading)skillHtml=`< div class = "panel" > < div class = "panel-head" > < h3 > 🧠 技能< / h3 > < / div > < div class = "panel-body" > < div class = "empty-msg loading" > 加载中...< / div > < / div > < / div > `;
else skillHtml=`< div class = "panel" > < div class = "panel-head" > < h3 > 🧠 技能< / h3 > < span class = "cnt" > ${skills.length}< / span > < / div > < div class = "panel-body" > ${skills.length?skills.map(sk=>`< div class = "skill-row" > < span class = "skill-icon" > 📘< / span > < div style = "flex:1;min-width:0" > < div class = "skill-name" > ${esc(sk.name)}${sk.version?`< span class = "skill-ver" > v${esc(sk.version)}< / span > `:''}< / div > ${sk.description?`< div class = "skill-desc" > ${esc(sk.description)}< / div > `:''}< / div > < / div > `).join(''):'< div class = "empty-msg" > 暂无技能< / div > '}< / div > < / div > `;
const roleText=loading?'加载中...':(on?'运行中':'离线');
const statusText=loading?'加载中...':(on?'轮询 '+pollInterval:'停止');
const taskBody=loading?'< div class = "empty-msg loading" > 加载中...< / div > ':(taskLogs.length?taskLogs.map(l=>`< div class = "log-entry" > < span class = "log-time" > ${l.time?l.time.substring(11,19):'--:--'}< / span > < span class = "log-tag ${tagCls[l.type]||'warn'}" > ${tagMap[l.type]||l.type}< / span > < span class = "log-content" > ${esc(l.content)}< / span > < / div > `).join(''):'< div class = "empty-msg" > 暂无任务执行记录< / div > ');
const sysBody=loading?'< div class = "empty-msg loading" > 加载中...< / div > ':(sysLogs.length?sysLogs.map(l=>`< div class = "log-entry" > < span class = "log-time" > ${l.time?l.time.substring(11,19):'--:--'}< / span > < span class = "log-tag ${tagCls[l.type]||'warn'}" > ${tagMap[l.type]||l.type}< / span > < span class = "log-content" > ${esc(l.content)}< / span > < / div > `).join(''):'< div class = "empty-msg" > 暂无< / div > ');
2026-03-11 11:37:35 +08:00
2026-03-11 18:37:57 +08:00
return`< div class = "bot-column${vis?'':' hidden'}" > < div class = "col-header" style = "border-left:3px solid ${b.color}" > < span class = "col-icon" > ${b.avatar}< / span > < div > < h2 > ${b.name}< / h2 > < div class = "col-role" > ${roleText}${on?' · 轮询:'+nextPoll:''}< / div > < / div > < div class = "col-actions" > ${restartButtonHtml(b.id,true)}< div class = "col-status" > < span class = "col-status-text" > ${statusText}< / span > < span class = "col-dot ${on?'healthy':'unhealthy'}" > < / span > < / div > < / div > < / div > < div class = "panel" > < div class = "panel-head" > < h3 > 🧠 思考 & 对话< / h3 > < span class = "sub" > ${msgCount}条< / span > < / div > < div class = "chat-body" id = "chatBody-${b.id}" style = "max-height:400px" > ${renderWorkerChatMsgs(msgs,b,loading)}< / div > < / div > < div class = "panel" > < div class = "panel-head" > < h3 > 🔧 任务执行< / h3 > < span class = "sub" > ${taskCount}条< / span > < / div > < div class = "panel-body" style = "max-height:300px" > ${taskBody}< / div > < / div > < div class = "panel" > < div class = "panel-head" > < h3 > 📡 系统日志< / h3 > < span class = "cnt" > ${sysCount}条< / span > < / div > < div class = "panel-body" style = "max-height:200px" > ${sysBody}< / div > < / div > ${mcpHtml}${skillHtml}< div class = "panel" > < div class = "panel-head" > < h3 > ⏰ 轮询调度< / h3 > < / div > < div class = "panel-body" > < div class = "c-row" > < span class = "c-dot ${on?'on':'off'}" > < / span > < div class = "c-info" > < div class = "c-name" > GitHub Issues 轮询< / div > < div class = "c-desc" > 每 ${pollInterval} 自动检查待处理任务< / div > < / div > < div class = "c-right" > < div class = "c-countdown${(nextPollMs!==null&&nextPollMs<60000&&nextPollMs>0)?' soon':''}" data-next = "${pm&&pm.lastPollAt?(pm.lastPollAt+pm.interval)*1000:''}" > ${on?nextPoll:(loading?'加载中...':'已停止')}< / div > < div class = "c-last" > 上次: ${lastPollTime}< / div > < / div > < / div > < / div > < / div > < / div > `;
2026-03-11 11:37:35 +08:00
}
2026-03-11 18:37:57 +08:00
function renderWorkerChatMsgs(msgs,bot,loading){
if(loading)return'< div class = "empty-msg loading" > 加载中...< / div > ';
2026-03-11 11:37:35 +08:00
if(!msgs.length)return'< div class = "empty-msg" > 暂无对话< / div > ';
return msgs.map((m,i)=>{
const time=m.timestamp?fmtTime(m.timestamp):'';
if(m.role==='user'){const long=m.content.length>300;const col=long&&!expandedReply[bot.id+'u'+i]; return`< div class = "chat-msg user" > < div class = "chat-role" > 👤 用户 < span class = "chat-time" > ${time}< / span > < / div > < div class = "chat-text${col?' collapsed':''}" > ${esc(m.content)}< / div > ${long?`< span class = "chat-expand" onclick = "toggleReply('${bot.id}u${i}')" > ${col?'展开 ▼':'收起 ▲'}< / span > `:''}< / div > `;}
const ht=m.thinking&&m.thinking.length>0; const tc=ht&&!expandedThink[bot.id+i]; const rl=m.content.length>500;const rc=rl&&!expandedReply[bot.id+'a'+i];
let th='';if(ht)th=`< div class = "think-label" > 💭 思考过程 < span class = "chat-expand" onclick = "toggleThink('${bot.id}${i}')" style = "margin-left:3px" > ${tc?'展开 ▼':'收起 ▲'}< / span > < / div > < div class = "think-block${tc?' collapsed':''}" > ${esc(m.thinking)}< / div > `;
let tsBadge='';if(m.turnStatus==='final')tsBadge='< span class = "turn-badge final" > ✅< / span > ';else if(m.turnStatus==='working')tsBadge='< span class = "turn-badge working" > 🔧< / span > ';else if(m.turnStatus==='text_only')tsBadge='< span class = "turn-badge text-only" > ⚠️< / span > ';
return`< div class = "chat-msg assistant" > < div class = "chat-role" > ${bot.avatar} ${bot.name} < span class = "chat-time" > ${time}< / span > ${tsBadge}< / div > ${th}< div class = "chat-text${rc?' collapsed':''}" > ${esc(m.content)}< / div > ${rl?`< span class = "chat-expand" onclick = "toggleReply('${bot.id}a${i}')" > ${rc?'展开 ▼':'收起 ▲'}< / span > `:''}< / div > `;
}).join('');
}
function renderChatMsgs(msgs){
if(!msgs.length)return'< div class = "empty-msg" > 暂无对话< / div > ';
return msgs.map((m,i)=>{
const time=m.timestamp?fmtTime(m.timestamp):'';
if(m.role==='user'){const long=m.content.length>300;const col=long&&!expandedReply['u'+i]; return`< div class = "chat-msg user" > < div class = "chat-role" > 👤 用户 < span class = "chat-time" > ${time}< / span > < / div > < div class = "chat-text${col?' collapsed':''}" > ${esc(m.content)}< / div > ${long?`< span class = "chat-expand" onclick = "toggleReply('u${i}')" > ${col?'展开 ▼':'收起 ▲'}< / span > `:''}< / div > `;}
const ht=m.thinking&&m.thinking.length>0; const tc=ht&&!expandedThink[i]; const rl=m.content.length>500;const rc=rl&&!expandedReply['a'+i];
let th='';if(ht)th=`< div class = "think-label" > 💭 思考过程 < span class = "chat-expand" onclick = "toggleThink(${i})" style = "margin-left:3px" > ${tc?'展开 ▼':'收起 ▲'}< / span > < / div > < div class = "think-block${tc?' collapsed':''}" > ${esc(m.thinking)}< / div > `;
let tsBadge='';if(m.turnStatus==='final')tsBadge='< span class = "turn-badge final" > ✅< / span > ';else if(m.turnStatus==='working')tsBadge='< span class = "turn-badge working" > 🔧< / span > ';else if(m.turnStatus==='text_only')tsBadge='< span class = "turn-badge text-only" > ⚠️< / span > ';
return`< div class = "chat-msg assistant" > < div class = "chat-role" > 🦞 大龙虾 < span class = "chat-time" > ${time}< / span > ${tsBadge}< / div > ${th}< div class = "chat-text${rc?' collapsed':''}" > ${esc(m.content)}< / div > ${rl?`< span class = "chat-expand" onclick = "toggleReply('a${i}')" > ${rc?'展开 ▼':'收起 ▲'}< / span > `:''}< / div > `;
}).join('');
}
function toggleThink(i){expandedThink[i]=!expandedThink[i];renderMonitor();}
function toggleReply(k){expandedReply[k]=!expandedReply[k];renderMonitor();}
function renderTimelineHtml(tl){
if(!tl.length)return'< div class = "empty-msg" > 暂无< / div > ';
return tl.slice().reverse().map(t=>{
const dc=!t.durationSec?'':t.durationSec< 15 ? ' fast ' :t . durationSec < 60 ? ' medium ' : ' slow ' ;
let dt;if(t.durationSec!=null)dt=t.durationSec+'s';else if(t.status==='thinking')dt=(t.elapsedSec?t.elapsedSec+'s':'思考中');else if(t.status==='streaming')dt=(t.elapsedSec?t.elapsedSec+'s':'输出中');else dt='等待';
const ac=(t.status==='thinking'||t.status==='streaming')?' active-msg':'';
return`< div class = "tl-entry${ac}" > < span class = "tl-status ${t.status}" > < / span > < span class = "tl-msg" title = "${esc(t.message||'')}" > ${esc(t.message||'')}< / span > < span class = "tl-dur ${dc}" > ${dt}< / span > < span class = "tl-time" > ${fmtTime(t.receivedAt)}< / span > < / div > `;
}).join('');
}
function renderLogHtml(logs){
const tl={incoming:'收到',processing:'处理',complete:'完成',streaming:'流式',stream_done:'结束',warn:'警告',error:'错误'};
return logs.length?logs.map(l=>`< div class = "log-entry" > < span class = "log-time" > ${fmtTime(l.time)}< / span > < span class = "log-tag ${l.type}" > ${tl[l.type]||l.type}< / span > < span class = "log-content" > ${esc(l.content)}< / span > < / div > `).join(''):'< div class = "empty-msg" > 暂无< / div > ';
}
function renderSystem(){
const s=monData.system;const cp=Math.round(s.load[0]/s.cpuCores*100);const mp=s.memory.usedPct;
const cc=cp>80?'var(--red)':cp>50?'var(--yellow)':'var(--green)';const mc=mp>80?'var(--red)':mp>60?'var(--yellow)':'var(--green)';
document.getElementById('systemSub').textContent=`负载:${s.load.join('/')} · ${s.cpuCores}核`;
document.getElementById('systemBody').innerHTML=`< div class = "meter-row" > < div class = "meter" > < div class = "m-label" > CPU< / div > < div class = "m-bar" > < div class = "m-fill" style = "width:${Math.min(cp,100)}%;background:${cc}" > < / div > < / div > < div class = "m-val" style = "color:${cc}" > ${cp}%< / div > < / div > < div class = "meter" > < div class = "m-label" > 内存< / div > < div class = "m-bar" > < div class = "m-fill" style = "width:${mp}%;background:${mc}" > < / div > < / div > < div class = "m-val" style = "color:${mc}" > ${mp}%< / div > < / div > < / div > `;
}
function renderMonCron(){}
/* ═══════════ App Mode Rendering ═══════════ */
function isAppMode(){return document.body.classList.contains('app-mode');}
let appBotDetailId=null;
let appMonitorBotId=null;
let _appBotCache=null;
async function openAppBotDetail(id,silent){
appBotDetailId=id;
const el=document.getElementById('appOverview');if(!el)return;
if(!silent){
const d=BD[id]||{};
el.innerHTML=appSidebar('overview')+`< div class = "app-content" > < div class = "app-empty" style = "padding:40px" > 加载 ${d.name||id} 数据中...< / div > < / div > `;
}
try{const r=await fetch('/api/bot/'+id);const data=await r.json();_appBotCache=data;_saveAppScroll();renderAppBotDetail(data);_restoreAppScroll();}
catch{if(!silent)el.querySelector('.app-content').innerHTML='< div class = "app-empty" > 加载失败< / div > ';}
}
function renderAppBotDetail(b){
const el=document.getElementById('appOverview');if(!el)return;
const d=BD[b.id]||{};const n=d.name||b.name;const c=d.color||b.color;const a=d.avatar||b.avatar;
const hc=b.status.health==='healthy'?'healthy':'unhealthy';
const on=b.status.running;
const tasks=b.tasks||[];const commits=b.commits||[];const mcps=b.mcps||[];
const installed=b.installedSkills||[];const cron=b.cron||[];
const cUrl=`https://github.com/${b.codeRepo}`;const sUrl=`https://github.com/${b.skillsRepo}`;
let sidebar=`< div class = "app-sidebar" > < div class = "app-sidebar-section" > < div class = "app-sidebar-label" > Bots< / div > `;
const botDefs=[{id:'leader',name:'大龙虾',avatar:'🦞'},{id:'qianwen',name:'全栈高手',avatar:'⚡'},{id:'kimi',name:'智囊团',avatar:'🔬'}];
botDefs.forEach(bd=>{
const bdd=BD[bd.id]||{};
let bon=false;
if(ovData){const fb=ovData.bots.find(x=>x.id===bd.id);if(fb)bon=fb.status?.running||false;}
sidebar+=`< div class = "app-sidebar-item${bd.id===b.id?' active':''}" onclick = "openAppBotDetail('${bd.id}')" > < span class = "si-icon" > ${bdd.avatar||bd.avatar}< / span > < span class = "si-name" > ${bdd.name||bd.name}< / span > < span class = "si-dot ${bon?'on':'off'}" > < / span > < / div > `;
});
sidebar+=`< / div > < div class = "app-sidebar-divider" > < / div > < div class = "app-sidebar-section" > `;
sidebar+=`< div class = "app-sidebar-item" onclick = "appBackToOverview()" > < span class = "si-icon" > ←< / span > < span class = "si-name" > 返回总览< / span > < / div > `;
sidebar+=`< / div > < / div > `;
let h=sidebar+`< div class = "app-content" > `;
/* Hero */
h+=`< div class = "app-listing-row" style = "border-left:4px solid ${c};cursor:default" >
< div class = "app-listing-icon" style = "background:${c}18;border:2px solid ${c}" > ${a}< / div >
< div class = "app-listing-info" >
< div class = "app-listing-name" style = "font-size:20px" > ${n}< / div >
< div class = "app-listing-role" > ${b.role}< / div >
< div class = "app-listing-caps" > ${b.capabilities.map(cap=>`< span class = "cap" > ${cap}< / span > `).join('')}< / div >
< / div >
< div class = "app-listing-right" >
< button class = "app-listing-btn ${on?'online':'offline'}" > ${on?'运行中':'离线'}< / button >
${restartButtonHtml(b.id)}
< div class = "app-listing-stats" > < a href = "${cUrl}" target = "_blank" style = "color:var(--cyan);font-size:10px;text-decoration:none" > 📦 代码仓< / a > < a href = "${sUrl}" target = "_blank" style = "color:var(--cyan);font-size:10px;text-decoration:none" > 🧠 技能仓< / a > < / div >
< / div >
< / div > `;
h+=`< div class = "app-feature-grid" > `;
/* Tasks */
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 📋 任务< / h3 > < span class = "af-cnt" > ${tasks.length}< / span > < / div > < div class = "app-feature-body" > `;
if(tasks.length){
tasks.forEach(t=>{
let sl,bc;
if(b.id==='leader'){
const m={pending:['待接收','dispatched'],'in-progress':['已接收','accepted'],done:['完成','done'],blocked:['阻塞','blocked']};
[sl,bc]=m[t.status]||['?','pending'];
} else {
sl={pending:'待处理','in-progress':'进行中',done:'完成',blocked:'阻塞'}[t.status]||'?';bc=t.status;
}
const tg=b.id==='leader'?(BD[t.assignedTo]||{}):{};
h+=`< div class = "app-task-row" onclick = "openTaskModal(${t.number})" > < span class = "app-task-num" > #${t.number}< / span > < span class = "app-task-title" > ${esc(t.title)}< / span > < span class = "app-task-badge ${bc}" > ${sl}< / span > ${b.id==='leader'?`< span class = "app-task-target" > ${tg.avatar||''}< / span > `:''}< span class = "app-task-time" > ${timeAgo(t.updatedAt)}< / span > < / div > `;
});
} else h+=`< div class = "app-empty" > 暂无任务< / div > `;
h+=`< / div > < / div > `;
/* Commits */
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 📦 提交< / h3 > < a class = "af-link" href = "${cUrl}" target = "_blank" > 仓库→< / a > < / div > < div class = "app-feature-body" > `;
if(commits.length){
commits.forEach(cm=>{
const title=(cm.message||'').split('\n')[0]||'';
h+=`< div class = "app-commit-row" > < a class = "app-commit-sha" href = "${cm.url||'#'}" target = "_blank" > ${cm.sha}< / a > < div class = "app-commit-msg" > ${esc(title)}< / div > < span class = "app-commit-time" > ${timeAgo(cm.date)}< / span > < / div > `;
});
} else h+=`< div class = "app-empty" > 暂无提交< / div > `;
h+=`< / div > < / div > `;
/* Cron */
if(cron.length){
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > ⏰ 定时任务< / h3 > < span class = "af-cnt" > ${cron.length}< / span > < / div > < div class = "app-feature-body" > `;
cron.forEach(j=>{
const jon=j.enabled;const next=j.nextRunAt||null;const rm=next?next-Date.now():null;
let cd;
if(!jon)cd='< span class = "app-cron-countdown paused" > 已暂停< / span > ';
else if(rm!==null)cd=`< span class = "app-cron-countdown${(rm<60000&&rm>0)?' soon':''}" data-next = "${next}" > ${fmtCountdownFull(rm)}< / span > `;
else cd='< span class = "app-cron-countdown" > --< / span > ';
const last=j.lastRunAt?timeAgo(new Date(j.lastRunAt).toISOString()):'尚未运行';
h+=`< div class = "app-cron-row" > < span class = "app-cron-dot ${jon?'on':'off'}" > < / span > < div class = "app-cron-info" > < div class = "app-cron-name" > ${j.name}< / div > < div class = "app-cron-desc" > ${j.description||''}< / div > < / div > < div class = "app-cron-right" > ${cd}< div class = "app-cron-last" > 上次: ${last}< / div > < / div > < / div > `;
});
h+=`< / div > < / div > `;
}
/* MCP / Tools */
if(mcps.length){
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 🔌 工具< / h3 > < span class = "af-cnt" > ${mcps.length}< / span > < / div > < div class = "app-feature-body" > < div class = "app-tools-grid" > `;
mcps.forEach(m=>{h+=`< div class = "app-tool-card" > < div class = "app-tool-icon" > 🔧< / div > < div class = "app-tool-name" > ${esc(m.name)}< / div > ${m.version?`< div class = "app-tool-ver" > v${esc(m.version)}< / div > `:''}< / div > `;});
h+=`< / div > < / div > < / div > `;
}
/* Skills */
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > 🧠 技能< / h3 > < span class = "af-cnt" > ${installed.length}< / span > < / div > < div class = "app-feature-body" > `;
if(installed.length){
h+=`< div class = "app-skills-grid" > `;
installed.forEach(s=>{h+=`< div class = "app-skill-card" > < div class = "app-skill-icon" > 📘< / div > < div class = "app-skill-info" > < div class = "app-skill-name" > ${esc(s.name)}< / div > ${s.description?`< div class = "app-skill-desc" > ${esc(s.description)}< / div > `:''}< / div > < / div > `;});
h+=`< / div > `;
} else h+=`< div class = "app-empty" > 暂无技能< / div > `;
h+=`< / div > < / div > `;
h+=`< / div > < / div > `;
el.innerHTML=h;
}
function appBackToOverview(){
appBotDetailId=null;
if(ovData)renderAppOverview();
}
function appBackToMonitorOverview(){
appMonitorBotId=null;
if(monData)renderAppMonitor();
}
function appGoToBotDetail(id){
if(activeTab==='monitor'){
appMonitorBotId=id;
renderAppMonitor();
return;
}
appMonitorBotId=null;
if(activeTab!=='overview')switchTab('overview');
openAppBotDetail(id);
}
function appScrollTo(id){
const target=document.getElementById(id);
const container=_getActiveAppContent();
if(target& & container){
container.scrollTo({top:target.offsetTop-container.offsetTop-12,behavior:'smooth'});
document.querySelectorAll('.app-sidebar-item').forEach(el=>el.classList.remove('active'));
event?.target?.closest?.('.app-sidebar-item')?.classList.add('active');
}
}
function appSidebar(tab,selectedBotId=null){
const botDefs=[{id:'leader',name:'大龙虾',avatar:'🦞',color:'#FF6B35'},{id:'qianwen',name:'全栈高手',avatar:'⚡',color:'#4ECDC4'},{id:'kimi',name:'智囊团',avatar:'🔬',color:'#A78BFA'}];
let s=`< div class = "app-sidebar" > < div class = "app-sidebar-section" > < div class = "app-sidebar-label" > Bots< / div > `;
botDefs.forEach(bd=>{
const d=BD[bd.id]||{};const n=d.name||bd.name;const a=d.avatar||bd.avatar;
let on=false,tCnt=0;
if(tab==='overview'& & ovData){
const b=ovData.bots.find(x=>x.id===bd.id);
if(b){on=b.status?.running||false;tCnt=(b.tasks?.pending||[]).length+(b.tasks?.inProgress||b.tasks?.accepted||[]).length;}
} else {
if(bd.id==='leader'){on=monData?.leader?.gateway?.running||false;}
else{const wl=workerLogs[bd.id]||{};const wst=wl.status||{};on=wst.running||wst.state==='running';}
}
s+=`< div class = "app-sidebar-item${bd.id===selectedBotId?' active':''}" onclick = "appGoToBotDetail('${bd.id}')" > < span class = "si-icon" > ${a}< / span > < span class = "si-name" > ${n}< / span > < span class = "si-dot ${on?'on':'off'}" > < / span > ${tCnt?`< span class = "si-badge" > ${tCnt}< / span > `:''}< / div > `;
});
s+=`< / div > < div class = "app-sidebar-divider" > < / div > < div class = "app-sidebar-section" > `;
if(tab==='overview'){
s+=`< div class = "app-sidebar-item active" onclick = "appScrollTo('as-today')" > < span class = "si-icon" > 📊< / span > < span class = "si-name" > 总览< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('as-tasks')" > < span class = "si-icon" > 📋< / span > < span class = "si-name" > 任务< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('as-commits')" > < span class = "si-icon" > 📦< / span > < span class = "si-name" > 提交< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('as-cron')" > < span class = "si-icon" > ⏰< / span > < span class = "si-name" > 定时任务< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('as-tools')" > < span class = "si-icon" > 🔌< / span > < span class = "si-name" > 工具< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('as-skills')" > < span class = "si-icon" > 🧠< / span > < span class = "si-name" > 技能< / span > < / div > `;
} else {
s+=`< div class = "app-sidebar-item active" onclick = "appScrollTo('am-status')" > < span class = "si-icon" > 📡< / span > < span class = "si-name" > 监控< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('am-chat')" > < span class = "si-icon" > 💬< / span > < span class = "si-name" > 对话< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('am-timeline')" > < span class = "si-icon" > 📊< / span > < span class = "si-name" > 时间线< / span > < / div > `;
s+=`< div class = "app-sidebar-item" onclick = "appScrollTo('am-logs')" > < span class = "si-icon" > 📡< / span > < span class = "si-name" > 日志< / span > < / div > `;
}
s+=`< / div > < / div > `;
return s;
}
let _appScrollCache={};
function _getActiveAppRoot(){
return activeTab==='monitor'?document.getElementById('appMonitor'):document.getElementById('appOverview');
}
function _getActiveAppContent(root){
const host=root||_getActiveAppRoot();
return host?host.querySelector('.app-content'):null;
}
function _saveAppScroll(){
const root=_getActiveAppRoot();
const c=_getActiveAppContent(root);
if(c)_appScrollCache[activeTab+'/content']=c.scrollTop;
root?.querySelectorAll('.app-feature-body').forEach(fb=>{
const card=fb.closest('.app-feature-card');
const h3=card?.querySelector('.app-feature-head h3');
if(h3){const k=activeTab+'/fb/'+h3.textContent.trim();_appScrollCache[k]=fb.scrollTop;}
});
const chatEl=root?.querySelector('.app-chat-body');
if(chatEl)_appScrollCache[activeTab+'/chat']=chatEl.scrollTop;
}
function _restoreAppScroll(){
requestAnimationFrame(()=>{
const root=_getActiveAppRoot();
const c=_getActiveAppContent(root);
const contentTop=_appScrollCache[activeTab+'/content'];
if(c&&contentTop!=null)c.scrollTop=contentTop;
root?.querySelectorAll('.app-feature-body').forEach(fb=>{
const card=fb.closest('.app-feature-card');
const h3=card?.querySelector('.app-feature-head h3');
if(h3){
const k=activeTab+'/fb/'+h3.textContent.trim();
if(_appScrollCache[k]!=null)fb.scrollTop=_appScrollCache[k];
}
});
const chatEl=root?.querySelector('.app-chat-body');
const chatTop=_appScrollCache[activeTab+'/chat'];
if(chatEl&&chatTop!=null)chatEl.scrollTop=chatTop;
});
}
function renderAppOverview(){
if(!ovData){
// 显示 App 模式的加载框架
showAppOverviewFramework();
return;
}
if(appBotDetailId){openAppBotDetail(appBotDetailId,true);return;}
const el=document.getElementById('appOverview');if(!el)return;
_saveAppScroll();
const bots=ovData.bots;
const allTasks=[],allCommits=[],allCron=[],allMcps=[],allSkills=[];
bots.forEach(b=>{
const ts=b.id==='leader'?(b.tasks.dispatched||[]):[...(b.tasks.blocked||[]),...(b.tasks.inProgress||[]),...(b.tasks.pending||[]),...(b.tasks.done||[])];
ts.forEach(t=>{t._bot=b.id;allTasks.push(t);});
(b.commits||[]).forEach(c=>{c._bot=b.id;allCommits.push(c);});
(b.cron||[]).forEach(c=>{c._bot=b.id;allCron.push(c);});
(b.mcps||[]).forEach(m=>{m._bot=b.id;allMcps.push(m);});
(b.installedSkills||[]).forEach(s=>{s._bot=b.id;allSkills.push(s);});
});
allTasks.sort((a,b)=>new Date(b.updatedAt)-new Date(a.updatedAt));
allCommits.sort((a,b)=>new Date(b.date)-new Date(a.date));
let h=appSidebar('overview');
h+=`< div class = "app-content" > `;
/* Bot listing row (App Store app-list style) */
h+=`< div class = "app-section-title" id = "as-today" > Today< / div > < div class = "app-section-sub" > ${bots.filter(b=>b.status.running).length}/${bots.length} 在线 · ${ovData.stats.openTasks} 待办 · ${ovData.stats.doneTasks} 完成< / div > `;
bots.forEach(b=>{
const d=BD[b.id]||{};const n=d.name||b.name;const c=d.color||b.color;const a=d.avatar||b.avatar;
const on=b.status.running;
let pn,ac,dn;
if(b.id==='leader'){pn=(b.tasks.pending||[]).length;ac=(b.tasks.accepted||[]).length;dn=(b.tasks.done||[]).length;}
else{pn=(b.tasks.pending||[]).length;ac=(b.tasks.inProgress||[]).length;dn=(b.tasks.done||[]).length;}
h+=`< div class = "app-listing-row" onclick = "openBotDetail('${b.id}')" >
< div class = "app-listing-icon" style = "background:${c}18;border:2px solid ${c}" > ${a}< / div >
< div class = "app-listing-info" >
< div class = "app-listing-name" > ${n}< / div >
< div class = "app-listing-role" > ${b.role}< / div >
< div class = "app-listing-caps" > ${b.capabilities.slice(0,4).map(cap=>`< span class = "cap" > ${cap}< / span > `).join('')}< / div >
< / div >
< div class = "app-listing-right" >
< button class = "app-listing-btn ${on?'online':'offline'}" > ${on?'在线':'离线'}< / button >
${restartButtonHtml(b.id)}
< div class = "app-listing-stats" > < span > 📋${pn+ac}< / span > < span > ✅${dn}< / span > < / div >
< / div >
< / div > `;
});
/* 2-col feature grid */
h+=`< div class = "app-section-title" > 详情< / div > < div class = "app-feature-grid" > `;
/* Tasks card */
h+=`< div class = "app-feature-card" id = "as-tasks" > < div class = "app-feature-head" > < h3 > 📋 全部任务< / h3 > < span class = "af-cnt" > ${allTasks.length}< / span > < / div > < div class = "app-feature-body" > `;
if(allTasks.length){
allTasks.slice(0,10).forEach(t=>{
const d=BD[t._bot]||{};
const sl={pending:'待处理','in-progress':'进行中',done:'完成',blocked:'阻塞'}[t.status]||'?';
h+=`< div class = "app-task-row" onclick = "openTaskModal(${t.number})" > < span class = "app-task-num" > #${t.number}< / span > < span class = "app-task-title" > ${esc(t.title)}< / span > < span class = "app-task-badge ${t.status}" > ${sl}< / span > < span class = "app-task-target" > ${d.avatar||''}< / span > < span class = "app-task-time" > ${timeAgo(t.updatedAt)}< / span > < / div > `;
});
} else h+=`< div class = "app-empty" > 暂无任务< / div > `;
h+=`< / div > < / div > `;
/* Commits card */
h+=`< div class = "app-feature-card" id = "as-commits" > < div class = "app-feature-head" > < h3 > 📦 代码提交< / h3 > < span class = "af-cnt" > ${allCommits.length}< / span > < / div > < div class = "app-feature-body" > `;
if(allCommits.length){
allCommits.slice(0,8).forEach(c=>{
const d=BD[c._bot]||{};const title=(c.message||'').split('\n')[0]||'';
h+=`< div class = "app-commit-row" > < a class = "app-commit-sha" href = "${c.url||'#'}" target = "_blank" > ${c.sha}< / a > < div class = "app-commit-msg" > ${d.avatar||''} ${esc(title)}< / div > < span class = "app-commit-time" > ${timeAgo(c.date)}< / span > < / div > `;
});
} else h+=`< div class = "app-empty" > 暂无提交< / div > `;
h+=`< / div > < / div > `;
/* Cron card */
h+=`< div class = "app-feature-card" id = "as-cron" > < div class = "app-feature-head" > < h3 > ⏰ 定时任务< / h3 > < span class = "af-cnt" > ${allCron.length}< / span > < / div > < div class = "app-feature-body" > `;
if(allCron.length){
allCron.forEach(j=>{
const d=BD[j._bot]||{};const on=j.enabled;const next=j.nextRunAt||null;const rm=next?next-Date.now():null;
let cd;
if(!on)cd='< span class = "app-cron-countdown paused" > 已暂停< / span > ';
else if(rm!==null)cd=`< span class = "app-cron-countdown${(rm<60000&&rm>0)?' soon':''}" data-next = "${next}" > ${fmtCountdownFull(rm)}< / span > `;
else cd='< span class = "app-cron-countdown" > 计算中...< / span > ';
const last=j.lastRunAt?timeAgo(new Date(j.lastRunAt).toISOString()):'尚未运行';
h+=`< div class = "app-cron-row" > < span class = "app-cron-dot ${on?'on':'off'}" > < / span > < div class = "app-cron-info" > < div class = "app-cron-name" > ${d.avatar||''} ${j.name}< / div > < div class = "app-cron-desc" > ${j.description||''}< / div > < / div > < div class = "app-cron-right" > ${cd}< div class = "app-cron-last" > 上次: ${last}< / div > < / div > < / div > `;
});
} else h+=`< div class = "app-empty" > 暂无< / div > `;
h+=`< / div > < / div > `;
/* Tools card */
h+=`< div class = "app-feature-card" id = "as-tools" > < div class = "app-feature-head" > < h3 > 🔌 工具< / h3 > < span class = "af-cnt" > ${allMcps.length}< / span > < / div > < div class = "app-feature-body" > `;
if(allMcps.length){
h+=`< div class = "app-tools-grid" > `;
allMcps.forEach(m=>{
h+=`< div class = "app-tool-card" > < div class = "app-tool-icon" > 🔧< / div > < div class = "app-tool-name" > ${esc(m.name)}< / div > ${m.version?`< div class = "app-tool-ver" > v${esc(m.version)}< / div > `:''}< / div > `;
});
h+=`< / div > `;
} else h+=`< div class = "app-empty" > 暂无工具< / div > `;
h+=`< / div > < / div > `;
/* Skills card (full width) */
h+=`< div class = "app-feature-card full" id = "as-skills" > < div class = "app-feature-head" > < h3 > 🧠 技能< / h3 > < span class = "af-cnt" > ${allSkills.length}< / span > < / div > < div class = "app-feature-body" > `;
if(allSkills.length){
h+=`< div class = "app-skills-grid" > `;
allSkills.forEach(s=>{
h+=`< div class = "app-skill-card" > < div class = "app-skill-icon" > 📘< / div > < div class = "app-skill-info" > < div class = "app-skill-name" > ${esc(s.name)}< / div > ${s.description?`< div class = "app-skill-desc" > ${esc(s.description)}< / div > `:''}< / div > < / div > `;
});
h+=`< / div > `;
} else h+=`< div class = "app-empty" > 暂无技能< / div > `;
h+=`< / div > < / div > `;
h+=`< / div > < / div > `;
el.innerHTML=h;
_restoreAppScroll();
}
function renderAppMonitorBotDetail(id){
const el=document.getElementById('appMonitor');if(!el||!monData)return;
const defs={leader:{name:'大龙虾',avatar:'🦞',color:'#FF6B35'},qianwen:{name:'全栈高手',avatar:'⚡',color:'#4ECDC4'},kimi:{name:'智囊团',avatar:'🔬',color:'#A78BFA'}};
const meta=defs[id]||{name:id,avatar:'🤖',color:'var(--cyan)'};
const d=BD[id]||{};
const name=d.name||meta.name;
const avatar=d.avatar||meta.avatar;
const color=d.color||meta.color;
let h=appSidebar('monitor',id);
h+=`< div class = "app-content" > < div class = "back-btn" onclick = "appBackToMonitorOverview()" > ← 返回实时监控< / div > `;
if(id==='leader'){
const g=monData.leader.gateway;const st=monData.leader.currentStatus;
const stMap={idle:'空闲',thinking:'思考中',streaming:'输出中',queued:'排队中'};
const ts=monData.leader.turnStatus;
let tsLabel='💤 空闲';
if(ts){if(ts.status==='final')tsLabel='✅ 完成';else if(ts.status==='working')tsLabel='🔧 执行中';else if(ts.status==='text_only')tsLabel='⚠️ 仅口头';}
const msgs=(convData&&convData.messages)||[];
const tl=monData.timeline||[];
const logs=monData.logs||[];
const cronJobs=monData.cronJobs||[];
h+=`< div class = "app-listing-row" style = "border-left:4px solid ${color};cursor:default" > < div class = "app-listing-icon" style = "background:${color}18;border:2px solid ${color}" > ${avatar}< / div > < div class = "app-listing-info" > < div class = "app-listing-name" > ${name}< / div > < div class = "app-listing-role" > 实时监控 · ${g.running?(stMap[st]||'运行中'):'离线'}< / div > < div class = "app-listing-caps" > < span class = "cap" > Gateway ${g.running?'在线':'离线'}< / span > < span class = "cap" > ${tsLabel}< / span > < span class = "cap" > ${g.running?(g.latencyMs||0)+'ms':'无响应'}< / span > < / div > < / div > < div class = "app-listing-right" > < button class = "app-listing-btn ${g.running?'online':'offline'}" > ${g.running?'运行中':'离线'}< / button > ${restartButtonHtml('leader')}< / div > < / div > `;
h+=`< div class = "app-feature-grid" > `;
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > 🧠 思考 & 对话< / h3 > < span class = "af-cnt" > ${msgs.length}条< / span > < / div > < div class = "app-feature-body" > < div class = "app-chat-body" > ${renderChatMsgs(msgs)}< / div > < / div > < / div > `;
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 💬 时间线< / h3 > < span class = "af-cnt" > ${tl.length}< / span > < / div > < div class = "app-feature-body" > < div class = "app-timeline" > ${renderTimelineHtml(tl)}< / div > < / div > < / div > `;
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 📡 日志< / h3 > < span class = "af-cnt" > ${logs.length}< / span > < / div > < div class = "app-feature-body" > < div class = "app-log-list" > ${renderLogHtml(logs)}< / div > < / div > < / div > `;
if(cronJobs.length){
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > ⏰ 定时任务< / h3 > < span class = "af-cnt" > ${cronJobs.length}< / span > < / div > < div class = "app-feature-body" > `;
cronJobs.forEach(j=>{
const on=j.enabled;const next=j.nextRunAt?fmtCountdownFull(j.nextRunAt-Date.now()):'--';
h+=`< div class = "app-cron-row" > < span class = "app-cron-dot ${on?'on':'off'}" > < / span > < div class = "app-cron-info" > < div class = "app-cron-name" > ${j.name}< / div > < div class = "app-cron-desc" > ${j.description||''}< / div > < / div > < div class = "app-cron-right" > < span class = "app-cron-countdown${(j.nextRunAt&&j.nextRunAt-Date.now()<60000&&j.nextRunAt-Date.now()>0)?' soon':''}" data-next = "${j.nextRunAt||''}" > ${on?next:'已暂停'}< / span > < / div > < / div > `;
});
h+=`< / div > < / div > `;
}
h+=`< / div > `;
} else {
const wl=workerLogs[id]||{};const logs=wl.logs||[];const st=wl.status||{};
const pm=wl.pollMeta;const on=st.running||st.state==='running';
let nextPoll='--',nextPollAt='';
if(pm&&pm.lastPollAt){nextPollAt=(pm.lastPollAt+pm.interval)*1000; nextPoll=fmtCountdownFull(nextPollAt-Date.now());}
const pollInterval=pm?Math.round(pm.interval/60)+'分钟':'--';
const lastPollTime=pm&&pm.lastPollAt?fmtTime(pm.lastPollAt*1000):'--';
const taskLogs=logs.filter(l=>l.type==='processing'||l.type==='working'||l.type==='complete'||l.type==='handoff');
const sysLogs=logs.filter(l=>l.type!=='processing'&&l.type!=='working'&&l.type!=='complete'&&l.type!=='handoff');
const msgs=(workerConvData[id]&&workerConvData[id].messages)||[];
const mcps=wl.mcps||[];const skills=wl.installedSkills||[];
const bot={id,name,avatar,color};
h+=`< div class = "app-listing-row" style = "border-left:4px solid ${color};cursor:default" > < div class = "app-listing-icon" style = "background:${color}18;border:2px solid ${color}" > ${avatar}< / div > < div class = "app-listing-info" > < div class = "app-listing-name" > ${name}< / div > < div class = "app-listing-role" > 实时监控 · ${on?'运行中':'离线'}< / div > < div class = "app-listing-caps" > < span class = "cap" > 轮询 ${pollInterval}< / span > < span class = "cap" > 下次 ${on?nextPoll:'已停止'}< / span > < span class = "cap" > 上次 ${lastPollTime}< / span > < / div > < / div > < div class = "app-listing-right" > < button class = "app-listing-btn ${on?'online':'offline'}" > ${on?'运行中':'离线'}< / button > ${restartButtonHtml(id)}< / div > < / div > `;
h+=`< div class = "app-feature-grid" > `;
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > 🧠 思考 & 对话< / h3 > < span class = "af-cnt" > ${msgs.length}条< / span > < / div > < div class = "app-feature-body" > < div class = "app-chat-body" > ${renderAppWorkerChatMsgs(msgs,bot)}< / div > < / div > < / div > `;
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > ⏰ 轮询调度< / h3 > < / div > < div class = "app-feature-body" > < div class = "app-cron-row" > < span class = "app-cron-dot ${on?'on':'off'}" > < / span > < div class = "app-cron-info" > < div class = "app-cron-name" > GitHub Issues 轮询< / div > < div class = "app-cron-desc" > 每 ${pollInterval} 自动检查待处理任务< / div > < / div > < div class = "app-cron-right" > < span class = "app-cron-countdown${(nextPollAt&&nextPollAt-Date.now()<60000&&nextPollAt-Date.now()>0)?' soon':''}" data-next = "${nextPollAt||''}" > ${on?nextPoll:'已停止'}< / span > < div class = "app-cron-last" > 上次: ${lastPollTime}< / div > < / div > < / div > < / div > < / div > `;
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 📡 状态< / h3 > < / div > < div class = "app-feature-body" > < div class = "app-log-list" > < div class = "app-log-entry" > < span class = "app-log-time" > 状态< / span > < span class = "app-log-tag ${on?'complete':'error'}" > ${on?'在线':'离线'}< / span > < span class = "app-log-content" > ${on?'Worker 正在运行':'Worker 未运行'}< / span > < / div > < div class = "app-log-entry" > < span class = "app-log-time" > 轮询< / span > < span class = "app-log-tag info" > 间隔< / span > < span class = "app-log-content" > ${pollInterval}< / span > < / div > < div class = "app-log-entry" > < span class = "app-log-time" > 下次< / span > < span class = "app-log-tag processing" > 计划< / span > < span class = "app-log-content" > ${on?nextPoll:'已停止'}< / span > < / div > < / div > < / div > < / div > `;
if(mcps.length){h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > 🔌 MCP / 工具< / h3 > < span class = "af-cnt" > ${mcps.length}< / span > < / div > < div class = "app-feature-body" > < div class = "app-tools-grid" > ${mcps.map(m=>`< div class = "app-tool-card" > < div class = "app-tool-icon" > 🔧< / div > < div class = "app-tool-name" > ${esc(m.name)}< / div > ${m.version?`< div class = "app-tool-ver" > v${esc(m.version)}< / div > `:''}< / div > `).join('')}< / div > < / div > < / div > `;}
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > 🧠 技能< / h3 > < span class = "af-cnt" > ${skills.length}< / span > < / div > < div class = "app-feature-body" > ${skills.length?`< div class = "app-skills-grid" > ${skills.map(s=>`< div class = "app-skill-card" > < span class = "skill-icon" > 📘< / span > < div > < div class = "skill-name" > ${esc(s.name)}${s.version?`< span class = "skill-ver" > v${esc(s.version)}< / span > `:''}< / div > ${s.description?`< div class = "skill-desc" style = "font-size:9px;color:var(--text3);margin-top:2px" > ${esc(s.description)}< / div > `:''}< / div > < / div > `).join('')}< / div > `:'< div class = "app-empty" > 暂无技能< / div > '}< / div > < / div > `;
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > 🔧 任务执行< / h3 > < span class = "af-cnt" > ${taskLogs.length}< / span > < / div > < div class = "app-feature-body" > < div class = "app-log-list" > ${taskLogs.length?taskLogs.map(l=>`< div class = "app-log-entry" > < span class = "app-log-time" > ${l.time?l.time.substring(11,19):'--:--'}< / span > < span class = "app-log-tag ${l.type}" > ${l.type}< / span > < span class = "app-log-content" > ${esc(l.content)}< / span > < / div > `).join(''):'< div class = "app-empty" > 暂无任务执行记录< / div > '}< / div > < / div > < / div > `;
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > 📄 系统日志< / h3 > < span class = "af-cnt" > ${sysLogs.length}< / span > < / div > < div class = "app-feature-body" > < div class = "app-log-list" > ${sysLogs.length?sysLogs.map(l=>`< div class = "app-log-entry" > < span class = "app-log-time" > ${l.time?l.time.substring(11,19):'--:--'}< / span > < span class = "app-log-tag ${l.type}" > ${l.type}< / span > < span class = "app-log-content" > ${esc(l.content)}< / span > < / div > `).join(''):'< div class = "app-empty" > 暂无系统日志< / div > '}< / div > < / div > < / div > `;
h+=`< / div > `;
}
h+=`< / div > `;
el.innerHTML=h;
_restoreAppScroll();
}
function renderAppWorkerChatMsgs(msgs,bot){
if(!msgs.length)return'< div class = "app-empty" > 暂无对话< / div > ';
let h='';
msgs.forEach((m,i)=>{
const time=m.timestamp?fmtTime(m.timestamp):'';
if(m.role==='user'){
const txt=m.content.length>300?m.content.slice(0,300)+'…':m.content;
h+=`< div class = "app-chat-time-label" > ${time}< / div > < div class = "app-chat-bubble user" > < div class = "app-chat-sender" > 👤 用户< / div > ${esc(txt)}< / div > `;
} else {
let think='';
if(m.thinking& & m.thinking.length>0){const tc=!expandedThink[bot.id+i];think=`< div class = "app-think-inline${tc?' collapsed':''}" onclick = "toggleThink('${bot.id}${i}')" > ${esc(m.thinking)}< / div > `;}
let badge='';
if(m.turnStatus==='final')badge='< span class = "app-chat-turn-badge final" > ✅< / span > ';
else if(m.turnStatus==='working')badge='< span class = "app-chat-turn-badge working" > 🔧< / span > ';
else if(m.turnStatus==='text_only')badge='< span class = "app-chat-turn-badge text-only" > ⚠️< / span > ';
const txt=m.content.length>400?m.content.slice(0,400)+'…':m.content;
h+=`< div class = "app-chat-time-label" > ${time}< / div > < div class = "app-chat-bubble assistant" > < div class = "app-chat-sender" > ${bot.avatar} ${bot.name} ${badge}< / div > ${think}${esc(txt)}< / div > `;
}
});
return h;
}
function renderAppMonitor(){
if(!monData){
showAppMonitorFramework();
return;
}
const el=document.getElementById('appMonitor');if(!el)return;
_saveAppScroll();
if(appMonitorBotId){renderAppMonitorBotDetail(appMonitorBotId);return;}
const g=monData.leader.gateway;
const st=monData.leader.currentStatus;
const stMap={idle:'空闲',thinking:'思考中',streaming:'输出中',queued:'排队中'};
const ts=monData.leader.turnStatus;
let tsLabel='💤 空闲';
if(ts){if(ts.status==='final')tsLabel='✅ 完成';else if(ts.status==='working')tsLabel='🔧 执行中';else if(ts.status==='text_only')tsLabel='⚠️ 仅口头';}
const workers=monData.workers||[];
const onW=workers.filter(w=>w.running).length;
let h=appSidebar('monitor');
h+=`< div class = "app-content" > `;
/* Status cards */
h+=`< div class = "app-section-title" id = "am-status" > 系统状态< / div > < div class = "app-status-grid" > `;
h+=`< div class = "app-status-card ${g.running?'ok':'err'}" > < div class = "as-label" > 网关< / div > < div class = "as-value" > ${g.running?'✓':'✗'}< / div > < div class = "as-sub" > ${g.running?(g.latencyMs||0)+'ms':'离线'}< / div > < / div > `;
h+=`< div class = "app-status-card ${g.running?'info':'err'}" > < div class = "as-label" > 状态< / div > < div class = "as-value" > ${stMap[st]||'--'}< / div > < div class = "as-sub" > ${tsLabel}< / div > < / div > `;
h+=`< div class = "app-status-card ${onW>0?'ok':'warn'}" > < div class = "as-label" > Worker< / div > < div class = "as-value" > ${onW}/${workers.length}< / div > < div class = "as-sub" > 在线< / div > < / div > `;
const s=monData.system;const cp=Math.round(s.load[0]/s.cpuCores*100);
h+=`< div class = "app-status-card ${cp>80?'err':cp>50?'warn':'ok'}" > < div class = "as-label" > CPU< / div > < div class = "as-value" > ${cp}%< / div > < div class = "as-sub" > ${s.cpuCores}核< / div > < / div > `;
h+=`< / div > `;
/* Resource rings + Workers side by side */
const mp=s.memory.usedPct;
const r=34;const circ=2*Math.PI*r;
const cpOff=circ*(1-Math.min(cp,100)/100);const mpOff=circ*(1-mp/100);
const cpC=cp>80?'var(--red)':cp>50?'var(--yellow)':'var(--green)';
const mcC=mp>80?'var(--red)':mp>60?'var(--yellow)':'var(--green)';
h+=`< div class = "app-feature-grid" > `;
/* Resource ring card */
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 📊 系统资源< / h3 > < span class = "af-cnt" > 负载 ${s.load.join('/')}< / span > < / div > < div class = "app-feature-body" > < div class = "app-resource-row" > `;
h+=`< div class = "app-resource-ring" > < svg class = "app-ring-svg" viewBox = "0 0 80 80" > < circle class = "app-ring-bg" cx = "40" cy = "40" r = "${r}" / > < circle class = "app-ring-fill" cx = "40" cy = "40" r = "${r}" stroke = "${cpC}" stroke-dasharray = "${circ}" stroke-dashoffset = "${cpOff}" / > < / svg > < div class = "app-ring-val" style = "color:${cpC}" > ${cp}%< / div > < div class = "app-ring-label" > CPU< / div > < / div > `;
h+=`< div class = "app-resource-ring" > < svg class = "app-ring-svg" viewBox = "0 0 80 80" > < circle class = "app-ring-bg" cx = "40" cy = "40" r = "${r}" / > < circle class = "app-ring-fill" cx = "40" cy = "40" r = "${r}" stroke = "${mcC}" stroke-dasharray = "${circ}" stroke-dashoffset = "${mpOff}" / > < / svg > < div class = "app-ring-val" style = "color:${mcC}" > ${mp}%< / div > < div class = "app-ring-label" > 内存< / div > < / div > `;
h+=`< / div > < / div > < / div > `;
/* Workers card */
h+=`< div class = "app-feature-card" > < div class = "app-feature-head" > < h3 > 🤖 Worker< / h3 > < / div > < div class = "app-feature-body" > < div class = "app-worker-list" > `;
const wBots=[{id:'leader',name:'大龙虾',avatar:'🦞',color:'#FF6B35'},{id:'qianwen',name:'全栈高手',avatar:'⚡',color:'#4ECDC4'},{id:'kimi',name:'智囊团',avatar:'🔬',color:'#A78BFA'}];
wBots.forEach(b=>{
let on=false,sub='';
if(b.id==='leader'){on=g.running;sub=on?stMap[st]||'运行中':'离线';}
else{const wl=workerLogs[b.id]||{};const wst=wl.status||{};on=wst.running||wst.state==='running';const pm=wl.pollMeta;let np='--';if(pm&&pm.lastPollAt){np=fmtCountdownFull((pm.lastPollAt+pm.interval)*1000-Date.now()); }sub=on?'运行中 · 轮询:'+np:'离线';}
h+=`< div class = "app-worker-card" style = "border-left:3px solid ${b.color};cursor:pointer" onclick = "appGoToBotDetail('${b.id}')" > < div class = "app-worker-avatar" style = "background:${b.color}18" > ${b.avatar}< / div > < div class = "app-worker-info" > < div class = "app-worker-name" > ${b.name}< / div > < div class = "app-worker-sub" > ${sub}< / div > < / div > < div class = "app-worker-status" > ${restartButtonHtml(b.id,true)}< span class = "app-worker-dot ${on?'on':'off'}" > < / span > < / div > < / div > `;
});
h+=`< / div > < / div > < / div > `;
/* Chat */
const msgs=(convData&&convData.messages)||[];
h+=`< div class = "app-feature-card full" id = "am-chat" > < div class = "app-feature-head" > < h3 > 🧠 思考 & 对话< / h3 > < span class = "af-cnt" > ${msgs.length}条< / span > < / div > < div class = "app-feature-body" > < div class = "app-chat-body" id = "appChatBody" > `;
if(msgs.length){
msgs.forEach((m,i)=>{
const time=m.timestamp?fmtTime(m.timestamp):'';
if(m.role==='user'){
const txt=m.content.length>300?m.content.slice(0,300)+'…':m.content;
h+=`< div class = "app-chat-time-label" > ${time}< / div > < div class = "app-chat-bubble user" > < div class = "app-chat-sender" > 👤 用户< / div > ${esc(txt)}< / div > `;
} else {
let think='';
if(m.thinking& & m.thinking.length>0){const tc=!expandedThink[i];think=`< div class = "app-think-inline${tc?' collapsed':''}" onclick = "toggleThink(${i})" > ${esc(m.thinking)}< / div > `;}
let badge='';
if(m.turnStatus==='final')badge='< span class = "app-chat-turn-badge final" > ✅< / span > ';
else if(m.turnStatus==='working')badge='< span class = "app-chat-turn-badge working" > 🔧< / span > ';
else if(m.turnStatus==='text_only')badge='< span class = "app-chat-turn-badge text-only" > ⚠️< / span > ';
const txt=m.content.length>400?m.content.slice(0,400)+'…':m.content;
h+=`< div class = "app-chat-time-label" > ${time}< / div > < div class = "app-chat-bubble assistant" > < div class = "app-chat-sender" > 🦞 大龙虾 ${badge}< / div > ${think}${esc(txt)}< / div > `;
}
});
} else h+=`< div class = "app-empty" > 暂无对话< / div > `;
h+=`< / div > < / div > < / div > `;
/* Timeline + Logs */
const tl=monData.timeline||[];
h+=`< div class = "app-feature-card" id = "am-timeline" > < div class = "app-feature-head" > < h3 > 💬 时间线< / h3 > < / div > < div class = "app-feature-body" > < div class = "app-timeline" > `;
if(tl.length){
tl.slice().reverse().slice(0,15).forEach(t=>{
const dc=!t.durationSec?'':t.durationSec< 15 ? ' fast ' :t . durationSec < 60 ? ' medium ' : ' slow ' ;
let dt;if(t.durationSec!=null)dt=t.durationSec+'s';else if(t.status==='thinking')dt=(t.elapsedSec?t.elapsedSec+'s':'…');else if(t.status==='streaming')dt=(t.elapsedSec?t.elapsedSec+'s':'…');else dt='等待';
const ac=(t.status==='thinking'||t.status==='streaming')?' active-msg':'';
h+=`< div class = "app-tl-entry${ac}" > < span class = "app-tl-dot ${t.status}" > < / span > < span class = "app-tl-msg" > ${esc(t.message||'')}< / span > < span class = "app-tl-dur ${dc}" > ${dt}< / span > < span class = "app-tl-time" > ${fmtTime(t.receivedAt)}< / span > < / div > `;
});
} else h+=`< div class = "app-empty" > 暂无< / div > `;
h+=`< / div > < / div > < / div > `;
const logs=monData.logs||[];
const logLabels={incoming:'收到',processing:'处理',complete:'完成',streaming:'流式',stream_done:'结束',warn:'警告',error:'错误'};
h+=`< div class = "app-feature-card" id = "am-logs" > < div class = "app-feature-head" > < h3 > 📡 日志< / h3 > < span class = "af-cnt" > ${logs.length}< / span > < / div > < div class = "app-feature-body" > < div class = "app-log-list" > `;
if(logs.length){
logs.slice(-20).forEach(l=>{
h+=`< div class = "app-log-entry" > < span class = "app-log-time" > ${fmtTime(l.time)}< / span > < span class = "app-log-tag ${l.type}" > ${logLabels[l.type]||l.type}< / span > < span class = "app-log-content" > ${esc(l.content)}< / span > < / div > `;
});
} else h+=`< div class = "app-empty" > 暂无日志< / div > `;
h+=`< / div > < / div > < / div > `;
/* Cron */
const cronJobs=monData.cronJobs||[];
if(cronJobs.length){
h+=`< div class = "app-feature-card full" > < div class = "app-feature-head" > < h3 > ⏰ 定时任务< / h3 > < span class = "af-cnt" > ${cronJobs.length}< / span > < / div > < div class = "app-feature-body" > `;
cronJobs.forEach(j=>{
const on=j.enabled;const next=j.nextRunAt?fmtCountdownFull(j.nextRunAt-Date.now()):'--';
h+=`< div class = "app-cron-row" > < span class = "app-cron-dot ${on?'on':'off'}" > < / span > < div class = "app-cron-info" > < div class = "app-cron-name" > ${j.name}< / div > < / div > < div class = "app-cron-right" > < span class = "app-cron-countdown${(j.nextRunAt&&j.nextRunAt-Date.now()<60000&&j.nextRunAt-Date.now()>0)?' soon':''}" data-next = "${j.nextRunAt||''}" > ${on?next:'已暂停'}< / span > < / div > < / div > `;
});
h+=`< / div > < / div > `;
}
h+=`< / div > < / div > < / div > `;
el.innerHTML=h;
_restoreAppScroll();
}
/* Countdown tickers */
function tickAll(){
document.querySelectorAll('.c-countdown[data-next]').forEach(el=>{const n=parseInt(el.dataset.next,10);if(!n||isNaN(n))return;const r=n-Date.now();el.textContent=fmtCountdownFull(r);if(r< 60000 & & r > 0)el.classList.add('soon');else el.classList.remove('soon');});
document.querySelectorAll('[data-mnext]').forEach(el=>{const n=parseInt(el.dataset.mnext,10);if(!n||isNaN(n))return;const r=n-Date.now();if(el.classList.contains('w-poll'))el.textContent='轮询:'+fmtCountdown(r);else el.textContent=fmtCountdown(r);});
}
document.addEventListener('keydown',e=>{if(e.key==='Escape'){closeModal();if(overviewView!=='home')goHome();}});
/* ═══ 防止滚动穿透 ═══ */
document.addEventListener('DOMContentLoaded', function() {
// 为所有可滚动容器添加滚动阻止
function preventScrollThrough(e) {
const target = e.currentTarget;
const atTop = target.scrollTop === 0;
const atBottom = target.scrollTop + target.clientHeight >= target.scrollHeight - 1;
if ((atTop & & e.deltaY < 0 ) | | ( atBottom & & e . deltaY > 0)) {
e.preventDefault();
}
}
// 监听所有可滚动元素
const observer = new MutationObserver(() => {
document.querySelectorAll('.panel-body, .chat-body, .app-chat-body, [style*="overflow-y: auto"], [style*="overflow: auto"]').forEach(el => {
if (!el.dataset.scrollProtected) {
el.addEventListener('wheel', preventScrollThrough, { passive: false });
el.dataset.scrollProtected = 'true';
}
});
});
observer.observe(document.body, { childList: true, subtree: true });
// 初始化已存在的元素
document.querySelectorAll('.panel-body, .chat-body, .app-chat-body').forEach(el => {
el.addEventListener('wheel', preventScrollThrough, { passive: false });
el.dataset.scrollProtected = 'true';
});
});
/* ═══ Layout Mode ═══ */
function showAppOverviewFramework(){
const el=document.getElementById('appOverview');if(!el)return;
let h=appSidebar('overview');
h+=`< div class = "app-content" style = "padding:20px" >
< div class = "app-section-title" > Today< / div >
< div class = "app-section-sub" > 加载中...< / div >
${['leader','qianwen','kimi'].map(id=>{
const d=BD[id]||{};
return`< div class = "app-listing-row" >
< div class = "app-listing-icon" style = "background:${d.color}18;border:2px solid ${d.color}" > ${d.avatar}< / div >
< div class = "app-listing-info" >
< div class = "app-listing-name" > ${d.name}< / div >
< div class = "app-listing-role" > 加载中...< / div >
< div class = "app-listing-caps" > < span class = "cap skeleton" style = "width:50px;height:16px" > < / span > < / div >
< / div >
< div class = "app-listing-right" >
< button class = "app-listing-btn" > --< / button >
< / div >
< / div > `;
}).join('')}
< / div > `;
el.innerHTML=h;
}
function showAppMonitorFramework(){
const el=document.getElementById('appMonitor');if(!el)return;
let h=appSidebar('monitor');
h+=`< div class = "app-content" style = "padding:20px" >
< div class = "app-section-title" > 系统状态< / div >
< div class = "app-status-grid" >
${[1,2,3,4].map(()=>`< div class = "app-status-card" > < div class = "as-label" > 加载中< / div > < div class = "skeleton" style = "width:60px;height:32px;margin:8px auto" > < / div > < / div > `).join('')}
< / div >
< / div > `;
el.innerHTML=h;
}
function toggleLayoutMode(){
const isApp=document.body.classList.toggle('app-mode');
localStorage.setItem('layoutMode',isApp?'app':'web');
document.getElementById('layoutBtn').textContent=isApp?'📱':'💻';
document.getElementById('layoutBtn').title=isApp?'切换到网页布局':'切换到 App 布局';
if(activeTab==='overview'&&ovData)renderOverview();
else if(activeTab==='monitor'&&monData)renderMonitor();
else if(activeTab==='stats'&&statsData)renderStats();
else if(activeTab==='memory'&&memoryData)renderMemory();
}
function initLayoutMode(){
const saved=localStorage.getItem('layoutMode')||'app';
if(saved==='app'){
document.body.classList.add('app-mode');
document.getElementById('layoutBtn').textContent='📱';
document.getElementById('layoutBtn').title='切换到网页布局';
}
}
initLayoutMode();
/* ═══ Theme ═══ */
function toggleTheme(){
const cur=document.documentElement.getAttribute('data-theme')||'dark';
const next=cur==='dark'?'light':'dark';
document.documentElement.setAttribute('data-theme',next);
localStorage.setItem('theme',next);
document.getElementById('themeBtn').textContent=next==='dark'?'🌙':'☀️';
}
function initTheme(){
const saved=localStorage.getItem('theme')||'dark';
document.documentElement.setAttribute('data-theme',saved);
document.getElementById('themeBtn').textContent=saved==='dark'?'🌙':'☀️';
}
initTheme();
/* ═══ Scroll Preservation ═══ */
(function(){
const _sp=new Map();
function _key(el){
if(el.id==='chatBody')return null;
const col=el.closest('.bot-column');
const p=el.closest('.panel');
const h=p?p.querySelector('.panel-head h3')?.textContent?.trim():'';
return(col?col.id:'_')+'/'+h;
}
document.addEventListener('scroll',function(e){
const el=e.target;
if(el.classList& & el.classList.contains('panel-body')){
const k=_key(el);if(k)_sp.set(k,el.scrollTop);
}
},true);
let _rt=null;
new MutationObserver(()=>{
if(_rt)cancelAnimationFrame(_rt);
_rt=requestAnimationFrame(()=>{
document.querySelectorAll('.panel-body').forEach(el=>{
const k=_key(el);
if(k&&_sp.has(k)&&el.scrollTop===0){el.scrollTop=_sp.get(k); }
});
_rt=null;
});
}).observe(document.body,{childList:true,subtree:true});
})();
// Boot
if(isAppMode()) showAppOverviewFramework();
else showOverviewFramework();
2026-03-11 18:37:57 +08:00
fetchOverview(true);
if('requestIdleCallback' in window) requestIdleCallback(()=>fetchOverview(false));
else setTimeout(()=>fetchOverview(false),300);
2026-03-11 11:37:35 +08:00
ovTimer=setInterval(fetchOverview,30000);
setInterval(tickAll,1000);
< / script >
< / body >
< / html >