Compare commits

..

23 Commits

Author SHA1 Message Date
21ba07e55b feat: 更新保持工作空间存活技能,优化错误处理并添加保活数据 2026-03-07 02:19:49 +08:00
81a3aae8ec fix 2026-03-07 02:08:40 +08:00
dd5331bbaa feat: 添加新的分享路由以获取cnb工作空间中助手的访问地址 2026-03-07 01:50:50 +08:00
2eecbe273e fix 2026-03-06 02:47:00 +08:00
a563f3e0d6 feat: 将所有中间件名称从'admin-auth'更新为'auth-admin',以统一认证方式 2026-03-06 02:45:29 +08:00
327db1e09a feat: 更新获取已关闭工作空间列表的请求,添加pageSize参数以限制返回数量 2026-03-06 02:42:38 +08:00
8465ba7182 feat: 更新端口默认值为51515,添加useKey以获取默认仓库ID,更新创建文件描述信息 2026-03-06 02:39:45 +08:00
841ed6ffa7 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.
2026-03-06 02:26:13 +08:00
5043392939 feat: 更新版本号和依赖项,重构keepAliveFilePath路径以支持新的目录结构 2026-02-28 04:15:06 +08:00
8a6bb9bbe9 feat: 更新createLiveData函数,修改pm2Name格式以包含前缀"keep_" 2026-02-25 18:53:38 +08:00
8ca6b77e4d feat: 更新保持工作空间存活功能,移除wsUrl参数并在createLiveData中生成 2026-02-25 18:31:35 +08:00
d3286e2766 feat: 重构保持工作空间存活功能,更新参数和添加数据管理,新增测试用例 2026-02-25 17:34:49 +08:00
dd691f7a59 feat: 更新版本号和依赖项,添加获取工作空间 Cookie 的功能,新增测试用例 2026-02-25 16:41:41 +08:00
e6042e025f feat: 添加buildByConfig2方法以支持新的构建配置,并创建build-log.ts文件 2026-02-25 15:50:06 +08:00
xiongxiao
43b61dc656 chore: 更新依赖项版本,提升兼容性和稳定性 2026-02-20 21:51:44 +08:00
xiongxiao
3ff9e4e374 update 2026-02-20 16:10:51 +08:00
xiongxiao
24ee793db1 up: 2026-02-20 14:58 2026-02-20 14:59:01 +08:00
xiongxiao
d16adc07fe Add test for fetching live metadata from cnb issue 2026-02-20 03:53:05 +08:00
xiongxiao
0ebc94a7d0 简化 cnb 配置,移除 gitea 同步相关配置 2026-02-19 21:26:04 +08:00
47229c6db9 feat: 更新WebSocket Keep-Alive客户端库以支持Bun和Node.js环境,添加统一的消息处理方法,并创建keep.ts文件以初始化KeepAlive 2026-02-15 20:48:45 +08:00
d231f3748a feat: 添加getRepo方法到Repo类以获取仓库信息,并更新相关导入 2026-02-14 18:12:37 +08:00
9bb9f447ec chore: 更新版本号至0.0.24,添加keep模块的外部依赖并更新相关依赖版本 2026-02-14 16:53:30 +08:00
ea137eb70b fix: 更新stopWorkspace方法的返回类型为Result 2026-02-09 19:55:02 +08:00
48 changed files with 1849 additions and 426 deletions

View File

@@ -4,8 +4,7 @@ include:
.common_env: &common_env .common_env: &common_env
env: env:
TO_REPO: kevisual/cnb USERNAME: root
TO_URL: git.xiongxiao.me
imports: imports:
- https://cnb.cool/kevisual/env/-/blob/main/.env.development - https://cnb.cool/kevisual/env/-/blob/main/.env.development
@@ -16,29 +15,6 @@ $:
services: services:
- vscode - vscode
- docker - docker
env: !reference [.common_env, env]
imports: !reference [.common_env, imports] imports: !reference [.common_env, imports]
# 开发环境启动后会执行的任务
# stages:
# - name: pnpm install
# script: pnpm install
stages: !reference [.dev_template, stages] stages: !reference [.dev_template, stages]
.common_sync_to_gitea: &common_sync_to_gitea
- <<: *common_env
services: !reference [.common_sync_to_gitea_template, services]
stages: !reference [.common_sync_to_gitea_template, stages]
.common_sync_from_gitea: &common_sync_from_gitea
- <<: *common_env
services: !reference [.common_sync_from_gitea_template, services]
stages: !reference [.common_sync_from_gitea_template, stages]
main:
web_trigger_sync_to_gitea:
- <<: *common_sync_to_gitea
web_trigger_sync_from_gitea:
- <<: *common_sync_from_gitea
api_trigger_sync_to_gitea:
- <<: *common_sync_to_gitea
api_trigger_sync_from_gitea:
- <<: *common_sync_from_gitea

1
.env.example Normal file
View File

@@ -0,0 +1 @@
CNB_COOKIE=CNBSESSION=1771242023.1935321989751226368.8841cb77d609c050b1a19877644487b6543b587a80953cbdf3018a15b9948b48;csrfkey=309068260

4
.gitignore vendored
View File

@@ -1,5 +1,5 @@
.env .env*
.env.local !.env*example
node_modules node_modules
.pnpm-store .pnpm-store

5
.opencode/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dependencies": {
"@opencode-ai/plugin": "1.2.9"
}
}

4
agent/command.ts Normal file
View File

@@ -0,0 +1,4 @@
import { app } from './index.ts';
import { parse } from '@kevisual/router/src/commander.ts';
parse({ app: app as any, description: 'CNB控制台命令行工具', parse: true })

View File

@@ -1,34 +1,31 @@
import { createSkill } from '@kevisual/router' import { createSkill, tool } from '@kevisual/router'
import { app } from '../../app.ts' import { app } from '../../app.ts'
import { tool } from '@opencode-ai/plugin/tool'
if (!app.hasRoute('call')) { // "调用 path: cnb key: list-repos"
// "调用 path: cnb key: list-repos" app.route({
app.route({ path: 'call',
path: 'call', key: '',
key: '', description: '调用',
description: '调用', middleware: ['auth-admin'],
middleware: ['admin-auth'], metadata: {
metadata: { tags: ['opencode'],
tags: ['opencode'], ...createSkill({
...createSkill({ skill: 'call-app',
skill: 'call-app', title: '调用app应用',
title: '调用app应用', summary: '调用router的应用, 参数path, key, payload',
summary: '调用router的应用, 参数path, key, payload', args: {
args: { path: tool.schema.string().describe('应用路径,例如 cnb'),
path: tool.schema.string().describe('应用路径,例如 cnb'), key: tool.schema.string().optional().describe('应用key,例如 list-repos'),
key: tool.schema.string().optional().describe('应用key例如 list-repos'), payload: tool.schema.object({}).optional().describe('调用参数'),
payload: tool.schema.object({}).optional().describe('调用参数'), }
} })
}) },
}, }).define(async (ctx) => {
}).define(async (ctx) => { const { path, key } = ctx.query;
const { path, key } = ctx.query; console.log('call app', ctx.query);
console.log('call app', ctx.query); if (!path) {
if (!path) { ctx.throw('路径path不能为空');
ctx.throw('路径path不能为空'); }
} const res = await ctx.run({ path, key, payload: ctx.query.payload || {} });
const res = await ctx.run({ path, key, payload: ctx.query.payload || {} }); ctx.forward(res);
ctx.forward(res); }).addTo(app, { overwrite: false })
}).addTo(app)
}

View File

