Compare commits

...

2 Commits

Author SHA1 Message Date
xiongxiao
b15b11fa02 feat: 重构代码结构,更新插件导出,添加构建配置和npm配置文件 2026-01-27 04:12:07 +08:00
xiongxiao
50332fe2f4 feat: update package dependencies and add new routes for CNB environment management
- Updated package.json and pnpm-lock.yaml with new dependencies and versions.
- Removed outdated readme files from requirements.
- Enhanced CNB environment configuration in cnb-env.ts with new VS Code remote SSH settings.
- Modified KnowledgeBase class to return structured results.
- Updated Workspace class to return structured results.
- Implemented new routes for managing CNB cookies and VS Code proxy URIs.
- Added AI chat functionality for querying knowledge base.
- Created skills for cleaning up closed workspaces.
2026-01-27 04:02:34 +08:00
28 changed files with 701 additions and 206 deletions

4
.gitignore vendored
View File

@@ -1,4 +1,6 @@
.env
.env.local
node_modules
.pnpm-store
.pnpm-store
dist

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
//npm.xiongxiao.me/:_authToken=${ME_NPM_TOKEN}
//npm.cnb.cool/kevisual/registry/-/packages/:_authToken=${CNB_API_KEY}
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View File

@@ -1 +1 @@
export * from "../../agent/opencode-plugin";
export * from "../../agent/opencode";

View File

@@ -1,6 +0,0 @@
{
"code-runner.executorMap": {
"ts": "opencode run $selectedText"
},
"code-runner.runInTerminal": true
}

View File

@@ -6,7 +6,10 @@ import { nanoid } from 'nanoid';
export const config = useConfig()
export const cnb = useContextKey<CNB>('cnb', () => {
const token = useKey('CNB_API_KEY') as string
// 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 });
})

View File

@@ -1,86 +0,0 @@
import { tool } from "@opencode-ai/plugin/tool"
import { type Plugin } from "@opencode-ai/plugin"
import { app, cnb, appId } from './index.ts';
import { } from 'es-toolkit'
import { Skill } from "@kevisual/router";
const routes = app.routes.filter(r => {
const metadata = r.metadata as Skill
if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
return !!metadata.skill
}
return false
})
// opencode run "请使用 cnb-login-verify 工具验证登录信信息,检查cookie"
export const CnbPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
'tool': {
...routes.reduce((acc, route) => {
const metadata = route.metadata as Skill
acc[metadata.skill!] = {
name: metadata.title || metadata.skill,
description: metadata.summary || '',
args: metadata.args || {},
async execute(args: Record<string, any>) {
console.log(`Executing skill ${metadata.skill} with args:`, args);
await client.app.log({
body: {
service: 'cnb',
level: 'info',
message: `Executing skill ${metadata.skill} with args: ${JSON.stringify(args)}`
}
});
const res = await app.run({
path: route.path,
key: route.key,
payload: args
}, { appId });
if (res.code === 200) {
if (res.data?.content) {
return res.data.content;
}
const str = JSON.stringify(res.data || res, null, 2);
if (str.length > 5000) {
return str.slice(0, 5000) + '... (truncated)';
}
return str;
}
return `Error: ${res?.message || '无法获取结果'}`;
}
}
return acc;
}, {} as Record<string, any>)
},
'tool.execute.before': async (opts) => {
// console.log('CnbPlugin: tool.execute.before', opts.tool);
// delete toolSkills['cnb-login-verify']
}
}
}
const demo = {
'tool': {
"cnb-login-verify": {
name: "CNB 登录验证信息",
description: "验证 CNB 登录信息是否有效",
args: {
checkToken: tool.schema.boolean().describe("是否检查 Token 的有效性").default(true),
checkCookie: tool.schema.boolean().describe("是否检查 Cookie 的有效性").default(false),
},
async execute(args) {
const res = await app.run({
path: 'cnb',
key: 'user-check',
payload: {
...args
}
}, { appId });
if (res.code === 200) {
return res.data?.output;
}
return '无法获取登录状态,请检查配置。';
},
},
}
}

6
agent/opencode.ts Normal file
View File

@@ -0,0 +1,6 @@
import { app} from './index.ts';
import { createRouterAgentPluginFn } from '@kevisual/router/opencode'
export const CnbPlugin = createRouterAgentPluginFn({
router: app,
})

View File

@@ -1 +0,0 @@
path: cnb key: get-repo-list payload: { page: 1, per_page: 10 }

View File

@@ -0,0 +1,48 @@
import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts';
// 设置 CNB_COOKIE环境变量和获取环境变量,用于界面操作定制模块功能
app.route({
path: 'cnb',
key: 'set-cnb-cookie',
description: '设置当前cnb工作空间的cookie环境变量',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'set-cnb-cookie',
title: '设置当前cnb工作空间的cookie环境变量',
summary: '设置当前cnb工作空间的cookie环境变量用于界面操作定制模块功能,例子CNBSESSION=xxxx;csrfkey=2222xxxx;',
args: {
cookie: tool.schema.string().describe('cnb的cookie值'),
}
})
}
}).define(async (ctx) => {
const cookie = ctx.query?.cookie;
if (!cookie) {
ctx.body = { content: '请提供有效的cookie值' };
return;
}
cnb.cookie = cookie;
ctx.body = { content: '已成功设置cnb的cookie环境变量' };
}).addTo(app);
// 获取 CNB_COOKIE环境变量
app.route({
path: 'cnb',
key: 'get-cnb-cookie',
description: '获取当前cnb工作空间的cookie环境变量',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'get-cnb-cookie',
title: '获取当前cnb工作空间的cookie环境变量',
summary: '获取当前cnb工作空间的cookie环境变量用于界面操作定制模块功能',
})
}
}).define(async (ctx) => {
const cookie = cnb.cookie || '未设置cookie环境变量';
ctx.body = { content: `当前cnb工作空间的cookie环境变量为${cookie}` };
}).addTo(app);

