@kevisual/cnb 0.0.33 → 0.0.34

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.
@@ -0,0 +1,341 @@
1
+
2
+ import { useKey } from "@kevisual/context"
3
+ import os from 'node:os';
4
+ import { execSync } from 'node:child_process';
5
+ import dayjs from 'dayjs';
6
+
7
+ export const getLiveMdContent = (opts?: { more?: boolean }) => {
8
+ const more = opts?.more ?? false
9
+ const url = useKey('CNB_VSCODE_PROXY_URI') || ''
10
+ const token = useKey('CNB_TOKEN') || ''
11
+ const openclawPort = useKey('OPENCLAW_PORT') || '80'
12
+ const openclawUrl = url.replace('{{port}}', openclawPort)
13
+ const openclawUrlSecret = openclawUrl + '/openclaw?token=' + token
14
+
15
+ const opencodePort = useKey('OPENCODE_PORT') || '100'
16
+ const opencodeUrl = url.replace('{{port}}', opencodePort)
17
+ // btoa('root:password'); //
18
+ const _opencodeURL = new URL(opencodeUrl)
19
+ _opencodeURL.username = 'root'
20
+ _opencodeURL.password = token
21
+ const opencodeUrlSecret = _opencodeURL.toString()
22
+
23
+ // console.log('btoa opencode auth: ', Buffer.from(`root:${token}`).toString('base64'))
24
+ const kevisualUrl = url.replace('{{port}}', '51515')
25
+
26
+ const openWebUrl = url.replace('{{port}}', '200')
27
+
28
+ const vscodeWebUrl = useKey('CNB_VSCODE_WEB_URL') || ''
29
+
30
+ const TEMPLATE = `# 开发环境模式配置
31
+
32
+ ### 服务访问地址
33
+ #### nginx 反向代理访问(推荐)
34
+ - OpenClaw: ${openclawUrl + '/openclaw'}
35
+ - OpenCode: ${opencodeUrl}
36
+ - VSCode Web: ${vscodeWebUrl}
37
+ - OpenWebUI: ${openWebUrl}
38
+ - Kevisual: ${kevisualUrl}
39
+
40
+ ### 密码访问
41
+ - OpenClaw: ${openclawUrlSecret}
42
+ - OpenCode: ${opencodeUrlSecret}
43
+
44
+ ### 环境变量
45
+ - CNB_TOKEN: ${token}
46
+
47
+ ### 其他说明
48
+
49
+ 1. 保活说明
50
+ 使用插件访问vscode web获取wss进行保活,避免长时间不操作导致的自动断开连接。
51
+
52
+ 方法1: 使用插件访问vscode web获取wss进行保活,避免长时间不操作导致的自动断开连接。
53
+
54
+ 1. 安装插件[CNB LIVE](https://chromewebstore.google.com/detail/cnb-live/iajpiophkcdghonpijkcgpjafbcjhkko?pli=1)
55
+ 2. 打开vscode web获取,点击插件,获取json数据,替换keep.json中的数据,保持在线状态。
56
+ 3. keep.json中的数据结构说明:
57
+ - wss: vscode web的websocket地址
58
+ - cookie: vscode web的cookie,保持和浏览器一致
59
+ - url: vscode web的访问地址,可以直接访问vscode web
60
+ 4. 运行cli命令,ev cnb live -c /workspace/live/keep.json.(直接对话opencode或者openclaw调用cnb-live技能即可)
61
+
62
+ 方法2:环境变量设置CNB_COOKIE,直接opencode或者openclaw的ui界面对话说,cnb-keep-live保活,他会自动调用保活,同时不需要点cnb-lie插件获取配置。
63
+
64
+ 2. Opencode web访问说明
65
+ Opencode打开web地址,需要在浏览器输入用户名和密码,用户名固定为root,密码为CNB_TOKEN的值. 纯连接打开包含账号密码,第一次点击后,需要把账号密码清理掉才能访问,opencode的bug导致的。
66
+ `
67
+ const labels = [
68
+ {
69
+ key: 'vscodeWebUrl',
70
+ title: 'VSCode Web 地址',
71
+ value: vscodeWebUrl,
72
+ description: 'VSCode Web 的访问地址'
73
+ },
74
+ {
75
+ key: 'kevisualUrl',
76
+ title: 'Kevisual 地址',
77
+ value: kevisualUrl,
78
+ description: 'Kevisual 的访问地址,可以通过该地址访问 Kevisual 服务'
79
+ },
80
+ {
81
+ key: 'cnbTempToken',
82
+ title: 'CNB Token',
83
+ value: token,
84
+ description: 'CNB 临时 Token,保持和环境变量 CNB_TOKEN 一致'
85
+ },
86
+ {
87
+ key: 'openWebUrl',
88
+ title: 'OpenWebUI 地址',
89
+ value: openWebUrl,
90
+ description: 'OpenWebUI 的访问地址,可以通过该地址访问 OpenWebUI 服务'
91
+ },
92
+ {
93
+ key: 'openclawUrl',
94
+ title: 'OpenClaw 地址',
95
+ value: openclawUrl + '/openclaw',
96
+ description: 'OpenClaw 的访问地址,可以通过该地址访问 OpenClaw 服务'
97
+ },
98
+ {
99
+ key: 'openclawUrlSecret',
100
+ title: 'OpenClaw 访问地址(含 Token)',
101
+ value: openclawUrlSecret,
102
+ description: 'OpenClaw 的访问地址,包含 token 参数,可以直接访问 OpenClaw 服务'
103
+ },
104
+ {
105
+ key: 'opencodeUrl',
106
+ title: 'OpenCode 地址',
107
+ value: opencodeUrl,
108
+ description: 'OpenCode 的访问地址,可以通过该地址访问 OpenCode 服务'
109
+ },
110
+ {
111
+ key: 'opencodeUrlSecret',
112
+ title: 'OpenCode 访问地址(含 Token)',
113
+ value: opencodeUrlSecret,
114
+ description: 'OpenCode 的访问地址,包含 token 参数,可以直接访问 OpenCode 服务'
115
+ },
116
+ {
117
+ key: 'docs',
118
+ title: '配置说明文档',
119
+ value: TEMPLATE,
120
+ description: '开发环境模式配置说明文档'
121
+ }
122
+ ]
123
+
124
+ const osInfoList = createOSInfo(more)
125
+ labels.push(...osInfoList)
126
+ return labels
127
+ }
128
+
129
+ const createOSInfo = (more = false) => {
130
+ const labels: Array<{ key: string; title: string; value: string | number; description: string }> = []
131
+ const startTimer = useKey('CNB_BUILD_START_TIME') || ''
132
+
133
+ // CPU 使用率
134
+ const cpus = os.cpus()
135
+ let totalIdle = 0
136
+ let totalTick = 0
137
+ cpus.forEach((cpu) => {
138
+ for (const type in cpu.times) {
139
+ totalTick += cpu.times[type as keyof typeof cpu.times]
140
+ }
141
+ totalIdle += cpu.times.idle
142
+ })
143
+ const cpuUsage = ((1 - totalIdle / totalTick) * 100).toFixed(2)
144
+
145
+ // 内存使用情况 (使用 free 命令)
146
+ let memUsed = 0
147
+ let memTotal = 0
148
+ let memFree = 0
149
+ try {
150
+ const freeOutput = execSync('free -b', { encoding: 'utf-8' })
151
+ const lines = freeOutput.trim().split('\n')
152
+ const memLine = lines.find(line => line.startsWith('Mem:'))
153
+ if (memLine) {
154
+ const parts = memLine.split(/\s+/)
155
+ memTotal = parseInt(parts[1])
156
+ memUsed = parseInt(parts[2])
157
+ memFree = parseInt(parts[3])
158
+ }
159
+ } catch (e) {
160
+ // 如果 free 命令失败,使用 os 模块
161
+ memTotal = os.totalmem()
162
+ memFree = os.freemem()
163
+ memUsed = memTotal - memFree
164
+ }
165
+ const memUsage = memTotal > 0 ? ((memUsed / memTotal) * 100).toFixed(2) : '0.00'
166
+
167
+ // 格式化字节为人类可读格式
168
+ const formatBytes = (bytes: number) => {
169
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
170
+ if (bytes === 0) return '0 B'
171
+ const i = Math.floor(Math.log(bytes) / Math.log(1024))
172
+ return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i]
173
+ }
174
+
175
+ // 运行时间格式化
176
+ const formatUptime = (seconds: number) => {
177
+ const days = Math.floor(seconds / 86400)
178
+ const hours = Math.floor((seconds % 86400) / 3600)
179
+ const minutes = Math.floor((seconds % 3600) / 60)
180
+ const secs = Math.floor(seconds % 60)
181
+ let uptimeStr = ''
182
+ if (days > 0) uptimeStr += `${days}天 `
183
+ if (hours > 0) uptimeStr += `${hours}小时 `
184
+ if (minutes > 0) uptimeStr += `${minutes}分钟 `
185
+ return `${uptimeStr}${secs}秒`
186
+ }
187
+
188
+ // 磁盘使用情况 (使用 du 命令,获取当前目录)
189
+ let diskUsage = ''
190
+ try {
191
+ const duOutput = execSync('du -sh .', { encoding: 'utf-8' })
192
+ diskUsage = duOutput.trim().split('\t')[0]
193
+ } catch (e) {
194
+ diskUsage = '获取失败'
195
+ }
196
+
197
+ labels.push(
198
+ {
199
+ key: 'cpuUsage',
200
+ title: 'CPU 使用率',
201
+ value: `${cpuUsage}%`,
202
+ description: 'CPU 使用率'
203
+ },
204
+ {
205
+ key: 'cpuCores',
206
+ title: 'CPU 核心数',
207
+ value: cpus.length,
208
+ description: 'CPU 核心数'
209
+ },
210
+ {
211
+ key: 'memoryUsed',
212
+ title: '已使用内存',
213
+ value: formatBytes(memUsed),
214
+ description: '已使用内存'
215
+ },
216
+ {
217
+ key: 'memoryTotal',
218
+ title: '总内存',
219
+ value: formatBytes(memTotal),
220
+ description: '总内存'
221
+ },
222
+ {
223
+ key: 'memoryFree',
224
+ title: '空闲内存',
225
+ value: formatBytes(memFree),
226
+ description: '空闲内存'
227
+ },
228
+ {
229
+ key: 'memoryUsage',
230
+ title: '内存使用率',
231
+ value: `${memUsage}%`,
232
+ description: '内存使用率'
233
+ },
234
+ {
235
+ key: 'diskUsage',
236
+ title: '磁盘使用',
237
+ value: diskUsage,
238
+ description: '当前目录磁盘使用情况'
239
+ },
240
+ )
241
+
242
+ // 如果有 CNB_BUILD_START_TIME,添加构建启动时间
243
+ if (startTimer) {
244
+ // startTimer 是日期字符串格式
245
+ const buildStartTime = dayjs(startTimer as string).format('YYYY-MM-DD HH:mm:ss')
246
+ const buildStartTimestamp = dayjs(startTimer as string).valueOf()
247
+ const buildUptime = Date.now() - buildStartTimestamp
248
+ const buildUptimeStr = formatUptime(Math.floor(buildUptime / 1000))
249
+ const maxRunTime = useKey('CNB_PIPELINE_MAX_RUN_TIME') || 0 // 毫秒
250
+
251
+ labels.push(
252
+ {
253
+ key: 'buildStartTime',
254
+ title: '构建启动时间',
255
+ value: buildStartTime,
256
+ description: '构建启动时间'
257
+ },
258
+ {
259
+ key: 'buildUptime',
260
+ title: '构建已运行时间',
261
+ value: buildUptime,
262
+ description: `构建已运行时间: ${buildUptimeStr}`
263
+ }
264
+ )
265
+ if (maxRunTime > 0) {
266
+ // 计算到达4点的倒计时
267
+ const now = dayjs()
268
+ const today4am = now.hour(4).minute(0).second(0).millisecond(0)
269
+ let timeTo4 = today4am.valueOf() - now.valueOf()
270
+ if (timeTo4 < 0) {
271
+ // 如果已经过了4点,计算到明天4点
272
+ timeTo4 = today4am.add(1, 'day').valueOf() - now.valueOf()
273
+ }
274
+ const timeTo4Str = `[距离晚上4点重启时间: ${formatUptime(Math.floor(timeTo4 / 1000))}]`
275
+
276
+ labels.push({
277
+ key: 'buildMaxRunTime',
278
+ title: '最大运行时间',
279
+ value: formatUptime(Math.floor(maxRunTime / 1000)),
280
+ description: '构建最大运行时间(限制时间)'
281
+ })
282
+ labels.unshift({
283
+ key: 'remainingTime',
284
+ title: '剩余时间',
285
+ value: maxRunTime - buildUptime,
286
+ description: '构建剩余时间' + formatUptime(Math.floor((maxRunTime - buildUptime) / 1000)) + ' ' + timeTo4Str
287
+ })
288
+ }
289
+ }
290
+
291
+ // more 为 true 时添加更多系统信息
292
+ if (more) {
293
+ const loadavg = os.loadavg()
294
+ labels.push(
295
+ {
296
+ key: 'hostname',
297
+ title: '主机名',
298
+ value: os.hostname(),
299
+ description: '主机名'
300
+ },
301
+ {
302
+ key: 'platform',
303
+ title: '运行平台',
304
+ value: os.platform(),
305
+ description: '运行平台'
306
+ },
307
+ {
308
+ key: 'arch',
309
+ title: '系统架构',
310
+ value: os.arch(),
311
+ description: '系统架构'
312
+ },
313
+ {
314
+ key: 'osType',
315
+ title: '操作系统类型',
316
+ value: os.type(),
317
+ description: '操作系统类型'
318
+ },
319
+ {
320
+ key: 'loadavg1m',
321
+ title: '系统负载 (1分钟)',
322
+ value: loadavg[0].toFixed(2),
323
+ description: '系统负载 (1分钟)'
324
+ },
325
+ {
326
+ key: 'loadavg5m',
327
+ title: '系统负载 (5分钟)',
328
+ value: loadavg[1].toFixed(2),
329
+ description: '系统负载 (5分钟)'
330
+ },
331
+ {
332
+ key: 'loadavg15m',
333
+ title: '系统负载 (15分钟)',
334
+ value: loadavg[2].toFixed(2),
335
+ description: '系统负载 (15分钟)'
336
+ }
337
+ )
338
+ }
339
+
340
+ return labels
341
+ }
@@ -0,0 +1 @@
1
+ export * from './is-cnb.ts';
@@ -0,0 +1,6 @@
1
+ import { useKey } from "@kevisual/context";
2
+
3
+ export const isCnb = () => {
4
+ const CNB = useKey('CNB');
5
+ return !!CNB;
6
+ }
@@ -6,6 +6,7 @@ import './call/index.ts'
6
6
  import './cnb-env/index.ts'