@@ -0,0 +1,424 @@
import { app } from '../../app.ts';
import { useKey } from '@kevisual/context'
import { getLiveMdContent } from './live/live-content.ts';
import z from 'zod';
const notCNBCheck = (ctx: any) => {
const isCNB = useKey('CNB');
if (!isCNB) {
ctx.body = {
title: '非 cnb-board 环境',
list: []
}
return true;
}
return false;
}
app.route({
path: 'cnb_board',
key: 'live',
description: '获取cnb-board live的mdContent内容',
middleware: ['auth-admin'],
metadata: {
args: {
more: z.boolean().optional().describe('是否获取更多系统信息默认false'),
}
}
}).define(async (ctx) => {
const more = ctx.query?.more ?? false
if (notCNBCheck(ctx)) return;
const list = getLiveMdContent({ more: more });
ctx.body = {
title: '开发环境模式配置',
list,
};
}).addTo(app);
app.route({
path: 'cnb_board',
key: 'live_repo_info',
description: '获取cnb-board live的repo信息',
middleware: ['auth-admin']
}).define(async (ctx) => {
const repoSlug = useKey('CNB_REPO_SLUG') || '';
const repoName = useKey('CNB_REPO_NAME') || '';
const repoId = useKey('CNB_REPO_ID') || '';
const repoUrlHttps = useKey('CNB_REPO_UR if (notCNBCheck(ctx)) return;L_HTTPS') || '';
if (notCNBCheck(ctx)) return;
// 从 repoSlug 提取仓库名称
const repoNameFromSlug = repoSlug.split('/').pop() || '';
const labels = [
{
title: 'CNB_REPO_SLUG',
value: repoSlug,
description: '目标仓库路径,格式为 group_slug / repo_namegroup_slug / sub_gourp_slug /.../repo_name'
},
{
title: 'CNB_REPO_SLUG_LOWERCASE',
value: repoSlug.toLowerCase(),
description: '目标仓库路径小写格式'
},
{
title: 'CNB_REPO_NAME',
value: repoName || repoNameFromSlug,
description: '目标仓库名称'
},
{
title: 'CNB_REPO_NAME_LOWERCASE',
value: (repoName || repoNameFromSlug).toLowerCase(),
description: '目标仓库名称小写格式'
},
{
title: 'CNB_REPO_ID',
value: repoId,
description: '目标仓库的 id'
},
{
title: 'CNB_REPO_URL_HTTPS',
value: repoUrlHttps,
description: '目标仓库 https 地址'
}
]
ctx.body = {
title: 'CNB_BOARD_LIVE_REPO_INFO',
list: labels
};
}).addTo(app);
// 构建类变量
app.route({
path: 'cnb_board',
key: 'live_build_info',
description: '获取cnb-board live的构建信息',
middleware: ['auth-admin']
}).define(async (ctx) => {
if (notCNBCheck(ctx)) return;
const labels = [
{
title: 'CNB_BUILD_ID',
value: useKey('CNB_BUILD_ID') || '',
description: '当前构建的流水号,全局唯一'
},
{
title: 'CNB_BUILD_WEB_URL',
value: useKey('CNB_BUILD_WEB_URL') || '',
description: '当前构建的日志地址'
},
{
title: 'CNB_BUILD_START_TIME',
value: useKey('CNB_BUILD_START_TIME') || '',
description: '当前构建的开始时间UTC 格式,示例 2025-08-21T09:13:45.803Z'
},
{
title: 'CNB_BUILD_USER',
value: useKey('CNB_BUILD_USER') || '',
description: '当前构建的触发者用户名'
},
{
title: 'CNB_BUILD_USER_NICKNAME',
value: useKey('CNB_BUILD_USER_NICKNAME') || '',
description: '当前构建的触发者昵称'
},
{
title: 'CNB_BUILD_USER_EMAIL',
value: useKey('CNB_BUILD_USER_EMAIL') || '',
description: '当前构建的触发者邮箱'
},
{
title: 'CNB_BUILD_USER_ID',
value: useKey('CNB_BUILD_USER_ID') || '',
description: '当前构建的触发者 id'
},
{
title: 'CNB_BUILD_USER_NPC_SLUG',
value: useKey('CNB_BUILD_USER_NPC_SLUG') || '',
description: '当前构建若为 NPC 触发,则为 NPC 所属仓库的路径'
},
{
title: 'CNB_BUILD_USER_NPC_NAME',
value: useKey('CNB_BUILD_USER_NPC_NAME') || '',
description: '当前构建若为 NPC 触发,则为 NPC 角色名'
},
{
title: 'CNB_BUILD_STAGE_NAME',
value: useKey('CNB_BUILD_STAGE_NAME') || '',
description: '当前构建的 stage 名称'
},
{
title: 'CNB_BUILD_JOB_NAME',
value: useKey('CNB_BUILD_JOB_NAME') || '',
description: '当前构建的 job 名称'
},
{
title: 'CNB_BUILD_JOB_KEY',
value: useKey('CNB_BUILD_JOB_KEY') || '',
description: '当前构建的 job key同 stage 下唯一'
},
{
title: 'CNB_BUILD_WORKSPACE',
value: useKey('CNB_BUILD_WORKSPACE') || '',
description: '自定义 shell 脚本执行的工作空间根目录'
},
{
title: 'CNB_BUILD_FAILED_MSG',
value: useKey('CNB_BUILD_FAILED_MSG') || '',
description: '流水线构建失败的错误信息,可在 failStages 中使用'
},
{
title: 'CNB_BUILD_FAILED_STAGE_NAME',
value: useKey('CNB_BUILD_FAILED_STAGE_NAME') || '',
description: '流水线构建失败的 stage 的名称,可在 failStages 中使用'
},
{
title: 'CNB_PIPELINE_NAME',
value: useKey('CNB_PIPELINE_NAME') || '',
description: '当前 pipeline 的 name没声明时为空'
},
{
title: 'CNB_PIPELINE_KEY',
value: useKey('CNB_PIPELINE_KEY') || '',
description: '当前 pipeline 的索引 key例如 pipeline-0'
},
{
title: 'CNB_PIPELINE_ID',
value: useKey('CNB_PIPELINE_ID') || '',
description: '当前 pipeline 的 id全局唯一字符串'
},
{
title: 'CNB_PIPELINE_DOCKER_IMAGE',
value: useKey('CNB_PIPELINE_DOCKER_IMAGE') || '',
description: '当前 pipeline 所使用的 docker imagealpine:latest'
},
{
title: 'CNB_PIPELINE_STATUS',
value: useKey('CNB_PIPELINE_STATUS') || '',
description: '当前流水线的构建状态,可在 endStages 中查看其可能的值包括success、error、cancel'
},
{
title: 'CNB_PIPELINE_MAX_RUN_TIME',
value: useKey('CNB_PIPELINE_MAX_RUN_TIME') || '',
description: '流水线最大运行时间,单位为毫秒'
},
{
title: 'CNB_RUNNER_IP',
value: useKey('CNB_RUNNER_IP') || '',
description: '当前 pipeline 所在 Runner 的 ip'
},
{
title: 'CNB_CPUS',
value: useKey('CNB_CPUS') || '',
description: '当前构建流水线可以使用的最大 CPU 核数'
},
{
title: 'CNB_MEMORY',
value: useKey('CNB_MEMORY') || '',
description: '当前构建流水线可以使用的最大内存大小,单位为 GiB'
},
{
title: 'CNB_IS_RETRY',
value: useKey('CNB_IS_RETRY') || '',
description: '当前构建是否由 rebuild 触发'
},
{
title: 'HUSKY_SKIP_INSTALL',
value: useKey('HUSKY_SKIP_INSTALL') || '',
description: '兼容 ci 环境下 husky'
}
]
ctx.body = {
title: 'CNB_BOARD_LIVE_BUILD_INFO',
list: labels
};
}).addTo(app);
// PR/合并类变量
app.route({
path: 'cnb_board',
key: 'live_pull_info',
description: '获取cnb-board live的PR信息',
middleware: ['auth-admin']
}).define(async (ctx) => {
const labels = [
{
title: 'CNB_PULL_REQUEST',
value: useKey('CNB_PULL_REQUEST') || '',
description: '对于由 pull_request、pull_request.update、pull_request.target 触发的构建,值为 true否则为 false'
},
{
title: 'CNB_PULL_REQUEST_LIKE',
value: useKey('CNB_PULL_REQUEST_LIKE') || '',
description: '对于由 合并类事件 触发的构建,值为 true否则为 false'
},
{
title: 'CNB_PULL_REQUEST_PROPOSER',
value: useKey('CNB_PULL_REQUEST_PROPOSER') || '',
description: '对于由 合并类事件 触发的构建,值为提出 PR 者名称,否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_TITLE',
value: useKey('CNB_PULL_REQUEST_TITLE') || '',
description: '对于由 合并类事件 触发的构建,值为提 PR 时候填写的标题,否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_BRANCH',
value: useKey('CNB_PULL_REQUEST_BRANCH') || '',
description: '对于由 合并类事件 触发的构建,值为发起 PR 的源分支名称,否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_SHA',
value: useKey('CNB_PULL_REQUEST_SHA') || '',
description: '对于由 合并类事件 触发的构建,值为当前 PR 源分支最新的提交 sha否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_TARGET_SHA',
value: useKey('CNB_PULL_REQUEST_TARGET_SHA') || '',
description: '对于由 合并类事件 触发的构建,值为当前 PR 目标分支最新的提交 sha否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_MERGE_SHA',
value: useKey('CNB_PULL_REQUEST_MERGE_SHA') || '',
description: '对于由 pull_request.merged 触发的构建,值为合并后的 sha对于 pull_request 等触发的构建,值为预合并后的 sha否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_SLUG',
value: useKey('CNB_PULL_REQUEST_SLUG') || '',
description: '对于由 合并类事件 触发的构建,值为源仓库的仓库 slug如 group_slug/repo_name否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_ACTION',
value: useKey('CNB_PULL_REQUEST_ACTION') || '',
description: '对于由 合并类事件 触发的构建可能的值有created(新建PR)、code_update(源分支push)、status_update(评审通过或CI状态变更),否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_ID',
value: useKey('CNB_PULL_REQUEST_ID') || '',
description: '对于由 合并类事件 触发的构建,值为当前或者关联 PR 的全局唯一 id否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_IID',
value: useKey('CNB_PULL_REQUEST_IID') || '',
description: '对于由 合并类事件 触发的构建,值为当前或者关联 PR 在仓库中的编号 iid否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_REVIEWERS',
value: useKey('CNB_PULL_REQUEST_REVIEWERS') || '',
description: '对于由 合并类事件 触发的构建,值为评审人列表,多个以 , 分隔,否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_REVIEW_STATE',
value: useKey('CNB_PULL_REQUEST_REVIEW_STATE') || '',
description: '对于由 合并类事件 触发的构建,有评审者且有人通过评审为 approve有评审者但无人通过评审为 unapprove否则为空字符串'
},
{
title: 'CNB_REVIEW_REVIEWED_BY',
value: useKey('CNB_REVIEW_REVIEWED_BY') || '',
description: '对于由 合并类事件 触发的构建,值为同意评审的评审人列表,多个以 , 分隔,否则为空字符串'
},
{
title: 'CNB_REVIEW_LAST_REVIEWED_BY',
value: useKey('CNB_REVIEW_LAST_REVIEWED_BY') || '',
description: '对于由 合并类事件 触发的构建,值为最后一个同意评审的评审人,否则为空字符串'
},
{
title: 'CNB_PULL_REQUEST_IS_WIP',
value: useKey('CNB_PULL_REQUEST_IS_WIP') || '',
description: '对于由 合并类事件 触发的构建,值为 true、false表示 PR 是否被设置为 [WIP],否则为空字符串'
}
]
ctx.body = {
title: 'CNB_BOARD_LIVE_PULL_INFO',
list: labels
};
}).addTo(app);
// NPC 类变量
app.route({
path: 'cnb_board',
key: 'live_npc_info',
description: '获取cnb-board live的NPC信息',
middleware: ['auth-admin']
}).define(async (ctx) => {
if (notCNBCheck(ctx)) return;
const labels = [
{
title: 'CNB_NPC_SLUG',
value: useKey('CNB_NPC_SLUG') || '',
description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库路径,否则为空字符串'
},
{
title: 'CNB_NPC_NAME',
value: useKey('CNB_NPC_NAME') || '',
description: '对于 NPC 事件触发的构建,值为 NPC 角色名,否则为空字符串'
},
{
title: 'CNB_NPC_SHA',
value: useKey('CNB_NPC_SHA') || '',
description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库默认分支最新提交的 sha否则为空字符串'
},
{
title: 'CNB_NPC_PROMPT',
value: useKey('CNB_NPC_PROMPT') || '',
description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色 Prompt否则为空字符串'
},
{
title: 'CNB_NPC_AVATAR',
value: useKey('CNB_NPC_AVATAR') || '',
description: '对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色头像,否则为空字符串'
},
{
title: 'CNB_NPC_ENABLE_THINKING',
value: useKey('CNB_NPC_ENABLE_THINKING') || '',
description: '对于 @npc 事件触发的构建,值为 NPC 角色是否开启思考,否则为空字符串'
}
]
ctx.body = {
title: 'CNB_BOARD_LIVE_NPC_INFO',
list: labels
};
}).addTo(app);
// 评论类变量
app.route({
path: 'cnb_board',
key: 'live_comment_info',
description: '获取cnb-board live的评论信息',
middleware: ['auth-admin']
}).define(async (ctx) => {
if (notCNBCheck(ctx)) return;
const labels = [
{
title: 'CNB_COMMENT_ID',
value: useKey('CNB_COMMENT_ID') || '',
description: '对于评论事件触发的构建,值为评论全局唯一 ID否则为空字符串'
},
{
title: 'CNB_COMMENT_BODY',
value: useKey('CNB_COMMENT_BODY') || '',
description: '对于评论事件触发的构建,值为评论内容,否则为空字符串'
},
{
title: 'CNB_COMMENT_TYPE',
value: useKey('CNB_COMMENT_TYPE') || '',
description: '对于 PR 代码评审评论,值为 diff_note对于 PR 非代码评审评论以及 Issue 评论,值为 note否则为空字符串'
},
{
title: 'CNB_COMMENT_FILE_PATH',
value: useKey('CNB_COMMENT_FILE_PATH') || '',
description: '对于 PR 代码评审评论,值为评论所在文件,否则为空字符串'
},
{
title: 'CNB_COMMENT_RANGE',
value: useKey('CNB_COMMENT_RANGE') || '',
description: '对于 PR 代码评审评论,值为评论所在代码行。如,单行为 L12多行为 L13-L16否则为空字符串'
},
{
title: 'CNB_REVIEW_ID',
value: useKey('CNB_REVIEW_ID') || '',
description: '对于 PR 代码评审,值为评审 ID否则为空字符串'
}
]
ctx.body = {
title: 'CNB_BOARD_LIVE_COMMENT_INFO',
list: labels
};
}).addTo(app);

View File

View File

@@ -0,0 +1,38 @@
import { app } from '../../app.ts';
import './cnb-dev-env.ts';
import { useKey } from '@kevisual/context';
import { spawnSync } from 'node:child_process';
export const execCommand = (command: string, options: { cwd?: string } = {}) => {
const { cwd } = options;
return spawnSync(command, {
stdio: 'inherit',
shell: true,
cwd: cwd,
env: process.env,
});
};
app.route({
path: 'cnb_board',
key: 'is-cnb-board',
description: '检查是否是 cnb-board 环境',
middleware: ['auth-admin']
}).define(async (ctx) => {
const isCNB = useKey('CNB');
ctx.body = {
isCNB: !!isCNB,
};
}).addTo(app);
app.route({
path: 'cnb_board',
key: 'exit',
description: 'cnb的工作环境退出程序',
middleware: ['auth-admin'],
}).define(async (ctx) => {
const cmd = 'kill 1';
execCommand(cmd);
}).addTo(app);

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
}

View File

@@ -0,0 +1 @@
export * from './is-cnb.ts';

View File

@@ -0,0 +1,6 @@
import { useKey } from "@kevisual/context";
export const isCnb = () => {
const CNB = useKey('CNB');
return !!CNB;
}

View File

