Compare commits
63 Commits
183c0d5b77
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7582d8a6d3 | ||
|
|
3971a428ff | ||
|
|
f05bf93650 | ||
|
|
95bcad7587 | ||
|
|
c7a5200aa5 | ||
|
|
296f893c22 | ||
|
|
d019ee0158 | ||
|
|
c7a0c6ac3c | ||
|
|
8813e06c61 | ||
|
|
ae5565cda7 | ||
|
|
fe89bdee5b | ||
| a2702915d5 | |||
|
|
700e86a4d2 | ||
|
|
d9fda44e78 | ||
|
|
cda1d0dc8f | ||
|
|
8249da006f | ||
|
|
176c82f1ac | ||
|
|
525f0af8ba | ||
| 2e630b2da9 | |||
| 79d82a1f8d | |||
| f48568132a | |||
| a551cbe79d | |||
|
|
460979b577 | ||
|
|
ef38fc0596 | ||
| 4e4f54b4cd | |||
| 07453f59df | |||
| 14719adbe7 | |||
| 3459066cd7 | |||
| fc6a5bd73c | |||
| 8379be1630 | |||
| 813005ab9c | |||
|
|
38ee73e48f | ||
| 7b8f6fbf9f | |||
| d08345d81c | |||
|
|
7d227d3913 | ||
|
|
a40c2352bf | ||
|
|
382c4809ea | ||
| 21ba07e55b | |||
| 81a3aae8ec | |||
| dd5331bbaa | |||
| 2eecbe273e | |||
| a563f3e0d6 | |||
| 327db1e09a | |||
| 8465ba7182 | |||
| 841ed6ffa7 | |||
| 5043392939 | |||
| 8a6bb9bbe9 | |||
| 8ca6b77e4d | |||
| d3286e2766 | |||
| dd691f7a59 | |||
| e6042e025f | |||
|
|
43b61dc656 | ||
|
|
3ff9e4e374 | ||
|
|
24ee793db1 | ||
|
|
d16adc07fe | ||
|
|
0ebc94a7d0 | ||
| 47229c6db9 | |||
| d231f3748a | |||
| 9bb9f447ec | |||
| ea137eb70b | |||
| 13401d9aa0 | |||
| a1339014a3 | |||
|
|
02a4b86338 |
43
.cnb.yml
43
.cnb.yml
@@ -4,11 +4,22 @@ include:
|
||||
|
||||
.common_env: &common_env
|
||||
env:
|
||||
TO_REPO: kevisual/cnb
|
||||
TO_URL: git.xiongxiao.me
|
||||
USERNAME: root
|
||||
imports:
|
||||
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
||||
|
||||
.npc: &npc
|
||||
- docker:
|
||||
image: docker.cnb.cool/kevisual/dev-env/ubuntu-bun:latest
|
||||
services:
|
||||
- docker
|
||||
env: !reference [.common_env, env]
|
||||
imports: !reference [.common_env, imports]
|
||||
stages:
|
||||
- name: "task"
|
||||
script: |
|
||||
git clone https://cnb.cool/kevisual/cnb cnb
|
||||
cd cnb && bun i && bun run agent/npc.ts cnb npc --args owner="小熊猫呜呜呜"
|
||||
$:
|
||||
vscode:
|
||||
- docker:
|
||||
@@ -16,29 +27,9 @@ $:
|
||||
services:
|
||||
- vscode
|
||||
- docker
|
||||
env: !reference [.common_env, env]
|
||||
imports: !reference [.common_env, imports]
|
||||
# 开发环境启动后会执行的任务
|
||||
# stages:
|
||||
# - name: pnpm install
|
||||
# script: pnpm install
|
||||
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
|
||||
issue.comment@npc: *npc
|
||||
issue.open: *npc
|
||||
issue.reopen: *npc
|
||||
6
.cnb/settings.yml
Normal file
6
.cnb/settings.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
npc:
|
||||
defaultRole: router
|
||||
roles:
|
||||
- name: router
|
||||
prompt: |
|
||||
你好
|
||||
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
||||
CNB_COOKIE=CNBSESSION=1771242023.1935321989751226368.8841cb77d609c050b1a19877644487b6543b587a80953cbdf3018a15b9948b48;csrfkey=309068260
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,8 +1,9 @@
|
||||
.env
|
||||
.env.local
|
||||
.env*
|
||||
!.env*example
|
||||
node_modules
|
||||
.pnpm-store
|
||||
|
||||
dist
|
||||
pack-dist
|
||||
|
||||
storage
|
||||
1
.npmrc
1
.npmrc
@@ -1,3 +1,2 @@
|
||||
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
|
||||
//npm.cnb.cool/kevisual/registry/-/packages/:_authToken=${CNB_API_KEY}
|
||||
//registry.npmjs.org/:_authToken=${NPM_TOKEN}
|
||||
|
||||
5
.opencode/package.json
Normal file
5
.opencode/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@opencode-ai/plugin": "1.2.26"
|
||||
}
|
||||
}
|
||||
16
SKILL.md
Normal file
16
SKILL.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: 'cloud-cnb'
|
||||
description: cloud-cnb 是一个基于 cnb.cool 的自动化工具,提供了工作区管理、代码仓库创建和 kevisual assistant app 等功能,旨在提升开发效率和项目管理的便捷性。
|
||||
---
|
||||
|
||||
# 执行命令
|
||||
|
||||
```sh
|
||||
## help
|
||||
cloud -h
|
||||
```
|
||||
## 运行查看cli列表
|
||||
|
||||
```sh
|
||||
cloud cli list
|
||||
```
|
||||
40
agent/app.ts
40
agent/app.ts
@@ -1,17 +1,35 @@
|
||||
import { QueryRouterServer as App } from '@kevisual/router'
|
||||
import { useContextKey } from '@kevisual/context'
|
||||
import { useConfig, useKey } from '@kevisual/use-config'
|
||||
import { useKey } from '@kevisual/use-config'
|
||||
import { CNB } from '../src/index.ts';
|
||||
import { CNBManager } from './modules/cnb-manager.ts'
|
||||
|
||||
export const config = useConfig()
|
||||
export const cnb = useContextKey<CNB>('cnb', () => {
|
||||
// CNB_TOKEN是降级兼容变量,推荐使用CNB_API_KEY
|
||||
// CNB_TOKEN 是流水线自己就有的变量,但是权限比较小
|
||||
const token = useKey('CNB_API_KEY') as string || useKey('CNB_TOKEN') as string
|
||||
// cookie 变量是可选的
|
||||
const cookie = useKey('CNB_COOKIE') as string
|
||||
return new CNB({ token: token, cookie: cookie });
|
||||
})
|
||||
export const app = useContextKey<App>('app', () => {
|
||||
export const cnbManager = new CNBManager()
|
||||
|
||||
// CNB_TOKEN是降级兼容变量,推荐使用CNB_API_KEY
|
||||
// CNB_TOKEN 是流水线自己就有的变量,但是权限比较小
|
||||
const token = useKey('CNB_API_KEY') as string || useKey('CNB_TOKEN') as string
|
||||
// cookie 变量是可选的
|
||||
const cookie = useKey('CNB_COOKIE') as string
|
||||
try {
|
||||
cnbManager.addCNB({
|
||||
username: 'default',
|
||||
token: token,
|
||||
cookie: cookie,
|
||||
cnb: new CNB({ token: token, cookie: cookie })
|
||||
})
|
||||
} catch (error) {
|
||||
process.exit(1)
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
export const app = await useContextKey<App>('app', () => {
|
||||
return new App({})
|
||||
})
|
||||
|
||||
export const notCNBCheck = (ctx: any) => {
|
||||
const isCNB = useKey('CNB');
|
||||
if (!isCNB) {
|
||||
ctx.throw(400, '当前环境非 cnb-board 环境,无法获取内容');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
4
agent/cli.ts
Normal file
4
agent/cli.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { app } from './index.ts';
|
||||
import { parse } from '@kevisual/router/src/commander.ts';
|
||||
|
||||
parse({ app: app, description: 'CNB控制台命令行工具', parse: true })
|
||||
18
agent/main.ts
Normal file
18
agent/main.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
// import { RemoteApp } from '@kevisual/remote-app';
|
||||
import { app } from './index.ts'
|
||||
// import { QueryLoginNode } from '@kevisual/api/login-node';
|
||||
// const queryLoginNode = new QueryLoginNode({});
|
||||
// await queryLoginNode.init()
|
||||
// const token = await queryLoginNode.getToken();
|
||||
// app.createRouteList()
|
||||
// const remoteApp = new RemoteApp({
|
||||
// id: 'cnb-agent',
|
||||
// token: token,
|
||||
// url: 'https://kevisual.cn/ws/proxy',
|
||||
// app: app as any,
|
||||
// })
|
||||
// const isConnected = await remoteApp.isConnect();
|
||||
// if (isConnected) {
|
||||
// console.log('Remote app connected successfully');
|
||||
// remoteApp.listenProxy();
|
||||
// }
|
||||
141
agent/modules/cnb-manager.ts
Normal file
141
agent/modules/cnb-manager.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { Result } from '@kevisual/query';
|
||||
import { CNB } from '../../src/index.ts';
|
||||
import { useKey } from '@kevisual/context';
|
||||
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
||||
export const getConfig = async (opts: { token?: string }) => {
|
||||
const kevisualEnv = useKey('KEVISUAL_ENV')
|
||||
const isCNB = useKey('CNB');
|
||||
let isProduction = kevisualEnv !== 'development' || (isCNB && !kevisualEnv);
|
||||
const baseUrl = isProduction ? 'https://kevisual.cn/api/router' : 'https://kevisual.xiongxiao.me/api/router';
|
||||
const res = await fetch(baseUrl, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
path: 'config',
|
||||
key: 'get',
|
||||
data: {
|
||||
key: "cnb_center_config.json"
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${opts.token!}`
|
||||
},
|
||||
}).then(res => res.json());
|
||||
return res as Result<{
|
||||
id: string, key: 'cnb_center_config.json', data: {
|
||||
CNB_API_KEY: string,
|
||||
CNB_COOKIE: string
|
||||
}
|
||||
}>;
|
||||
}
|
||||
type CNBItem = {
|
||||
username: string,
|
||||
token: string,
|
||||
cookie?: string
|
||||
runAt?: number
|
||||
owner?: boolean
|
||||
cnb: CNB
|
||||
cnbAi: ReturnType<typeof createOpenAICompatible>
|
||||
}
|
||||
// const repo = useKey('CNB_REPO_SLUG_LOWERCASE') as string || 'kevision/kevision';
|
||||
// export const cnbAi = createOpenAICompatible({
|
||||
// baseURL: `https://api.cnb.cool/${repo}/-/ai/`,
|
||||
// name: 'custom-cnb',
|
||||
// apiKey: token,
|
||||
// });
|
||||
export class CNBManager {
|
||||
cnbMap: Map<string, CNBItem> = new Map()
|
||||
constructor() {
|
||||
setInterval(() => {
|
||||
this.clearExpiredCNB()
|
||||
}, 1000 * 60 * 30) // 每30分钟清理一次过期的 CNB 实例
|
||||
}
|
||||
getDefaultCNB() {
|
||||
const cnbItem = this.cnbMap.get('default')
|
||||
if (!cnbItem) {
|
||||
throw new Error('Default CNB not found')
|
||||
}
|
||||
return cnbItem
|
||||
}
|
||||
async getCNB(opts?: { username?: string, kevisualToken?: string }): Promise<CNBItem | null> {
|
||||
const username = opts?.username
|
||||
const cnbItem = this.cnbMap.get(username)
|
||||
if (cnbItem) {
|
||||
cnbItem.runAt = Date.now()
|
||||
return cnbItem
|
||||
}
|
||||
|
||||
const res = await getConfig({ token: opts?.kevisualToken })
|
||||
if (res.code === 200) {
|
||||
const cookie = res.data?.data?.CNB_COOKIE
|
||||
const token = res.data?.data?.CNB_API_KEY
|
||||
if (token) {
|
||||
return this.addCNB({ username, token, cookie })
|
||||
}
|
||||
} else {
|
||||
console.error('获取 CNB 配置失败', username, res)
|
||||
}
|
||||
return null
|
||||
}
|
||||
/**
|
||||
* 通过上下文获取 CNB 实例(直接返回 cnb 对象)
|
||||
* @param ctx
|
||||
* @returns CNB 实例
|
||||
*/
|
||||
async getContext(ctx: any) {
|
||||
const item = await this.getCNBItem(ctx)
|
||||
return item.cnb
|
||||
}
|
||||
async getCNBItem(ctx: any) {
|
||||
const tokenUser = ctx?.state?.tokenUser
|
||||
const username = tokenUser?.username
|
||||
if (!username) {
|
||||
ctx.throw(403, 'Unauthorized')
|
||||
}
|
||||
if (username === 'default') {
|
||||
return this.getDefaultCNB()
|
||||
}
|
||||
const kevisualToken = ctx.query?.token;
|
||||
const item = await this.getCNB({ username, kevisualToken });
|
||||
if (!item) {
|
||||
ctx.throw(400, '不存在的 CNB 配置项,请检查 登录 Token 是否正确,或添加 CNB 配置')
|
||||
}
|
||||
return item;
|
||||
}
|
||||
addCNB(opts: Partial<CNBItem>) {
|
||||
if (!opts.username || !opts.token) {
|
||||
throw new Error('username and token are required')
|
||||
}
|
||||
const exist = this.cnbMap.get(opts.username)
|
||||
if (exist) {
|
||||
exist.runAt = Date.now()
|
||||
return exist
|
||||
}
|
||||
const cnb = opts?.cnb || new CNB({ token: opts.token, cookie: opts.cookie });
|
||||
opts.cnb = cnb;
|
||||
opts.runAt = Date.now()
|
||||
const repoSlug = useKey('CNB_REPO_SLUG_LOWERCASE') as string || 'kevision/kevision';
|
||||
opts.cnbAi = createOpenAICompatible({
|
||||
baseURL: `https://api.cnb.cool/${repoSlug}/-/ai/`,
|
||||
name: `custom-cnb-${opts.username}`,
|
||||
apiKey: opts.token,
|
||||
})
|
||||
this.cnbMap.set(opts.username, opts as CNBItem)
|
||||
return opts as CNBItem
|
||||
}
|
||||
// 定期清理过期的 CNB 实例,默认过期时间为 1 小时
|
||||
clearExpiredCNB(expireTime = 1000 * 60 * 60) {
|
||||
const now = Date.now()
|
||||
for (const [username, item] of this.cnbMap.entries()) {
|
||||
if (username === 'default') {
|
||||
continue
|
||||
}
|
||||
if (item.runAt && now - item.runAt > expireTime) {
|
||||
this.cnbMap.delete(username)
|
||||
}
|
||||
}
|
||||
}
|
||||
clearUsername(username: string) {
|
||||
this.cnbMap.delete(username)
|
||||
}
|
||||
}
|
||||
125
agent/npc.ts
Normal file
125
agent/npc.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { app } from './index.ts';
|
||||
import { parse } from '@kevisual/router/src/commander.ts';
|
||||
|
||||
import { useIssueEnv, useCommentEnv, useRepoInfoEnv, IssueLabel } from '../src/index.ts'
|
||||
import { pick } from 'es-toolkit';
|
||||
import z from 'zod';
|
||||
import { useKey } from '@kevisual/context';
|
||||
|
||||
const writeToProcess = (message: string) => {
|
||||
if (process.send) {
|
||||
process.send(message);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
const getIssuesLabels = async () => {
|
||||
const issueEnv = useIssueEnv();
|
||||
const repoInfoEnv = useRepoInfoEnv();
|
||||
const issueId = issueEnv.issueId;
|
||||
const repoSlug = repoInfoEnv.repoSlug;
|
||||
if (!issueId || !repoSlug) {
|
||||
return [];
|
||||
}
|
||||
const res = await app.run({
|
||||
path: 'cnb',
|
||||
key: 'getIssue',
|
||||
payload: {
|
||||
repo: repoSlug,
|
||||
issueNumber: issueId
|
||||
}
|
||||
});
|
||||
if (res.code === 200) {
|
||||
const issueData = res.data as any;
|
||||
const labels = issueData.labels || [];
|
||||
return labels as IssueLabel[];
|
||||
}
|
||||
console.error('获取 Issue 详情失败', res);
|
||||
return []
|
||||
|
||||
}
|
||||
|
||||
const main = async ({ exit, question }: { exit: (code: number) => void, question?: string }) => {
|
||||
const repoInfoEnv = useRepoInfoEnv();
|
||||
const commentEnv = useCommentEnv();
|
||||
const issueEnv = useIssueEnv();
|
||||
const pickCommentEnv = pick(commentEnv, ['commentId', 'commentIdLabel']);
|
||||
const pickIssueEnv = pick(issueEnv, ['issueId', 'issueIdLabel', 'issueIid', 'issueIidLabel', 'issueTitle', 'issueTitleLabel', 'issueDescription', 'issueDescriptionLabel']);
|
||||
const pickRepoInfoEnv = pick(repoInfoEnv, ['repoId', 'repoIdLabel', 'repoName', 'repoNameLabel', 'repoSlug', 'repoSlugLabel']);
|
||||
// const issueLabels = issueEnv.issueLabels || [];
|
||||
const isComment = !!commentEnv.commentId;
|
||||
const envList = [
|
||||
...Object.entries(pickRepoInfoEnv).map(([key, value]) => `${key}: ${value}`),
|
||||
...Object.entries(issueEnv).map(([key, value]) => `${key}: ${value}`),
|
||||
...Object.entries(pickCommentEnv).map(([key, value]) => `${key}: ${value}`),
|
||||
]
|
||||
writeToProcess('当前环境变量:');
|
||||
const issueLabels = await getIssuesLabels();
|
||||
const issueLabelsNames = issueLabels.map(label => label.name) || [];
|
||||
envList.forEach(item => writeToProcess(item));
|
||||
if (!isComment && !issueLabelsNames.includes('Run')) {
|
||||
writeToProcess('当前 Issue 不包含 Run 标签,跳过执行');
|
||||
return exit(0);
|
||||
}
|
||||
const messages = [
|
||||
{
|
||||
role: 'system',
|
||||
content: `你是一个智能的代码助手, 根据用户提供的上下文信息,提供有用的建议和帮助, 如果用户的要求和执行工具不一致,请说出你不能这么做。并把最后的结果提交一个评论到对应的issue中,提交的内容必须不能包含 @ 提及。用户提供的上下文信息如下:`
|
||||
},
|
||||
{
|
||||
role: 'system',
|
||||
content: `相关变量:${JSON.stringify({ ...pickCommentEnv, ...pickIssueEnv, ...pickRepoInfoEnv })}`
|
||||
}, {
|
||||
role: 'user',
|
||||
content: question || commentEnv.commentBody || pickIssueEnv.issueDescription || '无'
|
||||
}
|
||||
]
|
||||
writeToProcess('输入消息:');
|
||||
writeToProcess(JSON.stringify(messages, null, 2));
|
||||
const result = await app.run({
|
||||
path: 'cnb',
|
||||
key: 'chat',
|
||||
payload: {
|
||||
messages
|
||||
}
|
||||
}, { appId: app.appId })
|
||||
if (result.code === 200) {
|
||||
let _message = result.data.message || []
|
||||
writeToProcess('执行完成')
|
||||
writeToProcess(JSON.stringify(_message, null, 2))
|
||||
exit(0);
|
||||
} else {
|
||||
writeToProcess(result.message || '执行错误')
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'npc',
|
||||
description: 'CNB智能助手,提供智能建议和帮助, 程序入口',
|
||||
metadata: {
|
||||
tags: ['notInNpcAgent'],
|
||||
args: {
|
||||
needExit: z.boolean().optional().describe('是否需要在执行完成后退出进程'),
|
||||
owner: z.string().optional().describe('用户名称')
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const needExit = ctx.args.needExit ?? true;
|
||||
const owner = ctx.args.owner || '';
|
||||
const exit = (code: number) => {
|
||||
if (needExit) {
|
||||
process.exit(code);
|
||||
}
|
||||
}
|
||||
const buildUserNickName = useKey('CNB_BUILD_USER_NICKNAME')
|
||||
let admins = owner.split(',').map(item => item.trim());
|
||||
if (owner && admins.includes(buildUserNickName)) {
|
||||
await main({ exit });
|
||||
} else {
|
||||
await main({ exit, question: `你是${owner}的专属助手,请生成一条评论,说明你不具备其他用户能访问的能力。同时你需要提示说明,fork当前仓库后,即可成为你的专属助手` });
|
||||
}
|
||||
}).addTo(app)
|
||||
|
||||
parse({ app: app, description: 'CNB控制台命令行工具', parse: true })
|
||||
@@ -2,5 +2,5 @@ import { app } from './index.ts';
|
||||
import { createRouterAgentPluginFn } from '@kevisual/router/opencode'
|
||||
|
||||
export const CnbPlugin = createRouterAgentPluginFn({
|
||||
router: app,
|
||||
router: app as any,
|
||||
})
|
||||
|
||||
66
agent/routes/build/docker.ts
Normal file
66
agent/routes/build/docker.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { app, notCNBCheck } from '../../app.ts'
|
||||
import { z } from 'zod'
|
||||
import { title } from 'process';
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'docker-sync',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tag: ['opencode'],
|
||||
skill: 'cnb-docker-sync',
|
||||
title: 'CNB Docker 镜像同步',
|
||||
args: {
|
||||
image: z.string().describe('Docker 同步的具体的镜像名称.'),
|
||||
toVersion: z.string().optional().describe('修改后的版本号.'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const { image, toVersion } = ctx.args;
|
||||
notCNBCheck(ctx);
|
||||
if (!image) {
|
||||
ctx.body = {
|
||||
message: '请提供 Docker 镜像名称.',
|
||||
data: null,
|
||||
}
|
||||
return;
|
||||
}
|
||||
const config = {
|
||||
registry: 'docker.cnb.cool/kevisual/dev-env',
|
||||
dockers: [{ image, toVersion }]
|
||||
}
|
||||
// docker tag ghcr.io/esm-dev/esm.sh:v137 docker.cnb.cool/kevisual/dev-env/esm.sh:v137
|
||||
// docker push docker.cnb.cool/kevisual/dev-env/esm.sh:v137
|
||||
const run = async () => {
|
||||
const dockers = config.dockers;
|
||||
for (const { image, toVersion } of dockers) {
|
||||
const imageName = image.split(':')[0].split('/').slice(-1)[0]
|
||||
const tag = image.split(':')[1]
|
||||
const newImage = `${config.registry}/${imageName}:${toVersion || tag}`
|
||||
// console.log(`docker tag ${image} ${newImage}`)
|
||||
// console.log(`docker push ${newImage}`)
|
||||
const shell = `docker pull ${image} && docker tag ${image} ${newImage} && docker push ${newImage}`
|
||||
console.log(shell)
|
||||
|
||||
console.log('\n-------------new---------------------------------\n')
|
||||
console.log(`${newImage}`)
|
||||
console.log('\n--------------------------------------------------\n')
|
||||
try {
|
||||
execSync(shell, { stdio: 'inherit' })
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
run().then(() => {
|
||||
// TODO: 通知用户同步完成
|
||||
});;
|
||||
ctx.body = {
|
||||
message: 'Docker 镜像同步任务中,请稍后在目标仓库查看.',
|
||||
data: {
|
||||
registry: config.registry,
|
||||
dockers: config.dockers,
|
||||
}
|
||||
}
|
||||
}).addTo(app);
|
||||
1
agent/routes/build/index.ts
Normal file
1
agent/routes/build/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
import './docker.ts'
|
||||
@@ -1,34 +1,31 @@
|
||||
import { createSkill } from '@kevisual/router'
|
||||
import { createSkill, tool } from '@kevisual/router'
|
||||
import { app } from '../../app.ts'
|
||||
import { tool } from '@opencode-ai/plugin/tool'
|
||||
|
||||
if (!app.hasRoute('call')) {
|
||||
// "调用 path: cnb key: list-repos"
|
||||
app.route({
|
||||
path: 'call',
|
||||
key: '',
|
||||
description: '调用',
|
||||
middleware: ['admin-auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'call-app',
|
||||
title: '调用app应用',
|
||||
summary: '调用router的应用, 参数path, key, payload',
|
||||
args: {
|
||||
path: tool.schema.string().describe('应用路径,例如 cnb'),
|
||||
key: tool.schema.string().optional().describe('应用key,例如 list-repos'),
|
||||
payload: tool.schema.object({}).optional().describe('调用参数'),
|
||||
}
|
||||
})
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
const { path, key } = ctx.query;
|
||||
console.log('call app', ctx.query);
|
||||
if (!path) {
|
||||
ctx.throw('路径path不能为空');
|
||||
}
|
||||
const res = await ctx.run({ path, key, payload: ctx.query.payload || {} });
|
||||
ctx.forward(res);
|
||||
}).addTo(app)
|
||||
}
|
||||
// "调用 path: cnb key: list-repos"
|
||||
app.route({
|
||||
path: 'call',
|
||||
key: '',
|
||||
description: '调用',
|
||||
middleware: ['auth-admin'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'call-app',
|
||||
title: '调用app应用',
|
||||
summary: '调用router的应用, 参数path, key, payload',
|
||||
args: {
|
||||
path: tool.schema.string().describe('应用路径,例如 cnb'),
|
||||
key: tool.schema.string().optional().describe('应用key,例如 list-repos'),
|
||||
payload: tool.schema.object({}).optional().describe('调用参数'),
|
||||
}
|
||||
})
|
||||
},
|
||||
}).define(async (ctx) => {
|
||||
const { path, key } = ctx.query;
|
||||
console.log('call app', ctx.query);
|
||||
if (!path) {
|
||||
ctx.throw('路径path不能为空');
|
||||
}
|
||||
const res = await ctx.run({ path, key, payload: ctx.query.payload || {} });
|
||||
ctx.forward(res);
|
||||
}).addTo(app, { overwrite: false })
|
||||
49
agent/routes/chat/chat.ts
Normal file
49
agent/routes/chat/chat.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { runAgent } from '@kevisual/ai/agent'
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
import z from 'zod';
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'chat',
|
||||
description: 'cnb智能对话接口',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
args: {
|
||||
question: z.string().describe('用户输入的问题'),
|
||||
messages: z.array(z.object({
|
||||
role: z.enum(['user', 'assistant']).describe('消息角色,user表示用户输入,assistant表示助手回复'),
|
||||
content: z.string().describe('消息内容')
|
||||
})).describe('对话消息列表,按照时间顺序排列,包含用户和助手的历史消息'),
|
||||
model: z.string().optional().describe('默认auto')
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
// notCNBCheck(ctx);
|
||||
if (!ctx.args.question && !ctx.args.messages) {
|
||||
ctx.throw(400, '缺少必要参数,必须提供question或messages');
|
||||
return;
|
||||
}
|
||||
const model = ctx.args?.model || 'auto'
|
||||
const item = await cnbManager.getCNBItem(ctx);
|
||||
const cnbAi = item.cnbAi;
|
||||
const messages = ctx.args.messages || [{
|
||||
role: 'user',
|
||||
content: ctx.args.question
|
||||
}]
|
||||
const routes = app.routes.filter(route => {
|
||||
const tags = route.metadata?.tags || [];
|
||||
if (tags.includes('notInNpcAgent')) {
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
});
|
||||
const result = await runAgent({
|
||||
app,
|
||||
messages: messages,
|
||||
routes,
|
||||
languageModel: cnbAi(model),
|
||||
token: '',
|
||||
// token: ctx.query.token as string,
|
||||
});
|
||||
ctx.body = result;
|
||||
}).addTo(app);
|
||||
414
agent/routes/cnb-board/cnb-dev-env.ts
Normal file
414
agent/routes/cnb-board/cnb-dev-env.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import { app, notCNBCheck } from '../../app.ts';
|
||||
import { useKey } from '@kevisual/context'
|
||||
import { getLiveMdContent } from './live/live-content.ts';
|
||||
import z from 'zod';
|
||||
|
||||
app.route({
|
||||
path: 'cnb_board',
|
||||
key: 'live',
|
||||
description: '获取cnb-board live的mdContent内容',
|
||||
middleware: ['auth'],
|
||||
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']
|
||||
}).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_name,group_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']
|
||||
}).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 image,如:alpine: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);
|
||||
0
agent/routes/cnb-board/common.ts
Normal file
0
agent/routes/cnb-board/common.ts
Normal file
39
agent/routes/cnb-board/index.ts
Normal file
39
agent/routes/cnb-board/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { app, notCNBCheck } 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'],
|
||||
}).define(async (ctx) => {
|
||||
if (notCNBCheck(ctx)) return;
|
||||
const cmd = 'kill 1';
|
||||
execCommand(cmd);
|
||||
}).addTo(app);
|
||||
341
agent/routes/cnb-board/live/live-content.ts
Normal file
341
agent/routes/cnb-board/live/live-content.ts
Normal 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
|
||||
}
|
||||
1
agent/routes/cnb-board/modules/index.ts
Normal file
1
agent/routes/cnb-board/modules/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './is-cnb.ts';
|
||||
6
agent/routes/cnb-board/modules/is-cnb.ts
Normal file
6
agent/routes/cnb-board/modules/is-cnb.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { useKey } from "@kevisual/context";
|
||||
|
||||
export const isCnb = () => {
|
||||
const CNB = useKey('CNB');
|
||||
return !!CNB;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createSkill } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { tool } from '@opencode-ai/plugin/tool';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'user-check',
|
||||
description: '检查用户登录状态,参数checkToken,default true; checkCookie, default false',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -15,8 +15,8 @@ app.route({
|
||||
title: 'CNB 登录验证信息',
|
||||
summary: '验证 CNB 登录信息是否有效',
|
||||
args: {
|
||||
checkToken: tool.schema.boolean().describe('是否检查 Token 的有效性').default(true),
|
||||
checkCookie: tool.schema.boolean().describe('是否检查 Cookie 的有效性').default(false),
|
||||
checkToken: z.boolean().describe('是否检查 Token 的有效性').default(true),
|
||||
checkCookie: z.boolean().describe('是否检查 Cookie 的有效性').default(false),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -24,6 +24,7 @@ app.route({
|
||||
const checkToken = ctx.query?.checkToken ?? true;
|
||||
const checkCookie = ctx.query?.checkCookie ?? false;
|
||||
let content = '';
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
if (checkToken) {
|
||||
const res = await cnb.user.getUser();
|
||||
if (res?.code !== 200) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
|
||||
// 设置 CNB_COOKIE环境变量和获取环境变量,用于界面操作定制模块功能
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'set-cnb-cookie',
|
||||
description: '设置当前cnb工作空间的cookie环境变量',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -19,6 +19,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const cookie = ctx.query?.cookie;
|
||||
if (!cookie) {
|
||||
ctx.body = { content: '请提供有效的cookie值' };
|
||||
@@ -33,7 +34,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'get-cnb-cookie',
|
||||
description: '获取当前cnb工作空间的cookie环境变量',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -43,6 +44,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const cookie = cnb.cookie || '未设置cookie环境变量';
|
||||
ctx.body = { content: `当前cnb工作空间的cookie环境变量为:${cookie}` };
|
||||
}).addTo(app);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { app, notCNBCheck } from '../../app.ts';
|
||||
|
||||
import { CNB_ENV } from "@/common/cnb-env.ts";
|
||||
|
||||
@@ -11,7 +11,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'get-cnb-port-uri',
|
||||
description: '获取当前cnb工作空间的port代理uri',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -19,12 +19,13 @@ app.route({
|
||||
title: '获取当前cnb工作空间的port代理uri',
|
||||
summary: '获取当前cnb工作空间的port代理uri,用于端口转发',
|
||||
args: {
|
||||
port: tool.schema.number().optional().describe('端口号,默认为4096'),
|
||||
port: tool.schema.number().optional().describe('端口号,默认为51515'),
|
||||
}
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const port = ctx.query?.port || 4096;
|
||||
if (notCNBCheck(ctx)) return;
|
||||
const port = ctx.query?.port || 51515;
|
||||
const uri = CNB_ENV?.CNB_VSCODE_PROXY_URI as string || '';
|
||||
const finalUri = uri.replace('{{port}}', port.toString());
|
||||
let content = `
|
||||
@@ -40,7 +41,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'get-cnb-vscode-uri',
|
||||
description: '获取当前cnb工作空间的vscode代理uri, 包括多种访问方式, 如web、vscode、codebuddy、cursor、ssh',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -58,6 +59,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
if (notCNBCheck(ctx)) return;
|
||||
const web = ctx.query?.web ?? false;
|
||||
const vscode = ctx.query?.vscode ?? true; // 默认true
|
||||
const codebuddy = ctx.query?.codebuddy ?? false;
|
||||
|
||||
49
agent/routes/cnb-manager/index.ts
Normal file
49
agent/routes/cnb-manager/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
|
||||
// "列出我的代码仓库,search blog"
|
||||
// 列出我的知识库的代码仓库
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'clear-me-manager',
|
||||
description: '清理我的cnb-manager记录',
|
||||
middleware: ['auth'],
|
||||
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state?.tokenUser;
|
||||
if (!tokenUser) {
|
||||
ctx.throw(401, '未授权');
|
||||
}
|
||||
const username = tokenUser.username;
|
||||
if (!username) {
|
||||
ctx.throw(400, '无效的用户信息');
|
||||
}
|
||||
if (username !== 'default') {
|
||||
cnbManager.clearUsername(username);
|
||||
}
|
||||
ctx.body = { content: '已清理cnb-manager记录' };
|
||||
}).addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'get-my-config',
|
||||
description: '获取我的cnb配置',
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
const tokenUser = ctx.state?.tokenUser;
|
||||
const username = tokenUser?.username;
|
||||
const token = ctx.query?.token;
|
||||
if (!username) {
|
||||
ctx.throw(400, '未授权');
|
||||
}
|
||||
if (!token) {
|
||||
ctx.throw(400, '缺少token参数');
|
||||
}
|
||||
const cnbItem = await cnbManager.getCNB({ username, kevisualToken: token });
|
||||
if (!cnbItem) {
|
||||
ctx.throw(404, '未找到cnb-manager记录');
|
||||
}
|
||||
ctx.body = {
|
||||
token: cnbItem.token,
|
||||
cookie: cnbItem.cookie,
|
||||
}
|
||||
}).addTo(app);
|
||||
6
agent/routes/cnb-manager/test.ts
Normal file
6
agent/routes/cnb-manager/test.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
const token = 'st_logh1b3ozq2resntxlnk4bccao0bon8e'
|
||||
import { CNBManager } from "../../modules/cnb-manager.ts"
|
||||
const cnbManager = new CNBManager()
|
||||
|
||||
const cnbItem = await cnbManager.getCNB({ username: 'root', kevisualToken: token });
|
||||
console.log('cnbItem', cnbItem)
|
||||
@@ -6,6 +6,11 @@ import './call/index.ts'
|
||||
import './cnb-env/index.ts'
|
||||
import './knowledge/index.ts'
|
||||
import './issues/index.ts'
|
||||
import './cnb-board/index.ts';
|
||||
import './share/index.ts';
|
||||
import './cnb-manager/index.ts';
|
||||
import './build/index.ts';
|
||||
import './chat/chat.ts';
|
||||
|
||||
/**
|
||||
* 验证上下文中的 App ID 是否与指定的 App ID 匹配
|
||||
@@ -25,25 +30,29 @@ const checkAppId = (ctx: any, appId: string) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!app.hasRoute('auth')) {
|
||||
app.route({
|
||||
id: 'auth',
|
||||
path: 'auth',
|
||||
}).define(async (ctx) => {
|
||||
// ctx.body = 'Auth Route';
|
||||
if (checkAppId(ctx, app.appId)) {
|
||||
return;
|
||||
app.route({
|
||||
id: 'auth',
|
||||
path: 'auth',
|
||||
}).define(async (ctx) => {
|
||||
// ctx.body = 'Auth Route';
|
||||
if (checkAppId(ctx, app.appId)) {
|
||||
ctx.state.tokenUser = {
|
||||
username: 'default',
|
||||
}
|
||||
}).addTo(app);
|
||||
return;
|
||||
}
|
||||
}).addTo(app, { overwrite: false });
|
||||
|
||||
app.route({
|
||||
id: 'admin-auth',
|
||||
path: 'admin-auth',
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
// ctx.body = 'Admin Auth Route';
|
||||
if (checkAppId(ctx, app.appId)) {
|
||||
return;
|
||||
app.route({
|
||||
id: 'auth-admin',
|
||||
path: 'auth-admin',
|
||||
middleware: ['auth'],
|
||||
}).define(async (ctx) => {
|
||||
// ctx.body = 'Admin Auth Route';
|
||||
if (checkAppId(ctx, app.appId)) {
|
||||
ctx.state.tokenUser = {
|
||||
username: 'default',
|
||||
}
|
||||
}).addTo(app);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}).addTo(app, { overwrite: false });
|
||||
177
agent/routes/issues/comments.ts
Normal file
177
agent/routes/issues/comments.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
import { useKey } from '@kevisual/context';
|
||||
|
||||
// 查询 Issue 评论列表
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'list-issue-comments',
|
||||
description: '查询 Issue 评论列表, 参数 repo, issueNumber, page, page_size',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'list-issue-comments',
|
||||
title: '查询 Issue 评论列表',
|
||||
args: {
|
||||
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
issueNumber: tool.schema.number().describe('Issue 编号'),
|
||||
page: tool.schema.number().optional().describe('分页页码,默认: 1'),
|
||||
page_size: tool.schema.number().optional().describe('分页每页大小,默认: 30'),
|
||||
},
|
||||
summary: '查询 Issue 评论列表',
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
const issueNumber = ctx.query?.issueNumber;
|
||||
const page = ctx.query?.page ? Number(ctx.query.page) : undefined;
|
||||
const page_size = ctx.query?.page_size ? Number(ctx.query.page_size) : undefined;
|
||||
|
||||
if (!repo) {
|
||||
ctx.throw(400, '缺少参数 repo');
|
||||
}
|
||||
if (!issueNumber) {
|
||||
ctx.throw(400, '缺少参数 issueNumber');
|
||||
}
|
||||
|
||||
const params: Record<string, any> = {};
|
||||
if (page) params.page = page;
|
||||
if (page_size) params.page_size = page_size;
|
||||
|
||||
const res = await cnb.issue.getCommentList(repo, issueNumber, params);
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
// 创建 Issue 评论
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'create-issue-comment',
|
||||
description: '创建 Issue 评论, 参数 repo, issueNumber, body',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'create-issue-comment',
|
||||
title: '创建 Issue 评论',
|
||||
args: {
|
||||
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
issueNumber: tool.schema.number().describe('Issue 编号'),
|
||||
body: tool.schema.string().describe('评论内容'),
|
||||
clearAt: tool.schema.boolean().optional().describe('是否清除评论内容中的 @ 提及,默认: true'),
|
||||
},
|
||||
summary: '创建 Issue 评论',
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
const issueNumber = ctx.query?.issueNumber;
|
||||
let body = ctx.query?.body;
|
||||
const clearAt = ctx.query?.clearAt ?? true;
|
||||
|
||||
if (!repo) {
|
||||
ctx.throw(400, '缺少参数 repo');
|
||||
}
|
||||
if (!issueNumber) {
|
||||
ctx.throw(400, '缺少参数 issueNumber');
|
||||
}
|
||||
if (!body) {
|
||||
ctx.throw(400, '缺少参数 body');
|
||||
}
|
||||
if (clearAt && body) {
|
||||
// 清除评论内容中的 @ 提及
|
||||
body = body.replace(/@/g, '');
|
||||
}
|
||||
|
||||
const res = await cnb.issue.createComment(repo, issueNumber, body);
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
// 获取 Issue 指定评论
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'get-issue-comment',
|
||||
description: '获取 Issue 指定评论, 参数 repo, issueNumber, commentId',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'get-issue-comment',
|
||||
title: '获取 Issue 评论',
|
||||
args: {
|
||||
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
issueNumber: tool.schema.number().describe('Issue 编号'),
|
||||
commentId: tool.schema.number().describe('评论 ID'),
|
||||
},
|
||||
summary: '获取 Issue 评论',
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
const issueNumber = ctx.query?.issueNumber;
|
||||
const commentId = ctx.query?.commentId;
|
||||
|
||||
if (!repo) {
|
||||
ctx.throw(400, '缺少参数 repo');
|
||||
}
|
||||
if (!issueNumber) {
|
||||
ctx.throw(400, '缺少参数 issueNumber');
|
||||
}
|
||||
if (!commentId) {
|
||||
ctx.throw(400, '缺少参数 commentId');
|
||||
}
|
||||
|
||||
const res = await cnb.issue.getComment(repo, issueNumber, commentId);
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
// 修改 Issue 评论
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'update-issue-comment',
|
||||
description: '修改 Issue 评论, 参数 repo, issueNumber, commentId, body',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'update-issue-comment',
|
||||
title: '修改 Issue 评论',
|
||||
args: {
|
||||
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
issueNumber: tool.schema.number().describe('Issue 编号'),
|
||||
commentId: tool.schema.number().describe('评论 ID'),
|
||||
body: tool.schema.string().describe('评论内容'),
|
||||
clearAt: tool.schema.boolean().optional().describe('是否清除评论内容中的 @ 提及,默认: true'),
|
||||
},
|
||||
summary: '修改 Issue 评论',
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
const issueNumber = ctx.query?.issueNumber;
|
||||
const commentId = ctx.query?.commentId;
|
||||
let body = ctx.query?.body;
|
||||
const clearAt = ctx.query?.clearAt ?? true;
|
||||
if (!repo) {
|
||||
ctx.throw(400, '缺少参数 repo');
|
||||
}
|
||||
if (!issueNumber) {
|
||||
ctx.throw(400, '缺少参数 issueNumber');
|
||||
}
|
||||
if (!commentId) {
|
||||
ctx.throw(400, '缺少参数 commentId');
|
||||
}
|
||||
if (!body) {
|
||||
ctx.throw(400, '缺少参数 body');
|
||||
}
|
||||
if (clearAt && body) {
|
||||
// 清除评论内容中的 @ 提及
|
||||
body = body.replace(/@/g, '');
|
||||
}
|
||||
const res = await cnb.issue.updateComment(repo, issueNumber, commentId, body);
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
@@ -1,2 +1,3 @@
|
||||
import './list.ts'
|
||||
import './issue.ts'
|
||||
import './comments.ts'
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
import { IssueItem } from '@/index.ts';
|
||||
|
||||
// 创建cnb issue, 仓库为 kevisual/kevisual 标题为 "自动化测试创建issue", 内容为 "这是通过API创建的issue,用于测试目的", body: "这是通过API创建的issue,用于测试目的"
|
||||
@@ -7,7 +7,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'create-issue',
|
||||
description: '创建 Issue, 参数 repo, title, body, assignees, labels, priority',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -25,6 +25,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const repo = ctx.query?.repo;
|
||||
const title = ctx.query?.title;
|
||||
const body = ctx.query?.body;
|
||||
@@ -51,7 +52,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'complete-issue',
|
||||
description: '完成 Issue, 参数 repo, issueNumber',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -66,6 +67,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const repo = ctx.query?.repo;
|
||||
const issueNumber = ctx.query?.issueNumber;
|
||||
const state = ctx.query?.state ?? 'closed';
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
import { useKey } from '@kevisual/context';
|
||||
|
||||
// 查询 Issue 列表 repo是 kevisual/kevisual
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'list-issues',
|
||||
description: '查询 Issue 列表, 参数 repo, state, keyword, labels, page, page_size 等',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'list-issues',
|
||||
title: '查询 Issue 列表',
|
||||
args: {
|
||||
repo: tool.schema.string().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
state: tool.schema.string().optional().describe('Issue 状态:open 或 closed'),
|
||||
keyword: tool.schema.string().optional().describe('问题搜索关键词'),
|
||||
labels: tool.schema.string().optional().describe('问题标签,多个用逗号分隔'),
|
||||
@@ -25,7 +26,8 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const repo = ctx.query?.repo;
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
const state = ctx.query?.state;
|
||||
const keyword = ctx.query?.keyword;
|
||||
const labels = ctx.query?.labels;
|
||||
@@ -48,3 +50,36 @@ app.route({
|
||||
const res = await cnb.issue.getList(repo, params);
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'getIssue',
|
||||
description: '获取 单个 Issue',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'getIssue',
|
||||
title: '获取 单个 Issue',
|
||||
args: {
|
||||
repo: tool.schema.string().optional().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
issueNumber: tool.schema.union([tool.schema.string(), tool.schema.number()]).describe('Issue 编号'),
|
||||
},
|
||||
summary: '获取 单个 Issue',
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
const issueNumber = ctx.query?.issueNumber;
|
||||
|
||||
if (!repo) {
|
||||
ctx.throw(400, '缺少参数 repo');
|
||||
}
|
||||
if (!issueNumber) {
|
||||
ctx.throw(400, '缺少参数 issueNumber');
|
||||
}
|
||||
|
||||
const res = await cnb.issue.getItem(repo, issueNumber);
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
import { CNBChat } from '@kevisual/ai/browser'
|
||||
import { useKey } from '@kevisual/context';
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +13,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'cnb-ai-chat',
|
||||
description: '调用cnb的知识库ai对话功能进行聊天',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -26,6 +27,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const question = ctx.query?.question;
|
||||
if (!question) {
|
||||
ctx.body = { content: '请提供有效的消息内容' };
|
||||
@@ -88,7 +90,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'cnb-rag-query',
|
||||
description: '调用cnb的知识库RAG查询功能进行问答',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -102,12 +104,13 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const question = ctx.query?.question;
|
||||
if (!question) {
|
||||
ctx.body = { content: '请提供有效的消息内容' };
|
||||
return;
|
||||
}
|
||||
let repo = ctx.query?.repo;
|
||||
let repo = ctx.query?.repo || useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
if (!repo) {
|
||||
// 如果未指定知识库仓库ID,则使用默认知识库
|
||||
const res = await cnb.repo.getRepoList({ flags: 'KnowledgeBase' });
|
||||
|
||||
12
agent/routes/opencode/index.ts
Normal file
12
agent/routes/opencode/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createOpencodeClient } from "@opencode-ai/sdk"
|
||||
|
||||
const client = await createOpencodeClient({
|
||||
// baseUrl: "https://yccb64t1z-100.cnb.run",
|
||||
// auth: async () => {
|
||||
// return 'cm9vdDozR0I2MDg5ZGpYOE5oMDFjM1FteE5DWDd0ZkI='
|
||||
// }
|
||||
baseUrl: "http://localhost:4096",
|
||||
})
|
||||
|
||||
const sessionList = await client.session.list()
|
||||
console.log(sessionList.data)
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createSkill } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { tool } from "@opencode-ai/plugin/tool"
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
|
||||
// "列出我的代码仓库,search blog"
|
||||
// 列出我的知识库的代码仓库
|
||||
@@ -8,7 +7,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'list-repos',
|
||||
description: '列出我的代码仓库',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -23,6 +22,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const search = ctx.query?.search;
|
||||
const pageSize = ctx.query?.pageSize || 9999;
|
||||
const flags = ctx.query?.flags;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { createSkill, Skill } from '@kevisual/router'
|
||||
import { tool } from "@opencode-ai/plugin/tool"
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
import { createSkill, Skill, tool } from '@kevisual/router'
|
||||
|
||||
// 创建一个仓库 kevisual/test-repo
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'create-repo',
|
||||
description: '创建代码仓库, 参数name, visibility, description',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -22,6 +21,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const name = ctx.query?.name;
|
||||
const visibility = ctx.query?.visibility ?? 'public';
|
||||
const description = ctx.query?.description ?? '';
|
||||
@@ -43,11 +43,32 @@ app.route({
|
||||
}
|
||||
}).addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'get-repo',
|
||||
description: '获取代码仓库详情, 参数name',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
args: {
|
||||
name: tool.schema.string().describe('代码仓库名称, 如 my-user/my-repo'),
|
||||
}
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const name = ctx.query?.name;
|
||||
|
||||
if (!name) {
|
||||
ctx.throw(400, '缺少参数 name');
|
||||
}
|
||||
const res = await cnb.repo.getRepo(name);
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'create-repo-file',
|
||||
description: '在代码仓库中创建文件, repoName, filePath, content, encoding',
|
||||
middleware: ['admin-auth'],
|
||||
description: '在代码仓库中创建文件, repoName, filePath, content, encoding。使用CNB_COOKIE进行鉴权',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -63,6 +84,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const repoName = ctx.query?.repoName;
|
||||
const filePath = ctx.query?.filePath;
|
||||
const content = ctx.query?.content;
|
||||
@@ -86,7 +108,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'delete-repo',
|
||||
description: '删除代码仓库, 参数name',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -99,12 +121,62 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const name = ctx.query?.name;
|
||||
|
||||
if (!name) {
|
||||
ctx.throw(400, '缺少参数 name');
|
||||
}
|
||||
try {
|
||||
const resCookie = await cnb.user.checkCookieValid()
|
||||
if (resCookie.code !== 200) {
|
||||
ctx.throw(401, 'Cookie 无效或已过期');
|
||||
}
|
||||
const res = await cnb.repo.deleteRepoCookie(name);
|
||||
ctx.forward(res);
|
||||
} catch (error) {
|
||||
ctx.code = 200
|
||||
ctx.body = { content: '已经删除' }
|
||||
}
|
||||
}).addTo(app);
|
||||
|
||||
const res = await cnb.repo.deleteRepo(name);
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'update-repo-info',
|
||||
description: '更新代码仓库信息, 参数name, description',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'update-repo-info',
|
||||
title: '更新代码仓库信息',
|
||||
args: {
|
||||
name: tool.schema.string().describe('代码仓库名称'),
|
||||
description: tool.schema.string().describe('代码仓库描述'),
|
||||
license: tool.schema.string().describe('代码仓库许可证类型,如 MIT').optional(),
|
||||
site: tool.schema.string().describe('代码仓库主页链接').optional(),
|
||||
topics: tool.schema.array(tool.schema.string()).describe('代码仓库话题标签列表').optional(),
|
||||
},
|
||||
summary: '更新代码仓库的信息',
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const name = ctx.query?.name;
|
||||
const description = ctx.query?.description;
|
||||
const license = ctx.query?.license;
|
||||
const site = ctx.query?.site;
|
||||
const topics = ctx.query?.topics;
|
||||
|
||||
if (!name) {
|
||||
ctx.throw(400, '缺少参数 name');
|
||||
}
|
||||
if (!description) {
|
||||
ctx.throw(400, '缺少参数 description');
|
||||
}
|
||||
|
||||
const res = await cnb.repo.updateRepoInfo(name, { description, license, site, topics });
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
|
||||
48
agent/routes/share/index.ts
Normal file
48
agent/routes/share/index.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useKey } from '@kevisual/context';
|
||||
import { app } 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);
|
||||
43
agent/routes/workspace/build.ts
Normal file
43
agent/routes/workspace/build.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
|
||||
import { app, cnbManager, notCNBCheck } from '../../app.ts';
|
||||
|
||||
// 启动工作空间
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'cloud-build',
|
||||
description: '云端构建,参数 event, repo, branch, ref, config, env',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
skill: 'cloud-build',
|
||||
title: '云端构建',
|
||||
summary: '在云端构建代码仓库,参数包括 event, repo, branch, ref, config, env',
|
||||
args: {
|
||||
env: tool.schema.any().optional().describe('构建环境变量,格式为 { "KEY": "VALUE" }'),
|
||||
event: tool.schema.string().optional().describe('触发事件类型,例如 api_trigger_event'),
|
||||
branch: tool.schema.string().optional().describe('分支名称,默认主分支'),
|
||||
config: tool.schema.string().describe('构建config文件内容,例如 cloudbuild.yaml对应的yml的内容'),
|
||||
repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
|
||||
},
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const repo = ctx.query?.repo;
|
||||
const branch = ctx.query?.branch || 'main';
|
||||
const config = ctx.query?.config;
|
||||
const event = ctx.query?.event || 'api_trigger_event';
|
||||
const env = ctx.query?.env ?? {};
|
||||
if (!repo) {
|
||||
ctx.throw(400, '缺少参数 repo');
|
||||
}
|
||||
const res = await cnb.build.startBuild(repo, {
|
||||
branch,
|
||||
config,
|
||||
event,
|
||||
env,
|
||||
});
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
@@ -1,15 +1,16 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { app, cnbManager, notCNBCheck } from '../../app.ts';
|
||||
import z from 'zod';
|
||||
import './skills.ts';
|
||||
import './keep.ts';
|
||||
import './build.ts';
|
||||
|
||||
// 启动工作空间
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'start-workspace',
|
||||
description: '启动开发工作空间, 参数 repo',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -24,6 +25,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const repo = ctx.query?.repo;
|
||||
const branch = ctx.query?.branch;
|
||||
const ref = ctx.query?.ref;
|
||||
@@ -42,7 +44,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'list-workspace',
|
||||
description: '获取cnb开发工作空间列表,可选参数 status=running 获取运行中的环境',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -59,13 +61,14 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const { status = 'running', page, pageSize, slug, branch } = ctx.query || {};
|
||||
const res = await cnb.workspace.list({
|
||||
status: status as 'running' | 'closed' | undefined,
|
||||
page: page ?? 1,
|
||||
pageSize: pageSize ?? 100,
|
||||
});
|
||||
ctx.forward({ code: 200, message: 'success', data: res });
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
// 获取工作空间详情
|
||||
@@ -73,7 +76,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'get-workspace',
|
||||
description: '获取工作空间详情,通过 repo 和 sn 获取',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -87,6 +90,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const repo = ctx.query?.repo;
|
||||
const sn = ctx.query?.sn;
|
||||
if (!repo) {
|
||||
@@ -96,7 +100,7 @@ app.route({
|
||||
ctx.throw(400, '缺少参数 sn');
|
||||
}
|
||||
const res = await cnb.workspace.getDetail(repo, sn);
|
||||
ctx.forward({ code: 200, message: 'success', data: res });
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
// 删除工作空间
|
||||
@@ -104,7 +108,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'delete-workspace',
|
||||
description: '删除工作空间,通过 pipelineId 或 sn',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -119,6 +123,7 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const pipelineId = ctx.query?.pipelineId;
|
||||
const sn = ctx.query?.sn;
|
||||
const sns = ctx.query?.sns;
|
||||
@@ -143,7 +148,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'stop-workspace',
|
||||
description: '停止工作空间,通过 pipelineId 或 sn',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -157,12 +162,14 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const pipelineId = ctx.query?.pipelineId;
|
||||
const sn = ctx.query?.sn;
|
||||
if (!pipelineId && !sn) {
|
||||
ctx.throw(400, 'pipelineId 和 sn 必须提供其中一个');
|
||||
}
|
||||
const res = await cnb.workspace.stopWorkspace({ pipelineId, sn });
|
||||
ctx.forward({ code: 200, message: 'success', data: res });
|
||||
ctx.forward(res);
|
||||
}).addTo(app);
|
||||
|
||||
|
||||
|
||||
@@ -1,214 +1,101 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { nanoid } from 'nanoid';
|
||||
import dayjs from 'dayjs';
|
||||
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>();
|
||||
import { tool } from '@kevisual/router';
|
||||
import { app, cnbManager, notCNBCheck } from '../../app.ts';
|
||||
import { addKeepAliveData, KeepAliveData, removeKeepAliveData, createLiveData } from '../../../src/workspace/keep-file-live.ts';
|
||||
import { useKey } from '@kevisual/context';
|
||||
|
||||
// 保持工作空间存活技能
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'keep-workspace-alive',
|
||||
description: '保持工作空间存活技能,参数wsUrl:工作空间访问URL,cookie:访问工作空间所需的cookie',
|
||||
middleware: ['admin-auth'],
|
||||
description: '保持工作空间存活技能,参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: [],
|
||||
...({
|
||||
args: {
|
||||
wsUrl: tool.schema.string().describe('工作空间的访问URL'),
|
||||
cookie: tool.schema.string().describe('访问工作空间所需的cookie')
|
||||
repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
|
||||
pipelineId: tool.schema.string().describe('流水线ID,例如 cnb-708-1ji9sog7o-001'),
|
||||
}
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const wsUrl = ctx.query?.wsUrl as string;
|
||||
const cookie = ctx.query?.cookie as string;
|
||||
if (!wsUrl) {
|
||||
ctx.throw(400, '缺少工作空间访问URL参数');
|
||||
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const repo = ctx.query?.repo as string;
|
||||
const pipelineId = ctx.query?.pipelineId as string;
|
||||
if (notCNBCheck(ctx)) return;
|
||||
if (!repo || !pipelineId) {
|
||||
ctx.throw(400, '缺少参数 repo 或 pipelineId');
|
||||
}
|
||||
if (!cookie) {
|
||||
ctx.throw(400, '缺少访问工作空间所需的cookie参数');
|
||||
const validCookie = await cnb.user.checkCookieValid()
|
||||
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);
|
||||
if (existing) {
|
||||
ctx.body = { message: `工作空间 ${wsUrl} 的保持存活任务已在运行中`, id: (existing as AliveInfo).id };
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 添加保活数据
|
||||
const liveData = createLiveData({
|
||||
repo,
|
||||
pipelineId,
|
||||
cookie: res.data.cookie
|
||||
});
|
||||
addKeepAliveData(liveData);
|
||||
console.log('已添加 keep-alive 数据');
|
||||
|
||||
const id = nanoid(6).toLowerCase();
|
||||
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 };
|
||||
ctx.body = { content: `已启动保持工作空间 ${repo}/${pipelineId} 存活的任务`, data: liveData };
|
||||
}).addTo(app);
|
||||
|
||||
// 停止保持工作空间存活技能
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'stop-keep-workspace-alive',
|
||||
description: '停止保持工作空间存活技能, 参数wsUrl:工作空间访问URL或者id',
|
||||
middleware: ['admin-auth'],
|
||||
description: '停止保持工作空间存活技能, 参数repo:代码仓库路径,例如 user/repo,pipelineId:流水线ID,例如 cnb-708-1ji9sog7o-001',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: [],
|
||||
...({
|
||||
args: {
|
||||
wsUrl: tool.schema.string().optional().describe('工作空间的访问URL'),
|
||||
id: tool.schema.string().optional().describe('保持存活任务的唯一标识符'),
|
||||
repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
|
||||
pipelineId: tool.schema.string().describe('流水线ID,例如 cnb-708-1ji9sog7o-001'),
|
||||
}
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const wsUrl = ctx.query?.wsUrl as string;
|
||||
const id = ctx.query?.id as string;
|
||||
if (!wsUrl && !id) {
|
||||
ctx.throw(400, '缺少工作空间访问URL参数或唯一标识符');
|
||||
}
|
||||
if (notCNBCheck(ctx)) return;
|
||||
const repo = ctx.query?.repo as string;
|
||||
const pipelineId = ctx.query?.pipelineId as string;
|
||||
|
||||
let targetId: string | undefined;
|
||||
let wsUrlFound: string | undefined;
|
||||
|
||||
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: `没有找到对应的工作空间保持存活任务` };
|
||||
if (!repo || !pipelineId) {
|
||||
ctx.throw(400, '缺少参数 repo 或 pipelineId');
|
||||
}
|
||||
removeKeepAliveData(repo, pipelineId);
|
||||
ctx.body = { content: `已停止保持工作空间 ${repo}/${pipelineId} 存活的任务` };
|
||||
}).addTo(app);
|
||||
|
||||
|
||||
|
||||
|
||||
app.route({
|
||||
path: 'cnb',
|
||||
key: 'reset-keep-workspace-alive',
|
||||
description: '对存活的工作空间,startTime进行重置',
|
||||
middleware: ['admin-auth'],
|
||||
key: 'keep-alive-current-workspace',
|
||||
description: '保持当前工作空间存活技能',
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: [],
|
||||
tags: ['opencode'],
|
||||
skill: 'keep-alive-current-workspace',
|
||||
title: '保持当前工作空间存活',
|
||||
summary: '保持当前工作空间存活,防止被关闭或释放资源',
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const now = Date.now();
|
||||
for (const info of keepAliveMap.values()) {
|
||||
info.startTime = now;
|
||||
if (notCNBCheck(ctx)) return;
|
||||
const pipelineId = useKey('CNB_PIPELINE_ID');
|
||||
const repo = useKey('CNB_REPO_SLUG_LOWERCASE');
|
||||
if (!pipelineId || !repo) {
|
||||
ctx.throw(400, '当前环境缺少 CNB_PIPELINE_ID 或 CNB_REPO_SLUG_LOWERCASE 环境变量,无法保持工作空间存活');
|
||||
}
|
||||
ctx.body = { content: `已重置所有存活工作空间的开始时间` };
|
||||
const res = await app.run({ path: 'cnb', key: 'keep-workspace-alive', payload: { repo, pipelineId } }, ctx);
|
||||
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);
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createSkill, tool } from '@kevisual/router';
|
||||
import { app, cnb } from '../../app.ts';
|
||||
import { app, cnbManager } from '../../app.ts';
|
||||
|
||||
// 批量删除已停止的cnb工作空间
|
||||
// app.route({
|
||||
@@ -35,7 +35,7 @@ app.route({
|
||||
path: 'cnb',
|
||||
key: 'clean-closed-workspace',
|
||||
description: '批量删除已停止的cnb工作空间',
|
||||
middleware: ['admin-auth'],
|
||||
middleware: ['auth'],
|
||||
metadata: {
|
||||
tags: ['opencode'],
|
||||
...createSkill({
|
||||
@@ -45,7 +45,8 @@ app.route({
|
||||
})
|
||||
}
|
||||
}).define(async (ctx) => {
|
||||
const closedWorkspaces = await cnb.workspace.list({ status: 'closed' });
|
||||
const cnb = await cnbManager.getContext(ctx);
|
||||
const closedWorkspaces = await cnb.workspace.list({ status: 'closed', pageSize: 100 });
|
||||
if (closedWorkspaces.code !== 200) {
|
||||
ctx.throw(500, '获取已关闭工作空间列表失败');
|
||||
}
|
||||
|
||||
2
bin/index.js
Executable file
2
bin/index.js
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bun
|
||||
import '../dist/cli.js';
|
||||
2
bin/npc.js
Executable file
2
bin/npc.js
Executable file
@@ -0,0 +1,2 @@
|
||||
#!/usr/bin/env bun
|
||||
import '../dist/npc.js';
|
||||
@@ -1,4 +1,7 @@
|
||||
import { buildWithBun } from '@kevisual/code-builder'
|
||||
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: 'npc', entry: 'agent/npc.ts', dts: true });
|
||||
await buildWithBun({ naming: 'cli', entry: 'agent/cli.ts', dts: true, target: 'node' });
|
||||
302
bun.lock
Normal file
302
bun.lock
Normal file
@@ -0,0 +1,302 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@kevisual/cnb",
|
||||
"dependencies": {
|
||||
"@kevisual/query": "^0.0.53",
|
||||
"@kevisual/router": "^0.1.2",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"@opencode-ai/sdk": "^1.2.27",
|
||||
"es-toolkit": "^1.45.1",
|
||||
"nanoid": "^5.1.7",
|
||||
"unstorage": "^1.17.4",
|
||||
"ws": "npm:@kevisual/ws",
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-sdk/openai-compatible": "^2.0.35",
|
||||
"@kevisual/ai": "^0.0.28",
|
||||
"@kevisual/api": "^0.0.64",
|
||||
"@kevisual/code-builder": "^0.0.6",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/dts": "^0.0.4",
|
||||
"@kevisual/remote-app": "^0.0.7",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@opencode-ai/plugin": "^1.2.27",
|
||||
"@types/bun": "^1.3.10",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"ai": "^6.0.116",
|
||||
"commander": "^14.0.3",
|
||||
"dayjs": "^1.11.20",
|
||||
"dotenv": "^17.3.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
"overrides": {
|
||||
"zod": "^4.3.6",
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.58", "https://registry.npmmirror.com/@ai-sdk/anthropic/-/anthropic-3.0.58.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/53SACgmVukO4bkms4dpxpRlYhW8Ct6QZRe6sj1Pi5H00hYhxIrqfiLbZBGxkdRvjsBQeP/4TVGsXgH5rQeb8Q=="],
|
||||
|
||||
"@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.66", "https://registry.npmmirror.com/@ai-sdk/gateway/-/gateway-3.0.66.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-SIQ0YY0iMuv+07HLsZ+bB990zUJ6S4ujORAh+Jv1V2KGNn73qQKnGO0JBk+w+Res8YqOFSycwDoWcFlQrVxS4A=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@3.0.41", "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-3.0.41.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IZ42A+FO+vuEQCVNqlnAPYQnnUpUfdJIwn1BEDOBywiEHa23fw7PahxVtlX9zm3/zMvTW4JKPzWyvAgDu+SQ2A=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@2.0.35", "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-2.0.35.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-g3wA57IAQFb+3j4YuFndgkUdXyRETZVvbfAWM+UX7bZSxA3xjes0v3XKgIdKdekPtDGsh4ZX2byHD0gJIMPfiA=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-3.0.8.tgz", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.19", "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-4.0.19.tgz", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.6" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3eG55CrSWCu2SXlqq2QCsFjo3+E7+Gmg7i/oRVoSZzIodTuDSfLb3MRje67xE9RFea73Zao7Lm4mADIfUETKGg=="],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "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", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@kevisual/ai": ["@kevisual/ai@0.0.28", "https://registry.npmmirror.com/@kevisual/ai/-/ai-0.0.28.tgz", { "dependencies": { "@ai-sdk/anthropic": "^3.0.58", "@ai-sdk/openai": "^3.0.41", "@ai-sdk/openai-compatible": "^2.0.35", "@kevisual/js-filter": "^0.0.6", "@kevisual/logger": "^0.0.4", "@kevisual/permission": "^0.0.4", "@kevisual/query": "^0.0.53", "ai": "^6.0.116", "zod": "^4.3.6" } }, "sha512-GLwCNXfopDvOj+hEZwEIwOV2/3VGd+TCPgBClaYuAv30KzhgehlCW05HPjBducSg+uPcdKacEzZsecHjo5fMUQ=="],
|
||||
|
||||
"@kevisual/api": ["@kevisual/api@0.0.64", "http://mirrors.tencent.com/npm/@kevisual/api/-/api-0.0.64.tgz", { "dependencies": { "@kevisual/context": "^0.0.8", "@kevisual/js-filter": "^0.0.6", "@kevisual/load": "^0.0.6", "@paralleldrive/cuid2": "^3.3.0", "es-toolkit": "^1.45.1", "eventemitter3": "^5.0.4", "fuse.js": "^7.1.0", "nanoid": "^5.1.6", "path-browserify-esm": "^1.0.6", "sonner": "^2.0.7", "spark-md5": "^3.0.2", "zustand": "^5.0.11" } }, "sha512-y7wP8ucvi/rflVGd6uJpvuEUTwI7wMef8+ITQzv4flg7a2pwWZYe/DT0TOyaqDAqKOTlXaVIdBeI15jXuUxIIg=="],
|
||||
|
||||
"@kevisual/code-builder": ["@kevisual/code-builder@0.0.6", "https://registry.npmmirror.com/@kevisual/code-builder/-/code-builder-0.0.6.tgz", { "bin": { "code-builder": "bin/code.js", "builder": "bin/code.js" } }, "sha512-0aqATB31/yw4k4s5/xKnfr4DKbUnx8e3Z3BmKbiXTrc+CqWiWTdlGe9bKI9dZ2Df+xNp6g11W4xM2NICNyyCCw=="],
|
||||
|
||||
"@kevisual/context": ["@kevisual/context@0.0.8", "https://registry.npmmirror.com/@kevisual/context/-/context-0.0.8.tgz", {}, "sha512-DTJpyHI34NE76B7g6f+QlIqiCCyqI2qkBMQE736dzeRDGxOjnbe2iQY9W+Rt2PE6kmymM3qyOmSfNovyWyWrkA=="],
|
||||
|
||||
"@kevisual/dts": ["@kevisual/dts@0.0.4", "https://registry.npmmirror.com/@kevisual/dts/-/dts-0.0.4.tgz", { "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/js-filter": ["@kevisual/js-filter@0.0.6", "https://registry.npmmirror.com/@kevisual/js-filter/-/js-filter-0.0.6.tgz", {}, "sha512-FcbOsmS1inhwrfgXMM/XLFTGTHUxBCss32JEMYdEFWQDYCar5rN8cxD1W8FuKDTVRlpA+zBpQ/BE6XT4UaeljA=="],
|
||||
|
||||
"@kevisual/load": ["@kevisual/load@0.0.6", "https://registry.npmmirror.com/@kevisual/load/-/load-0.0.6.tgz", { "dependencies": { "eventemitter3": "^5.0.1" } }, "sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA=="],
|
||||
|
||||
"@kevisual/logger": ["@kevisual/logger@0.0.4", "https://registry.npmmirror.com/@kevisual/logger/-/logger-0.0.4.tgz", {}, "sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw=="],
|
||||
|
||||
"@kevisual/permission": ["@kevisual/permission@0.0.4", "https://registry.npmmirror.com/@kevisual/permission/-/permission-0.0.4.tgz", {}, "sha512-zwBYPnT/z21W4q2wkklJrxvoYBYWG/+a3iXFDKqXQAnDOcxm/SU1f1N6FQb9KxGKl36/fclVlhxlxqszvKCenQ=="],
|
||||
|
||||
"@kevisual/query": ["@kevisual/query@0.0.53", "https://registry.npmmirror.com/@kevisual/query/-/query-0.0.53.tgz", {}, "sha512-PAhpCLBr0emz0lGNlTVHMbJiC5wrtGLbInPddRzgKE35fiyNt+SWSsUWABiD0DeNrLN/OxWyAFobt880Z/e5MQ=="],
|
||||
|
||||
"@kevisual/remote-app": ["@kevisual/remote-app@0.0.7", "http://mirrors.tencent.com/npm/@kevisual/remote-app/-/remote-app-0.0.7.tgz", {}, "sha512-d0P8uyxoMnmyT8x1J9XC9ecDBbqW+jOP0ZM5fCgQRDUhWw35V/MnbCD4hNG4b6EmvoiS6a/PBC7RC5JGm3wpCg=="],
|
||||
|
||||
"@kevisual/router": ["@kevisual/router@0.1.2", "http://mirrors.tencent.com/npm/@kevisual/router/-/router-0.1.2.tgz", { "dependencies": { "crypto-js": "^4.2.0", "es-toolkit": "^1.45.1", "zod": "^4.3.6" } }, "sha512-GLLJMZXtv3nUQKJXyE+vJFiCuntpuBc0VT8hMQyGvxwzqN8BY8rX6yS9TNDWhSXLwLYed8BJtG+azEONDjFCpw=="],
|
||||
|
||||
"@kevisual/types": ["@kevisual/types@0.0.12", "https://registry.npmmirror.com/@kevisual/types/-/types-0.0.12.tgz", {}, "sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q=="],
|
||||
|
||||
"@kevisual/use-config": ["@kevisual/use-config@1.0.30", "https://registry.npmmirror.com/@kevisual/use-config/-/use-config-1.0.30.tgz", { "dependencies": { "@kevisual/load": "^0.0.6" }, "peerDependencies": { "dotenv": "^17" } }, "sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.0.1", "https://registry.npmmirror.com/@noble/hashes/-/hashes-2.0.1.tgz", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
|
||||
|
||||
"@opencode-ai/plugin": ["@opencode-ai/plugin@1.2.27", "http://mirrors.tencent.com/npm/@opencode-ai/plugin/-/plugin-1.2.27.tgz", { "dependencies": { "@opencode-ai/sdk": "1.2.27", "zod": "4.1.8" } }, "sha512-h+8Bw9v9nghMg7T+SUCTzxlIhOrsTqXW7U0HVLGQST5DjbN7uyCUM51roZWZ8LRjGxzbzFhvPnY1bj8i+ioZyw=="],
|
||||
|
||||
"@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.27", "http://mirrors.tencent.com/npm/@opencode-ai/sdk/-/sdk-1.2.27.tgz", {}, "sha512-Wk0o/I+Fo+wE3zgvlJDs8Fb67KlKqX0PrV8dK5adSDkANq6r4Z25zXJg2iOir+a8ntg3rAcpel1OY4FV/TwRUA=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "https://registry.npmmirror.com/@opentelemetry/api/-/api-1.9.0.tgz", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@paralleldrive/cuid2": ["@paralleldrive/cuid2@3.3.0", "https://registry.npmmirror.com/@paralleldrive/cuid2/-/cuid2-3.3.0.tgz", { "dependencies": { "@noble/hashes": "^2.0.1", "bignumber.js": "^9.3.1", "error-causes": "^3.0.2" }, "bin": { "cuid2": "bin/cuid2.js" } }, "sha512-OqiFvSOF0dBSesELYY2CAMa4YINvlLpvKOz/rv6NeZEqiyttlHgv98Juwv4Ch+GrEV7IZ8jfI2VcEoYUjXXCjw=="],
|
||||
|
||||
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.0", "https://registry.npmmirror.com/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", { "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", "https://registry.npmmirror.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", { "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", "https://registry.npmmirror.com/@rollup/plugin-typescript/-/plugin-typescript-12.3.0.tgz", { "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", "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", { "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", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.3.10", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.10.tgz", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@25.5.0", "http://mirrors.tencent.com/npm/@types/node/-/node-25.5.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||
|
||||
"@types/ws": ["@types/ws@8.18.1", "https://registry.npmmirror.com/@types/ws/-/ws-8.18.1.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
|
||||
|
||||
"@vercel/oidc": ["@vercel/oidc@3.1.0", "https://registry.npmmirror.com/@vercel/oidc/-/oidc-3.1.0.tgz", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="],
|
||||
|
||||
"ai": ["ai@6.0.116", "https://registry.npmmirror.com/ai/-/ai-6.0.116.tgz", { "dependencies": { "@ai-sdk/gateway": "3.0.66", "@ai-sdk/provider": "3.0.8", "@ai-sdk/provider-utils": "4.0.19", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7yM+cTmyRLeNIXwt4Vj+mrrJgVQ9RMIW5WO0ydoLoYkewIvsMcvUmqS4j2RJTUXaF1HphwmSKUMQ/HypNRGOmA=="],
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "https://registry.npmmirror.com/bignumber.js/-/bignumber.js-9.3.1.tgz", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.3.10", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.10.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "https://registry.npmmirror.com/chokidar/-/chokidar-5.0.0.tgz", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"commander": ["commander@14.0.3", "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
|
||||
"commondir": ["commondir@1.0.1", "https://registry.npmmirror.com/commondir/-/commondir-1.0.1.tgz", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||
|
||||
"cookie-es": ["cookie-es@1.2.2", "https://registry.npmmirror.com/cookie-es/-/cookie-es-1.2.2.tgz", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="],
|
||||
|
||||
"crossws": ["crossws@0.3.5", "https://registry.npmmirror.com/crossws/-/crossws-0.3.5.tgz", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA=="],
|
||||
|
||||
"crypto-js": ["crypto-js@4.2.0", "http://mirrors.tencent.com/npm/crypto-js/-/crypto-js-4.2.0.tgz", {}, "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="],
|
||||
|
||||
"dayjs": ["dayjs@1.11.20", "http://mirrors.tencent.com/npm/dayjs/-/dayjs-1.11.20.tgz", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="],
|
||||
|
||||
"deepmerge": ["deepmerge@4.3.1", "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"dotenv": ["dotenv@17.3.1", "https://registry.npmmirror.com/dotenv/-/dotenv-17.3.1.tgz", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="],
|
||||
|
||||
"error-causes": ["error-causes@3.0.2", "https://registry.npmmirror.com/error-causes/-/error-causes-3.0.2.tgz", {}, "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw=="],
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.45.1", "https://registry.npmmirror.com/es-toolkit/-/es-toolkit-1.45.1.tgz", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"eventemitter3": ["eventemitter3@5.0.4", "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "https://registry.npmmirror.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"fuse.js": ["fuse.js@7.1.0", "https://registry.npmmirror.com/fuse.js/-/fuse.js-7.1.0.tgz", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="],
|
||||
|
||||
"h3": ["h3@1.15.5", "https://registry.npmmirror.com/h3/-/h3-1.15.5.tgz", { "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", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"iron-webcrypto": ["iron-webcrypto@1.2.1", "https://registry.npmmirror.com/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", {}, "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-module": ["is-module@1.0.0", "https://registry.npmmirror.com/is-module/-/is-module-1.0.0.tgz", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||
|
||||
"is-reference": ["is-reference@1.2.1", "https://registry.npmmirror.com/is-reference/-/is-reference-1.2.1.tgz", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"json-schema": ["json-schema@0.4.0", "https://registry.npmmirror.com/json-schema/-/json-schema-0.4.0.tgz", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.2.6", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.2.6.tgz", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"nanoid": ["nanoid@5.1.7", "http://mirrors.tencent.com/npm/nanoid/-/nanoid-5.1.7.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-ua3NDgISf6jdwezAheMOk4mbE1LXjm1DfMUDMuJf4AqxLFK3ccGpgWizwa5YV7Yz9EpXwEaWoRXSb/BnV0t5dQ=="],
|
||||
|
||||
"node-fetch-native": ["node-fetch-native@1.6.7", "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="],
|
||||
|
||||
"node-mock-http": ["node-mock-http@1.0.4", "https://registry.npmmirror.com/node-mock-http/-/node-mock-http-1.0.4.tgz", {}, "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"ofetch": ["ofetch@1.5.1", "https://registry.npmmirror.com/ofetch/-/ofetch-1.5.1.tgz", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="],
|
||||
|
||||
"path-browserify-esm": ["path-browserify-esm@1.0.6", "https://registry.npmmirror.com/path-browserify-esm/-/path-browserify-esm-1.0.6.tgz", {}, "sha512-9nUwYvvu/yq1PYrUyYCihNWmpzacaRYF6gGbjLWErrZ4MRDWyfPN7RpE8E7tsw8eqBU/rr7mcoTXbS+Vih8uUA=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.3", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
|
||||
|
||||
"radix3": ["radix3@1.1.2", "https://registry.npmmirror.com/radix3/-/radix3-1.1.2.tgz", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="],
|
||||
|
||||
"react": ["react@19.2.4", "https://registry.npmmirror.com/react/-/react-19.2.4.tgz", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.4.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "https://registry.npmmirror.com/readdirp/-/readdirp-5.0.0.tgz", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"resolve": ["resolve@1.22.11", "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", { "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", "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz", { "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", "https://registry.npmmirror.com/rollup-plugin-dts/-/rollup-plugin-dts-6.3.0.tgz", { "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=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"sonner": ["sonner@2.0.7", "https://registry.npmmirror.com/sonner/-/sonner-2.0.7.tgz", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
|
||||
|
||||
"spark-md5": ["spark-md5@3.0.2", "https://registry.npmmirror.com/spark-md5/-/spark-md5-3.0.2.tgz", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"uncrypto": ["uncrypto@0.1.3", "https://registry.npmmirror.com/uncrypto/-/uncrypto-0.1.3.tgz", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
|
||||
|
||||
"undici-types": ["undici-types@7.18.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unstorage": ["unstorage@1.17.4", "https://registry.npmmirror.com/unstorage/-/unstorage-1.17.4.tgz", { "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", "https://registry.npmmirror.com/@kevisual/ws/-/ws-8.19.0.tgz", {}, "sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw=="],
|
||||
|
||||
"zod": ["zod@4.3.6", "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
||||
|
||||
"zustand": ["zustand@5.0.11", "https://registry.npmmirror.com/zustand/-/zustand-5.0.11.tgz", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg=="],
|
||||
|
||||
"@types/ws/@types/node": ["@types/node@25.3.0", "https://registry.npmmirror.com/@types/node/-/node-25.3.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"bun-types/@types/node": ["@types/node@25.3.5", "https://registry.npmmirror.com/@types/node/-/node-25.3.5.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
|
||||
}
|
||||
}
|
||||
13
keep.ts
Normal file
13
keep.ts
Normal 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,
|
||||
});
|
||||
57
package.json
57
package.json
@@ -1,34 +1,53 @@
|
||||
{
|
||||
"name": "@kevisual/cnb",
|
||||
"version": "0.0.19",
|
||||
"version": "0.0.51",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"basename": "/root/cnb",
|
||||
"app": {
|
||||
"type": "system-app",
|
||||
"entry": "./dist/routes.js",
|
||||
"engine": "bun"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"compile": "bun build --compile --minify agent/commander.ts --outfile=./dist/cnb ",
|
||||
"pub": "ev pack -u -m false -c -p"
|
||||
},
|
||||
"keywords": [],
|
||||
"bin": {
|
||||
"cnb": "bin/index.js",
|
||||
"cloud": "bin/index.js",
|
||||
"cloud-npc": "bin/npc.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"mod.ts",
|
||||
"agent"
|
||||
],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@kevisual/ai": "^0.0.24",
|
||||
"@ai-sdk/openai-compatible": "^2.0.35",
|
||||
"@kevisual/ai": "^0.0.28",
|
||||
"@kevisual/api": "^0.0.64",
|
||||
"@kevisual/code-builder": "^0.0.6",
|
||||
"@kevisual/context": "^0.0.4",
|
||||
"@kevisual/context": "^0.0.8",
|
||||
"@kevisual/dts": "^0.0.4",
|
||||
"@kevisual/remote-app": "^0.0.7",
|
||||
"@kevisual/types": "^0.0.12",
|
||||
"@opencode-ai/plugin": "^1.1.51",
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/node": "^25.2.1",
|
||||
"@opencode-ai/plugin": "^1.2.27",
|
||||
"@types/bun": "^1.3.10",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"dayjs": "^1.11.19",
|
||||
"dotenv": "^17.2.3"
|
||||
"ai": "^6.0.116",
|
||||
"commander": "^14.0.3",
|
||||
"dayjs": "^1.11.20",
|
||||
"dotenv": "^17.3.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -37,19 +56,21 @@
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@kevisual/query": "^0.0.39",
|
||||
"@kevisual/router": "^0.0.70",
|
||||
"@kevisual/query": "^0.0.53",
|
||||
"@kevisual/router": "^0.1.2",
|
||||
"@kevisual/use-config": "^1.0.30",
|
||||
"es-toolkit": "^1.44.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"@opencode-ai/sdk": "^1.2.27",
|
||||
"es-toolkit": "^1.45.1",
|
||||
"nanoid": "^5.1.7",
|
||||
"unstorage": "^1.17.4",
|
||||
"ws": "npm:@kevisual/ws",
|
||||
"zod": "^4.3.6"
|
||||
"ws": "npm:@kevisual/ws"
|
||||
},
|
||||
"exports": {
|
||||
".": "./mod.ts",
|
||||
".": "./src/index.ts",
|
||||
"./opencode": "./dist/opencode.js",
|
||||
"./keep": "./dist/keep.js",
|
||||
"./keep.ts": "./src/keep.ts",
|
||||
"./keep-file-live.ts": "./src/workspace/keep-file-live.ts",
|
||||
"./routes": "./dist/routes.js",
|
||||
"./src/*": "./src/*",
|
||||
"./agent/*": "./agent/*"
|
||||
|
||||
946
pnpm-lock.yaml
generated
946
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
packages:
|
||||
- web
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -1,5 +1,13 @@
|
||||
# cnb.cool 能做什么
|
||||
|
||||
所有的代码仓库,只基于一个准则 `group/repo`
|
||||
|
||||
## 环境变量
|
||||
|
||||
```sh
|
||||
CNB_API_KEY
|
||||
CNB_COOKIE
|
||||
```
|
||||
## 简介
|
||||
|
||||
纯粹调用api的模式去使用cnb.cool,自动化方案。
|
||||
|
||||
@@ -79,7 +79,7 @@ export class CNBCore {
|
||||
}
|
||||
delete _headers.Authorization;
|
||||
}
|
||||
console.log('Request URL:', url, data, _headers);
|
||||
// console.log('Request URL:', url, data, _headers);
|
||||
const response = await fetch(url || '', {
|
||||
method,
|
||||
headers: _headers,
|
||||
@@ -115,7 +115,7 @@ export class CNBCore {
|
||||
if (url && url.startsWith('http')) {
|
||||
return url;
|
||||
}
|
||||
console.log('url', url, this.baseURL)
|
||||
// console.log('url', url, this.baseURL)
|
||||
if (url.startsWith('/')) {
|
||||
return this.baseURL + url;
|
||||
}
|
||||
|
||||
13
src/index.ts
13
src/index.ts
@@ -68,3 +68,16 @@ export * from './build/index.ts'
|
||||
export * from './issue/index.ts'
|
||||
export * from './mission/index.ts'
|
||||
export * from './ai/index.ts'
|
||||
|
||||
export const getCNBVersion = () => {
|
||||
const url = 'https://cnb.cool/api/version';
|
||||
// {"version":"1.18.8-2e3e01f0-20260309","commitID":"2e3e01f0","hash":"897b088418dccd05"}
|
||||
return fetch(url).then(res => res.json()) as Promise<VersionInfo>;
|
||||
}
|
||||
type VersionInfo = {
|
||||
version: string;
|
||||
commitID: string;
|
||||
hash: string;
|
||||
}
|
||||
|
||||
export * from './issue/npc/env.ts'
|
||||
@@ -1,15 +1,27 @@
|
||||
import { CNBCore, CNBCoreOptions, RequestOptions, Result } from "../cnb-core.ts";
|
||||
|
||||
import { extractAliveInfo } from "./issue-alive.ts";
|
||||
import { useNPCEnv, useCommentEnv, usePullRequestEnv, useRepoInfoEnv } from "./npc/env.ts";
|
||||
export type IssueAssignee = {
|
||||
nickname: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type IssueLabel = {
|
||||
color: string;
|
||||
description: string;
|
||||
id: string;
|
||||
name: string;
|
||||
creator?: {
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
is_npc: boolean;
|
||||
};
|
||||
applied_by?: {
|
||||
username: string;
|
||||
nickname: string;
|
||||
email: string;
|
||||
is_npc: boolean;
|
||||
}
|
||||
};
|
||||
export type IssueState = 'open' | 'closed';
|
||||
|
||||
@@ -21,6 +33,26 @@ export type IssueAuthor = {
|
||||
ended_at: string;
|
||||
};
|
||||
|
||||
export type IssueCommentUser = {
|
||||
username: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
is_npc: boolean;
|
||||
};
|
||||
|
||||
export type IssueCommentReaction = {
|
||||
// 根据实际返回数据补充
|
||||
};
|
||||
|
||||
export type IssueComment = {
|
||||
id: string;
|
||||
body: string;
|
||||
author: IssueCommentUser;
|
||||
reactions: IssueCommentReaction[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export type IssueItem = {
|
||||
assignees: IssueAssignee[];
|
||||
author: IssueAuthor;
|
||||
@@ -41,14 +73,14 @@ export class Issue extends CNBCore {
|
||||
super(options);
|
||||
}
|
||||
|
||||
createIssue(repo: string, data: Partial<IssueItem>): Promise<any> {
|
||||
createIssue(repo: string, data: Partial<IssueItem>): Promise<Result<IssueItem>> {
|
||||
const url = `/${repo}/-/issues`;
|
||||
let postData = {
|
||||
...data,
|
||||
};
|
||||
return this.post({ url, data: postData });
|
||||
}
|
||||
updateIssue(repo: string, issueNumber: string | number, data: Partial<IssueItem>): Promise<any> {
|
||||
updateIssue(repo: string, issueNumber: string | number, data: Partial<IssueItem>): Promise<Result<IssueItem>> {
|
||||
const url = `/${repo}/-/issues/${issueNumber}`;
|
||||
let postData = {
|
||||
...data,
|
||||
@@ -75,10 +107,34 @@ export class Issue extends CNBCore {
|
||||
}
|
||||
});
|
||||
}
|
||||
getCommentList(repo: string, issueNumber: string | number): Promise<any> {
|
||||
getCommentList(repo: string, issueNumber: string | number, params?: { page?: number; page_size?: number }): Promise<Result<IssueComment[]>> {
|
||||
const url = `/${repo}/-/issues/${issueNumber}/comments`;
|
||||
return this.get({
|
||||
url,
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
getComment(repo: string, issueNumber: string | number, commentId: string | number): Promise<Result<IssueComment>> {
|
||||
const url = `/${repo}/-/issues/${issueNumber}/comments/${commentId}`;
|
||||
return this.get({
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
createComment(repo: string, issueNumber: string | number, body: string): Promise<Result<IssueComment>> {
|
||||
const url = `/${repo}/-/issues/${issueNumber}/comments`;
|
||||
return this.post({
|
||||
url,
|
||||
data: { body },
|
||||
});
|
||||
}
|
||||
|
||||
updateComment(repo: string, issueNumber: string | number, commentId: string | number, body: string): Promise<Result<IssueComment>> {
|
||||
const url = `/${repo}/-/issues/${issueNumber}/comments/${commentId}`;
|
||||
return this.patch({
|
||||
url,
|
||||
data: { body },
|
||||
});
|
||||
}
|
||||
setIssueProperty(repo: string, issueNumber: string | number, properties: { [key: string]: any }[]): Promise<any> {
|
||||
@@ -88,6 +144,63 @@ export class Issue extends CNBCore {
|
||||
};
|
||||
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 } },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
useNPCEnv() {
|
||||
return useNPCEnv();
|
||||
}
|
||||
useCommentEnv() {
|
||||
return useCommentEnv();
|
||||
}
|
||||
usePullRequestEnv() {
|
||||
return usePullRequestEnv();
|
||||
}
|
||||
useRepoInfoEnv() {
|
||||
return useRepoInfoEnv();
|
||||
}
|
||||
}
|
||||
|
||||
type GetListParams = {
|
||||
@@ -120,3 +233,16 @@ type GetListParams = {
|
||||
/** 问题更新时间过滤-结束,例如: 2022-01-31 */
|
||||
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
15
src/issue/issue-alive.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
24
src/issue/issue-alive/issue-alive.md
Normal file
24
src/issue/issue-alive/issue-alive.md
Normal 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}}
|
||||
```
|
||||
2
src/issue/issue-alive/issues-alive.html
Normal file
2
src/issue/issue-alive/issues-alive.html
Normal file
File diff suppressed because one or more lines are too long
217
src/issue/npc/build-env.ts
Normal file
217
src/issue/npc/build-env.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
|
||||
import { useKey } from "@kevisual/context";
|
||||
|
||||
// CNB_BUILD_ID cnb-75b-1jj9hnk99 当前构建的流水号,全局唯一
|
||||
// CNB_BUILD_WEB_URL https://cnb.cool/kevision/dev-cnb/-/build/logs/cnb-75b-1jj9hnk99 当前构建的日志地址
|
||||
// CNB_BUILD_START_TIME 2026-03-09T14:59:01.550Z 当前构建的开始时间,UTC 格式,示例 2025-08-21T09:13:45.803Z
|
||||
// CNB_BUILD_USER xiongxiao 当前构建的触发者用户名
|
||||
// CNB_BUILD_USER_NICKNAME 小熊猫呜呜呜 当前构建的触发者昵称
|
||||
// CNB_BUILD_USER_EMAIL kevisual@xiongxiao.me 当前构建的触发者邮箱
|
||||
// CNB_BUILD_USER_ID 1935321989751226368 当前构建的触发者 id
|
||||
// CNB_BUILD_USER_NPC_SLUG 当前构建若为 NPC 触发,则为 NPC 所属仓库的路径
|
||||
// CNB_BUILD_USER_NPC_NAME 当前构建若为 NPC 触发,则为 NPC 角色名
|
||||
// CNB_BUILD_STAGE_NAME 初始化开发机 当前构建的 stage 名称
|
||||
// CNB_BUILD_JOB_NAME 初始化开发机 当前构建的 job 名称
|
||||
// CNB_BUILD_JOB_KEY job-0 当前构建的 job key,同 stage 下唯一
|
||||
// CNB_BUILD_WORKSPACE /workspace/ 自定义 shell 脚本执行的工作空间根目录
|
||||
// CNB_BUILD_FAILED_MSG 流水线构建失败的错误信息,可在 failStages 中使用
|
||||
// CNB_BUILD_FAILED_STAGE_NAME 流水线构建失败的 stage 的名称,可在 failStages 中使用
|
||||
// CNB_PIPELINE_NAME pipeline-1 当前 pipeline 的 name,没声明时为空
|
||||
// CNB_PIPELINE_KEY pipeline-1 当前 pipeline 的索引 key,例如 pipeline-0
|
||||
// CNB_PIPELINE_ID cnb-75b-1jj9hnk99-001 当前 pipeline 的 id,全局唯一字符串
|
||||
// CNB_PIPELINE_DOCKER_IMAGE docker.cnb.cool/kevisual/dev-env:latest 当前 pipeline 所使用的 docker image,如:alpine:latest
|
||||
// CNB_PIPELINE_STATUS 当前流水线的构建状态,可在 endStages 中查看,其可能的值包括:success、error、cancel
|
||||
// CNB_PIPELINE_MAX_RUN_TIME 72000000 流水线最大运行时间,单位为毫秒
|
||||
// CNB_RUNNER_IP 10.235.16.3 当前 pipeline 所在 Runner 的 ip
|
||||
// CNB_CPUS 16 当前构建流水线可以使用的最大 CPU 核数
|
||||
// CNB_MEMORY 32 当前构建流水线可以使用的最大内存大小,单位为 GiB
|
||||
// CNB_IS_RETRY false 当前构建是否由 rebuild 触发
|
||||
// HUSKY_SKIP_INSTALL 1 兼容 ci 环境下 husky
|
||||
export const useBuildEnv = () => {
|
||||
const buildId = useKey("CNB_BUILD_ID");
|
||||
const buildWebUrl = useKey("CNB_BUILD_WEB_URL");
|
||||
const buildStartTime = useKey("CNB_BUILD_START_TIME");
|
||||
const buildUser = useKey("CNB_BUILD_USER");
|
||||
const buildUserNickname = useKey("CNB_BUILD_USER_NICKNAME");
|
||||
const buildUserEmail = useKey("CNB_BUILD_USER_EMAIL");
|
||||
const buildUserId = useKey("CNB_BUILD_USER_ID");
|
||||
const buildUserNpcSlug = useKey("CNB_BUILD_USER_NPC_SLUG");
|
||||
const buildUserNpcName = useKey("CNB_BUILD_USER_NPC_NAME");
|
||||
const buildStageName = useKey("CNB_BUILD_STAGE_NAME");
|
||||
const buildJobName = useKey("CNB_BUILD_JOB_NAME");
|
||||
const buildJobKey = useKey("CNB_BUILD_JOB_KEY");
|
||||
const buildWorkspace = useKey("CNB_BUILD_WORKSPACE");
|
||||
const buildFailedMsg = useKey("CNB_BUILD_FAILED_MSG");
|
||||
const buildFailedStageName = useKey("CNB_BUILD_FAILED_STAGE_NAME");
|
||||
const pipelineName = useKey("CNB_PIPELINE_NAME");
|
||||
const pipelineKey = useKey("CNB_PIPELINE_KEY");
|
||||
const pipelineId = useKey("CNB_PIPELINE_ID");
|
||||
const pipelineDockerImage = useKey("CNB_PIPELINE_DOCKER_IMAGE");
|
||||
const pipelineStatus = useKey("CNB_PIPELINE_STATUS");
|
||||
const pipelineMaxRunTime = useKey("CNB_PIPELINE_MAX_RUN_TIME");
|
||||
const runnerIp = useKey("CNB_RUNNER_IP");
|
||||
const cpus = useKey("CNB_CPUS");
|
||||
const memory = useKey("CNB_MEMORY");
|
||||
const isRetry = useKey("CNB_IS_RETRY");
|
||||
const huskySkipInstall = useKey("HUSKY_SKIP_INSTALL");
|
||||
|
||||
return {
|
||||
/**
|
||||
* @key CNB_BUILD_ID
|
||||
* @description:当前构建的流水号,全局唯一
|
||||
*/
|
||||
buildId,
|
||||
buildIdLabel: "当前构建的流水号,全局唯一",
|
||||
/**
|
||||
* @key CNB_BUILD_WEB_URL
|
||||
* @description:当前构建的日志地址
|
||||
*/
|
||||
buildWebUrl,
|
||||
buildWebUrlLabel: "当前构建的日志地址",
|
||||
/**
|
||||
* @key CNB_BUILD_START_TIME
|
||||
* @description:当前构建的开始时间,UTC 格式
|
||||
*/
|
||||
buildStartTime,
|
||||
buildStartTimeLabel: "当前构建的开始时间,UTC 格式",
|
||||
/**
|
||||
* @key CNB_BUILD_USER
|
||||
* @description:当前构建的触发者用户名
|
||||
*/
|
||||
buildUser,
|
||||
buildUserLabel: "当前构建的触发者用户名",
|
||||
/**
|
||||
* @key CNB_BUILD_USER_NICKNAME
|
||||
* @description:当前构建的触发者昵称
|
||||
*/
|
||||
buildUserNickname,
|
||||
buildUserNicknameLabel: "当前构建的触发者昵称",
|
||||
/**
|
||||
* @key CNB_BUILD_USER_EMAIL
|
||||
* @description:当前构建的触发者邮箱
|
||||
*/
|
||||
buildUserEmail,
|
||||
buildUserEmailLabel: "当前构建的触发者邮箱",
|
||||
/**
|
||||
* @key CNB_BUILD_USER_ID
|
||||
* @description:当前构建的触发者 id
|
||||
*/
|
||||
buildUserId,
|
||||
buildUserIdLabel: "当前构建的触发者 id",
|
||||
/**
|
||||
* @key CNB_BUILD_USER_NPC_SLUG
|
||||
* @description:当前构建若为 NPC 触发,则为 NPC 所属仓库的路径
|
||||
*/
|
||||
buildUserNpcSlug,
|
||||
buildUserNpcSlugLabel: "当前构建若为 NPC 触发,则为 NPC 所属仓库的路径",
|
||||
/**
|
||||
* @key CNB_BUILD_USER_NPC_NAME
|
||||
* @description:当前构建若为 NPC 触发,则为 NPC 角色名
|
||||
*/
|
||||
buildUserNpcName,
|
||||
buildUserNpcNameLabel: "当前构建若为 NPC 触发,则为 NPC 角色名",
|
||||
/**
|
||||
* @key CNB_BUILD_STAGE_NAME
|
||||
* @description:当前构建的 stage 名称
|
||||
*/
|
||||
buildStageName,
|
||||
buildStageNameLabel: "当前构建的 stage 名称",
|
||||
/**
|
||||
* @key CNB_BUILD_JOB_NAME
|
||||
* @description:当前构建的 job 名称
|
||||
*/
|
||||
buildJobName,
|
||||
buildJobNameLabel: "当前构建的 job 名称",
|
||||
/**
|
||||
* @key CNB_BUILD_JOB_KEY
|
||||
* @description:当前构建的 job key,同 stage 下唯一
|
||||
*/
|
||||
buildJobKey,
|
||||
buildJobKeyLabel: "当前构建的 job key,同 stage 下唯一",
|
||||
/**
|
||||
* @key CNB_BUILD_WORKSPACE
|
||||
* @description:自定义 shell 脚本执行的工作空间根目录
|
||||
*/
|
||||
buildWorkspace,
|
||||
buildWorkspaceLabel: "自定义 shell 脚本执行的工作空间根目录",
|
||||
/**
|
||||
* @key CNB_BUILD_FAILED_MSG
|
||||
* @description:流水线构建失败的错误信息
|
||||
*/
|
||||
buildFailedMsg,
|
||||
buildFailedMsgLabel: "流水线构建失败的错误信息",
|
||||
/**
|
||||
* @key CNB_BUILD_FAILED_STAGE_NAME
|
||||
* @description:流水线构建失败的 stage 的名称
|
||||
*/
|
||||
buildFailedStageName,
|
||||
buildFailedStageNameLabel: "流水线构建失败的 stage 的名称",
|
||||
/**
|
||||
* @key CNB_PIPELINE_NAME
|
||||
* @description:当前 pipeline 的 name
|
||||
*/
|
||||
pipelineName,
|
||||
pipelineNameLabel: "当前 pipeline 的 name",
|
||||
/**
|
||||
* @key CNB_PIPELINE_KEY
|
||||
* @description:当前 pipeline 的索引 key
|
||||
*/
|
||||
pipelineKey,
|
||||
pipelineKeyLabel: "当前 pipeline 的索引 key",
|
||||
/**
|
||||
* @key CNB_PIPELINE_ID
|
||||
* @description:当前 pipeline 的 id
|
||||
*/
|
||||
pipelineId,
|
||||
pipelineIdLabel: "当前 pipeline 的 id",
|
||||
/**
|
||||
* @key CNB_PIPELINE_DOCKER_IMAGE
|
||||
* @description:当前 pipeline 所使用的 docker image
|
||||
*/
|
||||
pipelineDockerImage,
|
||||
pipelineDockerImageLabel: "当前 pipeline 所使用的 docker image",
|
||||
/**
|
||||
* @key CNB_PIPELINE_STATUS
|
||||
* @description:当前流水线的构建状态,可能的值包括:success、error、cancel
|
||||
*/
|
||||
pipelineStatus,
|
||||
pipelineStatusLabel: "当前流水线的构建状态,可能的值包括:success、error、cancel",
|
||||
/**
|
||||
* @key CNB_PIPELINE_MAX_RUN_TIME
|
||||
* @description:流水线最大运行时间,单位为毫秒
|
||||
*/
|
||||
pipelineMaxRunTime,
|
||||
pipelineMaxRunTimeLabel: "流水线最大运行时间,单位为毫秒",
|
||||
/**
|
||||
* @key CNB_RUNNER_IP
|
||||
* @description:当前 pipeline 所在 Runner 的 ip
|
||||
*/
|
||||
runnerIp,
|
||||
runnerIpLabel: "当前 pipeline 所在 Runner 的 ip",
|
||||
/**
|
||||
* @key CNB_CPUS
|
||||
* @description:当前构建流水线可以使用的最大 CPU 核数
|
||||
*/
|
||||
cpus,
|
||||
cpusLabel: "当前构建流水线可以使用的最大 CPU 核数",
|
||||
/**
|
||||
* @key CNB_MEMORY
|
||||
* @description:当前构建流水线可以使用的最大内存大小,单位为 GiB
|
||||
*/
|
||||
memory,
|
||||
memoryLabel: "当前构建流水线可以使用的最大内存大小,单位为 GiB",
|
||||
/**
|
||||
* @key CNB_IS_RETRY
|
||||
* @description:当前构建是否由 rebuild 触发
|
||||
*/
|
||||
isRetry,
|
||||
isRetryLabel: "当前构建是否由 rebuild 触发",
|
||||
/**
|
||||
* @key HUSKY_SKIP_INSTALL
|
||||
* @description:兼容 ci 环境下 husky
|
||||
*/
|
||||
huskySkipInstall,
|
||||
huskySkipInstallLabel: "兼容 ci 环境下 husky"
|
||||
};
|
||||
}
|
||||
|
||||
201
src/issue/npc/env.ts
Normal file
201
src/issue/npc/env.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
// CNB_NPC_SLUG 对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库路径,否则为空字符串
|
||||
// CNB_NPC_NAME 对于 NPC 事件触发的构建,值为 NPC 角色名,否则为空字符串
|
||||
// CNB_NPC_SHA 对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库默认分支最新提交的 sha,否则为空字符串
|
||||
// CNB_NPC_PROMPT 对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色 Prompt,否则为空字符串
|
||||
// CNB_NPC_AVATAR 对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色头像,否则为空字符串
|
||||
// CNB_NPC_ENABLE_THINKING 对于 @npc 事件触发的构建,值为 NPC 角色是否开启思考,否则为空字符串
|
||||
import { useKey } from "@kevisual/context";
|
||||
|
||||
export function useNPCEnv() {
|
||||
const npcSlug = useKey("CNB_NPC_SLUG");
|
||||
const npcName = useKey("CNB_NPC_NAME");
|
||||
const npcSha = useKey("CNB_NPC_SHA");
|
||||
const npcPrompt = useKey("CNB_NPC_PROMPT");
|
||||
const npcAvatar = useKey("CNB_NPC_AVATAR");
|
||||
const npcEnableThinking = useKey("CNB_NPC_ENABLE_THINKING");
|
||||
|
||||
return {
|
||||
/**
|
||||
* @key CNB_NPC_SLUG
|
||||
* @description:对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库路径,否则为空字符串
|
||||
*/
|
||||
npcSlug,
|
||||
npcSlugLabel: "对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库路径,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_NPC_NAME
|
||||
* @description:对于 NPC 事件触发的构建,值为 NPC 角色名,否则为空字符串
|
||||
*/
|
||||
npcName,
|
||||
npcNameLabel: "对于 NPC 事件触发的构建,值为 NPC 角色名,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_NPC_SHA
|
||||
* @description:对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库默认分支最新提交的 sha,否则为空字符串
|
||||
*/
|
||||
npcSha,
|
||||
npcShaLabel: "对于 @ 知识库角色触发的 NPC 事件,值为 NPC 所属仓库默认分支最新提交的 sha,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_NPC_PROMPT
|
||||
* @description:对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色 Prompt,否则为空字符串
|
||||
*/
|
||||
npcPrompt,
|
||||
npcPromptLabel: "对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色 Prompt,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_NPC_AVATAR
|
||||
* @description:对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色头像,否则为空字符串
|
||||
*/
|
||||
npcAvatar,
|
||||
npcAvatarLabel: "对于 @ 知识库角色触发的 NPC 事件,值为 NPC 角色头像,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_NPC_ENABLE_THINKING
|
||||
* @description:对于 @npc 事件触发的构建,值为 NPC 角色是否开启思考,否则为空字符串
|
||||
*/
|
||||
npcEnableThinking,
|
||||
npcEnableThinkingLabel: "对于 @npc 事件触发的构建,值为 NPC 角色是否开启思考,否则为空字符串"
|
||||
};
|
||||
}
|
||||
|
||||
// CNB_COMMENT_ID 对于评论事件触发的构建,值为评论全局唯一 ID,否则为空字符串
|
||||
// CNB_COMMENT_BODY 对于评论事件触发的构建,值为评论内容,否则为空字符串
|
||||
// CNB_COMMENT_TYPE note 对于 PR 代码评审评论,值为 diff_note;对于 PR 非代码评审评论以及 Issue 评论,值为 note;否则为空字符串
|
||||
// CNB_COMMENT_FILE_PATH 对于 PR 代码评审评论,值为评论所在文件,否则为空字符串
|
||||
// CNB_COMMENT_RANGE 对于 PR 代码评审评论,值为评论所在代码行。如,单行为 L12,多行为 L13-L16,否则为空字符串
|
||||
// CNB_REVIEW_ID 对于 PR 代码评审,值为评审 ID,否则为空字符串
|
||||
|
||||
export function useCommentEnv() {
|
||||
const commentId = useKey("CNB_COMMENT_ID");
|
||||
const commentBody = useKey("CNB_COMMENT_BODY");
|
||||
const commentType = useKey("CNB_COMMENT_TYPE");
|
||||
const commentFilePath = useKey("CNB_COMMENT_FILE_PATH");
|
||||
const commentRange = useKey("CNB_COMMENT_RANGE");
|
||||
const reviewId = useKey("CNB_REVIEW_ID");
|
||||
|
||||
return {
|
||||
/**
|
||||
* @key CNB_COMMENT_ID
|
||||
* @description:对于评论事件触发的构建,值为评论全局唯一 ID,否则为空字符串
|
||||
*/
|
||||
commentId,
|
||||
commentIdLabel: "对于评论事件触发的构建,值为评论全局唯一 ID,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_COMMENT_BODY
|
||||
* @description:对于评论事件触发的构建,值为评论内容,否则为空字符串
|
||||
*/
|
||||
commentBody,
|
||||
commentBodyLabel: "对于评论事件触发的构建,值为评论内容,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_COMMENT_TYPE
|
||||
* @description:note 对于 PR 代码评审评论,值为 diff_note;对于 PR 非代码评审评论以及 Issue 评论,值为 note;否则为空字符串
|
||||
*/
|
||||
commentType,
|
||||
commentTypeLabel: "对于 PR 代码评审评论,值为 diff_note;对于 PR 非代码评审评论以及 Issue 评论,值为 note;否则为空字符串",
|
||||
/**
|
||||
* @key CNB_COMMENT_FILE_PATH
|
||||
* @description:对于 PR 代码评审评论,值为评论所在文件,否则为空字符串
|
||||
*/
|
||||
commentFilePath,
|
||||
commentFilePathLabel: "对于 PR 代码评审评论,值为评论所在文件,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_COMMENT_RANGE
|
||||
* @description:对于 PR 代码评审评论,值为评论所在代码行。如,单行为 L12,多行为 L13-L16,否则为空字符串
|
||||
*/
|
||||
commentRange,
|
||||
commentRangeLabel: "对于 PR 代码评审评论,值为评论所在代码行。如,单行为 L12,多行为 L13-L16,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_REVIEW_ID
|
||||
* @description:对于 PR 代码评审,值为评审 ID,否则为空字符串
|
||||
*/
|
||||
reviewId,
|
||||
reviewIdLabel: "对于 PR 代码评审,值为评审 ID,否则为空字符串"
|
||||
};
|
||||
}
|
||||
|
||||
// CNB_ISSUE_ID Issue 全局唯一 ID
|
||||
// CNB_ISSUE_IID Issue 仓库编号
|
||||
// CNB_ISSUE_TITLE Issue 标题
|
||||
// CNB_ISSUE_DESCRIPTION Issue 描述
|
||||
// CNB_ISSUE_OWNER Issue 作者
|
||||
// CNB_ISSUE_STATE Issue 状态
|
||||
// CNB_ISSUE_IS_RESOLVED Issue 是否已解决
|
||||
// CNB_ISSUE_ASSIGNEES Issue 处理人列表
|
||||
// CNB_ISSUE_LABELS Issue 标签列表
|
||||
// CNB_ISSUE_PRIORITY Issue 优先级
|
||||
export const useIssueEnv = () => {
|
||||
const issueId = useKey("CNB_ISSUE_ID");
|
||||
const issueIid = useKey("CNB_ISSUE_IID");
|
||||
const issueTitle = useKey("CNB_ISSUE_TITLE");
|
||||
const issueDescription = useKey("CNB_ISSUE_DESCRIPTION");
|
||||
const issueOwner = useKey("CNB_ISSUE_OWNER");
|
||||
const issueState = useKey("CNB_ISSUE_STATE");
|
||||
const issueIsResolved = useKey("CNB_ISSUE_IS_RESOLVED");
|
||||
const issueAssignees = useKey("CNB_ISSUE_ASSIGNEES");
|
||||
const issueLabels = useKey("CNB_ISSUE_LABELS");
|
||||
const issuePriority = useKey("CNB_ISSUE_PRIORITY");
|
||||
|
||||
return {
|
||||
/**
|
||||
* @key CNB_ISSUE_ID
|
||||
* @description:Issue 全局唯一 ID
|
||||
*/
|
||||
issueId,
|
||||
issueIdLabel: "Issue 全局唯一 ID",
|
||||
/**
|
||||
* @key CNB_ISSUE_IID
|
||||
* @description:Issue 仓库编号
|
||||
*/
|
||||
issueIid,
|
||||
issueIidLabel: "Issue 仓库编号",
|
||||
/**
|
||||
* @key CNB_ISSUE_TITLE
|
||||
* @description:Issue 标题
|
||||
*/
|
||||
issueTitle,
|
||||
issueTitleLabel: "Issue 标题",
|
||||
/**
|
||||
* @key CNB_ISSUE_DESCRIPTION
|
||||
* @description:Issue 描述
|
||||
*/
|
||||
issueDescription,
|
||||
issueDescriptionLabel: "Issue 描述",
|
||||
/**
|
||||
* @key CNB_ISSUE_OWNER
|
||||
* @description:Issue 作者
|
||||
*/
|
||||
issueOwner,
|
||||
issueOwnerLabel: "Issue 作者",
|
||||
/**
|
||||
* @key CNB_ISSUE_STATE
|
||||
* @description:Issue 状态
|
||||
*/
|
||||
issueState,
|
||||
issueStateLabel: "Issue 状态",
|
||||
/**
|
||||
* @key CNB_ISSUE_IS_RESOLVED
|
||||
* @description:Issue 是否已解决
|
||||
*/
|
||||
issueIsResolved,
|
||||
issueIsResolvedLabel: "Issue 是否已解决",
|
||||
/**
|
||||
* @key CNB_ISSUE_ASSIGNEES
|
||||
* @description:Issue 处理人列表
|
||||
*/
|
||||
issueAssignees,
|
||||
issueAssigneesLabel: "Issue 处理人列表",
|
||||
/**
|
||||
* @key CNB_ISSUE_LABELS
|
||||
* @description:Issue 标签列表, 多个以 , 分隔。
|
||||
*/
|
||||
issueLabels,
|
||||
issueLabelsLabel: "Issue 标签列表, 多个以 , 分隔。",
|
||||
/**
|
||||
* @key CNB_ISSUE_PRIORITY
|
||||
* @description:Issue 优先级
|
||||
*/
|
||||
issuePriority,
|
||||
issuePriorityLabel: "Issue 优先级"
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export * from './build-env.ts'
|
||||
export * from './pr-env.ts'
|
||||
export * from './repo-env.ts'
|
||||
95
src/issue/npc/pr-env.ts
Normal file
95
src/issue/npc/pr-env.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { useKey } from "@kevisual/context";
|
||||
|
||||
// CNB_PULL_REQUEST false 对于由 pull_request、pull_request.update、pull_request.target 触发的构建,值为 true,否则为 false
|
||||
// CNB_PULL_REQUEST_LIKE false 对于由 合并类事件 触发的构建,值为 true,否则为 false
|
||||
// CNB_PULL_REQUEST_PROPOSER 对于由 合并类事件 触发的构建,值为提出 PR 者名称,否则为空字符串
|
||||
// CNB_PULL_REQUEST_TITLE 对于由 合并类事件 触发的构建,值为提 PR 时候填写的标题,否则为空字符串
|
||||
// CNB_PULL_REQUEST_BRANCH 对于由 合并类事件 触发的构建,值为发起 PR 的源分支名称,否则为空字符串
|
||||
// CNB_PULL_REQUEST_SHA 对于由 合并类事件 触发的构建,值为当前 PR 源分支最新的提交 sha,否则为空字符串
|
||||
// CNB_PULL_REQUEST_TARGET_SHA 对于由 合并类事件 触发的构建,值为当前 PR 目标分支最新的提交 sha,否则为空字符串
|
||||
// CNB_PULL_REQUEST_MERGE_SHA 对于由 pull_request.merged 触发的构建,值为合并后的 sha;对于 pull_request 等触发的构建,值为预合并后的 sha,否则为空字符串
|
||||
// CNB_PULL_REQUEST_SLUG 对于由 合并类事件 触发的构建,值为源仓库的仓库 slug,如 group_slug/repo_name,否则为空字符串
|
||||
// CNB_PULL_REQUEST_ACTION 对于由 合并类事件 触发的构建,可能的值有:created(新建PR)、code_update(源分支push)、status_update(评审通过或CI状态变更),否则为空字符串
|
||||
// CNB_PULL_REQUEST_ID 对于由 合并类事件 触发的构建,值为当前或者关联 PR 的全局唯一 id,否则为空字符串
|
||||
export const usePullRequestEnv = () => {
|
||||
const pullRequest = useKey("CNB_PULL_REQUEST");
|
||||
const pullRequestLike = useKey("CNB_PULL_REQUEST_LIKE");
|
||||
const pullRequestProposer = useKey("CNB_PULL_REQUEST_PROPOSER");
|
||||
const pullRequestTitle = useKey("CNB_PULL_REQUEST_TITLE");
|
||||
const pullRequestBranch = useKey("CNB_PULL_REQUEST_BRANCH");
|
||||
const pullRequestSha = useKey("CNB_PULL_REQUEST_SHA");
|
||||
const pullRequestTargetSha = useKey("CNB_PULL_REQUEST_TARGET_SHA");
|
||||
const pullRequestMergeSha = useKey("CNB_PULL_REQUEST_MERGE_SHA");
|
||||
const pullRequestSlug = useKey("CNB_PULL_REQUEST_SLUG");
|
||||
const pullRequestAction = useKey("CNB_PULL_REQUEST_ACTION");
|
||||
const pullRequestId = useKey("CNB_PULL_REQUEST_ID");
|
||||
|
||||
return {
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST
|
||||
* @description:对于由 pull_request、pull_request.update、pull_request.target 触发的构建,值为 true,否则为 false
|
||||
*/
|
||||
pullRequest,
|
||||
pullRequestLabel: "对于由 pull_request、pull_request.update、pull_request.target 触发的构建,值为 true,否则为 false",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_LIKE
|
||||
* @description:对于由 合并类事件 触发的构建,值为 true,否则为 false
|
||||
*/
|
||||
pullRequestLike,
|
||||
pullRequestLikeLabel: "对于由 合并类事件 触发的构建,值为 true,否则为 false",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_PROPOSER
|
||||
* @description:对于由 合并类事件 触发的构建,值为提出 PR 者名称,否则为空字符串
|
||||
*/
|
||||
pullRequestProposer,
|
||||
pullRequestProposerLabel: "对于由 合并类事件 触发的构建,值为提出 PR 者名称,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_TITLE
|
||||
* @description:对于由 合并类事件 触发的构建,值为提 PR 时候填写的标题,否则为空字符串
|
||||
*/
|
||||
pullRequestTitle,
|
||||
pullRequestTitleLabel: "对于由 合并类事件 触发的构建,值为提 PR 时候填写的标题,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_BRANCH
|
||||
* @description:对于由 合并类事件 触发的构建,值为发起 PR 的源分支名称,否则为空字符串
|
||||
*/
|
||||
pullRequestBranch,
|
||||
pullRequestBranchLabel: "对于由 合并类事件 触发的构建,值为发起 PR 的源分支名称,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_SHA
|
||||
* @description:对于由 合并类事件 触发的构建,值为当前 PR 源分支最新的提交 sha,否则为空字符串
|
||||
*/
|
||||
pullRequestSha,
|
||||
pullRequestShaLabel: "对于由 合并类事件 触发的构建,值为当前 PR 源分支最新的提交 sha,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_TARGET_SHA
|
||||
* @description:对于由 合并类事件 触发的构建,值为当前 PR 目标分支最新的提交 sha,否则为空字符串
|
||||
*/
|
||||
pullRequestTargetSha,
|
||||
pullRequestTargetShaLabel: "对于由 合并类事件 触发的构建,值为当前 PR 目标分支最新的提交 sha,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_MERGE_SHA
|
||||
* @description:对于由 pull_request.merged 触发的构建,值为合并后的 sha;对于 pull_request 等触发的构建,值为预合并后的 sha,否则为空字符串
|
||||
*/
|
||||
pullRequestMergeSha,
|
||||
pullRequestMergeShaLabel: "对于由 pull_request.merged 触发的构建,值为合并后的 sha;对于 pull_request 等触发的构建,值为预合并后的 sha,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_SLUG
|
||||
* @description:对于由 合并类事件 触发的构建,值为源仓库的仓库 slug,如 group_slug/repo_name,否则为空字符串
|
||||
*/
|
||||
pullRequestSlug,
|
||||
pullRequestSlugLabel: "对于由 合并类事件 触发的构建,值为源仓库的仓库 slug,如 group_slug/repo_name,否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_ACTION
|
||||
* @description:对于由 合并类事件 触发的构建,可能的值有:created(新建PR)、code_update(源分支push)、status_update(评审通过或CI状态变更),否则为空字符串
|
||||
*/
|
||||
pullRequestAction,
|
||||
pullRequestActionLabel: "对于由 合并类事件 触发的构建,可能的值有:created(新建PR)、code_update(源分支push)、status_update(评审通过或CI状态变更),否则为空字符串",
|
||||
/**
|
||||
* @key CNB_PULL_REQUEST_ID
|
||||
* @description:对于由 合并类事件 触发的构建,值为当前或者关联 PR 的全局唯一 id,否则为空字符串
|
||||
*/
|
||||
pullRequestId,
|
||||
pullRequestIdLabel: "对于由 合并类事件 触发的构建,值为当前或者关联 PR 的全局唯一 id,否则为空字符串"
|
||||
};
|
||||
}
|
||||
56
src/issue/npc/repo-env.ts
Normal file
56
src/issue/npc/repo-env.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useKey } from "@kevisual/context";
|
||||
|
||||
// CNB_REPO_SLUG kevision/dev-cnb 目标仓库路径,格式为 group_slug / repo_name,group_slug / sub_gourp_slug /.../repo_name
|
||||
// CNB_REPO_SLUG_LOWERCASE kevision/dev-cnb 目标仓库路径小写格式
|
||||
// CNB_REPO_NAME dev-cnb 目标仓库名称
|
||||
// CNB_REPO_NAME_LOWERCASE dev-cnb 目标仓库名称小写格式
|
||||
// CNB_REPO_ID 2026263219584110592 目标仓库的 id
|
||||
// CNB_REPO_URL_HTTPS 目标仓库 https 地址
|
||||
export const useRepoInfoEnv = () => {
|
||||
const repoSlug = useKey("CNB_REPO_SLUG");
|
||||
const repoSlugLowercase = useKey("CNB_REPO_SLUG_LOWERCASE");
|
||||
const repoName = useKey("CNB_REPO_NAME");
|
||||
const repoNameLowercase = useKey("CNB_REPO_NAME_LOWERCASE");
|
||||
const repoId = useKey("CNB_REPO_ID");
|
||||
const repoUrlHttps = useKey("CNB_REPO_URL_HTTPS");
|
||||
|
||||
return {
|
||||
/**
|
||||
* @key CNB_REPO_SLUG
|
||||
* @description:目标仓库路径,格式为 group_slug/repo_name,group_slug/sub_group_slug/.../repo_name
|
||||
*/
|
||||
repoSlug,
|
||||
repoSlugLabel: "目标仓库路径,格式为 group_slug/repo_name,group_slug/sub_group_slug/.../repo_name",
|
||||
/**
|
||||
* @key CNB_REPO_SLUG_LOWERCASE
|
||||
* @description:目标仓库路径小写格式
|
||||
*/
|
||||
repoSlugLowercase,
|
||||
repoSlugLowercaseLabel: "目标仓库路径小写格式",
|
||||
/**
|
||||
* @key CNB_REPO_NAME
|
||||
* @description:目标仓库名称
|
||||
*/
|
||||
repoName,
|
||||
repoNameLabel: "目标仓库名称",
|
||||
/**
|
||||
* @key CNB_REPO_NAME_LOWERCASE
|
||||
* @description:目标仓库名称小写格式
|
||||
*/
|
||||
repoNameLowercase,
|
||||
repoNameLowercaseLabel: "目标仓库名称小写格式",
|
||||
/**
|
||||
* @key CNB_REPO_ID
|
||||
* @description:目标仓库的 id
|
||||
*/
|
||||
repoId,
|
||||
repoIdLabel: "目标仓库的 id",
|
||||
/**
|
||||
* @key CNB_REPO_URL_HTTPS
|
||||
* @description:目标仓库 https 地址
|
||||
*/
|
||||
repoUrlHttps,
|
||||
repoUrlHttpsLabel: "目标仓库 https 地址"
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CNBCore, CNBCoreOptions, RequestOptions, Result } from "../cnb-core.ts";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
export class Repo extends CNBCore {
|
||||
constructor(options: CNBCoreOptions) {
|
||||
super(options);
|
||||
@@ -22,10 +22,20 @@ export class Repo extends CNBCore {
|
||||
};
|
||||
return this.post({ url, data: postData });
|
||||
}
|
||||
deleteRepo(name: string): Promise<any> {
|
||||
const url = `${this.hackURL}/${name}`;
|
||||
deleteRepoCookie(repo: string): Promise<any> {
|
||||
const url = `${this.hackURL}/${repo}`;
|
||||
return this.delete({ url, useCookie: true });
|
||||
}
|
||||
deleteRepo(repo: string): Promise<any> {
|
||||
const url = `/${repo}`;
|
||||
return this.delete({ url });
|
||||
}
|
||||
/**
|
||||
* 使用cookie创建提交,如果没有指定parent_commit_sha,则会自动获取最新的提交作为父提交
|
||||
* @param repo
|
||||
* @param data
|
||||
* @returns
|
||||
*/
|
||||
async createCommit(repo: string, data: CreateCommitData): Promise<any> {
|
||||
const commitList = await this.getCommitList(repo, {
|
||||
page: 1,
|
||||
@@ -37,6 +47,12 @@ export class Repo extends CNBCore {
|
||||
const preCommitSha = commitList.length > 0 ? commitList[0].sha : undefined;
|
||||
if (!data.parent_commit_sha && preCommitSha) {
|
||||
data.parent_commit_sha = preCommitSha;
|
||||
} else if (data.parent_commit_sha) {
|
||||
// 如果指定了parent_commi_sha;
|
||||
if (!data.new_branch) {
|
||||
const date = dayjs().format('MMDDHHmm');
|
||||
data.new_branch = `refs/heads/${date}`;
|
||||
}
|
||||
}
|
||||
const url = `${this.hackURL}/${repo}/-/git/commits`;
|
||||
const postData: CreateCommitData = {
|
||||
@@ -48,6 +64,7 @@ export class Repo extends CNBCore {
|
||||
new_branch: data.new_branch || 'refs/heads/main',
|
||||
};
|
||||
if (!postData.parent_commit_sha) {
|
||||
// 如果没有父提交sha,则说明是第一次提交,可以删除parent_commit_sha和base_branch字段
|
||||
delete postData.parent_commit_sha;
|
||||
delete postData.base_branch;
|
||||
}
|
||||
@@ -95,11 +112,25 @@ export class Repo extends CNBCore {
|
||||
page: params.page || 1,
|
||||
page_size: params.page_size || 999,
|
||||
}
|
||||
if(!_params.search) {
|
||||
if (!_params.search) {
|
||||
delete _params.search;
|
||||
}
|
||||
return this.get({ url, params: _params });
|
||||
}
|
||||
updateRepoInfo(repo: string, params: UpdateRepoInfo): Promise<any> {
|
||||
const url = `/${repo}`;
|
||||
return this.patch({ url, data: params });
|
||||
}
|
||||
getRepo(repo: string): Promise<Result<RepoItem>> {
|
||||
const url = `/${repo}`;
|
||||
return this.get({ url });
|
||||
}
|
||||
}
|
||||
type UpdateRepoInfo = {
|
||||
description?: string;
|
||||
license?: 'MIT' | 'Apache-2.0' | 'GPL-3.0' | 'Unlicense';
|
||||
site?: string;
|
||||
topics?: string[];
|
||||
}
|
||||
|
||||
type CreateRepoData = {
|
||||
|
||||
@@ -16,6 +16,18 @@ export class User extends CNBCore {
|
||||
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 获取用户信息
|
||||
* @returns
|
||||
|
||||
@@ -62,7 +62,7 @@ export class Workspace extends CNBCore {
|
||||
* 停止我的云原生开发环境
|
||||
* @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 } = {};
|
||||
|
||||
if (params.pipelineId) {
|
||||
@@ -81,18 +81,7 @@ export class Workspace extends CNBCore {
|
||||
* @param repo 仓库路径,例如:groupname/reponame
|
||||
* @param sn 流水线构建号
|
||||
*/
|
||||
async getDetail(repo: string, sn: string): Promise<{
|
||||
codebuddy: string;
|
||||
codebuddycn: string;
|
||||
cursor: string;
|
||||
jetbrains: Record<string, string>;
|
||||
jumpUrl: string;
|
||||
remoteSsh: string;
|
||||
ssh: string;
|
||||
vscode: string;
|
||||
'vscode-insiders': string;
|
||||
webide: string;
|
||||
}> {
|
||||
async getDetail(repo: string, sn: string): Promise<Result<WorkspaceLinkDetail>> {
|
||||
return this.get({ url: `/${repo}/-/workspace/detail/${sn}` });
|
||||
}
|
||||
/**
|
||||
@@ -115,6 +104,8 @@ export class Workspace extends CNBCore {
|
||||
|
||||
if (params.branch) {
|
||||
data.branch = params.branch;
|
||||
} else {
|
||||
data.branch = 'main'
|
||||
}
|
||||
if (params.ref) {
|
||||
data.ref = params.ref;
|
||||
@@ -122,9 +113,75 @@ export class Workspace extends CNBCore {
|
||||
|
||||
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 {
|
||||
codebuddy: string;
|
||||
codebuddycn: string;
|
||||
cursor: string;
|
||||
jetbrains: Record<string, string>;
|
||||
jumpUrl: string;
|
||||
remoteSsh: string;
|
||||
ssh: string;
|
||||
vscode: string;
|
||||
'vscode-insiders': string;
|
||||
webide: string;
|
||||
}
|
||||
|
||||
export type ResultList<T> = {
|
||||
hasMore: boolean;
|
||||
list: T[];
|
||||
|
||||
137
src/workspace/keep-file-live.ts
Normal file
137
src/workspace/keep-file-live.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,16 @@
|
||||
// WebSocket Keep-Alive Client Library
|
||||
import WebSocket from "ws";
|
||||
// 运行时检测:Bun 使用原生 WebSocket,Node.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 {
|
||||
wsUrl: string;
|
||||
@@ -31,6 +42,7 @@ export class WSKeepAlive {
|
||||
private pingTimer: NodeJS.Timeout | null = null;
|
||||
private messageHandlers: Set<MessageHandler> = new Set();
|
||||
private url: URL;
|
||||
private readonly isBun: boolean;
|
||||
|
||||
constructor(config: KeepAliveConfig) {
|
||||
this.config = {
|
||||
@@ -48,6 +60,7 @@ export class WSKeepAlive {
|
||||
debug: config.debug ?? false,
|
||||
};
|
||||
this.url = new URL(this.config.wsUrl);
|
||||
this.isBun = typeof Bun !== 'undefined';
|
||||
}
|
||||
|
||||
private log(message: string) {
|
||||
@@ -107,44 +120,91 @@ export class WSKeepAlive {
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on("open", () => {
|
||||
debug && this.log("Connected!");
|
||||
this.reconnectAttempts = 0;
|
||||
this.config.onConnect();
|
||||
this.startPing();
|
||||
});
|
||||
if (this.isBun) {
|
||||
// Bun 环境:使用标准 Web API
|
||||
const ws = this.ws as any;
|
||||
ws.onopen = () => {
|
||||
debug && this.log("Connected!");
|
||||
this.reconnectAttempts = 0;
|
||||
this.config.onConnect();
|
||||
this.startPing();
|
||||
};
|
||||
|
||||
this.ws.on("message", (data: any) => {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
const parsed = this.parseMessage(data);
|
||||
this.config.onMessage(parsed?.raw ?? data);
|
||||
ws.onmessage = async (event: MessageEvent) => {
|
||||
let data: Buffer | string;
|
||||
|
||||
this.messageHandlers.forEach(handler => {
|
||||
if (parsed) handler(parsed);
|
||||
});
|
||||
} else {
|
||||
this.config.onMessage(data);
|
||||
}
|
||||
});
|
||||
if (event.data instanceof Blob) {
|
||||
data = Buffer.from(await event.data.arrayBuffer());
|
||||
} else if (event.data instanceof ArrayBuffer) {
|
||||
data = Buffer.from(event.data);
|
||||
} else if (typeof event.data === 'string') {
|
||||
data = event.data;
|
||||
} else {
|
||||
data = Buffer.from(event.data);
|
||||
}
|
||||
|
||||
this.ws.on("close", (code: number) => {
|
||||
debug && this.log(`Disconnected (code: ${code})`);
|
||||
this.stopPing();
|
||||
this.config.onDisconnect(code);
|
||||
this.handleReconnect();
|
||||
});
|
||||
this.handleMessage(data);
|
||||
};
|
||||
|
||||
this.ws.on("error", (err: Error) => {
|
||||
debug && this.log(`Error: ${err.message}`);
|
||||
this.config.onError(err);
|
||||
});
|
||||
ws.onclose = (event: CloseEvent) => {
|
||||
debug && this.log(`Disconnected (code: ${event.code})`);
|
||||
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() {
|
||||
this.stopPing();
|
||||
this.pingTimer = setInterval(() => {
|
||||
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.config.pingInterval);
|
||||
|
||||
12
test/a-config.ts
Normal file
12
test/a-config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { getConfig } from '../agent/modules/cnb-manager';
|
||||
import { QueryLoginNode } from '@kevisual/api/login-node';
|
||||
const queryLoginNode = new QueryLoginNode({});
|
||||
await queryLoginNode.init()
|
||||
const testConfig = async () => {
|
||||
const token = await queryLoginNode.getToken();
|
||||
console.log('Token:', token);
|
||||
const res = await getConfig({ token });
|
||||
console.log('Config:', res);
|
||||
}
|
||||
|
||||
testConfig();
|
||||
1
test/build-log.ts
Normal file
1
test/build-log.ts
Normal file
@@ -0,0 +1 @@
|
||||
// 不用查看
|
||||
@@ -5,7 +5,7 @@ import { token, showMore, cookie } from "./common.ts";
|
||||
const repo = new Build({ token: token, cookie: cookie });
|
||||
|
||||
const main = async () => {
|
||||
const build = await repo.startBuild('cnb', {
|
||||
const build = await repo.startBuild('kevisual/cnb', {
|
||||
branch: 'main',
|
||||
env: {
|
||||
},
|
||||
@@ -16,3 +16,78 @@ const main = async () => {
|
||||
}
|
||||
|
||||
// main()
|
||||
|
||||
const buildByConfig = async () => {
|
||||
const build = await repo.startBuild('kevisual/cnb', {
|
||||
branch: 'main',
|
||||
env: {
|
||||
},
|
||||
event: 'api_trigger_sync_to_gitea',
|
||||
config: config,
|
||||
});
|
||||
console.log("build", showMore(build));
|
||||
}
|
||||
|
||||
const config = `
|
||||
# .cnb.yml
|
||||
include:
|
||||
- https://cnb.cool/kevisual/cnb/-/blob/main/.cnb/template.yml
|
||||
|
||||
.common_env: &common_env
|
||||
env:
|
||||
TO_REPO: kevisual/cnb
|
||||
TO_URL: git.xiongxiao.me
|
||||
imports:
|
||||
- https://cnb.cool/kevisual/env/-/blob/main/.env.development
|
||||
|
||||
.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
|
||||
`
|
||||
// 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
5
test/get-live-meta.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { cnb, showMore } from './common'
|
||||
|
||||
const meta = await cnb.issue.getAliveMetadata('kevisual/kevisual', 34);
|
||||
|
||||
console.log('meta', showMore(meta));
|
||||
43
test/issue-comment.ts
Normal file
43
test/issue-comment.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Issue } from "../src/issue/index.ts";
|
||||
import { token, showMore, cookie } from "./common.ts";
|
||||
|
||||
const issue = new Issue({
|
||||
token: token,
|
||||
cookie: cookie
|
||||
});
|
||||
|
||||
const repo = "kevisual/dev-env";
|
||||
const issueNumber = 5;
|
||||
|
||||
// 1. 查询评论列表
|
||||
console.log("=== 1. 查询评论列表 ===");
|
||||
const commentListRes = await issue.getCommentList(repo, issueNumber, {
|
||||
page: 1,
|
||||
page_size: 30
|
||||
});
|
||||
console.log(showMore(commentListRes));
|
||||
|
||||
// 2. 创建评论
|
||||
console.log("\n=== 2. 创建评论 ===");
|
||||
const createRes = await issue.createComment(repo, issueNumber, "测试评论内容 " + new Date().toISOString());
|
||||
console.log(showMore(createRes));
|
||||
|
||||
// 如果创建成功,获取评论 ID
|
||||
let commentId = null;
|
||||
if (createRes.code === 200 && createRes.data?.id) {
|
||||
commentId = createRes.data.id;
|
||||
console.log("创建的评论 ID:", commentId);
|
||||
|
||||
// 3. 获取单个评论
|
||||
console.log("\n=== 3. 获取单个评论 ===");
|
||||
const getRes = await issue.getComment(repo, issueNumber, commentId);
|
||||
console.log(showMore(getRes));
|
||||
|
||||
// 4. 修改评论
|
||||
console.log("\n=== 4. 修改评论 ===");
|
||||
const updateRes = await issue.updateComment(repo, issueNumber, commentId, "这是修改后的评论内容 " + new Date().toISOString());
|
||||
console.log(showMore(updateRes));
|
||||
}
|
||||
|
||||
const env = issue.useCommentEnv();
|
||||
process.exit(0);
|
||||
11
test/keep-cookie-get.ts
Normal file
11
test/keep-cookie-get.ts
Normal 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
19
test/keep-file-live.ts
Normal 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');
|
||||
@@ -1,13 +1,16 @@
|
||||
import { createKeepAlive } from "@kevisual/cnb/keep";
|
||||
|
||||
// stable-9184b645cc7aa41b750e2f2ef956f2896512dd84 这个可以修改
|
||||
// reconnectionToken 不能修改
|
||||
// 但是可以删除 reconnectionToken=38837a9e-dd5a-4d28-9ec0-5e5b537a8b0f&skipWebSocketFrames=false
|
||||
const config = {
|
||||
"wss": "wss://cnb-dk4-1jgcjjqvc-001.cnb.space:443/stable-3c0b449c6e6e37b44a8a7938c0d8a3049926a64c?reconnectionToken=d70ab69b-5e92-471a-b3d2-31f554b468d4&reconnection=false&skipWebSocketFrames=false",
|
||||
"cookie": "orange:workspace:cookie-session:cnb-dk4-1jgcjjqvc-001=01fea6db-d73f-4ce8-8929-36903ee7a266",
|
||||
"url": "https://cnb-dk4-1jgcjjqvc-001.cnb.space/?folder=/workspace"
|
||||
"wss": "wss://cnb-708-1ji9sog7o-001.cnb.space:443?skipWebSocketFrames=false",
|
||||
"cookie": "orange:workspace:cookie-session:cnb-708-1ji9sog7o-001=3dc03d84-5617-4e44-a6b9-38ce4398aea5",
|
||||
"url": "https://cnb-708-1ji9sog7o-001.cnb.space/?folder=/workspace"
|
||||
}
|
||||
|
||||
createKeepAlive({
|
||||
wsUrl: config.wss,
|
||||
cookie: config.cookie,
|
||||
debug: true,
|
||||
});
|
||||
|
||||
|
||||
27
test/npc-test.ts
Normal file
27
test/npc-test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { fork } from 'node:child_process';
|
||||
import path from 'node:path';
|
||||
|
||||
const runCmd = () => {
|
||||
const filePath = path.resolve(process.cwd(), 'agent/npc.ts');
|
||||
const child = fork(filePath, {
|
||||
env: {
|
||||
...process.env,
|
||||
// CNB_COMMENT_BODY: '@kevisual/cnb(router) 我的kevisual/cnb的issues列表',
|
||||
CNB_COMMENT_BODY: '关闭仓库的issue。kevisual/cnb/-/issues/5',
|
||||
CNB_ISSUE_ID: '6',
|
||||
CNB_ISSUE_TITLE: '托尔斯泰',
|
||||
CNB_REPO_SLUG: 'kevisual/cnb',
|
||||
},
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
console.error('Error in child process:', err);
|
||||
});
|
||||
|
||||
child.on('exit', (code) => {
|
||||
console.log(`Child process exited with code ${code}`);
|
||||
});
|
||||
}
|
||||
|
||||
runCmd();
|
||||
20
test/repo.ts
20
test/repo.ts
@@ -5,9 +5,19 @@ import { token, showMore, cookie } from "./common.ts";
|
||||
const repo = new Repo({ token: token, cookie: cookie });
|
||||
|
||||
|
||||
const listRes = await repo.getRepoList({
|
||||
page: 1, page_size: 999, role: 'developer',
|
||||
flags: 'KnowledgeBase'
|
||||
});
|
||||
// const listRes = await repo.getRepoList({
|
||||
// page: 1, page_size: 999, role: 'developer',
|
||||
// flags: 'KnowledgeBase'
|
||||
// });
|
||||
|
||||
console.log("listRes", showMore(listRes), listRes.data?.length);
|
||||
// console.log("listRes", showMore(listRes), listRes.data?.length);
|
||||
|
||||
|
||||
const updateRepo = async () => {
|
||||
const res = await repo.updateRepoInfo('kevisual/cnb', {
|
||||
description: 'cnb 的 opencode 和 rest 的 api 进行封装和使用',
|
||||
topics: ['cnb', 'api']
|
||||
});
|
||||
console.log("updateRepo", showMore(res));
|
||||
}
|
||||
updateRepo();
|
||||
@@ -4,10 +4,14 @@ import { token, showMore, cookie } from "./common.ts";
|
||||
|
||||
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" });
|
||||
|
||||
// console.log("accessToken", showMore(accessToken));
|
||||
|
||||
const tokenUser = await user.getUser();
|
||||
|
||||
console.log("tokenUser", showMore(tokenUser));
|
||||
11
test/version.test.ts
Normal file
11
test/version.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { getCNBVersion } from '../src/index.ts';
|
||||
import { describe, it, expect } from 'bun:test';
|
||||
|
||||
describe('getCNBVersion', () => {
|
||||
it('should return version info', async () => {
|
||||
const versionInfo = await getCNBVersion();
|
||||
expect(versionInfo).toHaveProperty('version');
|
||||
expect(versionInfo).toHaveProperty('commitID');
|
||||
expect(versionInfo).toHaveProperty('hash');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user