View File

@@ -1 +1,3 @@
// 根据环境变量获取当前的 cnb 启动环境
// 根据环境变量获取当前的 cnb 启动环境
import './vscode.ts';
import './env.ts';

View File

@@ -0,0 +1,94 @@
import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts';
import { CNB_ENV } from "@/common/cnb-env.ts";
// 执行技能 get-cnb-port-uri端口为4096
// 执行技能 get-cnb-port-uri端口为51515
app.route({
path: 'cnb',
key: 'get-cnb-port-uri',
description: '获取当前cnb工作空间的port代理uri',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'get-cnb-port-uri',
title: '获取当前cnb工作空间的port代理uri',
summary: '获取当前cnb工作空间的port代理uri用于端口转发',
args: {
port: tool.schema.number().optional().describe('端口号默认为4096'),
}
})
}
}).define(async (ctx) => {
const port = ctx.query?.port || 4096;
const uri = CNB_ENV?.CNB_VSCODE_PROXY_URI as string || '';
const finalUri = uri.replace('{{port}}', port.toString());
let content = `
cnb工作空间的访问uri为${finalUri}
`
ctx.body = { content };
}).addTo(app);
// 获取当前cnb工作空间的vscode代理uri执行技能 get-cnb-vscode-uri
// 包括 web 访问urivscode 访问uricodebuddy 访问uricursor 访问urissh 连接字符串
app.route({
path: 'cnb',
key: 'get-cnb-vscode-uri',
description: '获取当前cnb工作空间的vscode代理uri, 包括多种访问方式, 如web、vscode、codebuddy、cursor、ssh',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'get-cnb-vscode-uri',
title: '获取当前cnb工作空间的编辑器访问地址',
summary: '获取当前cnb工作空间的vscode代理uri用于在浏览器中访问vscode包含多种访问方式如web、vscode、codebuddy、cursor、ssh',
args: {
web: tool.schema.boolean().optional().describe('是否获取vscode web的访问uri默认为false'),
vscode: tool.schema.boolean().optional().describe('是否获取vscode的代理uri默认为true'),
codebuddy: tool.schema.boolean().optional().describe('是否获取codebuddy的代理uri默认为false'),
cursor: tool.schema.boolean().optional().describe('是否获取cursor的代理uri默认为false'),
// trae: tool.schema.boolean().optional().describe('是否获取trae的代理uri默认为false'),
ssh: tool.schema.boolean().optional().describe('是否获取vscode remote ssh的连接字符串默认为false'),
}
})
}
}).define(async (ctx) => {
const web = ctx.query?.web ?? false;
const vscode = ctx.query?.vscode ?? true; // 默认true
const codebuddy = ctx.query?.codebuddy ?? false;
const cursor = ctx.query?.cursor ?? false;
// const trae = ctx.query?.trae ?? false;
const ssh = ctx.query?.ssh ?? false;
const webUri = CNB_ENV?.CNB_VSCODE_WEB_URL as string || '';
const vscodeSchma = CNB_ENV?.CNB_VSCODE_REMOTE_SSH_SCHEMA as string || '';
let content = `
当前的cnb 仓库为:${CNB_ENV?.CNB_REPO_SLUG}
`
if (web) {
content += `VS Code Web 访问 URI${webUri}\n\n`;
}
if (vscode) {
content += `VS Code 访问 URI${vscodeSchma}\n\n`;
}
if (codebuddy) {
const codebuddyUri = vscodeSchma.replace('vscode://vscode-remote/ssh-remote+', 'codebuddycn://vscode-remote/codebuddy-remote');
content += `CodeBuddy 访问 URI${codebuddyUri}\n\n`;
}
if (cursor) {
const cursorUri = vscodeSchma.replace('vscode://', 'cursor://');
content += `Cursor 访问 URI${cursorUri}\n\n`;
}
// if (trae) {
// const traeUri = vscodeSchma.replace('vscode://vscode-remote/ssh-remote+', 'traecn://ssh-remote+');
// content += `Trae 访问 URI${traeUri}\n\n`;
// }
if (ssh) {
content += `VS Code Remote SSH 连接字符串ssh ${CNB_ENV.CNB_PIPELINE_ID}.${CNB_ENV.CNB_VSCODE_SSH_TOKEN}@cnb.space`;
}
ctx.body = { content };
}).addTo(app);

View File

