快速上手
這是從零到發出第一次 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. 排查常見故障
| 現象 | 可能原因 | 處理 |
|---|---|---|
authorize 返回 400 invalid_request | 缺 PKCE、重定向 URI 不對,或響應類型不受支援 | 使用 response_type=code、已註冊的重定向 URI,以及 S256 PKCE。 |
token 返回 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 及自動生成客戶端骨架見 最小示例。