跳到正文

🧪 Beta — 能力已可用,接口契约可能微调。

第一次委托:30 分钟跑通 ​

读完本指南,你将拥有:

  • 一枚由用户亲自授权签发的委托令牌(Delegate Token)——不是密码,不是长期 API Key,是一份短时、缩权、可撤销的授权切片
  • 一次完整的验证与产品调用:确认令牌有效,并通过 SDK 调用 Web Search
  • 一条可追溯的审计链 auditId——谁授权、授权了什么、多长时间,全程在案

开始前:两件需要先对齐的事

① 端点里为什么是 eak 而不是 genauth:GenAuth 是 Qoni 的身份层,当前 SDK 包是 @qoniai/qoni;服务端 /api/v3/eak/... 路径继续作为 wire 兼容边界保留(详见 GenAuth 是什么)。

② 本文用什么当"被访问的资源":为了让你 30 分钟内跑通完整链路,示例访问 Qoni Web Search(scope 为 webagent.web_search:read 和 webagent.web_search:manage),不需要先改造自己的 API。

Step 0 —— 拿到访问密钥(3 分钟) ​

  1. 在 dashboard.qoni.ai 进入你的工作空间
  2. Credentials → Create,配置两个白名单:
    • allowedScopes:这把密钥允许发起委托的 scope 上限。本教程要用 webagent.web_search:manage 和 webagent.web_search:read,至少要包含这两个——配得比后面请求的更小会直接被拒。
    • allowedAgents:允许委托的 Agent 标识。本教程用 report-agent,把它填进去。
  3. 复制 AccessKey / SecretKey。**Secret 只显示一次。**丢了只能轮换重建
  4. 在同一处登记回调地址(redirectUri),本教程用 http://localhost:3000/qoni/callback
bash
export QONI_ACCESS_KEY=ak_demo_xxxxxxxxxxxxxxxx
export QONI_SECRET_KEY=sk_demo_xxxxxxxxxxxxxxxx

密钥只属于服务端

AK/SK 绝不进浏览器、App 或任何客户端代码。它能替你的组织发起委托——保管要求见安全考量。

Agent 标识(agent)从哪来

本教程直接用字符串 report-agent 跑通,前提是它在上一步的 allowedAgents 里。生产环境里 Agent 应当先注册进台账(有说明、有技术负责人与业务归属人),标识与注册记录对应——见 Agent 的身份模型。

Step 1 —— 装 SDK(30 秒) ​

bash
npm install @qoniai/qoni
bash
# 不用装。但 HTTP 直连需要自己构造 AK/SK 签名的 Authorization 头,
# 本教程的 cURL 片段用 <AK/SK 签名> 作占位。
# 想直连:用 SDK 导出的 buildStringToSign / buildSignature / buildAuthorization
# 三个函数生成,或先用 SDK 跑通再迁移。见 h2-sdk-reference 与 h1-api-reference。

cURL 片段是契约示意,不是可直接粘贴运行的命令

签名算法未在本页展开。要在 30 分钟内跑通,请走 TypeScript 路径,cURL 片段用来对照 HTTP 契约。

Step 2 —— 发起委托,拿到授权链接(8 分钟) ​

interactive 模式:你替 Agent 提出请求,用户说了才算。

typescript
import { Qoni, QoniScopes } from "@qoniai/qoni";

const qoni = new Qoni({
  accessKey: process.env.QONI_ACCESS_KEY!,
  secretKey: process.env.QONI_SECRET_KEY!,
});

const { data } = await qoni.delegateToken({
  mode: "interactive",
  agent: "report-agent",
  scopes: [QoniScopes.WEB_SEARCH_MANAGE, QoniScopes.WEB_SEARCH_READ],
  redirectUri: "http://localhost:3000/qoni/callback",
  state: "demo-state-001",
});

const authorizationUrl = new URL(data.authorizationUrl);
if (authorizationUrl.searchParams.get("grant_id") !== data.grantId) {
  throw new Error("Invalid delegation grant");
}
// 回调不会带回 grantId:按 grantState 保存,回调时用 grant_state 找回
await savePendingGrant(data.grantState, {
  grantId: data.grantId,
  businessState: "demo-state-001",
});

console.log(data.authorizationUrl); // 把用户带到这里
console.log(data.grantId);          // 已随 grantState 保存,兑换时要用
bash
curl -X POST "$QONI_HOST/api/v3/eak/delegations" \
  -H "Authorization: <AK/SK 签名>" \
  -H "Content-Type: application/json" \
  -d '{
    "mode": "interactive",
    "agent": "report-agent",
    "scopes": ["webagent.web_search:manage", "webagent.web_search:read"],
    "redirectUri": "http://localhost:3000/qoni/callback",
    "state": "demo-state-001"
  }'

