From 52ccf115fbe42d0a4dffa4bbf8880ba4a76b620a Mon Sep 17 00:00:00 2001 From: abearxiong Date: Mon, 15 Dec 2025 17:08:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Repo=20=E5=92=8C?= =?UTF-8?q?=20User=20=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=A2=9E=E5=BC=BA=20CNBCo?= =?UTF-8?q?re=20=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 9 ++- src/cnb-core.ts | 126 +++++++++++++++++++++++++++------- src/index.ts | 9 +-- src/repo/index.ts | 161 ++++++++++++++++++++++++++++++++++++++++++++ src/user/index.ts | 19 ++++++ test/common.ts | 10 ++- test/create-repo.ts | 69 +++++++++++++++++++ test/repo.ts | 10 +++ test/user.ts | 9 +++ 9 files changed, 390 insertions(+), 32 deletions(-) create mode 100644 src/repo/index.ts create mode 100644 src/user/index.ts create mode 100644 test/create-repo.ts create mode 100644 test/repo.ts create mode 100644 test/user.ts diff --git a/package.json b/package.json index 50d321c..6e79386 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "cnb", + "name": "@kevisual/cnb", "version": "0.0.1", "description": "", "main": "index.js", @@ -7,6 +7,11 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], + "files": [ + "src", + "mod.ts", + "agents" + ], "author": "abearxiong (https://www.xiongxiao.me)", "license": "MIT", "packageManager": "pnpm@10.24.0", @@ -16,4 +21,4 @@ "@types/node": "^24.10.1", "dotenv": "^17.2.3" } -} +} \ No newline at end of file diff --git a/src/cnb-core.ts b/src/cnb-core.ts index d54dda5..5bb37d5 100644 --- a/src/cnb-core.ts +++ b/src/cnb-core.ts @@ -1,56 +1,134 @@ export type CNBCoreOptions = { token: string; + cookie?: string; } & T; +export type RequestOptions = { + url?: string; + method?: string; + data?: Record; + body?: any; + params?: Record; + headers?: Record; + useCookie?: boolean; + useOrigin?: boolean; +}; export class CNBCore { baseURL = 'https://api.cnb.cool'; token: string; - + cookie?: string; constructor(options: CNBCoreOptions) { this.token = options.token; + this.cookie = options.cookie; } - async request({ url, method = 'GET', data, params }: { url: string, method?: string, data?: Record, params?: Record }): Promise { - const headers: Record = { + async request({ url, method = 'GET', data, params, headers, body, useCookie, useOrigin }: RequestOptions): Promise { + const defaultHeaders: Record = { 'Content-Type': 'application/json', - 'Accept': 'application/json, application/vnd.cnb.api+json, application/vnd.cnb.web+json', - 'Authorization': `Bearer ${this.token}`, + // 'Accept': 'application/json, application/vnd.cnb.api+json, application/vnd.cnb.web+json', + 'Accept': 'application/json', }; + if (this.token) { + defaultHeaders['Authorization'] = `Bearer ${this.token}`; + } if (params) { const queryString = new URLSearchParams(params).toString(); url += `?${queryString}`; + defaultHeaders['Accept'] = 'application/json'; } - const response = await fetch(url, { + const _headers = { ...defaultHeaders, ...headers }; + let _body = undefined; + if (data) { + _body = JSON.stringify(data); + } + if (body) { + _body = body; + } + if (!_headers.Authorization) { + delete _headers.Authorization; + } + if (useCookie) { + _headers['Cookie'] = this.cookie || ""; + delete _headers.Authorization; + } + console.log('Request URL:', url, data, _headers); + const response = await fetch(url || '', { method, - headers, - body: data ? JSON.stringify(data) : undefined, + headers: _headers, + body: _body, }); + const res = (data: any, message?: string) => { + if (useOrigin) { + return data; + } + return { + code: 200, + message: message || 'success', + data, + }; + } if (!response.ok) { const errorText = await response.text(); - throw new Error(`Request failed with status ${response.status}: ${errorText}`); + if (useOrigin) + throw new Error(`Request failed with status ${response.status}: ${errorText}`); + return res(null, `Request failed with status ${response.status}: ${errorText}`); } const contentType = response.headers.get('Content-Type'); if (contentType && contentType.includes('application/json')) { - return response.json(); + const values = await response.json(); + return res(values); } else { - return response.text(); + const text = await response.text(); + return res(text); } } - get({ url, params }: { url: string, params?: Record }): Promise { - const fullUrl = new URL(url, this.baseURL).toString(); - return this.request({ url: fullUrl, method: 'GET', params }); + makeUrl(url?: string): string { + if (url && url.startsWith('http')) { + return url; + } + return new URL(url || '', this.baseURL).toString(); } - post({ url, data }: { url: string, data?: Record }): Promise { - const fullUrl = new URL(url, this.baseURL).toString(); - return this.request({ url: fullUrl, method: 'POST', data }); + get({ url, ...REST }: RequestOptions): Promise { + const fullUrl = this.makeUrl(url); + return this.request({ url: fullUrl, method: 'GET', ...REST }); } - put({ url, data }: { url: string, data?: Record }): Promise { - const fullUrl = new URL(url, this.baseURL).toString(); - return this.request({ url: fullUrl, method: 'PUT', data }); + post({ url, ...REST }: RequestOptions): Promise { + const fullUrl = this.makeUrl(url); + return this.request({ url: fullUrl, method: 'POST', ...REST }); } - delete({ url, data }: { url: string, data?: Record }): Promise { - const fullUrl = new URL(url, this.baseURL).toString(); - return this.request({ url: fullUrl, method: 'DELETE', data }); + put({ url, ...REST }: RequestOptions): Promise { + const fullUrl = this.makeUrl(url); + return this.request({ url: fullUrl, method: 'PUT', ...REST }); } -} \ No newline at end of file + delete({ url, ...REST }: RequestOptions): Promise { + const fullUrl = this.makeUrl(url); + return this.request({ url: fullUrl, method: 'DELETE', ...REST }); + } + patch({ url, ...REST }: RequestOptions): Promise { + const fullUrl = this.makeUrl(url); + return this.request({ url: fullUrl, method: 'PATCH', ...REST }); + } + /** + * 通过 PUT 请求上传文件内容 + * @param data 包含 URL、token 和文件内容 + * @returns 上传结果 + */ + async putFile(data: { url: string, token: string, content: string | Buffer }): Promise { + return this.request({ + url: data.url, + method: 'PUT', + body: data.content, + headers: { + 'Authorization': `Bearer ${data.token}`, + 'Content-Type': 'application/octet-stream' + } + }); + } +} + +export type Result = { + code: number; + message: string; + data: T +}; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index fbf766e..a65ce76 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,10 @@ -import { CNBCore } from "./cnb-core"; +import { CNBCore, CNBCoreOptions } from "./cnb-core"; import { Workspace } from "./workspace"; +type CNBOptions = CNBCoreOptions<{}>; export class CNB extends CNBCore { workspace: Workspace; - constructor(token: string) { - super({ token }); - this.workspace = new Workspace(token); + constructor(options: CNBOptions) { + super({ token: options.token }); + this.workspace = new Workspace(options.token); } } \ No newline at end of file diff --git a/src/repo/index.ts b/src/repo/index.ts new file mode 100644 index 0000000..e0b999f --- /dev/null +++ b/src/repo/index.ts @@ -0,0 +1,161 @@ +import { CNBCore, CNBCoreOptions, RequestOptions, Result } from "../cnb-core"; + +type RepoOptions = CNBCoreOptions<{ + group?: string; +}> +export class Repo extends CNBCore { + group: string; + constructor(options: RepoOptions) { + super({ token: options.token, cookie: options.cookie }); + this.group = options.group || ''; + } + /** + * 创建代码仓库 + * @param group e.g. my-group + * @param data + * @returns + */ + createRepo(data: CreateRepoData): Promise { + const group = this.group || ''; + const url = `/${group}/-/repos`; + let postData: CreateRepoData = { + ...data, + description: data.description || '', + name: data.name, + license: data.license || 'Unlicense', + visibility: data.visibility || 'private', + }; + return this.post({ url, data: postData }); + } + async createCommit(repo: string, data: CreateCommitData): Promise { + const group = this.group || ''; + const commitList = await this.getCommitList(repo, { + page: 1, + page_size: 1, + }, { useOrigin: true }).catch((err) => { + console.error("Error fetching commit list:", err); + return [] + }); + const preCommitSha = commitList.length > 0 ? commitList[0].sha : undefined; + if (!data.parent_commit_sha && preCommitSha) { + data.parent_commit_sha = preCommitSha; + } + const url = `https://cnb.cool/${group}/${repo}/-/git/commits`; + const postData: CreateCommitData = { + ...data, + base_branch: data.base_branch || 'refs/heads/main', + message: data.message || 'commit from cnb sdk', + parent_commit_sha: data.parent_commit_sha, + files: data.files || [], + new_branch: data.new_branch || 'refs/heads/main', + }; + if (!postData.parent_commit_sha) { + delete postData.parent_commit_sha; + delete postData.base_branch; + } + return this.post({ url, data: postData, useCookie: true, }); + } + createBlobs(repo: string, data: { content: string, encoding?: 'utf-8' | 'base64' }): Promise { + const group = this.group || ''; + const url = `/${group}/${repo}/-/git/blobs`; + const postData = { + content: data.content, + encoding: data.encoding || 'utf-8', + }; + return this.post({ url, data: postData }); + } + uploadFiles(repo: string, data: { ext?: any, name?: string, path?: string, size?: number }): Promise { + const group = this.group || ''; + const url = `/${group}/${repo}/-/upload/files` + return this.post({ url, data }); + } + getCommitList(repo: string, params: { author?: string, commiter?: string, page?: number, page_size?: number, sha?: string, since?: string, until?: string }, opts?: RequestOptions): Promise { + const group = this.group || ''; + const url = `/${group}/${repo}/-/git/commits`; + return this.get({ url, params, ...opts }); + } + getRepoList(params: { + desc?: boolean; + filter_type?: 'private' | 'public' | 'secret'; + flags?: 'KnowledgeBase'; + order_by?: 'created_at' | 'last_updated_at' | 'stars' | 'slug_path' | 'forks'; + page?: number; + page_size?: number; + role?: 'owner' | 'maintainer' | 'developer' | 'reporter' | 'guest'; + search?: string; + status?: 'active' | 'archived'; + }): Promise> { + const url = '/user/repos'; + let _params = { + ...params, + page: params.page || 1, + page_size: params.page_size || 999, + } + return this.get({ url, params: _params }); + } +} + +type CreateRepoData = { + description: string; + license?: 'MIT' | 'Apache-2.0' | 'GPL-3.0' | 'Unlicense'; + name: string; + visibility: 'private' | 'public' | 'secret'; +} + +type CreateCommitData = { + base_branch?: string; // "refs/heads/main" + new_branch?: string; // "refs/heads/main" + message?: string; + parent_commit_sha?: string; + files?: Array<{ + content: string; + path: string; + encoding?: 'raw' | 'utf-8' | 'base64'; + is_delete?: boolean; + is_executable?: boolean; + }>; +} + +type RepoItem = { + id: string; + name: string; + freeze: boolean; + status: number; + visibility_level: 'Public' | 'Private' | 'Secret'; + flags: string; + created_at: string; + updated_at: string; + description: string; + site: string; + topics: string; + license: string; + display_module: { + activity: boolean; + contributors: boolean; + release: boolean; + }; + star_count: number; + fork_count: number; + mark_count: number; + last_updated_at: string | null; + web_url: string; + path: string; + tags: string[] | null; + open_issue_count: number; + open_pull_request_count: number; + languages: { + language: string; + color: string; + }; + second_languages: { + language: string; + color: string; + }; + last_update_username: string; + last_update_nickname: string; + access: string; + stared: boolean; + star_time: string; + pinned: boolean; + pinned_time: string; +} \ No newline at end of file diff --git a/src/user/index.ts b/src/user/index.ts new file mode 100644 index 0000000..2b3aaff --- /dev/null +++ b/src/user/index.ts @@ -0,0 +1,19 @@ +import { CNBCore, CNBCoreOptions } from "../cnb-core"; + +export class User extends CNBCore { + constructor(options: CNBCoreOptions<{}>) { + super({ token: options.token, cookie: options.cookie }); + } + + /** + * 获取当前用户信息 + * @returns + */ + getCurrentUser(): Promise { + const url = `https://cnb.cool/user`; + return this.get({ + url, + useCookie: true, + }); + } +} \ No newline at end of file diff --git a/test/common.ts b/test/common.ts index 5fa21c8..e69a9c0 100644 --- a/test/common.ts +++ b/test/common.ts @@ -1,8 +1,14 @@ import { CNB } from "../src"; import dotenv from "dotenv"; +import util from 'node:util'; dotenv.config(); -export const cnb = new CNB(process.env.CNB_TOKEN || ""); - +export const token = process.env.CNB_TOKEN || ""; +export const cookie = process.env.CNB_COOKIE || ""; +console.log("Using CNB_TOKEN:", token.slice(0, 4) + "****", cookie); +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' }); // console.log("worksaceList", worksaceList); diff --git a/test/create-repo.ts b/test/create-repo.ts new file mode 100644 index 0000000..7b7fcbf --- /dev/null +++ b/test/create-repo.ts @@ -0,0 +1,69 @@ +import { Repo } from "../src/repo"; + +import { token, showMore, cookie } from "./common.ts"; + +const repo = new Repo({ group: "kevisual/demo", token: token, cookie: cookie }); + +// const res = await repo.createRepo({ +// name: "test-cnb-2", +// description: "This is my new repository", +// visibility: "private", +// license: 'MIT', +// }); + +// console.log("res", showMore(res)); + +const repoName = "test-cnb"; +// const commitList = await repo.getCommitList(repoName, { +// page: 1, +// page_size: 1, +// }).catch((err) => { +// console.error("Error fetching commit list:", err); +// return [] +// }); +// console.log("commitList", showMore(commitList)); + +// const preCommitSha = commitList.length > 0 ? commitList[0].sha : undefined; + +const commitRes = await repo.createCommit(repoName, { + message: "Initial commit3", + files: [ + { path: "README.md", content: "# Hello World\nThis is my first commit!", encoding: 'raw' }, + { path: "a.md", content: "# Hello World\nThis is my first commit2!", encoding: 'raw' }, + ], +}); + +console.log("commitRes", showMore(commitRes)); + + + +// const blobRes = await repo.createBlobs(repoName, { +// content: Buffer.from("Hello, CNB!").toString("base64"), +// encoding: "base64", +// }); + +// console.log("blobRes", showMore(blobRes)); +// sha: 4b909f74428c24221a9f795c591f5eb560817f2d + +// const bufferText = Buffer.from("This is a sample file content. 232332"); +// const bufferSize = Buffer.byteLength(bufferText); +// console.log("bufferSize", bufferSize); +// const uploadRes = await repo.uploadFiles(repoName, { +// name: "sample.txt", +// ext: { "a": "b" }, +// size: Buffer.byteLength(bufferText), +// }); + +// console.log("uploadRes", showMore(uploadRes)); + +// const upload_url = uploadRes.upload_url +// const upload_token = uploadRes.token; + + +// const uploadResponse = await repo.putFile({ +// url: upload_url, +// token: upload_token, +// content: bufferText, +// }); + +// console.log("uploadResponse", showMore(uploadResponse)); \ No newline at end of file diff --git a/test/repo.ts b/test/repo.ts new file mode 100644 index 0000000..0fed4ff --- /dev/null +++ b/test/repo.ts @@ -0,0 +1,10 @@ +import { Repo } from "../src/repo"; + +import { token, showMore, cookie } from "./common.ts"; + +const repo = new Repo({ group: "kevisual/demo", token: token, cookie: cookie }); + + +const listRes = await repo.getRepoList({ page: 1, page_size: 10 }); + +console.log("listRes", showMore(listRes)); \ No newline at end of file diff --git a/test/user.ts b/test/user.ts new file mode 100644 index 0000000..695a0e4 --- /dev/null +++ b/test/user.ts @@ -0,0 +1,9 @@ +import { User } from "../src/user/index"; + +import { token, showMore, cookie } from "./common.ts"; + +const user = new User({ token: token, cookie: cookie }); + +const currentUser = await user.getCurrentUser(); + +console.log("currentUser", showMore(currentUser));