@@ -3,6 +3,8 @@ import './user/check.ts'
import './repo/index.ts'
import './workspace/index.ts'
import './call/index.ts'
import './cnb-env/index.ts'
import './knowledge/index.ts'
import { isEqual } from 'es-toolkit'
/**

View File

@@ -0,0 +1,142 @@
import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts';
import { CNBChat } from '@kevisual/ai/browser'
/**
调用cnb-ai-chat技能, repo为kevisual/starred-auto.
问题是用户提供的问题是OpenListTeam/OpenList是什么有多少star
*/
app.route({
path: 'cnb',
key: 'cnb-ai-chat',
description: '调用cnb的知识库ai对话功能进行聊天',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'cnb-ai-chat',
title: '调用cnb的知识库ai对话功能进行聊天',
summary: '调用cnb的知识库ai对话功能进行聊天基于cnb提供的ai能力',
args: {
question: tool.schema.string().describe('用户输入的消息内容'),
repo: tool.schema.string().optional().describe('知识库仓库ID默认为空表示使用默认知识库'),
}
})
}
}).define(async (ctx) => {
const question = ctx.query?.question;
if (!question) {
ctx.body = { content: '请提供有效的消息内容' };
return;
}
let repo = ctx.query?.repo;
if (!repo) {
// 如果未指定知识库仓库ID则使用默认知识库
const res = await cnb.repo.getRepoList({ flags: 'KnowledgeBase' });
if (res.code === 200 && res.data.length > 0) {
repo = res.data[0].path;
}
}
console.log("Using knowledge base repo:", repo);
const ragRes = await cnb.knowledgeBase.queryKnowledgeBase(repo || '', {
query: question,
score_threshold: 0.62,
top_k: 10,
});
if (ragRes.code !== 200) {
ctx.body = { content: `查询知识库失败,错误信息:${ragRes.message}` };
return;
}
const list = ragRes.data || [];
// 构建RAG上下文包含文件来源和相似度信息
const ragContext = list.map((item, index) => {
const source = item.metadata?.path || item.metadata?.name || '未知来源';
const type = item.metadata?.type === 'code' ? '〔代码〕' : '〔文档〕';
const scorePercent = Math.round((item.score || 0) * 100);
const url = item.metadata?.url || '无';
return `〔来源${index + 1}${type} ${source} (相似度: ${scorePercent}%)\n${item.chunk}\n访问地址: ${url}`;
}).join('\n\n---\n\n');
// hunyuan-a13b
// enable_thinking
const chat = new CNBChat({
repo,
token: cnb.token,
model: 'hunyuan-a13b'
});
const messages = [
{
role: 'system',
content: `[角色定义]='''\n你是一个专业的技术助手擅长基于提供的知识库内容进行准确、有条理的分析和回答。你的特点是\n1. 严格基于RAG检索到的上下文内容进行回答不添加未经验证的信息\n2. 回答时清晰标注信息来源,便于用户追溯查证\n3. 面对不确定的信息,明确标注「根据提供的内容无法确定」\n4. 代码相关问题注重可执行性和最佳实践\n'''[要求]='''\n1. 严格遵循用户的提问要求,优先解决用户的核心问题\n2. 避免侵犯版权的内容不复制原文超过100字技术术语和函数名除外\n3. 使用中文进行响应,语言专业且易于理解\n4. 如果上下文存在多个来源,优先使用相似度更高的内容\n5. 对于代码片段,确保完整且可直接使用\n6. 当上下文中没有相关信息时,直接说明「知识库中未找到相关内容」\n7. 在思考过程中分析:用户的真实意图是什么?提供的上下文是否足够回答?\n'''[回答格式]='''\n- 先简要说明回答的核心结论\n- 如有必要,分点阐述详细分析过程\n- 标注关键信息来源标注【来源X】即可\n- 提供可操作的建议或代码示例\n'''`
},
{
role: 'user',
content: `[上下文]='''\n${ragContext}\n'''\n\n[用户问题]='''\n${question}\n'''\n\n请基于以上上下文知识库内容回答用户问题。`
}
] as Array<{ role: 'system' | 'user' | 'assistant', content: string }>;
const response = await chat.chat({
messages
});
const txt = chat.responseText;
ctx.body = { content: txt, response };
}).addTo(app);
// RAG知识库查询技能: 查询openlist有多少star
app.route({
path: 'cnb',
key: 'cnb-rag-query',
description: '调用cnb的知识库RAG查询功能进行问答',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'cnb-rag-query',
title: '调用cnb的知识库RAG查询功能进行问答',
summary: '调用cnb的知识库RAG查询功能进行问答基于cnb提供的知识库能力',
args: {
question: tool.schema.string().describe('用户输入的消息内容'),
repo: tool.schema.string().optional().describe('知识库仓库ID默认为空表示使用默认知识库'),
}
})
}
}).define(async (ctx) => {
const question = ctx.query?.question;
if (!question) {
ctx.body = { content: '请提供有效的消息内容' };
return;
}
let repo = ctx.query?.repo;
if (!repo) {
// 如果未指定知识库仓库ID则使用默认知识库
const res = await cnb.repo.getRepoList({ flags: 'KnowledgeBase' });
if (res.code === 200 && res.data.length > 0) {
repo = res.data[0].path;
}
}
console.log("Using knowledge base repo:", repo);
const ragRes = await cnb.knowledgeBase.queryKnowledgeBase(repo || '', {
query: question,
score_threshold: 0.62,
top_k: 10,
});
if (ragRes.code !== 200) {
ctx.body = { content: `查询知识库失败,错误信息:${ragRes.message}` };
return;
}
const list = ragRes.data || [];
let answer = `基于知识库「${repo}」的查询结果:\n\n`;
if (list.length === 0) {
answer += '知识库中未找到相关内容。';
} else {
list.forEach((item, index) => {
const source = item.metadata?.path || item.metadata?.name || '未知来源';
const type = item.metadata?.type === 'code' ? '〔代码〕' : '〔文档〕';
const scorePercent = Math.round((item.score || 0) * 100);
const url = item.metadata?.url || '无';
answer += `【来源${index + 1}${type} ${source} (相似度: ${scorePercent}%)\n${item.chunk}\n访问地址: ${url}\n\n`;
});
}
ctx.body = { content: answer };
}).addTo(app);

View File