响应里的 authorizationUrl 就是授权确认页入口。响应 state 是服务端生成 grantState 的兼容别名,不会回显调用方传入值;稳定关联契约是 URL 查询参数 grant_id 等于响应 grantId。scope 用 服务.能力:动作 格式——本例只要了 Web 搜索的执行与读取,没有别的。

两个容易卡住的地方

redirectUri 必须与 Step 0 登记的一致,否则请求会被拒绝。本地开发用 http://localhost:3000/... 即可,不需要公网 https。

interactive 不传 user:Qoni Console 会在授权过程中识别当前登录用户。只有 silent 模式才必须传 user: { id: <genauth-user-id> }。

两种写法的对应关系

silent 模式的 SDK 写法是 user: { id },HTTP 契约会转换为 userId。SDK 顶层 userId 已弃用;interactive 模式两者都不传(见 SDK Reference)。

Step 3 —— 用户点同意,换取委托令牌(10 分钟) ​

用户打开 authorizationUrl,登录后看到授权确认页:哪个 Agent、哪些权限、多长时间。点同意后,浏览器带着一次性 code、你的业务 state 和 grant_state 回跳到你的 redirectUri。回调不带 grantId,要按 grant_state 找回创建时保存的记录。在回调处理里完成兑换:

typescript
// 你的回调路由(下例是 Express;其他框架取 query 的方式类似)
// GET /qoni/callback?code=...&state=<业务 state>&grant_state=...(回调不带 grantId)
app.get("/qoni/callback", async (req, res) => {
  const query = req.query as Record<string, string>;
  // 按 grant_state 找回创建请求时保存的记录;找不到说明回调无效或已用过。
  const pending = await loadPendingGrant(query.grant_state);
  // 业务 state 原样回到回调里,和服务端保存的值比对,防止回调被伪造或串用。
  if (!pending || query.state !== pending.businessState) return res.status(400).send("state mismatch");

  const { data: grant } = await qoni.completeDelegateToken({
    grantId: pending.grantId,
    code: query.code,
    state: query.grant_state,
  });

  console.log(grant.token);         // 委托令牌
  console.log(grant.expiresIn);     // 有效期(秒)
  console.log(grant.auditId);       // 审计链 ID,从此每一步都在案

  res.send("已授权");
});
bash
curl -X POST "$QONI_HOST/api/v3/eak/delegations/complete" \
  -H "Authorization: <AK/SK 签名>" \
  -H "Content-Type: application/json" \
  -d '{ "grantId": "grant_xxx", "code": "code_xxx", "state": "grant_state_xxx" }'

code 是一次性的

授权回调 code 消费一次即失效,重放直接失败。拿到的 token 才是接下来要用的东西。

Step 4 —— 验证并调用 Web Search(8 分钟) ​

先确认这枚令牌真实有效——introspect 会回显全部授权事实:

typescript
const { data: info } = await qoni.genauth.introspectDelegationToken({
  token: grant.token,
});
// { active: true, sub: "usr_demo_0001", agent_id: "report-agent",
//   scope: ["webagent.web_search:manage", ...], grant_id, audit_id, ... }

为什么 introspect 的返回是下划线命名

introspect 直接回显的是令牌里的 claims,使用 agent_id、audit_id 和 scope。委托响应中的审计字段使用驼峰 auditId,但不包含实际 scope 列表;需要实际授予范围时,以 introspection 的 scope 为准(完整字段表见 Token 与 Claim 参考)。

产品调用直接使用 grant.token。SDK 会在内部完成产品 token exchange,不要在应用代码中手动调用 /api/v3/eak/token-exchange:

ts
const search = await qoni.webSearch.run({
  token: grant.token,
  prompt: 'Qoni Agent Identity documentation',
  maxResultsPerQuery: 5,
})

const result = await search.wait()
console.log(result.output)

然后呢 ​

  • 令牌会过期:expiresIn 到点即失效(60 秒到 24 小时,按你的请求)。任务级委托建议给短期限。
  • 随时可撤:吊销的分层与时效见吊销与应急处置。
  • 全程可查:拿着 auditId 去看审计与追责链。

服务端受信集成(免用户逐次确认)

你的应用已有登录体系、想以组织名义直接为用户签发?走组织级授权确认的 silent 路径——先读接入你已有的认证体系与安全考量的硬约束清单。

下一步 ​