
字节笔记本
2026年10月24日 · 约 12 分钟读完
ChatGPT OAuth:让订阅额度跑自建 Agent

本文是《从 0 写 Agent》系列第 39 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「ChatGPT OAuth:用订阅额度跑 Agent」。
agents-server 有个特别的能力,用 ChatGPT 订阅账号跑 Agent,不烧 API 额度。这靠 OAuth 2.1 + PKCE 实现。
这一章拆这个 OAuth 流程,本地回调服务器、postMessage 弹窗通信、token 自动刷新。这是 Part VI 的收尾。
引子:为什么用 ChatGPT 订阅
OpenAI 有两种计费:
- API,按 token 付费,每百万 token 几美元
- ChatGPT 订阅(Plus/Pro/Max),月费固定,含一定使用额度
如果你有 ChatGPT Pro 订阅(月费固定),用它跑 Agent 比 API 便宜得多,尤其对重度使用者。
agents-server 的 ChatGPT OAuth 功能,就是让你的 Agent 用 ChatGPT 订阅额度,而不是 API key。它复用了 Codex CLI 的 OAuth 流程(同样的 client_id)。

OAuth 2.1 + PKCE 流程
// bridge/chatgpt_oauth.go:21
const (
clientID = "app_EMoamEEZ73f0CkXaXp7hrann" // Codex CLI 的公开 client_id
authorizeURL = "https://auth.openai.com/authorize"
tokenURL = "https://auth.openai.com/token"
redirectURI = "http://localhost:1455/auth/callback"
)完整流程(chatgpt_oauth.go:63):
1. 前端点"用 ChatGPT 登录"
2. 后端 StartLogin:
a. 生成 state(32 字节随机)和 PKCE verifier(64 字节)
b. 算 challenge = base64(SHA256(verifier))
c. 起一个临时 HTTP 服务器监听 localhost:1455(回调)
d. 拼 authorize URL(含 challenge、state)
e. 返回 URL 给前端
3. 前端弹窗打开 authorize URL
4. 用户在弹窗里登录 OpenAI 并授权
5. OpenAI 重定向到 localhost:1455/auth/callback?code=xxx&state=xxx
6. 临时服务器接收回调:
a. 校验 state(防 CSRF)
b. 用 code + verifier 换 token(token endpoint)
c. 存 token 到数据库
d. 返回 HTML,用 postMessage 通知弹窗关闭
7. 弹窗关闭,前端知道登录完成关键设计一:本地回调服务器
// chatgpt_oauth.go:119(概念)
func listenCallback(redirectURI) {
// 在 localhost:1455 起一个临时 HTTP 服务器
// (端口被占则退 1457)
server := &http.Server{Addr: ":1455", Handler: mux}
go server.ListenAndServe()
}为什么起本地服务器当回调? 因为 OAuth 回调必须是 http://(重定向到 localhost)。不能回调到你的 Agent 平台(那要公网域名 + HTTPS)。
临时服务器在登录期间存在,登录完就关。 不影响主 gin server(在 8080 端口)。这是"按需启动"的精简设计。
关键设计二:postMessage 弹窗通信
// chatgpt_oauth.go:389(概念)
func callbackHTML(status string) string {
return `<script>
window.opener.postMessage({type: 'chatgpt-oauth-done', status: '` + status + `'}, '*');
window.close();
</script>`
}OAuth 回调发生在弹窗里(前端用 window.open 打开的)。 弹窗完成授权后,要通知"父窗口"(主界面)登录完了。
window.opener.postMessage,弹窗给打开它的窗口发消息。父窗口监听这个消息,知道"登录完成",关闭弹窗,刷新登录状态。
这是浏览器里"弹窗 ↔ 主窗口"通信的标准方式。 比"轮询后端状态"简单。
关键设计三:PKCE 防授权码拦截
// chatgpt_oauth.go:384
func pkceChallenge(verifier string) string {
hash := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(hash[:])
}PKCE 流程:
- 后端生成随机
verifier(64 字节) - 算
challenge = base64(SHA256(verifier)) - 授权时发
challenge(不给 verifier) - 换 token 时发
verifier
即使攻击者截获了授权码(callback URL 里的 code),他没有 verifier,换不到 token。 因为换 token 时要同时提供 code + verifier,OpenAI 会验证 SHA256(verifier) == challenge。
PKCE 是 OAuth 2.1 的强制要求,所有公共客户端(不能安全存 secret 的,如桌面/移动应用)必须用 PKCE。
Token 自动刷新
// chatgpt_oauth.go:217(概念)
func GetCredentials(agentConfigID) {
token := loadFromDB(agentConfigID)
// 过期前 60 秒自动 refresh
if time.Until(token.ExpiresAt) < 60*time.Second {
token = refreshToken(token.RefreshToken)
saveToDB(token)
}
return token.AccessToken
}Access token 会过期(通常 1 小时)。 agents-server 在过期前 60 秒自动用 refresh token 换新 access token。用户无感知,不会跑到一半 token 过期失败。
从 JWT 提取 account_id
// chatgpt_oauth.go:249
func decodeAccountID(accessToken string) string {
// access token 是 JWT,payload 里有 account_id
payload := parseJWT(accessToken)
return payload["https://api.openai.com/auth.chatgpt_account_id"]
}ChatGPT 的 API 要带 account_id header。account_id 嵌在 access token(JWT)的 payload 里。agents-server 解析 JWT 提取它。
设计权衡:per-agent 登录
agents-server 的 ChatGPT OAuth 是 per-agent 的,每个 AgentConfig 可以独立登录不同 ChatGPT 账号:
// 存在 agent_configs 表的 chatgpt_token 列
// 每个 agent config 有自己的 token为什么 per-agent? 因为不同 Agent 可能用不同账号,你的个人 Agent 用你的 ChatGPT 账号,团队共享 Agent 用团队账号。per-agent 让每个 Agent 独立计费、独立额度。
小结:Part VI 回顾
第 34-39 章,把所有组件组装成完整平台:
| 章节 | 组件 |
|---|---|
| 34 总览 | 四层架构 + 手动 DI |
| 35 数据库 | 10 表 + JSON 列 + 泛型 CrudStore |
| 36 WebSocket | 三层流式管道 |
| 37 HITL | pending map + 端到端审批 |
| 38 前端 | go:embed + 零构建 |
| 39 OAuth | PKCE + 本地回调 + postMessage |
agents-server 是 agents-go SDK 的"完整应用范本",从 SDK 到产品。 ChatGPT OAuth 是它的一个亮点功能,也是 OAuth 桌面应用的标准实现。
下一篇
第 40 章,结语:你现在已经能从 0 写 Agent 了。Part VII 的最后一章,回顾全书,总结 Agent 运行时的 7 个核心设计模式,以及从 agents-go 到生产环境还差什么。
本文是《从 0 写 Agent》系列第 39 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