@@ -0,0 +1 @@
import './ai.ts'

View File

@@ -2,6 +2,8 @@ import { createSkill } from '@kevisual/router';
import { app, cnb } from '../../app.ts';
import { tool } from "@opencode-ai/plugin/tool"
// "列出我的代码仓库search blog"
// 列出我的知识库的代码仓库
app.route({
path: 'cnb',
key: 'list-repos',
@@ -11,18 +13,24 @@ app.route({
tags: ['opencode'],
...createSkill({
skill: 'list-repos',
title: '列出代码仓库',
summary: '列出代码仓库',
title: '列出cnb代码仓库',
summary: '列出cnb代码仓库, 可选flags参数如 KnowledgeBase',
args: {
search: tool.schema.string().optional().describe('搜索关键词'),
pageSize: tool.schema.number().optional().describe('每页数量默认999'),
flags: tool.schema.string().optional().describe('仓库标记,如果是知识库则填写 KnowledgeBase'),
},
})
}
}).define(async (ctx) => {
const search = ctx.query?.search;
const pageSize = ctx.query?.pageSize || 9999;
const res = await cnb.repo.getRepoList({ search, page_size: pageSize, role: 'developer' });
const flags = ctx.query?.flags;
const params: any = {};
if (flags) {
params.flags = flags;
}
const res = await cnb.repo.getRepoList({ search, page_size: pageSize, role: 'developer', ...params });
if (res.code === 200) {
const repos = res.data.map((item) => ({
name: item.name,
@@ -30,7 +38,7 @@ app.route({
description: item.description,
web_url: item.web_url,
}));
ctx.body = { content: JSON.stringify(repos) };
ctx.body = { content: JSON.stringify(repos), list: res.data };
} else {
ctx.throw(500, '获取仓库列表失败');
}

View File

@@ -1,6 +1,8 @@
import { app, cnb } from '../../app.ts';
import { createSkill, Skill } from '@kevisual/router'
import { tool } from "@opencode-ai/plugin/tool"
// 创建一个仓库 kevisual/test-repo
app.route({
path: 'cnb',
key: 'create-repo',
@@ -79,7 +81,7 @@ app.route({
ctx.forward(res);
}).addTo(app);
// 删除一个仓库 kevisual/test-repo
app.route({
path: 'cnb',
key: 'delete-repo',

View File

@@ -1,12 +1,26 @@
import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts';
import z from 'zod';
import './skills.ts';
// 启动工作空间
app.route({
path: 'cnb',
key: 'start-workspace',
description: '启动开发工作空间, 参数 repo',
middleware: ['auth'],
metadata: {
tags: ['opencode']
tags: ['opencode'],
...createSkill({
skill: 'start-workspace',
title: '启动cnb工作空间',
summary: '启动cnb工作空间',
args: {
repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
branch: tool.schema.string().optional().describe('分支名称,默认主分支'),
ref: tool.schema.string().optional().describe('提交引用,例如 commit sha'),
},
})
}
}).define(async (ctx) => {
const repo = ctx.query?.repo;
@@ -21,3 +35,133 @@ app.route({
});
ctx.forward(res);
}).addTo(app);
// 获取cnb工作空间列表
app.route({
path: 'cnb',
key: 'list-workspace',
description: '获取cnb开发工作空间列表可选参数 status=running 获取运行中的环境',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'list-workspace',
title: '列出cnb工作空间',
summary: '列出cnb工作空间列表支持按状态过滤 status 可选值 running 或 closed',
args: {
status: tool.schema.string().optional().describe('开发环境状态running: 运行中closed: 已关闭和停止的'),
page: tool.schema.number().optional().describe('分页页码,默认 1'),
pageSize: tool.schema.number().optional().describe('分页大小,默认 20最大 100'),
slug: tool.schema.string().optional().describe('仓库路径,例如 groupname/reponame'),
branch: tool.schema.string().optional().describe('分支名称'),
},
})
}
}).define(async (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 });
}).addTo(app);
// 获取工作空间详情
app.route({
path: 'cnb',
key: 'get-workspace',
description: '获取工作空间详情,通过 repo 和 sn 获取',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'get-workspace',
title: '获取工作空间详情',
summary: '获取工作空间详细信息',
args: {
repo: tool.schema.string().describe('代码仓库路径,例如 user/repo'),
sn: tool.schema.string().describe('工作空间流水线的 sn'),
},
})
}
}).define(async (ctx) => {
const repo = ctx.query?.repo;
const sn = ctx.query?.sn;
if (!repo) {
ctx.throw(400, '缺少参数 repo');
}
if (!sn) {
ctx.throw(400, '缺少参数 sn');
}
const res = await cnb.workspace.getDetail(repo, sn);
ctx.forward({ code: 200, message: 'success', data: res });
}).addTo(app);
// 删除工作空间
app.route({
path: 'cnb',
key: 'delete-workspace',
description: '删除工作空间,通过 pipelineId 或 sn',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'delete-workspace',
title: '删除工作空间',
summary: '删除工作空间pipelineId 和 sn 二选一',
args: {
pipelineId: tool.schema.string().optional().describe('流水线 ID优先使用'),
sn: tool.schema.string().optional().describe('流水线构建号'),
sns: tool.schema.array(z.string()).optional().describe('流水线构建号'),
},
})
}
}).define(async (ctx) => {
const pipelineId = ctx.query?.pipelineId;
const sn = ctx.query?.sn;
const sns = ctx.query?.sns;
if (!pipelineId && !sn && (!sns || sns.length === 0)) {
ctx.throw(400, 'pipelineId 和 sn 必须提供其中一个');
}
if (sns && sns.length > 0) {
const results = [];
for (const snItem of sns) {
const res = await cnb.workspace.deleteWorkspace({ sn: snItem });
results.push(res);
}
ctx.forward({ code: 200, message: 'success', data: results });
return;
}
const res = await cnb.workspace.deleteWorkspace({ pipelineId, sn });
ctx.forward(res);
}).addTo(app);
// 停止工作空间
app.route({
path: 'cnb',
key: 'stop-workspace',
description: '停止工作空间,通过 pipelineId 或 sn',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'stop-workspace',
title: '停止工作空间',
summary: '停止运行中的工作空间',
args: {
pipelineId: tool.schema.string().optional().describe('流水线 ID优先使用'),
sn: tool.schema.string().optional().describe('流水线构建号'),
},
})
}
}).define(async (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 });
}).addTo(app);

