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://) 协议在浏览器和桌面应用之间传递授权码。
┌──────────┐ shell.openExternal ┌──────────┐ HTTPS ┌──────────────┐
│ Desktop │───────────────────────────►│ Browser │──────────────►│ Auth Server │
│ App │ │ (User) │◄──────────────│ (OAuth) │
│ │◄───────────────────────────│ │ └──────────────┘
│ │ agnes://auth/callback │ │
└──────────┘ └──────────┘2. 完整认证流程
2.1 步骤详解
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Desktop │ │ Web Browser │ │ Agnes Auth │
│ App │ │ (User) │ │ Server │
└────┬─────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ 1. 生成 state │ │
│ (32 bytes hex) │ │
│ │ │
│ 2. 构造登录 URL │ │
│ {AGNES_SA_WEB_LOGIN_URL}/login │
│ ?client=agnes-code │
│ &redirect_uri=agnes://auth/callback │
│ &state={state} │
│ │ │
│ 3. shell.openExternal│ │
├──────────────────────► │
│ │ │
│ 4. 用户在浏览器登录 │ │
│ ├──── login ────────────►│
│ │◄─── 302 Redirect ─────┤
│ │ agnes://auth/callback│
│ │ ?code={auth_code} │
│ │ &state={state} │
│ │ │
│ 5. macOS: open-url │ │
│ Windows: deep link│ │
│◄──────────────────────┤ │
│ │ │
│ 6. 验证 state 匹配 │ │
│ │ │
│ 7. IPC: auth-deeplink│ │
│ {code, state} │ │
│ → Renderer │ │
│ │ │
│ 8. Renderer 交换 code│ │
│ POST /api/v1/code │ │
│ /auth/exchange-code│ │
├───────────────────────────────────────────────►│
│◄───────────────────────────────────────────────┤
│ {access_token, user_info} │
│ │ │
│ 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. 流程图
用户点击"登录"
│
▼
生成 32B state
│
▼
构造 URL: {login_url}/login?client=agnes-code&redirect_uri=agnes://auth/callback&state={state}
│
▼
shell.openExternal(url) → 系统浏览器打开登录页
│
▼
用户输入账号密码完成登录
│
▼
浏览器重定向到 agnes://auth/callback?code={code}&state={state}
│
▼
操作系统唤起 Electron 应用
│
▼
app.on('open-url') 事件
│
▼
验证 state 匹配 → 不匹配 → 拒绝
│
匹配
▼
IPC 发送到渲染进程
│
▼
POST /api/v1/code/auth/exchange-code
│
▼
获取 access_token + user_info
│
▼
保存到 localStorage + 密钥链
│
▼
登录完成