Improve dashboard initial load and monitor fetch

This commit is contained in:
fang
2026-03-11 18:37:57 +08:00
parent 05ad5baaa1
commit e0f9ba56ce
2 changed files with 241 additions and 51 deletions

View File

@@ -810,7 +810,7 @@ async function restartBot(id, event){
}
/* ═══════════ TAB 1: Overview ═══════════ */
async function fetchOverview(){
async function fetchOverview(lite=false){
// 立即显示框架和骨架屏
if(!ovData) {
showOverviewFramework();
@@ -818,7 +818,7 @@ async function fetchOverview(){
}
try{
const r=await fetch('/api/status');
const r=await fetch(lite?'/api/status?lite=1':'/api/status');
ovData=await r.json();
if(activeTab==='overview'&&overviewView==='home')renderOverview();
}catch(e){
@@ -879,18 +879,21 @@ function renderOverview(){
}
// Web 模式渲染
const isLoading=!!ovData.lite;
const s=ovData.stats;const on=ovData.bots.filter(b=>b.status.running).length;
document.getElementById('globalStats').innerHTML=`<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('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):renderWorkerTasks(b,n)}${renderBotCron(b)}${renderBotCommits(b)}${renderBotMcps(b)}${renderBotSkills(b)}</div>`;}).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){const t=b.tasks;const all=t.dispatched||[];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">${t.pending.length}</div><div class="l">待接收</div></div><div class="ms w"><div class="n">${t.accepted.length}</div><div class="l">已接收</div></div><div class="ms d"><div class="n">${t.done.length}</div><div class="l">完成</div></div><div class="ms b"><div class="n">${t.blocked.length}</div><div class="l">阻塞</div></div></div><div class="panel-body">${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>'}</div></div>`;}
function renderWorkerTasks(b,name){const t=b.tasks;const all=[...(t.blocked||[]),...(t.inProgress||[]),...(t.pending||[]),...(t.done||[])];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">${t.pending.length}</div><div class="l">待处理</div></div><div class="ms w"><div class="n">${(t.inProgress||[]).length}</div><div class="l">进行中</div></div><div class="ms d"><div class="n">${t.done.length}</div><div class="l">完成</div></div><div class="ms b"><div class="n">${t.blocked.length}</div><div class="l">阻塞</div></div></div><div class="panel-body">${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>'}</div></div>`;}
function renderBotCron(b){const jobs=b.cron||[];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){const c=b.commits||[];const u=`https://github.com/${b.codeRepo}`;return`<div class="panel"><div class="panel-head"><h3>📦 代码</h3><a href="${u}" target="_blank">仓库→</a></div><div class="panel-body">${c.length?c.slice(0,4).map(renderCommitRow).join(''):'<div class="empty-msg">暂无提交</div>'}</div></div>`;}
function renderBotMcps(b){const m=b.mcps||[];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){const s=b.installedSkills||[];return`<div class="panel"><div class="panel-head"><h3>🧠 技能</h3><span class="cnt">${s.length}</span></div><div class="panel-body">${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>'}</div></div>`;}
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 */
@@ -1336,24 +1339,57 @@ async function fetchMonitor(){
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,r3,r4,r5,r6,r7,r8]=await Promise.all([
fetch('/api/monitor'),fetch('/api/monitor/conversation?limit=20&botId=leader'),
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'),fetch('/api/bot/kimi')
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();
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||[];
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);}
}
@@ -1456,24 +1492,38 @@ function renderLeaderMonCol(b,vis){
}
function renderWorkerMonCol(b,vis){
const wl=workerLogs[b.id]||{};const logs=wl.logs||[];const st=wl.status||{};
const pm=wl.pollMeta;const on=st.running||st.state==='running';
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){const lp=pm.lastPollAt;if(lp){nextPollMs=(lp+pm.interval)*1000-Date.now();nextPoll=fmtCountdownFull(nextPollMs);}}
const pollInterval=pm?Math.round(pm.interval/60)+'分钟':'--';
const lastPollTime=pm&&pm.lastPollAt?fmtTime(pm.lastPollAt*1000):'--';
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(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=`<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>`;
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>`;
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">${on?'运行中':'离线'}${on?' · 轮询:'+nextPoll:''}</div></div><div class="col-actions">${restartButtonHtml(b.id,true)}<div class="col-status"><span class="col-status-text">${on?'轮询 '+pollInterval:'停止'}</span><span class="col-dot ${on?'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-${b.id}" style="max-height:400px">${renderWorkerChatMsgs(msgs,b)}</div></div><div class="panel"><div class="panel-head"><h3>🔧 任务执行</h3><span class="sub">${taskLogs.length}条</span></div><div class="panel-body" style="max-height:300px">${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>'}</div></div><div class="panel"><div class="panel-head"><h3>📡 系统日志</h3><span class="cnt">${sysLogs.length}条</span></div><div class="panel-body" style="max-height:200px">${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>'}</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:'已停止'}</div><div class="c-last">上次: ${lastPollTime}</div></div></div></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){
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):'';
@@ -2203,7 +2253,9 @@ initTheme();
// Boot
if(isAppMode()) showAppOverviewFramework();
else showOverviewFramework();
fetchOverview();
fetchOverview(true);
if('requestIdleCallback' in window) requestIdleCallback(()=>fetchOverview(false));
else setTimeout(()=>fetchOverview(false),300);
ovTimer=setInterval(fetchOverview,30000);
setInterval(tickAll,1000);
</script>

View File

@@ -14,6 +14,7 @@ const LOG_FILE = path.join(HOME, '.openclaw/logs/gateway.log');
// ════════════════ 日志工具 ════════════════
const DASHBOARD_LOG_DIR = path.join(__dirname, 'logs');
const DASHBOARD_LOG_FILE = path.join(DASHBOARD_LOG_DIR, 'dashboard.log');
const DASHBOARD_CACHE_DIR = path.join(__dirname, '.cache');
// 日志配置(先设置默认值,后面从配置文件读取)
let LOGGING_CONFIG = {
@@ -37,6 +38,10 @@ const LOG_LEVEL_PRIORITY = {
if (!fs.existsSync(DASHBOARD_LOG_DIR)) {
fs.mkdirSync(DASHBOARD_LOG_DIR, { recursive: true });
}
// 确保缓存目录存在
if (!fs.existsSync(DASHBOARD_CACHE_DIR)) {
fs.mkdirSync(DASHBOARD_CACHE_DIR, { recursive: true });
}
// 日志级别颜色
const LOG_COLORS = {
@@ -130,10 +135,34 @@ logger.info('系统', `工作目录: ${__dirname}`);
const CACHE = {
commits: new Map(),
skills: new Map(),
installedSkills: new Map(),
issues: null,
issuesTime: 0,
issuesInflight: null,
commitsInflight: new Map(),
skillsInflight: new Map(),
};
const CACHE_TTL = 30000; // 30秒缓存
const DISK_CACHE_TTL = 300000; // 5分钟磁盘缓存
const DISK_REFRESH_INTERVAL = 10000; // 10秒内只刷新一次
function readDiskCache(key) {
try {
const file = path.join(DASHBOARD_CACHE_DIR, `${key}.json`);
if (!fs.existsSync(file)) return null;
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
if (!data || !data.updatedAt) return null;
const ageMs = Date.now() - new Date(data.updatedAt).getTime();
return { value: data.value, ageMs };
} catch { return null; }
}
function writeDiskCache(key, value) {
try {
const file = path.join(DASHBOARD_CACHE_DIR, `${key}.json`);
fs.writeFileSync(file, JSON.stringify({ updatedAt: new Date().toISOString(), value }, null, 2), 'utf-8');
} catch {}
}
const CONFIG_PATH = path.join(__dirname, 'config.json');
let userConfig = {};
@@ -313,6 +342,34 @@ function getGitHubIssues(limit = 100) {
logger.debug('GitHub', `Issues 使用缓存`, { age: `${now - CACHE.issuesTime}ms` });
return CACHE.issues;
}
const diskKey = `issues-${limit}`;
const diskCached = readDiskCache(diskKey);
if (diskCached && diskCached.value !== undefined) {
CACHE.issues = diskCached.value;
CACHE.issuesTime = now;
const shouldRefresh = diskCached.ageMs > DISK_REFRESH_INTERVAL;
const isStale = diskCached.ageMs > DISK_CACHE_TTL;
if ((shouldRefresh || isStale) && !CACHE.issuesInflight) {
CACHE.issuesInflight = true;
setImmediate(() => {
try { refreshGitHubIssues(limit); } finally { CACHE.issuesInflight = null; }
});
}
return diskCached.value;
}
if (CACHE.issuesInflight) {
return CACHE.issues || [];
}
CACHE.issuesInflight = true;
const data = refreshGitHubIssues(limit);
CACHE.issuesInflight = null;
return data;
}
function refreshGitHubIssues(limit) {
const now = Date.now();
const timer = perf('GitHub', 'getGitHubIssues');
const q = IS_WIN ? '"."' : "'.'";
@@ -325,6 +382,7 @@ function getGitHubIssues(limit = 100) {
const data = JSON.parse(raw);
CACHE.issues = data;
CACHE.issuesTime = now;
writeDiskCache(`issues-${limit}`, data);
timer.end(`成功获取 ${data.length} 个 Issues`);
return data;
} catch (err) {
@@ -354,6 +412,33 @@ function getRepoCommits(repoFullName, limit = 15) {
logger.debug('GitHub', `Commits 使用缓存 [${repoFullName}]`, { age: `${Date.now() - cached.time}ms` });
return cached.data;
}
const diskKey = `commits-${repoFullName.replace(/[^a-zA-Z0-9._-]/g, '_')}-${limit}`;
const diskCached = readDiskCache(diskKey);
if (diskCached && diskCached.value !== undefined) {
CACHE.commits.set(cacheKey, { data: diskCached.value, time: Date.now() });
const shouldRefresh = diskCached.ageMs > DISK_REFRESH_INTERVAL;
const isStale = diskCached.ageMs > DISK_CACHE_TTL;
if ((shouldRefresh || isStale) && !CACHE.commitsInflight.get(cacheKey)) {
CACHE.commitsInflight.set(cacheKey, true);
setImmediate(() => {
try { refreshRepoCommits(repoFullName, limit, cacheKey, diskKey); } finally { CACHE.commitsInflight.delete(cacheKey); }
});
}
return diskCached.value;
}
if (CACHE.commitsInflight.get(cacheKey)) {
return cached ? cached.data : [];
}
CACHE.commitsInflight.set(cacheKey, true);
const data = refreshRepoCommits(repoFullName, limit, cacheKey, diskKey);
CACHE.commitsInflight.delete(cacheKey);
return data;
}
function refreshRepoCommits(repoFullName, limit, cacheKey, diskKey) {
const cached = CACHE.commits.get(cacheKey);
const timer = perf('GitHub', `getRepoCommits [${repoFullName}]`);
const raw = exec(`gh api repos/${repoFullName}/commits?per_page=${limit} ${DEVNULL}`);
@@ -365,6 +450,7 @@ function getRepoCommits(repoFullName, limit = 15) {
const data = JSON.parse(raw);
const result = data.map(c => ({ sha: c.sha?.substring(0, 7), fullSha: c.sha, message: c.commit?.message, author: c.commit?.author?.name, date: c.commit?.author?.date, url: c.html_url }));
CACHE.commits.set(cacheKey, { data: result, time: Date.now() });
writeDiskCache(diskKey, result);
timer.end(`成功获取 ${result.length} 个 Commits`);
return result;
} catch (err) {
@@ -379,6 +465,33 @@ function getRepoSkills(repoFullName) {
if (cached && (Date.now() - cached.time < CACHE_TTL)) {
return cached.data;
}
const diskKey = `skills-${repoFullName.replace(/[^a-zA-Z0-9._-]/g, '_')}`;
const diskCached = readDiskCache(diskKey);
if (diskCached && diskCached.value !== undefined) {
CACHE.skills.set(repoFullName, { data: diskCached.value, time: Date.now() });
const shouldRefresh = diskCached.ageMs > DISK_REFRESH_INTERVAL;
const isStale = diskCached.ageMs > DISK_CACHE_TTL;
if ((shouldRefresh || isStale) && !CACHE.skillsInflight.get(repoFullName)) {
CACHE.skillsInflight.set(repoFullName, true);
setImmediate(() => {
try { refreshRepoSkills(repoFullName, diskKey); } finally { CACHE.skillsInflight.delete(repoFullName); }
});
}
return diskCached.value;
}
if (CACHE.skillsInflight.get(repoFullName)) {
return cached ? cached.data : [];
}
CACHE.skillsInflight.set(repoFullName, true);
const data = refreshRepoSkills(repoFullName, diskKey);
CACHE.skillsInflight.delete(repoFullName);
return data;
}
function refreshRepoSkills(repoFullName, diskKey) {
const cached = CACHE.skills.get(repoFullName);
const raw = exec(`gh api repos/${repoFullName}/contents ${DEVNULL}`);
if (!raw) return cached ? cached.data : [];
@@ -386,6 +499,7 @@ function getRepoSkills(repoFullName) {
const data = JSON.parse(raw);
const result = data.filter(f => f.name?.endsWith('.md') && f.name !== 'README.md').map(f => ({ name: f.name, path: f.path, url: f.html_url, sha: f.sha?.substring(0, 7) }));
CACHE.skills.set(repoFullName, { data: result, time: Date.now() });
writeDiskCache(diskKey, result);
return result;
} catch {
return cached ? cached.data : [];
@@ -393,6 +507,12 @@ function getRepoSkills(repoFullName) {
}
function getInstalledSkills(bot) {
const cacheKey = bot.id || bot.container || bot.name || 'unknown';
const cached = CACHE.installedSkills.get(cacheKey);
if (cached && (Date.now() - cached.time < CACHE_TTL)) {
return cached.data;
}
const mcps = [];
const skills = [];
if (bot.type === 'host') {
@@ -480,7 +600,9 @@ function getInstalledSkills(bot) {
} catch {}
}
}
return { mcps, skills };
const result = { mcps, skills };
CACHE.installedSkills.set(cacheKey, { data: result, time: Date.now() });
return result;
}
function parseSkillMeta(content) {
@@ -808,6 +930,7 @@ app.get('/api/monitor', (req, res) => {
app.get('/api/status', (req, res) => {
const timer = perf('API', 'GET /api/status');
logger.debug('状态', '开始获取总览数据');
const lite = req.query?.lite === '1' || req.query?.lite === 'true';
const t1 = Date.now();
const dockerStatuses = getDockerStatus();
@@ -815,17 +938,23 @@ app.get('/api/status', (req, res) => {
logger.debug('状态', `容器状态获取完成`, { duration: `${Date.now() - t1}ms` });
const t2 = Date.now();
const issues = getGitHubIssues(50);
logger.debug('状态', `GitHub Issues 获取完成`, { count: issues.length, duration: `${Date.now() - t2}ms` });
const issues = lite ? [] : getGitHubIssues(50);
if (!lite) {
logger.debug('状态', `GitHub Issues 获取完成`, { count: issues.length, duration: `${Date.now() - t2}ms` });
}
const cronJobs = getCronJobs();
const allFormatted = issues.map(formatIssue);
const cronJobs = lite ? [] : getCronJobs();
const allFormatted = lite ? [] : issues.map(formatIssue);
const t3 = Date.now();
const bots = BOTS.map((bot) => {
const status = bot.type === 'host' ? hostStatus : (dockerStatuses[bot.container] || { state: 'unknown', status: 'Unknown', health: 'unknown', running: false });
let tasks;
if (bot.id === 'leader') {
if (lite) {
tasks = bot.id === 'leader'
? { dispatched: [], pending: [], accepted: [], done: [], blocked: [], total: 0 }
: { pending: [], inProgress: [], done: [], blocked: [], total: 0 };
} else if (bot.id === 'leader') {
const dispatched = allFormatted.filter((t) => t.assignedTo !== 'unassigned');
tasks = { dispatched, pending: dispatched.filter((t) => t.status === 'pending'), accepted: dispatched.filter((t) => t.status === 'in-progress'), done: dispatched.filter((t) => t.status === 'done'), blocked: dispatched.filter((t) => t.status === 'blocked'), total: dispatched.length };
} else {
@@ -833,8 +962,8 @@ app.get('/api/status', (req, res) => {
tasks = { pending: botIssues.filter((t) => t.status === 'pending'), inProgress: botIssues.filter((t) => t.status === 'in-progress'), done: botIssues.filter((t) => t.status === 'done'), blocked: botIssues.filter((t) => t.status === 'blocked'), total: botIssues.length };
}
let botCron = [];
if (bot.id === 'leader') { botCron = cronJobs; }
else if (bot.pollInterval) {
if (!lite && bot.id === 'leader') { botCron = cronJobs; }
else if (!lite && bot.pollInterval) {
const pollMeta = getPollMeta(bot.container);
const nowSec = Math.floor(Date.now() / 1000);
let nextRunAt = null, lastRunAt = null;
@@ -845,9 +974,9 @@ app.get('/api/status', (req, res) => {
}
botCron = [{ id: `poll-${bot.id}`, name: '任务轮询', description: `${bot.pollInterval} 分钟检查 GitHub 新任务`, enabled: status.running, schedule: { kind: 'every', everyMs: bot.pollInterval * 60000 }, lastRunAt, nextRunAt, lastStatus: status.running ? 'ok' : 'stopped', lastDuration: null, errors: 0 }];
}
const commits = getRepoCommits(bot.codeRepo, 5);
const skills = getRepoSkills(bot.skillsRepo);
const { mcps, skills: localSkills } = getInstalledSkills(bot);
const commits = lite ? [] : getRepoCommits(bot.codeRepo, 5);
const skills = lite ? [] : getRepoSkills(bot.skillsRepo);
const { mcps, skills: localSkills } = lite ? { mcps: [], skills: [] } : getInstalledSkills(bot);
return { ...bot, status, tasks, cron: botCron, commits, skills, mcps, installedSkills: localSkills };
});
logger.debug('状态', `Bot 数据组装完成`, { duration: `${Date.now() - t3}ms` });
@@ -859,22 +988,31 @@ app.get('/api/status', (req, res) => {
openTasks: allFormatted.filter((t) => t.state === 'OPEN').length
});
res.json({ timestamp: new Date().toISOString(), bots, stats: { totalTasks: allFormatted.length, openTasks: allFormatted.filter((t) => t.state === 'OPEN').length, doneTasks: allFormatted.filter((t) => t.status === 'done').length } });
const stats = lite
? { totalTasks: 0, openTasks: 0, doneTasks: 0 }
: { totalTasks: allFormatted.length, openTasks: allFormatted.filter((t) => t.state === 'OPEN').length, doneTasks: allFormatted.filter((t) => t.status === 'done').length };
res.json({ timestamp: new Date().toISOString(), bots, stats, lite });
});
app.get('/api/bot/:id', (req, res) => {
const bot = BOTS.find((b) => b.id === req.params.id);
if (!bot) return res.status(404).json({ error: 'Bot not found' });
const lite = req.query?.lite === '1' || req.query?.lite === 'true';
const dockerStatuses = getDockerStatus();
const hostStatus = getHostGatewayStatus();
const status = bot.type === 'host' ? hostStatus : (dockerStatuses[bot.container] || { state: 'unknown', status: 'Unknown', health: 'unknown', running: false });
const issues = getGitHubIssues(200);
const allFormatted = issues.map(formatIssue);
let tasks = bot.id === 'leader' ? allFormatted.filter((t) => t.assignedTo !== 'unassigned') : allFormatted.filter((t) => t.labels.includes(bot.label));
const commits = getRepoCommits(bot.codeRepo, 50);
const skills = getRepoSkills(bot.skillsRepo);
const issues = lite ? [] : getGitHubIssues(200);
const allFormatted = lite ? [] : issues.map(formatIssue);
let tasks = [];
if (!lite) {
tasks = bot.id === 'leader'
? allFormatted.filter((t) => t.assignedTo !== 'unassigned')
: allFormatted.filter((t) => t.labels.includes(bot.label));
}
const commits = lite ? [] : getRepoCommits(bot.codeRepo, 50);
const skills = lite ? [] : getRepoSkills(bot.skillsRepo);
const { mcps, skills: localSkills } = getInstalledSkills(bot);
res.json({ ...bot, status, tasks, commits, skills, mcps, installedSkills: localSkills, timestamp: new Date().toISOString() });
res.json({ ...bot, status, tasks, commits, skills, mcps, installedSkills: localSkills, lite, timestamp: new Date().toISOString() });
});
// ── 对话内容 API读取 OpenClaw 会话 JSONL──