View File

@@ -0,0 +1,64 @@
import { createSkill, tool } from '@kevisual/router';
import { app, cnb } from '../../app.ts';
// 批量删除已停止的cnb工作空间
// app.route({
// path: 'cnb',
// key: 'clean-closed-workspace-skill',
// description: '批量删除已停止的cnb工作空间',
// middleware: ['auth'],
// metadata: {
// tags: ['opencode'],
// ...createSkill({
// skill: 'clean-closed-workspace-skill',
// title: '清理已关闭的cnb工作空间',
// summary: '批量删除已停止的cnb工作空间释放资源',
// args: {
// question: tool.schema.string().optional().describe('具体的要求的信息'),
// }
// })
// }
// }).define(async (ctx) => {
// const question = ctx.query?.question || '';
// let content = `这是一个技能任务, 批量删除已停止的cnb工作空间释放资源
// 执行步骤:
// 1. 执行list-workspace获取状态为 closed 的工作空间列表提取sn
// 2. 执行delete-workspace技能传入sns列表的数组批量删除工作空间`
// if (question) {
// content += `\n注意用户的具体要求是${question}`;
// }
// ctx.body = { content }
// }).addTo(app);
// 批量删除已停止的cnb工作空间
app.route({
path: 'cnb',
key: 'clean-closed-workspace',
description: '批量删除已停止的cnb工作空间',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'clean-closed-workspace',
title: '清理已关闭的cnb工作空间',
summary: '批量删除已停止的cnb工作空间释放资源',
})
}
}).define(async (ctx) => {
const closedWorkspaces = await cnb.workspace.list({ status: 'closed' });
if (closedWorkspaces.code !== 200) {
ctx.throw(500, '获取已关闭工作空间列表失败');
}
const list = closedWorkspaces.data?.list || [];
if (list.length === 0) {
ctx.forward({ code: 200, message: '没有已关闭的工作空间需要删除', data: [] });
return;
}
const sns = list.map(ws => ws.sn);
const results = [];
for (const sn of sns) {
const res = await cnb.workspace.deleteWorkspace({ sn });
results.push(res);
}
ctx.forward({ code: 200, message: '已关闭的工作空间删除完成', data: results });
}).addTo(app);

View File

@@ -0,0 +1,19 @@
import { resolvePath } from '@kevisual/use-config';
import { execSync } from 'node:child_process';
const entry = 'agent/opencode.ts';
const naming = 'opencode';
const external: string[] = ["bun"];
await Bun.build({
target: 'node',
format: 'esm',
entrypoints: [resolvePath(entry, { meta: import.meta })],
outdir: resolvePath('./dist', { meta: import.meta }),
naming: {
entry: `${naming}.js`,
},
external,
});
const cmd = 'dts -i agent/opencode.ts -o opencode.d.ts';
execSync(cmd);

View File

@@ -1,38 +1,46 @@
{
"name": "@kevisual/cnb",
"version": "0.0.1",
"version": "0.0.3",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "bun run bun.config.ts"
},
"keywords": [],
"files": [
"dist",
"src",
"mod.ts",
"agent"
],
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
"license": "MIT",
"packageManager": "pnpm@10.28.0",
"packageManager": "pnpm@10.28.2",
"type": "module",
"devDependencies": {
"@kevisual/ai": "^0.0.22",
"@kevisual/context": "^0.0.4",
"@kevisual/types": "^0.0.11",
"@opencode-ai/plugin": "^1.1.23",
"@kevisual/types": "^0.0.12",
"@opencode-ai/plugin": "^1.1.36",
"@types/bun": "^1.3.6",
"@types/node": "^25.0.9",
"@types/node": "^25.0.10",
"dotenv": "^17.2.3"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@kevisual/query": "^0.0.35",
"@kevisual/router": "^0.0.55",
"@kevisual/query": "^0.0.38",
"@kevisual/router": "^0.0.62",
"@kevisual/use-config": "^1.0.28",
"es-toolkit": "^1.43.0",
"es-toolkit": "^1.44.0",
"nanoid": "^5.1.6",
"zod": "^4.3.5"
"zod": "^4.3.6"
},
"exports": {
".": "./mod.ts",
"./opencode": "./dist/opencode.js",
"./src/*": "./src/*",
"./agent/*": "./agent/*"
}
}

110
pnpm-lock.yaml generated
View File