7
7
  import './knowledge/index.ts'
8
8
  import './issues/index.ts'
9
+ import './cnb-board/index.ts';
9
10
 
10
11
  /**
11
12
  * 验证上下文中的 App ID 是否与指定的 App ID 匹配
@@ -25,25 +26,23 @@ const checkAppId = (ctx: any, appId: string) => {
25
26
  return false;
26
27
  }
27
28
 
28
- if (!app.hasRoute('auth')) {
29
- app.route({
30
- id: 'auth',
31
- path: 'auth',
32
- }).define(async (ctx) => {
33
- // ctx.body = 'Auth Route';
34
- if (checkAppId(ctx, app.appId)) {
35
- return;
36
- }
37
- }).addTo(app);
29
+ app.route({
30
+ id: 'auth',
31
+ path: 'auth',
32
+ }).define(async (ctx) => {
33
+ // ctx.body = 'Auth Route';
34
+ if (checkAppId(ctx, app.appId)) {
35
+ return;
36
+ }
37
+ }).addTo(app, { overwrite: false });
38
38
 
39
- app.route({
40
- id: 'admin-auth',
41
- path: 'admin-auth',
42
- middleware: ['auth'],
43
- }).define(async (ctx) => {
44
- // ctx.body = 'Admin Auth Route';
45
- if (checkAppId(ctx, app.appId)) {
46
- return;
47
- }
48
- }).addTo(app);
49
- }
39
+ app.route({
40
+ id: 'admin-auth',
41
+ path: 'admin-auth',
42
+ middleware: ['auth'],
43
+ }).define(async (ctx) => {
44
+ // ctx.body = 'Admin Auth Route';
45
+ if (checkAppId(ctx, app.appId)) {
46
+ return;
47
+ }
48
+ }).addTo(app, { overwrite: false });
@@ -1,6 +1,7 @@
1
1
  import { tool } from '@kevisual/router';
2
2
  import { app, cnb } from '../../app.ts';
3
3
  import { addKeepAliveData, KeepAliveData, removeKeepAliveData, createLiveData } from '../../../src/workspace/keep-file-live.ts';
4
+ import { useKey } from '@kevisual/context';
4
5
 
5
6
  // 保持工作空间存活技能
6
7
  app.route({
@@ -75,3 +76,23 @@ app.route({
75
76
 
76
77
 
77
78
 
79
+ app.route({
80
+ path: 'cnb',
81
+ key: 'keep-alive-current-workspace',
82
+ description: '保持当前工作空间存活技能',
83
+ middleware: ['admin-auth'],
84
+ metadata: {
85
+ tags: ['opencode'],
86
+ skill: 'keep-alive-current-workspace',
87
+ title: '保持当前工作空间存活',
88
+ summary: '保持当前工作空间存活,防止被关闭或释放资源',
89
+ }
90
+ }).define(async (ctx) => {
91
+ const pipelineId = useKey('CNB_PIPELINE_ID');
92
+ const repo = useKey('CNB_REPO_SLUG_LOWERCASE');
93
+ if (!pipelineId || !repo) {
94
+ ctx.throw(400, '当前环境缺少 CNB_PIPELINE_ID 或 CNB_REPO_SLUG_LOWERCASE 环境变量,无法保持工作空间存活');
95
+ }
96
+ const res = await app.run({ path: 'cnb', key: 'keep-workspace-alive', payload: { repo, pipelineId } }, ctx);
97
+ ctx.forward(res);
98
+ }).addTo(app);