@@ -7,7 +7,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'user-check', key: 'user-check',
description: '检查用户登录状态参数checkToken,default true; checkCookie, default false', description: '检查用户登录状态参数checkToken,default true; checkCookie, default false',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({

View File

@@ -6,7 +6,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'set-cnb-cookie', key: 'set-cnb-cookie',
description: '设置当前cnb工作空间的cookie环境变量', description: '设置当前cnb工作空间的cookie环境变量',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -33,7 +33,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'get-cnb-cookie', key: 'get-cnb-cookie',
description: '获取当前cnb工作空间的cookie环境变量', description: '获取当前cnb工作空间的cookie环境变量',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({

View File

@@ -11,7 +11,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'get-cnb-port-uri', key: 'get-cnb-port-uri',
description: '获取当前cnb工作空间的port代理uri', description: '获取当前cnb工作空间的port代理uri',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -19,12 +19,12 @@ app.route({
title: '获取当前cnb工作空间的port代理uri', title: '获取当前cnb工作空间的port代理uri',
summary: '获取当前cnb工作空间的port代理uri用于端口转发', summary: '获取当前cnb工作空间的port代理uri用于端口转发',
args: { args: {
port: tool.schema.number().optional().describe('端口号,默认为4096'), port: tool.schema.number().optional().describe('端口号,默认为51515'),
} }
}) })
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const port = ctx.query?.port || 4096; const port = ctx.query?.port || 51515;
const uri = CNB_ENV?.CNB_VSCODE_PROXY_URI as string || ''; const uri = CNB_ENV?.CNB_VSCODE_PROXY_URI as string || '';
const finalUri = uri.replace('{{port}}', port.toString()); const finalUri = uri.replace('{{port}}', port.toString());
let content = ` let content = `
@@ -40,7 +40,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'get-cnb-vscode-uri', key: 'get-cnb-vscode-uri',
description: '获取当前cnb工作空间的vscode代理uri, 包括多种访问方式, 如web、vscode、codebuddy、cursor、ssh', description: '获取当前cnb工作空间的vscode代理uri, 包括多种访问方式, 如web、vscode、codebuddy、cursor、ssh',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({

View File

@@ -6,7 +6,8 @@ import './call/index.ts'
import './cnb-env/index.ts' import './cnb-env/index.ts'
import './knowledge/index.ts' import './knowledge/index.ts'
import './issues/index.ts' import './issues/index.ts'
import './cnb-board/index.ts';
import './share/index.ts';
/** /**
* 验证上下文中的 App ID 是否与指定的 App ID 匹配 * 验证上下文中的 App ID 是否与指定的 App ID 匹配
* @param {any} ctx - 上下文对象,可能包含 appId 属性 * @param {any} ctx - 上下文对象,可能包含 appId 属性
@@ -25,25 +26,23 @@ const checkAppId = (ctx: any, appId: string) => {
return false; return false;
} }
if (!app.hasRoute('auth')) { app.route({
app.route({ id: 'auth',
id: 'auth', path: 'auth',
path: 'auth', }).define(async (ctx) => {
}).define(async (ctx) => { // ctx.body = 'Auth Route';
// ctx.body = 'Auth Route'; if (checkAppId(ctx, app.appId)) {
if (checkAppId(ctx, app.appId)) { return;
return; }
} }).addTo(app, { overwrite: false });
}).addTo(app);
app.route({ app.route({
id: 'admin-auth', id: 'auth-admin',
path: 'admin-auth', path: 'auth-admin',
middleware: ['auth'], middleware: ['auth'],
}).define(async (ctx) => { }).define(async (ctx) => {
// ctx.body = 'Admin Auth Route'; // ctx.body = 'Admin Auth Route';
if (checkAppId(ctx, app.appId)) { if (checkAppId(ctx, app.appId)) {
return; return;
} }
}).addTo(app); }).addTo(app, { overwrite: false });
}

View File

@@ -7,7 +7,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'create-issue', key: 'create-issue',
description: '创建 Issue, 参数 repo, title, body, assignees, labels, priority', description: '创建 Issue, 参数 repo, title, body, assignees, labels, priority',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -51,7 +51,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'complete-issue', key: 'complete-issue',
description: '完成 Issue, 参数 repo, issueNumber', description: '完成 Issue, 参数 repo, issueNumber',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({

View File

@@ -1,12 +1,13 @@
import { createSkill, tool } from '@kevisual/router'; import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts'; import { app, cnb } from '../../app.ts';
import { useKey } from '@kevisual/context';
// 查询 Issue 列表 repo是 kevisual/kevisual // 查询 Issue 列表 repo是 kevisual/kevisual
app.route({ app.route({
path: 'cnb', path: 'cnb',
key: 'list-issues', key: 'list-issues',
description: '查询 Issue 列表, 参数 repo, state, keyword, labels, page, page_size 等', description: '查询 Issue 列表, 参数 repo, state, keyword, labels, page, page_size 等',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -25,7 +26,7 @@ app.route({
}) })
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const repo = ctx.query?.repo; const repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
const state = ctx.query?.state; const state = ctx.query?.state;
const keyword = ctx.query?.keyword; const keyword = ctx.query?.keyword;
const labels = ctx.query?.labels; const labels = ctx.query?.labels;

View File

@@ -1,6 +1,7 @@
import { createSkill, tool } from '@kevisual/router'; import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts'; import { app, cnb } from '../../app.ts';
import { CNBChat } from '@kevisual/ai/browser' import { CNBChat } from '@kevisual/ai/browser'
import { useKey } from '@kevisual/context';
/** /**
@@ -12,7 +13,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'cnb-ai-chat', key: 'cnb-ai-chat',
description: '调用cnb的知识库ai对话功能进行聊天', description: '调用cnb的知识库ai对话功能进行聊天',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -88,7 +89,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'cnb-rag-query', key: 'cnb-rag-query',
description: '调用cnb的知识库RAG查询功能进行问答', description: '调用cnb的知识库RAG查询功能进行问答',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -107,7 +108,7 @@ app.route({
ctx.body = { content: '请提供有效的消息内容' }; ctx.body = { content: '请提供有效的消息内容' };
return; return;
} }
let repo = ctx.query?.repo; let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
if (!repo) { if (!repo) {
// 如果未指定知识库仓库ID则使用默认知识库 // 如果未指定知识库仓库ID则使用默认知识库
const res = await cnb.repo.getRepoList({ flags: 'KnowledgeBase' }); const res = await cnb.repo.getRepoList({ flags: 'KnowledgeBase' });

View File

@@ -1,6 +1,5 @@
import { createSkill } from '@kevisual/router'; import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts'; import { app, cnb } from '../../app.ts';
import { tool } from "@opencode-ai/plugin/tool"
// "列出我的代码仓库search blog" // "列出我的代码仓库search blog"
// 列出我的知识库的代码仓库 // 列出我的知识库的代码仓库
@@ -8,7 +7,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'list-repos', key: 'list-repos',
description: '列出我的代码仓库', description: '列出我的代码仓库',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({

View File

@@ -1,13 +1,12 @@
import { app, cnb } from '../../app.ts'; import { app, cnb } from '../../app.ts';
import { createSkill, Skill } from '@kevisual/router' import { createSkill, Skill, tool } from '@kevisual/router'
import { tool } from "@opencode-ai/plugin/tool"
// 创建一个仓库 kevisual/test-repo // 创建一个仓库 kevisual/test-repo
app.route({ app.route({
path: 'cnb', path: 'cnb',
key: 'create-repo', key: 'create-repo',
description: '创建代码仓库, 参数name, visibility, description', description: '创建代码仓库, 参数name, visibility, description',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -46,8 +45,8 @@ app.route({
app.route({ app.route({
path: 'cnb', path: 'cnb',
key: 'create-repo-file', key: 'create-repo-file',
description: '在代码仓库中创建文件, repoName, filePath, content, encoding', description: '在代码仓库中创建文件, repoName, filePath, content, encoding。使用CNB_COOKIE进行鉴权',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -86,7 +85,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'delete-repo', key: 'delete-repo',
description: '删除代码仓库, 参数name', description: '删除代码仓库, 参数name',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({

View File

@@ -0,0 +1,48 @@
import { useKey } from '@kevisual/context';
import { app, cnb } from '../../app.ts';
import z from 'zod';
app.route({
path: 'cnb',
key: 'get-assistant-url',
description: '获取cnb工作空间中部署的各个助手的访问地址',
middleware: ['auth'],
metadata: {
args: {
more: z.boolean().describe('需要更多信息')
}
}
}).define(async (ctx) => {
const uri = useKey('CNB_VSCODE_PROXY_URI') as string || '';
const base = {
base: uri,
link: uri.replace('{{port}}', '51515'),
kevisual: uri.replace('{{port}}', '51515'),
openclaw: uri.replace('{{port}}', '80'),
opencode: uri.replace('{{port}}', '100'),
openwebui: uri.replace('{{port}}', '200'),
note: uri.replace('{{port}}', '3000'),
uptime: uri.replace('{{port}}', '3001'),
immich: uri.replace('{{port}}', '2283'),
nocodb: uri.replace('{{port}}', '4000'),
openlist: uri.replace('{{port}}', '5244'),
xiaoyao: uri.replace('{{port}}', '5678'),
meilisearch: uri.replace('{{port}}', '7700'),
bark: uri.replace('{{port}}', '9111'),
vaultwarden: uri.replace('{{port}}', '8180'),
music: uri.replace('{{port}}', '8096'),
jellyfin: uri.replace('{{port}}', '8096'),
homeassistant: uri.replace('{{port}}', '8123'),
cloudreve: uri.replace('{{port}}', '5212'),
filebrowser: uri.replace('{{port}}', '8081'),
// newapi: uri.replace('{{port}}', '8080'),
vscode: useKey('CNB_VSCODE_WEB_URL') as string || '',
codeServer: uri.replace('{{port}}', '10000'),
gitea: uri.replace('{{port}}', '3000'),
calibre: uri.replace('{{port}}', '8083'),
searXNG: uri.replace('{{port}}', '8888'),
}
ctx.body = {
...base,
}
}).addTo(app);

View File

@@ -9,7 +9,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'start-workspace', key: 'start-workspace',
description: '启动开发工作空间, 参数 repo', description: '启动开发工作空间, 参数 repo',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -42,7 +42,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'list-workspace', key: 'list-workspace',
description: '获取cnb开发工作空间列表可选参数 status=running 获取运行中的环境', description: '获取cnb开发工作空间列表可选参数 status=running 获取运行中的环境',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -73,7 +73,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'get-workspace', key: 'get-workspace',
description: '获取工作空间详情,通过 repo 和 sn 获取', description: '获取工作空间详情,通过 repo 和 sn 获取',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -104,7 +104,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'delete-workspace', key: 'delete-workspace',
description: '删除工作空间,通过 pipelineId 或 sn', description: '删除工作空间,通过 pipelineId 或 sn',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -143,7 +143,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'stop-workspace', key: 'stop-workspace',
description: '停止工作空间,通过 pipelineId 或 sn', description: '停止工作空间,通过 pipelineId 或 sn',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({

View File

@@ -1,214 +1,97 @@
import { createSkill, tool } from '@kevisual/router'; import { tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts'; import { app, cnb } from '../../app.ts';
import { nanoid } from 'nanoid'; import { addKeepAliveData, KeepAliveData, removeKeepAliveData, createLiveData } from '../../../src/workspace/keep-file-live.ts';
import dayjs from 'dayjs'; import { useKey } from '@kevisual/context';
import { createKeepAlive } from '../../../src/keep.ts';
type AliveInfo = {
startTime: number;
updatedTime?: number;
KeepAlive: ReturnType<typeof createKeepAlive>;
id: string;// 6位唯一标识符
}
const keepAliveMap = new Map<string, AliveInfo>();
// 保持工作空间存活技能 // 保持工作空间存活技能
app.route({ app.route({
path: 'cnb', path: 'cnb',
key: 'keep-workspace-alive', key: 'keep-workspace-alive',
description: '保持工作空间存活技能,参数wsUrl:工作空间访问URLcookie:访问工作空间所需的cookie', description: '保持工作空间存活技能参数repo:代码仓库路径,例如 user/repopipelineId:流水线ID例如 cnb-708-1ji9sog7o-001',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: [], tags: [],
...({ ...({
args: { args: {
wsUrl: tool.schema.string().describe('工作空间的访问URL'), repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
cookie: tool.schema.string().describe('访问工作空间所需的cookie') pipelineId: tool.schema.string().describe('流水线ID例如 cnb-708-1ji9sog7o-001'),
} }
}) })
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const wsUrl = ctx.query?.wsUrl as string; const repo = ctx.query?.repo as string;
const cookie = ctx.query?.cookie as string; const pipelineId = ctx.query?.pipelineId as string;
if (!wsUrl) {
ctx.throw(400, '缺少工作空间访问URL参数'); if (!repo || !pipelineId) {
ctx.throw(400, '缺少参数 repo 或 pipelineId');
} }
if (!cookie) { const validCookie = await cnb.user.checkCookieValid()
ctx.throw(400, '缺少访问工作空间所需的cookie参数'); if (validCookie.code !== 200) {
ctx.throw(401, 'CNB_COOKIE 环境变量无效或已过期请重新登录获取新的cookie');
}
const res = await cnb.workspace.getWorkspaceCookie(repo, pipelineId);
if (res.code !== 200 || !res.data?.cookie) {
ctx.throw(500, `获取工作空间 Cookie 失败: ${res.message}`);
} }
// 检测是否已在运行(通过 wsUrl 遍历检查) // 添加保活数据
const existing = Array.from(keepAliveMap.values()).find(info => (info as AliveInfo).id && (info as any).KeepAlive?.wsUrl === wsUrl); const liveData = createLiveData({
if (existing) { repo,
ctx.body = { message: `工作空间 ${wsUrl} 的保持存活任务已在运行中`, id: (existing as AliveInfo).id }; pipelineId,
return; cookie: res.data.cookie
}
console.log(`启动保持工作空间 ${wsUrl} 存活的任务`);
const keep = createKeepAlive({
wsUrl,
cookie,
onConnect: () => {
console.log(`工作空间 ${wsUrl} 保持存活任务已连接`);
},
onMessage: (data) => {
// 可选:处理收到的消息
// console.log(`工作空间 ${wsUrl} 收到消息: ${data}`);
// 通过 wsUrl 找到对应的 id 并更新时间
for (const info of keepAliveMap.values()) {
if ((info as any).KeepAlive?.wsUrl === wsUrl) {
info.updatedTime = Date.now();
break;
}
}
},
debug: true,
onExit: (code) => {
console.log(`工作空间 ${wsUrl} 保持存活任务已退出,退出码: ${code}`);
// 通过 wsUrl 找到对应的 id 并删除
for (const [id, info] of keepAliveMap.entries()) {
if ((info as any).KeepAlive?.wsUrl === wsUrl) {
keepAliveMap.delete(id);
break;
}
}
}
}); });
addKeepAliveData(liveData);
console.log('已添加 keep-alive 数据');
const id = nanoid(6).toLowerCase(); ctx.body = { content: `已启动保持工作空间 ${repo}/${pipelineId} 存活的任务`, data: liveData };
keepAliveMap.set(id, { startTime: Date.now(), updatedTime: Date.now(), KeepAlive: keep, id });
ctx.body = { content: `已启动保持工作空间 ${wsUrl} 存活的任务`, id };
}).addTo(app);
// 获取保持工作空间存活任务列表技能
app.route({
path: 'cnb',
key: 'list-keep-alive-tasks',
description: '获取保持工作空间存活任务列表技能',
middleware: ['admin-auth'],
metadata: {
tags: [],
}
}).define(async (ctx) => {
const list = Array.from(keepAliveMap.entries()).map(([id, info]) => {
const now = Date.now();
const duration = Math.floor((now - info.startTime) / 60000); // 分钟
return {
id,
wsUrl: (info as any).KeepAlive?.wsUrl,
startTime: info.startTime,
startTimeStr: dayjs(info.startTime).format('YYYY-MM-DD HH:mm'),
updatedTime: info.updatedTime,
updatedTimeStr: dayjs(info.updatedTime).format('YYYY-MM-DD HH:mm'),
duration,
}
});
ctx.body = { list };
}).addTo(app); }).addTo(app);
// 停止保持工作空间存活技能 // 停止保持工作空间存活技能
app.route({ app.route({
path: 'cnb', path: 'cnb',
key: 'stop-keep-workspace-alive', key: 'stop-keep-workspace-alive',
description: '停止保持工作空间存活技能, 参数wsUrl:工作空间访问URL或者id', description: '停止保持工作空间存活技能, 参数repo:代码仓库路径,例如 user/repopipelineId:流水线ID例如 cnb-708-1ji9sog7o-001',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: [], tags: [],
...({ ...({
args: { args: {
wsUrl: tool.schema.string().optional().describe('工作空间的访问URL'), repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
id: tool.schema.string().optional().describe('保持存活任务的唯一标识符'), pipelineId: tool.schema.string().describe('流水线ID例如 cnb-708-1ji9sog7o-001'),
} }
}) })
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const wsUrl = ctx.query?.wsUrl as string; const repo = ctx.query?.repo as string;
const id = ctx.query?.id as string; const pipelineId = ctx.query?.pipelineId as string;
if (!wsUrl && !id) {
ctx.throw(400, '缺少工作空间访问URL参数或唯一标识符');
}
let targetId: string | undefined; if (!repo || !pipelineId) {
let wsUrlFound: string | undefined; ctx.throw(400, '缺少参数 repo 或 pipelineId');
if (id) {
const info = keepAliveMap.get(id);
if (info) {
targetId = id;
wsUrlFound = (info as any).KeepAlive?.wsUrl;
}
} else if (wsUrl) {
for (const [key, info] of keepAliveMap.entries()) {
if ((info as any).KeepAlive?.wsUrl === wsUrl) {
targetId = key;
wsUrlFound = wsUrl;
break;
}
}
}
if (targetId) {
const keepAlive = keepAliveMap.get(targetId);
const endTime = Date.now();
const duration = endTime - keepAlive!.startTime;
keepAlive?.KeepAlive?.disconnect();
keepAliveMap.delete(targetId);
ctx.body = { content: `已停止保持工作空间 ${wsUrlFound} 存活的任务,持续时间: ${duration}ms`, id: targetId };
} else {
ctx.body = { content: `没有找到对应的工作空间保持存活任务` };
} }
removeKeepAliveData(repo, pipelineId);
ctx.body = { content: `已停止保持工作空间 ${repo}/${pipelineId} 存活的任务` };
}).addTo(app); }).addTo(app);
app.route({ app.route({
path: 'cnb', path: 'cnb',
key: 'reset-keep-workspace-alive', key: 'keep-alive-current-workspace',
description: '对存活的工作空间startTime进行重置', description: '保持当前工作空间存活技能',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: [], tags: ['opencode'],
skill: 'keep-alive-current-workspace',
title: '保持当前工作空间存活',
summary: '保持当前工作空间存活,防止被关闭或释放资源',
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const now = Date.now(); const pipelineId = useKey('CNB_PIPELINE_ID');
for (const info of keepAliveMap.values()) { const repo = useKey('CNB_REPO_SLUG_LOWERCASE');
info.startTime = now; if (!pipelineId || !repo) {
ctx.throw(400, '当前环境缺少 CNB_PIPELINE_ID 或 CNB_REPO_SLUG_LOWERCASE 环境变量,无法保持工作空间存活');
} }
ctx.body = { content: `已重置所有存活工作空间的开始时间` }; const res = await app.run({ path: 'cnb', key: 'keep-workspace-alive', payload: { repo, pipelineId } }, ctx);
}).addTo(app); ctx.forward(res);
}).addTo(app);
app.route({
path: 'cnb',
key: 'clear-keep-workspace-alive',
description: '对存活的工作空间超过5小时的进行清理',
middleware: ['admin-auth'],
metadata: {
tags: [],
}
}).define(async (ctx) => {
const res = clearKeepAlive();
ctx.body = {
content: `已清理所有存活工作空间中超过5小时的任务` + (res.length ? `,清理项:${res.map(i => i.wsUrl).join(', ')}` : ''),
list: res
};
}).addTo(app);
const clearKeepAlive = () => {
const now = Date.now();
let clearedArr: { id: string; wsUrl: string }[] = [];
for (const [id, info] of keepAliveMap.entries()) {
if (now - info.startTime > FIVE_HOURS) {
console.log(`工作空间 ${(info as any).KeepAlive?.wsUrl} 超过5小时自动停止`);
info.KeepAlive?.disconnect?.();
keepAliveMap.delete(id);
clearedArr.push({ id, wsUrl: (info as any).KeepAlive?.wsUrl });
}
}
return clearedArr;
}
// 每5小时自动清理超时的keepAlive任务
const FIVE_HOURS = 5 * 60 * 60 * 1000;
setInterval(() => {
clearKeepAlive();
}, FIVE_HOURS);

View File

@@ -35,7 +35,7 @@ app.route({
path: 'cnb', path: 'cnb',
key: 'clean-closed-workspace', key: 'clean-closed-workspace',
description: '批量删除已停止的cnb工作空间', description: '批量删除已停止的cnb工作空间',
middleware: ['admin-auth'], middleware: ['auth-admin'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
...createSkill({ ...createSkill({
@@ -45,7 +45,7 @@ app.route({
}) })
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const closedWorkspaces = await cnb.workspace.list({ status: 'closed' }); const closedWorkspaces = await cnb.workspace.list({ status: 'closed', pageSize: 100 });
if (closedWorkspaces.code !== 200) { if (closedWorkspaces.code !== 200) {
ctx.throw(500, '获取已关闭工作空间列表失败'); ctx.throw(500, '获取已关闭工作空间列表失败');
} }

2
bin/index.js Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bun
import '../dist/cli.js';

View File

@@ -1,4 +1,6 @@
import { buildWithBun } from '@kevisual/code-builder' import { buildWithBun } from '@kevisual/code-builder'
await buildWithBun({ naming: 'opencode', entry: 'agent/opencode.ts', dts: true }); await buildWithBun({ naming: 'opencode', entry: 'agent/opencode.ts', dts: true });
await buildWithBun({ naming: 'keep', entry: 'src/keep.ts', dts: true }); await buildWithBun({ naming: 'keep', entry: 'src/keep.ts', dts: true, target: 'node' });
await buildWithBun({ naming: 'routes', entry: 'agent/index.ts', dts: true }); await buildWithBun({ naming: 'routes', entry: 'agent/index.ts', dts: true });
await buildWithBun({ naming: 'cli', entry: 'agent/command.ts', dts: true, target: 'node' });

236
bun.lock Normal file
View File

@@ -0,0 +1,236 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "@kevisual/cnb",
"dependencies": {
"@kevisual/query": "^0.0.49",
"@kevisual/router": "^0.0.80",
"@kevisual/use-config": "^1.0.30",
"es-toolkit": "^1.44.0",
"nanoid": "^5.1.6",
"unstorage": "^1.17.4",
"ws": "npm:@kevisual/ws",
"zod": "^4.3.6",
},
"devDependencies": {
"@kevisual/ai": "^0.0.24",
"@kevisual/code-builder": "^0.0.6",
"@kevisual/context": "^0.0.8",
"@kevisual/dts": "^0.0.4",
"@kevisual/types": "^0.0.12",
"@opencode-ai/plugin": "^1.2.10",
"@types/bun": "^1.3.9",
"@types/node": "^25.3.0",
"@types/ws": "^8.18.1",
"dayjs": "^1.11.19",
"dotenv": "^17.3.1",
},
},
},
"overrides": {
"zod": "^4.3.6",
},
"packages": {
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@kevisual/ai": ["@kevisual/ai@0.0.24", "", { "dependencies": { "@kevisual/logger": "^0.0.4", "@kevisual/permission": "^0.0.3", "@kevisual/query": "^0.0.38" } }, "sha512-7jvZk1/L//VIClK7usuNgN4ZA9Etgbooka1Sj5quE/0UywR+NNnwqXVZ89Y1fBhI1TkhauDsdJBAtcQ7r/vbVw=="],
"@kevisual/code-builder": ["@kevisual/code-builder@0.0.6", "", { "bin": { "code-builder": "bin/code.js", "builder": "bin/code.js" } }, "sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw=="],
"@kevisual/context": ["@kevisual/context@0.0.8", "", {}, "sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA=="],
"@kevisual/dts": ["@kevisual/dts@0.0.4", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-typescript": "^12.3.0", "rollup": "^4.57.1", "rollup-plugin-dts": "^6.3.0", "tslib": "^2.8.1" }, "bin": { "dts": "bin/dts.mjs" } }, "sha512-FVUaH/0nyhbHWpEVjFTGP54PLMm4Hf06aqWLdHOYHNPIgr1aK1C26kOH7iumklGFGk9w93IGxj8Zxe5fap5N2A=="],
"@kevisual/load": ["@kevisual/load@0.0.6", "", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA=="],
"@kevisual/logger": ["@kevisual/logger@0.0.4", "", {}, "sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw=="],
"@kevisual/permission": ["@kevisual/permission@0.0.3", "", {}, "sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA=="],
"@kevisual/query": ["@kevisual/query@0.0.49", "", {}, "sha512-GrWW+QlBO5lkiqvb7PjOstNtpTQVSR74EHHWjm7YoL9UdT1wuPQXGUApZHmMBSh3NIWCf0AL2G1hPWZMC7YeOQ=="],
"@kevisual/router": ["@kevisual/router@0.0.80", "", { "dependencies": { "es-toolkit": "^1.44.0" } }, "sha512-rVwi6Yf411bnNm2x94lMm+s4Csw0Yb7u/aj+VJJ59iouAYhjLuL7Rs1EcARhnQf47cegBJi6zozfGHgLsLHN2w=="],
"@kevisual/types": ["@kevisual/types@0.0.12", "", {}, "sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q=="],
"@kevisual/use-config": ["@kevisual/use-config@1.0.30", "", { "dependencies": { "@kevisual/load": "^0.0.6" }, "peerDependencies": { "dotenv": "^17" } }, "sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw=="],
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.10", "", { "dependencies": { "@opencode-ai/sdk": "1.2.10", "zod": "4.1.8" } }, "sha512-Z1BMqNHnD8AGAEb+kUz0b2SOuiODwdQLdCA4aVGTXqkGzhiD44OVxr85MeoJ5AMTnnea9SnJ3jp9GAQ5riXA5g=="],
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.10", "", {}, "sha512-SyXcVqry2hitPVvQtvXOhqsWyFhSycG/+LTLYXrcq8AFmd9FR7dyBSDB3f5Ol6IPkYOegk8P2Eg2kKPNSNiKGw=="],
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ=="],
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
"@rollup/plugin-typescript": ["@rollup/plugin-typescript@12.3.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-7DP0/p7y3t67+NabT9f8oTBFE6gGkto4SA6Np2oudYmZE/m1dt8RB0SjL1msMxFpLo631qjRCcBlAbq1ml/Big=="],
"@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
"cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
"crossws": ["crossws@0.3.5", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
"dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
"es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"h3": ["h3@1.15.5", "", { "dependencies": { "cookie-es": "^1.2.2", "crossws": "^0.3.5", "defu": "^6.1.4", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", "radix3": "^1.1.2", "ufo": "^1.6.3", "uncrypto": "^0.1.3" } }, "sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"iron-webcrypto": ["iron-webcrypto@1.2.1", "", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
"is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
"node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
"node-mock-http": ["node-mock-http@1.0.4", "", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
"resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
"rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
"rollup-plugin-dts": ["rollup-plugin-dts@6.3.0", "", { "dependencies": { "magic-string": "^0.30.21" }, "optionalDependencies": { "@babel/code-frame": "^7.27.1" }, "peerDependencies": { "rollup": "^3.29.4 || ^4", "typescript": "^4.5 || ^5.0" } }, "sha512-d0UrqxYd8KyZ6i3M2Nx7WOMy708qsV/7fTHMHxCMCBOAe3V/U7OMPu5GkX8hC+cmkHhzGnfeYongl1IgiooddA=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"unstorage": ["unstorage@1.17.4", "", { "dependencies": { "anymatch": "^3.1.3", "chokidar": "^5.0.0", "destr": "^2.0.5", "h3": "^1.15.5", "lru-cache": "^11.2.0", "node-fetch-native": "^1.6.7", "ofetch": "^1.5.1", "ufo": "^1.6.3" }, "peerDependencies": { "@azure/app-configuration": "^1.8.0", "@azure/cosmos": "^4.2.0", "@azure/data-tables": "^13.3.0", "@azure/identity": "^4.6.0", "@azure/keyvault-secrets": "^4.9.0", "@azure/storage-blob": "^12.26.0", "@capacitor/preferences": "^6 || ^7 || ^8", "@deno/kv": ">=0.9.0", "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", "@planetscale/database": "^1.19.0", "@upstash/redis": "^1.34.3", "@vercel/blob": ">=0.27.1", "@vercel/functions": "^2.2.12 || ^3.0.0", "@vercel/kv": "^1 || ^2 || ^3", "aws4fetch": "^1.0.20", "db0": ">=0.2.1", "idb-keyval": "^6.2.1", "ioredis": "^5.4.2", "uploadthing": "^7.4.4" }, "optionalPeers": ["@azure/app-configuration", "@azure/cosmos", "@azure/data-tables", "@azure/identity", "@azure/keyvault-secrets", "@azure/storage-blob", "@capacitor/preferences", "@deno/kv", "@netlify/blobs", "@planetscale/database", "@upstash/redis", "@vercel/blob", "@vercel/functions", "@vercel/kv", "aws4fetch", "db0", "idb-keyval", "ioredis", "uploadthing"] }, "sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw=="],
"ws": ["@kevisual/ws@8.19.0", "", {}, "sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw=="],
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
"@kevisual/ai/@kevisual/query": ["@kevisual/query@0.0.38", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
}
}

13
keep.ts Normal file
View File

@@ -0,0 +1,13 @@
import { createKeepAlive } from "@kevisual/cnb/keep";
const config = {
"wss": "wss://cnb-tmm-1jhgl3i0m-001.cnb.space:443/stable-3c0b449c6e6e37b44a8a7938c0d8a3049926a64c?reconnectionToken=26ba6a08-1c57-41cc-8099-1f6e64863bf6&reconnection=false&skipWebSocketFrames=false",
"cookie": "orange:workspace:cookie-session:cnb-tmm-1jhgl3i0m-001=93d7bc9b-9ca0-4867-963d-1928ad3038c7",
"url": "https://cnb-tmm-1jhgl3i0m-001.cnb.space/?folder=/workspace"
}
createKeepAlive({
wsUrl: config.wss,
cookie: config.cookie,
debug: true,
});

View File

@@ -1,13 +1,16 @@
{ {
"name": "@kevisual/cnb", "name": "@kevisual/cnb",
"version": "0.0.22", "version": "0.0.37",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "bun bun.config.ts", "build": "bun bun.config.ts",
"flow":"ev npm patch && pnpm build && ev npm publish npm -p" "flow": "ev npm patch && pnpm build && ev npm publish npm -p"
}, },
"keywords": [], "keywords": [],
"bin": {
"cnb": "./bin/index.js"
},
"files": [ "files": [
"dist", "dist",
"src", "src",
@@ -16,20 +19,21 @@
], ],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)", "author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@10.29.1", "packageManager": "pnpm@10.30.3",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"@kevisual/ai": "^0.0.24", "@kevisual/ai": "^0.0.26",
"@kevisual/code-builder": "^0.0.6", "@kevisual/code-builder": "^0.0.6",
"@kevisual/dts": "^0.0.3", "@kevisual/context": "^0.0.8",
"@kevisual/context": "^0.0.4", "@kevisual/dts": "^0.0.4",
"@kevisual/types": "^0.0.12", "@kevisual/types": "^0.0.12",
"@opencode-ai/plugin": "^1.1.53", "@opencode-ai/plugin": "^1.2.20",
"@types/bun": "^1.3.8", "@types/bun": "^1.3.10",
"@types/node": "^25.2.2", "@types/node": "^25.3.5",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"commander": "^14.0.3",
"dayjs": "^1.11.19", "dayjs": "^1.11.19",
"dotenv": "^17.2.4" "dotenv": "^17.3.1"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
@@ -38,10 +42,10 @@
"zod": "^4.3.6" "zod": "^4.3.6"
}, },
"dependencies": { "dependencies": {
"@kevisual/query": "^0.0.40", "@kevisual/query": "^0.0.53",
"@kevisual/router": "^0.0.70", "@kevisual/router": "^0.0.88",
"@kevisual/use-config": "^1.0.30", "@kevisual/use-config": "^1.0.30",
"es-toolkit": "^1.44.0", "es-toolkit": "^1.45.1",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"unstorage": "^1.17.4", "unstorage": "^1.17.4",
"ws": "npm:@kevisual/ws", "ws": "npm:@kevisual/ws",
@@ -51,6 +55,8 @@
".": "./mod.ts", ".": "./mod.ts",
"./opencode": "./dist/opencode.js", "./opencode": "./dist/opencode.js",
"./keep": "./dist/keep.js", "./keep": "./dist/keep.js",
"./keep.ts": "./src/keep.ts",
"./keep-file-live.ts": "./src/workspace/keep-file-live.ts",
"./routes": "./dist/routes.js", "./routes": "./dist/routes.js",
"./src/*": "./src/*", "./src/*": "./src/*",
"./agent/*": "./agent/*" "./agent/*": "./agent/*"

173
pnpm-lock.yaml generated
View File

@@ -12,17 +12,17 @@ importers:
.: .:
dependencies: dependencies:
'@kevisual/query': '@kevisual/query':
specifier: ^0.0.40 specifier: ^0.0.53
version: 0.0.40 version: 0.0.53
'@kevisual/router': '@kevisual/router':
specifier: ^0.0.70 specifier: ^0.0.88
version: 0.0.70 version: 0.0.88
'@kevisual/use-config': '@kevisual/use-config':
specifier: ^1.0.30 specifier: ^1.0.30
version: 1.0.30(dotenv@17.2.4) version: 1.0.30(dotenv@17.3.1)
es-toolkit: es-toolkit:
specifier: ^1.44.0 specifier: ^1.45.1
version: 1.44.0 version: 1.45.1
nanoid: nanoid:
specifier: ^5.1.6 specifier: ^5.1.6
version: 5.1.6 version: 5.1.6
@@ -37,38 +37,41 @@ importers:
version: 4.3.6 version: 4.3.6
devDependencies: devDependencies:
'@kevisual/ai': '@kevisual/ai':
specifier: ^0.0.24 specifier: ^0.0.26
version: 0.0.24 version: 0.0.26
'@kevisual/code-builder': '@kevisual/code-builder':
specifier: ^0.0.6 specifier: ^0.0.6
version: 0.0.6 version: 0.0.6
'@kevisual/context': '@kevisual/context':
specifier: ^0.0.4 specifier: ^0.0.8
version: 0.0.4 version: 0.0.8
'@kevisual/dts': '@kevisual/dts':
specifier: ^0.0.3 specifier: ^0.0.4
version: 0.0.3(typescript@5.9.3) version: 0.0.4(typescript@5.9.3)
'@kevisual/types': '@kevisual/types':
specifier: ^0.0.12 specifier: ^0.0.12
version: 0.0.12 version: 0.0.12
'@opencode-ai/plugin': '@opencode-ai/plugin':
specifier: ^1.1.53 specifier: ^1.2.20
version: 1.1.53 version: 1.2.20
'@types/bun': '@types/bun':
specifier: ^1.3.8 specifier: ^1.3.10
version: 1.3.8 version: 1.3.10
'@types/node': '@types/node':
specifier: ^25.2.2 specifier: ^25.3.5
version: 25.2.2 version: 25.3.5
'@types/ws': '@types/ws':
specifier: ^8.18.1 specifier: ^8.18.1
version: 8.18.1 version: 8.18.1
commander:
specifier: ^14.0.3
version: 14.0.3
dayjs: dayjs:
specifier: ^1.11.19 specifier: ^1.11.19
version: 1.11.19 version: 1.11.19
dotenv: dotenv:
specifier: ^17.2.4 specifier: ^17.3.1
version: 17.2.4 version: 17.3.1
packages: packages:
@@ -83,18 +86,18 @@ packages:
'@jridgewell/sourcemap-codec@1.5.5': '@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@kevisual/ai@0.0.24': '@kevisual/ai@0.0.26':
resolution: {integrity: sha512-7jvZk1/L//VIClK7usuNgN4ZA9Etgbooka1Sj5quE/0UywR+NNnwqXVZ89Y1fBhI1TkhauDsdJBAtcQ7r/vbVw==} resolution: {integrity: sha512-lhaMpxi+vgqPdyBKiuNbSil4hy13tNLbDiqCtG0qUXKtvoowK6xMx269pSSYkYBivczM8g8I0XEouuJceUpJPg==}
'@kevisual/code-builder@0.0.6': '@kevisual/code-builder@0.0.6':
resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==} resolution: {integrity: sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw==}
hasBin: true hasBin: true
'@kevisual/context@0.0.4': '@kevisual/context@0.0.8':
resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==} resolution: {integrity: sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA==}
'@kevisual/dts@0.0.3': '@kevisual/dts@0.0.4':
resolution: {integrity: sha512-4T/m2LqhtwWEW+lWmg7jLxKFW7VtIAftsWFDDZvh10bZunqFf8iXxChHcVSQWikghJb4cq1IkWzPkvc2l+Asdw==} resolution: {integrity: sha512-FVUaH/0nyhbHWpEVjFTGP54PLMm4Hf06aqWLdHOYHNPIgr1aK1C26kOH7iumklGFGk9w93IGxj8Zxe5fap5N2A==}
hasBin: true hasBin: true
'@kevisual/load@0.0.6': '@kevisual/load@0.0.6':
@@ -103,17 +106,17 @@ packages:
'@kevisual/logger@0.0.4': '@kevisual/logger@0.0.4':
resolution: {integrity: sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw==} resolution: {integrity: sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw==}
'@kevisual/permission@0.0.3': '@kevisual/permission@0.0.4':
resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==} resolution: {integrity: sha512-zwBYPnT/z21W4q2wkklJrxvoYBYWG/+a3iXFDKqXQAnDOcxm/SU1f1N6FQb9KxGKl36/fclVlhxlxqszvKCenQ==}
'@kevisual/query@0.0.38': '@kevisual/query@0.0.52':
resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==} resolution: {integrity: sha512-m1UbyDTIxtfAQXM+EqhXA4ytE2V8rV8mXTZVBwzfW9O6+gtvAcRY7K1YYxfewTSXLVh9nwvfHe0KQ8MDL5ukyw==}
'@kevisual/query@0.0.40': '@kevisual/query@0.0.53':
resolution: {integrity: sha512-7m5BgDzd01m51hCHUId6ugQHdwgrLTb6fI7DSuMY17VjWb0+zGnkYmvRBqkTXzoIjjYbP5iwtRnrooEoToQfhg==} resolution: {integrity: sha512-PAhpCLBr0emz0lGNlTVHMbJiC5wrtGLbInPddRzgKE35fiyNt+SWSsUWABiD0DeNrLN/OxWyAFobt880Z/e5MQ==}
'@kevisual/router@0.0.70': '@kevisual/router@0.0.88':
resolution: {integrity: sha512-vXlIj9jRufhcIfeuPWemjSI+dxdzSmIBq5eRxQzqEfAJ7k+mBPhoI4KxH8vHnwyL30bqm8EdODL/p6Wg8uBw3g==} resolution: {integrity: sha512-T8kEbxyTGxZpbxAKDplDjZMIY+HCnXOeEdjwQ11AQetrLuqLFDZS5PnaWdVAHnONUDLhYoftkNj7bGWLtyQDlg==}
'@kevisual/types@0.0.12': '@kevisual/types@0.0.12':
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==} resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
@@ -127,14 +130,14 @@ packages:
resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==} resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==}
engines: {node: '>=10.0.0'} engines: {node: '>=10.0.0'}
'@opencode-ai/plugin@1.1.53': '@opencode-ai/plugin@1.2.20':
resolution: {integrity: sha512-9ye7Wz2kESgt02AUDaMea4hXxj6XhWwKAG8NwFhrw09Ux54bGaMJFt1eIS8QQGIMaD+Lp11X4QdyEg96etEBJw==} resolution: {integrity: sha512-BE6TOXVxgF24g5QgtlogSY5B+/AmZJ3cYaVjHZhUVuAli9JEg4RblrbrK2rfgbyZBoZDpjBLGTYtIRTVmOccEA==}
'@opencode-ai/sdk@1.1.53': '@opencode-ai/sdk@1.2.20':
resolution: {integrity: sha512-RUIVnPOP1CyyU32FrOOYuE7Ge51lOBuhaFp2NSX98ncApT7ffoNetmwzqrhOiJQgZB1KrbCHLYOCK6AZfacxag==} resolution: {integrity: sha512-U5ROpG21D8jg9rkc1IgKAk1g5dn6X/rkOBfveupd0peSDO9n6VM9aikYccVLaMObxVqdjtG08IeQOFTPVS8ySQ==}
'@rollup/plugin-commonjs@28.0.9': '@rollup/plugin-commonjs@29.0.0':
resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==} resolution: {integrity: sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==}
engines: {node: '>=16.0.0 || 14 >= 14.17'} engines: {node: '>=16.0.0 || 14 >= 14.17'}
peerDependencies: peerDependencies:
rollup: ^2.68.0||^3.0.0||^4.0.0 rollup: ^2.68.0||^3.0.0||^4.0.0
@@ -311,14 +314,14 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@types/bun@1.3.8': '@types/bun@1.3.10':
resolution: {integrity: sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA==} resolution: {integrity: sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ==}
'@types/estree@1.0.8': '@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
'@types/node@25.2.2': '@types/node@25.3.5':
resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==} resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==}
'@types/resolve@1.20.2': '@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -330,13 +333,17 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
bun-types@1.3.8: bun-types@1.3.10:
resolution: {integrity: sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q==} resolution: {integrity: sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg==}
chokidar@5.0.0: chokidar@5.0.0:
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
engines: {node: '>= 20.19.0'} engines: {node: '>= 20.19.0'}
commander@14.0.3:
resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==}
engines: {node: '>=20'}
commondir@1.0.1: commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
@@ -359,12 +366,12 @@ packages:
destr@2.0.5: destr@2.0.5:
resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==}
dotenv@17.2.4: dotenv@17.3.1:
resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==}
engines: {node: '>=12'} engines: {node: '>=12'}
es-toolkit@1.44.0: es-toolkit@1.45.1:
resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==} resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==}
estree-walker@2.0.2: estree-walker@2.0.2:
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
@@ -493,8 +500,8 @@ packages:
uncrypto@0.1.3: uncrypto@0.1.3:
resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==}
undici-types@7.16.0: undici-types@7.18.2:
resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
unstorage@1.17.4: unstorage@1.17.4:
resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==}
@@ -575,19 +582,19 @@ snapshots:
'@jridgewell/sourcemap-codec@1.5.5': {} '@jridgewell/sourcemap-codec@1.5.5': {}
'@kevisual/ai@0.0.24': '@kevisual/ai@0.0.26':
dependencies: dependencies:
'@kevisual/logger': 0.0.4 '@kevisual/logger': 0.0.4
'@kevisual/permission': 0.0.3 '@kevisual/permission': 0.0.4
'@kevisual/query': 0.0.38 '@kevisual/query': 0.0.52
'@kevisual/code-builder@0.0.6': {} '@kevisual/code-builder@0.0.6': {}
'@kevisual/context@0.0.4': {} '@kevisual/context@0.0.8': {}
'@kevisual/dts@0.0.3(typescript@5.9.3)': '@kevisual/dts@0.0.4(typescript@5.9.3)':
dependencies: dependencies:
'@rollup/plugin-commonjs': 28.0.9(rollup@4.57.1) '@rollup/plugin-commonjs': 29.0.0(rollup@4.57.1)
'@rollup/plugin-node-resolve': 16.0.3(rollup@4.57.1) '@rollup/plugin-node-resolve': 16.0.3(rollup@4.57.1)
'@rollup/plugin-typescript': 12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3) '@rollup/plugin-typescript': 12.3.0(rollup@4.57.1)(tslib@2.8.1)(typescript@5.9.3)
rollup: 4.57.1 rollup: 4.57.1
@@ -602,37 +609,33 @@ snapshots:
'@kevisual/logger@0.0.4': {} '@kevisual/logger@0.0.4': {}
'@kevisual/permission@0.0.3': {} '@kevisual/permission@0.0.4': {}
'@kevisual/query@0.0.38': '@kevisual/query@0.0.52': {}
dependencies:
tslib: 2.8.1
'@kevisual/query@0.0.40': '@kevisual/query@0.0.53': {}
dependencies:
tslib: 2.8.1
'@kevisual/router@0.0.70': '@kevisual/router@0.0.88':
dependencies: dependencies:
es-toolkit: 1.44.0 es-toolkit: 1.45.1
'@kevisual/types@0.0.12': {} '@kevisual/types@0.0.12': {}
'@kevisual/use-config@1.0.30(dotenv@17.2.4)': '@kevisual/use-config@1.0.30(dotenv@17.3.1)':
dependencies: dependencies:
'@kevisual/load': 0.0.6 '@kevisual/load': 0.0.6
dotenv: 17.2.4 dotenv: 17.3.1
'@kevisual/ws@8.19.0': {} '@kevisual/ws@8.19.0': {}
'@opencode-ai/plugin@1.1.53': '@opencode-ai/plugin@1.2.20':
dependencies: dependencies:
'@opencode-ai/sdk': 1.1.53 '@opencode-ai/sdk': 1.2.20
zod: 4.3.6 zod: 4.3.6
'@opencode-ai/sdk@1.1.53': {} '@opencode-ai/sdk@1.2.20': {}
'@rollup/plugin-commonjs@28.0.9(rollup@4.57.1)': '@rollup/plugin-commonjs@29.0.0(rollup@4.57.1)':
dependencies: dependencies:
'@rollup/pluginutils': 5.3.0(rollup@4.57.1) '@rollup/pluginutils': 5.3.0(rollup@4.57.1)
commondir: 1.0.1 commondir: 1.0.1
@@ -746,35 +749,37 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.57.1': '@rollup/rollup-win32-x64-msvc@4.57.1':
optional: true optional: true
'@types/bun@1.3.8': '@types/bun@1.3.10':
dependencies: dependencies:
bun-types: 1.3.8 bun-types: 1.3.10
'@types/estree@1.0.8': {} '@types/estree@1.0.8': {}
'@types/node@25.2.2': '@types/node@25.3.5':
dependencies: dependencies:
undici-types: 7.16.0 undici-types: 7.18.2
'@types/resolve@1.20.2': {} '@types/resolve@1.20.2': {}
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 25.2.2 '@types/node': 25.3.5
anymatch@3.1.3: anymatch@3.1.3:
dependencies: dependencies:
normalize-path: 3.0.0 normalize-path: 3.0.0
picomatch: 2.3.1 picomatch: 2.3.1
bun-types@1.3.8: bun-types@1.3.10:
dependencies: dependencies:
'@types/node': 25.2.2 '@types/node': 25.3.5
chokidar@5.0.0: chokidar@5.0.0:
dependencies: dependencies:
readdirp: 5.0.0 readdirp: 5.0.0
commander@14.0.3: {}
commondir@1.0.1: {} commondir@1.0.1: {}
cookie-es@1.2.2: {} cookie-es@1.2.2: {}
@@ -791,9 +796,9 @@ snapshots:
destr@2.0.5: {} destr@2.0.5: {}
dotenv@17.2.4: {} dotenv@17.3.1: {}
es-toolkit@1.44.0: {} es-toolkit@1.45.1: {}
estree-walker@2.0.2: {} estree-walker@2.0.2: {}
@@ -927,7 +932,7 @@ snapshots:
uncrypto@0.1.3: {} uncrypto@0.1.3: {}
undici-types@7.16.0: {} undici-types@7.18.2: {}
unstorage@1.17.4: unstorage@1.17.4:
dependencies: dependencies:

View File

@@ -1,5 +0,0 @@
packages:
- web
onlyBuiltDependencies:
- esbuild

View File

@@ -1,4 +1,5 @@
import { CNBCore, CNBCoreOptions, RequestOptions, Result } from "../cnb-core.ts"; import { CNBCore, CNBCoreOptions, RequestOptions, Result } from "../cnb-core.ts";
import { extractAliveInfo } from "./issue-alive.ts";
export type IssueAssignee = { export type IssueAssignee = {
nickname: string; nickname: string;
@@ -88,6 +89,51 @@ export class Issue extends CNBCore {
}; };
return this.post({ url, data: postData }); return this.post({ url, data: postData });
} }
/**
* 获取alive issue的元数据
* @param repo
* @param issueNumber
* @returns
*/
async getAliveMetadata(repo: string, issueNumber: string | number): Promise<Result<AliveMetadata>> {
const url = this.hackURL + `/${repo}/-/issues/${issueNumber}`;
const resHtml = await this.get({
url,
useCookie: true,
headers: {
'Content-Type': 'text/html; charset=utf-8',
Accept: "text/html; charset=utf-8",
}
});
if (resHtml.code !== 200) {
return resHtml;
}
const html = resHtml.data as string;
const { aliveSessionID, aliveChannelID } = extractAliveInfo(html);
if (!aliveSessionID || !aliveChannelID) {
return {
code: 500,
message: 'Failed to extract alive metadata',
data: null,
};
}
const off = Date.now();
return {
code: 200,
message: 'success',
data: {
aliveSessionID,
aliveChannelID,
domain: 'alive.cnb.cool',
subscriptions: [
{ event: 'subscribe', data: { id: `${aliveChannelID}--issue:info-update`, off } },
{ event: 'subscribe', data: { id: `${aliveChannelID}--issue:add-comment`, off } },
{ event: 'subscribe', data: { id: `${aliveChannelID}--issue:comment-update`, off } },
{ event: 'subscribe', data: { id: `${aliveChannelID}--issue:invisible`, off } },
],
},
};
}
} }
type GetListParams = { type GetListParams = {
@@ -119,4 +165,17 @@ type GetListParams = {
updated_time_begin?: string; updated_time_begin?: string;
/** 问题更新时间过滤-结束,例如: 2022-01-31 */ /** 问题更新时间过滤-结束,例如: 2022-01-31 */
updated_time_end?: string; updated_time_end?: string;
}
export type AliveMetadata = {
aliveSessionID: string;
aliveChannelID: string;
domain: string;
subscriptions: {
event: 'subscribe',
data: {
id: string;
off: number;
}
}[]
} }

15
src/issue/issue-alive.ts Normal file
View File

@@ -0,0 +1,15 @@
export function extractAliveInfo(html: string): { aliveSessionID: string | null; aliveChannelID: string | null } {
const match = html.match(/<script[^>]*id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
if (!match || !match[1]) {
return { aliveSessionID: null, aliveChannelID: null };
}
try {
const data = JSON.parse(match[1]);
const aliveSessionID = data?.props?.pageProps?.aliveSessionID ?? null;
const aliveChannelID = data?.props?.pageProps?.aliveChannelID ?? null;
return { aliveSessionID, aliveChannelID };
} catch {
return { aliveSessionID: null, aliveChannelID: null };
}
}

View File

@@ -0,0 +1,24 @@
```js
fetch("wss://alive.cnb.cool/?id=26354bd0-3e00-4869-93c9-b687f19d96c1", {
"headers": {
"accept-language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7",
"cache-control": "no-cache",
"pragma": "no-cache",
"sec-websocket-extensions": "permessage-deflate; client_max_window_bits",
"sec-websocket-key": "vXnvsfo4splpfeDSnJLxKA==",
"sec-websocket-protocol": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXBlIjoiY29va2llIiwic2NvcGUiOiIiLCJtZXRhIjp7IkxpY2Vuc2UiOnsiY29tbW9uTmFtZSI6IiouY25iLmNvb2wiLCJub3RCZWZvcmUiOiIyMDI1LTA2LTE4VDA5OjE3OjE5WiIsIm5vdEFmdGVyIjoiMjAzNS0wNi0xNlQwOToxNzoxOVoiLCJpc3N1ZXIiOiJUZW5jZW50LCBJbmMiLCJvcmdhbml6YXRpb24iOiLohb7orq_kupEiLCJtZW1iZXJzIjoxMDAwMDAwLCJjaGFubmVsIjoiU2FhUyJ9LCJyb290X29yZ2FuaXphdGlvbl9zZXR0aW5nIjp7ImhpZGVfbWVtYmVycyI6MCwiaGlkZV9zdWJfZ3JvdXBzIjowLCJzaG93X3ByaXZhdGVfcmVwb193YXRlcm1hcmsiOjAsImdyb3VwX3Byb3RlY3Rpb24iOjEsImVtYWlsX3ZlcmlmaWNhdGlvbiI6IiIsInZhbHVlcyI6IiJ9fSwicGxhdGZvcm0iOiIiLCJ1c2VyX2lkIjoiIiwidXNlcl9lbWFpbCI6IiIsIm5pY2tuYW1lIjoiIiwidXNlcm5hbWUiOiIiLCJ2ZXJpZmllZCI6ZmFsc2UsImZyZWV6ZSI6ZmFsc2UsImJhbiI6ZmFsc2UsImxvY2tlZCI6ZmFsc2UsInVzZXJfZGV2aWNlX3R5cGUiOjAsInNsdWciOiJrZXZpc3VhbC9rZXZpc3VhbCIsInNsdWdfaWQiOiIxOTE1NDE1NDE0NTE4NjU3MDI0Iiwic2x1Z190eXBlIjoxLCJzbHVnX3N0YXR1cyI6MCwic2x1Z19mcmVlemUiOmZhbHNlLCJzbHVnX3Zpc2liaWxpdHkiOiJQdWJsaWMiLCJzbHVnX3Jvb3RfaWQiOiIxOTE1MzUzNzE5MDE4MzM2MjU2Iiwic2x1Z19yb2xlIjoiVW5rbm93biIsImxhbmd1YWdlIjoiZW4tVVMiLCJjb250ZXh0Ijoie30iLCJpc3MiOiJhY2Nlc3Mtcm91dGVyLTVmNzg5ZGM3N2ItN3Y1bnMiLCJpYXQiOjE3NzE1Mjk1MTQsImp0aSI6Ijk2MjYzIn0.PrlpGLNw7-1ucm3CiTQsPH7nIYFgvhZqQGhlws3R4ME",
"sec-websocket-version": "13"
},
"body": null,
"method": "GET",
"mode": "cors",
"credentials": "omit"
});
```
```json
{"event":"subscribe","data":{"id":"495b92b2fbcd908e6ca1c809a3873bb373a7a6c6b814dfce5ff60b5f87dd0944--issue:info-update","off":1771529514922}}
{"event":"subscribe","data":{"id":"495b92b2fbcd908e6ca1c809a3873bb373a7a6c6b814dfce5ff60b5f87dd0944--issue:add-comment","off":1771529514922}}
{"event":"subscribe","data":{"id":"495b92b2fbcd908e6ca1c809a3873bb373a7a6c6b814dfce5ff60b5f87dd0944--issue:comment-update","off":1771529514922}}
{"event":"subscribe","data":{"id":"495b92b2fbcd908e6ca1c809a3873bb373a7a6c6b814dfce5ff60b5f87dd0944--issue:invisible","off":1771529514922}}
```

File diff suppressed because one or more lines are too long

View File

@@ -108,6 +108,10 @@ export class Repo extends CNBCore {
const url = `/${repo}`; const url = `/${repo}`;
return this.patch({ url, data: params }); return this.patch({ url, data: params });
} }
getRepo(repo: string): Promise<Result<RepoItem>> {
const url = `/${repo}`;
return this.get({ url });
}
} }
type UpdateRepoInfo = { type UpdateRepoInfo = {
description?: string; description?: string;

View File

@@ -16,6 +16,18 @@ export class User extends CNBCore {
useCookie: true, useCookie: true,
}); });
} }
/**
* 判断当前 Cookie 是否有效
* @returns
*/
async checkCookieValid(): Promise<Result> {
const user = await this.getCurrentUser();
if (user.code === 200) {
return { code: 200, message: 'cookie valid' };
} else {
return { code: 401, message: 'cookie invalid' };
}
}
/** /**
* 使用 Token 获取用户信息 * 使用 Token 获取用户信息
* @returns * @returns

View File

@@ -62,7 +62,7 @@ export class Workspace extends CNBCore {
* 停止我的云原生开发环境 * 停止我的云原生开发环境
* @param params 停止参数pipelineId 和 sn 二选一,优先使用 pipelineId * @param params 停止参数pipelineId 和 sn 二选一,优先使用 pipelineId
*/ */
async stopWorkspace(params: { pipelineId?: string; sn?: string }): Promise<{ buildLogUrl: string; message: string; sn: string }> { async stopWorkspace(params: { pipelineId?: string; sn?: string }): Promise<Result<{ buildLogUrl: string; message: string; sn: string }>> {
const data: { pipelineId?: string; sn?: string } = {}; const data: { pipelineId?: string; sn?: string } = {};
if (params.pipelineId) { if (params.pipelineId) {
@@ -113,7 +113,62 @@ export class Workspace extends CNBCore {
return this.post({ url: `/${repo}/-/workspace/start`, data }); return this.post({ url: `/${repo}/-/workspace/start`, data });
} }
/**
* 添加使用cookie获取工作空间访问权限的功能适用于需要保持工作空间连接状态的场景
* 例如使用 WebSocket 连接工作空间时需要携带 cookie 进行身份验证。
* https://cnb.cool/kevisual/dev-env/-/workspace/vscode-web/cnb-708-1ji9sog7o-001
* @param repo
* @param pipelineId
* @returns
*/
async getWorkspaceCookie(repo: string, pipelineId: string): Promise<Result<{ value: string, cookie: string; cookieName: string }>> {
const url = `${this.hackURL}/${repo}/-/workspace/vscode-web/${pipelineId}`;
const response = await fetch(url, {
method: 'GET',
redirect: 'manual',
headers: {
'Cookie': this.cookie || '',
'Accept': 'application/json',
}
});
// 第一次 302 重定向
if (response.status === 302 || response.status === 301) {
// 包含token的重定向 URL 通常在 Location 头中返回
// 类似 https://cnb-708-1ji9sog7o-001.cnb.space/login?t=orange:workspace:login-token:963691a2-35ce-4fef-a7ba-72723cefd226
const loginURL = response.headers.get('Location');
// 从 URL 参数中获取 cookieName例如: orange:workspace:cookie-session:cnb-708-1ji9sog7o-001
const cookieName = `orange:workspace:cookie-session:${pipelineId}`;
// 第二次请求,也设置为 manual 防止自动重定向
const response2 = await fetch(loginURL || '', {
method: 'GET',
redirect: 'manual',
headers: {
'Cookie': this.cookie || '',
'Accept': 'application/json',
}
});
// 第二次 302 重定向,获取最终的 cookie 值
if (response2.status === 302 || response2.status === 301) {
// 从 Set-Cookie 头中获取 cookie 值
const setCookie = response2.headers.get('Set-Cookie');
// 解析 cookie 值
const cookieValue = setCookie?.split(';')[0]?.split('=')[1] || '';
return {
code: 200,
message: 'success',
data: { value: cookieValue, cookieName, cookie: `${cookieName}=${cookieValue}` }
};
}
// 如果不是重定向,尝试获取 JSON 数据
return { code: 500 };
}
return { code: 500, };
}
} }
export interface WorkspaceLinkDetail { export interface WorkspaceLinkDetail {
codebuddy: string; codebuddy: string;

View File

@@ -0,0 +1,137 @@
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 baseDir = path.join(os.homedir(), '.cnb', 'live');
const keepAliveFilePath = path.join(baseDir, '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();
const item = cache.data.find(item => item.repo === repo && item.pipelineId === pipelineId);
if (item) {
stopLive(item.pm2Name);
}
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: { cookie: string, repo: string, pipelineId: string }): KeepAliveData => {
const { cookie, repo, pipelineId } = data;
const createdTime = Date.now();
const wsUrl = `wss://${pipelineId}.cnb.space:443?skipWebSocketFrames=false`;
const pm2Name = `keep_${repo}__${pipelineId}`.replace(/\//g, '__');
const filePath = path.join(baseDir, `${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;
}
export class KeepAliveManager {
static getCache() {
return getKeepAliveCache();
}
static add(data: KeepAliveData) {
return addKeepAliveData(data);
}
static createLiveData(data: { cookie: string, repo: string, pipelineId: string }): KeepAliveData {
return createLiveData(data);
}
static remove(repo: string, pipelineId: string) {
return removeKeepAliveData(repo, pipelineId);
}
}

View File

@@ -1,5 +1,16 @@
// WebSocket Keep-Alive Client Library // WebSocket Keep-Alive Client Library
import WebSocket from "ws"; // 运行时检测Bun 使用原生 WebSocketNode.js 使用 ws 库
let WebSocketModule: any;
if (typeof Bun !== 'undefined') {
// Bun 环境:使用原生 WebSocket
WebSocketModule = { WebSocket: globalThis.WebSocket };
} else {
// Node.js 环境:使用 ws 库
WebSocketModule = await import('ws');
}
const WebSocket = WebSocketModule.WebSocket;
export interface KeepAliveConfig { export interface KeepAliveConfig {
wsUrl: string; wsUrl: string;
@@ -31,6 +42,7 @@ export class WSKeepAlive {
private pingTimer: NodeJS.Timeout | null = null; private pingTimer: NodeJS.Timeout | null = null;
private messageHandlers: Set<MessageHandler> = new Set(); private messageHandlers: Set<MessageHandler> = new Set();
private url: URL; private url: URL;
private readonly isBun: boolean;
constructor(config: KeepAliveConfig) { constructor(config: KeepAliveConfig) {
this.config = { this.config = {
@@ -48,6 +60,7 @@ export class WSKeepAlive {
debug: config.debug ?? false, debug: config.debug ?? false,
}; };
this.url = new URL(this.config.wsUrl); this.url = new URL(this.config.wsUrl);
this.isBun = typeof Bun !== 'undefined';
} }
private log(message: string) { private log(message: string) {
@@ -107,44 +120,91 @@ export class WSKeepAlive {
} }
}); });
this.ws.on("open", () => { if (this.isBun) {
debug && this.log("Connected!"); // Bun 环境:使用标准 Web API
this.reconnectAttempts = 0; const ws = this.ws as any;
this.config.onConnect(); ws.onopen = () => {
this.startPing(); debug && this.log("Connected!");
}); this.reconnectAttempts = 0;
this.config.onConnect();
this.startPing();
};
this.ws.on("message", (data: any) => { ws.onmessage = async (event: MessageEvent) => {
if (Buffer.isBuffer(data)) { let data: Buffer | string;
const parsed = this.parseMessage(data);
this.config.onMessage(parsed?.raw ?? data);
this.messageHandlers.forEach(handler => { if (event.data instanceof Blob) {
if (parsed) handler(parsed); data = Buffer.from(await event.data.arrayBuffer());
}); } else if (event.data instanceof ArrayBuffer) {
} else { data = Buffer.from(event.data);
this.config.onMessage(data); } else if (typeof event.data === 'string') {
} data = event.data;
}); } else {
data = Buffer.from(event.data);
}
this.ws.on("close", (code: number) => { this.handleMessage(data);
debug && this.log(`Disconnected (code: ${code})`); };
this.stopPing();
this.config.onDisconnect(code);
this.handleReconnect();
});
this.ws.on("error", (err: Error) => { ws.onclose = (event: CloseEvent) => {
debug && this.log(`Error: ${err.message}`); debug && this.log(`Disconnected (code: ${event.code})`);
this.config.onError(err); this.stopPing();
}); this.config.onDisconnect(event.code);
this.handleReconnect();
};
ws.onerror = (event: Event) => {
debug && this.log(`Error: ${event}`);
this.config.onError(new Error("WebSocket error"));
};
} else {
// Node.js (ws 库):使用 EventEmitter 模式
const ws = this.ws as any;
ws.on("open", () => {
debug && this.log("Connected!");
this.reconnectAttempts = 0;
this.config.onConnect();
this.startPing();
});
ws.on("message", (data: any) => {
this.handleMessage(data);
});
ws.on("close", (code: number) => {
debug && this.log(`Disconnected (code: ${code})`);
this.stopPing();
this.config.onDisconnect(code);
this.handleReconnect();
});
ws.on("error", (err: Error) => {
debug && this.log(`Error: ${err.message}`);
this.config.onError(err);
});
}
}
// 统一的消息处理方法
private handleMessage(data: Buffer | string) {
if (Buffer.isBuffer(data)) {
const parsed = this.parseMessage(data);
this.config.onMessage(parsed?.raw ?? data);
this.messageHandlers.forEach(handler => {
if (parsed) handler(parsed);
});
} else {
this.config.onMessage(data);
}
} }
private startPing() { private startPing() {
this.stopPing(); this.stopPing();
this.pingTimer = setInterval(() => { this.pingTimer = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) { if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.ping(); // 使用 JSON 格式的 ping 消息,兼容 Bun 和 Node.js
this.ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
this.log("Sent ping"); this.log("Sent ping");
} }
}, this.config.pingInterval); }, this.config.pingInterval);

1
test/build-log.ts Normal file
View File

@@ -0,0 +1 @@
// 不用查看

View File

@@ -60,4 +60,34 @@ main:
api_trigger_sync_from_gitea: api_trigger_sync_from_gitea:
- <<: *common_sync_from_gitea - <<: *common_sync_from_gitea
` `
buildByConfig() // buildByConfig()
const buildByConfig2 = async () => {
const build = await repo.startBuild('kevisual/cnb', {
branch: 'main',
env: {
},
event: 'api_trigger_events',
config: config2,
});
console.log("build", showMore(build));
}
const config2 = `# .cnb.yml
include:
- https://cnb.cool/kevisual/cnb/-/blob/main/.cnb/template.yml
main:
api_trigger_events:
-
docker:
image: docker.cnb.cool/kevisual/dev-env:latest
services:
- vscode
- docker
stages:
- name: test
steps:
- run: echo "hello world"
`
buildByConfig2()

5
test/get-live-meta.ts Normal file
View File

@@ -0,0 +1,5 @@
import { cnb, showMore } from './common'
const meta = await cnb.issue.getAliveMetadata('kevisual/kevisual', 34);
console.log('meta', showMore(meta));

11
test/keep-cookie-get.ts Normal file
View File

@@ -0,0 +1,11 @@
// https://cnb.cool/kevisual/dev-env/-/workspace/vscode-web/cnb-708-1ji9sog7o-001
import { Build } from "../src/index.ts";
import { cnb, showMore } from "./common.ts";
const repo = 'kevisual/dev-env';
const pipelineId = 'cnb-708-1ji9sog7o-001';
const res = await cnb.workspace.getWorkspaceCookie(repo, pipelineId);
console.log('token', showMore(res));

19
test/keep-file-live.ts Normal file
View File

@@ -0,0 +1,19 @@
import { addKeepAliveData, createLiveData, getKeepAliveCache } from '../agent/routes/workspace/keep-file-live';
const repo = 'kevisual/dev-env';
const pipelineId = 'cnb-708-1ji9sog7o-001';
const testData = createLiveData({
wsUrl: "wss://cnb-708-1ji9sog7o-001.cnb.space:443?skipWebSocketFrames=false",
cookie: "orange:workspace:cookie-session:cnb-708-1ji9sog7o-001=3dc03d84-5617-4e44-a6b9-38ce4398aea5",
repo: repo,
pipelineId: pipelineId
});
addKeepAliveData(testData);
// 运行后可以在 ~/.cnb/kevisual_dev-env_cnb-708-1ji9sog7o-001.json 中看到保持存活的数据
// 同时可以通过 pm2 list 命令看到对应的保持存活的进程
// 注意:如果要测试停止保持存活,可以调用 stopLive(testData.pm2Name) 来停止对应的进程
// 例如stopLive('kevisual_dev-env_cnb-708-1ji9sog7o-001');

View File

@@ -1,13 +1,16 @@
import { createKeepAlive } from "@kevisual/cnb/keep"; import { createKeepAlive } from "@kevisual/cnb/keep";
// stable-9184b645cc7aa41b750e2f2ef956f2896512dd84 这个可以修改
// reconnectionToken 不能修改
// 但是可以删除 reconnectionToken=38837a9e-dd5a-4d28-9ec0-5e5b537a8b0f&skipWebSocketFrames=false
const config = { const config = {
"wss": "wss://cnb-dk4-1jgcjjqvc-001.cnb.space:443/stable-3c0b449c6e6e37b44a8a7938c0d8a3049926a64c?reconnectionToken=d70ab69b-5e92-471a-b3d2-31f554b468d4&reconnection=false&skipWebSocketFrames=false", "wss": "wss://cnb-708-1ji9sog7o-001.cnb.space:443?skipWebSocketFrames=false",
"cookie": "orange:workspace:cookie-session:cnb-dk4-1jgcjjqvc-001=01fea6db-d73f-4ce8-8929-36903ee7a266", "cookie": "orange:workspace:cookie-session:cnb-708-1ji9sog7o-001=3dc03d84-5617-4e44-a6b9-38ce4398aea5",
"url": "https://cnb-dk4-1jgcjjqvc-001.cnb.space/?folder=/workspace" "url": "https://cnb-708-1ji9sog7o-001.cnb.space/?folder=/workspace"
} }
createKeepAlive({ createKeepAlive({
wsUrl: config.wss, wsUrl: config.wss,
cookie: config.cookie, cookie: config.cookie,
debug: true, debug: true,
}); });

View File

@@ -4,10 +4,14 @@ import { token, showMore, cookie } from "./common.ts";
const user = new User({ token: token, cookie: cookie }); const user = new User({ token: token, cookie: cookie });
const currentUser = await user.getCurrentUser(); // const currentUser = await user.getCurrentUser();
console.log("currentUser", showMore(currentUser)); // console.log("currentUser", showMore(currentUser));
// const accessToken = await user.createAccessToken({ description: "Test Token from API" }); // const accessToken = await user.createAccessToken({ description: "Test Token from API" });
// console.log("accessToken", showMore(accessToken)); // console.log("accessToken", showMore(accessToken));
const tokenUser = await user.getUser();
console.log("tokenUser", showMore(tokenUser));