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

@@ -1,114 +0,0 @@
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import { execSync } from 'node:child_process';
export type KeepAliveData = {
wsUrl: string;
cookie: string;
repo: string;
pipelineId: string;
createdTime: number;
filePath: string;
pm2Name: string;
}
type KeepAliveCache = {
data: KeepAliveData[];
}
const keepAliveFilePath = path.join(os.homedir(), '.cnb/keepAliveCache.json');
export const runLive = (filePath: string, pm2Name: string) => {
// 使用 npx 运行命令
const cmdArgs = `cnb live -c ${filePath}`;
// 先停止已存在的同名 pm2 进程
const stopCmd = `pm2 delete ${pm2Name} 2>/dev/null || true`;
console.log('停止已存在的进程:', stopCmd);
try {
execSync(stopCmd, { stdio: 'inherit' });
} catch (error) {
console.log('停止进程失败或进程不存在:', error);
}
// 使用pm2启动
const pm2Cmd = `pm2 start ev --name ${pm2Name} --no-autorestart -- ${cmdArgs}`;
console.log('执行命令:', pm2Cmd);
try {
const result = execSync(pm2Cmd, { stdio: 'pipe', encoding: 'utf8' });
console.log(result);
} catch (error) {
console.error("状态码:", error.status);
console.error("错误详情:", error.stderr.toString()); // 这里会显示 ev 命令报的具体错误
}
}
export const stopLive = (pm2Name: string): boolean => {
const stopCmd = `pm2 delete ${pm2Name} 2>/dev/null || true`;
console.log('停止进程:', stopCmd);
try {
execSync(stopCmd, { stdio: 'inherit' });
console.log(`已停止 ${pm2Name} 的保持存活任务`);
return true;
} catch (error) {
console.error('停止进程失败:', error);
}
return false;
}
export function getKeepAliveCache(): KeepAliveCache {
try {
if (fs.existsSync(keepAliveFilePath)) {
const data = fs.readFileSync(keepAliveFilePath, 'utf-8');
const cache = JSON.parse(data) as KeepAliveCache;
return cache;
} else {
return { data: [] };
}
} catch (error) {
console.error('读取保持存活缓存文件失败:', error);
return { data: [] };
}
}
export function addKeepAliveData(data: KeepAliveData): KeepAliveCache {
const cache = getKeepAliveCache();
cache.data.push(data);
runLive(data.filePath, data.pm2Name);
try {
if (!fs.existsSync(path.dirname(keepAliveFilePath))) {
fs.mkdirSync(path.dirname(keepAliveFilePath), { recursive: true });
}
fs.writeFileSync(keepAliveFilePath, JSON.stringify(cache, null, 2), 'utf-8');
return cache;
} catch (error) {
console.error('写入保持存活缓存文件失败:', error);
return { data: [] };
}
}
export function removeKeepAliveData(repo: string, pipelineId: string): KeepAliveCache {
const cache = getKeepAliveCache();
cache.data = cache.data.filter(item => item.repo !== repo || item.pipelineId !== pipelineId);
try {
fs.writeFileSync(keepAliveFilePath, JSON.stringify(cache, null, 2), 'utf-8');
return cache;
} catch (error) {
console.error('写入保持存活缓存文件失败:', error);
return { data: [] };
}
}
export const createLiveData = (data: { wsUrl: string, cookie: string, repo: string, pipelineId: string }): KeepAliveData => {
const { wsUrl, cookie, repo, pipelineId } = data;
const createdTime = Date.now();
const pm2Name = `${repo}__${pipelineId}`.replace(/\//g, '__');
const filePath = path.join(os.homedir(), '.cnb', `${pm2Name}.json`);
const _newData = { wss: wsUrl, wsUrl, cookie, repo, pipelineId, createdTime, filePath, pm2Name };
if (!fs.existsSync(path.dirname(filePath))) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
}
fs.writeFileSync(filePath, JSON.stringify(_newData, null, 2), 'utf-8');
return _newData;
}

View File

@@ -1,6 +1,7 @@
import { tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts';
import { addKeepAliveData, KeepAliveData, removeKeepAliveData, createLiveData } from '../../../src/workspace/keep-file-live.ts';
import { useKey } from '@kevisual/context';
// 保持工作空间存活技能
app.route({
@@ -75,3 +76,23 @@ app.route({
app.route({
path: 'cnb',
key: 'keep-alive-current-workspace',
description: '保持当前工作空间存活技能',
middleware: ['admin-auth'],
metadata: {
tags: ['opencode'],
skill: 'keep-alive-current-workspace',
title: '保持当前工作空间存活',
summary: '保持当前工作空间存活,防止被关闭或释放资源',
}
}).define(async (ctx) => {
const pipelineId = useKey('CNB_PIPELINE_ID');
const repo = useKey('CNB_REPO_SLUG_LOWERCASE');
if (!pipelineId || !repo) {
ctx.throw(400, '当前环境缺少 CNB_PIPELINE_ID 或 CNB_REPO_SLUG_LOWERCASE 环境变量,无法保持工作空间存活');
}
const res = await app.run({ path: 'cnb', key: 'keep-workspace-alive', payload: { repo, pipelineId } }, ctx);
ctx.forward(res);
}).addTo(app);