@@ -9,76 +9,88 @@ importers:
.:
dependencies:
'@kevisual/query':
specifier: ^0.0.35
version: 0.0.35
specifier: ^0.0.38
version: 0.0.38
'@kevisual/router':
specifier: ^0.0.55
version: 0.0.55
specifier: ^0.0.62
version: 0.0.62
'@kevisual/use-config':
specifier: ^1.0.28
version: 1.0.28(dotenv@17.2.3)
es-toolkit:
specifier: ^1.43.0
version: 1.43.0
specifier: ^1.44.0
version: 1.44.0
nanoid:
specifier: ^5.1.6
version: 5.1.6
zod:
specifier: ^4.3.5
version: 4.3.5
specifier: ^4.3.6
version: 4.3.6
devDependencies:
'@kevisual/ai':
specifier: ^0.0.22
version: 0.0.22
'@kevisual/context':
specifier: ^0.0.4
version: 0.0.4
'@kevisual/types':
specifier: ^0.0.11
version: 0.0.11
specifier: ^0.0.12
version: 0.0.12
'@opencode-ai/plugin':
specifier: ^1.1.23
version: 1.1.23
specifier: ^1.1.36
version: 1.1.36
'@types/bun':
specifier: ^1.3.6
version: 1.3.6
'@types/node':
specifier: ^25.0.9
version: 25.0.9
specifier: ^25.0.10
version: 25.0.10
dotenv:
specifier: ^17.2.3
version: 17.2.3
packages:
'@kevisual/ai@0.0.22':
resolution: {integrity: sha512-9kth0pvPD4jT8c6rJ0DzSuvUY0s0pdw+CnO+YU8bwpMg04k1jFkd1RSZdtFrVbc60e2cpooQMUt/oimkfs082A==}
'@kevisual/context@0.0.4':
resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==}
'@kevisual/load@0.0.6':
resolution: {integrity: sha512-+3YTFehRcZ1haGel5DKYMUwmi5i6f2psyaPZlfkKU/cOXgkpwoG9/BEqPCnPjicKqqnksEpixVRkyHJ+5bjLVA==}
'@kevisual/query@0.0.35':
resolution: {integrity: sha512-80dyy2LMCmEC72g+X4QWUKlZErhawQPgnGSBNR4yhrBcFgHIJQ14LR1Z+bS5S1I7db+1PDNpaxBTjIaoYoXunw==}
'@kevisual/logger@0.0.4':
resolution: {integrity: sha512-+fpr92eokSxoGOW1SIRl/27lPuO+zyY+feR5o2Q4YCNlAdt2x64NwC/w8r/3NEC5QenLgd4K0azyKTI2mHbARw==}
'@kevisual/router@0.0.55':
resolution: {integrity: sha512-DVhXbbUCfSWWXsp1id1HBrkGiMZ6nFUBD1/C5E7IpLE5B32w7sv2xjKUt98OriFl0uyuneMEIZuZsAQaKplQ5g==}
'@kevisual/permission@0.0.3':
resolution: {integrity: sha512-8JsA/5O5Ax/z+M+MYpFYdlioHE6jNmWMuFSokBWYs9CCAHNiSKMR01YLkoVDoPvncfH/Y8F5K/IEXRCbptuMNA==}
'@kevisual/types@0.0.11':
resolution: {integrity: sha512-idNLDTEKVdNXZHFQq8PTN62nflh94kvGtx+v8YDcMxt0Zo+HWVZTFElm+dMQxAs/vn4wo8F2r3VwzWNX/vcqwQ==}
'@kevisual/query@0.0.38':
resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==}
'@kevisual/router@0.0.62':
resolution: {integrity: sha512-kKQFYkJ/qemGAygGSKkM/TujvreoU9HxL7/2vx2RYDMRyRuZOYUuvrTF92XIffFnZHugFbs8uEt+Z8I11SrFUg==}
'@kevisual/types@0.0.12':
resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==}
'@kevisual/use-config@1.0.28':
resolution: {integrity: sha512-ngF+LDbjxpXWrZNmnShIKF/jPpAa+ezV+DcgoZIIzHlRnIjE+rr9sLkN/B7WJbiH9C/j1tQXOILY8ujBqILrow==}
peerDependencies:
dotenv: ^17
'@opencode-ai/plugin@1.1.23':
resolution: {integrity: sha512-O/iLSKOUuzD95UWhj9y/tEuycPEBv36de0suHXXqeYLWZLZ16DAUSKR+YG7rvRjJS0sbn4biVMw+k7XXk/oxiQ==}
'@opencode-ai/plugin@1.1.36':
resolution: {integrity: sha512-b2XWeFZN7UzgwkkzTIi6qSntkpEA9En2zvpqakQzZAGQm6QBdGAlv6r1u5hEnmF12Gzyj5umTMWr5GzVbP/oAA==}
'@opencode-ai/sdk@1.1.23':
resolution: {integrity: sha512-YjN9ogzkLol92s+/iARXRop9/5oFIezUkvWVay12u1IM6A/WJs50DeKl3oL0x4a68P1a5tI5gD98dLnk2+AlsA==}
'@opencode-ai/sdk@1.1.36':
resolution: {integrity: sha512-feNHWnbxhg03TI2QrWnw3Chc0eYrWSDSmHIy/ejpSVfcKlfXREw1Tpg0L4EjrpeSc4jB1eM673dh+WM/Ko2SFQ==}
'@types/bun@1.3.6':
resolution: {integrity: sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA==}
'@types/node@25.0.9':
resolution: {integrity: sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw==}
'@types/node@25.0.10':
resolution: {integrity: sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==}
bun-types@1.3.6:
resolution: {integrity: sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ==}
@@ -87,12 +99,16 @@ packages:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
es-toolkit@1.43.0:
resolution: {integrity: sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==}
es-toolkit@1.44.0:
resolution: {integrity: sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==}
eventemitter3@5.0.1:
resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==}
hono@4.11.6:
resolution: {integrity: sha512-ofIiiHyl34SV6AuhE3YT2mhO5HRWokce+eUYE82TsP6z0/H3JeJcjVWEMSIAiw2QkjDOEpES/lYsg8eEbsLtdw==}
engines: {node: '>=16.9.0'}
nanoid@5.1.6:
resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==}
engines: {node: ^18 || >=20}
@@ -107,55 +123,69 @@ packages:
zod@4.1.8:
resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==}
zod@4.3.5:
resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==}
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
snapshots:
'@kevisual/ai@0.0.22':
dependencies:
'@kevisual/logger': 0.0.4
'@kevisual/permission': 0.0.3
'@kevisual/query': 0.0.38
'@kevisual/context@0.0.4': {}
'@kevisual/load@0.0.6':
dependencies:
eventemitter3: 5.0.1
'@kevisual/query@0.0.35':
'@kevisual/logger@0.0.4': {}
'@kevisual/permission@0.0.3': {}
'@kevisual/query@0.0.38':
dependencies:
tslib: 2.8.1
'@kevisual/router@0.0.55': {}
'@kevisual/router@0.0.62':
dependencies:
hono: 4.11.6
'@kevisual/types@0.0.11': {}
'@kevisual/types@0.0.12': {}
'@kevisual/use-config@1.0.28(dotenv@17.2.3)':
dependencies:
'@kevisual/load': 0.0.6
dotenv: 17.2.3
'@opencode-ai/plugin@1.1.23':
'@opencode-ai/plugin@1.1.36':
dependencies:
'@opencode-ai/sdk': 1.1.23
'@opencode-ai/sdk': 1.1.36
zod: 4.1.8
'@opencode-ai/sdk@1.1.23': {}
'@opencode-ai/sdk@1.1.36': {}
'@types/bun@1.3.6':
dependencies:
bun-types: 1.3.6
'@types/node@25.0.9':
'@types/node@25.0.10':
dependencies:
undici-types: 7.16.0
bun-types@1.3.6:
dependencies:
'@types/node': 25.0.9
'@types/node': 25.0.10
dotenv@17.2.3: {}
es-toolkit@1.43.0: {}
es-toolkit@1.44.0: {}
eventemitter3@5.0.1: {}
hono@4.11.6: {}
nanoid@5.1.6: {}
tslib@2.8.1: {}
@@ -164,4 +194,4 @@ snapshots:
zod@4.1.8: {}
zod@4.3.5: {}
zod@4.3.6: {}

