更新代码仓库相关功能,修改 API 参数,添加删除仓库功能,更新文档和测试用例

This commit is contained in:
xiongxiao
2026-01-16 03:46:14 +08:00
parent d85f42d38b
commit f10f588ea5
10 changed files with 208 additions and 45 deletions

View File

@@ -6,9 +6,8 @@ import { nanoid } from 'nanoid';
export const config = useConfig() export const config = useConfig()
export const cnb = useContextKey<CNB>('cnb', () => { export const cnb = useContextKey<CNB>('cnb', () => {
const token = useKey('CNB_TOKEN') as string const token = useKey('CNB_API_KEY') as string
const cookie = useKey('CNB_COOKIE') as string const cookie = useKey('CNB_COOKIE') as string
return new CNB({ token: token, cookie: cookie }); return new CNB({ token: token, cookie: cookie });
}) })
export const appId = nanoid(); export const appId = nanoid();

View File

@@ -3,6 +3,7 @@ import { type Plugin } from "@opencode-ai/plugin"
import { app, cnb, appId } from './index.ts'; import { app, cnb, appId } from './index.ts';
import { } from 'es-toolkit' import { } from 'es-toolkit'
import { Skill } from "@kevisual/router"; import { Skill } from "@kevisual/router";
const routes = app.routes.filter(r => { const routes = app.routes.filter(r => {
const metadata = r.metadata as Skill const metadata = r.metadata as Skill
if (metadata && metadata.tags && metadata.tags.includes('opencode')) { if (metadata && metadata.tags && metadata.tags.includes('opencode')) {
@@ -10,30 +11,46 @@ const routes = app.routes.filter(r => {
} }
return false return false
}) })
const toolSkills = routes.reduce((acc, route) => {
// 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 const metadata = route.metadata as Skill
acc[metadata.skill!] = { acc[metadata.skill!] = {
name: metadata.title || metadata.skill, name: metadata.title || metadata.skill,
description: metadata.summary || '', description: metadata.summary || '',
args: metadata.args || {}, args: metadata.args || {},
async execute(args: Record<string, any>) { 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({ const res = await app.run({
path: route.path, path: route.path,
key: route.key, key: route.key,
payload: args payload: args
}, { appId }); }, { appId });
return res.data?.content || res.data || res; 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; return acc;
}, {} as Record<string, any>); }, {} as Record<string, any>)
console.log('CnbPlugin loaded skills:', Object.keys(toolSkills));
// opencode run "请使用 cnb-login-verify 工具验证登录信信息,检查cookie"
export const CnbPlugin: Plugin = async ({ project, client, $, directory, worktree }) => {
return {
'tool': {
...toolSkills
}, },
'tool.execute.before': async (opts) => { 'tool.execute.before': async (opts) => {
// console.log('CnbPlugin: tool.execute.before', opts.tool); // console.log('CnbPlugin: tool.execute.before', opts.tool);

View File

@@ -12,7 +12,8 @@ app.route({
skill: 'create-repo', skill: 'create-repo',
title: '创建代码仓库', title: '创建代码仓库',
args: { args: {
name: tool.schema.string().describe('代码仓库名称'), name: tool.schema.string().describe('代码仓库名称, 如 my-user/my-repo'),
visibility: tool.schema.string().describe('代码仓库可见性, public 或 private').default('public'),
description: tool.schema.string().describe('代码仓库描述'), description: tool.schema.string().describe('代码仓库描述'),
}, },
summary: '创建一个新的代码仓库', summary: '创建一个新的代码仓库',
@@ -20,14 +21,14 @@ app.route({
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const name = ctx.query?.name; const name = ctx.query?.name;
const visibility = ctx.query?.visibility ?? 'private'; const visibility = ctx.query?.visibility ?? 'public';
const description = ctx.query?.description ?? ''; const description = ctx.query?.description ?? '';
if (!name) { if (!name) {
ctx.throw(400, '缺少参数 name'); ctx.throw(400, '缺少参数 name');
} }
const res = await cnb.repo.createRepo(cnb.group, { const res = await cnb.repo.createRepo({
name, name,
visibility, visibility,
description, description,
@@ -38,7 +39,7 @@ app.route({
app.route({ app.route({
path: 'cnb', path: 'cnb',
key: 'create-repo-file', key: 'create-repo-file',
description: '在代码仓库中创建文件, 参数repoName, path, content, encoding', description: '在代码仓库中创建文件, name, path, content, encoding',
middleware: ['auth'], middleware: ['auth'],
metadata: { metadata: {
tags: ['opencode'], tags: ['opencode'],
@@ -46,7 +47,7 @@ app.route({
skill: 'create-repo-file', skill: 'create-repo-file',
title: '在代码仓库中创建文件', title: '在代码仓库中创建文件',
args: { args: {
repoName: tool.schema.string().describe('代码仓库名称'), name: tool.schema.string().describe('代码仓库名称'),
path: tool.schema.string().describe('文件路径, 如 src/index.ts'), path: tool.schema.string().describe('文件路径, 如 src/index.ts'),
content: tool.schema.string().describe('文件内容'), content: tool.schema.string().describe('文件内容'),
encoding: tool.schema.string().describe('编码方式, 默认为 raw').optional(), encoding: tool.schema.string().describe('编码方式, 默认为 raw').optional(),
@@ -55,16 +56,16 @@ app.route({
}) })
} }
}).define(async (ctx) => { }).define(async (ctx) => {
const repoName = ctx.query?.repoName; const name = ctx.query?.name;
const path = ctx.query?.path; const path = ctx.query?.path;
const content = ctx.query?.content; const content = ctx.query?.content;
const encoding = ctx.query?.encoding ?? 'raw'; const encoding = ctx.query?.encoding ?? 'raw';
if (!repoName || !path || !content) { if (!name || !path || !content) {
ctx.throw(400, '缺少参数 repoName, path 或 content'); ctx.throw(400, '缺少参数 name, path 或 content');
} }
const res = await cnb.repo.createCommit(repoName, { const res = await cnb.repo.createCommit(name, {
message: `添加文件 ${path} 通过 API `, message: `添加文件 ${path} 通过 API `,
files: [ files: [
{ path, content, encoding }, { path, content, encoding },
@@ -72,3 +73,31 @@ app.route({
}); });
ctx.forward(res); ctx.forward(res);
}).addTo(app); }).addTo(app);
app.route({
path: 'cnb',
key: 'delete-repo',
description: '删除代码仓库, 参数name',
middleware: ['auth'],
metadata: {
tags: ['opencode'],
...createSkill({
skill: 'delete-repo',
title: '删除代码仓库',
args: {
name: tool.schema.string().describe('代码仓库名称'),
},
summary: '删除一个代码仓库',
})
}
}).define(async (ctx) => {
const name = ctx.query?.name;
if (!name) {
ctx.throw(400, '缺少参数 name');
}
const res = await cnb.repo.deleteRepo(name);
ctx.forward(res);
}).addTo(app);

14
create-repo-test.ts Normal file
View File

@@ -0,0 +1,14 @@
import { CNB } from './src/index.ts';
const cnb = new CNB({
token: 'cIDfLOOIr1Trt15cdnwfndupEZG',
cookie: ''
});
const res = await cnb.repo.createRepo('kevisual', {
name: 'exam-kevisual',
description: 'exam repository for kevisual',
visibility: 'public'
});
console.log('Result:', JSON.stringify(res, null, 2));

View File

@@ -1,14 +1,66 @@
--- ---
name: create-new-repo name: create-new-repo
description: 创建一个新的代码仓库,并自动添加必要的配置文件。 description: 创建一个基本的新的代码仓库,并自动添加必要的配置文件。
--- ---
# 创建新的代码仓库 # 创建新的代码仓库
该技能用于创建一个新的代码仓库,并自动添加必要的配置文件,如 `.cnb.yml``opencode.json` 该技能用于创建一个新的代码仓库,并自动添加必要的配置文件,如 `.cnb.yml`
## 调用工具链 ## 调用工具链
1. 执行`create-repo`工具 1. 执行`create-repo`工具
2. 判断是否需要立刻需要云开发打开 2. 添加.cnb.yml配置文件
3. 如果需要,执行`open-cloud-editor`工具
4. 返回创建的仓库信息和云开发环境信息(如果适用) ### .cnb.yml配置文件内容示例
假设新仓库名称为 REPO_NAME
TO_REPO 为 kevisual/REPO_NAME
```yaml
# .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
$:
vscode:
- docker:
image: docker.cnb.cool/kevisual/dev-env:latest
services:
- vscode
- docker
imports: !reference [.common_env, imports]
# 开发环境启动后会执行的任务
# stages:
# - name: pnpm install
# script: pnpm install
stages: !reference [.dev_tempalte, 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
```

View File

@@ -9,7 +9,6 @@ import { Mission } from "./mission/index.ts";
import { AiBase } from "./ai/index.ts"; import { AiBase } from "./ai/index.ts";
type CNBOptions = CNBCoreOptions<{ type CNBOptions = CNBCoreOptions<{
group?: string;
}>; }>;
export class CNB extends CNBCore { export class CNB extends CNBCore {
@@ -21,7 +20,6 @@ export class CNB extends CNBCore {
issue!: Issue; issue!: Issue;
mission!: Mission; mission!: Mission;
ai!: AiBase; ai!: AiBase;
group!: string;
constructor(options: CNBOptions) { constructor(options: CNBOptions) {
super({ token: options.token, cookie: options.cookie, cnb: options.cnb }); super({ token: options.token, cookie: options.cookie, cnb: options.cnb });
this.init(options); this.init(options);

View File

@@ -6,27 +6,32 @@ export class Repo extends CNBCore {
} }
/** /**
* 创建代码仓库 * 创建代码仓库
* @param group e.g. my-group
* @param data * @param data
* @returns * @returns
*/ */
createRepo(group: string, data: CreateRepoData): Promise<any> { createRepo(data: CreateRepoData): Promise<any> {
const name = data.name;
const [group, repo] = name.includes('/') ? name.split('/') : ['', name];
const url = `/${group}/-/repos`; const url = `/${group}/-/repos`;
let postData: CreateRepoData = { let postData: CreateRepoData = {
...data, ...data,
description: data.description || '', description: data.description || '',
name: data.name, name: repo,
license: data.license || 'Unlicense', license: data.license || 'Unlicense',
visibility: data.visibility || 'private', visibility: data.visibility || 'private',
}; };
return this.post({ url, data: postData }); return this.post({ url, data: postData });
} }
deleteRepo(name: string): Promise<any> {
const url = `https://cnb.cool/${name}`;
return this.delete({ url, useCookie: true });
}
async createCommit(repo: string, data: CreateCommitData): Promise<any> { async createCommit(repo: string, data: CreateCommitData): Promise<any> {
const commitList = await this.getCommitList(repo, { const commitList = await this.getCommitList(repo, {
page: 1, page: 1,
page_size: 1, page_size: 1,
}, { useOrigin: true }).catch((err) => { }, { useOrigin: true }).catch((err) => {
console.error("Error fetching commit list:", err); // console.error("Error fetching commit list:", err);
return [] return []
}); });
const preCommitSha = commitList.length > 0 ? commitList[0].sha : undefined; const preCommitSha = commitList.length > 0 ? commitList[0].sha : undefined;
@@ -46,7 +51,14 @@ export class Repo extends CNBCore {
delete postData.parent_commit_sha; delete postData.parent_commit_sha;
delete postData.base_branch; delete postData.base_branch;
} }
return this.post({ url, data: postData, useCookie: true, }); return this.post({
url,
data: postData,
useCookie: true,
headers: {
'Accept': 'application/vnd.cnb.web+json',
}
});
} }
createBlobs(repo: string, data: { content: string, encoding?: 'utf-8' | 'base64' }): Promise<any> { createBlobs(repo: string, data: { content: string, encoding?: 'utf-8' | 'base64' }): Promise<any> {
const url = `/${repo}/-/git/blobs`; const url = `/${repo}/-/git/blobs`;

37
test/agent.ts Normal file
View File

@@ -0,0 +1,37 @@
import { app, showMore } from './common.ts';
// const res = await app.run({
// path: 'cnb',
// key: 'create-repo',
// payload: {
// name: 'kevisual/exam',
// description: 'kevisual 创建的代码仓库',
// visibility: 'public',
// }
// })
// console.log(showMore(res));
// const res2 = await app.run({
// path: 'cnb',
// key: 'create-repo-file',
// payload: {
// name: 'kevisual/exam',
// path: 'README.md',
// content: '# Example Skill\nThis is an example skill created via API.',
// encoding: 'raw',
// },
// })
// console.log(showMore(res2));
// const deleteRes = await app.run({
// path: 'cnb',
// key: 'delete-repo',
// payload: {
// name: 'kevisual/exam',
// },
// })
// console.log(showMore(deleteRes));

View File

@@ -6,13 +6,16 @@ const config = useConfig()
export const token = useKey("CNB_TOKEN") as string || ''; export const token = useKey("CNB_TOKEN") as string || '';
export const cookie = useKey("CNB_COOKIE") as string || ''; export const cookie = useKey("CNB_COOKIE") as string || '';
console.log('token', token) console.log('token', token)
import { app } from '../agent/index.ts'
export { app }
export const cnb = new CNB({ token, cookie }); export const cnb = new CNB({ token, cookie });
export const showMore = (obj: any) => { export const showMore = (obj: any) => {
return util.inspect(obj, { showHidden: false, depth: null, colors: true }); return util.inspect(obj, { showHidden: false, depth: null, colors: true });
} }
const worksaceList = await cnb.workspace.list({ status: 'running' }); // const worksaceList = await cnb.workspace.list({ status: 'running' });
console.log("worksaceList", showMore(worksaceList)); // console.log("worksaceList", showMore(worksaceList));
// const sn = 'cnb-o18-1jbklfuoh' // const sn = 'cnb-o18-1jbklfuoh'

View File

@@ -1,9 +1,11 @@
import { Repo } from "../src/repo"; import { Repo } from "../src/repo";
import { app } from '../agent/index.ts'
import { token, showMore, cookie } from "./common.ts"; import { token, showMore, cookie } from "./common.ts";
import util from 'node:util';
const repo = new Repo({ token: token, cookie: cookie }); const repo = new Repo({ token: token, cookie: cookie });
export { app }
// const res = await repo.createRepo({ // const res = await repo.createRepo({
// name: "test-cnb-2", // name: "test-cnb-2",
// description: "This is my new repository", // description: "This is my new repository",