npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xindiwo/git-sdk

v2.0.0

Published

统一封装 Gitee / GitCode / CNB / GitHub / GitLab / Bitbucket 六家代码托管平台 OpenAPI 的 Node.js SDK

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-sdk

CNB 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 ✅ sunchengxin

2. 仓库与代码

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; // true

5. 自动翻页

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 --tags

tag_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