@kevisual/cnb 0.0.29 → 0.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agent/routes/workspace/keep-file-live.ts +114 -0
- package/agent/routes/workspace/keep.ts +33 -170
- package/dist/opencode.js +482 -3879
- package/dist/routes.js +433 -3830
- package/package.json +2 -1
- package/src/workspace/keep-file-live.ts +137 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export type KeepAliveData = {
|
|
7
|
+
wsUrl: string;
|
|
8
|
+
cookie: string;
|
|
9
|
+
repo: string;
|
|
10
|
+
pipelineId: string;
|
|
11
|
+
createdTime: number;
|
|
12
|
+
filePath: string;
|
|
13
|
+
pm2Name: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type KeepAliveCache = {
|
|
17
|
+
data: KeepAliveData[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const keepAliveFilePath = path.join(os.homedir(), '.cnb/keepAliveCache.json');
|
|
21
|
+
|
|
22
|
+
export const runLive = (filePath: string, pm2Name: string) => {
|
|
23
|
+
// 使用 npx 运行命令
|
|
24
|
+
const cmdArgs = `cnb live -c ${filePath}`;
|
|
25
|
+
|
|
26
|
+
// 先停止已存在的同名 pm2 进程
|
|
27
|
+
const stopCmd = `pm2 delete ${pm2Name} 2>/dev/null || true`;
|
|
28
|
+
console.log('停止已存在的进程:', stopCmd);
|
|
29
|
+
try {
|
|
30
|
+
execSync(stopCmd, { stdio: 'inherit' });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.log('停止进程失败或进程不存在:', error);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 使用pm2启动
|
|
36
|
+
const pm2Cmd = `pm2 start ev --name ${pm2Name} --no-autorestart -- ${cmdArgs}`;
|
|
37
|
+
console.log('执行命令:', pm2Cmd);
|
|
38
|
+
try {
|
|
39
|
+
const result = execSync(pm2Cmd, { stdio: 'pipe', encoding: 'utf8' });
|
|
40
|
+
console.log(result);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error("状态码:", error.status);
|
|
43
|
+
console.error("错误详情:", error.stderr.toString()); // 这里会显示 ev 命令报的具体错误
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const stopLive = (pm2Name: string): boolean => {
|
|
48
|
+
const stopCmd = `pm2 delete ${pm2Name} 2>/dev/null || true`;
|
|
49
|
+
console.log('停止进程:', stopCmd);
|
|
50
|
+
try {
|
|
51
|
+
execSync(stopCmd, { stdio: 'inherit' });
|
|
52
|
+
console.log(`已停止 ${pm2Name} 的保持存活任务`);
|
|
53
|
+
return true;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('停止进程失败:', error);
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getKeepAliveCache(): KeepAliveCache {
|
|
61
|
+
try {
|
|
62
|
+
if (fs.existsSync(keepAliveFilePath)) {
|
|
63
|
+
const data = fs.readFileSync(keepAliveFilePath, 'utf-8');
|
|
64
|
+
const cache = JSON.parse(data) as KeepAliveCache;
|
|
65
|
+
return cache;
|
|
66
|
+
} else {
|
|
67
|
+
return { data: [] };
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('读取保持存活缓存文件失败:', error);
|
|
71
|
+
return { data: [] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function addKeepAliveData(data: KeepAliveData): KeepAliveCache {
|
|
76
|
+
const cache = getKeepAliveCache();
|
|
77
|
+
cache.data.push(data);
|
|
78
|
+
runLive(data.filePath, data.pm2Name);
|
|
79
|
+
try {
|
|
80
|
+
if (!fs.existsSync(path.dirname(keepAliveFilePath))) {
|
|
81
|
+
fs.mkdirSync(path.dirname(keepAliveFilePath), { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
fs.writeFileSync(keepAliveFilePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
84
|
+
return cache;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('写入保持存活缓存文件失败:', error);
|
|
87
|
+
return { data: [] };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function removeKeepAliveData(repo: string, pipelineId: string): KeepAliveCache {
|
|
92
|
+
const cache = getKeepAliveCache();
|
|
93
|
+
cache.data = cache.data.filter(item => item.repo !== repo || item.pipelineId !== pipelineId);
|
|
94
|
+
try {
|
|
95
|
+
fs.writeFileSync(keepAliveFilePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
96
|
+
return cache;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('写入保持存活缓存文件失败:', error);
|
|
99
|
+
return { data: [] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const createLiveData = (data: { wsUrl: string, cookie: string, repo: string, pipelineId: string }): KeepAliveData => {
|
|
104
|
+
const { wsUrl, cookie, repo, pipelineId } = data;
|
|
105
|
+
const createdTime = Date.now();
|
|
106
|
+
const pm2Name = `${repo}__${pipelineId}`.replace(/\//g, '__');
|
|
107
|
+
const filePath = path.join(os.homedir(), '.cnb', `${pm2Name}.json`);
|
|
108
|
+
const _newData = { wss: wsUrl, wsUrl, cookie, repo, pipelineId, createdTime, filePath, pm2Name };
|
|
109
|
+
if (!fs.existsSync(path.dirname(filePath))) {
|
|
110
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
111
|
+
}
|
|
112
|
+
fs.writeFileSync(filePath, JSON.stringify(_newData, null, 2), 'utf-8');
|
|
113
|
+
return _newData;
|
|
114
|
+
}
|
|
@@ -1,214 +1,77 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tool } from '@kevisual/router';
|
|
2
2
|
import { app, cnb } from '../../app.ts';
|
|
3
|
-
import {
|
|
4
|
-
import dayjs from 'dayjs';
|
|
5
|
-
import { createKeepAlive } from '../../../src/keep.ts';
|
|
6
|
-
|
|
7
|
-
type AliveInfo = {
|
|
8
|
-
startTime: number;
|
|
9
|
-
updatedTime?: number;
|
|
10
|
-
KeepAlive: ReturnType<typeof createKeepAlive>;
|
|
11
|
-
id: string;// 6位唯一标识符
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const keepAliveMap = new Map<string, AliveInfo>();
|
|
3
|
+
import { addKeepAliveData, KeepAliveData, removeKeepAliveData, createLiveData } from '../../../src/workspace/keep-file-live.ts';
|
|
15
4
|
|
|
16
5
|
// 保持工作空间存活技能
|
|
17
6
|
app.route({
|
|
18
7
|
path: 'cnb',
|
|
19
8
|
key: 'keep-workspace-alive',
|
|
20
|
-
description: '
|
|
9
|
+
description: '保持工作空间存活技能,参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001',
|
|
21
10
|
middleware: ['admin-auth'],
|
|
22
11
|
metadata: {
|
|
23
12
|
tags: [],
|
|
24
13
|
...({
|
|
25
14
|
args: {
|
|
26
|
-
|
|
27
|
-
|
|
15
|
+
repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
|
|
16
|
+
pipelineId: tool.schema.string().describe('流水线ID,例如 cnb-708-1ji9sog7o-001'),
|
|
28
17
|
}
|
|
29
18
|
})
|
|
30
19
|
}
|
|
31
20
|
}).define(async (ctx) => {
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
21
|
+
const repo = ctx.query?.repo as string;
|
|
22
|
+
const pipelineId = ctx.query?.pipelineId as string;
|
|
23
|
+
|
|
24
|
+
if (!repo || !pipelineId) {
|
|
25
|
+
ctx.throw(400, '缺少参数 repo 或 pipelineId');
|
|
36
26
|
}
|
|
37
|
-
|
|
38
|
-
|
|
27
|
+
const validCookie = await cnb.user.checkCookieValid()
|
|
28
|
+
if (validCookie.code !== 200) {
|
|
29
|
+
ctx.throw(401, 'CNB_COOKIE 环境变量无效或已过期,请重新登录获取新的cookie');
|
|
39
30
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
const res = await cnb.workspace.getWorkspaceCookie(repo, pipelineId);
|
|
32
|
+
let wsUrl = `wss://${pipelineId}.cnb.space:443?skipWebSocketFrames=false`;
|
|
33
|
+
let cookie = '';
|
|
34
|
+
if (res.code === 200) {
|
|
35
|
+
cookie = res.data.value;
|
|
36
|
+
console.log(`启动保持工作空间 ${wsUrl} 存活的任务`);
|
|
37
|
+
} else {
|
|
38
|
+
ctx.throw(500, `获取工作空间访问cookie失败: ${res.message}`);
|
|
46
39
|
}
|
|
47
40
|
|
|
48
41
|
console.log(`启动保持工作空间 ${wsUrl} 存活的任务`);
|
|
49
|
-
const keep = createKeepAlive({
|
|
50
|
-
wsUrl,
|
|
51
|
-
cookie,
|
|
52
|
-
onConnect: () => {
|
|
53
|
-
console.log(`工作空间 ${wsUrl} 保持存活任务已连接`);
|
|
54
|
-
},
|
|
55
|
-
onMessage: (data) => {
|
|
56
|
-
// 可选:处理收到的消息
|
|
57
|
-
// console.log(`工作空间 ${wsUrl} 收到消息: ${data}`);
|
|
58
|
-
// 通过 wsUrl 找到对应的 id 并更新时间
|
|
59
|
-
for (const info of keepAliveMap.values()) {
|
|
60
|
-
if ((info as any).KeepAlive?.wsUrl === wsUrl) {
|
|
61
|
-
info.updatedTime = Date.now();
|
|
62
|
-
break;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
debug: true,
|
|
67
|
-
onExit: (code) => {
|
|
68
|
-
console.log(`工作空间 ${wsUrl} 保持存活任务已退出,退出码: ${code}`);
|
|
69
|
-
// 通过 wsUrl 找到对应的 id 并删除
|
|
70
|
-
for (const [id, info] of keepAliveMap.entries()) {
|
|
71
|
-
if ((info as any).KeepAlive?.wsUrl === wsUrl) {
|
|
72
|
-
keepAliveMap.delete(id);
|
|
73
|
-
break;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
42
|
|
|
79
|
-
const
|
|
80
|
-
|
|
43
|
+
const config: KeepAliveData = createLiveData({ cookie, repo, pipelineId });
|
|
44
|
+
addKeepAliveData(config);
|
|
81
45
|
|
|
82
|
-
ctx.body = { content: `已启动保持工作空间 ${wsUrl} 存活的任务`,
|
|
83
|
-
}).addTo(app);
|
|
84
|
-
|
|
85
|
-
// 获取保持工作空间存活任务列表技能
|
|
86
|
-
app.route({
|
|
87
|
-
path: 'cnb',
|
|
88
|
-
key: 'list-keep-alive-tasks',
|
|
89
|
-
description: '获取保持工作空间存活任务列表技能',
|
|
90
|
-
middleware: ['admin-auth'],
|
|
91
|
-
metadata: {
|
|
92
|
-
tags: [],
|
|
93
|
-
}
|
|
94
|
-
}).define(async (ctx) => {
|
|
95
|
-
const list = Array.from(keepAliveMap.entries()).map(([id, info]) => {
|
|
96
|
-
const now = Date.now();
|
|
97
|
-
const duration = Math.floor((now - info.startTime) / 60000); // 分钟
|
|
98
|
-
return {
|
|
99
|
-
id,
|
|
100
|
-
wsUrl: (info as any).KeepAlive?.wsUrl,
|
|
101
|
-
startTime: info.startTime,
|
|
102
|
-
startTimeStr: dayjs(info.startTime).format('YYYY-MM-DD HH:mm'),
|
|
103
|
-
updatedTime: info.updatedTime,
|
|
104
|
-
updatedTimeStr: dayjs(info.updatedTime).format('YYYY-MM-DD HH:mm'),
|
|
105
|
-
duration,
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
ctx.body = { list };
|
|
46
|
+
ctx.body = { content: `已启动保持工作空间 ${wsUrl} 存活的任务`, data: config };
|
|
109
47
|
}).addTo(app);
|
|
110
48
|
|
|
111
49
|
// 停止保持工作空间存活技能
|
|
112
50
|
app.route({
|
|
113
51
|
path: 'cnb',
|
|
114
52
|
key: 'stop-keep-workspace-alive',
|
|
115
|
-
description: '停止保持工作空间存活技能, 参数
|
|
53
|
+
description: '停止保持工作空间存活技能, 参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001',
|
|
116
54
|
middleware: ['admin-auth'],
|
|
117
55
|
metadata: {
|
|
118
56
|
tags: [],
|
|
119
57
|
...({
|
|
120
58
|
args: {
|
|
121
|
-
|
|
122
|
-
|
|
59
|
+
repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
|
|
60
|
+
pipelineId: tool.schema.string().describe('流水线ID,例如 cnb-708-1ji9sog7o-001'),
|
|
123
61
|
}
|
|
124
62
|
})
|
|
125
63
|
}
|
|
126
64
|
}).define(async (ctx) => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
129
|
-
if (!wsUrl && !id) {
|
|
130
|
-
ctx.throw(400, '缺少工作空间访问URL参数或唯一标识符');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let targetId: string | undefined;
|
|
134
|
-
let wsUrlFound: string | undefined;
|
|
135
|
-
|
|
136
|
-
if (id) {
|
|
137
|
-
const info = keepAliveMap.get(id);
|
|
138
|
-
if (info) {
|
|
139
|
-
targetId = id;
|
|
140
|
-
wsUrlFound = (info as any).KeepAlive?.wsUrl;
|
|
141
|
-
}
|
|
142
|
-
} else if (wsUrl) {
|
|
143
|
-
for (const [key, info] of keepAliveMap.entries()) {
|
|
144
|
-
if ((info as any).KeepAlive?.wsUrl === wsUrl) {
|
|
145
|
-
targetId = key;
|
|
146
|
-
wsUrlFound = wsUrl;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
65
|
+
const repo = ctx.query?.repo as string;
|
|
66
|
+
const pipelineId = ctx.query?.pipelineId as string;
|
|
151
67
|
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
const endTime = Date.now();
|
|
155
|
-
const duration = endTime - keepAlive!.startTime;
|
|
156
|
-
keepAlive?.KeepAlive?.disconnect();
|
|
157
|
-
keepAliveMap.delete(targetId);
|
|
158
|
-
ctx.body = { content: `已停止保持工作空间 ${wsUrlFound} 存活的任务,持续时间: ${duration}ms`, id: targetId };
|
|
159
|
-
} else {
|
|
160
|
-
ctx.body = { content: `没有找到对应的工作空间保持存活任务` };
|
|
68
|
+
if (!repo || !pipelineId) {
|
|
69
|
+
ctx.throw(400, '缺少参数 repo 或 pipelineId');
|
|
161
70
|
}
|
|
71
|
+
removeKeepAliveData(repo, pipelineId);
|
|
72
|
+
ctx.body = { content: `已停止保持工作空间 ${repo}/${pipelineId} 存活的任务` };
|
|
162
73
|
}).addTo(app);
|
|
163
74
|
|
|
164
|
-
app.route({
|
|
165
|
-
path: 'cnb',
|
|
166
|
-
key: 'reset-keep-workspace-alive',
|
|
167
|
-
description: '对存活的工作空间,startTime进行重置',
|
|
168
|
-
middleware: ['admin-auth'],
|
|
169
|
-
metadata: {
|
|
170
|
-
tags: [],
|
|
171
|
-
}
|
|
172
|
-
}).define(async (ctx) => {
|
|
173
|
-
const now = Date.now();
|
|
174
|
-
for (const info of keepAliveMap.values()) {
|
|
175
|
-
info.startTime = now;
|
|
176
|
-
}
|
|
177
|
-
ctx.body = { content: `已重置所有存活工作空间的开始时间` };
|
|
178
|
-
}).addTo(app);
|
|
179
75
|
|
|
180
|
-
app.route({
|
|
181
|
-
path: 'cnb',
|
|
182
|
-
key: 'clear-keep-workspace-alive',
|
|
183
|
-
description: '对存活的工作空间,超过5小时的进行清理',
|
|
184
|
-
middleware: ['admin-auth'],
|
|
185
|
-
metadata: {
|
|
186
|
-
tags: [],
|
|
187
|
-
}
|
|
188
|
-
}).define(async (ctx) => {
|
|
189
|
-
const res = clearKeepAlive();
|
|
190
|
-
ctx.body = {
|
|
191
|
-
content: `已清理所有存活工作空间中超过5小时的任务` + (res.length ? `,清理项:${res.map(i => i.wsUrl).join(', ')}` : ''),
|
|
192
|
-
list: res
|
|
193
|
-
};
|
|
194
|
-
}).addTo(app);
|
|
195
76
|
|
|
196
|
-
const clearKeepAlive = () => {
|
|
197
|
-
const now = Date.now();
|
|
198
|
-
let clearedArr: { id: string; wsUrl: string }[] = [];
|
|
199
|
-
for (const [id, info] of keepAliveMap.entries()) {
|
|
200
|
-
if (now - info.startTime > FIVE_HOURS) {
|
|
201
|
-
console.log(`工作空间 ${(info as any).KeepAlive?.wsUrl} 超过5小时,自动停止`);
|
|
202
|
-
info.KeepAlive?.disconnect?.();
|
|
203
|
-
keepAliveMap.delete(id);
|
|
204
|
-
clearedArr.push({ id, wsUrl: (info as any).KeepAlive?.wsUrl });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return clearedArr;
|
|
208
|
-
}
|
|
209
77
|
|
|
210
|
-
// 每5小时自动清理超时的keepAlive任务
|
|
211
|
-
const FIVE_HOURS = 5 * 60 * 60 * 1000;
|
|
212
|
-
setInterval(() => {
|
|
213
|
-
clearKeepAlive();
|
|
214
|
-
}, FIVE_HOURS);
|