341 lines
10 KiB
TypeScript
341 lines
10 KiB
TypeScript
|
||
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
|
||
} |