AgnesCode OAuth & Deep Link 协议分析报告
协议端点: App ↔ System Browser ↔ Agnes Auth Server
通信方式: Deep Link (agnes://) + HTTPS
报告日期: 2026-07-25
分析版本: v1.0.17
相关文档:
1. 协议概述
AgnesCode 使用 OAuth2 变体 进行用户认证, 通过 Deep Link (agnes://) 协议在浏览器和桌面应用之间传递授权码。
mermaid
graph LR
subgraph "桌面"
App["Desktop App"]
end
subgraph "浏览器"
Browser["Browser (User)"]
end
subgraph "服务器"
Auth["Auth Server (OAuth)"]
end
App -->|"shell.openExternal"| Browser
Browser -->|"HTTPS"| Auth
Auth -->|"HTTPS"| Browser
Browser -->|"agnes://auth/callback"| App2. 完整认证流程
2.1 步骤详解
mermaid
sequenceDiagram
participant App as Desktop App
participant Browser as Web Browser (User)
participant Auth as Agnes Auth Server
App->>App: 1. 生成 state (32 bytes hex)
App->>App: 2. 构造登录 URL
App->>Browser: 3. shell.openExternal
Browser->>Auth: 4. 用户在浏览器登录
Auth->>Browser: 302 Redirect agnes://auth/callback?code={auth_code}&state={state}
Browser->>App: 5. macOS: open-url / Windows: deep link
App->>App: 6. 验证 state 匹配
App->>App: 7. IPC: auth-deeplink {code, state} → Renderer
App->>Auth: 8. Renderer 交换 code POST /api/v1/code/auth/exchange-code
Auth->>App: { access_token, user_info }
App->>App: 9. 保存 token (localStorage + Keychain)2.2 关键代码
登录 URL 构建 (main.js):
javascript
function Oj(state) {
const loginUrl = process.env.AGNES_SA_WEB_LOGIN_URL?.trim();
// 生产: https://app.agnes-ai.com/
const url = new URL("/login", loginUrl);
url.searchParams.set("client", "agnes-code");
url.searchParams.set("redirect_uri", "agnes://auth/callback");
url.searchParams.set("state", state);
return url.toString();
}State 管理 (main.js):
javascript
// 生成 32 字节随机 state
const state = crypto.randomBytes(32).toString('hex');
// 保存 state 与窗口的映射
const pendingAuth = { state, windowId: browserWindow.id };Deep Link 回调处理 (main.js):
javascript
async function bs(event, url, targetWindow) {
if (url.hostname === "auth" && url.pathname === "/callback") {
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
// 验证 state 匹配
if (!pendingAuth || pendingAuth.state !== state) {
console.warn("State mismatch");
return;
}
// 发送到渲染进程
targetWindow.webContents.send("auth-deeplink", { code, state });
}
}Code 交换 (renderer):
javascript
async function exchangeCode({ code, state }) {
const url = `${AGNES_API_URL}/api/v1/code/auth/exchange-code`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
redirect_uri: 'agnes://auth/callback',
state,
client_id: 'agnes-code'
})
});
const data = await response.json();
if (data.code !== 0) throw new Error(data.message);
return data.data; // { access_token, user_info }
}Token 保存 (renderer):
javascript
function saveAuth(data) {
localStorage.setItem("token", data.access_token);
localStorage.setItem("userinfo", JSON.stringify(data.user_info));
window.dispatchEvent(new CustomEvent('agnes:userinfo-updated'));
// 同步到密钥链
window.electron.setSetting('AGNES_AI_API_KEY', data.access_token).catch(console.warn);
}3. Deep Link 协议 (agnes://)
3.1 协议注册
- macOS: 通过
Info.plist中的CFBundleURLTypes注册 - Windows: 通过安装程序注册自定义协议
- Bundle ID:
com.agnes.code
3.2 URL 类型
| URL 格式 | 描述 | 处理位置 |
|---|---|---|
agnes://auth/callback?code=X&state=Y | OAuth 回调 | 主进程 → Renderer |
agnes://subscription?status=success | 订阅成功 | 主进程 |
agnes://subscription/success | 订阅成功 (路径) | 主进程 |
agnes://new-session?prompt=HELLO | 创建新会话 | 主进程 |
agnes://resume/{sessionId} | 恢复会话 | 主进程 |
agnes://extension?name=... | 安装扩展 | 主进程 → Renderer |
agnes://sessions/... | 打开共享会话 | 主进程 → Renderer |
agnes://bot?param=... | 打开 Bot/Recipe | 主进程 |
agnes://recipe?param=... | 打开 Recipe | 主进程 |
3.3 macOS 事件处理
javascript
app.on("open-url", async (event, url) => {
const parsed = new URL(url);
// 根据 hostname 分发处理
switch (parsed.hostname) {
case "auth":
if (parsed.pathname === "/callback") {
await handleAuthCallback(url, parsed);
}
break;
case "new-session":
await createNewSession(parsed.searchParams.get("prompt"));
break;
case "resume":
await resumeSession(parsed.pathname);
break;
case "subscription":
if (isSubscriptionSuccess(parsed)) {
await handleSubscriptionSuccess(url);
}
break;
case "extension":
// 发送到渲染进程
targetWindow.webContents.send("add-extension", url);
break;
case "sessions":
targetWindow.webContents.send("open-shared-session", url);
break;
}
});4. 安全机制
4.1 CSRF 防护
- 使用 32 字节 (256 位) 随机 state
- state 在服务端无状态, 仅存储在内存中
- 回调时验证 state 精确匹配
- state 使用后立即清除
4.2 超时处理
javascript
// 登录超时 60 秒
const TIMEOUT = 60000;
const timeoutId = setTimeout(() => {
console.warn("Login timed out");
// 清除 state
pendingAuth = null;
}, TIMEOUT);4.3 防重放
javascript
// 防止重复处理同一个 code
const processedKeys = new Set();
const key = `${state}:${code}`;
if (processedKeys.has(key)) return; // 已处理
processedKeys.add(key);5. API 端点
5.1 认证端点
| 端点 | 方法 | 描述 |
|---|---|---|
{AGNES_API_URL}/api/v1/code/auth/exchange-code | POST | 交换授权码获取 Token |
5.2 请求格式
POST /api/v1/code/auth/exchange-code
Content-Type: application/json
{
"code": "auth_code",
"redirect_uri": "agnes://auth/callback",
"state": "32_byte_hex",
"client_id": "agnes-code"
}5.3 响应格式
json
{
"code": 0,
"data": {
"access_token": "jwt_token",
"user_info": {
"id": "user_id",
"name": "User Name"
}
}
}6. 环境变量
| 变量 | 用途 | 默认值 |
|---|---|---|
AGNES_SA_WEB_LOGIN_URL | 登录页面地址 | https://app.agnes-ai.com/ |
AGNES_API_URL | API 地址 | https://api-agnes-code.agnes-ai.com/v1 |
AGNES_EXTERNAL_BACKEND | 外部后端地址 | 无 |
7. 流程图
mermaid
graph TD
Start["用户点击登录"] --> GenState["生成 32B state"]
GenState --> BuildURL["构造登录 URL<br/>{login_url}/login?client=agnes-code&redirect_uri=agnes://auth/callback&state={state}"]
BuildURL --> OpenBrowser["shell.openExternal(url)<br/>→ 系统浏览器打开登录页"]
OpenBrowser --> UserLogin["用户输入账号密码完成登录"]
UserLogin --> Redirect["浏览器重定向到<br/>agnes://auth/callback?code={code}&state={state}"]
Redirect --> OpenApp["操作系统唤起 Electron 应用"]
OpenApp --> OnOpenUrl["app.on('open-url') 事件"]
OnOpenUrl --> VerifyState{"验证 state 匹配"}
VerifyState -->|"不匹配"| Reject["拒绝"]
VerifyState -->|"匹配"| IPC["IPC 发送到渲染进程"]
IPC --> Exchange["POST /api/v1/code/auth/exchange-code"]
Exchange --> GetToken["获取 access_token + user_info"]
GetToken --> SaveToken["保存到 localStorage + 密钥链"]
SaveToken --> Done["登录完成"]