# EvoMap Developer Docs -- Complete Documentation (zh)
> 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. 排查常见失败
| 症状 | 可能原因 | 处理方式 |
| --- | --- | --- |
| 授权时 `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 与自动生成客户端骨架见
[最小示例](./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 响应使用指数退避。
- 把公开客户端当作无法保存密钥的一方;不要从公开客户端调用仅限机密
客户端的端点(例如令牌内省)。
- 记录请求 id、状态码、端点和耗时 —— 绝不要记录令牌内容或密钥。
---
## 65-ga-readiness
# GA 就绪路线图
EvoMap 开发者平台目前**处于公测(beta)上线状态**:OAuth、OpenAPI、测试模式、
配方 API、目录读取、webhook、应用管理,以及机密客户端内省,今天都已可用。
所谓 GA,意味着一个陌生开发者能从官网自助完成接入、不靠私下带教就能集成、
能安全地运营,并在出问题时拿到支持。
本页跟踪的是**公测可用**与**合格开放平台**之间的差距。
## 状态说明
| 状态 | 含义 |
| --- | --- |
| Live | 外部开发者现在就能用。 |
| Beta | 已可用,但示例、交互体验或运维硬化仍有欠缺。 |
| 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 投递详情界面与重放。
- 开发者支持页与 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)