{"repo": "0mnitx/openclaw-dashboard", "files": {"src/server.js": {"a471c8c64b9809463ccb2a51917797f90e3ac4f8": {"t": 1785674017.726663, "n": 9579, "h": "b2a136c4a4b79559", "c": "const express = require('express');\nconst { exec } = require('child_process');\nconst os = require('os');\nconst axios = require('axios');\n\nconst app = express();\nconst PORT = 3000;\n\napp.use(express.static('public'));\n\n// MiniMax Token Plan 配置\n// API 文档: https://platform.minimaxi.com/docs/token-plan/intro\nconst TOKENPLAN_CONFIG = {\n enabled: true,\n apiUrl: 'https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains',\n apiKey: 'sk-cp-H58PybR65Oz-Rv3wNgLjcXtRb_YZTjP-WbejPSsUMKsIDtolbjQb-172gEc9Bk6Tnl7xpuu24s-j4TKlBzlRozDcSe6E2WbuMfxBgnVFAFDFFpA0Y8Kgn0M'\n};\n\n// 获取 OpenClaw 状态\nfunction getOpenClawStatus() {\n return new Promise((resolve) => {\n exec('openclaw status 2>/dev/null', { maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {\n if (err) {\n resolve({ error: err.message, stdout: stdout, stderr: stderr });\n } else {\n resolve({ data: stdout, error: null });\n }\n });\n });\n}\n\n// 获取系统资源\nfunction getSystemResources() {\n const cpu = os.cpus();\n const totalMem = os.totalmem();\n const freeMem = os.freemem();\n const usedMem = totalMem - freeMem;\n \n let totalIdle = 0, totalTick = 0;\n cpu.forEach(c => {\n for (const type in c.times) {\n totalTick += c.times[type];\n }\n totalIdle += c.times.idle;\n });\n const cpuUsage = ((1 - totalIdle / totalTick) * 100).toFixed(1);\n\n return {\n cpu: cpuUsage,\n cpuCores: cpu.length,\n memory: {\n total: formatBytes(totalMem),\n used: formatBytes(usedMem),\n free: formatBytes(freeMem),\n usedPercent: ((usedMem / totalMem) * 100).toFixed(1)\n },\n loadAvg: os.loadavg(),\n uptime: formatUptime(os.uptime()),\n hostname: os.hostname(),\n platform: os.platform() + ' ' + os.release()\n };\n}\n\nfunction formatBytes(bytes) {\n const gb = bytes / (1024 * 1024 * 1024);\n return gb.toFixed(1) + ' GB';\n}\n\nfunction formatUptime(seconds) {\n const d = Math.floor(seconds / 86400);\n const h = Math.floor((seconds % 86400) / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n return d > 0 ? `${d}d ${h}h ${m}m` : `${h}h ${m}m`;\n}\n\n// 解析 Sessions 表格\nfunction parseSessions(text) {\n const sessions = [];\n if (!text) return sessions;\n \n const lines = text.split('\\n');\n let state = 'search';\n let prevLine = '';\n \n for (const line of lines) {\n if (state === 'search') {\n if (line.trim() === 'Sessions') {\n state = 'found';\n }\n continue;\n }\n \n if (state === 'found') {\n if (line.includes('├')) {\n // Check if the PREVIOUS line was the header row (│ Key │ Kind │...)\n if (prevLine.includes('│') && prevLine.includes('Key')) {\n state = 'data';\n }\n // If not, keep searching\n }\n prevLine = line;\n continue;\n }\n \n if (state !== 'data') continue;\n \n if (line.includes('└')) {\n break;\n }\n \n if (!line.includes('│') || line.trim() === '') continue;\n \n const cleaned = line.replace(/[┌┬┤┘┴└├┼─│]/g, ' ');\n const cells = cleaned.split(/\\s{2,}/).filter(c => c.trim());\n \n if (cells.length >= 4) {\n const key = cells[0].trim();\n const kind = cells[1].trim();\n const age = cells[2].trim();\n const model = cells[3].trim();\n const tokens = cells[4] ? cells[4].trim() : '';\n \n if (!key || key === 'Key') continue;\n \n const sessionId = key.replace('agent:', '').replace(/:/g, ' › ');\n \n // age 可能被拆成两个 cell,需要合并\n // 情况1: \"1m\" + \"ago\" = \"1m ago\"(分钟前)\n // 情况2: \"just\" + \"now\" = \"just now\"(活跃)\n let fullAge = age;\n if (cells[3] === 'ago' && (age.endsWith('m') || age.match(/^\\d+h$/))) {\n fullAge = age + ' ' + cells[3];\n } else if (cells[3] === 'now' && age === 'just') {\n fullAge = 'just now';\n }\n \n const finalAgeDisplay = fullAge === 'just now' ? '刚刚' : \n fullAge.endsWith('m ago') ? fullAge.replace('m ago', '分钟前') :\n fullAge.endsWith('h ago') ? fullAge.replace('h ago', '小时前') :\n fullAge.endsWith('d ago') ? fullAge.replace('d ago', '天前') : fullAge;\n \n const tokenMatch = tokens.match(/(\\d+)k\\/(\\d+)k\\s*\\((\\d+)%\\)/);\n const tokenPercent = tokenMatch ? parseInt(tokenMatch[3]) : 0;\n \n const isReallyActive = fullAge === 'just now';\n \n sessions.push({\n id: sessionId,\n kind: kind,\n age: finalAgeDisplay,\n model: model,\n tokens: tokens,\n tokenPercent: tokenPercent,\n isActive: isReallyActive\n });\n }\n }\n \n return sessions;\n}\n\n// API: 系统资源\napp.get('/api/system', (req, res) => {\n const resources = getSystemResources();\n exec('df -h / | tail -1 | awk \\'{print $2,$3,$5}\\'', (err, stdout) => {\n if (!err && stdout) {\n const parts = stdout.trim().split(/\\s+/);\n resources.disk = {\n total: parts[0],\n used: parts[1],\n percent: parts[2]\n };\n }\n res.json(resources);\n });\n});\n\n// API: OpenClaw 状态\napp.get('/api/openclaw', async (req, res) => {\n const status = await getOpenClawStatus();\n const sessions = parseSessions(status.data);\n \n let gatewayStatus = { running: false, detail: '' };\n if (status.data) {\n const lines = status.data.split('\\n');\n lines.forEach(line => {\n if (line.includes('Gateway') && line.includes('running')) {\n gatewayStatus = { running: true, detail: '运行中' };\n }\n });\n }\n \n const activeCount = sessions.filter(s => s.isActive).length;\n const totalSessions = sessions.length;\n \n res.json({\n error: status.error,\n data: status.data,\n sessions: sessions,\n gateway: gatewayStatus,\n summary: {\n totalSessions: totalSessions,\n activeSessions: activeCount,\n activeModels: [...new Set(sessions.map(s => s.model))]\n }\n });\n});\n\n// API: Token 用量(MiniMax Token Plan)\napp.get('/api/token', async (req, res) => {\n if (!TOKENPLAN_CONFIG.enabled || !TOKENPLAN_CONFIG.apiUrl) {\n res.json({\n configured: false,\n message: '请在 src/server.js 中配置 TOKENPLAN_CONFIG'\n });\n return;\n }\n \n try {\n const response = await axios.get(TOKENPLAN_CONFIG.apiUrl, {\n headers: {\n 'Authorization': `Bearer ${TOKENPLAN_CONFIG.apiKey}`,\n 'Accept': 'application/json'\n },\n timeout: 10000\n });\n \n const rawData = response.data;\n const modelRemains = rawData.model_remains || [];\n \n // 提取主要模型\n const mainModel = modelRemains.find(m => m.model_name && m.model_name.includes('MiniMax-M'));\n const speechModel = modelRemains.find(m => m.model_name && m.model_name.includes('speech'));\n const imageModel = modelRemains.find(m => m.model_name && m.model_name.includes('image'));\n \n // 计算5小时窗口重置时间\n let resetIn = '未知';\n if (mainModel && mainModel.end_time) {\n const now = Date.now();\n const remainingMs = Math.max(0, mainModel.end_time - now);\n const hours = Math.floor(remainingMs / (1000 * 60 * 60));\n const minutes = Math.floor((remainingMs % (1000 * 60 * 60)) / (1000 * 60));\n resetIn = hours > 0 ? `${hours}小时${minutes}分钟` : `${minutes}分钟`;\n }\n \n // 计算周额度重置时间\n let weeklyResetIn = '未知';\n if (mainModel && mainModel.weekly_end_time) {\n const now = Date.now();\n const remainingMs = Math.max(0, mainModel.weekly_end_time - now);\n const days = Math.floor(remainingMs / (1000 * 60 * 60 * 24));\n const hours = Math.floor((remainingMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));\n weeklyResetIn = `${days}天${hours}小时`;\n }\n \n res.json({\n configured: true,\n error: null,\n models: {\n main: mainModel ? {\n name: 'MiniMax-M*',\n used: mainModel.current_interval_total_count - mainModel.current_interval_usage_count,\n total: mainModel.current_interval_total_count,\n remaining: mainModel.current_interval_usage_count,\n percent: Math.round(((mainModel.current_interval_total_count - mainModel.current_interval_usage_count) / mainModel.current_interval_total_count) * 100),\n resetIn: resetIn\n } : null,\n speech: speechModel && speechModel.current_interval_total_count > 0 ? {\n name: 'Speech-HD',\n used: speechModel.current_interval_total_count - speechModel.current_interval_usage_count,\n total: speechModel.current_interval_total_count,\n remaining: speechModel.current_interval_usage_count,\n percent: Math.round(((speechModel.current_interval_total_count - speechModel.current_interval_usage_count) / speechModel.current_interval_total_count) * 100)\n } : null,\n image: imageModel && imageModel.current_interval_total_count > 0 ? {\n name: 'Image-01',\n used: imageModel.current_interval_total_count - imageModel.current_interval_usage_count,\n total: imageModel.current_interval_total_count,\n remaining: imageModel.current_interval_usage_count,\n percent: Math.round(((imageModel.current_interval_total_count - imageModel.current_interval_usage_count) / imageModel.current_interval_total_count) * 100)\n } : null\n },\n weekly: mainModel ? {\n used: mainModel.current_weekly_total_count - mainModel.current_weekly_usage_count,\n total: mainModel.current_weekly_total_count,\n remaining: mainModel.current_weekly_usage_count,\n resetIn: weeklyResetIn\n } : null\n });\n } catch (e) {\n res.json({\n configured: true,\n error: e.message\n });\n }\n});\n\napp.listen(PORT, '0.0.0.0', () => {\n console.log(`🚀 OpenClaw Dashboard 运行中: http://0.0.0.0:${PORT}`);\n});"}}}}