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`
${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`${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`📤 已分派
全部 →${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.inProgress||[]).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||''}
`;}).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||''}
`;}).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``;}
+function renderBotMcps(b,isLoading){const m=b.mcps||[];if(isLoading)return``;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``;}
/* 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=``;
+ 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`🧠 思考 & 对话
${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`🧠 思考 & 对话
${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)──