feat(cnb-board): add cnb-dev-env routes and related functionalities

- Implemented routes for cnb-board to fetch live environment configurations, repository info, build info, pull request info, NPC info, and comment info.
- Created a utility function to execute shell commands.
- Added a check to determine if the environment is a CNB environment.
- Introduced a method to retrieve live markdown content with detailed service access information.
- Enhanced system information retrieval with CPU, memory, and disk usage metrics.
- Established a module structure for better organization of cnb-board related functionalities.
This commit is contained in:
2026-03-06 02:26:13 +08:00
parent 5043392939
commit 841ed6ffa7
12 changed files with 932 additions and 225 deletions

View File

@@ -0,0 +1,341 @@
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
}