View File

@@ -1,16 +0,0 @@
对/agents 下的代码进行修改
需求1
创建仓库, 需要仓库名字
1. 自动添加.cnb.yml, 对.cnb.yml的内容进行修改。修改TO_REPO对应的内容 TO_REPO是“group+repo”需要传入仓库名字默认group为kevisual
2. 自动添加opencode.json
需求2
获取当前启动的云开发环境。
输出对应的请求的地址token等信息
输出打开云开发的vscodebuddycursortraeqcoder等工具的地址
需求3

View File

@@ -7,41 +7,51 @@
import { useKey } from "@kevisual/use-config"
export const CNB_ENV = {
// 仓库相关配置
/** 仓库的 HTTPS 地址,如 "https://cnb.cool/kevisual/cnb" */
CNB_REPO_URL_HTTPS: useKey('CNB_REPO_URL_HTTPS'),
// 仓库相关配置
/** 仓库的 HTTPS 地址,如 "https://cnb.cool/kevisual/cnb" */
CNB_REPO_URL_HTTPS: useKey('CNB_REPO_URL_HTTPS'),
// 构建相关配置
/** 流水线 ID唯一标识一次构建流水线如 "cnb-108-1jer5qekq-001" */
CNB_PIPELINE_ID: useKey('CNB_PIPELINE_ID'),
/** 构建 ID与流水线 ID 相关联,如 "cnb-108-1jer5qekq" */
CNB_BUILD_ID: useKey('CNB_BUILD_ID'),
/** 构建开始时间ISO 8601 格式,如 "2026-01-13T07:58:41.825Z" */
CNB_BUILD_START_TIME: useKey('CNB_BUILD_START_TIME'),
/** 构建日志 Web 界面 URL用于在浏览器中查看构建日志 */
CNB_BUILD_WEB_URL: useKey('CNB_BUILD_WEB_URL'),
/** 触发构建的事件类型,如 "vscode" 表示由 VS Code 触发 */
CNB_EVENT: useKey('CNB_EVENT'),
/** 当前构建对应的 Git 提交哈希值 */
CNB_COMMIT: useKey('CNB_COMMIT'),
// 构建相关配置
/** 流水线 ID唯一标识一次构建流水线如 "cnb-upo-1jfth1771-001" */
CNB_PIPELINE_ID: useKey('CNB_PIPELINE_ID'),
/** 构建 ID与流水线 ID 相关联,如 "cnb-upo-1jfth1771" */
CNB_BUILD_ID: useKey('CNB_BUILD_ID'),
/** 构建开始时间ISO 8601 格式,如 "2026-01-13T07:58:41.825Z" */
CNB_BUILD_START_TIME: useKey('CNB_BUILD_START_TIME'),
/** 构建日志 Web 界面 URL用于在浏览器中查看构建日志 */
CNB_BUILD_WEB_URL: useKey('CNB_BUILD_WEB_URL'),
/** 触发构建的事件类型,如 "vscode" 表示由 VS Code 触发 */
CNB_EVENT: useKey('CNB_EVENT'),
/** 当前构建对应的 Git 提交哈希值 */
CNB_COMMIT: useKey('CNB_COMMIT'),
// VS Code 相关配置
/** VS Code Web 界面的访问 URL用于在浏览器中打开 VS Code */
CNB_VSCODE_WEB_URL: useKey('CNB_VSCODE_WEB_URL'),
/** VS Code 代理 URI用于端口转发{{port}} 会被替换为实际端口号, 例如: "https://1wnts22gq3-{{port}}.cnb.run"*/
CNB_VSCODE_PROXY_URI: useKey('CNB_VSCODE_PROXY_URI'),
// VS Code 相关配置
/** VS Code Web 界面的访问 URL用于在浏览器中打开 VS Code,例如:'https://cnb.cool/kevisual/cnb/-/workspace/vscode-web/cnb-upo-1jfth1771-001/'*/
CNB_VSCODE_WEB_URL: useKey('CNB_VSCODE_WEB_URL'),
/** VS Code 代理 URI用于端口转发{{port}} 会被替换为实际端口号, 例如: "https://1wnts22gq3-{{port}}.cnb.run"*/
CNB_VSCODE_PROXY_URI: useKey('CNB_VSCODE_PROXY_URI'),
/**
* VS Code Remote SSH 连接字符串,例如: vscode://vscode-remote/ssh-remote+cnb-upo-1jfth1771-001.8939f3d1-de13-486e-921f-f07943fcfa28-qng@cnb.space/workspace/"
* 是CNB_PIPELINE_ID和CNB_VSCODE_SSH_TOKEN的组合
*/
CNB_VSCODE_REMOTE_SSH_SCHEMA: useKey('CNB_VSCODE_REMOTE_SSH_SCHEMA'),
/**
* VS Code Remote SSH 连接的认证 Token 8939f3d1-de13-486e-921f-f07943fcfa28-qng
* 组装为ssh的链接字符串是 ssh CNB_PIPELINE_ID + '.' + CNB_VSCODE_SSH_TOKEN@cnb.space
*/
CNB_VSCODE_SSH_TOKEN: useKey('CNB_VSCODE_SSH_TOKEN'),
// 仓库标识配置
/** 仓库标识符,格式为 "组名/仓库名",如 "kevisual/cnb" */
CNB_REPO_SLUG: useKey('CNB_REPO_SLUG'),
/** 组名/命名空间标识符,如 "kevisual" */
CNB_GROUP_SLUG: useKey('CNB_GROUP_SLUG'),
// 仓库标识配置
/** 仓库标识符,格式为 "组名/仓库名",如 "kevisual/cnb" */
CNB_REPO_SLUG: useKey('CNB_REPO_SLUG'),
/** 组名/命名空间标识符,如 "kevisual" */
CNB_GROUP_SLUG: useKey('CNB_GROUP_SLUG'),
// 运行器资源配置
/** 运行器分配的 CPU 核心数,单位为核, 例如: "8"*/
CNB_CPUS: useKey('CNB_CPUS'),
/** 运行器分配的内存大小,单位为 GB 例如: "16"*/
CNB_MEMORY: useKey('CNB_MEMORY'),
/** 运行器的 IP 地址,用于网络连接和调试 */
CNB_RUNNER_IP: useKey('CNB_RUNNER_IP'),
// 运行器资源配置
/** 运行器分配的 CPU 核心数,单位为核, 例如: "8"*/
CNB_CPUS: useKey('CNB_CPUS'),
/** 运行器分配的内存大小,单位为 GB 例如: "16"*/
CNB_MEMORY: useKey('CNB_MEMORY'),
/** 运行器的 IP 地址,用于网络连接和调试 */
CNB_RUNNER_IP: useKey('CNB_RUNNER_IP'),
}

