2264 lines
146 KiB
HTML
2264 lines
146 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>AI Team 调度中心</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
:root, [data-theme="dark"]{
|
||
--bg:#0b0b10;--surface:#13131d;--surface2:#1c1c2e;--surface3:#24243a;
|
||
--border:#2a2a44;--border2:#363655;
|
||
--text:#e8e8f4;--text2:#a0a0be;--text3:#6a6a88;
|
||
--green:#22c55e;--yellow:#eab308;--red:#ef4444;--blue:#3b82f6;
|
||
--purple:#a78bfa;--cyan:#4ecdc4;--orange:#ff6b35;
|
||
--think-bg:rgba(234,179,8,.06);--think-border:rgba(234,179,8,.15);
|
||
--chat-fade:var(--surface);--chat-fade2:rgba(11,11,16,.95);
|
||
}
|
||
[data-theme="light"]{
|
||
--bg:#f5f5f8;--surface:#ffffff;--surface2:#f0f0f5;--surface3:#e8e8f0;
|
||
--border:#d8d8e4;--border2:#c8c8d8;
|
||
--text:#1a1a2e;--text2:#555570;--text3:#888898;
|
||
--green:#16a34a;--yellow:#ca8a04;--red:#dc2626;--blue:#2563eb;
|
||
--purple:#7c3aed;--cyan:#0891b2;--orange:#ea580c;
|
||
--think-bg:rgba(234,179,8,.08);--think-border:rgba(234,179,8,.25);
|
||
--chat-fade:var(--surface);--chat-fade2:rgba(245,245,248,.95);
|
||
}
|
||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC',sans-serif;background:var(--bg);color:var(--text);min-height:100vh}
|
||
::-webkit-scrollbar{width:5px;height:5px}
|
||
::-webkit-scrollbar-track{background:var(--surface)}
|
||
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
|
||
::-webkit-scrollbar-thumb:hover{background:var(--text3)}
|
||
|
||
/* ── Header ── */
|
||
.header{padding:10px 28px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:100}
|
||
.header h1{font-size:18px;font-weight:700;display:flex;align-items:center;gap:8px;cursor:pointer;flex-shrink:0}
|
||
.header h1 span{font-size:22px}
|
||
.header-right{display:flex;align-items:center;gap:12px}
|
||
.refresh-info{font-size:10px;color:var(--text3)}
|
||
.stats-bar{display:flex;gap:5px}
|
||
.stat-badge{padding:2px 8px;border-radius:14px;font-size:10px;font-weight:600;background:var(--surface2);border:1px solid var(--border)}
|
||
.stat-badge.green{color:var(--green);border-color:rgba(34,197,94,.25)}
|
||
.stat-badge.yellow{color:var(--yellow);border-color:rgba(234,179,8,.25)}
|
||
.stat-badge.blue{color:var(--blue);border-color:rgba(59,130,246,.25)}
|
||
.live-dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 1.5s infinite;display:inline-block}
|
||
|
||
/* ── Tabs (in header) ── */
|
||
.header-tabs{display:flex;gap:0;margin-left:18px}
|
||
.tab-btn{padding:6px 14px;font-size:11px;font-weight:600;color:var(--text3);background:transparent;border:1px solid transparent;border-radius:7px;cursor:pointer;transition:all .15s;display:flex;align-items:center;gap:4px;white-space:nowrap}
|
||
.tab-btn:hover{color:var(--text2);background:var(--surface2)}
|
||
.tab-btn.active{color:var(--cyan);background:var(--surface2);border-color:var(--border)}
|
||
.tab-content{display:none}
|
||
.tab-content.active{display:block}
|
||
|
||
/* ── Common ── */
|
||
.main{padding:16px 28px;display:flex;flex-direction:column;gap:16px}
|
||
.panel{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
||
.panel.full{grid-column:1/-1}
|
||
.panel-head{padding:8px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--surface2)}
|
||
.panel-head h3{font-size:11px;font-weight:700;display:flex;align-items:center;gap:5px}
|
||
.panel-head .cnt{font-size:10px;color:var(--text3);font-family:monospace}
|
||
.panel-head a,.panel-head .link{font-size:10px;color:var(--cyan);text-decoration:none;cursor:pointer}
|
||
.panel-head a:hover,.panel-head .link:hover{text-decoration:underline}
|
||
.panel-head .sub{font-size:10px;color:var(--text3)}
|
||
.panel-body{max-height:260px;overflow-y:auto}
|
||
.panel-body::-webkit-scrollbar{width:5px}
|
||
.panel-body::-webkit-scrollbar-track{background:var(--surface2)}
|
||
.panel-body::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
|
||
.empty-msg{padding:18px;text-align:center;color:var(--text3);font-size:11px}
|
||
|
||
/* ══════ 骨架屏加载动画 ══════ */
|
||
.skeleton{background:linear-gradient(90deg,var(--surface2) 0%,var(--surface3) 50%,var(--surface2) 100%);background-size:200% 100%;animation:skeleton-loading 1.5s ease-in-out infinite;border-radius:6px}
|
||
@keyframes skeleton-loading{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
||
.skeleton-panel{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;margin-bottom:10px}
|
||
.skeleton-header{height:40px;margin-bottom:12px;border-radius:10px}
|
||
.skeleton-row{height:48px;margin-bottom:8px;border-radius:8px}
|
||
.skeleton-card{height:120px;border-radius:10px}
|
||
.skeleton-stat{height:80px;border-radius:10px}
|
||
|
||
/* ══════ TAB 1: 总览 ══════ */
|
||
.col-toggles{display:flex;gap:6px;margin-bottom:12px}
|
||
.col-toggle{padding:6px 14px;border-radius:8px;font-size:11px;font-weight:600;cursor:pointer;border:1px solid var(--border);background:var(--surface);color:var(--text2);transition:all .15s;display:flex;align-items:center;gap:6px}
|
||
.col-toggle:hover{background:var(--surface2)}
|
||
.col-toggle.active{border-color:var(--cyan);color:var(--cyan);background:rgba(78,205,196,.08)}
|
||
.col-toggle .tog-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||
.col-toggle .tog-dot.on{background:var(--green);box-shadow:0 0 4px rgba(34,197,94,.4)}
|
||
.col-toggle .tog-dot.off{background:var(--text3)}
|
||
.bot-columns{display:flex;gap:14px;align-items:flex-start}
|
||
.bot-column{flex:1;min-width:0;display:flex;flex-direction:column;gap:10px;transition:flex .25s ease,opacity .2s ease}
|
||
.bot-column.hidden{display:none}
|
||
.col-header{display:flex;align-items:center;gap:6px;padding:6px 10px;background:var(--surface);border:1px solid var(--border);border-radius:10px}
|
||
.col-header .col-icon{font-size:20px;flex-shrink:0}
|
||
.col-header h2{font-size:13px;font-weight:700}
|
||
.col-header .col-role{font-size:9px;color:var(--text2);margin-left:2px}
|
||
.col-header .col-actions{margin-left:auto;display:flex;align-items:center;gap:6px}
|
||
.col-header .col-status{display:flex;align-items:center;gap:4px}
|
||
.col-header .col-dot{width:7px;height:7px;border-radius:50%}
|
||
.col-header .col-dot.healthy{background:var(--green);box-shadow:0 0 5px rgba(34,197,94,.4)}
|
||
.col-header .col-dot.unhealthy{background:var(--red)}
|
||
.col-header .col-dot.unknown{background:var(--text3)}
|
||
.col-header .col-status-text{font-size:9px;color:var(--text3)}
|
||
.restart-btn{border:1px solid rgba(239,68,68,.25);background:rgba(239,68,68,.08);color:var(--red);border-radius:8px;padding:4px 10px;font-size:10px;font-weight:700;cursor:pointer;transition:all .12s;white-space:nowrap}
|
||
.restart-btn:hover{background:rgba(239,68,68,.14)}
|
||
.restart-btn:disabled{opacity:.65;cursor:wait}
|
||
.restart-btn.compact{padding:3px 8px;font-size:9px}
|
||
.cap-row{display:flex;flex-wrap:wrap;gap:3px;padding:0 2px}
|
||
.cap{padding:2px 7px;border-radius:5px;font-size:9px;background:var(--surface2);color:var(--text2);border:1px solid var(--border)}
|
||
.t-row{padding:7px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:6px;font-size:11px;transition:background .1s;cursor:pointer}
|
||
.t-row:hover{background:var(--surface2)}
|
||
.t-row:last-child{border-bottom:none}
|
||
.t-num{color:var(--text3);font-family:monospace;min-width:20px;font-size:10px}
|
||
.t-title{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.t-badge{padding:1px 6px;border-radius:4px;font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:.3px;white-space:nowrap}
|
||
.t-badge.pending{background:rgba(234,179,8,.12);color:var(--yellow)}
|
||
.t-badge.in-progress{background:rgba(59,130,246,.12);color:var(--blue)}
|
||
.t-badge.done{background:rgba(34,197,94,.12);color:var(--green)}
|
||
.t-badge.blocked{background:rgba(239,68,68,.12);color:var(--red)}
|
||
.t-badge.dispatched{background:rgba(255,107,53,.12);color:var(--orange)}
|
||
.t-badge.accepted{background:rgba(78,205,196,.12);color:var(--cyan)}
|
||
.t-target{font-size:9px;color:var(--text3);white-space:nowrap}
|
||
.t-time{font-size:9px;color:var(--text3);white-space:nowrap}
|
||
.c-row{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;font-size:11px}
|
||
.c-row:last-child{border-bottom:none}
|
||
.c-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
||
.c-dot.on{background:var(--green)}.c-dot.off{background:var(--text3)}
|
||
.c-info{flex:1;min-width:0}
|
||
.c-name{font-weight:600;font-size:11px}
|
||
.c-desc{font-size:9px;color:var(--text3);margin-top:1px}
|
||
.c-right{text-align:right;flex-shrink:0}
|
||
.c-countdown{font-size:12px;font-weight:800;color:var(--cyan);font-family:'SF Mono',Menlo,monospace;letter-spacing:.5px}
|
||
.c-countdown.soon{color:var(--green);animation:pulse 1s infinite}
|
||
.c-last{font-size:9px;color:var(--text3)}
|
||
.commit-row{padding:7px 12px;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;gap:7px;font-size:10px;transition:background .1s}
|
||
.commit-row:hover{background:var(--surface2)}
|
||
.commit-row:last-child{border-bottom:none}
|
||
.commit-sha{font-family:'SF Mono',Menlo,monospace;font-size:9px;color:var(--cyan);background:var(--surface3);padding:1px 4px;border-radius:3px;flex-shrink:0;text-decoration:none}
|
||
.commit-sha:hover{color:var(--text);background:var(--border)}
|
||
.commit-msg{flex:1;color:var(--text2);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.commit-msg .cm-title{color:var(--text);font-weight:600}
|
||
.commit-time{font-size:9px;color:var(--text3);white-space:nowrap;flex-shrink:0}
|
||
.skill-row{padding:6px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:7px;font-size:11px;transition:background .1s}
|
||
.skill-row:hover{background:var(--surface2)}
|
||
.skill-row:last-child{border-bottom:none}
|
||
.skill-icon{font-size:13px;flex-shrink:0}
|
||
.skill-name{flex:1;color:var(--text);font-weight:500;min-width:0}
|
||
.skill-desc{font-size:9px;color:var(--text3);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.skill-link{font-size:9px;color:var(--cyan);text-decoration:none}
|
||
.skill-link:hover{text-decoration:underline}
|
||
.skill-ver{font-size:8px;color:var(--text3);font-weight:400;margin-left:3px}
|
||
.mini-stats{display:flex;gap:4px;padding:0 12px 8px}
|
||
.ms{flex:1;text-align:center;padding:5px 0;border-radius:6px;background:var(--surface3)}
|
||
.ms .n{font-size:14px;font-weight:800}
|
||
.ms .l{font-size:8px;color:var(--text3);margin-top:1px}
|
||
.ms.p .n{color:var(--yellow)}.ms.w .n{color:var(--blue)}.ms.d .n{color:var(--green)}.ms.b .n{color:var(--red)}
|
||
/* Sub-page */
|
||
.sub-page{display:none;padding:16px 28px}
|
||
.sub-page.active{display:block}
|
||
.main.hidden{display:none}
|
||
.back-btn{display:inline-flex;align-items:center;gap:5px;font-size:12px;color:var(--cyan);cursor:pointer;padding:5px 10px;border-radius:7px;background:var(--surface);border:1px solid var(--border);margin-bottom:14px;transition:background .1s}
|
||
.back-btn:hover{background:var(--surface2)}
|
||
.detail-hero{display:flex;align-items:center;gap:14px;padding:16px;background:var(--surface);border:1px solid var(--border);border-radius:12px;margin-bottom:16px}
|
||
.detail-avatar{width:56px;height:56px;border-radius:14px;display:flex;align-items:center;justify-content:center;font-size:32px}
|
||
.detail-info h2{font-size:20px;font-weight:800}
|
||
.detail-info .role{font-size:12px;color:var(--text2);margin-top:2px}
|
||
.detail-repos{display:flex;gap:6px;margin-top:6px}
|
||
.detail-repos a{font-size:10px;color:var(--cyan);text-decoration:none;padding:2px 7px;border-radius:5px;background:var(--surface3);border:1px solid var(--border)}
|
||
.detail-repos a:hover{background:var(--border)}
|
||
.detail-status{margin-left:auto;text-align:right}
|
||
.detail-status .big-dot{width:12px;height:12px;border-radius:50%;display:inline-block;vertical-align:middle;margin-right:5px}
|
||
.detail-status .big-dot.healthy{background:var(--green);box-shadow:0 0 8px rgba(34,197,94,.5)}
|
||
.detail-status .big-dot.unhealthy{background:var(--red)}
|
||
.detail-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||
.detail-grid .panel-body{max-height:400px}
|
||
/* Modal */
|
||
.modal-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200;justify-content:center;align-items:center}
|
||
.modal-overlay.active{display:flex}
|
||
.modal{background:var(--surface);border:1px solid var(--border);border-radius:14px;width:90%;max-width:680px;max-height:80vh;overflow:hidden;display:flex;flex-direction:column}
|
||
.modal-head{padding:14px 18px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;background:var(--surface2)}
|
||
.modal-head h2{font-size:14px;font-weight:700}
|
||
.modal-close{width:26px;height:26px;border:none;background:var(--surface3);border-radius:6px;color:var(--text2);cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center}
|
||
.modal-close:hover{background:var(--border);color:var(--text)}
|
||
.modal-body{padding:16px;overflow-y:auto;flex:1}
|
||
.modal-body h3{font-size:12px;font-weight:700;color:var(--text2);margin:14px 0 6px;padding-top:10px;border-top:1px solid var(--border)}
|
||
.modal-body h3:first-child{margin-top:0;padding-top:0;border-top:none}
|
||
.modal-body pre{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:10px;font-size:11px;line-height:1.5;overflow-x:auto;white-space:pre-wrap;word-break:break-all;color:var(--text2);margin:6px 0}
|
||
.comment-item{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:10px;margin:6px 0}
|
||
.comment-meta{font-size:9px;color:var(--text3);margin-bottom:4px}
|
||
.comment-body{font-size:11px;color:var(--text2);line-height:1.5;white-space:pre-wrap;word-break:break-all}
|
||
|
||
/* ══════ TAB 2: 监控 ══════ */
|
||
.mon-status-bar{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
|
||
.status-card{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px;text-align:center}
|
||
.status-card .label{font-size:9px;color:var(--text3);text-transform:uppercase;letter-spacing:.8px}
|
||
.status-card .value{font-size:24px;font-weight:800;margin-top:3px}
|
||
.status-card .sub2{font-size:10px;color:var(--text3);margin-top:1px}
|
||
.status-card.ok .value{color:var(--green)}
|
||
.status-card.warn .value{color:var(--yellow)}
|
||
.status-card.err .value{color:var(--red)}
|
||
.status-card.info .value{color:var(--cyan)}
|
||
.mon-panels{display:grid;grid-template-columns:1fr 1fr;gap:14px}
|
||
/* Chat */
|
||
.chat-body{max-height:500px;overflow-y:auto;padding:10px 0}
|
||
.chat-msg{padding:7px 14px;margin:3px 0}
|
||
.chat-msg.user{border-left:3px solid var(--blue)}
|
||
.chat-msg.assistant{border-left:3px solid var(--green)}
|
||
.chat-role{font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:.4px;margin-bottom:2px;display:flex;align-items:center;gap:5px}
|
||
.chat-msg.user .chat-role{color:var(--blue)}
|
||
.chat-msg.assistant .chat-role{color:var(--green)}
|
||
.chat-role .chat-time{font-weight:400;color:var(--text3);letter-spacing:0}
|
||
.chat-text{font-size:11px;line-height:1.6;color:var(--text);white-space:pre-wrap;word-break:break-word}
|
||
.chat-text.collapsed{max-height:100px;overflow:hidden;position:relative}
|
||
.chat-text.collapsed::after{content:'';position:absolute;bottom:0;left:0;right:0;height:35px;background:linear-gradient(transparent,var(--chat-fade))}
|
||
.chat-expand{font-size:9px;color:var(--cyan);cursor:pointer;margin-top:3px;display:inline-block}
|
||
.chat-expand:hover{text-decoration:underline}
|
||
.think-block{margin:3px 0 6px;padding:7px 10px;border-radius:7px;background:var(--think-bg);border:1px solid var(--think-border);font-size:10px;line-height:1.5;color:var(--yellow);white-space:pre-wrap;word-break:break-word}
|
||
.think-block.collapsed{max-height:70px;overflow:hidden;position:relative}
|
||
.think-block.collapsed::after{content:'';position:absolute;bottom:0;left:0;right:0;height:28px;background:linear-gradient(transparent,var(--chat-fade2))}
|
||
.think-label{font-size:8px;font-weight:700;color:var(--yellow);opacity:.7;margin-bottom:2px;display:flex;align-items:center;gap:4px}
|
||
/* Log */
|
||
.log-entry{padding:5px 12px;border-bottom:1px solid var(--border);font-size:10px;font-family:'SF Mono',Menlo,monospace;display:flex;gap:6px;line-height:1.5}
|
||
.log-entry:last-child{border-bottom:none}
|
||
.log-time{color:var(--text3);flex-shrink:0;min-width:54px}
|
||
.log-tag{padding:0 4px;border-radius:3px;font-size:8px;font-weight:700;flex-shrink:0;text-align:center;min-width:30px;line-height:16px}
|
||
.log-tag.incoming{background:rgba(59,130,246,.15);color:var(--blue)}
|
||
.log-tag.processing{background:rgba(234,179,8,.15);color:var(--yellow)}
|
||
.log-tag.complete{background:rgba(34,197,94,.15);color:var(--green)}
|
||
.log-tag.streaming{background:rgba(167,139,250,.15);color:var(--purple)}
|
||
.log-tag.stream_done{background:rgba(78,205,196,.15);color:var(--cyan)}
|
||
.log-tag.warn{background:rgba(234,179,8,.15);color:var(--yellow)}
|
||
.log-tag.error{background:rgba(239,68,68,.15);color:var(--red)}
|
||
.log-content{flex:1;color:var(--text2);word-break:break-all}
|
||
/* Timeline */
|
||
.tl-entry{padding:8px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;font-size:11px}
|
||
.tl-entry:last-child{border-bottom:none}
|
||
.tl-status{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||
.tl-status.done{background:var(--green)}
|
||
.tl-status.thinking{background:var(--yellow);animation:pulse 1s infinite}
|
||
.tl-status.streaming{background:var(--purple);animation:pulse .7s infinite}
|
||
.tl-status.queued{background:var(--text3)}
|
||
.tl-msg{flex:1;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.tl-dur{font-family:'SF Mono',Menlo,monospace;font-size:10px;font-weight:700;flex-shrink:0;min-width:36px;text-align:right}
|
||
.tl-dur.fast{color:var(--green)}.tl-dur.medium{color:var(--yellow)}.tl-dur.slow{color:var(--orange)}
|
||
.tl-time{font-size:9px;color:var(--text3);flex-shrink:0}
|
||
.tl-entry.active-msg{background:rgba(234,179,8,.06);border-left:2px solid var(--yellow)}
|
||
/* Workers / Cron (monitor tab) */
|
||
.worker-row{display:flex;gap:10px;padding:8px 12px}
|
||
.worker-card{flex:1;background:var(--surface3);border-radius:8px;padding:10px;display:flex;align-items:center;gap:8px}
|
||
.worker-card .w-avatar{font-size:20px}
|
||
.worker-card .w-info{flex:1}
|
||
.worker-card .w-name{font-size:12px;font-weight:700}
|
||
.worker-card .w-sub{font-size:9px;color:var(--text3)}
|
||
.worker-card .w-dot{width:7px;height:7px;border-radius:50%;display:inline-block}
|
||
.worker-card .w-dot.on{background:var(--green)}.worker-card .w-dot.off{background:var(--red)}
|
||
.worker-card .w-poll{font-size:9px;color:var(--cyan);font-family:monospace;margin-top:1px}
|
||
.mcron-entry{padding:7px 12px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;font-size:11px}
|
||
.mcron-entry:last-child{border-bottom:none}
|
||
.mcron-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
|
||
.mcron-dot.on{background:var(--green)}.mcron-dot.off{background:var(--text3)}
|
||
.mcron-name{flex:1;font-weight:600}
|
||
.mcron-next{font-family:monospace;font-size:10px;color:var(--cyan)}
|
||
/* System */
|
||
.meter-row{display:flex;gap:14px;padding:12px}
|
||
.meter{flex:1;text-align:center}
|
||
.meter .m-label{font-size:9px;color:var(--text3);margin-bottom:5px}
|
||
.meter .m-bar{height:5px;border-radius:3px;background:var(--surface3);overflow:hidden}
|
||
.meter .m-fill{height:100%;border-radius:3px;transition:width .5s}
|
||
.meter .m-val{font-size:13px;font-weight:800;margin-top:3px}
|
||
|
||
/* Turn status badges */
|
||
.turn-badge{font-size:9px;padding:1px 6px;border-radius:4px;margin-left:6px;font-weight:600;vertical-align:middle}
|
||
.turn-badge.final{background:rgba(34,197,94,.15);color:var(--green)}
|
||
.turn-badge.working{background:rgba(234,179,8,.15);color:var(--yellow)}
|
||
.turn-badge.text-only{background:rgba(239,68,68,.15);color:var(--red)}
|
||
.status-card.err{border-color:rgba(239,68,68,.3)}
|
||
.status-card.err .value{color:var(--red)}
|
||
|
||
/* Theme toggle */
|
||
.theme-btn{width:30px;height:30px;border-radius:8px;border:1px solid var(--border);background:var(--surface2);cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:background .15s}
|
||
.theme-btn:hover{background:var(--surface3)}
|
||
|
||
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
||
.loading{animation:pulse 1.2s infinite}
|
||
@media(max-width:1100px){.bot-columns{flex-direction:column}.detail-grid,.mon-panels{grid-template-columns:1fr}.mon-status-bar{grid-template-columns:repeat(2,1fr)}}
|
||
|
||
/* ══════════════════════════════════════════════════════════
|
||
App Store 风格 UI —— macOS 桌面端左右布局
|
||
通过 body.app-mode 激活
|
||
══════════════════════════════════════════════════════════ */
|
||
|
||
/* ── 布局切换按钮 ── */
|
||
.layout-btn{width:30px;height:30px;border-radius:8px;border:1px solid var(--border);background:var(--surface2);cursor:pointer;font-size:14px;display:flex;align-items:center;justify-content:center;transition:background .15s}
|
||
.layout-btn:hover{background:var(--surface3)}
|
||
|
||
/* ── 过渡动画 ── */
|
||
body{transition:background .3s}
|
||
.tab-content,.main,.header,.panel,.bot-column,.sub-page{transition:all .3s ease}
|
||
|
||
/* ── App 模式:全局控制 ── */
|
||
body.app-mode{background:var(--bg)}
|
||
body.app-mode .app-container{display:flex}
|
||
body:not(.app-mode) .app-container{display:none}
|
||
body.app-mode #tab-overview > .main,
|
||
body.app-mode #tab-overview > .sub-page,
|
||
body.app-mode #tab-monitor > .main{display:none!important}
|
||
|
||
/* ── App 模式:Header ── */
|
||
body.app-mode .header{
|
||
padding:10px 20px;
|
||
background:var(--surface);
|
||
backdrop-filter:saturate(180%) blur(20px);
|
||
-webkit-backdrop-filter:saturate(180%) blur(20px);
|
||
border-bottom:1px solid var(--border);
|
||
}
|
||
|
||
/* ══ App 容器:左右双栏 ══ */
|
||
.app-container{
|
||
display:flex;height:calc(100vh - 51px);overflow:hidden;
|
||
}
|
||
|
||
/* ── 左侧边栏 ── */
|
||
.app-sidebar{
|
||
width:220px;flex-shrink:0;
|
||
background:var(--surface);border-right:1px solid var(--border);
|
||
overflow-y:auto;padding:16px 0;
|
||
display:flex;flex-direction:column;
|
||
}
|
||
.app-sidebar-section{padding:0 12px;margin-bottom:16px}
|
||
.app-sidebar-label{
|
||
font-size:9px;font-weight:700;color:var(--text3);text-transform:uppercase;
|
||
letter-spacing:1px;padding:0 8px;margin-bottom:6px;
|
||
}
|
||
.app-sidebar-item{
|
||
display:flex;align-items:center;gap:10px;
|
||
padding:8px 12px;border-radius:8px;cursor:pointer;
|
||
font-size:13px;font-weight:500;color:var(--text2);
|
||
transition:all .12s;margin-bottom:2px;
|
||
}
|
||
.app-sidebar-item:hover{background:var(--surface2);color:var(--text)}
|
||
.app-sidebar-item.active{background:var(--surface3);color:var(--text);font-weight:600}
|
||
.app-sidebar-item .si-icon{font-size:18px;width:24px;text-align:center;flex-shrink:0}
|
||
.app-sidebar-item .si-name{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.app-sidebar-item .si-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||
.app-sidebar-item .si-dot.on{background:var(--green);box-shadow:0 0 4px rgba(34,197,94,.5)}
|
||
.app-sidebar-item .si-dot.off{background:var(--red)}
|
||
.app-sidebar-item .si-badge{
|
||
font-size:9px;font-weight:700;padding:1px 6px;border-radius:10px;
|
||
background:var(--surface3);color:var(--text3);min-width:18px;text-align:center;
|
||
}
|
||
.app-sidebar-divider{height:1px;background:var(--border);margin:8px 12px}
|
||
|
||
/* ── 右侧主内容区 ── */
|
||
.app-content{
|
||
flex:1;overflow-y:auto;padding:24px 28px;min-width:0;
|
||
}
|
||
|
||
/* ── 区域标题 ── */
|
||
.app-section-title{
|
||
font-size:22px;font-weight:800;margin:28px 0 14px;
|
||
color:var(--text);letter-spacing:-.3px;
|
||
}
|
||
.app-section-title:first-child{margin-top:0}
|
||
.app-section-sub{
|
||
font-size:13px;color:var(--text3);font-weight:500;margin:-8px 0 14px;
|
||
}
|
||
|
||
/* ══════ Bot 卡片行(App Store 顶部 App 列表风格) ══════ */
|
||
.app-listing-row{
|
||
display:flex;align-items:center;gap:14px;
|
||
padding:14px 16px;border-radius:12px;
|
||
background:var(--surface);border:1px solid var(--border);
|
||
cursor:pointer;transition:all .15s;margin-bottom:10px;
|
||
}
|
||
.app-listing-row:hover{background:var(--surface2);box-shadow:0 2px 12px rgba(0,0,0,.08)}
|
||
.app-listing-icon{
|
||
width:56px;height:56px;border-radius:14px;
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:30px;flex-shrink:0;
|
||
}
|
||
.app-listing-info{flex:1;min-width:0}
|
||
.app-listing-name{font-size:15px;font-weight:700}
|
||
.app-listing-role{font-size:11px;color:var(--text3);margin-top:1px}
|
||
.app-listing-caps{display:flex;flex-wrap:wrap;gap:3px;margin-top:4px}
|
||
.app-listing-caps .cap{padding:1px 6px;border-radius:6px;font-size:8px;background:var(--surface3);color:var(--text3);border:1px solid var(--border)}
|
||
.app-listing-right{display:flex;flex-direction:column;align-items:center;gap:4px;flex-shrink:0}
|
||
.app-listing-btn{
|
||
padding:5px 16px;border-radius:16px;font-size:12px;font-weight:700;
|
||
border:none;cursor:pointer;transition:all .12s;
|
||
}
|
||
.app-listing-btn.online{background:rgba(34,197,94,.15);color:var(--green)}
|
||
.app-listing-btn.offline{background:rgba(239,68,68,.12);color:var(--red)}
|
||
.app-listing-btn:hover{filter:brightness(1.1)}
|
||
.app-listing-stats{display:flex;gap:8px;font-size:9px;color:var(--text3)}
|
||
.app-listing-stats span{display:flex;align-items:center;gap:2px}
|
||
|
||
/* ══════ 2 列特色大卡片网格 ══════ */
|
||
.app-feature-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
|
||
.app-feature-card{
|
||
background:var(--surface);border:1px solid var(--border);border-radius:16px;
|
||
overflow:hidden;transition:transform .15s,box-shadow .15s;
|
||
}
|
||
.app-feature-card:hover{transform:translateY(-2px);box-shadow:0 6px 24px rgba(0,0,0,.12)}
|
||
.app-feature-card.full{grid-column:1/-1}
|
||
.app-feature-head{
|
||
padding:12px 16px;display:flex;align-items:center;justify-content:space-between;
|
||
border-bottom:1px solid var(--border);background:var(--surface2);
|
||
}
|
||
.app-feature-head h3{font-size:13px;font-weight:700;display:flex;align-items:center;gap:6px}
|
||
.app-feature-head .af-link{font-size:11px;color:var(--cyan);cursor:pointer;text-decoration:none}
|
||
.app-feature-head .af-link:hover{text-decoration:underline}
|
||
.app-feature-head .af-cnt{font-size:10px;color:var(--text3);font-family:monospace}
|
||
.app-feature-body{padding:12px 16px;max-height:360px;overflow-y:auto}
|
||
|
||
/* ── 任务行(在特色卡内) ── */
|
||
.app-task-row{
|
||
padding:8px 0;border-bottom:1px solid var(--border);
|
||
display:flex;align-items:center;gap:8px;font-size:12px;
|
||
cursor:pointer;transition:background .1s;
|
||
}
|
||
.app-task-row:last-child{border-bottom:none}
|
||
.app-task-row:hover{background:var(--surface2);border-radius:6px;padding-left:6px;margin-left:-6px}
|
||
.app-task-num{font-family:'SF Mono',Menlo,monospace;font-size:10px;color:var(--text3);min-width:28px}
|
||
.app-task-title{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:500}
|
||
.app-task-badge{padding:2px 8px;border-radius:10px;font-size:9px;font-weight:700}
|
||
.app-task-badge.pending{background:rgba(234,179,8,.12);color:var(--yellow)}
|
||
.app-task-badge.in-progress{background:rgba(59,130,246,.12);color:var(--blue)}
|
||
.app-task-badge.done{background:rgba(34,197,94,.12);color:var(--green)}
|
||
.app-task-badge.blocked{background:rgba(239,68,68,.12);color:var(--red)}
|
||
.app-task-badge.dispatched{background:rgba(255,107,53,.12);color:var(--orange)}
|
||
.app-task-badge.accepted{background:rgba(78,205,196,.12);color:var(--cyan)}
|
||
.app-task-target{font-size:10px;color:var(--text3);white-space:nowrap}
|
||
.app-task-time{font-size:10px;color:var(--text3);white-space:nowrap}
|
||
|
||
/* ── 提交行 ── */
|
||
.app-commit-row{
|
||
padding:8px 0;border-bottom:1px solid var(--border);
|
||
display:flex;align-items:flex-start;gap:8px;font-size:11px;
|
||
}
|
||
.app-commit-row:last-child{border-bottom:none}
|
||
.app-commit-sha{
|
||
font-family:'SF Mono',Menlo,monospace;font-size:10px;
|
||
color:var(--cyan);background:var(--surface3);
|
||
padding:2px 6px;border-radius:4px;flex-shrink:0;text-decoration:none;
|
||
}
|
||
.app-commit-sha:hover{color:var(--text);background:var(--border)}
|
||
.app-commit-msg{flex:1;color:var(--text2);line-height:1.4;font-weight:500;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
.app-commit-time{font-size:10px;color:var(--text3);white-space:nowrap;flex-shrink:0}
|
||
|
||
/* ── Cron 行 ── */
|
||
.app-cron-row{
|
||
padding:8px 0;border-bottom:1px solid var(--border);
|
||
display:flex;align-items:center;gap:10px;
|
||
}
|
||
.app-cron-row:last-child{border-bottom:none}
|
||
.app-cron-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||
.app-cron-dot.on{background:var(--green)}.app-cron-dot.off{background:var(--text3)}
|
||
.app-cron-info{flex:1;min-width:0}
|
||
.app-cron-name{font-size:12px;font-weight:600}
|
||
.app-cron-desc{font-size:10px;color:var(--text3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.app-cron-right{text-align:right;flex-shrink:0}
|
||
.app-cron-countdown{font-size:14px;font-weight:800;color:var(--cyan);font-family:'SF Mono',Menlo,monospace}
|
||
.app-cron-countdown.soon{color:var(--green);animation:pulse 1s infinite}
|
||
.app-cron-countdown.paused{color:var(--text3);font-size:11px;font-weight:600}
|
||
.app-cron-last{font-size:9px;color:var(--text3);margin-top:1px}
|
||
|
||
/* ══════ 工具 & 技能网格 ══════ */
|
||
.app-tools-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:10px}
|
||
.app-tool-card{
|
||
background:var(--surface);border:1px solid var(--border);border-radius:14px;
|
||
padding:12px 8px;display:flex;flex-direction:column;align-items:center;
|
||
gap:5px;text-align:center;transition:transform .15s,box-shadow .15s;
|
||
}
|
||
.app-tool-card:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.1)}
|
||
.app-tool-icon{
|
||
width:40px;height:40px;border-radius:10px;
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:20px;background:var(--surface3);
|
||
}
|
||
.app-tool-name{font-size:10px;font-weight:600;line-height:1.3;word-break:break-all}
|
||
.app-tool-ver{font-size:8px;color:var(--text3)}
|
||
|
||
.app-skills-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
|
||
.app-skill-card{
|
||
background:var(--surface);border:1px solid var(--border);border-radius:14px;
|
||
padding:12px;display:flex;align-items:flex-start;gap:10px;
|
||
transition:transform .15s,box-shadow .15s;
|
||
}
|
||
.app-skill-card:hover{transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.1)}
|
||
.app-skill-icon{
|
||
width:36px;height:36px;border-radius:9px;flex-shrink:0;
|
||
display:flex;align-items:center;justify-content:center;
|
||
font-size:18px;background:var(--surface3);
|
||
}
|
||
.app-skill-info{flex:1;min-width:0}
|
||
.app-skill-name{font-size:11px;font-weight:700;line-height:1.3}
|
||
.app-skill-desc{font-size:9px;color:var(--text3);margin-top:2px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||
|
||
/* ══════ 监控页 ══════ */
|
||
.app-status-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
|
||
.app-status-card{
|
||
background:var(--surface);border:1px solid var(--border);border-radius:14px;
|
||
padding:16px;text-align:center;transition:transform .15s;
|
||
}
|
||
.app-status-card:hover{transform:scale(1.02)}
|
||
.app-status-card .as-label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.8px}
|
||
.app-status-card .as-value{font-size:28px;font-weight:800;margin-top:4px}
|
||
.app-status-card .as-sub{font-size:10px;color:var(--text3);margin-top:2px}
|
||
.app-status-card.ok .as-value{color:var(--green)}
|
||
.app-status-card.warn .as-value{color:var(--yellow)}
|
||
.app-status-card.err .as-value{color:var(--red)}
|
||
.app-status-card.info .as-value{color:var(--cyan)}
|
||
|
||
/* 对话 + 日志 + 时间线 */
|
||
.app-feature-body:has(.app-chat-body){max-height:none;overflow:visible}
|
||
.app-chat-body{display:flex;flex-direction:column;gap:8px;padding:8px 0;max-height:500px;overflow-y:auto}
|
||
.app-chat-bubble{
|
||
max-width:75%;padding:10px 14px;border-radius:18px;
|
||
font-size:12px;line-height:1.6;word-break:break-word;white-space:pre-wrap;
|
||
}
|
||
.app-chat-bubble.user{align-self:flex-end;background:var(--blue);color:#fff;border-bottom-right-radius:4px}
|
||
.app-chat-bubble.assistant{align-self:flex-start;background:var(--surface2);color:var(--text);border:1px solid var(--border);border-bottom-left-radius:4px}
|
||
.app-chat-sender{font-size:9px;font-weight:700;margin-bottom:3px;opacity:.7}
|
||
.app-chat-bubble.user .app-chat-sender{color:rgba(255,255,255,.8)}
|
||
.app-chat-bubble.assistant .app-chat-sender{color:var(--text3)}
|
||
.app-chat-time-label{font-size:9px;color:var(--text3);text-align:center;margin:6px 0}
|
||
.app-chat-bubble .app-think-inline{
|
||
margin:4px 0 6px;padding:6px 10px;border-radius:10px;
|
||
background:var(--think-bg);border:1px solid var(--think-border);
|
||
font-size:10px;color:var(--yellow);line-height:1.5;
|
||
}
|
||
.app-chat-bubble .app-think-inline.collapsed{max-height:60px;overflow:hidden;position:relative}
|
||
.app-chat-bubble .app-think-inline.collapsed::after{
|
||
content:'';position:absolute;bottom:0;left:0;right:0;height:24px;
|
||
background:linear-gradient(transparent,var(--surface2));
|
||
}
|
||
.app-chat-turn-badge{display:inline-block;font-size:8px;padding:1px 6px;border-radius:8px;margin-left:4px;font-weight:700;vertical-align:middle}
|
||
.app-chat-turn-badge.final{background:rgba(34,197,94,.15);color:var(--green)}
|
||
.app-chat-turn-badge.working{background:rgba(234,179,8,.15);color:var(--yellow)}
|
||
.app-chat-turn-badge.text-only{background:rgba(239,68,68,.15);color:var(--red)}
|
||
|
||
.app-log-list{display:flex;flex-direction:column;gap:1px;padding:4px 0}
|
||
.app-log-entry{padding:6px 0;display:flex;gap:8px;align-items:flex-start;font-size:11px;font-family:'SF Mono',Menlo,monospace}
|
||
.app-log-entry:hover{background:var(--surface2);border-radius:4px}
|
||
.app-log-time{color:var(--text3);flex-shrink:0;font-size:10px}
|
||
.app-log-tag{padding:1px 5px;border-radius:4px;font-size:9px;font-weight:700;flex-shrink:0;min-width:30px;text-align:center}
|
||
.app-log-tag.incoming{background:rgba(59,130,246,.15);color:var(--blue)}
|
||
.app-log-tag.processing{background:rgba(234,179,8,.15);color:var(--yellow)}
|
||
.app-log-tag.complete{background:rgba(34,197,94,.15);color:var(--green)}
|
||
.app-log-tag.streaming{background:rgba(167,139,250,.15);color:var(--purple)}
|
||
.app-log-tag.stream_done{background:rgba(78,205,196,.15);color:var(--cyan)}
|
||
.app-log-tag.warn{background:rgba(234,179,8,.15);color:var(--yellow)}
|
||
.app-log-tag.error{background:rgba(239,68,68,.15);color:var(--red)}
|
||
.app-log-content{flex:1;color:var(--text2);word-break:break-all;line-height:1.5}
|
||
|
||
.app-timeline{display:flex;flex-direction:column;gap:0;padding:4px 0}
|
||
.app-tl-entry{padding:8px 0;display:flex;align-items:center;gap:8px;font-size:12px}
|
||
.app-tl-entry:hover{background:var(--surface2);border-radius:4px}
|
||
.app-tl-entry.active-msg{background:rgba(234,179,8,.06);border-left:3px solid var(--yellow);padding-left:6px}
|
||
.app-tl-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||
.app-tl-dot.done{background:var(--green)}
|
||
.app-tl-dot.thinking{background:var(--yellow);animation:pulse 1s infinite}
|
||
.app-tl-dot.streaming{background:var(--purple);animation:pulse .7s infinite}
|
||
.app-tl-dot.queued{background:var(--text3)}
|
||
.app-tl-msg{flex:1;color:var(--text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.app-tl-dur{font-family:'SF Mono',Menlo,monospace;font-size:10px;font-weight:700;flex-shrink:0;min-width:36px;text-align:right}
|
||
.app-tl-dur.fast{color:var(--green)}.app-tl-dur.medium{color:var(--yellow)}.app-tl-dur.slow{color:var(--orange)}
|
||
.app-tl-time{font-size:10px;color:var(--text3);flex-shrink:0}
|
||
|
||
/* 圆形进度环 */
|
||
.app-resource-row{display:flex;gap:24px;justify-content:flex-start;padding:10px 0}
|
||
.app-resource-ring{display:flex;flex-direction:column;align-items:center;gap:6px}
|
||
.app-ring-svg{width:72px;height:72px;transform:rotate(-90deg)}
|
||
.app-ring-bg{fill:none;stroke:var(--surface3);stroke-width:5}
|
||
.app-ring-fill{fill:none;stroke-width:5;stroke-linecap:round;transition:stroke-dashoffset .6s ease}
|
||
.app-ring-label{font-size:10px;color:var(--text3);text-transform:uppercase;letter-spacing:.5px}
|
||
.app-ring-val{font-size:16px;font-weight:800}
|
||
|
||
/* Worker 卡 */
|
||
.app-worker-list{display:flex;gap:12px;flex-wrap:wrap}
|
||
.app-worker-card{
|
||
flex:1;min-width:200px;
|
||
background:var(--surface);border:1px solid var(--border);border-radius:14px;
|
||
padding:14px 16px;display:flex;align-items:center;gap:12px;
|
||
cursor:pointer;transition:all .15s;
|
||
}
|
||
.app-worker-card:hover{background:var(--surface2);box-shadow:0 2px 12px rgba(0,0,0,.08)}
|
||
.app-worker-avatar{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:22px}
|
||
.app-worker-info{flex:1;min-width:0}
|
||
.app-worker-name{font-size:13px;font-weight:700}
|
||
.app-worker-sub{font-size:10px;color:var(--text3);margin-top:1px}
|
||
.app-worker-status{display:flex;align-items:center;gap:5px}
|
||
.app-worker-dot{width:7px;height:7px;border-radius:50%}
|
||
.app-worker-dot.on{background:var(--green);box-shadow:0 0 4px rgba(34,197,94,.4)}
|
||
.app-worker-dot.off{background:var(--red)}
|
||
|
||
/* ── 空状态 ── */
|
||
.app-empty{padding:24px;text-align:center;color:var(--text3);font-size:12px}
|
||
|
||
/* ── 响应式 ── */
|
||
@media(max-width:900px){
|
||
.app-sidebar{width:180px}
|
||
.app-content{padding:16px 18px}
|
||
.app-feature-grid{grid-template-columns:1fr}
|
||
.app-status-grid{grid-template-columns:repeat(2,1fr)}
|
||
}
|
||
@media(max-width:600px){
|
||
.app-container{flex-direction:column}
|
||
.app-sidebar{width:100%;height:auto;max-height:140px;border-right:none;border-bottom:1px solid var(--border);flex-direction:row;overflow-x:auto;padding:8px}
|
||
.app-sidebar-section{display:flex;gap:4px;margin:0}
|
||
.app-sidebar-label{display:none}
|
||
.app-sidebar-divider{display:none}
|
||
.app-content{height:auto;overflow-y:visible}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<h1 onclick="goHome()"><span>🦞</span> AI Team 调度中心</h1>
|
||
<div class="header-tabs" id="tabBar">
|
||
<button class="tab-btn active" onclick="switchTab('overview')">📊 总览面板</button>
|
||
<button class="tab-btn" onclick="switchTab('monitor')">📡 实时监控</button>
|
||
<button class="tab-btn" onclick="switchTab('stats')">📈 数据统计</button>
|
||
<button class="tab-btn" onclick="switchTab('memory')">🧠 每日记忆</button>
|
||
</div>
|
||
<div class="header-right">
|
||
<div class="stats-bar" id="globalStats"></div>
|
||
<span class="live-dot"></span>
|
||
<button class="layout-btn" onclick="toggleLayoutMode()" id="layoutBtn" title="切换布局">💻</button>
|
||
<button class="theme-btn" onclick="toggleTheme()" id="themeBtn" title="切换主题">🌙</button>
|
||
<div class="refresh-info" id="refreshInfo">加载中...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 1: 总览 ═══ -->
|
||
<div class="tab-content active" id="tab-overview">
|
||
<div class="main" id="mainView">
|
||
<div class="col-toggles" id="colToggles"></div>
|
||
<div class="bot-columns" id="botColumns"><div class="empty-msg loading">加载数据中...</div></div>
|
||
</div>
|
||
<div class="sub-page" id="botDetailPage"></div>
|
||
<div class="app-container" id="appOverview"></div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 2: 监控 ═══ -->
|
||
<div class="tab-content" id="tab-monitor">
|
||
<div class="main">
|
||
<div class="col-toggles" id="monColToggles"></div>
|
||
<div class="bot-columns" id="monColumns"></div>
|
||
<div id="monitorContent" style="margin-top:14px"></div>
|
||
</div>
|
||
<div class="app-container" id="appMonitor"></div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 3: 数据统计 ═══ -->
|
||
<div class="tab-content" id="tab-stats">
|
||
<div class="main">
|
||
<div style="margin-bottom:16px">
|
||
<h2 style="font-size:18px;font-weight:800;margin-bottom:6px">📈 API 调用统计 & Token 监控</h2>
|
||
<p style="font-size:11px;color:var(--text3)">实时监控各 Bot 的 API 调用次数和 Token 使用量</p>
|
||
</div>
|
||
<div id="statsCards"></div>
|
||
<div id="statsContent"></div>
|
||
</div>
|
||
<div class="app-container" id="appStats"></div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 4: 每日记忆 ═══ -->
|
||
<div class="tab-content" id="tab-memory">
|
||
<div class="main">
|
||
<div style="margin-bottom:16px;display:flex;align-items:center;justify-content:space-between">
|
||
<div>
|
||
<h2 style="font-size:18px;font-weight:800;margin-bottom:6px">🧠 每日记忆 & 成长复盘</h2>
|
||
<p style="font-size:11px;color:var(--text3)">记录每个机器人每天的学习内容、成就和挑战,方便复盘与成长</p>
|
||
</div>
|
||
<div style="display:flex;gap:8px;align-items:center">
|
||
<label style="font-size:11px;color:var(--text3)">选择日期:</label>
|
||
<input type="date" id="memoryDatePicker" style="padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--surface2);color:var(--text);font-size:11px;cursor:pointer" />
|
||
<button onclick="loadMemoryByDate()" style="padding:6px 14px;border:1px solid var(--border);border-radius:6px;background:var(--surface);color:var(--text);font-size:11px;cursor:pointer;font-weight:600;transition:background .15s" onmouseover="this.style.background='var(--surface2)'" onmouseout="this.style.background='var(--surface)'">查看</button>
|
||
<button onclick="loadTodayMemory()" style="padding:6px 14px;border:1px solid var(--cyan);border-radius:6px;background:rgba(78,205,196,.08);color:var(--cyan);font-size:11px;cursor:pointer;font-weight:600;transition:all .15s" onmouseover="this.style.background='rgba(78,205,196,.15)'" onmouseout="this.style.background='rgba(78,205,196,.08)'">今天</button>
|
||
</div>
|
||
</div>
|
||
<div id="memoryContent"></div>
|
||
</div>
|
||
<div class="app-container" id="appMemory"></div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 2: 监控 (continued system resources panel) ═══ -->
|
||
<!-- This content is part of tab-monitor, rendered dynamically -->
|
||
|
||
<!-- Modal -->
|
||
<div class="modal-overlay" id="taskModal">
|
||
<div class="modal">
|
||
<div class="modal-head"><h2 id="modalTitle">任务详情</h2><button class="modal-close" onclick="closeModal()">×</button></div>
|
||
<div class="modal-body" id="modalBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* ═══════════ Shared ═══════════ */
|
||
const BD = {leader:{name:'大龙虾',avatar:'🦞',color:'#FF6B35'},qianwen:{name:'全栈高手',avatar:'⚡',color:'#4ECDC4'},kimi:{name:'智囊团',avatar:'🔬',color:'#A78BFA'}};
|
||
let ovData = null, monData = null, convData = null, statsData = null, memoryData = null;
|
||
let workerConvData = {};
|
||
let activeTab = 'overview';
|
||
let overviewView = 'home';
|
||
let expandedThink = {}, expandedReply = {};
|
||
let ovTimer = null, monTimer = null, statsTimer = null, memoryTimer = null;
|
||
let colVisible = {leader:true, qianwen:true, kimi:true};
|
||
let monColVisible = {leader:true, qianwen:true, kimi:true};
|
||
let workerLogs = {};
|
||
let webBotDetailId = null;
|
||
|
||
function switchTab(tab) {
|
||
activeTab = tab;
|
||
document.querySelectorAll('.tab-btn').forEach((b,i) => b.classList.toggle('active', (i===0&&tab==='overview')||(i===1&&tab==='monitor')||(i===2&&tab==='stats')||(i===3&&tab==='memory')));
|
||
document.getElementById('tab-overview').classList.toggle('active', tab==='overview');
|
||
document.getElementById('tab-monitor').classList.toggle('active', tab==='monitor');
|
||
document.getElementById('tab-stats').classList.toggle('active', tab==='stats');
|
||
document.getElementById('tab-memory').classList.toggle('active', tab==='memory');
|
||
clearInterval(ovTimer); clearInterval(monTimer); clearInterval(statsTimer); clearInterval(memoryTimer);
|
||
|
||
if (tab==='overview') {
|
||
if(!ovData){
|
||
if(isAppMode())showAppOverviewFramework();
|
||
else showOverviewFramework();
|
||
}
|
||
fetchOverview();
|
||
ovTimer = setInterval(fetchOverview, 30000);
|
||
}
|
||
else if (tab==='monitor') {
|
||
if(!monData){
|
||
if(isAppMode())showAppMonitorFramework();
|
||
else showMonitorFramework();
|
||
}
|
||
fetchMonitor();
|
||
monTimer = setInterval(fetchMonitor, 5000);
|
||
}
|
||
else if (tab==='stats') {
|
||
fetchStats();
|
||
statsTimer = setInterval(fetchStats, 10000);
|
||
}
|
||
else if (tab==='memory') {
|
||
fetchMemory(selectedMemoryDate);
|
||
memoryTimer = setInterval(() => fetchMemory(selectedMemoryDate), 30000);
|
||
}
|
||
}
|
||
|
||
/* ═══════════ 骨架屏加载动画 ═══════════ */
|
||
function showSkeletonOverview(){
|
||
const skeletonHTML = `
|
||
<div class="bot-columns">
|
||
${['leader','qianwen','kimi'].map(id=>{
|
||
const d=BD[id]||{};
|
||
return `<div class="bot-column" id="col-${id}">
|
||
<div class="skeleton skeleton-header"></div>
|
||
<div class="skeleton-panel">
|
||
${[1,2,3,4,5].map(()=>'<div class="skeleton skeleton-row"></div>').join('')}
|
||
</div>
|
||
<div class="skeleton-panel">
|
||
${[1,2,3].map(()=>'<div class="skeleton skeleton-row"></div>').join('')}
|
||
</div>
|
||
</div>`;
|
||
}).join('')}
|
||
</div>
|
||
`;
|
||
document.getElementById('botColumns').innerHTML = skeletonHTML;
|
||
}
|
||
|
||
function showSkeletonMonitor(){
|
||
const skeletonHTML = `
|
||
<div class="mon-status-bar">
|
||
${[1,2,3,4].map(()=>'<div class="skeleton skeleton-stat"></div>').join('')}
|
||
</div>
|
||
<div class="mon-panels" style="margin-top:14px">
|
||
<div class="skeleton-panel"><div class="skeleton skeleton-card"></div></div>
|
||
<div class="skeleton-panel"><div class="skeleton skeleton-card"></div></div>
|
||
</div>
|
||
`;
|
||
document.getElementById('monitorContent').innerHTML = skeletonHTML;
|
||
}
|
||
|
||
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML;}
|
||
function timeAgo(iso){if(!iso)return'--';const d=Date.now()-new Date(iso).getTime();if(d<0)return'刚刚';const s=Math.floor(d/1000);if(s<60)return s+'秒前';const m=Math.floor(s/60);if(m<60)return m+'分钟前';const h=Math.floor(m/60);if(h<24)return h+'小时前';return Math.floor(h/24)+'天前';}
|
||
function fmtCountdown(ms){if(ms<=0)return'即将';const s=Math.floor(ms/1000);const m=Math.floor(s/60);if(m>=60)return Math.floor(m/60)+'时'+String(m%60).padStart(2,'0')+'分';return String(m).padStart(2,'0')+':'+String(s%60).padStart(2,'0');}
|
||
function fmtCountdownFull(ms){if(ms<=0)return'即将执行';const t=Math.floor(ms/1000);const m=Math.floor(t/60);const s=t%60;if(m>=60){const h=Math.floor(m/60);return h+'时'+String(m%60).padStart(2,'0')+'分';}return String(m).padStart(2,'0')+'分'+String(s).padStart(2,'0')+'秒';}
|
||
function fmtTime(iso){if(!iso)return'--:--';let d;if(typeof iso==='number')d=new Date(iso);else d=new Date(iso);if(isNaN(d.getTime()))return'--:--';return d.toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit'});}
|
||
function botName(id){return (BD[id]&&BD[id].name)||id;}
|
||
function restartButtonHtml(id, compact=false){return `<button class="restart-btn${compact?' compact':''}" onclick="restartBot('${id}', event)" title="重启 ${botName(id)}">重启</button>`;}
|
||
async function restartBot(id, event){
|
||
if(event){event.preventDefault();event.stopPropagation();}
|
||
const btn=event&&event.currentTarget?event.currentTarget:null;
|
||
if(btn&&btn.disabled)return;
|
||
const name=botName(id);
|
||
if(!confirm(`确认重启 ${name} 吗?\n当前任务可能会短暂中断。`))return;
|
||
const oldText=btn?btn.textContent:'';
|
||
if(btn){btn.disabled=true;btn.textContent='重启中...';}
|
||
try{
|
||
const r=await fetch(`/api/bot/${id}/restart`,{method:'POST'});
|
||
const data=await r.json().catch(()=>({}));
|
||
if(!r.ok||!data.ok)throw new Error(data.message||data.result?.stderr||'重启失败');
|
||
alert(data.message||`${name} 重启命令已发送`);
|
||
await new Promise(resolve=>setTimeout(resolve,1500));
|
||
await Promise.all([fetchOverview(),fetchMonitor()]);
|
||
if(activeTab==='overview'&&!isAppMode()&&overviewView==='detail'&&webBotDetailId===id)await openBotDetail(id,true);
|
||
if(isAppMode()&&appBotDetailId===id)await openAppBotDetail(id,true);
|
||
if(activeTab==='monitor'&&isAppMode()&&appMonitorBotId===id)renderAppMonitorBotDetail(id);
|
||
}catch(err){
|
||
alert(`重启失败:${err.message||err}`);
|
||
}finally{
|
||
if(btn){btn.disabled=false;btn.textContent=oldText||'重启';}
|
||
}
|
||
}
|
||
|
||
/* ═══════════ TAB 1: Overview ═══════════ */
|
||
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>
|