import { useKey } from "@kevisual/context" import os from 'node:os'; import { execSync } from 'node:child_process'; import dayjs from 'dayjs'; export const getLiveMdContent = (opts?: { more?: boolean }) => { const more = opts?.more ?? false const url = useKey('CNB_VSCODE_PROXY_URI') || '' const token = useKey('CNB_TOKEN') || '' const openclawPort = useKey('OPENCLAW_PORT') || '80' const openclawUrl = url.replace('{{port}}', openclawPort) const openclawUrlSecret = openclawUrl + '/openclaw?token=' + token const opencodePort = useKey('OPENCODE_PORT') || '100' const opencodeUrl = url.replace('{{port}}', opencodePort) // btoa('root:password'); // const _opencodeURL = new URL(opencodeUrl) _opencodeURL.username = 'root' _opencodeURL.password = token const opencodeUrlSecret = _opencodeURL.toString() // console.log('btoa opencode auth: ', Buffer.from(`root:${token}`).toString('base64')) const kevisualUrl = url.replace('{{port}}', '51515') const openWebUrl = url.replace('{{port}}', '200') const vscodeWebUrl = useKey('CNB_VSCODE_WEB_URL') || '' const TEMPLATE = `# 开发环境模式配置 ### 服务访问地址 #### nginx 反向代理访问(推荐) - OpenClaw: ${openclawUrl + '/openclaw'} - OpenCode: ${opencodeUrl} - VSCode Web: ${vscodeWebUrl} - OpenWebUI: ${openWebUrl} - Kevisual: ${kevisualUrl} ### 密码访问 - OpenClaw: ${openclawUrlSecret} - OpenCode: ${opencodeUrlSecret} ### 环境变量 - CNB_TOKEN: ${token} ### 其他说明 1. 保活说明 使用插件访问vscode web获取wss进行保活,避免长时间不操作导致的自动断开连接。 方法1: 使用插件访问vscode web获取wss进行保活,避免长时间不操作导致的自动断开连接。 1. 安装插件[CNB LIVE](https://chromewebstore.google.com/detail/cnb-live/iajpiophkcdghonpijkcgpjafbcjhkko?pli=1) 2. 打开vscode web获取,点击插件,获取json数据,替换keep.json中的数据,保持在线状态。 3. keep.json中的数据结构说明: - wss: vscode web的websocket地址 - cookie: vscode web的cookie,保持和浏览器一致 - url: vscode web的访问地址,可以直接访问vscode web 4. 运行cli命令,ev cnb live -c /workspace/live/keep.json.(直接对话opencode或者openclaw调用cnb-live技能即可) 方法2:环境变量设置CNB_COOKIE,直接opencode或者openclaw的ui界面对话说,cnb-keep-live保活,他会自动调用保活,同时不需要点cnb-lie插件获取配置。 2. Opencode web访问说明 Opencode打开web地址,需要在浏览器输入用户名和密码,用户名固定为root,密码为CNB_TOKEN的值. 纯连接打开包含账号密码,第一次点击后,需要把账号密码清理掉才能访问,opencode的bug导致的。 ` const labels = [ { key: 'vscodeWebUrl', title: 'VSCode Web 地址', value: vscodeWebUrl, description: 'VSCode Web 的访问地址' }, { key: 'kevisualUrl', title: 'Kevisual 地址', value: kevisualUrl, description: 'Kevisual 的访问地址,可以通过该地址访问 Kevisual 服务' }, { key: 'cnbTempToken', title: 'CNB Token', value: token, description: 'CNB 临时 Token,保持和环境变量 CNB_TOKEN 一致' }, { key: 'openWebUrl', title: 'OpenWebUI 地址', value: openWebUrl, description: 'OpenWebUI 的访问地址,可以通过该地址访问 OpenWebUI 服务' }, { key: 'openclawUrl', title: 'OpenClaw 地址', value: openclawUrl + '/openclaw', description: 'OpenClaw 的访问地址,可以通过该地址访问 OpenClaw 服务' }, { key: 'openclawUrlSecret', title: 'OpenClaw 访问地址(含 Token)', value: openclawUrlSecret, description: 'OpenClaw 的访问地址,包含 token 参数,可以直接访问 OpenClaw 服务' }, { key: 'opencodeUrl', title: 'OpenCode 地址', value: opencodeUrl, description: 'OpenCode 的访问地址,可以通过该地址访问 OpenCode 服务' }, { key: 'opencodeUrlSecret', title: 'OpenCode 访问地址(含 Token)', value: opencodeUrlSecret, description: 'OpenCode 的访问地址,包含 token 参数,可以直接访问 OpenCode 服务' }, { key: 'docs', title: '配置说明文档', value: TEMPLATE, description: '开发环境模式配置说明文档' } ] const osInfoList = createOSInfo(more) labels.push(...osInfoList) return labels } const createOSInfo = (more = false) => { const labels: Array<{ key: string; title: string; value: string | number; description: string }> = [] const startTimer = useKey('CNB_BUILD_START_TIME') || '' // CPU 使用率 const cpus = os.cpus() let totalIdle = 0 let totalTick = 0 cpus.forEach((cpu) => { for (const type in cpu.times) { totalTick += cpu.times[type as keyof typeof cpu.times] } totalIdle += cpu.times.idle }) const cpuUsage = ((1 - totalIdle / totalTick) * 100).toFixed(2) // 内存使用情况 (使用 free 命令) let memUsed = 0 let memTotal = 0 let memFree = 0 try { const freeOutput = execSync('free -b', { encoding: 'utf-8' }) const lines = freeOutput.trim().split('\n') const memLine = lines.find(line => line.startsWith('Mem:')) if (memLine) { const parts = memLine.split(/\s+/) memTotal = parseInt(parts[1]) memUsed = parseInt(parts[2]) memFree = parseInt(parts[3]) } } catch (e) { // 如果 free 命令失败,使用 os 模块 memTotal = os.totalmem() memFree = os.freemem() memUsed = memTotal - memFree } const memUsage = memTotal > 0 ? ((memUsed / memTotal) * 100).toFixed(2) : '0.00' // 格式化字节为人类可读格式 const formatBytes = (bytes: number) => { const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] if (bytes === 0) return '0 B' const i = Math.floor(Math.log(bytes) / Math.log(1024)) return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i] } // 运行时间格式化 const formatUptime = (seconds: number) => { const days = Math.floor(seconds / 86400) const hours = Math.floor((seconds % 86400) / 3600) const minutes = Math.floor((seconds % 3600) / 60) const secs = Math.floor(seconds % 60) let uptimeStr = '' if (days > 0) uptimeStr += `${days}天 ` if (hours > 0) uptimeStr += `${hours}小时 ` if (minutes > 0) uptimeStr += `${minutes}分钟 ` return `${uptimeStr}${secs}秒` } // 磁盘使用情况 (使用 du 命令,获取当前目录) let diskUsage = '' try { const duOutput = execSync('du -sh .', { encoding: 'utf-8' }) diskUsage = duOutput.trim().split('\t')[0] } catch (e) { diskUsage = '获取失败' } labels.push( { key: 'cpuUsage', title: 'CPU 使用率', value: `${cpuUsage}%`, description: 'CPU 使用率' }, { key: 'cpuCores', title: 'CPU 核心数', value: cpus.length, description: 'CPU 核心数' }, { key: 'memoryUsed', title: '已使用内存', value: formatBytes(memUsed), description: '已使用内存' }, { key: 'memoryTotal', title: '总内存', value: formatBytes(memTotal), description: '总内存' }, { key: 'memoryFree', title: '空闲内存', value: formatBytes(memFree), description: '空闲内存' }, { key: 'memoryUsage', title: '内存使用率', value: `${memUsage}%`, description: '内存使用率' }, { key: 'diskUsage', title: '磁盘使用', value: diskUsage, description: '当前目录磁盘使用情况' }, ) // 如果有 CNB_BUILD_START_TIME,添加构建启动时间 if (startTimer) { // startTimer 是日期字符串格式 const buildStartTime = dayjs(startTimer as string).format('YYYY-MM-DD HH:mm:ss') const buildStartTimestamp = dayjs(startTimer as string).valueOf() const buildUptime = Date.now() - buildStartTimestamp const buildUptimeStr = formatUptime(Math.floor(buildUptime / 1000)) const maxRunTime = useKey('CNB_PIPELINE_MAX_RUN_TIME') || 0 // 毫秒 labels.push( { key: 'buildStartTime', title: '构建启动时间', value: buildStartTime, description: '构建启动时间' }, { key: 'buildUptime', title: '构建已运行时间', value: buildUptime, description: `构建已运行时间: ${buildUptimeStr}` } ) if (maxRunTime > 0) { // 计算到达4点的倒计时 const now = dayjs() const today4am = now.hour(4).minute(0).second(0).millisecond(0) let timeTo4 = today4am.valueOf() - now.valueOf() if (timeTo4 < 0) { // 如果已经过了4点,计算到明天4点 timeTo4 = today4am.add(1, 'day').valueOf() - now.valueOf() } const timeTo4Str = `[距离晚上4点重启时间: ${formatUptime(Math.floor(timeTo4 / 1000))}]` labels.push({ key: 'buildMaxRunTime', title: '最大运行时间', value: formatUptime(Math.floor(maxRunTime / 1000)), description: '构建最大运行时间(限制时间)' }) labels.unshift({ key: 'remainingTime', title: '剩余时间', value: maxRunTime - buildUptime, description: '构建剩余时间' + formatUptime(Math.floor((maxRunTime - buildUptime) / 1000)) + ' ' + timeTo4Str }) } } // more 为 true 时添加更多系统信息 if (more) { const loadavg = os.loadavg() labels.push( { key: 'hostname', title: '主机名', value: os.hostname(), description: '主机名' }, { key: 'platform', title: '运行平台', value: os.platform(), description: '运行平台' }, { key: 'arch', title: '系统架构', value: os.arch(), description: '系统架构' }, { key: 'osType', title: '操作系统类型', value: os.type(), description: '操作系统类型' }, { key: 'loadavg1m', title: '系统负载 (1分钟)', value: loadavg[0].toFixed(2), description: '系统负载 (1分钟)' }, { key: 'loadavg5m', title: '系统负载 (5分钟)', value: loadavg[1].toFixed(2), description: '系统负载 (5分钟)' }, { key: 'loadavg15m', title: '系统负载 (15分钟)', value: loadavg[2].toFixed(2), description: '系统负载 (15分钟)' } ) } return labels }