diff --git a/dashboard/public/index.html b/dashboard/public/index.html index 353b631..1a9cd86 100644 --- a/dashboard/public/index.html +++ b/dashboard/public/index.html @@ -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=`${on}/${ovData.bots.length} 在线${s.openTasks} 待办${s.doneTasks} 完成`; + document.getElementById('globalStats').innerHTML=isLoading + ? `加载中...` + : `${on}/${ovData.bots.length} 在线${s.openTasks} 待办${s.doneTasks} 完成`; 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``;}).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`
${a}

${n}

${b.role}
${restartButtonHtml(b.id,true)}
${st}
${b.capabilities.map(cap=>`${cap}`).join('')}
${b.id==='leader'?renderLeaderTasks(b):renderWorkerTasks(b,n)}${renderBotCron(b)}${renderBotCommits(b)}${renderBotMcps(b)}${renderBotSkills(b)}
`;}).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`
${a}

${n}

${b.role}
${restartButtonHtml(b.id,true)}
${st}
${b.capabilities.map(cap=>`${cap}`).join('')}
${b.id==='leader'?renderLeaderTasks(b,isLoading):renderWorkerTasks(b,n,isLoading)}${renderBotCron(b,isLoading)}${renderBotCommits(b,isLoading)}${renderBotMcps(b,isLoading)}${renderBotSkills(b,isLoading)}
`;}).join(''); } -function renderLeaderTasks(b){const t=b.tasks;const all=t.dispatched||[];return`

📤 已分派

全部 →
${t.pending.length}
待接收
${t.accepted.length}
已接收
${t.done.length}
完成
${t.blocked.length}
阻塞
${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`
#${i.number}${esc(i.title)}${badge}→${tg.avatar||''}${tg.name||i.assignedTo}${timeAgo(i.updatedAt)}
`;}).join(''):'
暂无
'}
`;} -function renderWorkerTasks(b,name){const t=b.tasks;const all=[...(t.blocked||[]),...(t.inProgress||[]),...(t.pending||[]),...(t.done||[])];return`

📥 任务

全部 →
${t.pending.length}
待处理
${(t.inProgress||[]).length}
进行中
${t.done.length}
完成
${t.blocked.length}
阻塞
${all.length?all.slice(0,6).map(i=>{const sl={pending:'待处理','in-progress':'进行中',done:'完成',blocked:'阻塞'}[i.status]||'?';return`
#${i.number}${esc(i.title)}${sl}${timeAgo(i.updatedAt)}
`;}).join(''):'
暂无
'}
`;} -function renderBotCron(b){const jobs=b.cron||[];if(!jobs.length)return'';return`

⏰ 定时任务

${jobs.length}
${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`
${j.name}
${j.description||''}
${cd}
上次: ${last}
`;}).join('')}
`;} -function renderBotCommits(b){const c=b.commits||[];const u=`https://github.com/${b.codeRepo}`;return`

📦 代码

仓库→
${c.length?c.slice(0,4).map(renderCommitRow).join(''):'
暂无提交
'}
`;} -function renderBotMcps(b){const m=b.mcps||[];if(!m.length)return'';return`

🔌 MCP / 工具

${m.length}
${m.map(sk=>`
🔧
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join('')}
`;} -function renderBotSkills(b){const s=b.installedSkills||[];return`

🧠 技能

${s.length}
${s.length?s.map(sk=>`
📘
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join(''):'
暂无技能
'}
`;} +function renderLeaderTasks(b,isLoading){const t=b.tasks||{};const all=t.dispatched||[];const num=v=>isLoading?'--':v;const body=isLoading?'
加载中...
':(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`
#${i.number}${esc(i.title)}${badge}→${tg.avatar||''}${tg.name||i.assignedTo}${timeAgo(i.updatedAt)}
`;}).join(''):'
暂无
');return`

📤 已分派

全部 →
${num((t.pending||[]).length)}
待接收
${num((t.accepted||[]).length)}
已接收
${num((t.done||[]).length)}
完成
${num((t.blocked||[]).length)}
阻塞
${body}
`;} +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?'
加载中...
':(all.length?all.slice(0,6).map(i=>{const sl={pending:'待处理','in-progress':'进行中',done:'完成',blocked:'阻塞'}[i.status]||'?';return`
#${i.number}${esc(i.title)}${sl}${timeAgo(i.updatedAt)}
`;}).join(''):'
暂无
');return`

📥 任务

全部 →
${num((t.pending||[]).length)}
待处理
${num(((t.inProgress)||[]).length)}
进行中
${num((t.done||[]).length)}
完成
${num((t.blocked||[]).length)}
阻塞
${body}
`;} +function renderBotCron(b,isLoading){const jobs=b.cron||[];if(isLoading)return`

⏰ 定时任务

加载中...
`;if(!jobs.length)return'';return`

⏰ 定时任务

${jobs.length}
${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`
${j.name}
${j.description||''}
${cd}
上次: ${last}
`;}).join('')}
`;} +function renderBotCommits(b,isLoading){const c=b.commits||[];const u=`https://github.com/${b.codeRepo}`;const body=isLoading?'
加载中...
':(c.length?c.slice(0,4).map(renderCommitRow).join(''):'
暂无提交
');return`

