@xindiwo/git-sdk
v2.0.0
Published
统一封装 Gitee / GitCode / CNB / GitHub / GitLab / Bitbucket 六家代码托管平台 OpenAPI 的 Node.js SDK
Maintainers
Readme
@xindiwo/git-sdk
统一封装 Gitee / GitCode / CNB / GitHub / GitLab / Bitbucket 六家代码托管平台 OpenAPI 的 Node.js SDK。
同一套资源接口、同一套数据模型、同一套错误与分页语义,换平台只需换一个客户端实例。
import { createGitSdk } from '@xindiwo/git-sdk';
// 每个平台只要一把钥匙,其它参数全部有默认值;没配的平台也能匿名读公开数据
const sdk = createGitSdk({
gitee: 'gitee-token',
gitcode: 'gitcode-token',
cnb: 'cnb-token',
github: 'ghp_xxx',
gitlab: 'glpat-xxx',
bitbucket: 'bitbucket-token',
});
// 六家平台,同一套写法
const issues = await sdk.gitee.issues.list({ owner: 'mindspore', repo: 'mindspore', state: 'open' });
const pulls = await sdk.gitcode.pullRequests.list({ owner: 'mindspore', repo: 'mindspore' });
const repo = await sdk.cnb.repositories.get({ owner: 'xindiwo', repo: 'git-sdk' });
const gh = await sdk.github.repositories.get({ owner: 'nodejs', repo: 'node' });
const gl = await sdk.gitlab.repositories.list({ q: 'git-sdk' });
const bb = await sdk.bitbucket.repositories.get({ owner: 'atlassian', repo: 'python-bitbucket' });
console.log(issues.items[0].title, repo.fullName, gh.fullName, gl.total, bb.fullName);特性
- 统一资源接口:
repositories/branches/tags/commits/issues/pullRequests/labels/releases/webhooks/users/organizations/members/search,20 个资源域在六家平台上方法签名一致。 - 深度归一化模型:返回的
Repository/Issue/PullRequest/Comment/Release等对象跨平台字段一致,同时每个模型都保留raw原始响应,需要平台独有字段时无需二次请求。 - 统一认证注入:自动处理 Gitee 的
Authorization: token、GitCode 的private-token、CNB / GitHub / GitLab / Bitbucket 各自的Bearer与PRIVATE-TOKEN,六家的令牌都不进 URL。 - 统一分页:屏蔽 Gitee/GitCode/GitHub/GitLab 的
page/per_page、CNB 的page/page_size、Bitbucket 的pagelen游标差异,并按各平台响应头(total_count/X-Cnb-Total/Link)还原total与hasNext;内置iterateItems/collectItems自动翻页。 - 统一错误:把六家五花八门的错误体(
message/errmsg/error_message)归一为AuthenticationError/NotFoundError/RateLimitError等类型,并保留status/platform/method/url/body上下文。 - 重试与限流:对
429/5xx/ 网络抖动自动指数退避重试并遵循Retry-After;只重试幂等方法(GET/PUT/DELETE),POST/PATCH不自动重试以避免重复副作用。 - 零运行时依赖:基于 Node 原生
fetch,ESM + CJS + 类型声明三份产物,Node.js >= 18。 - raw 逃生通道:任何未覆盖的接口都可以直通调用,不阻塞业务。
安装
包发布在两个源,内容与版本完全一致:
公共 npm(npmjs.com)
npm install @xindiwo/git-sdkCNB npm 制品库
在业务仓库的 .npmrc 中配置:
@xindiwo:registry=https://npm.cnb.cool/xindiwo/git-sdk-v1/-/packages/
//npm.cnb.cool/xindiwo/git-sdk-v1/-/packages/:_authToken=${CNB_TOKEN}npm install @xindiwo/git-sdk在 CNB 云原生开发/构建环境中
CNB_TOKEN已自动注入,无需额外配置。
注意:若
.npmrc里配了上面的@xindiwo作用域映射,npm install会走 CNB 制品库; 想改回公共 npm,去掉这两行即可。
快速开始
1. 初始化:只配一把钥匙
// 方式一:零配置,令牌放环境变量,代码里什么都不用写
// GITEE_TOKEN / GITCODE_TOKEN / CNB_TOKEN
import sdk from '@xindiwo/git-sdk';
await sdk.cnb.repositories.get({ owner: 'xindiwo', repo: 'git-sdk' });
// 方式二:显式传令牌字符串(推荐)
import { createGitSdk } from '@xindiwo/git-sdk';
const sdk = createGitSdk({
gitee: 'gitee-token',
gitcode: 'gitcode-token',
cnb: 'cnb-token',
});
// 方式三:只有需要调超时 / 重试时才用对象形式
const sdk = createGitSdk({ cnb: { token: 'cnb-token', timeout: 10_000, maxRetries: 3 } });
// 单平台客户端同样只收一把钥匙
import { CnbClient } from '@xindiwo/git-sdk';
const cnb = new CnbClient('cnb-token');三个客户端恒可用,不需要写 sdk.cnb?.。未配令牌时不会在启动阶段报错,而是在首次请求时给出可执行的提示:
try {
await sdk.cnb.repositories.get({ owner: 'xindiwo', repo: 'git-sdk' });
} catch (error) {
// AuthenticationError: 缺少 CNB 访问令牌:请设置环境变量 CNB_TOKEN,或 createGitSdk({ cnb: "你的令牌" })
}部署后想确认三把钥匙都通,用一次调用自检:
for (const check of await sdk.whoami()) {
console.log(
check.platform,
check.ok ? `✅ ${check.user.username}` : `❌ ${String(check.error)}`,
);
}
// gitee ✅ sunchengxin
// gitcode ❌ AuthenticationError: ...
// cnb ✅ sunchengxin2. 仓库与代码
const repo = await sdk.cnb.repositories.get({ owner: 'xindiwo', repo: 'git-sdk' });
repo.fullName; // 'xindiwo/git-sdk'
repo.defaultBranch; // 'main'
repo.private; // false
repo.raw; // 平台原始响应
const branches = await sdk.gitee.branches.list({ owner: 'mindspore', repo: 'mindspore', perPage: 10 });
const commit = await sdk.gitcode.commits.get({ owner: 'mindspore', repo: 'mindspore', ref: 'master' });
// 建分支 / 删分支:不传 from 就以仓库默认分支为起点
await sdk.cnb.branches.create({ owner: 'xindiwo', repo: 'git-sdk', name: 'feat/probe' });
await sdk.cnb.branches.delete({ owner: 'xindiwo', repo: 'git-sdk', name: 'feat/probe' });
// 建标签 / 删标签:target 省略时指向默认分支,message 用于注解标签
await sdk.cnb.tags.create({ owner: 'xindiwo', repo: 'git-sdk', name: 'v1.2.0', message: '正式版' });
await sdk.cnb.tags.delete({ owner: 'xindiwo', repo: 'git-sdk', name: 'v1.2.0' });
// 提交比较与提交评论
const diff = await sdk.gitcode.commits.compare({ owner: 'o', repo: 'r', base: 'main', head: 'feat/x' });
diff.totalCommits; // 3
diff.files[0]?.filename; // 'src/a.ts'(GitCode 的差异文件放在 diffs 字段里,SDK 已兼容)
await sdk.gitcode.commits.createComment({ owner: 'o', repo: 'r', sha: 'abc123', body: '这里补个测试' });
// 仓库生态数据
await sdk.cnb.repositories.listForks({ owner: 'xindiwo', repo: 'git-sdk' });
await sdk.gitcode.repositories.languages({ owner: 'o', repo: 'r' }); // { TypeScript: 1200 }
await sdk.gitcode.repositories.listStargazers({ owner: 'o', repo: 'r' });
// 用户维度
await sdk.cnb.users.listRepos({ username: 'xindiwo' });
await sdk.gitcode.users.listStarred({ username: 'xindiwo' });
// Release 生命周期:id 直接回传即可(GitCode 的 id 就是 tag 名)
const release = await sdk.cnb.releases.getByTag({ owner: 'xindiwo', repo: 'git-sdk', tag: 'v1.5.0' });
await sdk.cnb.releases.update({ owner: 'xindiwo', repo: 'git-sdk', id: release.id, name: 'v1.5.0 正式版' });
await sdk.cnb.releases.delete({ owner: 'xindiwo', repo: 'git-sdk', id: release.id });
// 仓库动态(事件流):GitCode / Gitee 支持,CNB 未开放
const feed = await sdk.gitcode.repositories.listEvents({ owner: 'o', repo: 'r', perPage: 20 });
feed.items.map((item) => `${item.type}:${item.actor?.username}`);
commit.title; // 首行提交信息
commit.parents; // ['...']2.1 读取与提交文件
// 读文件(base64 已自动解码成文本)与读目录
const readme = await sdk.cnb.contents.readme({ owner: 'xindiwo', repo: 'webhook' });
readme.content; // '# Webhook ...'
const entries = await sdk.cnb.contents.list({ owner: 'xindiwo', repo: 'webhook' });
entries.map((entry) => `${entry.kind}:${entry.name}`); // ['file:.cnb.yml', 'dir:.ide', ...]
// 提交变更(Gitee / GitCode 支持;CNB 会抛 UnsupportedOperationError)
await sdk.gitee.contents.createFile({
owner: 'xindiwo',
repo: 'git-sdk',
path: 'docs/notes.md',
content: '# 说明\n',
message: 'docs: 新增说明',
});
// 更新与删除会自动补全 sha,无需自己先查
await sdk.gitee.contents.updateFile({
owner: 'xindiwo',
repo: 'git-sdk',
path: 'docs/notes.md',
content: '# 说明(已更新)\n',
message: 'docs: 更新说明',
});
await sdk.gitee.contents.deleteFile({
owner: 'xindiwo',
repo: 'git-sdk',
path: 'docs/notes.md',
message: 'docs: 移除说明',
});2.2 里程碑、标签与贡献者
const milestones = await sdk.gitee.milestones.list({ owner: 'o', repo: 'r', state: 'open' });
const created = await sdk.gitee.milestones.create({
owner: 'o',
repo: 'r',
title: 'v0.3.0',
dueOn: '2026-10-01',
});
await sdk.cnb.labels.create({ owner: 'xindiwo', repo: 'git-sdk', name: 'feature', color: '00ff00' });
await sdk.cnb.labels.update({ owner: 'xindiwo', repo: 'git-sdk', name: 'feature', newName: 'feat' });
await sdk.cnb.labels.delete({ owner: 'xindiwo', repo: 'git-sdk', name: 'feat' });
const contributors = await sdk.gitee.contributors.list({ owner: 'o', repo: 'r', perPage: 10 });
contributors.items[0]?.contributions;2.3 仓库生命周期与协作者
// 建仓:Gitee / GitCode 不传 org 就建到当前用户空间,CNB 必须指定组织
const repo = await sdk.cnb.repositories.create({
name: 'demo',
org: 'xindiwo',
description: '演示仓库',
private: true,
});
// Fork 与删除(删除不可恢复)
await sdk.gitee.repositories.fork({ owner: 'o', repo: 'r', organization: 'xindiwo', name: 'r-fork' });
await sdk.gitee.repositories.delete({ owner: 'xindiwo', repo: 'r-fork' });
// 归档 / 转移仅 CNB 支持
await sdk.cnb.repositories.archive({ owner: 'xindiwo', repo: 'demo' });
await sdk.cnb.repositories.transfer({ owner: 'xindiwo', repo: 'demo', target: 'other/demo' });
// 协作者管理
const members = await sdk.cnb.collaborators.list({ owner: 'xindiwo', repo: 'git-sdk' });
await sdk.cnb.collaborators.add({
owner: 'xindiwo',
repo: 'git-sdk',
username: 'alice',
permission: 'Developer',
});
const permission = await sdk.cnb.collaborators.permission({
owner: 'xindiwo',
repo: 'git-sdk',
username: 'alice',
});
await sdk.cnb.collaborators.remove({ owner: 'xindiwo', repo: 'git-sdk', username: 'alice' });2.4 PR 评审与处理人
// 评审:CNB 支持三种结论;Gitee 与 GitCode 的接口都只表达「审查通过」
await sdk.cnb.pullRequests.review({ owner: 'o', repo: 'r', number: 7, decision: 'approve' });
await sdk.cnb.pullRequests.review({
owner: 'o',
repo: 'r',
number: 7,
decision: 'requestChanges',
body: '请补充单元测试',
});
// 评审记录(仅 CNB)
const reviews = await sdk.cnb.pullRequests.listReviews({ owner: 'o', repo: 'r', number: 7 });
// 处理人
await sdk.cnb.issues.addAssignees({ owner: 'o', repo: 'r', number: 1, assignees: ['alice'] });
await sdk.cnb.pullRequests.removeAssignees({ owner: 'o', repo: 'r', number: 7, assignees: ['alice'] });2.5 附件、评审人与成员
// 附件:只有 CNB 开放,返回的 assetLink 可以直接粘进 Issue / PR 正文
const [attachment] = await sdk.cnb.attachments.upload({
owner: 'xindiwo',
repo: 'git-sdk',
files: [{ name: 'screenshot.png', content: pngBytes, contentType: 'image/png' }],
});
await sdk.cnb.issues.createComment({
owner: 'xindiwo',
repo: 'git-sdk',
number: 1,
body: attachment.assetLink,
});
// 评审人:三家接口形态不同,统一成 addReviewers / removeReviewers
await sdk.gitcode.pullRequests.addReviewers({ owner: 'o', repo: 'r', number: 7, reviewers: ['alice'] });
await sdk.gitee.pullRequests.removeReviewers({ owner: 'o', repo: 'r', number: 7, reviewers: ['alice'] });
// 成员:传 repo 作用到仓库成员,不传则作用到组织成员(Gitee / GitCode 只支持仓库维度)
await sdk.cnb.members.add({ owner: 'o', repo: 'r', username: 'alice', permission: 'push' });
await sdk.cnb.members.update({ owner: 'o', repo: 'r', username: 'alice', permission: 'admin' });
await sdk.cnb.members.remove({ owner: 'o', repo: 'r', username: 'alice' });2.6 分支保护与构建(CNB)
// 分支保护:Gitee 简化接口、GitCode 规则接口、CNB 改用锁定
await sdk.gitee.branches.protect({ owner: 'o', repo: 'r', branch: 'main' });
await sdk.gitcode.branches.protect({ owner: 'o', repo: 'r', branch: 'release/*', pusher: 'develop' });
await sdk.cnb.branches.lock({ owner: 'o', repo: 'r', branch: 'main' });
// 触发构建并等待结束(仅 CNB)
const build = await sdk.cnb.builds.start({ owner: 'o', repo: 'r', branch: 'main' });
const finished = await waitForBuildFinished(sdk.cnb.builds, {
owner: 'o',
repo: 'r',
sn: build.sn,
});
finished.status; // 'success' | 'error' | 'cancel'
// 等 PR 被合并
const pr = await waitForPullMerged(sdk.cnb.pullRequests, { owner: 'o', repo: 'r', number: 7 });3. Issue 全流程
const issue = await sdk.cnb.issues.create({
owner: 'xindiwo',
repo: 'git-sdk',
title: 'feat: 支持 Webhook 校验',
body: '需求描述',
labels: ['feature'],
priority: 'P1', // CNB 专有,其他平台自动忽略
});
await sdk.cnb.issues.createComment({ owner: 'xindiwo', repo: 'git-sdk', number: issue.number, body: '已排期' });
await sdk.cnb.issues.close({ owner: 'xindiwo', repo: 'git-sdk', number: issue.number });
// 时间线(操作日志)
const events = await sdk.cnb.issues.listEvents({ owner: 'xindiwo', repo: 'git-sdk', number: issue.number });
events.items.map((event) => `${event.type}:${event.actor?.username}`); // ['opened:sunchengxin', ...]
// 标签:setLabels 是整体替换,removeLabels 按名称移除
await sdk.cnb.issues.setLabels({ owner: 'xindiwo', repo: 'git-sdk', number: issue.number, labels: ['feature'] });
await sdk.cnb.issues.removeLabels({ owner: 'xindiwo', repo: 'git-sdk', number: issue.number, labels: ['feature'] });4. Pull Request
const pr = await sdk.gitcode.pullRequests.create({
owner: 'xindiwo',
repo: 'git-sdk',
title: 'feat: 新增统一映射',
head: 'feat/map',
base: 'main',
});
await sdk.gitcode.pullRequests.createComment({ owner: 'xindiwo', repo: 'git-sdk', number: pr.number, body: '请评审' });
const files = await sdk.gitcode.pullRequests.listFiles({ owner: 'xindiwo', repo: 'git-sdk', number: pr.number });
const result = await sdk.gitcode.pullRequests.merge({
owner: 'xindiwo',
repo: 'git-sdk',
number: pr.number,
mergeStyle: 'squash',
});
result.merged; // true5. 自动翻页
import { iterateItems } from '@xindiwo/git-sdk';
for await (const issue of iterateItems((params) =>
sdk.cnb.issues.list({ owner: 'xindiwo', repo: 'git-sdk', ...params }),
)) {
console.log(issue.number, issue.title);
}
// 或一次性收集(注意控制 maxPages)
const all = await collectItems((p) => sdk.gitee.issues.list({ owner: 'o', repo: 'r', ...p }), {}, { maxPages: 5 });6. raw 逃生通道
// 返回响应体
const data = await sdk.cnb.raw({ path: '/xindiwo/git-sdk/-/git/contents/README.md' });
// 返回完整响应(可读响应头)
const { data, headers, status } = await sdk.gitee.request({ method: 'GET', path: '/repos/o/r' });
// 需要平台独有字段时,用模型上的 raw 即可,无需再发请求
const extra = (issue.raw as Record<string, unknown>)['issue_state'];2.7 下载与 CNB 深度能力
// 原始内容:不解码,二进制文件直接用 content(Uint8Array)
const logo = await sdk.cnb.contents.raw({ owner: 'o', repo: 'r', ref: 'main', path: 'docs/logo.png' });
const text = await sdk.gitee.contents.raw({ owner: 'o', repo: 'r', ref: 'main', path: 'README.md' });
text.text; // 文本类会顺带给出解码结果
// 仓库归档(tar.gz)与 LFS 预签名链接
const archive = await sdk.cnb.repositories.downloadArchive({ owner: 'o', repo: 'r', ref: 'main' });
const lfs = await sdk.cnb.repositories.lfsDownloadLink({ owner: 'o', repo: 'r', oid: 'abc123' });
// 提交元数据与状态检查(CNB)
await sdk.cnb.commits.putAnnotations({
owner: 'o',
repo: 'r',
sha: 'abc',
annotations: [{ key: 'ci', value: 'passed' }],
});
const statuses = await sdk.cnb.commits.listStatuses({ owner: 'o', repo: 'r', ref: 'abc' });
// 构建深度:Stage 详情、AI 用量与 Runner 日志
const stage = await sdk.cnb.builds.getStage({
owner: 'o',
repo: 'r',
sn: 'cnb-91g-1k2d15h4n',
pipelineId: 'cnb-91g-1k2d15h4n-001',
stageId: 'stage-1',
});
const log = await sdk.cnb.builds.downloadRunnerLog({ owner: 'o', repo: 'r', pipelineId: 'cnb-91g-1k2d15h4n-001' });
// 徽章(CNB)
const badges = await sdk.cnb.repositories.listBadges({ owner: 'o', repo: 'r' });2.8 组织、成员深度与提交附件
// 组织管理(仅 CNB):写到空间级,增删改请谨慎
await sdk.cnb.organizations.create({ path: 'acme', description: '演示组织' });
const subs = await sdk.cnb.organizations.listSubGroups({ org: 'acme' });
const settings = await sdk.cnb.organizations.settings({ org: 'acme' });
await sdk.cnb.organizations.updateSettings({ org: 'acme', hideMembers: 1 });
// 成员深度:继承成员与外部贡献者(仅 CNB)
const inherited = await sdk.cnb.members.listInherited({ owner: 'o', repo: 'r' });
const outside = await sdk.cnb.collaborators.listOutside({ owner: 'o' });
// 账号(仅 CNB)
const emails = await sdk.cnb.users.listEmails();
const gpgKeys = await sdk.cnb.users.listGpgKeys();
// 提交附件:一步上传(申请凭据 → 直传 → 确认 → 回读),或拆成两步自己控制
const asset = await sdk.cnb.commits.uploadAsset({
owner: 'o',
repo: 'r',
sha: 'abc',
name: 'report.txt',
content: 'hello',
});
await sdk.cnb.commits.deleteAsset({ owner: 'o', repo: 'r', sha: 'abc', id: asset.id });
// 变更文件包(zip):单提交 / 两次提交比较
const changed = await sdk.cnb.commits.downloadChangedFiles({ owner: 'o', repo: 'r', ref: 'abc' });
const diff = await sdk.cnb.commits.downloadCompareChangedFiles({
owner: 'o',
repo: 'r',
base: 'main',
head: 'dev',
});能力矩阵
下表逐平台列出 Gitee / GitCode / CNB 三家的能力覆盖与差异;GitHub / GitLab / Bitbucket 三家复用同一套资源接口与归一化模型(见上方「特性」),并参与同一份六家平台全量能力矩阵(
tests/full),不另起对照表。
| 能力 | Gitee | GitCode | CNB |
| --- | :-: | :-: | :-: |
| 仓库信息 / 列表 / 搜索 / 更新 | ✅ | ✅ | ✅(更新支持描述/主页/可见性/主题) |
| 仓库创建 / 删除 | ✅ | ✅ | ✅(建仓必须指定组织) |
| 仓库归档 / 解除归档 / 转移 | ⛔ | ⛔ | ✅ |
| 仓库 Fork | ✅ | ✅ | ⛔ 仅 Fork 列表 |
| 仓库 Fork 列表 | ✅ | ✅ | ✅ |
| 仓库语言构成 | ✅ | ✅ | ⛔ 只在仓库详情给出主语言 |
| Star 用户列表 | ✅ | ✅ | ✅ |
| 协作者(增删 / 查权限) | ✅ | ✅ | ✅ |
| 文件内容:读取文件 / 目录 / README | ✅ | ✅ | ✅ |
| 文件内容:提交变更(新建 / 更新 / 删除) | ✅ | ✅ | ⛔ 需走 git push |
| 里程碑(列表 / 详情 / 增删改) | ✅ | ✅ | ⛔ 无该概念 |
| 贡献者列表 | ✅(带提交数) | ✅(带提交数) | ✅(活跃排行,无提交数) |
| 分支(列表 / 详情 / 创建) | ✅ | ✅ | ✅ |
| 分支删除 | ⛔ 未开放该接口 | ✅ | ✅ |
| 分支保护规则 | ✅ | ✅ | ⛔ 用分支锁定替代 |
| 分支锁定 / 解锁 | ⛔ | ⛔ | ✅ |
| 构建触发 / 状态 / 停止 | ⛔ | ⛔ | ✅ |
| Git 标签(列表 / 详情 / 创建) | ✅(详情为列表检索) | ✅(同左) | ✅ |
| Git 标签删除 | ⛔ 未开放该接口 | ✅ | ✅ |
| 提交(列表 / 详情) | ✅ | ✅ | ✅ |
| 提交比较 | ✅ | ✅ | ✅ |
| 提交评论(列表 / 创建) | ✅ | ✅ | ⛔ 未开放该接口 |
| Issue(列表/详情/创建/更新/关闭/重开) | ✅ | ✅ | ✅ |
| Issue 评论(列表 / 创建) | ✅ | ✅ | ✅ |
| Issue 时间线(操作日志) | ⛔ 只提供 PR 操作日志 | ✅ | ✅ |
| Issue 标签整体替换 / 移除 | ✅ | ✅ | ✅ |
| PR(列表/详情/创建/更新/合并) | ✅ | ✅ | ✅ |
| PR 评论 / 提交 / 变更文件 | ✅ | ✅ | ✅ |
| PR 时间线(操作日志) | ✅ | ✅ | ⛔ 未开放该接口 |
| PR 评审(提交 / 列表) | 仅审查通过 | 仅审查通过 | ✅ 完整 |
| PR 评审人(指派 / 移除) | ✅ 走 assignees | ✅ | ✅ 最多 8 人 |
| Issue / PR 处理人指派 | ⛔ 改走 update | PR ✅ / Issue ⛔ | ✅ |
| 标签 label(列表 / 增删改) | ✅ | ✅ | ✅ |
| Release(列表 / 详情 / 按 tag / 创建 / 更新) | ✅ | ✅ | ✅ |
| Release 删除 | ✅ | ⛔ 未开放该接口 | ✅ |
| 仓库动态(事件流) | ✅ | ✅ | ⛔ 未开放该接口 |
| Webhook(列表/详情/创建/更新/删除) | ✅ | ✅ | ⛔ 不支持(CNB 用流水线触发替代) |
| 用户(当前用户 / 指定用户) | ✅ | ✅ | ✅ |
| 用户仓库列表 | ✅(仅公开仓库) | ✅ | ✅ |
| 用户 Star 列表 | ✅ | ✅ | ⛔ 未开放该接口 |
| 组织(详情 / 用户所属组织列表) | ✅ | ✅ | ✅ |
| 成员(组织 / 仓库) | ✅ | ✅ | ✅ |
| 组织管理(创建 / 更新 / 删除 / 子组织 / 配置) | ⛔ | ⛔ | ✅ |
| 组织长尾(转移资源 / 仓库墙 / 配额与用量) | ⛔ | ⛔ | ✅ |
| 仓库安全总览(敏感信息 / 漏洞 / 问题 / 许可证) | ⛔ | ⛔ | ✅ |
| Issue 自定义属性定义(可见 / 隐藏) | ⛔ | ⛔ | ✅ |
| PR 按编号批量查询 | ⛔ | ⛔ | ✅ |
| 有效成员列表(含继承) | ⛔ | ⛔ | ✅ |
| 用户资料更新 | ⛔ | ⛔ | ✅ |
| 构建计划同步(crontab) | ⛔ | ⛔ | ✅ |
| CLI 写操作子命令 | ✅(Gitee 支持文件写入等) | ✅ | 部分(CNB 不能写文件) |
| 继承成员 / 外部贡献者 | ⛔ | ⛔ | ✅(外部贡献者仅真实组织可用) |
| 用户邮箱 / GPG 公钥 | ⛔ | ⛔ | ✅(邮箱需 account-email:r) |
| 提交附件(列表 / 上传 / 删除) | ⛔ | ⛔ | ✅ 三步链路 |
| 提交变更文件包(单提交 / 两提交比较) | ⛔ | ⛔ | ✅ zip |
| 原始文件内容(不解码) | ✅ | ✅(同源实现,未实测) | ✅ |
| 仓库归档下载(tar.gz) | ✅ | ✅(同源实现,未实测) | ✅ |
| LFS 下载链接 | ⛔ | ⛔ | ✅ |
| 仓库徽章(列表 / 图片 / 上传) | ⛔ | ⛔ | ✅ |
| 提交元数据 annotations(查 / 批量 / 写 / 删) | ⛔ | ⛔ | ✅ |
| 提交状态检查(提交 / PR 维度) | ⛔ | ⛔ | ✅ |
| 构建 Stage 详情 / AI 用量 / Runner 日志 / 删日志 | ⛔ | ⛔ | ✅ |
| 成员增删改(仓库维度) | ✅ | ✅ | ✅ 需 repo-manage:rw |
| 成员增删改(组织维度) | ⛔ | ⛔ | ✅ 需 repo-manage:rw |
| 附件上传(Issue / PR) | ⛔ 未开放该接口 | ⛔ 未开放该接口 | ✅ 三步链路 |
| 搜索(仓库 / Issue) | ✅ | ✅ | 仓库 ✅ / Issue ⛔ |
不支持的能力会抛出 UnsupportedOperationError,而不是静默返回错误数据。
统一模型
所有模型都带 platform 与 raw 两个字段:
interface Issue {
platform: 'gitee' | 'gitcode' | 'cnb' | 'github' | 'gitlab' | 'bitbucket';
id: string;
number: string;
title: string;
body?: string;
state: 'open' | 'closed';
author?: User;
assignees: User[];
labels: Label[];
commentCount: number;
htmlUrl?: string;
createdAt?: string; // 统一 ISO 8601
updatedAt?: string;
closedAt?: string;
raw: unknown; // 平台原始响应
}主要归一化规则:
- 时间:统一为 ISO 8601 字符串(
2026-09-12T08:02:15.000Z),无法解析时保持undefined。 - ID / 编号:统一为
string(Gitee 的number可能是I1ABC,CNB 为数字字符串,GitCode 为数字)。 - 状态:Issue 归一为
open/closed(Gitee 的progressing、rejected也会正确归一);PR 归一为open/closed/merged。 - 分支:PR/Issue 里的
refs/heads/main会裁剪成main。 - 附件 / 事件:Gitee 的
assets: string[]会归一为{ name, downloadUrl };Webhook 的布尔事件开关会归一为events: string[]。
分页语义
interface Page<T> {
items: T[];
page: number;
perPage: number;
total?: number; // 平台未返回时为 undefined
hasNext: boolean; // total 已知时精确判断,未知时按「本页条数 == perPage」推断
}| 平台 | 请求参数 | 总数来源 |
| --- | --- | --- |
| Gitee | page / per_page | 响应头 total_count |
| GitCode | page / per_page | 响应头 total_count |
| CNB | page / page_size | 响应头 X-Cnb-Total |
| GitHub | page / per_page | 响应头 Link: rel="next"(含 page 游标)+ x-total |
| GitLab | page / per_page | 响应头 x-total |
| Bitbucket | pagelen | 响应体 next(游标),MAX_CURSOR_HOPS 防死循环 |
错误处理
import { GitSdkError, isGitSdkError, NotFoundError } from '@xindiwo/git-sdk';
try {
await sdk.cnb.repositories.get({ owner: 'xindiwo', repo: 'not-exists' });
} catch (error) {
if (isGitSdkError(error)) {
error.code; // 'NOT_FOUND' | 'AUTHENTICATION' | 'RATE_LIMIT' | ...
error.status; // 404
error.platform; // 'cnb'
error.url; // 完整请求地址
error.body; // 平台原始错误体
}
}| 错误类 | 触发条件 |
| --- | --- |
| AuthenticationError | 401 令牌无效 / 缺失 |
| ForbiddenError | 403 权限不足 |
| NotFoundError | 404 资源不存在 |
| ValidationError | 400 / 422 参数错误 |
| ConflictError | 409 冲突 |
| RateLimitError | 429 超限(含 retryAfter) |
| ServerError | 5xx |
| NetworkError / TimeoutError | 网络异常 / 超时 |
| UnsupportedOperationError | 该平台未提供该能力 |
重试策略:默认对 408/425/429/5xx 与网络异常重试 2 次,退避 300ms × 2^n(上限 10s),429 优先遵循 Retry-After(封顶 60s)。可通过 maxRetries / retryDelay / maxRetryDelay / timeout 调整。
只重试幂等方法:GET / PUT / DELETE 会自动重试;POST / PATCH 不重试——服务端可能已经执行成功、只是响应丢失,重试会建出重复的 Issue 或触发重复构建。
认证说明
每个平台只需要一把全权限令牌,创建一个就够用,不需要为不同能力拆分成多个密钥。
| 平台 | 环境变量(按优先级) | 传递方式 | 未配令牌时 |
| --- | --- | --- | --- |
| Gitee | GITEE_TOKEN → GITEE_ACCESS_TOKEN | Authorization: token 请求头 | 可匿名读公开数据;限流 60 次/小时,触发时抛 RateLimitError |
| GitCode | GITCODE_TOKEN → GITCODE_ACCESS_TOKEN | private-token 请求头 | 首次请求抛 AuthenticationError,提示该配哪个变量 |
| CNB | CNB_TOKEN → CNB_ACCESS_TOKEN | Authorization: Bearer | 首次请求抛 AuthenticationError,提示该配哪个变量 |
- 令牌只保留在客户端实例内存里,不会写入日志或错误信息。
sdk.platforms返回已配令牌的平台;单平台可用client.hasToken判断。- 建议优先通过环境变量注入,不要硬编码进代码。
使用 CNB npm 制品库
# 本地安装
npm config set @xindiwo:registry https://npm.cnb.cool/xindiwo/git-sdk-v1/-/packages/
npm config set //npm.cnb.cool/xindiwo/git-sdk-v1/-/packages/:_authToken=$CNB_TOKEN
npm install @xindiwo/git-sdk命令行工具
npx @xindiwo/git-sdk whoami
npx @xindiwo/git-sdk repo cnb xindiwo/git-sdk
npx @xindiwo/git-sdk issues cnb xindiwo/git-sdk --state open --limit 10
npx @xindiwo/git-sdk prs gitee mindspore/mindspore
npx @xindiwo/git-sdk branches cnb xindiwo/webhook
npx @xindiwo/git-sdk contents cnb xindiwo/webhook README.md
npx @xindiwo/git-sdk builds cnb xindiwo/git-sdk
npx @xindiwo/git-sdk reviews gitee mindspore/mindspore 123写操作子命令(会真的改动远端,每条都会打印结果便于确认):
npx @xindiwo/git-sdk issue-create cnb xindiwo/git-sdk --title '登录失败' --labels bug --assignees alice
npx @xindiwo/git-sdk issue-comment cnb xindiwo/git-sdk --number 1 --body '已定位到原因'
npx @xindiwo/git-sdk issue-close cnb xindiwo/git-sdk --number 1
npx @xindiwo/git-sdk pr-comment gitee o/r --number 7 --body '请补充单测'
npx @xindiwo/git-sdk pr-review gitee o/r --number 7 --decision approve
npx @xindiwo/git-sdk pr-reviewers gitcode o/r --number 7 --reviewers alice,bob
npx @xindiwo/git-sdk pr-merge gitee o/r --number 7 --style squash --force
npx @xindiwo/git-sdk label-create cnb o/r --name bug --color FF0000
npx @xindiwo/git-sdk build-start cnb o/r --branch main
npx @xindiwo/git-sdk build-stop cnb o/r --sn cnb-91g-1k2d15h4n
npx @xindiwo/git-sdk release-create cnb o/r --tag v1.0.0 --name '首个版本'
npx @xindiwo/git-sdk content-write gitee o/r --path docs/a.md --content '# hi' --message 'docs: 新增'令牌同样只从环境变量读取,命令行不接受令牌参数,避免写进 shell history。
Webhook 与可观测性
统一事件解析与验签
import { parseWebhookEvent, verifyWebhook } from '@xindiwo/git-sdk';
// 口令校验(Gitee / GitCode / CNB / GitLab 通过请求头投递固定令牌,SDK 用定时安全比较;
// GitHub / Bitbucket 走 HMAC-SHA256 签名,校验需传原始 body)
if (!verifyWebhook({ platform: 'gitee', secret: process.env.WEBHOOK_SECRET, headers: req.headers })) {
throw new Error('签名不合法');
}
// 解析成统一事件
const event = parseWebhookEvent({ platform: 'gitee', headers: req.headers, body: rawBody });
event.kind; // 'push' | 'tag' | 'issue' | 'pullRequest' | 'comment' | 'release' | 'unknown'
event.repository?.fullName;
event.sender?.username;
event.action; event.number; event.ref; event.sha;
event.payload; // 原始 payload,需要平台独有字段时用它请求钩子与配额
const sdk = createGitSdk({
cnb: {
token,
hooks: {
onRequest: ({ method, url, attempt }) => logger.debug(`${method} ${url} #${attempt}`),
onResponse: ({ status, durationMs, rateLimit }) =>
logger.info(`${status} ${durationMs}ms 剩余配额 ${rateLimit.remaining ?? '?'}`),
onRetry: ({ attempt, delayMs }) => logger.warn(`第 ${attempt} 次重试,等待 ${delayMs}ms`),
onError: ({ error }) => logger.error(error),
},
},
});质量保障
发布前跑一次全量测试(0.x 为预览版,1.0 起为正式版):
| 手段 | 位置 | 规模 |
| --- | --- | --- |
| 全量能力矩阵 | tests/full/matrix.test.ts | 140+ 个能力 × 6 家平台(含附件、评审人、成员与组织、下载与 CNB 深度能力) |
| 脏数据健壮性 | tests/full/robustness.test.ts | 16 种畸形载荷 × 44 接口 × 6 平台 |
| 随机模糊 | tests/full/fuzz.test.ts | 固定种子,120 个接口 × 500 轮 × 6 平台 |
| 异常路径 | tests/full/errors.test.ts | 10 个状态码 × 160 个能力 × 6 平台 |
| 契约矩阵 | tests/unit/contract.test.ts | 能力边界断言,逐平台锁定「支持 / 明确不支持」 |
| 映射层矩阵 | tests/unit/map.test.ts | 326 条:六家 20 个映射函数的健壮性、别名回退与关键字段值 |
| 公共 API 面 | tests/unit/api-surface.test.ts | 编译期校验:方法返回/接收的类型都能从包根导入 |
| CLI | tests/unit/cli.test.ts | 参数解析、取值校验与命令分支 |
| 边界用例 | tests/unit/edge-cases.test.ts | 分页边界、取消语义、退避时长与 Retry-After 封顶 |
| 分支 / 标签写操作 | tests/unit/v12-branch-tag-write.test.ts | 请求形状、起点兜底(默认分支 / git/head)与平台差异 |
| 提交与生态数据 | tests/unit/v13-eco-data.test.ts | 提交比较、提交评论、Fork / 语言 / Star、Webhook 更新与用户仓库的路径与边界 |
| 时间线与标签 | tests/unit/v14-timeline-labels.test.ts | 三家时间线字段差异、标签整体替换的三种做法与回读兜底 |
| Release 与仓库动态 | tests/unit/v15-release-events.test.ts | Release 标识符差异、必填说明的回读补全、事件流拆包 |
| 附件与协作 | tests/unit/v16-attachments-reviewers.test.ts | 附件三步上传(含直传绝对地址)、评审人的三种接口形态、成员权限映射 |
| CNB 深度能力 | tests/unit/v17-cnb-deep.test.ts | 二进制下载与重定向处理、提交元数据与状态、构建 Stage / AI 用量、徽章 |
| 组织与提交附件 | tests/unit/v18-org-assets.test.ts | 组织增删改与配置、继承成员与外部贡献者、账号信息、提交附件三步链路 |
| 长尾能力 | tests/unit/v19-extra.test.ts | 组织转移 / 仓库墙 / 配额用量、Issue 属性、PR 批量查询、安全总览、构建计划同步 |
| CLI 写操作 | tests/unit/v19-cli-writes.test.ts | 13 个子命令的请求形状、必填校验、参数解析与输出 |
| 真实接口回归 | tests/unit/real-api-regressions.test.ts | 线上实测发现的平台差异(颜色格式、审查 force、合并门槛) |
| 请求层 | tests/unit/http.test.ts | 序列化、表单/JSON、重试退避、超时 |
| 评审回归 | tests/unit/review-regressions.test.ts | 令牌脱敏、钩子接线、重试幂等、路径编码 |
| 真实写操作 | tests/integration/gitee-write.test.ts | Gitee 全量写入链路(建仓→Issue→PR→合并→删库),需 GIT_SDK_TEST_WRITE=1 |
| 真实写操作 | tests/integration/gitcode-write.test.ts | GitCode 全量写入链路(含里程碑与文件写入),需 GIT_SDK_TEST_WRITE=1 + GITCODE_TOKEN |
| 真实接口 | tests/integration/v16-cnb.test.ts | CNB 贡献者 / 成员 / 附件(附件需 GIT_SDK_TEST_WRITE=1) |
| 真实接口 | tests/integration/v17-cnb.test.ts | CNB 原始内容 / 归档 / 徽章 / 提交状态与元数据;Gitee 原始内容与归档 |
| 真实接口 | tests/integration/v18-cnb.test.ts | CNB 组织配置 / 继承成员 / 账号 / 提交附件(附件需 GIT_SDK_TEST_WRITE=1) |
| 真实接口 | tests/integration/v19-cnb.test.ts | CNB 配额用量 / 仓库墙 / 有效成员 / Issue 属性 / 安全总览 / PR 批量查询 |
| 性能基线 | npm run bench → docs/PERF.md | 单页 20 条 0.17 ms/次、1000 条 6.1 ms/次 |
最近一次云端流水线全绿(0 失败):单元 950 / 全量 4792,集成测试按令牌按需启用,报告见 docs/FULL-TEST.md。
npm run test # 单元测试(950 条,秒级)
npm run test:full # 全量测试(4792 条,含模糊测试)
npm run test:integration # 真实接口集成测试(未配置令牌的组自动跳过)
npm run test:all # 全量 + 单元 + 集成,并输出汇总报告
npm run verify:full # 发布前门禁:规范检查 + 全部测试 + 构建
npm run bench # 性能基线集成套件使用独立的
vitest.integration.config.ts(单用例预算 180 秒):真实接口的最坏耗时要算上Retry-After休眠,即timeout × (maxRetries + 1) + min(retryAfter, 60s),最坏可达 120 秒。 预算不足时先抛出的是 vitest 的裸超时错误,tolerateEnvironment无法识别,平台抖动会被误判成失败。
代码规范与检查
规范检查分两层,npm run review 一次跑完;流水线里与单元测试、构建串联,任一项不通过即阻断合并与发布。
覆盖范围为 src / tests / scripts / examples / 根目录配置(共 96 个文件):
| 规则 | 阈值 | 适用范围 |
| --- | --- | --- |
| 单文件行数 | ≤ 800 行 | 全部 |
| 单行长度 | ≤ 120 字符(URL 豁免) | 全部 |
| 文件头注释 | 必须说明该文件职责 | 全部 |
| 作者标记 | 文件头注释必须含 @author fntp | 全部 |
| 注释占比 | ≥ 3% | src、scripts |
| 行注释 // | 含逻辑的文件必须有 | src、scripts |
| 导出 JSDoc | 导出的函数/类/常量/类型必须有 | src |
| 禁用写法 | console.*、debugger、@ts-ignore、@ts-nocheck、any | 库代码严格;示例与脚本允许打印 |
| 层 | 工具 | 覆盖内容 |
| --- | --- | --- |
| 语法与风格 | ESLint + typescript-eslint | 未使用变量、隐式 any、eqeqeq、no-console、类型导入、空行与排版等 |
| 项目约定 | scripts/check-standards.mjs | 文件头注释、作者标记、注释覆盖率、行注释、导出注释、行数上限等 |
硬性规则
| 规则 | 阈值 | 说明 |
| --- | --- | --- |
| 单文件行数 | ≤ 800 行 | 超过请按职责拆分模块 |
| 单行长度 | ≤ 120 字符 | URL 与长字符串豁免 |
| 文件头注释 | 必须 | 每个源文件都要有注释块说明该文件职责 |
| 作者标记 | 必须 | 文件头注释块里要写 @author fntp;函数级 JSDoc 不重复署名 |
| 注释占比 | ≥ 3%(src) | 纯类型声明文件不强制行注释,避免为了凑规则写废话 |
| 行注释 | 含逻辑的文件必须有 // | 用于解释关键分支与平台差异 |
| 导出注释 | 必须 | 导出的函数 / 类 / 常量 / 类型都要有 JSDoc |
| 禁用写法 | 必须为 0 | console.*、debugger、@ts-ignore、@ts-nocheck、any |
| 待办标记 | 仅提示 | TODO / FIXME 不阻断,可用 --strict-todo 升级为错误 |
本地执行
npm run review # 类型检查 + ESLint + 规范检查(与流水线一致)
npm run check:standards # 只跑规范检查
npm run lint:fix # 自动修复可修复的 ESLint 问题
node scripts/check-standards.mjs --max-lines 800 --max-len 120 --dirs src
node scripts/check-standards.mjs --strict-todo # 把 TODO 也当作错误
node scripts/check-standards.mjs --author fntp # 指定要求的作者署名(传空字符串可关闭该规则)检查失败会直接列出「文件 + 行号 + 原因」:
✗ src/platforms/gitee/client.ts (611 行 / 注释 28 行)
✗ L1 注释占比 2.4% 低于要求 3%
✗ L120 导出声明缺少 JSDoc 注释:export const mapTag =文档导航
| 文档 | 内容 |
| --- | --- |
| docs/STATUS.md | 现状交接:当前基线、待补外部条件、如何恢复现场 |
| docs/ROADMAP.md | 版本路线、每版任务清单与验收结果 |
| CHANGELOG.md | 各版本对外变更与破坏性变更 |
| docs/MIGRATION.md | 0.x 各版本之间的升级步骤 |
| docs/PERF.md | 性能基线(npm run bench 生成) |
| docs/FULL-TEST.md | 全量测试报告(云端流水线快照,可用 test:full --write-doc 重新生成) |
| docs/CODE-REVIEW-v2.0.0.md | v2.0.0 代码评审记录:缺陷分析、修复方案与架构说明 |
| docs/DEVELOPMENT-NOTES.md | 开发与调试笔记:云端验证方法、平台实测硬事实、测试体系、规范雷区、已知遗留 |
| examples/ | 可直接运行的示例集(含 examples/README.md) |
| npm run docs | 用 typedoc 生成 API 文档站点(输出到 docs/api) |
迭代规划
版本路线、每版任务清单与验收标准见 docs/ROADMAP.md。
开发
npm install
npm run typecheck # 类型检查
npm run test # 单元测试(Mock,无需令牌)
npm run build # 构建 ESM + CJS + d.ts
npm run test:integration # 真实接口集成测试集成测试按令牌自动启用/跳过:
| 平台 | 前置条件 | 默认测试仓库 |
| --- | --- | --- |
| Gitee | 无需令牌(公开数据),GITEE_TOKEN 可解锁授权接口 | mindspore/mindspore |
| GitCode | 需要 GITCODE_TOKEN | mindspore/mindspore |
| CNB | 需要 CNB_TOKEN | xindiwo/webhook |
可通过 GIT_SDK_TEST_REPO_GITEE / GIT_SDK_TEST_REPO_GITCODE / GIT_SDK_TEST_REPO_CNB 覆盖测试仓库。
真实写操作集成测试另需 GIT_SDK_TEST_WRITE=1(会建测试仓库,跑完自动删除):
Gitee 见 tests/integration/gitee-write.test.ts,GitCode 见 tests/integration/gitcode-write.test.ts,
临时仓库名可用 GIT_SDK_TEST_WRITE_REPO 覆盖。
发布流程
版本发布由流水线自动完成(见 .cnb.yml):
npm version 2.0.0 --no-git-tag-version # 或直接改 package.json
git commit -am "chore: release v2.0.0"
git tag v2.0.0
git push origin main --tagstag_push 触发流水线:安装依赖 → 用 tag 名回写版本号 → 规范检查 + 单测 → 发布到 xindiwo/git-sdk-v1 制品库(CNB)。
push / pull_request 触发同样的「规范检查 + 单测 + 构建」,但不发布。
公共 npm(npmjs.com)首次发布走 api_trigger 手动触发(见 .cnb.yml 注释),在触发请求里传入 NPMJS_TOKEN;该 stage 不传令牌会自动跳过,不会误发。
已知差异与限制
- CNB 不支持通过 API 提交文件变更(只提供 blob 创建,落库需要 git push),
contents.createFile/updateFile/deleteFile会抛UnsupportedOperationError;读取文件、目录、README 不受影响。 - CNB 没有里程碑概念,
milestones.*全部抛UnsupportedOperationError;贡献者列表来自平台的活跃用户排行,只返回用户信息、没有提交数(contributions为undefined)。 - 构建能力只有 CNB 提供:
builds.start/status/list/stop在 Gitee / GitCode 上抛UnsupportedOperationError;CNB 要求触发事件名以api_trigger开头,SDK 会自动补前缀。 - 分支保护三家风格不同:Gitee 是简化接口(直接保护/取消保护),GitCode 是规则接口(需
pusher/merger,未指定时 SDK 默认给admin/admin——GitCode 要求pusher权限同时含合并权限),CNB 没有保护规则、只有分支锁定,两者通过branches.protect与branches.lock区分。 - GitCode 的仓库链接字段名与 Gitee 不同:用
web_url/http_url_to_repo/ssh_url_to_repo,SDK 已归一为htmlUrl/cloneUrl/sshUrl,并在平台未返回时兜底到gitcode.com。 - GitCode 的标签写操作走查询参数:该接口只读 query / form 参数,JSON 体里的字段会被整体忽略,SDK 已按平台切换;改名用平台的
name参数。 - GitCode 的 Issue 状态用事件名:关闭 / 重新打开下发的是
state=close/state=reopen(传closed/open会被拒),SDK 已按平台归一。 - GitCode 的 PR 列表用
notes表达评论数,建 PR 的响应只有source_branch/target_branch(没有 Gitee 的head/base对象),SDK 会分别回退取值。 - GitCode 把
body当必填:issues.create与releases.create不传body会报「body不能为空」,SDK 缺省补空串。 - CNB 的分支详情返回完整 ref(
refs/heads/main,而列表接口返回短名),SDK 统一裁成main。 - 创建分支 / 标签时起点可以省略:
from/target不传时 SDK 用仓库默认分支兜底;CNB 的仓库响应没有默认分支字段,会额外查一次git/head。 - 提交比较的差异文件字段名不同:Gitee 用
files,GitCode 用diffs,SDK 两者都会读;CNB 用head_commit表达比较目标。 repositories.languages返回的是「语言 → 字节数」映射,只保留数值字段,忽略平台附加的非数值项。- Webhook 更新只下发显式传入的字段,未传的事件开关不会被重置。
- 时间线接口三家叫法不同:Gitee / GitCode 是
operate_logs(事件类型在action_type、描述在content),CNB 是activities(事件类型在type);CNB 的 PR 没有时间线。 - Issue 标签三家的写语义不同:CNB 是追加 / 按名移除,Gitee 用独立接口整体替换,GitCode 只能通过 Issue 更新的
labels字段整体替换。SDK 统一成setLabels(整体替换)+removeLabels(按名称逐个移除),不做「追加」这种三家都保证不了的语义。 - Release 的标识符三家不同:Gitee / CNB 用数字 id,GitCode 的响应里没有 id(SDK 用 tag 兜底,PATCH 也按 tag 定位),所以
releases.update({ id })直接把release.id回传即可。 - GitCode 把发布说明当必填:
releases.update只传name会被判「must not be blank」,SDK 会先读一次现有说明再下发,不会把已有说明清空;CNB 的更新接口返回 200 但空体,SDK 会回查一次拿到真实对象。 - 仓库动态(事件流)列表被包在
{ events: [...] }里,事件类型在action_name、操作人在author,SDK 已归一为RepositoryEvent。 - Gitee 未开放删除分支 / 删除标签 / Issue 操作日志(线上实测 405 / 404 / 404),这三个能力在 Gitee 上抛
UnsupportedOperationError;PR 操作日志是开放的。 - Gitee 的 Issue 标签替换接口要 JSON 数组请求体(
["bug"]),发表单会被判「Problems parsing JSON」。 - Gitee 把
target_commitish(建 Release)、tag_name+body(改 Release)、name(改仓库)当必填,SDK 会先回读现有对象补全这些字段,既满足必填也不会清空已有内容。 - Gitee 的
GET /releases/tags/:tag在 Release 不存在时返回 200 +null,SDK 统一归一成NotFoundError。 - Gitee 的
/users/:username/repos只返回公开仓库,私有仓库要用repositories.list()(不带 owner)查。 - PR 评审三家差异最大:Gitee 与 GitCode 的评审接口都只表达「审查通过」,
decision传requestChanges/comment会抛错;CNB 支持完整的approve/requestChanges/comment,也是唯一提供评审列表的平台。 - Gitee 的 PR 合并有两道门槛:仓库可配置「审查」与「测试」要求,未通过时合并会被拒。
在 Gitee 上给
pullRequests.merge传force: true,SDK 会先强制通过这两道门槛再合并; 其余两家的force是各自的强制合并开关,语义不变。 - Gitee 读取不存在的路径返回 200 + 空数组(不是 404)。因为 Git 不跟踪空目录,
contents.get遇到空数组会翻译成NotFoundError,contents.list则返回空列表。 - Gitee 的标签颜色只认
RRGGBB(带#会报「请输入正确的16进制颜色值」), SDK 会按平台自动归一,两种写法都能传。 - 处理人指派:CNB 的 Issue 与 PR 都支持;GitCode 仅 PR 支持;Gitee 的同类接口语义是「审查人员」而非处理人,因此
addAssignees会提示改用pullRequests.addReviewers,Issue 维度则统一走issues.update的assignees字段。 - Gitee / GitCode 不开放归档与转移仓库,
repositories.archive/unarchive/transfer会抛UnsupportedOperationError。 - CNB 不开放 Fork 创建(只有 Fork 列表查询),且建仓接口挂在空间路径下,
repositories.create必须传org。 - CNB 的权限查询返回「用户在各资源上的权限数组」,
collaborators.permission会自动挑出当前仓库那一条;成员权限字段是access_level。 - Gitee / GitCode 更新或删除文件需要回传目标文件 sha,未传
sha时 SDK 会先读取一次文件自动补全;里程碑更新同理,会先读一次补齐平台标为必填的字段。 - GitCode 没有 README 专用接口,
contents.readme按README.md→readme.md→README.MD→README顺序探测。 - Gitee 的写操作使用表单编码(
application/x-www-form-urlencoded),GitCode / CNB 使用 JSON,SDK 已按平台自动处理。 - CNB 没有仓库 Webhook 接口(用流水线触发替代),调用
webhooks.*会抛UnsupportedOperationError。 - CNB 的公开仓库搜索一次返回
topN条、不支持分页,因此search.repositories的hasNext恒为false。CNB 也未开放 Issue 搜索。 - Gitee / GitCode 没有「查询单个 Tag」接口,
tags.get通过列表检索实现(最多扫描 10 页 × 100 条)。 - 附件只有 CNB 开放:链路是「建附件组 → 直传对象存储 → 取引用」,
attachments.upload会返回可粘进正文的assetLink与downloadUrl;Gitee 的issues/{n}/attach_files(两种路径风格)与 GitCode 的同类接口在线上实测都是 404。 CNB 的附件在 API 层没有回读与删除接口(实测 404),只能在网页端查看。 - 评审人的接口形态三家不同:GitCode 用
POST/DELETE /pulls/{n}/reviewers(reviewers是逗号分隔字符串, 追加时带add,且 PR 作者不能指派自己);Gitee 没有 reviewers 接口(实测 404),SDK 走语义等同审查人员的pulls/{n}/assignees;CNB 用reviewers数组、一次最多 8 人(超出会提前抛ValidationError)。 - 成员增删改在 Gitee / GitCode 上就是协作者接口(两家的接口文档也把它归在 Member 分类),
传
repo即作用到仓库;组织维度的成员管理需要组织管理员令牌且未开放 OpenAPI,会抛UnsupportedOperationError。 - CNB 的成员写入需要
repo-manage:rw权限,令牌只带仓库读写时会返回 403 并提示缺失的 scope。 - CNB 的成员权限是字符串枚举(
Guest<Reporter<Developer<Master<Owner), SDK 会把统一权限映射过去:pull/read→Reporter,push/write→Developer,maintain/admin→Master,owner→Owner;传入 CNB 原生级别时原样透传。 - 原始内容与归档返回字节流:
contents.raw不做 base64 解码,只对text/*与 JSON 响应给出text; 二进制文件(图片、压缩包)请直接用content(Uint8Array)。repositories.downloadArchive返回 tar.gz 字节流。 - Gitee 的归档接口会 302 到预签名地址,SDK 跟随重定向拿到 tar.gz;GitCode 走同源接口但未实测。
- CNB 的 LFS 下载链接是 302 重定向,SDK 用
redirect: 'manual'读location而不是把大文件拉下来; 少数网关会剥掉该响应头,此时退回响应体里的url字段。 - CNB 的提交元数据写入要键值对数组(
{ annotations: [{ key, value }] }),查询返回数组且每条带meta; 批量查询的参数名是commit_hashes而不是shas。 - CNB 的徽章列表线上返回裸数组(OpenAPI 描述里写的是
{ badges: [...] }),SDK 两种形态都兼容。 - CNB 的构建日志接口需要
pipelineId(形如cnb-91g-1k2d15h4n-001,即构建 sn 加序号), 它不在构建列表的顶层字段里,而在列表项的pipelines[].id中。 - CNB 的提交附件是三步链路:申请凭据(字段名是
asset_name)→ 直传upload_url→ POST 确认verify_url。确认地址是平台 API 上的接口,必须带令牌(漏带会 401),而直传地址是对象存储地址、 必须原样直传不带令牌——SDK 已按这一点区分处理,并提供了commits.uploadAsset一步到位。 - CNB 的外部贡献者接口只在真实组织下可用:个人空间调用返回 400「组织不支持当前操作」, SDK 如实抛出该错误而不是伪装成空列表。
- CNB 的用户邮箱接口需要
account-email:r权限,令牌不带该 scope 时返回 403。 - CNB 的组织增删改会真的动到空间,因此
organizations.create/update/delete只做了单元测试, 没有放进集成测试;组织配置与子组织是只读接口,已做真实回归。 - 下载类接口会把整个响应读进内存(
contents.raw、repositories.downloadArchive、getBadge、builds.downloadRunnerLog、commits.downloadChangedFiles):几百 MB 级的大仓库归档建议改用raw()/request({ responseType: 'binary' })直通接口自行做流式处理。 - 错误体里的令牌会被自动脱敏:Gitee 的
access_token与另外两家的授权头都会参与替换, 因此平台即使把令牌回显进错误响应,日志里也只会看到***。 - Gitee 的写操作使用表单编码(
application/x-www-form-urlencoded),GitCode / CNB 使用 JSON,SDK 已按平台自动处理。 - CNB 的仓库更新仅支持描述、主页、可见性、主题标签,其余字段会被忽略。
- 三家平台对同一份数据的权限/可见性规则不同,
private、archived等字段以各平台语义为准。
License
MIT
