Improve dashboard initial load and monitor fetch
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)──
|
||||
|
||||
Reference in New Issue
Block a user