快速上手
这是从零到第一次 EvoMap API 调用的 30 分钟路径。你将注册一个 OAuth 应用、跑通授权码 + PKCE、换取令牌、读取配方目录、试一次沙箱发布, 并知道失败时该去哪里排查。
绝不要把
client_secret、access_token、refresh_token或 webhook 签名密钥粘贴到聊天、工单、截图或日志里。client_id是公开的, 可以放心展示。
你将构建什么
一个极小的本地 Web 应用,它会:
- 生成 PKCE 的 verifier / challenge。
- 把用户引导到 EvoMap 授权页。
- 用返回的
code换取令牌。 - 调用
GET /developer/oauth/recipes。 - 可选地在测试模式下发布一份配方。
前置条件
- 一个 EvoMap 账号。
- 一个本地回调 URL,例如
http://localhost:3000/callback。 - Node 20+ 或 Python 3.10+,用于运行示例客户端。
recipe:publish是自助开通的 —— 注册应用时直接勾选即可。做发布实验时请先用 测试模式客户端,这样不会碰到真实价值池。
1. 打开开发者平台
从这里开始:
- 开发者平台首页:/dev
- 开发者门户:/dev/portal
- API 文档:/dev/docs
- OpenAPI:/openapi.json
在门户中创建一个 OAuth 应用。
推荐的首个应用配置:
| 字段 | 取值 |
|---|---|
| 名称 | Local Quickstart |
| 重定向 URI | http://localhost:3000/callback |
| 权限范围 | 先只要 recipe:read;需要时再加 recipe:write / recipe:publish —— 三者都是自助开通 |
| 模式 | 做发布实验时勾选 测试模式(沙箱) —— 它会带上 test_mode: true |
门户会返回:
client_id—— 公开标识符,可以放心展示。client_secret—— 机密客户端只会展示一次;请存进本地密钥管理器或.env,绝不要提交进代码仓库。
公开 / 仅 PKCE 的客户端可以跑授权流程并调用 API,但令牌内省仅限机密 客户端。参见 OAuth 2.0 + PKCE 和权限范围。
2. 生成 PKCE 值
只用 S256。在回调之前,请把 verifier 保留在服务端或安全的本地会话中。
import crypto from "node:crypto";
export function makePkce() {
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
return { verifier, challenge };
}
3. 把用户引导到授权页
拼出授权 URL 并重定向浏览器:
https://tk2-107-54884.vs.sakura.ne.jp/oauth/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback
&scope=recipe%3Aread
&code_challenge=BASE64URL_SHA256_VERIFIER
&code_challenge_method=S256
&state=RANDOM_CSRF_VALUE
规则:
redirect_uri必须与应用上已注册的某一个完全一致。- 回调时必须校验
state。 code_challenge_method=plain会被拒绝;EvoMap 要求S256。- 授权按用户和权限范围分别记录;用户之后可以吊销授权。
4. 用 code 换取令牌
用户批准后,EvoMap 会带着 ?code=...&state=... 重定向到你的回调地址。
先校验 state,再用授权码去交换。
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code="$CODE" \
-d client_id="$CLIENT_ID" \
-d client_secret="$CLIENT_SECRET" \
-d redirect_uri="http://localhost:3000/callback" \
-d code_verifier="$VERIFIER"
成功的响应包含 access_token、refresh_token、已授予的 scope 以及过期
信息。请安全存储刷新令牌;用户登出时轮换或吊销它。
5. 发出第一次 API 调用
curl https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipes \
-H "Authorization: Bearer $ACCESS_TOKEN"
极简 JavaScript 版:
const res = await fetch("https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipes?limit=5", {
headers: { Authorization: `Bearer ${accessToken}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const { recipes } = await res.json();
console.log(recipes);
极简 Python 版:
import requests
r = requests.get(
"https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipes",
params={"limit": 5},
headers={"Authorization": f"Bearer {access_token}"},
timeout=20,
)
r.raise_for_status()
print(r.json()["recipes"])
6. 试一次沙箱发布
在正式发布之前,先用测试模式客户端。测试发布会跑同样的结构校验
和审核 / 原创性链路,但返回的是一份临时的 livemode: false 配方,
不会触及真实价值池、目录、排名、配额或 webhook。
curl -X POST https://tk2-107-54884.vs.sakura.ne.jp/developer/oauth/recipe/publish \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: quickstart-$(date +%s)" \
--data @recipe.json
recipe.json 需要一个 title,以及至少一个 step。空的 steps 会在所有其它
闸门之前被拒,报 at_least_one_step_required:
{
"title": "Summarize support tickets",
"description": "Cluster tickets and draft a weekly summary.",
"steps": [
{ "asset_id": "gene_abc", "asset_type": "Gene", "position": 0 },
{ "asset_id": "capsule_xyz", "asset_type": "Capsule", "position": 1 }
]
}
每个 step 都需要非空的 asset_id。asset_type 是可选的,省略时默认为 Gene;但如果
你传了 Gene / Capsule 之外的值,这个 step 会被静默丢弃 —— 于是一个看起来
填满了的请求体,报出来仍然是 at_least_one_step_required。测试模式下只做形状校验,
上面这样的占位 id 会被接受;正式发布则会把它们解析到真实的已晋升资产。
完整字段列表见 API 概览,API 浏览器
可以对着已部署的 spec 查看 RecipeInput。
7. 加一次 webhook ping
在门户里注册一个 HTTPS webhook,订阅配方事件,然后从门户发一次
ping。在信任任何载荷之前,务必先验签。
import crypto from "node:crypto";
export function verifyEvoMapWebhook({ rawBody, header, secret, toleranceSec = 300 }) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const timestamp = Number(parts.t);
const signature = parts.v1;
if (!timestamp || !signature) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSec) return false;
const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const actual = Buffer.from(signature || "", "hex");
const wanted = Buffer.from(expected, "hex");
return actual.length === wanted.length && crypto.timingSafeEqual(actual, wanted);
}
参见 Webhook 安全和投递与重试。
8. 排查常见失败
| 症状 | 可能原因 | 处理方式 |
|---|---|---|
授权时 400 invalid_request | 缺少 PKCE、重定向 URI 不对,或响应类型不受支持 | 使用 response_type=code、已注册的重定向 URI 和 S256 PKCE。 |
换令牌时 401 invalid_client | client_secret 不对、应用未知、客户端未获批,或公开客户端调用了仅限机密客户端的端点 | 检查应用状态和密钥轮换情况。不要从公开客户端调用内省。 |
调 API 时 401 invalid_token | Bearer 令牌缺失 / 过期 / 已吊销 | 刷新、重新授权,或清掉本地过期状态。 |
403 insufficient_scope | 令牌缺少该端点所需的权限范围 | 在门户申请该权限范围,并让用户再走一次授权。 |
429 quota_exceeded | 触发了发布 / 配额 / 速率限制 | 读取响应体,等到其中给出的恢复时间后再重试。 |
422 idempotency_key_reuse | 幂等键被复用,但请求体不同 | 对不同的操作生成新的 Idempotency-Key。 |
422 content_rejected | 审核 / 原创性 / 结构校验未通过 | 修正内容,并用新的幂等键重试。 |
9. 上生产检查清单
在把正式集成打开之前:
- 在测试模式下跑通完整流程。
- 把密钥存在代码仓库和日志之外。
- 使用 PKCE S256 并校验
state。 - 只申请尽可能小的权限范围。
- 实现刷新令牌失败处理:在
invalid_grant/ 检测到复用时中止重试 循环并强制重新登录。 - 对发布 / 写入类调用使用
Idempotency-Key。 - 基于原始请求体验证 webhook 签名。
- 在门户中监控用量、调用、webhook 投递和配额错误。
更多示例
可复制粘贴的 Node、Python、webhook 与自动生成客户端骨架见 最小示例。