📦 代码

仓库→
${body}
`;} +function renderBotMcps(b,isLoading){const m=b.mcps||[];if(isLoading)return`

🔌 MCP / 工具

加载中...
`;if(!m.length)return'';return`

🔌 MCP / 工具

${m.length}
${m.map(sk=>`
🔧
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join('')}
`;} +function renderBotSkills(b,isLoading){const s=b.installedSkills||[];const body=isLoading?'
加载中...
':(s.length?s.map(sk=>`
📘
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join(''):'
暂无技能
');return`

🧠 技能

${isLoading?'--':s.length}
${body}
`;} function renderCommitRow(c){const t=(c.message||'').split('\n')[0]||'';return`
${c.sha}
${esc(t)}
${timeAgo(c.date)}
`;} /* 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=`

🔌 MCP / 工具

${mcps.length}
${mcps.map(sk=>`
🔧
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join('')}
`; - let skillHtml=`

🧠 技能

${skills.length}
${skills.length?skills.map(sk=>`
📘
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join(''):'
暂无技能
'}
`; + let mcpHtml=''; + if(loading)mcpHtml=`

🔌 MCP / 工具

加载中...
`; + else if(mcps.length)mcpHtml=`

🔌 MCP / 工具

${mcps.length}
${mcps.map(sk=>`
🔧
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join('')}
`; + let skillHtml=''; + if(loading)skillHtml=`

🧠 技能

加载中...
`; + else skillHtml=`

🧠 技能

${skills.length}
${skills.length?skills.map(sk=>`
📘
${esc(sk.name)}${sk.version?`v${esc(sk.version)}`:''}
${sk.description?`
${esc(sk.description)}
`:''}
`).join(''):'
暂无技能
'}
`; - return`
${b.avatar}

${b.name}

${on?'运行中':'离线'}${on?' · 轮询:'+nextPoll:''}
${restartButtonHtml(b.id,true)}
${on?'轮询 '+pollInterval:'停止'}

🧠 思考 & 对话

${msgs.length}条
${renderWorkerChatMsgs(msgs,b)}

🔧 任务执行

${taskLogs.length}条
${taskLogs.length?taskLogs.map(l=>`
${l.time?l.time.substring(11,19):'--:--'}${tagMap[l.type]||l.type}${esc(l.content)}
`).join(''):'
暂无任务执行记录
'}

📡 系统日志

${sysLogs.length}条
${sysLogs.length?sysLogs.map(l=>`
${l.time?l.time.substring(11,19):'--:--'}${tagMap[l.type]||l.type}${esc(l.content)}
`).join(''):'
暂无
'}
${mcpHtml}${skillHtml}

⏰ 轮询调度

GitHub Issues 轮询
每 ${pollInterval} 自动检查待处理任务
${on?nextPoll:'已停止'}
上次: ${lastPollTime}
`; + const roleText=loading?'加载中...':(on?'运行中':'离线'); + const statusText=loading?'加载中...':(on?'轮询 '+pollInterval:'停止'); + const taskBody=loading?'
加载中...
':(taskLogs.length?taskLogs.map(l=>`
${l.time?l.time.substring(11,19):'--:--'}${tagMap[l.type]||l.type}${esc(l.content)}
`).join(''):'
暂无任务执行记录
'); + const sysBody=loading?'
加载中...
':(sysLogs.length?sysLogs.map(l=>`
${l.time?l.time.substring(11,19):'--:--'}${tagMap[l.type]||l.type}${esc(l.content)}
`).join(''):'
暂无
'); + + return`
${b.avatar}

${b.name}

${roleText}${on?' · 轮询:'+nextPoll:''}
${restartButtonHtml(b.id,true)}
${statusText}

🧠 思考 & 对话

${msgCount}条
${renderWorkerChatMsgs(msgs,b,loading)}

🔧 任务执行

${taskCount}条
${taskBody}

📡 系统日志

${sysCount}条
${sysBody}
${mcpHtml}${skillHtml}

⏰ 轮询调度

GitHub Issues 轮询
每 ${pollInterval} 自动检查待处理任务
${on?nextPoll:(loading?'加载中...':'已停止')}
上次: ${lastPollTime}
`; } -function renderWorkerChatMsgs(msgs,bot){ +function renderWorkerChatMsgs(msgs,bot,loading){ + if(loading)return'
加载中...
'; if(!msgs.length)return'
暂无对话
'; 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); diff --git a/dashboard/server.js b/dashboard/server.js index 1f4575b..ef93fcc 100644 --- a/dashboard/server.js +++ b/dashboard/server.js @@ -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)──