写入 API
登记仓库与发布项目两个写入接口的用法。
两个写入接口都要带 key,都收 JSON,都可能因为要抓 GitHub 数据而比读接口慢
(/repos 最长 60 秒,/projects 最长 300 秒——大仓库要拉 README、翻译技能文档)。
「登记」与「发布」是两件事,也是两个 scope
| 接口 | scope | 写进哪张表 | 公开站可见 |
|---|---|---|---|
POST /api/v1/repos | repos:write | repos、user_repos | 否 |
POST /api/v1/projects | projects:write | projects(含 README、技能文档) | 是 |
登记只是让雷达开始跟踪一个仓库;发布是把它放到公开页面上。自助签发拿不到
projects:write,需要发布就找管理员开。
请求体只有两种形态
两个写入端点的请求体形状相同,都只收一个地址——要么完整 URL,要么裸的
owner/repo。两者都可选地带回调参数。
不接受调用方带来的 GitHub 数据
请求体里没有 stars / topics / languages 这些字段,多一个字段整条拒绝
(invalid_body)。那份数据是调用方某一时刻的快照,落库等于让雷达的统计建立在
一个可能过期的副本上;服务端自己去取,多一次请求换来「库里那份和 GitHub 上一致」。
要登记又带分类信息的,走 POST /api/v1/projects——那里有 type,而这里没有。
形态一:完整 URL
curl -X POST "$CONSOLE_ORIGIN/api/v1/repos" \
-H "Authorization: Bearer $CONSOLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://github.com/vercel/next.js"}'url 接受 https://github.com/owner/repo、不带协议的 github.com/owner/repo、
owner/repo,末尾带 .git 或 / 也认。
形态二:裸 owner/repo
curl -X POST "$CONSOLE_ORIGIN/api/v1/repos" \
-H "Authorization: Bearer $CONSOLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"repo":"vercel/next.js"}'这种形态拒绝带协议的输入({"repo":"https://github.com/vercel/next.js"} 返回
invalid_url)——两种形态各有各的写法,混着写只会让一半的请求白跑。
回调参数
两个形态都接受 callbackUrl + callbackSecret,成对出现:给了地址没给密钥返回
invalid_body。
curl -X POST "$CONSOLE_ORIGIN/api/v1/repos" \
-H "Authorization: Bearer $CONSOLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"repo": "vercel/next.js",
"callbackUrl": "https://example.com/hooks/radar",
"callbackSecret": "your-shared-secret"
}'| 字段 | 类型 | 说明 |
|---|---|---|
callbackUrl | 字符串 | 收 POST 的地址,必须是 http(s) |
callbackSecret | 字符串 | 至少 16 字符,用于算签名 |
落库完成之后服务端同步发一次回调,等它返回或超时(30 秒)才响应你的请求。 同步是故意的:异步发出去的话,回调地址写错这件事永远不会被发现。
请求体:
{
"eventId": "repo.registered.V1StGXR8Z5jd",
"event": "repo.registered",
"occurredAt": "2026-03-24T08:12:03.000Z",
"fullName": "vercel/next.js",
"repoId": "V1StGXR8Z5jd",
"created": true
}event 只有两个取值:登记是 repo.registered,发布是 repo.published(多带一个
projectId)。eventId 由事件名加 repoId 派生,不是随机值——同一个写请求
因为超时被重发时它不变,接收方按它去重就能认出这是同一件事,而不是两次独立的
登记。
签名头与订阅投递完全一致,用同一段代码算:
| 头 | 内容 |
|---|---|
X-Webhook-Id | 同 body 里的 eventId,与订阅投递的 id 同一套语义 |
X-Webhook-Timestamp | Unix 秒 |
X-Webhook-Signature | sha256=<hex>,签名对象是 <timestamp>.<原始请求体> |
X-Webhook-Event | 事件名 |
验签要按原始字节算:先解析 JSON 再重新序列化,字段顺序或空格变了,签名就对不上。 收到的时间戳与当前时间相差超过 300 秒就丢掉——那是重放。
回调失败不影响这次调用的结果
数据在回调之前就已经落库了,所以投递失败只进日志,不会把一次成功的写入报成失败。 你的重试只会登记同一个仓库第二次。回调是「可以开始读了」的信号,不是这次写入的 交付凭证——要确认写入结果,看响应体。
POST /api/v1/repos — 登记一个仓库
让它开始跟踪,不会发布任何东西。
这个端点不接受 type 字段
带 type 的请求会返回 400 type_not_accepted,而不是被忽略——静默忽略会让
调用方以为「我发布成功了」。要传 type 就调 POST /api/v1/projects。
响应
| 状态码 | 什么时候 |
|---|---|
201 | 新登记了一个仓库 |
200 | 这个仓库早就在跟踪了(幂等) |
{
"ok": true,
"repo": { "id": "V1StGXR8Z5jd", "full_name": "vercel/next.js", "stars": 132000 },
"created": true,
"projectCount": 0
}created 区分「新建」与「刷新」;projectCount 是已经有多少个已发布的项目
指向这个仓库——登记端点自己不建 project,所以它非零只可能因为这个仓库先前被
策展过,这时你传的 type 会被拒绝而不是被采纳,看到这个数就知道了。
失败时是统一的错误形状(见调用、限流与错误):invalid_body
(附 detail 指出第一个不合法的字段)、invalid_url、type_not_accepted、
insufficient_scope、rate_limited、github_unavailable。
POST /api/v1/projects — 发布一个项目
把一个仓库策展成 project,也就是放到公开站上。地址同样有两种写法,另加可选的
type(缺省 application)与那对回调参数。
curl -X POST "$CONSOLE_ORIGIN/api/v1/projects" \
-H "Authorization: Bearer $CONSOLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"repo":"vercel/next.js","type":"application"}'请求体:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
url / repo | 字符串(二选一) | — | 完整 URL,或裸的 owner/repo |
type | 枚举(可选) | application | client / server / application / skill / persona |
callbackUrl / callbackSecret | 字符串(可选) | — | 与登记端点相同,成对出现 |
响应
| 状态码 | status | 什么时候 |
|---|---|---|
201 | "created" | 新建了一个项目 |
200 | "existing" | 这个仓库早就发布过(幂等) |
{
"ok": true,
"status": "created",
"repo": { "full_name": "vercel/next.js" },
"project": { "...": "整行 project 数据" },
"readme": { "...": "解析后的 README" },
"skills": { "count": 0, "translated": 0, "empty": false },
"authorLinked": true,
"delivered": null
}project、readme、skills是这次写进去的东西;skills只有type: "skill"时才有意义。delivered只对skill有意义:true表示技能已经推到下游站点、那边现在就能用;false表示有技能没推成功(行已经进重试队列);null表示这个部署没配下游, 不是失败。- 已经发布过的仓库再调一次不会建第二行,返回
existing。此时若type是skill, 技能同步会重新跑一遍——调用方要的是「现在就能拿到技能」,不能只答一个existing就把人丢去等调度器。
失败:invalid_body、invalid_url、insufficient_scope(这把 key 没有
projects:write)、create_failed(502,可以重试)、github_unavailable。
写入之后会发生什么
仓库被雷达跟踪,下一轮周期任务开始采集它的 stargazer 到达时间;之后的 读取 API里才会出现它的档案与统计。榜单上出现还要更久—— 统计要先攒够一期。
发布的项目进入公开站的项目列表;分类(type)决定它落在哪个栏目。
每次调用都记一条审计:谁的 key、什么时间、改了什么。管理员可以在
/dashboard/api-keys 看到。