From f10f588ea50e97f256611953b3ac17aaf7ccd19e Mon Sep 17 00:00:00 2001 From: xiongxiao Date: Fri, 16 Jan 2026 03:46:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BB=A3=E7=A0=81=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E7=9B=B8=E5=85=B3=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=20API=20=E5=8F=82=E6=95=B0=EF=BC=8C=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BB=93=E5=BA=93=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3=E5=92=8C=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent/app.ts | 3 +- agent/opencode-plugin.ts | 55 ++++++++++++++++++++------------ agent/routes/repo/index.ts | 47 ++++++++++++++++++++++------ create-repo-test.ts | 14 +++++++++ skills/create-repo/SKILL.md | 62 ++++++++++++++++++++++++++++++++++--- src/index.ts | 2 -- src/repo/index.ts | 22 ++++++++++--- test/agent.ts | 37 ++++++++++++++++++++++ test/common.ts | 7 +++-- test/create-repo.ts | 4 ++- 10 files changed, 208 insertions(+), 45 deletions(-) create mode 100644 create-repo-test.ts create mode 100644 test/agent.ts diff --git a/agent/app.ts b/agent/app.ts index b88cd83..74001ad 100644 --- a/agent/app.ts +++ b/agent/app.ts @@ -6,9 +6,8 @@ import { nanoid } from 'nanoid'; export const config = useConfig() export const cnb = useContextKey('cnb', () => { - const token = useKey('CNB_TOKEN') as string + const token = useKey('CNB_API_KEY') as string const cookie = useKey('CNB_COOKIE') as string - return new CNB({ token: token, cookie: cookie }); }) export const appId = nanoid(); diff --git a/agent/opencode-plugin.ts b/agent/opencode-plugin.ts index 09dd2de..6d84cef 100644 --- a/agent/opencode-plugin.ts +++ b/agent/opencode-plugin.ts @@ -3,6 +3,7 @@ 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')) { @@ -10,30 +11,46 @@ const routes = app.routes.filter(r => { } return false }) -const toolSkills = 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) { - const res = await app.run({ - path: route.path, - key: route.key, - payload: args - }, { appId }); - return res.data?.content || res.data || res; - } - } - return acc; -}, {} as Record); -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 + ...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) { + 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) }, 'tool.execute.before': async (opts) => { // console.log('CnbPlugin: tool.execute.before', opts.tool); diff --git a/agent/routes/repo/index.ts b/agent/routes/repo/index.ts index be8947f..1985247 100644 --- a/agent/routes/repo/index.ts +++ b/agent/routes/repo/index.ts @@ -12,7 +12,8 @@ app.route({ skill: 'create-repo', title: '创建代码仓库', 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('代码仓库描述'), }, summary: '创建一个新的代码仓库', @@ -20,14 +21,14 @@ app.route({ } }).define(async (ctx) => { const name = ctx.query?.name; - const visibility = ctx.query?.visibility ?? 'private'; + const visibility = ctx.query?.visibility ?? 'public'; const description = ctx.query?.description ?? ''; if (!name) { ctx.throw(400, '缺少参数 name'); } - const res = await cnb.repo.createRepo(cnb.group, { + const res = await cnb.repo.createRepo({ name, visibility, description, @@ -38,7 +39,7 @@ app.route({ app.route({ path: 'cnb', key: 'create-repo-file', - description: '在代码仓库中创建文件, 参数repoName, path, content, encoding', + description: '在代码仓库中创建文件, name, path, content, encoding', middleware: ['auth'], metadata: { tags: ['opencode'], @@ -46,7 +47,7 @@ app.route({ skill: 'create-repo-file', title: '在代码仓库中创建文件', args: { - repoName: tool.schema.string().describe('代码仓库名称'), + name: tool.schema.string().describe('代码仓库名称'), path: tool.schema.string().describe('文件路径, 如 src/index.ts'), content: tool.schema.string().describe('文件内容'), encoding: tool.schema.string().describe('编码方式, 默认为 raw').optional(), @@ -55,20 +56,48 @@ app.route({ }) } }).define(async (ctx) => { - const repoName = ctx.query?.repoName; + const name = ctx.query?.name; const path = ctx.query?.path; const content = ctx.query?.content; const encoding = ctx.query?.encoding ?? 'raw'; - if (!repoName || !path || !content) { - ctx.throw(400, '缺少参数 repoName, path 或 content'); + if (!name || !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 `, files: [ { path, content, encoding }, ], }); ctx.forward(res); +}).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); \ No newline at end of file diff --git a/create-repo-test.ts b/create-repo-test.ts new file mode 100644 index 0000000..7733268 --- /dev/null +++ b/create-repo-test.ts @@ -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)); diff --git a/skills/create-repo/SKILL.md b/skills/create-repo/SKILL.md index 427296f..79c6796 100644 --- a/skills/create-repo/SKILL.md +++ b/skills/create-repo/SKILL.md @@ -1,14 +1,66 @@ --- name: create-new-repo -description: 创建一个新的代码仓库,并自动添加必要的配置文件。 +description: 创建一个基本的新的代码仓库,并自动添加必要的配置文件。 --- # 创建新的代码仓库 -该技能用于创建一个新的代码仓库,并自动添加必要的配置文件,如 `.cnb.yml` 和 `opencode.json`。 +该技能用于创建一个新的代码仓库,并自动添加必要的配置文件,如 `.cnb.yml` + ## 调用工具链 1. 执行`create-repo`工具 -2. 判断是否需要立刻需要云开发打开 -3. 如果需要,执行`open-cloud-editor`工具 -4. 返回创建的仓库信息和云开发环境信息(如果适用) \ No newline at end of file +2. 添加.cnb.yml配置文件 + +### .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 + +``` \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5fe7571..334ecbf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,7 +9,6 @@ import { Mission } from "./mission/index.ts"; import { AiBase } from "./ai/index.ts"; type CNBOptions = CNBCoreOptions<{ - group?: string; }>; export class CNB extends CNBCore { @@ -21,7 +20,6 @@ export class CNB extends CNBCore { issue!: Issue; mission!: Mission; ai!: AiBase; - group!: string; constructor(options: CNBOptions) { super({ token: options.token, cookie: options.cookie, cnb: options.cnb }); this.init(options); diff --git a/src/repo/index.ts b/src/repo/index.ts index 84081e0..21d16b1 100644 --- a/src/repo/index.ts +++ b/src/repo/index.ts @@ -6,27 +6,32 @@ export class Repo extends CNBCore { } /** * 创建代码仓库 - * @param group e.g. my-group * @param data * @returns */ - createRepo(group: string, data: CreateRepoData): Promise { + createRepo(data: CreateRepoData): Promise { + const name = data.name; + const [group, repo] = name.includes('/') ? name.split('/') : ['', name]; const url = `/${group}/-/repos`; let postData: CreateRepoData = { ...data, description: data.description || '', - name: data.name, + name: repo, license: data.license || 'Unlicense', visibility: data.visibility || 'private', }; return this.post({ url, data: postData }); } + deleteRepo(name: string): Promise { + const url = `https://cnb.cool/${name}`; + return this.delete({ url, useCookie: true }); + } async createCommit(repo: string, data: CreateCommitData): Promise { const commitList = await this.getCommitList(repo, { page: 1, page_size: 1, }, { useOrigin: true }).catch((err) => { - console.error("Error fetching commit list:", err); + // console.error("Error fetching commit list:", err); return [] }); 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.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 { const url = `/${repo}/-/git/blobs`; diff --git a/test/agent.ts b/test/agent.ts new file mode 100644 index 0000000..0eb3229 --- /dev/null +++ b/test/agent.ts @@ -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)); \ No newline at end of file diff --git a/test/common.ts b/test/common.ts index b19eebc..4674ddb 100644 --- a/test/common.ts +++ b/test/common.ts @@ -6,13 +6,16 @@ const config = useConfig() export const token = useKey("CNB_TOKEN") as string || ''; export const cookie = useKey("CNB_COOKIE") as string || ''; console.log('token', token) +import { app } from '../agent/index.ts' + +export { app } export const cnb = new CNB({ token, cookie }); export const showMore = (obj: any) => { 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' diff --git a/test/create-repo.ts b/test/create-repo.ts index 518aebf..bdf953d 100644 --- a/test/create-repo.ts +++ b/test/create-repo.ts @@ -1,9 +1,11 @@ import { Repo } from "../src/repo"; - +import { app } from '../agent/index.ts' import { token, showMore, cookie } from "./common.ts"; +import util from 'node:util'; const repo = new Repo({ token: token, cookie: cookie }); +export { app } // const res = await repo.createRepo({ // name: "test-cnb-2", // description: "This is my new repository",