数据截至 (上游 commit 9fe68b96f0a7)
Composio — 鉴权与连接账户
本章讲什么: Composio 怎么替你管「每个用户对每个第三方服务的登录态」。三层模型(authConfig / connectionRequest / connectedAccount)各是什么,OAuth 授权怎么发起、怎么轮询等待完成,执行工具时凭据怎么自动挂上。想接真实有鉴权的 toolkit(几乎所有都有)必读。
1. 它要解决的小问题
你的 agent 要替 alice@acme.org 读她的 Gmail。Gmail 要 OAuth:Alice 得去 Google 点「同意授权」,你的服务才能拿到她的 token。而且:
- token 会过期,要刷新;
- 每个用户一套 token,要按用户存;
- 不同服务鉴权方式不同(OAuth2 / API Key / Bearer / Basic)。
Composio 把这一坨全托管。你的代码只处理「发起授权 → 等用户点完 → 之后正常执行工具」,token 的存储/刷新/挂载都在它后端。
2. 三层模型(先建立心智)
这是最容易绕晕的地方,先用一张图把三个概念的关系钉死。
怎么读这张图
从左到右是「配置 → 一次授权 → 长期连接」的时间顺序。authConfig 是复用的模板(整个 app 配一次);connectionRequest 是一次性的授权流程;connectedAccount 是授权成功后长期存在的连接。
┌──────────────┐ 定义「用哪套 OAuth app /
│ authConfig │ 哪种鉴权方式」(每个 toolkit 配一次)
└──────┬───────┘
│ 基于它,为某个 userId 发起授权
▼
┌──────────────────┐ 一次性流程:返回 redirectUrl,
│ connectionRequest │ 用户去点同意;可 waitForConnection() 轮询
└──────┬───────────┘
│ 用户授权完成 → status: ACTIVE
▼
┌──────────────────┐ 长期存在:userId × toolkit 的已连接凭据
│ connectedAccount │ 执行工具时后端自动按它挑 token
└──────────────────┘
一句话各自职责
| 概念 | 是什么 | 类比 |
|---|---|---|
authConfig | 「某 toolkit 用哪套 OAuth app + 哪种鉴权 scheme」的模板 | 「Google 登录按钮」的配置 |
connectionRequest | 一次具体的授权流程(有 redirectUrl,可等待) | 用户正在「同意授权」的那个页面会话 |
connectedAccount | 授权成功后的长期连接(带 status) | 已绑定的「Alice 的 Gmail 账户」 |
3. 鉴权 scheme:不止 OAuth
AuthScheme 这个工具类列出了支持的鉴权方式,各有一个工厂方法(models/AuthScheme.ts:8-120):
| scheme | 工厂方法 | 用于 |
|---|---|---|
| OAuth2 | AuthScheme.OAuth2(...) | Gmail、GitHub、Slack 等主流 |
| API Key | AuthScheme.APIKey(...) | 很多 SaaS 的 key 鉴权 |
| Bearer Token | AuthScheme.BearerToken(...) | 直接给 token 的服务 |
OAuth2 走「跳转授权」流程(下一节);API Key / Bearer 这类「你直接有凭据」的不需要跳转,创建连接时直接带上凭据即可。
4. 主线:发起 OAuth 并等待完成
端到端流程
composio.connectedAccounts.initiate(userId, authConfigId) // 或 .link(...)
│
▼
返回 connectionRequest:{ id, status: INITIATED, redirectUrl }
│
把 redirectUrl 给用户 ──▶ 用户在浏览器点「同意」
│
你的代码:await connectionRequest.waitForConnection(timeout)
│ 每 1s 轮询后端 connectedAccounts.retrieve(id)
▼
status 变成 ACTIVE → 返回完整的 connectedAccount
(或 FAILED/EXPIRED/REVOKED → 抛错;超时 → 抛 timeout 错)
waitForConnection 的轮询逻辑
核心是个「先查一次,没 active 就每秒轮询直到超时」的循环(models/ConnectionRequest.ts:75-135)。两个设计细节值得看:
(1) 终态错误立即抛,不傻等。FAILED/EXPIRED/REVOKED 是终态,撞上就直接抛 ConnectionRequestFailedError,不浪费剩余 timeout(models/ConnectionRequest.ts:75-97):
const terminalErrorStates = [
ConnectedAccountStatuses.FAILED,
ConnectedAccountStatuses.EXPIRED,
ConnectedAccountStatuses.REVOKED,
];
const failIfTerminal = (response) => {
if (terminalErrorStates.includes(response.status)) {
throw new ConnectionRequestFailedError(
`Connection request failed with status: ${response.status}...`, {...});
}
};
(2) 默认超时 60s,轮询间隔 1s(models/ConnectionRequest.ts:123-127)。这是个朴素的轮询,不是 webhook 推送——简单可靠,代价是最坏多等 1s。
原理演示(简化的端到端)
// 示意,非源码:让某用户连上 GitHub
const req = await composio.connectedAccounts.initiate('alice@acme.org', githubAuthConfigId);
console.log('请去这里授权:', req.redirectUrl); // 把链接发给用户
const account = await req.waitForConnection(120_000); // 阻塞等最多 2 分钟
console.log('已连接!account id =', account.id);
// 之后正常执行工具,后端会自动用这个 connectedAccount 的凭据
await composio.tools.execute('GITHUB_CREATE_ISSUE',
{ userId: 'alice@acme.org', arguments: {...}, dangerouslySkipVersionCheck: true });
重点看:execute 时你不传任何 token——只传 userId。后端按 userId × toolkit 找到对应 connectedAccount,自己挂凭据。这就是「托管鉴权」的核心体验。