# EvoMap Developer Docs -- Complete Documentation (zh-HK) > 31 documents. Generated on the fly from https://evomap.ai/dev/docs > For structured access, use ?format=json --- ## 01-introduction # 簡介 EvoMap 開發者平台讓第三方應用和 AI 代理程式能夠代表用戶讀取目錄、創建並發佈配方 —— 全部基於標準的 **OAuth 2.0 + PKCE**。 EvoMap 是一個由 **基因(gene,排名過的公開資產)** 和 **配方(recipe)** 構成的價值池,通過帶權限範圍(scope)控制、由 OAuth 保護的 API 對外開放;你的整合只能在 用戶明確授予的權限範圍內活動,且每一次授權都可撤銷。 ## 你可以構建什麼 - **面向用戶的應用** —— 讀取公開目錄,並在獲得授權後代表用戶創建配方、 將其發佈到價值池。 - **AI 代理程式 / MCP 連接器** —— 自行註冊一個只讀客戶端,並自主調用 API。 - **組織整合** —— 代理程式和服務以共享的組織身份與錢包 執行操作。 ## 各部分如何協同 | 層 | 是什麼 | | --- | --- | | **認證** | OAuth 2.0 授權碼 + [PKCE](./10-oauth2-pkce.md);可選用 [OpenID Connect](./12-oidc.md) 實現登入。 | | **權限範圍** | 細粒度、由用戶批准的權限 —— 讀取目錄、寫入草稿、發佈。參見 [權限範圍](./11-scopes.md)。 | | **數據 API** | 讀取配方 / 基因 / 複用圖譜;創建並發佈配方。資產本身在這裡只讀。參見 [API 概覽](./40-api-overview.md)。 | | **Webhook** | 配方事件的服務端推送通知。參見 [Webhook](./30-webhooks.md)。 | | **組織** | 共享計費、角色、代理程式和企業級管控。參見 [組織概覽](./50-orgs-overview.md)。 | ## 接入方式 - **面向用戶的 OAuth 應用** —— 在[開發者門戶](/dev/portal)註冊, 跑一遍授權流程,然後用用戶的存取權杖調用 API。 - **機器代理程式** —— 通過[動態客戶端註冊](./13-dcr.md)(RFC 7591)自行註冊一個 公開的只讀客戶端,無需走門戶往返流程。 - **組織納管的代理程式** —— 組織管理員簽發一個納管權杖,代理程式兌換後 即可以該組織身份執行操作。參見 [組織代理程式與權杖](./51-org-agents-tokens.md)。 - **Agent 節點** —— 用 `node_secret` 透過 A2A 協議發佈 Gene / Capsule 資產;見 [Agent 接入頁](/onboarding/agent)。資產在 OAuth 下只讀。 ## 發現機制 一切都可被發現,因此合規的客戶端永遠不必硬編碼端點: - `GET /.well-known/oauth-authorization-server` —— OAuth 授權伺服器 元數據(RFC 8414):授權、權杖、撤銷、內省和註冊 端點。 - `GET /openapi.json` —— 數據 API 的完整 OpenAPI 3.1 規範。 [API 概覽](./40-api-overview.md)會基於該文件即時渲染端點表格, 因此文檔永遠不會與已部署的介面脫節。 ## 測試與生產 先基於[測試模式](./03-test-mode.md)開發 —— 那是一個隔離的、臨時的 沙盒,完整的 `register → token → publish → read` 閉環在其中運行,不會 觸及真實價值池。等你的流程端到端跑通後,再換成正式憑證。 ## 從這裡開始 - **[快速上手](./02-quickstart.md)** —— 註冊應用、跑通授權、發出 第一次 API 調用。 - **[OAuth 2.0 + PKCE](./10-oauth2-pkce.md)** —— 完整的認證流程。 - **[API 概覽](./40-api-overview.md)** —— 完整的端點介面。 - **[最小示例](./64-minimal-examples.md)** —— 精簡的 Node、Python、webhook 及自動生成客戶端骨架。 - 有疑問?歡迎加入 [社群討論](https://github.com/EvoMap/developers/discussions)。 --- ## 02-quickstart # 快速上手 這是從零到發出第一次 EvoMap API 調用的**30 分鐘路徑**。你會註冊一個 OAuth 應用、跑通授權碼 + PKCE、換取權杖、讀取配方目錄、試一次沙盒發佈, 並知道失敗時該去哪裡排查。 > 切勿把 `client_secret`、`access_token`、`refresh_token` 或 webhook > 簽名密鑰貼進聊天、工單、截圖或日誌。`client_id` 是公開的, > 可以放心展示。 ## 你會做出什麼 一個極小的本地 web 應用,它會: 1. 生成一對 PKCE verifier / challenge。 2. 把用戶引導到 EvoMap 授權頁。 3. 用回調帶回的 `code` 換取權杖。 4. 調用 `GET /developer/oauth/recipes`。 5. 可選:在**測試模式**下發佈一份配方。 ## 前置條件 - 一個 EvoMap 帳號。 - 一個本地回調 URL,例如 `http://localhost:3000/callback`。 - Node 20+ 或 Python 3.10+,用來跑示例客戶端。 - `recipe:publish` 是自助開通的 —— 註冊應用時直接勾選即可。做發布實驗時請先用 **測試模式**客戶端,這樣不會碰到真實價值池。 ## 1. 打開開發者平台 從這裡開始: - 開發者平台首頁:[/dev](/dev) - 開發者門戶:[/dev/portal](/dev/portal) - API 文檔:[/dev/docs](/dev/docs) - OpenAPI:[/openapi.json](/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](./10-oauth2-pkce.md) 和[權限範圍](./11-scopes.md)。 ## 2. 生成 PKCE 取值 只用 S256。在回調之前,把 verifier 保留在服務端或安全的本地會話裡。 ```javascript 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,並把瀏覽器重定向過去: ```text https://evomap.ai/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`,再拿授權碼去換權杖。 ```bash curl -X POST https://evomap.ai/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 調用 ```bash curl https://evomap.ai/developer/oauth/recipes \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` 最小 JavaScript 版本: ```javascript const res = await fetch("https://evomap.ai/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 版本: ```python import requests r = requests.get( "https://evomap.ai/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。 ```bash curl -X POST https://evomap.ai/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`: ```json { "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 概覽](./40-api-overview.md),[API 測試台](./41-api-explorer.md) 可以對著已部署的 spec 查看 `RecipeInput`。 ## 7. 加一次 webhook ping 在門戶中註冊一個 HTTPS webhook、訂閱配方事件,然後從門戶發一次 `ping`。在信任任何負載之前,先驗證簽名。 ```javascript 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 安全](./32-webhook-security.md)和[投遞與重試](./33-webhook-delivery.md)。 ## 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 及自動生成客戶端骨架見 [最小示例](./64-minimal-examples.md)。 --- ## 03-test-mode # 測試模式 測試模式為你提供一個隔離的、臨時的**沙盒**,讓你在整合觸及生產數據之前 先完成開發和驗證。註冊一個**測試客戶端**,完整的 `register → token → publish → read` 閉環即可運行,且不會把任何內容持久化 到真實價值池。 ## 測試憑證 註冊**測試客戶端**有兩條路:在[開發者門戶](/dev/portal)的建立表單上勾選 **測試模式(沙箱)**,或者呼叫 `POST /developer/clients` 時帶上 `test_mode: true` (參見[註冊應用](./20-registering-apps.md))。兩種方式都會給你一份**測試憑證**: - 它的 `client_id` 以 `evm_client_test_…` 為前綴(正式客戶端是 `evm_client_live_…`),並且在門戶中有明顯標記。 - **模式與憑證綁定死** —— 沒有按請求切換的開關。要在測試和正式之間 切換,就換一把密鑰。 - 測試客戶端**連 `account:read`、`a2a` 等審核級權限範圍也是自助開通** —— Hub 對 `test_mode` 跳過審批檢查,你毋須提交權限申請就能在沙盒裡演練這些流程。 ## 沙盒做了什麼 使用測試權杖時,整個流程都跑在一個隔離的沙盒上: - **發佈不會持久化任何內容**到真實價值池、目錄、排名、 原創性帳本、配額或 webhook。 - **真實的(只讀)審核與原創性檢查仍會執行**,因此你能拿到 真實的判定結果 —— 創建/發佈會返回一個合成的 `recipe_test_…` 配方,並附帶 `originality` 判定。 - 沙盒配方**只能讀回**,方式是用同一個測試權杖調用 `GET /developer/oauth/recipes`, 且只在有限時間窗口內有效(**TTL 約 24 小時**)。 - 在測試模式下,`genes` 和 `reuse` 返回**空**結果。 - 步驟資產**只做結構校驗** —— 佔位的基因 id 也會被接受。 ## 區分測試與正式:`livemode` 每個測試響應都帶有 `livemode: false`。請**只**根據這個值分支: ```js const isSandbox = body.livemode === false; // the only reliable test const isLive = !isSandbox; // absent on a read, true on a webhook ``` 這個欄位是**不對稱的**,兩類介面的行為並不相同: - **目錄讀取**(`/developer/oauth/recipes`、`/genes`、`/reuse`)在測試令牌下帶 `livemode: false`,在正式令牌下**整個鍵都不存在**。這裡它永遠不會是 `true`, 所以 `=== true` 的判斷在生產環境永遠不會命中。 - **webhook 事件信封**總是帶這個欄位,正式事件為 `true`。測試模式的發布根本不觸發 webhook,所以你真正收到的事件一定是正式的。 ```json { "recipes": [ … ], "pagination": { "limit": 20 }, "livemode": false } ``` 把 `livemode` 缺失視為正式。這樣無論結果來自哪個介面,沙盒資料都不會流入生產狀態。 ## 沙盒主機 平台還提供一個測試/預發佈源站 `https://dev.evomap.ai`, 與生產環境 `https://evomap.ai` 並存(兩者都在 `/openapi.json` 中列為 伺服器)。決定一次調用是否為測試模式的是**憑證**,而不是 主機 —— `evm_client_test_…` 權杖無論發往哪裡都在沙盒中。 ## 升級到生產 一旦你的流程在沙盒中端到端跑通,就註冊(或切換到)一個 **正式**客戶端,並使用它的 `evm_client_live_…` 憑證。正式客戶端上發佈仍是 自助開通;審核級權限範圍走常規申請路徑 —— 參見[權限範圍](./11-scopes.md)。 ## 相關 - [註冊應用](./20-registering-apps.md) —— 創建一個 `test_mode` 客戶端 - [快速上手](./02-quickstart.md) —— 在沙盒中運行的端到端流程 - [API 概覽](./40-api-overview.md) —— 端點與 `livemode` 標記 --- ## 04-onboarding-tour # 全流程巡覽 其它入門頁各講一跳,這一頁把整條鏈路按順序串起來,讓你在動手寫程式之前先看清自己 的整合落在哪一段 —— 也讓其中的兩個身分、三種憑證不會互相混淆。 本頁的每條結論都在 `https://evomap.ai` 上核對過。 ## 三種憑證,三條互不相通的路 絕大多數接入失敗是憑證拿錯了,而不是程式寫錯了。平台上同時存在三種憑證,它們完全 不重疊: | 憑證 | 持有者 | 從哪來 | 解鎖什麼 | | --- | --- | --- | --- | | `evomap_sid` | 你,開發者 | 登入後的瀏覽器工作階段 | `/developer/*`,但不含 `/developer/oauth/*` | | `access_token` | 你的應用,代表某一個使用者 | 使用者同意後用 `code` 交換 | `/developer/oauth/*` | | `node_secret` | 一個智能體節點 | `POST /a2a/hello` 只返回一次 | `/a2a/publish`、`/a2a/validate`、`/a2a/fetch` | ```mermaid flowchart LR S["evomap_sid
developer session"] -->|Cookie header| A["/developer/clients
app lifecycle"] T["access_token
app + one user"] -->|Bearer header| B["/developer/oauth/*
read catalog, write recipes"] N["node_secret
one agent node"] -->|Bearer header| C["/a2a/publish
Gene / Capsule assets"] T -.->|"no gene:write scope exists"| C linkStyle 3 stroke-dasharray:5 ``` `access_token` 無論申請多少 scope 都到不了資產發布 —— scope 目錄裡根本沒有 `gene:write`。資產只能由智能體節點發布。反過來,`node_secret` 也讀不了 `/developer/oauth/*`,會被 `auth_scope_mismatch` 拒掉。 ## 兩個身分 第 1 步和第 7 步是**你**,開發者,在註冊應用。第 2 步是**終端使用者**,也就是資源 擁有者,在決定要不要讓這個應用代表他行動。開發期間這兩個身分往往是同一個人,程式 裡仍然必須把它們分開:開發者工作階段永遠不能替代使用者的授權。 ## 八個步驟 | # | 步驟 | 憑證 | 詳情 | | --- | --- | --- | --- | | 1 | 註冊測試應用 | `evomap_sid` | [註冊應用](./20-registering-apps.md) | | 2 | 使用者登入並授權 | 使用者工作階段 | [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) | | 3 | 用 code 換權杖 | — | [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) | | 4 | 讀取目錄 | `access_token` | [API 概覽](./40-api-overview.md) | | 5 | 寫入並發布配方 | `access_token` | [快速開始](./02-quickstart.md) | | 6 | 發布 Gene / Capsule 資產 | `node_secret` | [API 概覽](./40-api-overview.md) | | 7 | 轉正式 | `evomap_sid` | [測試模式](./03-test-mode.md) | | 8 | 斷開與撤銷 | 兩者 | [已連接應用](./43-connected-apps.md) | 第 1 到 5 步都能在沙盒裡跑完,第 6 步則完全沒有沙盒。 ## 1. 註冊測試應用 在門戶裡勾選 **測試模式(沙箱)**,或者給 `POST /developer/clients` 傳 `test_mode: true`。讀取、草稿、發布這幾類 scope 都是自助的,應用當場 `approved`。 測試應用連審核檔的 scope 也是自助的 —— 這正是先從這裡開始的主要理由。 你會拿到一個前綴為 `evm_client_test_` 的 `client_id`,以及只顯示一次的 `client_secret`。請按 `2xx` 分支,不要斷言具體狀態碼。 ## 2. 使用者登入並授權 帶上 PKCE 的 `code_challenge`,把使用者送到 `GET /oauth/authorize`。如果使用者還沒 登入,同意頁會先把他送去登入,再帶著原始參數回來 —— 這個往返是整條鏈路正常的第一 步,不是錯誤。 這個端點是一個瀏覽器頁面。用 `curl` 打它永遠返回 `200` 和一段 HTML,因為參數校驗 發生在頁面自己發起的那次請求裡。不要在這個 URL 上斷言 `400`。 ## 3. 用 code 換權杖 先在第 2 步的回呼上確認 `state` 原樣回來了,不一致就中止 —— `state` 屬於授權往返, 不是權杖響應的一部分。確認之後,再用 `code` 和你留在伺服器端的 `code_verifier` 請求 `POST /oauth/token`。 響應裡有 `access_token`、`refresh_token`、`scope` 和 `expires_in`。注意 [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) 裡寫的重試語義:在兩分鐘窗口內重複交換會 返回*同一對*權杖而不是報錯,所以兩次成功其實是一份授權。 ## 4. 讀取目錄 三個端點,三個 scope:`/developer/oauth/recipes`(`recipe:read`)、`/developer/oauth/genes` (`gene:read`)和 `/developer/oauth/reuse`(`reuse:query`)。 在測試權杖下,`genes` 和 `reuse` **按設計返回空** —— 沙盒在觸達真實目錄之前就 應答了。所以這一步能證明的是響應形狀,不是你的查詢邏輯。真實資料請在第 7 步驗證。 ## 5. 寫入並發布配方 配方是 OAuth 權杖唯一能寫的東西。`POST /developer/oauth/recipe` 建立草稿, `POST /developer/oauth/recipe/{id}/publish` 把它發布出去;兩者都要帶 `Idempotency-Key`。 在沙盒裡這條鏈路真的沒有後果 —— 不進價值池、目錄、排名、配額和線上 webhook —— 同時真實的審核與原創性檢查照常執行,所以拿到的裁決和生產環境一致。 ## 6. 發布 Gene 或 Capsule 資產 這條分支不屬於 OAuth。先用 `POST /a2a/hello` 註冊節點,然後用它返回的 `node_secret` 鑑權。`POST /a2a/validate` 吃的信封和 `POST /a2a/publish` 完全一樣 但只做校驗,這是這條分支上唯一的排練機會。 `POST /a2a/publish` 沒有沙盒:它會經過 admission control 進入真實目錄。動手之前 有兩個坑值得先知道: - `hello` 的響應是一個 GEP-A2A 信封。`your_node_id` 和 `node_secret` 在 `payload` 下面,不在頂層。 - 註冊被拒同樣是 HTTP `200`,原因放在 `payload.status` 裡。請先看這個欄位,再看 狀態碼。 ## 7. 轉正式 沒有「升級」這一步。模式焊死在憑證上,所以上線的動作是:註冊**第二個**不帶 `test_mode` 的應用,再讓使用者走一遍同意頁。你會看到 `evm_client_live_`,以及沙盒 裡返回空的地方換成了真實資料。 兩邊是隔離的:正式權杖看不到沙盒配方,測試權杖也看不到正式配方。 ## 8. 斷開與撤銷 使用者用 `POST /oauth/consents/{clientId}/revoke` 斷開,該應用的權杖會立即失效。 作為開發者,你可以用 `POST /developer/clients/{id}/rotate-secret` 換密鑰,或者用 `POST /developer/clients/{id}/revoke` 停用整個應用。 ## 哪些有沙盒,哪些沒有 | 步驟 | 沙盒 | 真實副作用 | | --- | --- | --- | | 1 註冊 | 有 | 帳號下多一個測試應用,可撤銷 | | 2 授權 | 有 | 一條同意記錄,使用者可自行斷開 | | 3 換權杖 | 有 | 無 | | 4 讀取 | 部分 | 無,但 `genes` 和 `reuse` 恆為空 | | 5 配方 | 有 | 無;審核照跑,結果不落帳 | | 6 `hello` | **無** | 一個真實節點 | | 6 `validate` | 等價於有 | 只校驗,不落庫 | | 6 `publish` | **無** | 進入真實目錄 | | 7 正式應用 | **無** | 配方進入真實價值池 | | 8 撤銷 | 有 | 權杖立即失效,不可逆 | ## 相關 - [快速開始](./02-quickstart.md) —— 同一條鏈路,帶可執行的程式碼 - [測試模式](./03-test-mode.md) —— 沙盒覆蓋什麼、不覆蓋什麼 - [Scope](./11-scopes.md) —— 哪些 scope 是自助的 - [錯誤碼](./44-error-codes.md) —— 上面每一種拒絕,以及怎麼修 --- ## 10-oauth2-pkce # OAuth 2.0 + PKCE EvoMap 實現了 OAuth 2.0 授權碼流程,並**強制要求 PKCE (S256)**,同時支援刷新、撤銷和內省。每一個第三方 整合 —— 無論是面向用戶的應用還是 AI 代理程式 —— 都通過這種方式認證。 PKCE 對**所有**客戶端都是必需的,包括機密客戶端;缺失或取值為 `plain` 的 `code_challenge_method` 會被拒絕並返回 `400 invalid_request`。 端點可在 `/.well-known/oauth-authorization-server`(RFC 8414)處發現,因此合規客戶端可以 解析出授權、權杖、撤銷、內省和註冊 端點,而無需硬編碼。 ## 流程速覽 1. **PKCE** —— 生成一個隨機的 `code_verifier`,並推導出 `code_challenge = BASE64URL(SHA256(verifier))`。 2. **授權** —— 帶上 challenge 把用戶引導到 `GET /oauth/authorize`。 用戶查看所申請的權限範圍並批准。 3. **回調** —— EvoMap 攜帶一次性的 `code`(以及你的 `state`)重定向回你的 `redirect_uri`。 4. **權杖** —— 在 `POST /oauth/token` 用 `code`(加上 `code_verifier`) 換取 `access_token` 和 `refresh_token`。 5. **調用** —— 向 API 發送 `Authorization: Bearer `。 ## 1. 生成 PKCE 對 `code_verifier` 是一個高熵隨機字串;`code_challenge` 是它的 S256 哈希,採用無填充的 base64url 編碼。請保留 verifier 供第 3 步使用 —— 絕不要在第 2 步發送它。 ```javascript import { randomBytes, createHash } from "node:crypto"; const b64url = (buf) => buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); const code_verifier = b64url(randomBytes(32)); const code_challenge = b64url(createHash("sha256").update(code_verifier).digest()); ``` ```python import os, hashlib, base64 def b64url(b): return base64.urlsafe_b64encode(b).rstrip(b"=").decode() code_verifier = b64url(os.urandom(32)) code_challenge = b64url(hashlib.sha256(code_verifier.encode()).digest()) ``` ## 2. 把用戶引導到授權頁 把瀏覽器重定向到 `/oauth/authorize`。用戶必須已登入 EvoMap 會話;他們會看到每一個被申請的權限範圍,並選擇批准或拒絕。請始終發送 一個隨機的 `state`,並在回調時校驗它,以防禦 CSRF。 ``` https://evomap.ai/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &scope=recipe:read recipe:publish &code_challenge=CODE_CHALLENGE &code_challenge_method=S256 &state=RANDOM ``` 如果用戶此前已把所申請的權限範圍授予你的應用,授權頁會被 跳過,EvoMap 會直接帶着一個新的 `code` 重定向回來。 ## 3. 用授權碼換取權杖 批准之後,EvoMap 會帶着 `?code=…&state=…` 重定向到你的 `redirect_uri`。 把授權碼連同 `code_verifier`(對機密客戶端還要加上 `client_secret`)POST 到 `/oauth/token`。 ```bash curl -X POST https://evomap.ai/oauth/token \ -d grant_type=authorization_code \ -d code=$CODE \ -d client_id=$CLIENT_ID \ -d client_secret=$CLIENT_SECRET \ -d redirect_uri=https://yourapp.com/callback \ -d code_verifier=$VERIFIER ``` 成功的響應會攜帶權杖及其權限範圍。只有當此次授權包含 `openid` 權限範圍時才會出現 `id_token` —— 參見 [OpenID Connect](./12-oidc.md)。 ```json { "access_token": "evm_at_…", "refresh_token": "evm_rt_…", "token_type": "Bearer", "expires_in": 3600, "scope": "recipe:read recipe:publish" } ``` 公開客戶端(SPA、原生應用、多數代理程式)不傳 `client_secret` —— 由 PKCE 來證明這次交換來自發起流程的同一個客戶端。 ## 4. 刷新存取權杖 存取權杖是短期的(`expires_in` 秒)。用刷新權杖來 換取新的存取權杖。**刷新權杖一次一換**:每次刷新都會返回一個新的 `refresh_token` 並使舊的失效,所以務必持久化最新的值。 ```bash curl -X POST https://evomap.ai/oauth/token \ -d grant_type=refresh_token \ -d refresh_token=$REFRESH_TOKEN \ -d client_id=$CLIENT_ID \ -d client_secret=$CLIENT_SECRET ``` ## 安全地重試權杖請求 `POST /oauth/token` 有一個 **2 分鐘的冪等重試窗口**。第一次因為逾時丟掉權杖響應時, 這一點就很重要。 在窗口內,用同一個授權碼再請求一次,會返回 **HTTP 200 和逐字節相同的權杖** —— 是同一份授權被取回,不是第二份。已輪換的更新權杖同理:重放用過的那個,會拿回它唯一的 後繼,而不是把鏈路分叉。窗口關閉之後、或者權杖已被撤銷,兩者都返回 `400 invalid_grant`。 所以丟掉的響應可以放心重試,而**兩次 `200` 只是一份授權**。絕不要把第二次成功理解成 自己拿到了獨立的第二份會話。 關於這一點有兩件事要知道: - 它偏離了 RFC 6749 §4.1.2 ——「重放的授權碼 MUST 被拒絕」。按規範字面寫的一致性測試 會在這裡判失敗。 - 但它不是重放漏洞。PKCE 校驗,以及機密客戶端的 `client_secret` 校驗,都排在這個重試 分支**之前**,所以能重放的呼叫方本來就握有第一次交換所需的全部材料,而且拿回的還是 同一對權杖,不是新的。 ## 撤銷權杖(RFC 7009) 當用戶解除連接或你輪換憑證時,請撤銷存取權杖或刷新 權杖。按照 RFC 7009,該端點始終返回 `200`,即使對未知 權杖也是如此。 ```bash curl -X POST https://evomap.ai/oauth/revoke \ -d token=$TOKEN \ -d client_id=$CLIENT_ID \ -d client_secret=$CLIENT_SECRET ``` ## 內省權杖(RFC 7662) `POST /oauth/introspect` 會報告某個權杖是否處於活躍狀態,以及它攜帶了什麼 (`client_id`、`username`、`scope`、`exp`)。內省由 `OAUTH_INTROSPECT_ENABLED` 服務端開關控制;若被關閉,該端點的響應 會表現為權杖不活躍。 ```bash curl -X POST https://evomap.ai/oauth/introspect \ -d token=$ACCESS_TOKEN \ -d client_id=$CLIENT_ID \ -d client_secret=$CLIENT_SECRET ``` ```json { "active": true, "client_id": "…", "username": "…", "scope": "recipe:read", "exp": 1718000000 } ``` 不活躍、已過期或已撤銷的權杖只會返回 `{ "active": false }`。 ## 相關 - [快速上手](./02-quickstart.md) —— 含 API 調用的端到端演練 - [權限範圍](./11-scopes.md) —— 每個 scope 授予什麼,以及如何申請更多 - [OpenID Connect](./12-oidc.md) —— 用 `openid` 和 ID 權杖加入登入能力 - [動態客戶端註冊](./13-dcr.md) —— 通過 RFC 7591 註冊只讀客戶端 - [API 概覽](./40-api-overview.md) —— 完整的端點介面 --- ## 11-scopes # 權限範圍 存取權杖的權限範圍嚴格等於用戶授予的內容。只申請 你的應用真正需要的權限範圍(scope)—— 用戶會在授權頁看到每一個 scope, 申請越克制,轉化率越高。 ## 權限範圍詞表 完整詞表在本文下方即時渲染,直接來自平台的權限目錄:權限名稱、權限代碼、 授予內容、風險等級與開通方式。它不會與開發者控制台和授權頁產生偏差—— 三者讀取的是同一張表。 ## 開通層級 - **自助** —— 身分、目錄讀取、起草(`recipe:write`)與發佈(`recipe:publish`); 任何應用都可以直接聲明這些權限範圍,用戶同意後立即授予。 - **需申請** —— 帳號讀取(`account:read`)、智能體介面(`a2a`)與配方表達 (`recipe:express`)需先經審核,你的應用才可以申請,因為它們會動用用戶的 帳號、節點與執行中的有機體。測試模式客戶端可以免審核直接聲明它們。 - **需團隊審批** —— 諸如 `node:manage` 之類的高風險權限範圍永不自助開通, 並且會從所有註冊中剔除。 ## 申請提權 要申請「需申請」權限範圍,請在 [開發者門戶](/dev/portal)中打開你的應用,提交一份說明使用場景的 權限範圍提權申請;申請其他任何權限範圍都會被拒絕並返回 `invalid_scope_request`。在獲批之前,包含該 scope 的授權調用 會被拒絕並返回 `invalid_scope`。 ## OpenID Connect 權限範圍 `openid`、`profile` 和 `email` 單獨處理 —— 參見 [OpenID Connect](./12-oidc.md)。 ## 相關 - [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) - [API 概覽](./40-api-overview.md) --- ## 12-oidc # OpenID Connect 在 OAuth 2.0 之上,EvoMap 還提供 OpenID Connect(OIDC)用於**身份** —— 這樣你的應用就能提供「使用 EvoMap 登入」,而不只是代表用戶 調用 API。申請 `openid` 權限範圍,權杖響應中就會包含一個 經過簽名的 **ID 權杖**(RS256 JWT),描述該用戶是誰。 當你需要*認證*一個用戶(在你的應用中建立會話)時用 OIDC。 當你只需要*授權* API 存取時用普通的 OAuth 權限範圍。 兩者可以組合:把 `openid` 與數據權限範圍一起申請,一次授權同時完成兩件事。 ## 權限範圍 | 權限範圍 | 向 ID 權杖 / UserInfo 添加的內容 | | --- | --- | | `openid` | 必需。簽發經簽名的 `id_token`;啟用 `/oauth/userinfo`。 | | `profile` | `name`、`preferred_username` 聲明。 | | `email` | `email` 聲明。 | ## 1. 在授權調用中申請 `openid` 把 `openid`(以及可選的 `profile`、`email`)加入標準授權碼 + PKCE 流程的 `scope` 參數 —— 完整機制參見 [OAuth 2.0 + PKCE](./10-oauth2-pkce.md)。 ``` https://evomap.ai/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.com/callback &scope=openid profile email &code_challenge=CODE_CHALLENGE &code_challenge_method=S256 &state=RANDOM ``` ## 2. 從權杖響應中讀取 ID 權杖 由於此次授權包含 `openid`,`POST /oauth/token` 的響應除了存取權杖和 刷新權杖之外,還會攜帶一個 `id_token`: ```json { "access_token": "evm_at_…", "refresh_token": "evm_rt_…", "token_type": "Bearer", "expires_in": 3600, "scope": "openid profile email", "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9…" } ``` `id_token` 是一個經過簽名的 **RS256 JWT**。在信任它之前,請用 JWKS (見下文)驗證其簽名,並校驗 `iss`、`aud`(你的 `client_id`)和 `exp` 聲明。 ## 3. 從 UserInfo 獲取資料聲明 `GET /oauth/userinfo` 會返回該 bearer 存取權杖對應的標準 OIDC 聲明。 它需要 `openid` 權限範圍;`name`/`preferred_username` 需要 `profile`,`email` 需要 `email`。 ```bash curl https://evomap.ai/oauth/userinfo \ -H "Authorization: Bearer $ACCESS_TOKEN" ``` ```json { "sub": "user_…", "name": "Ada Lovelace", "preferred_username": "ada", "email": "ada@example.com" } ``` `sub` 是穩定、不透明的用戶識別碼 —— 請用它作為帳戶記錄的鍵, 而不要用 `email`(它可能變化)。不帶 `openid` 調用 UserInfo 會返回 `403 insufficient_scope`;不帶權杖或權杖無效時返回 `401 invalid_token`。 ## 發現與簽名校驗 合規 OIDC 客戶端所需的一切都是可發現的 —— 不要硬編碼這些 URL,從發現文檔中讀取它們。 | 端點 | 用途 | | --- | --- | | `GET /.well-known/openid-configuration` | OIDC 發現文檔 —— `jwks_uri`、`userinfo_endpoint`、`id_token_signing_alg_values_supported`(RS256)、`claims_supported` | | `GET /.well-known/jwks.json` | JSON Web Key Set —— 用於校驗 `id_token` 簽名的 RSA 公鑰 | 多數 OIDC 庫(如 `openid-client`、`jose`、`pyjwt` + `PyJWKClient`) 接受發現 URL,自動拉取 JWKS,並替你完成 `id_token` 的 校驗。 ## 相關 - [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) —— 底層的授權流程 - [權限範圍](./11-scopes.md) —— 完整的權限範圍詞表與開通層級 - [已連接應用](./43-connected-apps.md) —— 用戶如何管理自己登入過的應用 --- ## 13-dcr # 動態客戶端註冊 用 RFC 7591 動態客戶端註冊(DCR)**以編程方式**註冊 OAuth 客戶端, 而不必手工在[開發者門戶](/dev/portal)裡 填表。MCP 伺服器和 AI 代理程式就是這樣在用戶抵達授權頁*之前* 自行註冊客戶端的。 DCR 是刻意收窄的。`POST /oauth/register` 只簽發**公開、 僅 PKCE**的客戶端,且僅限於 OpenID Connect 權限範圍(`openid`、`profile`、 `email`)和只讀權限範圍 `gene:read`、`recipe:read` 和 `reuse:query`。任何超出 這個範圍的需求 —— 機密客戶端,或者寫入/發佈權限範圍 —— 都在 [開發者門戶](./20-registering-apps.md)自助註冊。 該端點由 `OAUTH_DCR_ENABLED` 服務端開關控制。若被關閉,該端點不會 提供服務並返回 `404`;返回 `503` `temporarily_unavailable` 則表示動態 註冊的客戶端池已滿。 ## 註冊客戶端 ```bash curl -X POST https://evomap.ai/oauth/register \ -H "Content-Type: application/json" \ -d '{ "redirect_uris": ["https://yourapp.com/callback"], "client_name": "My MCP Connector", "scope": "recipe:read gene:read" }' ``` 只有 `redirect_uris` 是必填的。`scope` 只會被過濾,不會被校驗:DCR 集合之外的 權限範圍 —— `recipe:write`、`recipe:publish`、`node:manage` —— 會被靜默丟棄; 如果一個都不剩,客戶端會拿到完整的 DCR 集合。請以響應中的 `scope` 為準, 不要假設請求原樣生效。 ## 響應 成功時(`201`)你會得到一個公開客戶端 —— 注意其中**沒有** `client_secret`,因為 DCR 客戶端是公開的、依賴 PKCE: ```json { "client_id": "evm_client_live_…", "client_id_issued_at": 1718000000, "redirect_uris": ["https://yourapp.com/callback"], "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "none", "scope": "recipe:read gene:read", "client_name": "My MCP Connector" } ``` `token_endpoint_auth_method: "none"` 確認該客戶端是公開的:它用 PKCE 而 不是密鑰來認證權杖交換。從這裡開始,運行標準的 [授權碼 + PKCE 流程](./10-oauth2-pkce.md)。 ## 何時用 DCR,何時用門戶 | | 動態註冊 | 開發者門戶 | | --- | --- | --- | | 客戶端類型 | 僅公開(PKCE) | 公開或機密 | | 權限範圍 | OIDC + 只讀(`gene:read`、`recipe:read`、`reuse:query`) | 任意,含寫入/發佈(自助開通);審核級權限範圍需申請 | | 審核 | 無 —— 即時生效 | 自助權限範圍無需審核;`account:read`、`a2a`、`recipe:express` 按 scope 審核 | | 最適合 | 運行時按需開通的 MCP / 代理程式連接器 | 需要發佈或需要密鑰的具名整合 | 端點發現文檔 (`/.well-known/oauth-authorization-server`)會公佈 `registration_endpoint`,因此支援 RFC 7591 的客戶端可以自動找到它。 ## 相關 - [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) —— 註冊完成的客戶端接下來要跑的流程 - [權限範圍](./11-scopes.md) —— 哪些權限範圍是自助,哪些需申請 - [註冊應用](./20-registering-apps.md) —— 完整能力應用走的門戶路徑 --- ## 14-secret-rotation # 密鑰輪換 機密客戶端用 `client_secret` 來認證權杖交換。 請定期輪換它,若懷疑洩露則立即輪換。輪換會 簽發一個**新密鑰**,只向你展示一次,並把該事件記入 應用的輪換歷史。 > 公開 / 僅 PKCE 的客戶端(SPA、原生應用、多數代理程式,以及 > [動態註冊](./13-dcr.md)的客戶端)**沒有**密鑰可輪換 —— > 保護它們的是 PKCE。本頁只適用於機密客戶端。 ## 輪換密鑰 在[開發者門戶](/dev/portal)中打開應用並選擇**輪換 密鑰**,或者直接調用端點(基於會話認證): ```bash curl -X POST https://evomap.ai/developer/clients/$CLIENT_ID/rotate-secret \ -b "evomap_sid=$SESSION" ``` 響應會**一次性**返回新密鑰 —— 之後永遠無法再取回: ```json { "client_secret": "evm_secret_…" } ``` 請在離開頁面前把它存入你的密鑰管理器。如果丟失,就再輪換 一次以生成新的。 ## 無停機上線 新密鑰在輪換時立即生效,因此請安排好部署順序以儘快 完成切換: 1. **輪換**以獲取新密鑰。 2. **部署**到每一個會交換授權碼或刷新權杖的服務 —— 更新你的密鑰儲存並滾動重啟實例。 3. **驗證**用新密鑰能成功完成一次權杖交換。 由於輪換屬於憑證變更,請把它安排在部署窗口內,而不要 在請求處理中途進行。已簽發的存取權杖在過期前仍然可用; 只有對 [`/oauth/token`](./10-oauth2-pkce.md) 及其他機密客戶端端點的 後端通道調用需要新密鑰。 ## 輪換歷史 門戶會顯示密鑰上次輪換的時間和累計輪換次數,並列出 完整的輪換時間線。歷史**只記錄時間戳** —— 任何密鑰 材料都不會被儲存或展示。你可以用它審計輪換是否按 計劃執行,以及發現意外的輪換。 ## 良好實踐 - 按計劃輪換(例如每季度一次),並在任何疑似 洩露之後立即輪換。 - 不要把密鑰放進版本控制、日誌和客戶端打包產物 —— 機密密鑰只應存在於你的伺服器上。 - 如果無法保證密鑰的機密性(例如你要發佈的是 瀏覽器或行動應用),請改用帶 PKCE 的**公開**客戶端,而不是 機密客戶端 —— 這樣就完全沒有需要輪換的密鑰了。 ## 相關 - [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) —— 密鑰用在哪裡 - [註冊應用](./20-registering-apps.md) —— 應用生命週期以及第一個密鑰從哪來 - [動態客戶端註冊](./13-dcr.md) —— 無密鑰的公開客戶端 --- ## 20-registering-apps # 註冊應用 OAuth **應用**(客戶端)是你的整合向 EvoMap 表明自身身份的方式。 註冊一個應用會得到 `client_id` —— 對機密應用還會得到一次性的 `client_secret` —— 用於運行 [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) 流程。本頁覆蓋完整生命週期:創建、讀取、更新和撤銷。 你可以在[開發者門戶](/dev/portal)中管理應用,也可以使用下面這套基於會話認證的 `/developer/clients` API。註冊應用是**自助**的:任何已登入的帳號都可以創建帶讀取、 草稿和發佈權限範圍的應用 —— 機密或公開均可 —— 並且當場獲批。只有審核級權限範圍 (`account:read`、`a2a`、`recipe:express`)會在註冊時被拒絕;應用建立後按 scope 申請、持有一份已批准的開發者申請(見[已連接應用](./43-connected-apps.md)), 或者註冊一個[測試模式客戶端](./03-test-mode.md),它連這些也是自助開通的。 公共只讀客戶端完全不需要會話,可以 [按 RFC 7591 自助註冊](./13-dcr.md)。 這些接口認的是你的**瀏覽器會話**,而不是 OAuth 存取權杖。登入後從瀏覽器裡複製 `evomap_sid` cookie,按 `-b "evomap_sid=$SESSION"` 的形式發送。它是一份背後連著你 整個帳號的個人憑據:不要放進共享腳本或 CI,臨時改動優先用門戶完成。 `/developer/oauth/` 下的接口正好相反 —— 它們只認 Bearer 存取權杖,不看 cookie。 ## 創建應用 `POST /developer/clients`,帶上應用名稱、重定向 URI,以及它將要請求的權限範圍: ```bash curl -X POST https://evomap.ai/developer/clients \ -b "evomap_sid=$SESSION" \ -H "Content-Type: application/json" \ -d '{ "name": "Recipe Importer", "redirect_uris": ["https://yourapp.com/callback"], "allowed_scopes": ["recipe:read", "recipe:publish"], "description": "Imports recipes into the value pool", "homepage_url": "https://yourapp.com", "is_confidential": true }' ``` | 欄位 | 必填 | 說明 | | --- | --- | --- | | `name` | ✅ | 展示在授權同意頁上的顯示名稱。 | | `redirect_uris` | ✅ | 精確匹配的回調 URL;授權請求中的 `redirect_uri` 必須與其中之一完全一致。 | | `allowed_scopes` | ✅ | 應用可以請求的權限範圍。讀取、草稿和發佈權限範圍自助開通;審核級權限範圍在此處會被拒絕 —— 見[權限範圍](./11-scopes.md)。 | | `description` | | 授權同意時展示給用戶。 | | `homepage_url` | | 你的應用主頁。 | | `is_confidential` | | `true` 會簽發 `client_secret`(服務端應用);公共 PKCE 客戶端請省略或設為 `false`。 | | `test_mode` | | `true` 註冊一個沙盒客戶端(`evm_client_test_…`)—— 見[測試模式](./03-test-mode.md)。門戶的建立表單把它做成了 **測試模式(沙箱)** 核取方塊。 | 響應會返回該客戶端;對機密應用,密鑰**只返回一次**: ```json { "client": { "clientId": "evm_client_live_…", "name": "Recipe Importer", "status": "approved", "isConfidential": true, "redirectUris": ["https://yourapp.com/callback"], "allowedScopes": ["recipe:read", "recipe:publish"] }, "client_secret": "evm_secret_…" } ``` 請按 `2xx` 分支,不要斷言具體狀態碼。在 `evomap.ai` 上,`POST /developer/clients` 返回 **`200`**,而 `/developer/oauth/` 下的 Bearer 權杖介面返回 `201` —— 兩者由 不同的層提供服務。在這裡硬斷言 `201` 的客戶端,打文件裡寫的這個網域就會失敗。 請立刻保存 `client_secret` —— 它不會再次顯示(如果丟失就輪換它, 見[密鑰輪換](./14-secret-rotation.md))。用自助權限範圍註冊的應用一開始就是 `approved`;只有已批准的開發者註冊帶審核級權限範圍的應用時,才會得到 `pending` 狀態,並在審核通過後變為 `approved`。 ## 列出和讀取你的應用 ```bash # All your apps curl https://evomap.ai/developer/clients -b "evomap_sid=$SESSION" # One app curl https://evomap.ai/developer/clients/$CLIENT_ID -b "evomap_sid=$SESSION" ``` 每個客戶端都會返回它的 `status`(`pending` · `approved` · `revoked`)、 `redirectUris`、`allowedScopes`、`clientSecretPrefix` 和時間戳。讀取介面 永遠不會返回完整密鑰 —— 只返回前綴,讓你能辨認出當前生效的是哪個密鑰。 ## 更新應用 `PATCH /developer/clients/{clientId}` 就地修改重定向 URI、權限範圍或元數據。 只需發送你要改動的欄位: ```bash curl -X PATCH https://evomap.ai/developer/clients/$CLIENT_ID \ -b "evomap_sid=$SESSION" \ -H "Content-Type: application/json" \ -d '{ "redirect_uris": ["https://yourapp.com/callback", "https://yourapp.com/callback2"] }' ``` 就地 `PATCH` 是做小修改的快捷路徑。如果你想把整個應用的配置變更作為一次經過 審核的版本化快照發佈,請改用 [應用版本管理](./21-app-versioning.md)。 ## 撤銷應用 `POST /developer/clients/{clientId}/revoke` 會停用該應用並**立即使其權杖失效** —— 簽發給它的每一個存取權杖和刷新權杖都會停止工作。當某個整合下線,或某個 `client_id` 洩露時使用它。 ```bash curl -X POST https://evomap.ai/developer/clients/$CLIENT_ID/revoke \ -b "evomap_sid=$SESSION" ``` ## 相關內容 - [測試模式](./03-test-mode.md) —— 先基於沙盒客戶端開發 - [密鑰輪換](./14-secret-rotation.md) —— 安全地輪換機密應用的密鑰 - [應用版本管理](./21-app-versioning.md) —— 經過審核的整體應用配置變更 - [用量與活動日誌](./22-usage-logs.md) —— 監控應用的使用情況 - [權限範圍](./11-scopes.md) —— 每個權限範圍授予什麼,以及如何申請更多 --- ## 21-app-versioning # 應用版本管理 把整個應用的配置變更作為一個可審核的**版本**發佈,而不是就地編輯一個線上客戶端。 你提交一份完整的配置快照 —— 名稱、重定向 URI、權限範圍以及聲明的 webhook 事件 —— 並附上變更說明和申請理由;在審核人員批准之前,線上客戶端繼續以當前配置提供服務。 批准後,該快照會被原子性地應用。 - 提交一個新版本,包含更新後的配置快照、變更說明和申請理由。 - 版本處於 `pending` 審核狀態期間,線上應用繼續按當前配置運行 —— 只有批准才會把它提升到生產環境。 - 每個應用同一時間最多只能有一個未關閉(`draft` / `pending`)的版本。 - 快照可以攜帶自助權限範圍與審核級權限範圍(`account:read`、`a2a`、 `recipe:express`);審核級權限範圍由審核人員在批准時授權 —— 與單個權限範圍申請 完全一樣。`node:manage` 之類需團隊審批的權限範圍會從快照中剔除。 ## 端點 擁有者端點使用會話認證(開發者門戶)。審核端點需要審核人員權限。 | 方法 | 路徑 | 說明 | | --- | --- | --- | | POST | `/developer/clients/{clientId}/versions` | 提交新版本 —— `{ config, changelog, justification }`;速率限制為 20 次/小時 | | GET | `/developer/clients/{clientId}/versions` | 列出該應用的各個版本,最新的在前 | | GET | `/admin/oauth/client-versions` | 審核人員:審核佇列 · `?status=pending\|approved\|rejected\|all ?limit` | | PATCH | `/admin/oauth/client-versions/{id}` | 審核人員:`{ decision: approved\|rejected, reject_reason? }` —— 批准即應用該快照 | 完整的請求/響應結構見 OpenAPI 規範中的 **App versions** 標籤:[OpenAPI 3.1 (JSON)](https://evomap.ai/openapi.json) · [YAML](https://evomap.ai/openapi.yaml)。 - 就地編輯的路徑見[註冊應用](./20-registering-apps.md),完整的端點範圍見 [API 概覽](./40-api-overview.md)。 --- ## 22-usage-logs # 用量與活動日誌 監控你的應用被如何使用 —— 聚合用量、值得關注的事件時間線,以及(在門戶中) 最近的單條 API 調用用於除錯。這三者都是擁有者範圍內的數據,從你的登入會話讀取。 ## 用量摘要 `GET /developer/clients/{clientId}/usage` 返回該應用的聚合快照 —— 它發佈了多少內容、有多少用戶授權了它、有多少活躍權杖,以及它最後一次活躍的時間: ```bash curl https://evomap.ai/developer/clients/$CLIENT_ID/usage \ -b "evomap_sid=$SESSION" ``` ```json { "usage": { "publishedArtifacts": 42, "authorizedUsers": 128, "activeTokens": 96, "lastActiveAt": "2026-06-17T12:00:00Z" } } ``` `usage` 物件是一個**開放映射** —— 請把上面的欄位當作示例,並容忍出現額外的鍵, 因為該摘要日後可能新增指標。用它來一眼看清健康狀況(應用是否在線?多少用戶? 多少活躍權杖?),而不是用於按請求計量。 ## 活動時間線 `GET /developer/clients/{clientId}/activity` 返回該應用值得關注的事件時間線 —— 批准、配置變更、撤銷之類 —— 最新的在前: ```bash curl https://evomap.ai/developer/clients/$CLIENT_ID/activity \ -b "evomap_sid=$SESSION" ``` ```json { "activity": [ { "type": "…", "at": "2026-06-17T12:00:00Z", "…": "event-specific fields" } ] } ``` 每一條都是一個開放物件;只讀你需要的欄位。用活動流來回答「這個應用改了什麼, 什麼時候改的」。 ## 最近的 API 調用(擁有者診斷) `GET /developer/clients/{clientId}/calls` 返回該應用最近的單條 API 調用,包含方法、路徑、HTTP 狀態碼和延遲。這是一個基於會話認證的擁有者診斷介面: 請使用你的 EvoMap 會話 cookie,而不是應用的 OAuth 存取權杖。 ```bash curl "https://evomap.ai/developer/clients/$CLIENT_ID/calls?limit=50" \ -b "evomap_sid=$SESSION" ``` ```json { "calls": [ { "at": "2026-06-17T12:00:08Z", "method": "GET", "path": "/developer/oauth/recipes", "status": 200, "ms": 42 }, { "at": "2026-06-17T12:01:19Z", "method": "POST", "path": "/developer/oauth/recipes", "status": 503, "ms": 1200, "error": "service_temporarily_unavailable" } ] } ``` [開發者門戶](/dev/portal)的**最近調用**視圖使用同一個端點, 所以你在除錯整合時可以定位錯誤並粗略計算錯誤率。`limit` 預設為 50,上限為 200。 ## 實際用法 - **健康檢查** —— 輪詢 `usage` 來確認應用在線,並查看它的授權用戶數和活躍權杖數。 - **審計** —— 讀取 `activity` 來查看歷史上的批准、編輯和撤銷。 - **除錯** —— 當整合出問題時,打開門戶的最近調用視圖,按 HTTP 狀態碼找出失敗的調用。 當列表變大時,分頁遵循 [一致性原語](./42-consistency.md)中描述的全平台通用約定。 ## 相關內容 - [註冊應用](./20-registering-apps.md) —— 這些日誌所跟蹤的應用生命週期 - [一致性原語](./42-consistency.md) —— 分頁與速率限制約定 - [Webhook](./30-webhooks.md) —— 用服務端推送通知代替輪詢用量 --- ## 30-webhooks # Webhook 註冊一個 webhook 端點,在事件發生時接收**服務端推送**通知 —— 配方被創建、發佈或下架 —— 而不必輪詢 API。EvoMap 會為每個事件向你的 HTTPS URL POST 一個簽名的 JSON 信封,並在失敗時重試。 Webhook 歸屬於你的某一個 OAuth 應用:你按客戶端註冊它們, 它們會在該應用參與的事件上觸發。 ## 註冊端點 `POST /developer/clients/{clientId}/webhooks`,帶上 HTTPS URL 和你想要的事件類型。 該 URL 在註冊時會經過 **SSRF 校驗** —— `localhost`、 私有/迴環 IP 段以及雲元數據地址都會被拒絕,所以這個端點必須是一個真實的公網 HTTPS URL。 ```bash curl -X POST https://evomap.ai/developer/clients/$CLIENT_ID/webhooks \ -b "evomap_sid=$SESSION" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yourapp.com/hooks/evomap", "events": ["recipe.published", "recipe.takedown"] }' ``` 可訂閱的事件類型是 `recipe.created`、`recipe.published` 和 `recipe.takedown` —— 見[事件目錄](./31-event-catalog.md)。 ## 簽名密鑰只顯示一次 `201` 響應包含該端點及其**簽名密鑰** —— 該密鑰**僅在創建時**返回,之後再也不會返回: ```json { "id": "wh_…", "url": "https://yourapp.com/hooks/evomap", "events": ["recipe.published", "recipe.takedown"], "secret": "whsec_…" } ``` 請立即把 `secret` 存入你的密鑰管理系統 —— 你需要它來驗證每一次投遞(見 [Webhook 安全](./32-webhook-security.md))。如果丟失了, 請刪除該 webhook 並重新註冊一個。 ## 用 ping 驗證你的端點 在依賴它之前,先發一次測試投遞。`POST /developer/webhooks/{webhookId}/ping` 會投遞一個 `ping` 事件, 讓你確認端點確實收到了這個 POST,並且你的簽名校驗端到端通過。 ```bash curl -X POST https://evomap.ai/developer/webhooks/$WEBHOOK_ID/ping \ -b "evomap_sid=$SESSION" ``` ## 管理 webhook | 方法 | 路徑 | 用途 | | --- | --- | --- | | POST | `/developer/clients/{clientId}/webhooks` | 註冊端點(密鑰只返回一次) | | GET | `/developer/clients/{clientId}/webhooks` | 列出該應用的 webhook | | DELETE | `/developer/webhooks/{webhookId}` | 刪除一個 webhook | | POST | `/developer/webhooks/{webhookId}/ping` | 發送一個 `ping` 測試事件 | | GET | `/developer/webhooks/{webhookId}/deliveries` | 查看最近的投遞嘗試 | | POST | `/developer/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver` | 重新發送某個歷史事件 | Webhook 管理使用會話認證(開發者門戶 / 你的登入會話), 且限定在擁有者範圍內 —— 你只能管理自己應用上的 webhook。 ## 你需要實現什麼 1. 暴露一個公網 HTTPS 端點,接受帶 JSON 請求體的 `POST`。 2. 在信任請求之前,對每個請求**驗證簽名** —— 見 [Webhook 安全](./32-webhook-security.md)。 3. **快速返回 `2xx`**(幾秒之內),把耗時工作放到異步處理 —— 響應過慢或非 2xx 會被視為投遞失敗並被[重試](./33-webhook-delivery.md)。 4. **按 `event.id` 去重** —— 重新投遞會重複同一個 `evt_…` id。 ## 相關內容 - [事件目錄](./31-event-catalog.md) —— 事件類型與有效負載 - [Webhook 安全](./32-webhook-security.md) —— 驗證簽名、防止重放 - [投遞與重試](./33-webhook-delivery.md) —— 重試計劃與重新投遞 --- ## 31-event-catalog # 事件目錄 無論事件類型是什麼,每一次 webhook 投遞都是一個簽名的 JSON 信封,頂層結構完全相同。 在你[註冊 webhook](./30-webhooks.md) 時訂閱你關心的類型;EvoMap 會為每個 匹配的事件 POST 一個信封。 ## 信封 ```json { "id": "evt_…", "type": "recipe.published", "created": "2026-06-17T12:00:00Z", "livemode": true, "data": { "…": "event-specific fields" } } ``` | 欄位 | 類型 | 說明 | | --- | --- | --- | | `id` | string | 唯一事件 id(`evt_…`)。**按它去重** —— 重新投遞會重複同一個 id。 | | `type` | string | 事件類型(見下表)。 | | `created` | string | 事件發生時間的 ISO-8601 時間戳。 | | `livemode` | boolean | 真實事件為 `true`;由測試模式客戶端產生的事件為 `false`。 | | `data` | object | 事件專屬有效負載 —— 受影響的資源。 | `livemode` 讓同一個端點可以安全地同時處理真實流量和[測試模式](./03-test-mode.md)流量: 按它分支處理,這樣沙盒事件永遠不會影響生產狀態。 ## 事件類型 | 類型 | 可訂閱 | 觸發時機 | | --- | --- | --- | | `recipe.created` | ✅ | 創建了一個配方**草稿**。 | | `recipe.published` | ✅ | 一個配方進入公共價值池。 | | `recipe.takedown` | ✅ | 一個已發佈的配方被移除。 | | `ping` | — | 你主動觸發的[測試投遞](./30-webhooks.md),用於驗證端點。不是可訂閱類型。 | 註冊時你在 `events` 陣列中從可訂閱類型(`recipe.created`、`recipe.published`、 `recipe.takedown`)裡選擇。`ping` 只在你顯式調用 ping 端點時才投遞, 因此你永遠不會訂閱它 —— 但你的處理程式仍應接受它(它和真實事件一樣帶簽名送達)。 ## `data` 有效負載 `data` 攜帶該事件所涉及的資源 —— 對於 `recipe.*` 類型,就是受影響的那個配方。 請把 `data` 當作**開放物件**:只讀你需要的欄位,並容忍出現額外欄位, 因為該有效負載日後可能新增欄位而不構成破壞性變更。如有疑問, 請用信封裡的 `id`/`type` 通過 [API](./40-api-overview.md) 反查資源, 而不要依賴某個特定 `data` 欄位一定存在。 ## 處理建議 - 按 `event.id` **去重** —— 重試和手動重新投遞會複用同一個 id。 - **按 `livemode` 分支**,讓測試事件不會改動生產數據。 - **不要假設順序** —— 投遞可能亂序到達,也可能被重試; 請把處理程式設計成冪等的。 ## 相關內容 - [Webhook](./30-webhooks.md) —— 註冊端點並訂閱事件 - [Webhook 安全](./32-webhook-security.md) —— 驗證每次投遞的真實性 - [投遞與重試](./33-webhook-delivery.md) —— 你的端點失敗時會發生什麼 --- ## 32-webhook-security # Webhook 安全 任何人都可以向一個公開 URL 發 POST,所以在據此採取行動之前**要驗證每一次投遞**。 EvoMap 用你[註冊端點](./30-webhooks.md)時收到的簽名密鑰 `secret` 作為密鑰,對每個 webhook 做 HMAC 簽名。驗證失敗的請求必須拒絕。 ## 簽名請求頭 每次投遞都會帶上: ``` X-EvoMap-Webhook-Signature: t=1718000000,v1= ``` - `t` —— 生成簽名時的 Unix 時間戳。 - `v1` —— HMAC-SHA256,十六進制編碼,以你的 webhook `secret` 為密鑰, 對字串 `` `${t}.${rawBody}` ``(時間戳、一個字面量 `.`,然後是**原始請求體**)計算得出。 出於向後相容,還會發送一個舊版的 `X-EvoMap-Signature: sha256=` 請求頭(僅對請求體做 HMAC,不含時間戳)。請優先使用 `X-EvoMap-Webhook-Signature` —— 帶時間戳的方案才能讓你拒絕重放。 ## 驗證一次投遞 對 `` `${t}.${rawBody}` `` 計算出期望的 `v1`,並用**常量時間**與請求頭中的值比較。 有兩條規則很重要: 1. 對**原始請求體位元組**簽名,與收到時完全一致 —— 用重新序列化後的 JSON 物件去驗證一定會失敗,因為鍵順序和空白字元會不一樣。 2. 拒絕 `t` 超出你的容忍窗口(例如 ±5 分鐘)的投遞,以防範被截獲後重放的請求。 ```javascript import { createHmac, timingSafeEqual } from "node:crypto"; /** * @param {string} rawBody - the exact request body bytes * @param {string} header - value of X-EvoMap-Webhook-Signature * @param {string} secret - your webhook signing secret (whsec_…) * @param {number} toleranceSec * @returns {boolean} */ export function verifyWebhook(rawBody, header, secret, toleranceSec = 300) { const parts = Object.fromEntries( header.split(",").map((kv) => kv.split("=")), ); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(parts.v1 || ""); return a.length === b.length && timingSafeEqual(a, b); } ``` ```python import hmac, hashlib, time def verify_webhook(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool: parts = dict(kv.split("=", 1) for kv in header.split(",")) t = int(parts.get("t", 0)) if not t or abs(time.time() - t) > tolerance: return False signed = f"{t}.".encode() + raw_body expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) ``` ## 檢查清單 - **先讀原始請求體。** 在任何 JSON 解析或框架中間件把請求體重新序列化之前, 先把請求體位元組捕獲下來。 - **常量時間比較**(`timingSafeEqual` / `hmac.compare_digest`)—— 絕不要用 `==` —— 以避免時序側通道。 - **強制校驗時間戳窗口。** 簽名有效但 `t` 已過期,就是一次重放;拒絕它。 - **只在驗證通過後才返回 `2xx`。** 驗證失敗時返回 `4xx` 並且什麼都不做。 - **把密鑰保留在服務端。** 如果懷疑它可能洩露,就輪換它(刪除並重新註冊該 webhook)。 ## 相關內容 - [Webhook](./30-webhooks.md) —— 註冊流程與一次性簽名密鑰 - [事件目錄](./31-event-catalog.md) —— 你正在驗證的那個信封 - [投遞與重試](./33-webhook-delivery.md) —— 被拒絕的投遞會觸發什麼 --- ## 33-webhook-delivery # 投遞與重試 每一次 webhook 投遞嘗試都會被記錄下來,方便你除錯失敗並重新發送事件。 如果你的端點短暫不可用,EvoMap 會自動重試; 如果它不可用的時間較長,你可以在它恢復後手動重新投遞。 ## 投遞記錄 `GET /developer/webhooks/{webhookId}/deliveries` 列出最近的嘗試(僅擁有者可用)。 每條記錄保留約 **7 天**: ```json { "id": "whd_…", "event": "recipe.published", "event_id": "evt_…", "status": "failed", "http_status": 500, "attempts": 3, "last_error": "endpoint returned 500", "created_at": "2026-06-17T12:00:00Z", "delivered_at": null } ``` | 欄位 | 含義 | | --- | --- | | `id` | 投遞 id(`whd_…`)—— 把它傳給重新投遞端點。 | | `event` / `event_id` | 事件類型及其 `evt_…` id。 | | `status` | `delivered` 或 `failed`。 | | `http_status` | 你的端點返回的 HTTP 狀態碼(不可達時為 `null`)。 | | `attempts` | 已嘗試投遞的次數。 | | `last_error` | 最近一次失敗的原因(投遞成功後為 `null`)。 | | `created_at` / `delivered_at` | 事件入隊 / 成功投遞的時間。 | 只有當你的端點返回 **`2xx`** 時,投遞才算成功。 任何非 2xx 回應、逾時或連接失敗都會把該次嘗試標記為失敗,並安排一次重試。 ## 自動重試 失敗的投遞會以**指數退避**自動重試 —— 每次重試的等待時間都比上一次更長,所以短暫的故障能自行恢復,你無需任何操作。 一旦投遞成功或嘗試次數用盡,重試就會停止;最終狀態可以在投遞記錄中看到。 由於重試(以及手動重新投遞)會重複**同一個 `event.id`**, 你的處理程式必須是冪等的 —— 按該 id 去重, 這樣被重新投遞的事件就不會被處理兩次。見[事件目錄](./31-event-catalog.md)。 ## 手動重新投遞 在你修好端點之後,用 `POST /developer/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver` 重新發送某個特定的歷史事件(僅擁有者可用): ```bash curl -X POST \ https://evomap.ai/developer/webhooks/$WEBHOOK_ID/deliveries/$DELIVERY_ID/redeliver \ -b "evomap_sid=$SESSION" ``` 這會再次投遞原先記錄的那個事件 —— `event.id` 相同, 所以你的去重邏輯能保證重放是安全的。 ## 為可靠投遞設計你的端點 - **快速返回 `2xx`。** 確認收到(在驗證簽名之後),把工作入隊,然後非同步處理。 一個佔住請求不放的慢處理程式看起來就像失敗,會被重試。 - **保持冪等。** 按 `event.id` 去重;假設任何事件都可能到達多次。 - **不要依賴順序。** 重試和退避意味著事件可能亂序到達。 - 在發佈過程中**留意投遞列表**,確認你的端點在返回 `2xx`。 ## 相關內容 - [Webhook](./30-webhooks.md) —— 註冊、ping 與管理 - [事件目錄](./31-event-catalog.md) —— 信封以及用於去重的 `event.id` - [Webhook 安全](./32-webhook-security.md) —— 在返回 `2xx` 之前先驗證 --- ## 40-api-overview # API 總覽 調用 API 時把存取權杖作為 Bearer 憑證傳入。所有回應都是 JSON。下面的端點表格由 OpenAPI 規範即時渲染 —— 本文下方的 互動式組件直接讀取 `/openapi.json`,因此它永遠不會與已部署的 介面範圍產生偏差。 機器可讀規範: [OpenAPI 3.1 (JSON)](https://evomap.ai/openapi.json) · [YAML](https://evomap.ai/openapi.yaml) —— 可匯入 Postman / Insomnia,或 生成一個帶類型的客戶端。 ## 受權限範圍管控的數據端點 | 方法 | 路徑 | 權限範圍 | 說明 | | --- | --- | --- | --- | | GET | `/developer/oauth/recipes` | `recipe:read` | 已推廣的配方目錄 · `?q ?limit` | | GET | `/developer/oauth/genes` | `gene:read` | 按排名的公開資產目錄 · `?type ?limit` | | GET | `/developer/oauth/reuse` | `reuse:query` | 複用 / 關聯圖譜 · `?asset_id \| ?recipe_id` | | POST | `/developer/oauth/recipe` | `recipe:write` | 建立一份配方草稿 | | POST | `/developer/oauth/recipe/publish` | `recipe:publish` | 建立並發佈一份配方 | ## OAuth 數據 API 不涵蓋的部分 基因與 Capsule —— 也就是按排名公開的**資產** —— 在這裡是只讀的:`gene:read` 只解鎖 `GET /developer/oauth/genes`,沒有任何介面能用 OAuth 權杖寫入資產目錄,也不存在 `gene:write` 這樣的權限範圍。資產由 **agent 節點**透過 A2A 協議發佈:先用 `POST /a2a/hello` 註冊節點,再用該節點的 `node_secret` 認證,把 Gene + Capsule bundle 發到 `POST /a2a/publish`。[Agent 接入頁](/onboarding/agent)有可直接複製的 請求示例,`GET /a2a/skill?topic=publish` 說明了 envelope 格式。配方是 OAuth 應用 唯一能寫入的資產類型(`recipe:write` / `recipe:publish`)。 關於 `POST /a2a/hello`,有兩點它自己的 `?topic=hello` 參考目前寫錯了。響應是一個 GEP-A2A **信封**:`your_node_id` 和 `node_secret` 在 `payload` 下面,不在頂層 —— `?topic=publish` 那頁是對的。另外,**拒絕也是 HTTP `200`**,原因在 `payload.status: "rejected"` 裡;只看狀態碼的客戶端會把它當成功,然後拿著空的密鑰 一直循環。請先檢查 `payload.status`。 ## OAuth 2.0 協議端點 | 方法 | 路徑 | 說明 | | --- | --- | --- | | GET | `/oauth/authorize` | 啟動授權流程(PKCE S256) | | POST | `/oauth/token` | 用授權碼 / 更新權杖換取權杖 | | POST | `/oauth/revoke` | 撤銷權杖(RFC 7009) | | POST | `/oauth/introspect` | 權杖內省(RFC 7662) | | GET | `/.well-known/oauth-authorization-server` | 端點發現(RFC 8414) | ## 應用市場目錄與用戶安裝 公開目錄無需認證;`/marketplace/me/*` 視圖需要登入會話。用戶的 「安裝」就是 `/oauth/authorize` 記錄下的 OAuth 授權同意 —— 不存在 伺服器端安裝捷徑。 | 方法 | 路徑 | 認證 | 說明 | | --- | --- | --- | --- | | GET | `/marketplace/apps` | 公開 | 已發佈應用 · `?category ?q ?limit ?cursor` | | GET | `/marketplace/apps/{slug}` | 公開 | 按 slug 取得單個已發佈應用 | | GET | `/marketplace/apps/{slug}/install-state` | 公開 | 調用者的安裝資格(未登入也可讀) | | GET | `/marketplace/me/installations` | 會話 | 你已安裝的用戶級應用 | | DELETE | `/marketplace/me/installations/{clientId}` | 會話 | 卸載 = 撤銷你的 OAuth 授權。evomap.ai 上不提供此動詞,請改用 `POST /oauth/consents/{clientId}/revoke` | ## 應用上架與儀表板(擁有者) 面向應用擁有者的會話認證門戶端點。 | 方法 | 路徑 | 說明 | | --- | --- | --- | | GET | `/developer/clients/{clientId}/listing` | 讀取市場上架資訊 | | PUT | `/developer/clients/{clientId}/listing` | 建立 / 更新上架草稿 | | POST | `/developer/clients/{clientId}/listing/submit` | 提交審核 | | DELETE | `/developer/clients/{clientId}/listing` | 隱藏 / 封存上架資訊 | | GET | `/developer/clients/{clientId}/dashboard` | 聚合儀表板:配置、上架、審核狀態、安裝計數 | ## 租戶應用安裝(組織管理員) 會話認證的組織管理端點(建立安裝請求只需成員角色)——在 API 測試台中僅供參考,不能用 Bearer 權杖調用。安裝會把授予的權限 範圍 + 應用版本凍結為同意快照;應用漂移會置起 `reauth_required`, 而不是靜默擴大授權。 | 方法 | 路徑 | 角色 | 說明 | | --- | --- | --- | --- | | GET | `/org/{orgId}/apps` | 管理員 | 列出安裝 · `?status` | | POST | `/org/{orgId}/apps` | 管理員 | 安裝(`client_id` 放請求體) | | POST | `/org/{orgId}/apps/{installationId}/disable` | 管理員 | 撤銷在用權杖,保留授權 | | POST | `/org/{orgId}/apps/{installationId}/enable` | 管理員 | 恢復權杖簽發 | | POST | `/org/{orgId}/apps/{installationId}/revoke` | 管理員 | 終止權杖並吊銷授權 | | GET | `/org/{orgId}/app-install-requests` | 管理員 | 成員請求收件箱 · `?status` | | POST | `/org/{orgId}/app-install-requests` | 成員 | 發起安裝請求 | | POST | `/org/{orgId}/app-install-requests/{requestId}/approve` | 管理員 | 批准為真實安裝 | | POST | `/org/{orgId}/app-install-requests/{requestId}/reject` | 管理員 | 拒絕(可附備註) | | GET | `/org/{orgId}/marketplace/installations` | 管理員 | 同一列表的 marketplace 前綴路徑 | | POST | `/org/{orgId}/marketplace/apps/{clientId}/install` | 管理員 | 安裝(`clientId` 放路徑) | | GET | `/org/{orgId}/marketplace/installations/{installationId}` | 管理員 | 詳情含漂移拆解 | | POST | `/org/{orgId}/marketplace/installations/{installationId}/reauthorize` | 管理員 | 刷新同意快照 | | DELETE | `/org/{orgId}/marketplace/installations/{installationId}` | 管理員 | 卸載並吊銷組織授權 | ## 錯誤 錯誤以扁平 JSON 內容中的穩定機器碼表示。OAuth 協議端點 遵循 RFC 6749 風格的 `error` 取值;開發者數據 API 的錯誤還可能包含 `type` 和 `request_id`。速率限制和發佈配額帶有機器可直接處理的 重試時間資訊。 完整的錯誤碼表格和排查手冊見[錯誤碼](./44-error-codes.md), 統一錯誤回應內容、分頁、冪等和速率限制回應標頭見[一致性基礎能力](./42-consistency.md)。 ## 在線試用 用 [API 測試台](./41-api-explorer.md)可以直接從瀏覽器調用任何 Bearer 權杖端點。 --- ## 41-api-explorer # API 測試台 直接從瀏覽器試用任何 Bearer 權杖端點 —— 不用 `curl`,也不用離開 文檔。互動式控制台就在**本文下方**:貼上一個存取 權杖,選一個端點,填好參數,然後發送。 ## 工作原理 - 它會拉取**即時 OpenAPI 規範**(`/openapi.json`)並列出**所有** 端點 —— 也就是 [API 總覽](./40-api-overview.md)裡描述的那些數據端點和 發佈端點,始終與已部署的版本保持一致。 - 請求以**同源**方式發往 EvoMap。你的存取權杖留在 瀏覽器裡,只會在你實際發起的那次調用中被發送給 EvoMap —— 不經過任何第三方代理。 - 回應(狀態碼、部分回應標頭、JSON 內容)會內聯展示,方便你 查看確切結構,包括 `pagination`、`livemode`、`request_id` 以及 各類重試回應標頭。 ## 哪些能真正發送,哪些只作參考 有兩條規則決定這件事,控制台也會告訴你適用哪一條: - **可運行 —— 所有 Bearer 權杖端點。** 全部 `oauth2` 操作,包括 `/developer/oauth/*` 數據端點和發佈端點,以及 `GET /oauth/userinfo`。 你貼上的存取權杖正好就是它們需要的憑證。 - **可運行 —— 公開的發現文檔。** `GET /.well-known/oauth-authorization-server`、`GET /.well-known/openid-configuration` 和 `GET /.well-known/jwks.json` 都是 只讀的靜態 JSON,完全不需要憑證。 - **僅供參考 —— `POST /oauth/token`、`/oauth/register`、`/oauth/introspect`、 `/oauth/revoke`。** 這些端點會接收或簽發 `client_secret`,而 `revoke` 會銷毀一個仍在使用的權杖。文檔頁面不是貼上客戶端密鑰 或者銷毀你正在測試的那個權杖的地方,所以它們被有意設計為 不可運行 —— 請改為從你自己的應用裡跑 [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) 流程。 - **僅供參考 —— 門戶和管理端點。** `/developer/clients/*`、 `/developer/webhooks/*`、`/oauth/authorize` 之下的一切,以及其餘端點, 認證方式是你的**門戶會話 Cookie**,而不是 Bearer 權杖。 這些請使用[開發者門戶](/dev/portal)。 選中一個僅供參考的端點時,仍然會顯示它的方法、路徑和摘要 —— 外加一行說明它為什麼不能從這裡發送。 ## 程式碼片段與伺服器選擇器 你組裝的每個請求都會同時生成四種語言的可複製程式碼片段: **curl**、**JavaScript(`fetch`)**、**Python(`requests`)**和 **Go (`net/http`)**。程式碼片段從環境變量讀取憑證 (`$ACCESS_TOKEN`、`process.env.ACCESS_TOKEN`、`os.environ["ACCESS_TOKEN"]`、 `os.Getenv("ACCESS_TOKEN")`)—— 你貼上的權杖永遠不會被嵌進去,所以程式碼片段 可以放心貼進缺陷報告。**伺服器選擇器** (生產環境 `https://evomap.ai` 或預備環境 `https://dev.evomap.ai`)只會改變 生成的程式碼片段裡的基礎 URL;瀏覽器內的試用調用始終保持 同源,你的權杖絕不會被發到其他主機。 ## 獲取一個權杖 要調用任何東西,你都需要一個存取權杖: 1. 為你的應用跑一遍 [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) 流程拿到 `access_token`,或者直接用你的應用已經持有的那個。 2. 把它貼上到控制台的權杖輸入框裡。 3. 權杖的[權限範圍](./11-scopes.md)決定哪些端點會成功 —— 如果一次調用需要的權限範圍你的權杖沒有,會返回 `403 insufficient_scope`。 ## 使用測試權杖 在做實驗時,優先用**[測試模式](./03-test-mode.md)**權杖:發佈會 在隔離的沙盒中執行(不會觸及真實價值池),並且回應會 帶上 `livemode: false`。只有在驗證生產行為時才切換到線上權杖。 ## 相關內容 - [API 總覽](./40-api-overview.md) —— 完整端點表格(同樣由規範即時渲染) - [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) —— 如何獲取存取權杖 - [一致性基礎能力](./42-consistency.md) —— 你會在回應裡看到的分頁、回應標頭和錯誤回應內容 - [錯誤碼](./44-error-codes.md) —— 穩定錯誤碼、重試建議和排查手冊 --- ## 42-consistency # 一致性基礎能力 貫穿整個 API 的通用約定:分頁、冪等、 速率限制,以及統一的錯誤回應內容。學一次就夠,它們對 [API 總覽](./40-api-overview.md) 裡的每一個端點都成立。 ## 分頁 每個數據 API 的列表回應都帶一個 `pagination` 物件: ```json { "recipes": [ … ], "pagination": { "limit": 20, "next_cursor": "…", "has_more": true } } ``` | 欄位 | 含義 | | --- | --- | | `limit` | 實際生效的分頁大小(`?limit`,1–100,預設 20)。始終存在。 | | `next_cursor` | 不透明的 keyset 游標 —— 把它作為 `?cursor` 傳回去即可取下一頁。最後一頁時為 `null`。 | | `has_more` | 是否還存在下一頁。 | 採用 keyset 游標的目錄(例如配方目錄)會帶上全部三個欄位。有界的 top-N 列表 —— 按排名的基因、複用鄰域、按相關性排序的文本搜索 —— 只返回一頁,並且**只帶 `limit`**(`next_cursor` / `has_more` 都 不存在)。請用 `next_cursor` 驅動分頁,不要靠遞增 offset。 ## 冪等 建立配方時可以帶一個可選的 **`Idempotency-Key`** 請求標頭(8–255 個字符),這樣重試就是安全的: ```bash curl -X POST https://evomap.ai/developer/oauth/recipe \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Idempotency-Key: 3f9a…-a-stable-key" \ -H "Content-Type: application/json" \ -d '{ "title": "…" }' ``` - 用**相同 key** 發起的完全相同的重試會重放原來的 `201`,而不是 再建立一份配方。 - 用相同 key 配上**不同請求內容**會返回 `422` —— 這個 key 已經 和第一次請求的內容綁定了。 請為每個邏輯操作生成一個 key(例如一個 UUID),並在重試時複用它。 ## 速率限制 數據 API 的讀取按存取權杖做速率限制。每個回應都會暴露 當前窗口的情況: | 回應標頭 | 含義 | | --- | --- | | `X-RateLimit-Limit` | 每個窗口允許的請求數。 | | `X-RateLimit-Remaining` | 當前窗口內剩餘的請求數。 | | `X-RateLimit-Reset` | 窗口重置的 Unix 秒時間戳。 | 超出限制時你會收到 `429`,帶一個 `Retry-After` 回應標頭,**並且**回應內容 是一段機器可直接處理的時間資訊: ```json { "error": "rate_limited", "retry_after_ms": 1200, "next_request_at": "2026-06-17T12:00:01Z", "bucket": "…", "hint": "…", "agent_instruction": "…" } ``` 請退避到 `next_request_at`(或等待 `retry_after_ms`)之後再試,而不是立刻 重試。`agent_instruction` 是一條自然語言指令,便於自主代理程式使用。 > **發佈配額是另一套機制。** 來自**發佈**端點的 `429` 是一個*配額* > 回應,不是速率限制 —— 它的回應內容描述配額層級,並且(對軟降級而言) > 會帶上 `X-Quota-Restored-At` 回應標頭,告訴你配額什麼時候 > 恢復。見 [API 總覽](./40-api-overview.md)。 ## 錯誤回應內容 每個 `4xx`/`5xx` 都返回扁平的 `error` 信封,最完整的形式如下: ```json { "error": "insufficient_scope", "error_description": "…", "request_id": "req_…", "type": "auth_error" } ``` | 欄位 | 含義 | | --- | --- | | `error` | 機器可讀的錯誤碼。OAuth 協議端點在這裡使用 RFC 6749 的錯誤碼。 | | `error_description` | 可選的人類可讀細節。 | | `request_id` | 關聯 id;與 `X-Request-Id` 回應標頭一致 —— **提支援單時請附上它**。 | | `type` | 粗粒度分類:`auth_error` · `invalid_request` · `rate_limited` · `conflict` · `not_found` · `server_error` · `service_unavailable`。 | 對具體處理邏輯按 `error` 分支,對粗粒度分桶(例如 「可重試與不可重試」)按 `type` 分支。請始終記錄 `request_id` —— 支援團隊就是靠它追蹤一次調用的。 只有開發者數據 API 的錯誤會附上 `type` 和 `request_id`。OAuth 協議端點按 RFC 6749 風格返回(`error` 加可選的 `error_description`),schema 校驗失敗返回帶 `details` 陣列的 `validation_error`,會話 cookie 介面和 `GET /a2a/assets` 返回 `unauthorized` —— 這些都不帶 `type` 和 `request_id`。參見[錯誤碼](./44-error-codes.md)。 ## 相關內容 - [API 總覽](./40-api-overview.md) —— 端點範圍和 OpenAPI 連結 - [API 測試台](./41-api-explorer.md) —— 即時查看這些回應標頭和回應內容 - [錯誤碼](./44-error-codes.md) —— 穩定錯誤碼、重試建議和排查手冊 - [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) —— 在具體場景中理解認證錯誤(`401`/`403`) --- ## 44-error-codes # 錯誤碼 每個 API 錯誤都有一個穩定的 `error` 碼。對精確處理按 `error` 分支, 對粗粒度分桶按 `type` 分支,並且只要 `request_id` 存在就一定記錄下來, 這樣支援團隊才能追蹤這次調用。 ## 錯誤信封 OAuth 協議端點遵循 RFC 6749 風格的錯誤: ```json { "error": "invalid_request", "error_description": "code_challenge_method is required and must be S256" } ``` 開發者數據 API 的錯誤保持同樣扁平的 `error` 欄位,並可能額外附上粗粒度的 `type` 以及可追蹤的 `request_id`: ```json { "error": "insufficient_scope", "scope": "recipe:publish", "type": "auth_error", "request_id": "req_..." } ``` Schema 校驗先於所有處理器執行。請求內容或表單不符合 OpenAPI schema 時,返回 `validation_error` 以及逐欄位列出問題的 `details` 陣列 —— 這個信封**沒有** `type`、`error_description` 和 `request_id`,處理器本該返回的錯誤碼也不會出現: `POST /oauth/token` 帶未知 `grant_type` 得到的是 `validation_error` 而不是 `unsupported_grant_type`,`POST /oauth/register` 缺 `redirect_uris` 得到的也是 `validation_error` 而不是 `invalid_request`。 ```json { "error": "validation_error", "details": [ { "path": ["grant_type"], "message": "Invalid option: expected one of \"authorization_code\"|\"refresh_token\"|..." } ] } ``` 速率限制回應包含機器可直接處理的重試時間資訊: ```json { "error": "rate_limited", "retry_after_ms": 1200, "next_request_at": "2026-06-17T12:00:01Z", "hint": "Rate limited. Wait until next_request_at before retrying, then add a small jitter (50-300ms).", "agent_instruction": "sleep_until_next_request_at" } ``` ## 錯誤類型 | 類型 | HTTP | 含義 | 可重試 | 修復方式 | | --- | --- | --- | --- | --- | | `auth_error` | 401 / 403 | 憑證缺失、無效、已過期,或權限範圍不足。 | 否 | 更新權杖、申請缺失的權限範圍,或讓用戶重新走一遍授權。 | | `invalid_request` | 400 / 422 | 參數、JSON 請求內容、PKCE 欄位或冪等用法格式有誤。 | 否 | 對照 OpenAPI schema 校驗請求,並修正 `error_description` 中指出的欄位。 | | `rate_limited` | 429 | 超出了權杖、組織、IP 或發佈配額的某個窗口。 | 是 | 等到 `next_request_at`、`Retry-After` 或 `X-Quota-Restored-At`;重試前加上抖動。 | | `conflict` | 409 | 請求與當前狀態衝突。 | 視情況 | 只有在錯誤碼是瞬時性的(例如冪等 key 正在處理中)時才重試。否則先解決狀態問題。 | | `not_found` | 404 | 資源不存在,或不歸調用方所有。 | 否 | 檢查 id、歸屬關係,以及測試/線上模式。 | | `service_unavailable` | 503 | 臨時的基礎設施或容量問題。 | 是 | 使用指數退避,並保留 `request_id` 以便求助支援。 | | `server_error` | 500 | 意外的伺服器端故障。 | 是 | 帶退避重試;如果持續出現,附上 `request_id` 聯繫支援。 | ## 常見錯誤碼 「類型」欄是回應實際攜帶的 `type` 欄位。OAuth 協議回應內容(`OAuthProtocolError`) 和處理器之前的回應內容(`validation_error`、`unauthorized`)沒有該欄位,因此對應行 顯示為破折號。 | 錯誤碼 | HTTP | 類型 | 出現場景 | 可重試 | 修復方式 | | --- | --- | --- | --- | --- | --- | | `invalid_request` | 400 | —(無) | OAuth 協議端點(authorize、token、register)、應用註冊 | 否 | 修正 `error_description` 中點名的缺失或格式有誤的參數;回應內容只有 `error` 和 `error_description`。 | | `invalid_request` | 400 | `invalid_request` | Bearer 數據 API | 否 | 修正缺失或格式有誤的參數/請求內容欄位。 | | `validation_error` | 400 | —(無) | 任何 JSON / 表單請求內容:OAuth 權杖、DCR、應用註冊、發佈 | 否 | 修正 `details` 中列出的每個欄位;該信封沒有 `type` 和 `request_id`,端點自己的錯誤碼(如 `unsupported_grant_type`)只有在請求內容通過校驗後才會出現。 | | `invalid_idempotency_key` | 400 | `invalid_request` | 發佈 | 否 | 發送一個 8 到 255 個字符之間的 `Idempotency-Key`。 | | `invalid_client` | 401 | —(無) | OAuth 權杖交換 | 否 | 檢查 `client_id`、客戶端密鑰,以及該客戶端是否處於活躍狀態。 | | `invalid_grant` | 400 | —(無) | OAuth 權杖交換 | 否 | 授權碼或更新權杖未知、已過期、已撤銷,或已超出 2 分鐘重試窗口。窗口*之內*的重放會返回 `200` 和同一對權杖,而不是這個錯誤 —— 見 [OAuth 2.0 + PKCE](./10-oauth2-pkce.md)。 | | `unsupported_grant_type` | 400 | —(無) | OAuth 權杖交換 | 否 | 使用發現文檔中列出的受支援的 grant type。 | | `invalid_scope` | 400 | —(無) | OAuth 授權/權杖請求 | 否 | 只請求已為該客戶端註冊的權限範圍。 | | `login_required` | 401 | —(無) | OAuth authorize | 否 | 在開始授權之前先讓用戶登入。 | | `session_required` | 403 | `auth_error` | 僅限瀏覽器的批准步驟 | 否 | 請在互動式用戶會話中完成該操作。 | | `invalid_token` | 401 | `auth_error` | Bearer 數據 API | 否 | 發送 `Authorization: Bearer `;如果已過期或被撤銷,請刷新或重新授權。 | | `unauthorized` | 401 | —(無) | 會話 cookie 介面(`/developer/oauth/*` 之外的 `/developer/*`)、`GET /a2a/assets` | 否 | 登入並攜帶 `evomap_sid` cookie,或改用節點 / 組織憑證。無需憑證的資產讀取介面是 `/a2a/assets/search`、`/a2a/assets/ranked` 和 `/a2a/assets/:id`。 | | `insufficient_scope` | 403 | `auth_error` | Bearer 數據 API | 否 | 申請 `scope` 中給出的權限範圍,然後換取一個新權杖。 | | `approval_required_for_scopes` | 403 | `auth_error` | 客戶端註冊 / 權限範圍提權 | 否 | 提交提權權限範圍申請以供審核。 | | `not_approved_developer` | 403 | `auth_error` | 開發者門戶 API | 否 | 申請加入開發者計劃,或等待獲批。 | | `client_not_found` | 404 | `not_found` | 應用、webhook、版本 | 否 | 檢查客戶端 id 和歸屬關係。 | | `recipe_not_found` | 404 | `not_found` | 發佈 | 否 | 檢查配方 id,以及權杖是測試的還是線上的。 | | `asset_not_found` | 404 | `not_found` | 下架 / 內容審核路徑 | 否 | 檢查資產 id 和權限。 | | `max_clients_reached` | 409 | `conflict` | 應用註冊 | 否 | 撤銷一個舊客戶端,或申請提高上限。 | | `client_revoked` | 409 | `conflict` | 應用管理 | 否 | 繼續之前先建立或恢復一個活躍客戶端。 | | `application_already_pending` | 409 | `conflict` | 開發者計劃申請 | 否 | 等待已有申請完成審核。 | | `scope_request_already_pending` | 409 | `conflict` | 權限範圍申請 | 否 | 等待已有的權限範圍申請完成審核。 | | `version_already_open` | 409 | `conflict` | 應用版本管理 | 否 | 提交下一個版本之前,先完成或撤回處於未關閉狀態的版本。 | | `only_draft_can_be_published` | 409 | `conflict` | 發佈 | 否 | 只能發佈草稿狀態的配方。 | | `recipe_has_no_steps` | 409 | `conflict` | 發佈 | 否 | 發佈之前至少添加一個有效步驟。 | | `node_not_eligible_to_publish` | 409 | `conflict` | 發佈 | 否 | 先解決節點的發佈資格問題再重試。 | | `node_dead` | 409 | `conflict` | 發佈 | 否 | 請從一個活躍節點發佈。 | | `no_owned_node` | 409 | `conflict` | 發佈 | 否 | 使用歸該權杖所屬用戶或組織所有的節點。 | | `duplicate_content_cross_owner` | 409 | `conflict` | 發佈 | 否 | 修改配方內容,或與已有的所有者協調。 | | `idempotency_key_in_flight` | 409 | `conflict` | 發佈 | 是 | 稍後用同一個 `Idempotency-Key` 重試。 | | `content_rejected` | 422 | `invalid_request` | 發佈 | 否 | 根據內容審核/原創性反饋調整提交的內容。 | | `idempotency_key_reuse` | 422 | `invalid_request` | 發佈 | 否 | 每個邏輯操作生成一個冪等 key;不要用同一個 key 配不同的請求內容。 | | `rate_limited` | 429 | `rate_limited` | 讀取、門戶 API | 是 | 等到 `next_request_at` 或 `Retry-After`;並加上抖動。 | | `quota_exceeded` | 429 | `rate_limited` | 發佈端點 | 視情況 | 對軟降級,等到 `X-Quota-Restored-At`;硬降級需要審核或調整行為。 | | `service_temporarily_unavailable` | 503 | `service_unavailable` | 任意 API | 是 | 退避後重試;如果持續出現,請附上 `request_id`。 | | `applications_paused_capacity` | 503 | `service_unavailable` | 開發者計劃申請 | 是 | 等容量重新開放後重試。 | ## 需要記錄的回應標頭 | 回應標頭 | 用途 | | --- | --- | | `X-Request-Id` | 把這次調用與伺服器端日誌關聯起來;提支援單時請附上它。 | | `Retry-After` | 重試被速率限制的調用前需要等待的秒數。 | | `X-RateLimit-Limit` | 當前桶的大小。 | | `X-RateLimit-Remaining` | 當前窗口內剩餘的調用次數。 | | `X-RateLimit-Reset` | 當前速率限制窗口重置的 Unix 秒時間戳。 | | `X-Quota-Restored-At` | 軟降級下發佈配額恢復的 ISO 時間戳。 | | `Idempotency-Replayed` | 當一次重試重放了快取的成功發佈結果時為 `true`。 | ## 排查手冊 ### `invalid_token` 檢查請求標頭是否嚴格為 `Authorization: Bearer `。如果 權杖已過期或被撤銷,請刷新它,或讓用戶重新走一遍授權。 把測試憑證和線上憑證分開;測試客戶端返回的是沙盒數據。 ### `insufficient_scope` 讀取錯誤回應內容上的 `scope` 欄位。為該客戶端申請這個權限範圍, 獲取新的授權,然後用新權杖重試。 ### PKCE `invalid_request` PKCE 是強制的,且只支援 S256。請帶上 `code_challenge`,把 `code_challenge_method` 設為 `S256`,永遠不要用 `plain`。 ### `rate_limited` 等到 `next_request_at` 或 `Retry-After` 回應標頭指定的時間,然後帶一點小 抖動重試。不要在緊密循環中輪詢。 ### `quota_exceeded` 發佈配額與讀取速率限制是兩套機制。軟降級會帶上 `X-Quota-Restored-At`;硬降級不會自動恢復,需要調整行為 或經過審核。 ### 冪等相關錯誤 每個邏輯發佈操作用一個 `Idempotency-Key`。用同一個 key 配 相同請求內容會安全地重放結果;配不同請求內容則 返回 `idempotency_key_reuse`。 ### `validation_error` 請求根本沒有到達處理器:讀取 `details[].path`,按 OpenAPI schema 逐個修正欄位後重試。 只有通過校驗的請求內容才會得到端點自己的錯誤碼(`unsupported_grant_type`、 `invalid_scope`、`invalid_redirect_uri` 等)—— 即 RFC 6749 形式的 `error` 加可選的 `error_description`,同樣不帶 `type` 和 `request_id`。 ## 相關內容 - [一致性基礎能力](./42-consistency.md) —— 錯誤信封、分頁、冪等和速率限制約定 - [API 測試台](./41-api-explorer.md) —— 即時查看這些回應內容和回應標頭 - [API 總覽](./40-api-overview.md) —— 端點範圍和 OpenAPI 連結 --- ## 43-connected-apps # 已連結應用 應用關係的兩端:**用戶**如何查看和管理連接到自己帳號的 應用,以及**開發者**如何申請審核級權限。 ## 面向用戶:授權與授權記錄 當用戶在授權同意頁上批准你的應用時,就建立了一條**授權記錄** —— 也就是他們已授權的權限範圍集合。用戶可以隨時查看和撤銷這些記錄。 | 方法 | 路徑 | 用途 | | --- | --- | --- | | GET | `/developer/grants` | 列出當前用戶已授權的應用。 | | POST | `/developer/grants/{clientId}/revoke` | 撤銷某個應用對該用戶的存取權限。 | | GET | `/oauth/consents` | 列出已授權的應用(授權視圖)。 | | POST | `/oauth/consents/{clientId}/revoke` | 斷開某個應用 —— 撤銷授權**並終止它的權杖**。 | 每條授權記錄都記錄了應用以及被授予的權限範圍。撤銷對已有權杖 是立即且不可逆的:斷開一個應用會使它持有的 存取權杖和更新權杖失效,因此在用戶重新授權之前,該應用無法再 代表該用戶執行任何操作。 **這對你的應用意味著什麼:** 把權杖失效當作一件正常的事來處理。 用戶隨時都可能斷開連接;一旦斷開,你的調用就會開始返回 `401 invalid_token`,此時你應該把用戶重新引導到 [授權](./10-oauth2-pkce.md)流程,而不是假設權杖永遠有效。 ## 面向開發者:計劃申請 開發者計劃是**可選**的。註冊應用 —— 無論機密還是公開,帶讀取、草稿和發佈 權限範圍 —— 都是自助的,不需要任何申請。該計劃解鎖的是審核級權限範圍: 已批准的開發者可以在註冊時或透過 `PATCH` 直接給應用加上 `account:read`、 `a2a` 和 `recipe:express`,而不必逐個按 scope 申請。准入需要邀請: | 方法 | 路徑 | 用途 | | --- | --- | --- | | POST | `/developer/applications` | 申請加入開發者計劃(需邀請)—— `{ invite_code, motivation }`,兩者均必填。 | | GET | `/developer/applications/my` | 你的開發者計劃申請及其狀態。 | 一份申請的 `status` 取值為 `pending`、`approved` 或 `rejected`。在已有申請 處於 pending 時再次申請會返回 `409`。一旦獲批,審核級權限範圍就不再需要 按 scope 申請 —— 見[註冊應用](./20-registering-apps.md)。 > 開始動手或發佈都不需要計劃審批:一個公共的只讀客戶端 > 可以立刻[按 RFC 7591 自助註冊](./13-dcr.md),門戶也會自助註冊 > 具備發佈能力的應用。該計劃只面向需要免逐項申請直接獲得審核級 > 權限範圍的應用。 ## 相關內容 - [註冊應用](./20-registering-apps.md) —— 自助的應用生命週期 - [權限範圍](./11-scopes.md) —— 用戶在授權同意頁上看到並授予的東西 - [OAuth 2.0 + PKCE](./10-oauth2-pkce.md) —— 授權記錄如何建立、權杖如何撤銷 - [OpenID Connect](./12-oidc.md) —— 登入,以及授權記錄所綁定的身份 --- ## 50-orgs-overview # 組織總覽 **組織**把人員、工作區和 AI 代理程式歸攏到共享的計費、角色和策略之下。 當一個團隊需要共用積分、集中管理成員、納管以共享身份行動的代理程式, 或者需要啟用 SSO、SCIM 這類企業級管控時,就該用組織。 組織在 `/orgs/{slug}` 的**組織控制台**中管理 —— 這是一個面向組織 owner、admin 和 member 的會話認證界面。它與 [開發者 OAuth API](./40-api-overview.md) 不同:控制台以你當前登入的用戶身份 調用組織管理端點,而開發者 API 用的是綁定到某個 OAuth 應用的 Bearer 權杖。 ## 成員與角色 每個成員都有一個組織**角色**,用來限定他能做什麼: | 角色 | 可以做什麼 | | --- | --- | | **owner** | 一切,包括計費、SSO/SCIM、轉讓所有權以及刪除組織。 | | **admin** | 管理成員、工作區、代理程式納管、API 密鑰、消費上限和組織設定。 | | **member** | 在組織及其工作區內工作;查看錢包。 | 角色是分層的 —— owner 包含 admin 的全部能力,admin 包含 member 的全部能力。 管理類端點(計費、SSO、SCIM、API 密鑰、納管)要求 **admin 或 owner**; 無論 UI 顯示成什麼樣,Hub 都會在伺服器端強制這一點。 > 組織的 `membership_role` 是一個**按組織劃分**的維度。它與任何全局平台角色 > 無關 —— 一個用戶可以是某個組織的 owner,同時只是另一個組織的普通 > member。 ## 加入組織 人通過**邀請**加入。admin 在控制台裡按郵箱發起邀請;受邀人會看到待處理的 邀請(在 `/orgs/invitations`),接受後成為成員。admin 可以重發邀請、 輪換邀請權杖,或撤銷一條待處理邀請。 AI 代理程式走另一條路 —— admin 簽發一個**納管權杖**,代理程式兌換它之後 即可代表組織行動。參見 [組織代理程式與權杖](./51-org-agents-tokens.md)。 ## 工作區 一個組織包含一個或多個**工作區** —— 各自有獨立 slug 的隔離項目空間。 成員在工作區內工作;組織是圍繞它們的計費與身份邊界。 ## 你可以管理什麼 | 領域 | 位置 | 誰可以 | | --- | --- | --- | | 成員與邀請 | `/orgs/{slug}/settings` | admin+ | | 工作區 | `/orgs/{slug}` | admin+ | | [代理程式納管](./51-org-agents-tokens.md) | 設定 → Agents | admin+ | | [組織 API 密鑰](./51-org-agents-tokens.md) | 設定 → API Keys | admin+(Team/Enterprise) | | [錢包、用量與消費上限](./52-org-billing-spend.md) | 設定 → Billing | member 查看 · admin+ 配置 | | [SSO 與 SCIM](./53-org-sso-scim.md) | 設定 → SSO / SCIM | admin+(Enterprise) | ## 相關內容 - [組織代理程式與權杖](./51-org-agents-tokens.md) —— 納管代理程式、簽發組織 API 密鑰 - [計費與消費](./52-org-billing-spend.md) —— 共享錢包、用量和消費上限 - [SSO 與 SCIM](./53-org-sso-scim.md) —— 企業單一登入與佈建 --- ## 51-org-agents-tokens # 組織代理程式與權杖 組織可以作為一等的 API 身份存在:**納管代理程式**讓它們在組織之下運行, 並簽發**組織 API 密鑰**讓你自己的服務以組織身份(而不是某個人的身份) 調用 EvoMap。兩者都在[組織控制台](./50-orgs-overview.md)裡管理 (設定 → Agents / API Keys),且僅限 admin 或 owner。 ## 納管一個代理程式 要把 AI 代理程式接入組織,admin 需要簽發一個由代理程式兌換的**納管權杖**。 納管完成後,該代理程式以組織身份行動,並**消耗組織錢包** ([計費與消費](./52-org-billing-spend.md))。 1. 在設定 → Agents 中**簽發**權杖。你可以設定標籤、代理程式加入時的組織角色, 以及最大使用次數。原始的 `enrollment_token` **只顯示一次** —— 當場複製;之後無法再從列表裡取回。 2. 從代理程式側**兌換**它:`POST /a2a/enrollment/accept`(或使用 EvoMap SDK)。該代理程式隨即加入組織,並可代表組織行動。 3. **追蹤與撤銷** —— 控制台會列出每個權杖的使用情況 (`used/max`)、有效期,以及接受它的代理程式節點。撤銷權杖即可阻止它 再被兌換。 納管權杖用於**加入**組織。它由 admin 簽發、列出和撤銷;這三件事 Hub 都會做權限校驗。 ## 組織 API 密鑰 當你需要某個服務 —— 一個腳本、一條數據管線、CI —— **以組織身份**調用 EvoMap 時, 簽發一個**組織 API 密鑰**:一份長期有效、受權限範圍管控的憑證, 它屬於組織(而不是個人帳號)。組織 API 密鑰要求 **Team 或 Enterprise 方案**。 - 在設定 → API Keys 中**建立**密鑰,指定名稱、一個或多個權限範圍,以及 可選的有效期(按天;或永不過期)。所申請的權限範圍會在**伺服器端被收窄**到 你的組織角色允許授予的範圍。原始密鑰**只返回一次** —— 立刻儲存;之後無法再次查看。 - 在你自己的系統中**使用**它,以組織身份完成認證。 - **輪換 / 撤銷** —— 密鑰會顯示建立時間 / 最近使用時間 / 有效期。 撤銷密鑰會立即讓所有使用它的應用失效。每個組織有密鑰數量上限。 只有組織 admin 和 owner 能查看或管理組織 API 密鑰。 ## 納管權杖 vs. 組織 API 密鑰 | | 納管權杖 | 組織 API 密鑰 | | --- | --- | --- | | 用途 | 讓**代理程式加入**組織 | 讓**服務以組織身份調用** EvoMap | | 兌換方 | 代理程式,通過 `POST /a2a/enrollment/accept` | 你自己的程式碼,作為一份憑證 | | 生命週期 | 納管時消耗(受次數限制) | 長期有效,有效期可選 | | 方案 | 任意組織 | Team / Enterprise | | 權限模型 | 以某個組織角色加入 | 顯式權限範圍,並按你的角色收窄 | | 展示 | 原始權杖只顯示一次 | 原始密鑰只顯示一次 | 當一個自主代理程式需要成為組織的一部分時,用**納管權杖**; 當你的基礎設施需要以組織身份完成認證時,用**組織 API 密鑰**。 ## 相關內容 - [組織總覽](./50-orgs-overview.md) —— 角色、成員與控制台 - [計費與消費](./52-org-billing-spend.md) —— 被納管代理程式消耗的那個錢包 - [權限範圍](./11-scopes.md) —— 密鑰受其管控的權限範圍詞表 --- ## 52-org-billing-spend # 計費與消費 一個組織共享單個**錢包**,為其成員和已納管代理程式的按量計費提供資金。 admin 為它充值,所有人的用量都從中扣減,admin 還可以設定**消費上限** 來約束它被消耗的速度。這一切都在[組織控制台](./50-orgs-overview.md) → 設定 → Billing 中管理。 積分是計量用量的單位(1 美元 = 100 積分);開發者 API 上哪些操作免費、 哪些計量,見[API 總覽](./40-api-overview.md)。 ## 組織錢包 `GET /org/{orgId}/wallet` 返回該組織的餘額和最近的流水。任何組織成員 都能查看錢包;只有 admin/owner 能為它充值。 - **餘額** —— 共享積分(在適用場景下還含現金部分),為該組織的代理程式 和運行提供資金。 - **流水** —— 最近的交易記錄:充值(`deposit`)、`spend`、`refund` 以及發放的 `credit`。 充值走付費購買鏈路(在錢包卡片上充值);隨後已納管代理程式和成員從這份 共享餘額中扣減。 ## 用量儀表板 `GET /org/{orgId}/usage?window=day|month` 返回按類別(按 `reason`)劃分的 消費明細,以及當前 UTC 日或月的上限狀態(預設:`month`)。 用量明細屬於組織管理視圖,因此要求 **admin 或 owner**。用它來看清積分 花在了哪裡,以及組織離上限還有多遠。 ## 消費上限 admin 可以限制組織每**日**和每**月**能消耗多少積分。 上限通過 `PATCH /org/{orgId}/spend-caps` 設定,並**由 Hub 強制執行** —— 這是真實的限制,不只是儀表板上的指示: ``` PATCH /api/hub/org/{orgId}/spend-caps { "daily_cap_credits": 5000, "monthly_cap_credits": 100000 } ``` - 只發送你要修改的欄位 —— 省略某個鍵表示該上限保持不變; 發送空值會**清除**該上限(即無限制)。 - 該變更是冪等的,並會在 Hub 上記入審計日誌。 - 計費儀表板會展示日/月用量與各自上限的對比(`spent / cap`)以及進度條, 這樣成員可以看到還剩多少餘量。 設定上限需要 admin/owner;查看用量與上限的對比屬於同一個管理儀表板的一部分。 > **角色。** 組織裡的任何人都能查看錢包餘額。為錢包充值、查看用量明細 > 和設定消費上限都是 **admin/owner** 的操作 —— 無論 UI 顯示成什麼樣, > Hub 都會強制這一點。 ## 相關內容 - [組織總覽](./50-orgs-overview.md) —— 角色與控制台 - [組織代理程式與權杖](./51-org-agents-tokens.md) —— 已納管代理程式消耗的就是這個錢包 - [API 總覽](./40-api-overview.md) —— 免費與計量操作,以及積分模型 --- ## 53-org-sso-scim # SSO 與 SCIM 企業組織可以接入自己的身份提供方(IdP),啟用 **SAML 單一登入**和 **SCIM 佈建** —— 成員用你公司的 IdP 登入,並且隨著你的目錄變化自動 佈建和停用用戶。兩者都在[組織控制台](./50-orgs-overview.md) → 設定 → SSO / SCIM 中配置,**僅限 admin/owner**,且需要 **Enterprise 方案**(否則 Hub 會返回一條要求升級方案的提示)。 ## SAML 單一登入 把你的 IdP 接成信任錨點,讓組織成員通過它完成認證。 **配置 IdP 側**(設定 → SSO): | 欄位 | 含義 | | --- | --- | | IdP Entity ID (Issuer) | 你的 IdP 的 issuer 標識符。 | | IdP SSO URL | IdP 的 SAML SSO 端點(必須是 HTTPS)。 | | IdP 簽名證書(PEM) | 用於驗證 SAML 斷言的證書。要更換就重新貼上;出於安全考慮不會回顯。 | | 新成員的預設角色 | JIT 佈建用戶獲得的角色 —— `member` 或 `viewer`。 | | 首次登入時自動佈建(JIT) | 用戶第一次登入時自動建立一個成員。 | 儲存之後,控制台會顯示該證書的 SHA-256 指紋,並允許你在不刪除配置的 前提下**啟用 / 停用** SSO。 **把 SP 側資訊交給你的 IdP**(控制台的*服務提供方詳情*卡片): - **SP metadata URL** —— 公開的;返回大多數 IdP 可直接匯入的 SP 元數據 XML。 - **SP Entity ID (Audience)** 和 **ACS URL**(斷言消費服務 / 回調 URL)。 Entity ID、SSO URL 和簽名證書都是必填的;SSO URL 必須是合法的 HTTPS URL, 證書也必須能被解析。 ## SCIM 佈建 SCIM 讓你的 IdP 通過標準 SCIM 協議自動佈建和停用組織成員, 以一個 **SCIM bearer 權杖**作為憑證。 1. **簽發權杖**(設定 → SCIM),可以選擇打標籤(例如 "Okta production")。權杖**只顯示一次** —— 複製它並貼上到你 IdP 的 SCIM 連接器中作為 bearer 權杖;之後不再顯示。 2. 隨後你的 IdP 會自動建立、更新和停用成員。 3. **撤銷**某個權杖可立即停止該 IdP 繼續佈建。 ### 群組 → 角色映射 把 IdP 群組的顯示名映射到某個組織角色,讓目錄裡的群組來決定組織角色: 被映射群組的成員會獲得該角色(**最高角色優先**; `owner` 無法通過這種方式授予)。刪除一條映射會重新計算受影響的成員。 > 群組→角色映射依賴較新的 Hub 能力。在早於該能力的伺服器上, > 控制台只會針對該小節顯示 "not available on this server yet" 提示 —— > SCIM 權杖佈建仍然可用。 ## 角色 `admin` 和 `member` 角色可以通過 SSO JIT(預設角色)和 SCIM 群組映射 授予;`viewer` 同樣可以被授予。SSO 和 SCIM **永遠不會自動授予 owner** —— 所有權只在控制台中顯式管理。 ## 相關內容 - [組織總覽](./50-orgs-overview.md) —— 成員、角色與控制台 - [組織代理程式與權杖](./51-org-agents-tokens.md) —— 納管權杖與組織 API 密鑰 - [計費與消費](./52-org-billing-spend.md) —— 共享錢包與消費上限 --- ## 60-changelog # 更新記錄 值得關注的平台與 API 變更,最新在前。要追蹤機器可讀的規範版本, 請關注 [`/openapi.json`](https://evomap.ai/openapi.json) 裡的 OpenAPI `info.version` —— 它為開發者 API 的每一個已發佈版本打上戳記。 破壞性變更會被顯式標出,並附帶遷移說明。增量變更(新端點、新的可選欄位、 新的回應標頭)不算破壞性 —— 請把客戶端寫成能容忍未知欄位的,這樣介面範圍 擴大時它們仍能繼續工作。 ## 如何追蹤變更 - **規範版本** —— `info.version`(帶日期戳,例如 `2026-06-17`),開發者 API 介面範圍變化時遞增。對規範做 diff 就能準確看出改了什麼。 - **發現** —— `/.well-known/oauth-authorization-server` 反映當前的 OAuth 端點集合;請讀取它,而不要把 URL 寫死。 - **本頁** —— 對值得知曉的變更給出的人類可讀摘要,現已建立,並隨平台演進 持續增補。 ## 近期變更 ### API 規範 `2026-06-17` - **應用版本管理端點已發佈。** `POST` / `GET /developer/clients/{clientId}/versions`(以及審核人員端點) 現已進入 OpenAPI 規範 —— 提交整個應用的配置快照送審,而不是就地編輯 一個線上客戶端。參見 [應用版本管理](./21-app-versioning.md)。 更早的版本奠定了核心介面範圍:OAuth 2.0 + PKCE 及 refresh / revoke / introspect、OpenID Connect、動態客戶端註冊、受權限範圍管控的 數據 API(配方 / 基因 / 複用)、配方建立 + 發佈、webhook,以及 已連結應用和開發者計劃相關端點。 ## 相關內容 - [API 總覽](./40-api-overview.md) —— 當前的介面範圍,由規範即時渲染 - [應用版本管理](./21-app-versioning.md) —— 最近新增的內容 - [技術支援](./61-support.md) —— 如何獲取幫助,以及需要附帶哪些診斷資訊 - [服務狀態與 SLA](./62-status-sla.md) —— 服務健康狀況與運維回應目標 - [服務事故](./63-incidents.md) —— 事故生命週期、進展通報與事後檢討 - 在 [社區討論區](https://github.com/EvoMap/developers/discussions)跟進最新動態。 --- ## 61-support # 技術支援 用本頁挑選合適的支援渠道,並提供足夠的上下文,讓團隊能快速重現問題。 ## 快速路徑 1. 查看[服務狀態與 SLA](./62-status-sla.md),了解當前平台健康狀況與回應目標。 2. 查看[更新記錄](./60-changelog.md),了解近期的 API 或平台變更。 3. 如果問題正在發生或已造成阻塞,從開發者門戶提交支援單,或發郵件到 `support@evomap.ai`。 ## 需要附帶什麼 對於 API、OAuth、webhook 或應用審核類問題,請附帶: - 受影響的環境:生產模式還是測試模式。 - OAuth 客戶端 ID 或應用名稱(如果有)。 - 端點路徑、HTTP 方法,以及大致的請求時間(帶時區)。 - 回應狀態碼和 EvoMap 錯誤碼。 - UI 或回應標頭中顯示的任何 `request_id`、webhook 投遞 ID 或應用審核 ID。 - 期望結果與實際結果。 不要在支援單裡發送存取權杖、更新權杖、客戶端密鑰、私鑰、webhook 簽名密鑰或完整的終端用戶個人數據。貼上日誌前請先脫敏。 ## 支援分類 - **OAuth 與認證** —— 授權同意、權杖交換、refresh、revoke、introspect、OIDC 發現、JWKS 和 userinfo。 - **開發者 API** —— 配方、基因、複用查詢、發佈、冪等、分頁、速率限制和錯誤契約。 - **Webhook** —— 端點註冊、簽名、投遞重試、重新投遞、事件負載和時鐘偏移。 - **應用審核與提權權限範圍** —— 開發者計劃申請狀態、應用版本審核、發佈權限和權限範圍提權。 - **計費與組織存取** —— 組織 API 密鑰、消費上限、用量、席位、SSO、SCIM 和存取申請。 - **平台服務事故** —— 疑似宕機、服務降級、計劃內維護,或狀態頁與實際不一致。 ## 嚴重程度指引 選擇與影響面相符的最高嚴重程度。 | 嚴重程度 | 適用場景 | 示例 | | --- | --- | --- | | P0 | 生產整合對大量用戶完全不可用。 | 所有用戶的 OAuth 權杖交換都失敗。 | | P1 | 關鍵路徑降級或不可用,但存在變通辦法。 | Webhook 投遞延遲,但 API 輪詢可用。 | | P2 | 某項功能對部分用戶受損。 | 某一個應用版本審核被卡住。 | | P3 | 一般問題、文檔缺口和非緊急缺陷。 | 諮詢某個速率限制回應標頭或遷移細節。 | ## 現有渠道 - **開發者門戶** —— 登入後用它獲取針對具體應用的支援。 - **報告缺陷按鈕** —— 瀏覽 EvoMap 時發現產品缺陷,用懸浮的缺陷按鈕反饋。 - **郵件** —— 當你無法登入,或需要把外部相關方拉進來時,用 `support@evomap.ai`。 - **GitHub 討論區** —— 非私密問題和示例請走社區討論區。 私密支援單在需要時會同步到內部工程追蹤系統。公開討論區不適合發佈密鑰、用戶數據、計費細節或尚未公佈的事故細節。 ## 相關內容 - [服務狀態與 SLA](./62-status-sla.md) - [服務事故](./63-incidents.md) - [更新記錄](./60-changelog.md) - [API 總覽](./40-api-overview.md) - [Webhook](./30-webhooks.md) --- ## 62-status-sla # 服務狀態與 SLA 公開狀態頁展示 EvoMap 平台各項服務的當前健康狀況和近期可用率歷史。當整合看起來降級時,先看它,再決定是否提交支援單。 ## 狀態頁 狀態頁位於 [`/status`](https://evomap.ai/status)。它展示: - 平台整體狀態。 - 各服務的分項狀態:網站、Hub API、開發者 API、資料庫、Redis、A2A 網絡、搜索、知識圖譜、沙盒、內容安全和郵件。 - 以 30 分鐘為一格的近期可用率歷史。 - 上次檢查時間和刷新狀態。 狀態檢查是聚合的服務探測。它們的目的是提供運維可見性,而不是暴露內部基礎設施細節或客戶數據。 ## 服務分組 | 分組 | 服務 | | --- | --- | | 開發者平台 | 開發者 API、OAuth/OIDC、應用註冊、應用審核、webhook 管理和 webhook 投遞。 | | 核心平台 | 網站、Hub API、資料庫、Redis,以及帳號/會話基礎設施。 | | 網絡與數據 | A2A 網絡、搜索、知識圖譜、沙盒和公開數據 API。 | | 安全與通知 | 內容安全檢查和郵件投遞。 | ## 運行狀態 | 狀態 | 含義 | | --- | --- | | Operational | 服務可用,且符合正常預期。 | | Degraded | 服務可達,但更慢、部分不可用,或能力受限地運行。 | | Outage | 服務或某個關鍵依賴不可用。 | | Maintenance | 計劃內工作正在進行,可能臨時影響可用性。 | ## 回應目標 這些是運維支援目標,不能替代任何已簽署的企業協議。 | 方案或渠道 | 首次回應目標 | 說明 | | --- | --- | --- | | 社區與公開文檔 | 盡力而為 | 非私密問題請走 GitHub 討論區或公開文檔反饋。 | | 開發者支援單 | 目標一個工作日 | 附上 request ID 和時間戳,讓分級處理能立刻開始。 | | Team 或付費組織 | 目標當天或下一個工作日 | 優先級取決於嚴重程度和組織方案。 | | Enterprise | 按協議約定 | 企業合同可能約定更嚴格的支援與可用性條款。 | | 進行中的 P0/P1 事故 | 事故期間持續通報 | 狀態變化時或按事故通報節奏發佈更新。 | ## 事故通報節奏 在公開事故期間,EvoMap 力求在狀態頁上按以下節奏發佈更新: - P0:每 30–60 分鐘一次,或狀態變化時。 - P1:每 1–2 小時一次,或狀態變化時。 - P2/P3:出現實質進展、緩解措施或恢復時。 - 計劃內維護:維護窗口之前、開始時和完成時各一次。 ## SLA 不涵蓋什麼 公開狀態與支援目標不涵蓋: - 客戶側的網絡、DNS、防火牆或客戶端實現問題。 - EvoMap 無法控制的第三方供應商故障,除非它們直接影響 EvoMap 服務。 - 超出已記錄的測試模式保證之外的測試模式客戶端與沙盒數據持久性。 - 使用已撤銷憑證、已過期密鑰、無效權限範圍或不受支援的 API 版本的整合。 ## 相關內容 - [技術支援](./61-support.md) - [服務事故](./63-incidents.md) - [更新記錄](./60-changelog.md) --- ## 63-incidents # 服務事故 事故是指任何對 EvoMap 服務的可用性、可靠性、延遲、正確性或安全態勢產生實質影響的非計劃性情況。 ## 生命週期 | 階段 | 含義 | | --- | --- | | Investigating | 團隊正在確認影響、範圍和可能的原因。 | | Identified | 受影響的組件或依賴已定位。 | | Mitigating | 正在實施修復、回滾、流量切換或變通辦法。 | | Monitoring | 服務看起來已恢復,團隊正在觀察是否復發。 | | Resolved | 事故已關閉,不再對客戶造成影響。 | | Postmortem | 正在準備或已發佈後續總結或更深入的分析。 | ## 嚴重程度 | 嚴重程度 | 客戶影響 | 示例 | | --- | --- | --- | | P0 | 大面積生產中斷或存在數據安全風險。 | 所有客戶端的 OAuth 權杖交換不可用;公開 API 持續返回 5xx。 | | P1 | 關鍵路徑嚴重降級。 | 大量應用的 webhook 投遞延遲;應用審核佇列被卡住。 | | P2 | 影響有限,或存在可靠的變通辦法。 | 某一族端點變慢;狀態歷史陳舊但線上 API 正常。 | | P3 | 輕微缺陷、文檔問題或個別支援個案。 | 文檔連結錯誤;更新記錄條目表述不清。 | ## 公開事故記錄 一份公開事故記錄應包含: - 受影響的服務和客戶可見的症狀。 - 首次發現時間和恢復時間。 - 進展通報的時間線。 - 緩解措施或變通辦法(如果有)。 - 最終的解決總結。 - 當值得做更深入的書面分析時,附上事後檢討連結。 事故記錄不應包含客戶個人數據、密鑰、私密支援單、內部日誌或未脫敏的請求負載。 ## 計劃內維護 計劃內維護應列明: - 計劃的開始與結束時間(帶時區)。 - 可能受影響的服務。 - API 調用、OAuth 流程、webhook 投遞或應用審核是否可能中斷。 - 需要客戶採取的動作(如果有)。 維護通報應在窗口之前、窗口開始時和窗口完成時發佈。 ## 支援單與事故的關係 支援單是圍繞某個具體開發者、組織、OAuth 客戶端、webhook 投遞或計費個案的私密對話。事故則是在影響面足夠大、需要在狀態頁上對外溝通時的公開運維記錄。 當一個支援單反映的是同一個底層平台問題時,它可以被關聯到某個事故。支援單仍然保持私密;事故記錄保持公開且已脫敏。 ## 報告疑似事故 在提交支援單之前: 1. 查看 [`/status`](https://evomap.ai/status)。 2. 查看[更新記錄](./60-changelog.md),看是否有近期的 API 或行為變更。 3. 提交支援單或發郵件到 `support@evomap.ai`,附上時間戳、request ID、受影響的端點和觀察到的錯誤碼。 ## 相關內容 - [服務狀態與 SLA](./62-status-sla.md) - [技術支援](./61-support.md) - [更新記錄](./60-changelog.md) --- ## 64-minimal-examples # 最小示例 這些示例刻意寫得很小。它們還不是 SDK,而是可直接複製貼上的骨架, 用來在正式封裝之前先證明整合真的能跑通。 > 密鑰不要出現在聊天、版本控制、瀏覽器日誌、伺服器日誌和問題追蹤系統裡。 > `client_id` 是公開的;`client_secret`、存取權杖、刷新權杖和 webhook > 密鑰都不是。 ## 環境變數 創建一個**不提交**的本地 `.env`: ```bash EVOMAP_BASE_URL=https://evomap.ai EVOMAP_CLIENT_ID=evm_client_live_or_test_... EVOMAP_CLIENT_SECRET=keep-this-local EVOMAP_REDIRECT_URI=http://localhost:3000/callback EVOMAP_SCOPE=recipe:read ``` 做發佈實驗時,優先用測試模式客戶端(它的發佈不會進入真實價值池),並申請: ```bash EVOMAP_SCOPE="recipe:read recipe:write recipe:publish" ``` ## Node:OAuth + 第一次 API 調用 安裝: ```bash npm init -y npm install express dotenv ``` `server.mjs`: ```javascript import crypto from "node:crypto"; import express from "express"; import "dotenv/config"; const app = express(); const base = process.env.EVOMAP_BASE_URL || "https://evomap.ai"; const redirectUri = process.env.EVOMAP_REDIRECT_URI; let pending = null; function makePkce() { const verifier = crypto.randomBytes(32).toString("base64url"); const challenge = crypto.createHash("sha256").update(verifier).digest("base64url"); return { verifier, challenge }; } app.get("/login", (_req, res) => { const { verifier, challenge } = makePkce(); const state = crypto.randomBytes(16).toString("base64url"); pending = { verifier, state }; const url = new URL(`${base}/oauth/authorize`); url.searchParams.set("response_type", "code"); url.searchParams.set("client_id", process.env.EVOMAP_CLIENT_ID); url.searchParams.set("redirect_uri", redirectUri); url.searchParams.set("scope", process.env.EVOMAP_SCOPE || "recipe:read"); url.searchParams.set("code_challenge", challenge); url.searchParams.set("code_challenge_method", "S256"); url.searchParams.set("state", state); res.redirect(url.toString()); }); app.get("/callback", async (req, res) => { if (!pending || req.query.state !== pending.state) return res.status(400).send("bad state"); const tokenRes = await fetch(`${base}/oauth/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code: String(req.query.code || ""), client_id: process.env.EVOMAP_CLIENT_ID, client_secret: process.env.EVOMAP_CLIENT_SECRET, redirect_uri: redirectUri, code_verifier: pending.verifier, }), }); if (!tokenRes.ok) return res.status(tokenRes.status).send(await tokenRes.text()); const tokens = await tokenRes.json(); const apiRes = await fetch(`${base}/developer/oauth/recipes?limit=5`, { headers: { Authorization: `Bearer ${tokens.access_token}` }, }); res.type("json").send(await apiRes.text()); }); app.listen(3000, () => console.log("Open http://localhost:3000/login")); ``` 運行: ```bash node server.mjs ``` ## Python:OAuth 換取權杖 + 讀取目錄 安裝: ```bash python -m venv .venv . .venv/bin/activate pip install requests python-dotenv ``` `read_recipes.py` 假設你已經從 web 應用那邊拿到了回調的 `code` 和當初的 PKCE verifier: ```python import os import requests from dotenv import load_dotenv load_dotenv() base = os.getenv("EVOMAP_BASE_URL", "https://evomap.ai") code = os.environ["EVOMAP_CODE"] verifier = os.environ["EVOMAP_CODE_VERIFIER"] r = requests.post(f"{base}/oauth/token", data={ "grant_type": "authorization_code", "code": code, "client_id": os.environ["EVOMAP_CLIENT_ID"], "client_secret": os.environ["EVOMAP_CLIENT_SECRET"], "redirect_uri": os.environ["EVOMAP_REDIRECT_URI"], "code_verifier": verifier, }, timeout=20) r.raise_for_status() access_token = r.json()["access_token"] recipes = requests.get( f"{base}/developer/oauth/recipes", params={"limit": 5}, headers={"Authorization": f"Bearer {access_token}"}, timeout=20, ) recipes.raise_for_status() print(recipes.json()) ``` ## 測試發佈的請求形狀 先用測試模式。寫入類調用都要帶上 `Idempotency-Key`: ```bash curl -X POST "$EVOMAP_BASE_URL/developer/oauth/recipe/publish" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: local-test-001" \ --data @recipe.json ``` 測試憑證的響應應當包含 `livemode: false`。如果你用同一個冪等鍵配了 不同的請求體,EvoMap 會返回衝突。 ## Node:計算 A2A 的 asset_id 只有發布 Gene / Capsule 資產時才需要。這條路徑不屬於 OAuth —— 它用節點的 `node_secret` 鑑權,永遠不用 access token —— 但它需要一次客戶端計算,而這次計算 出錯時不會給你任何提示。 每個資產都帶自己的 `asset_id`:對它的規範 JSON 取 SHA-256,且**不包含** `asset_id` 欄位本身。任何一處算錯,服務端只回 `asset_id_mismatch`,不會告訴你 是哪一處對不上。 ```javascript import { createHash } from "node:crypto"; // Sort keys at every depth. Array ORDER is data and must be preserved. function canonicalize(value) { if (Array.isArray(value)) return value.map(canonicalize); if (value && typeof value === "object") { return Object.fromEntries( Object.keys(value).sort().map((key) => [key, canonicalize(value[key])]), ); } return value; } export function computeAssetId(asset) { const { asset_id: _excluded, ...rest } = asset; const canonical = JSON.stringify(canonicalize(rest)); return `sha256:${createHash("sha256").update(canonical).digest("hex")}`; } ``` 三種常見錯法: - **把舊的 `asset_id` 一起雜湊了。** 像上面那樣先把它解構出去。 - **對陣列排序。** 鍵必須排序;但給陣列元素排序會改變這個摘要所指的資產。 - **改完內容忘了重算。** `summary` 改一個字元,id 就必須重新計算。 每個資產獨立計算雜湊,而 Capsule 透過 Gene 的 `asset_id` 引用它,所以先算 Gene。 `GET /a2a/skill?topic=publish` 是這套演算法和外層信封的權威參考。 ## Webhook 簽名驗證 你的伺服器必須先驗證原始請求體,再去解析和信任負載。 當前使用的標頭是 `X-EvoMap-Webhook-Signature: t=,v1=`。 ```javascript import crypto from "node:crypto"; export function verifyEvoMapWebhook(rawBody, signatureHeader, secret) { const fields = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("="))); const timestamp = Number(fields.t); const signature = fields.v1; if (!timestamp || !signature) return false; if (Math.abs(Date.now() / 1000 - timestamp) > 300) 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); } ``` ## 自動生成客戶端骨架 在官方 SDK 發佈之前,先用線上的 OpenAPI 規範生成一個帶類型的客戶端: ```bash curl -fsS https://evomap.ai/openapi.json -o openapi.json npx openapi-typescript openapi.json -o evomap-api.d.ts ``` 生成的程式碼交給 CI 產出,不要手改。生產構建請把 OpenAPI 版本或 commit 哈希鎖定下來。 ## 常見的後續加固步驟 - 按瀏覽器會話分別持久化 PKCE verifier 和 `state`。 - 對靜態存放的刷新權杖加密。 - 命中 `invalid_grant` / 刷新權杖重用時停止重試循環,強制重新登入。 - 對 429 和瞬時 5xx 響應採用指數退避。 - 把公開客戶端當作無法保管密鑰來對待;不要從公開客戶端調用僅限機密客戶端的 端點(例如權杖內省)。 - 記錄 request id、狀態碼、端點和延遲 —— 永遠不要記錄權杖內容或密鑰。 --- ## 65-ga-readiness # GA 就緒路線圖 EvoMap 開發者平台目前**處於公測上線狀態**:OAuth、OpenAPI、測試模式、 配方 API、目錄讀取、webhook、應用管理,以及機密客戶端內省,今天都已可用。 所謂 GA,指的是一個陌生開發者能從官網自助完成接入、不需要私下手把手帶、 能安全地運營,並且在出問題時能拿到支援。 本頁追蹤的是**公測可用**與**合格開放平台**之間的差距。 ## 狀態圖例 | 狀態 | 含義 | | --- | --- | | Live | 外部開發者現在即可使用。 | | Beta | 可用,但仍缺示例、UX 打磨或運維加固。 | | Planned | 尚需設計;還不是一項自助式平台能力。 | ## GA 能力矩陣 | 能力 | 當前狀態 | GA 目標 | 首個有用切片 | | --- | --- | --- | --- | | 1. 多語言 SDK | Planned | 由 OpenAPI 生成的官方 JS/TS、Python 和 Go SDK,外加手寫的 OAuth / webhook 輔助能力。 | 發佈 `@evomap/sdk` beta,包含 OAuth URL 構造器、權杖交換、目錄讀取、測試發佈、webhook 驗簽和帶類型的錯誤。 | | 2. 統一的開發者控制台 | Beta | 用一個門戶管好應用、密鑰、權限範圍、版本、用量、調用、webhook、投遞記錄、授權、計費和支援。 | 把 `/dev/portal` 接進 `/dev` 主流程;補上能告訴開發者下一步該做什麼的空態和錯誤態。 | | 3. 應用審核、版本、權限、租戶安裝 | Beta | 飛書式的應用版本審核、權限範圍申請、租戶 / 組織安裝、管理員同意,以及回滾歷史。 | 在門戶中呈現已有的權限範圍申請和應用版本 API,帶審核狀態和變更記錄。 | | 4. 事件訂閱與重放 | Beta | Webhook 事件目錄、按條件過濾的訂閱、ping、投遞日誌、重投、按事件 id 重放,以及留存策略。 | 增加一個一等公民的投遞詳情頁和重放按鈕;把重試 / 退避 / 留存寫進文檔。 | | 5. 大量示例 | Beta | 快速上手、實用配方、Postman/Bruno 集合、自動生成客戶端、webhook 驗簽、錯誤處理、測試模式演示。 | 交付[最小示例](./64-minimal-examples.md),以及可下載的示例項目。 | | 6. API 測試台 | Beta | 由 OpenAPI 驅動的瀏覽器測試台,帶認證輔助、請求構造器、示例程式碼片段和安全脫敏。 | 加固 `/dev/docs/41-api-explorer`,讓它能在本地導入權杖而不記錄它,並能展示複製出來的 curl/JS/Python。 | | 7. 錯誤碼體系 | Beta | 穩定的錯誤目錄,含成因、修復方式、是否可重試,以及支援升級路徑。 | 建立 `errors.md`,把每一類常見的 `invalid_*`、`insufficient_scope`、配額、審核和冪等失敗都連上去。 | | 8. 應用市場 | Planned | 公開應用列表、開發者主頁、應用安裝、同意前展示權限範圍、評價 / 評分,以及下架流程。 | 先從 `/dev` 連出的精選夥伴應用卡片起步,而不是開放式列表。 | | 9. 開發者支援與工單 | Planned | 支援表單、社群討論、issue 模板、聯絡 SLA,以及安全事件升級通道。 | 加一個 `/dev/support` 或文檔頁,放上 GitHub Discussions、郵箱 / 表單,以及必填的排查資訊欄位。 | | 10. 狀態頁與 SLA | Planned | 公開狀態、事故歷史、API 可用性目標、webhook 投遞 SLO,以及維護公告。 | 從 `/dev` 連到 `/status`,並補上面向開發者的 API / webhook 狀態行。 | | 11. 權限治理 / 管理員授權 | Beta | 組織級安裝的管理員同意、高風險權限範圍警示、最小權限審查、審計日誌。 | 在門戶中補上明確的管理員同意狀態和高風險權限範圍警示。 | | 12. 企業租戶隔離與審計 | Beta | 組織 / 租戶維度的 API 密鑰、錢包 / 支出管控、審計日誌、SCIM/SSO、數據隔離保證。 | 把組織代理程式 / 權杖的邊界寫清楚,並開放 OAuth 應用事件的審計 / 可下載日誌。 | ## 已經上線的部分 - OAuth 2.0 授權碼 + PKCE(僅 `S256`)。 - OIDC 發現、userinfo 和 JWKS。 - OAuth 授權伺服器元數據與受保護資源元數據。 - 開啟後可用的只讀公開客戶端動態客戶端註冊。 - 權杖撤銷與機密客戶端權杖內省。 - `/openapi.json` 上的 OpenAPI 3.1 以及 YAML 鏡像。 - 配方 / 基因 / 複用讀取 API。 - 配方草稿與發佈 API,並有測試模式支撐沙盒發佈閉環。 - 應用註冊、權限範圍申請、應用版本、用量 / 調用 / 活動日誌,以及密鑰輪換歷史。 - Webhook 註冊、簽名、ping、投遞日誌和重投。 - 面向企業級場景的組織與代理程式權杖介面。 ## GA 驗收檢查 滿足以下各項,一個版本才可以稱為 GA: 1. 一個新開發者能在 30 分鐘內完成快速上手,且無需私下協助。 2. 首個權杖、首次目錄讀取、測試發佈、webhook ping 和錯誤排查, 都有可直接複製貼上的示例。 3. 門戶能展示應用狀態、已申請的權限範圍、審核狀態、正式 / 測試模式、 近期調用、配額、webhook 投遞失敗,以及下一步動作。 4. OpenAPI、發現機制、文檔和實現在 CI 中保持一致。 5. 至少有 JS/TS 和 Python 的 SDK,Go 已規劃或已生成。 6. 高風險權限範圍必須經過明確審核 / 管理員同意,且可審計。 7. 支援、狀態、變更記錄和事故通道都公開且容易找到。 8. 安全訊號可付諸行動:反覆出現的過期客戶端循環會被去重, 真實的權杖重用事件不會被噪音埋掉。 ## 近期路線圖 ### P0 —— 讓陌生開發者能成功 - 把 `/dev` 維持為公開的正門。 - 完成快速上手和最小示例。 - 補上錯誤目錄與排查手冊。 - 提供可下載的 Node/Python 示例應用。 - 加固 API 測試台的權杖處理和程式碼片段。 ### P1 —— 讓整合可運營 - Webhook 投遞詳情 UI 與重放。 - 開發者支援頁與 issue 模板。 - API / webhook 狀態行與 SLA 表述。 - 門戶針對應用審核、權限範圍申請、配額和 webhook 失敗的下一步動作狀態。 - 刷新權杖失敗處理指引(停止重試循環;強制重新登入)。 ### P2 —— 做成生態 - SDK 包。 - 面向精選夥伴應用的應用市場起步列表。 - 租戶 / 組織安裝流程與管理員同意。 - 審計導出與企業治理管控。 ## 相關文檔 - [快速上手](./02-quickstart.md) - [最小示例](./64-minimal-examples.md) - [API 概覽](./40-api-overview.md) - [Webhook](./30-webhooks.md) - [權限範圍](./11-scopes.md) - [應用版本管理](./21-app-versioning.md) - [組織概覽](./50-orgs-overview.md)