Files
ai-team-dashboard/dashboard/public/index.html

2264 lines
146 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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()">&times;</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 ═══════════ */
async function fetchOverview(lite=false){
// 立即显示框架和骨架屏
if(!ovData) {
showOverviewFramework();
showSkeletonOverview();
}
try{
const r=await fetch(lite?'/api/status?lite=1':'/api/status');
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 模式渲染
const isLoading=!!ovData.lite;
const s=ovData.stats;const on=ovData.bots.filter(b=>b.status.running).length;
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>`;
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('');
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>`;}
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();
}
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:[]};
}
try{
const[r1,r2]=await Promise.all([
fetch('/api/monitor'),
fetch('/api/monitor/conversation?limit=20&botId=leader')
]);
monData=await r1.json();
convData=await r2.json();
if(activeTab==='monitor')renderMonitor();
(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);
}
})();
}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||{};
const loading=!!wl.loading||!!(workerConvData[b.id]&&workerConvData[b.id].loading);
const pm=wl.pollMeta;const on=!loading&&(st.running||st.state==='running');
let nextPoll='--';let nextPollMs=null;
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):'--';
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)||[];
const msgCount=loading?'--':msgs.length;
const taskCount=loading?'--':taskLogs.length;
const sysCount=loading?'--':sysLogs.length;
const mcps=wl.mcps||[];const skills=wl.installedSkills||[];
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>');
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>`;
}
function renderWorkerChatMsgs(msgs,bot,loading){
if(loading)return'<div class="empty-msg loading">加载中...</div>';
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();
fetchOverview(true);
if('requestIdleCallback' in window) requestIdleCallback(()=>fetchOverview(false));
else setTimeout(()=>fetchOverview(false),300);
ovTimer=setInterval(fetchOverview,30000);
setInterval(tickAll,1000);
</script>
</body>
</html>