View File

@@ -10,7 +10,7 @@ export class KnowledgeBase extends CNBCore {
score_threshold?: number,
top_k?: number,
metadata_filtering_conditions?: MetadataFilteringConditions
}): Promise<any> {
}): Promise<Result<QueryRag[]>> {
const url = `/${repo}/-/knowledge/base/query`;
let postData = {
query: data.query,
@@ -43,4 +43,17 @@ type MetadataFilteringConditions = {
value?: string
}>
logical_operator?: 'adn' | 'or'
}
type QueryRag = {
chunk: string;
score: number;
metadata: {
hash: string;
name: string;
path: string;
position: string;
type: string; // code, text
url: string;
}
}

View File

@@ -54,7 +54,7 @@ export class Workspace extends CNBCore {
return this.post({ url: '/workspace/delete', data });
}
/** 获取我的云原生开发环境列表 */
async list(params?: ListParams): Promise<WorkspaceResult> {
async list(params?: ListParams): Promise<Result<WorkspaceResult>> {
return this.get({ url: '/workspace/list', params });
}

View File

@@ -3,7 +3,7 @@ import { KnowledgeBase } from "../src/knowledge/index.ts";
import { token, showMore, cookie } from "./common.ts";
// group: "kevisual/test",
const repo = new KnowledgeBase({ token: token, cookie: cookie });
const repoName = "test-local-docs";
const repoName = "kevisual/starred-auto";
// const queryRes = await repo.getEmbeddingModels(repoName);

View File

@@ -5,6 +5,9 @@ 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' });
const listRes = await repo.getRepoList({
page: 1, page_size: 999, role: 'developer',
flags: 'KnowledgeBase'
});
console.log("listRes", showMore(listRes), listRes.data?.length);