Skip to content

Commit f7767f2

Browse files
committed
feat: add codex oauth login and reauth flow
1 parent 407a248 commit f7767f2

28 files changed

Lines changed: 1109 additions & 19 deletions

app.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,21 @@ type DownloadFileResponse struct {
5454
ContentBase64 string `json:"contentBase64"`
5555
}
5656

57+
type OAuthStartResult struct {
58+
URL string `json:"url"`
59+
State string `json:"state,omitempty"`
60+
}
61+
62+
type OAuthStatusResult struct {
63+
Status string `json:"status"`
64+
Error string `json:"error,omitempty"`
65+
}
66+
67+
type CompleteCodexOAuthInput struct {
68+
ExistingName string `json:"existingName"`
69+
PreviousNames []string `json:"previousNames"`
70+
}
71+
5772
type CodexQuotaWindow struct {
5873
ID string `json:"id"`
5974
Label string `json:"label"`
@@ -230,6 +245,35 @@ func (a *App) DownloadAuthFile(name string) (*DownloadFileResponse, error) {
230245
}, nil
231246
}
232247

248+
func (a *App) StartCodexOAuth() (*OAuthStartResult, error) {
249+
result, err := a.core.StartCodexOAuth()
250+
if err != nil {
251+
return nil, err
252+
}
253+
return &OAuthStartResult{
254+
URL: result.URL,
255+
State: result.State,
256+
}, nil
257+
}
258+
259+
func (a *App) GetOAuthStatus(state string) (*OAuthStatusResult, error) {
260+
result, err := a.core.GetOAuthStatus(state)
261+
if err != nil {
262+
return nil, err
263+
}
264+
return &OAuthStatusResult{
265+
Status: result.Status,
266+
Error: result.Error,
267+
}, nil
268+
}
269+
270+
func (a *App) FinalizeCodexOAuth(input CompleteCodexOAuthInput) error {
271+
return a.core.FinalizeCodexOAuth(wailsapp.CompleteCodexOAuthInput{
272+
ExistingName: input.ExistingName,
273+
PreviousNames: input.PreviousNames,
274+
})
275+
}
276+
233277
func (a *App) GetCodexQuota(name string) (*CodexQuotaResponse, error) {
234278
result, err := a.core.GetCodexQuota(name)
235279
if err != nil {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# 2026-04-26 Codex OAuth Bridge Boundary
2+
3+
## 背景
4+
5+
GetTokens 原先在账号池里只支持两种新增凭据方式:
6+
7+
1. 上传 / 粘贴 auth file
8+
2. 新建 Codex API Key
9+
10+
对于 `nolon/chatgpt(codex)` 账号,这意味着用户在 token 过期后只能看到失败原因,无法从当前界面直接恢复。
11+
12+
## 本轮结论
13+
14+
### 1. 登录不是全局会话,而是账号池内的局部动作
15+
16+
GetTokens 是桌面端账号池,不是 Web 管理后台。
17+
18+
因此:
19+
20+
1. 单个 `codex` 账号过期,不等于整个应用需要“退出登录”
21+
2. 不引入独立全局登录页或 `ProtectedRoute` 式路由守卫
22+
3. 登录入口和恢复入口都收敛到账号池自身
23+
24+
### 2. OAuth 只桥接 sidecar 现有能力,不重造协议
25+
26+
当前最小 bridge 使用 sidecar 现有管理接口:
27+
28+
1. `GET /v0/management/codex-auth-url?is_webui=true`
29+
2. `GET /v0/management/get-auth-status?state=...`
30+
31+
前端负责:
32+
33+
1. 调 Wails `StartCodexOAuth`
34+
2.`BrowserOpenURL(...)` 打开系统浏览器
35+
3. 轮询 `GetOAuthStatus`
36+
37+
Wails 负责:
38+
39+
1. 把请求桥接给 sidecar
40+
2. 在成功后执行本地账号资产回填
41+
42+
### 3. “重新登录”默认回填原账号资产,而不是新增重复账号
43+
44+
仅仅发起 sidecar OAuth 会在 `auth-dir` 里新增一个新的 `codex` auth file。
45+
46+
这对桌面账号池不够好,因为会留下:
47+
48+
1. 一个失败的旧账号
49+
2. 一个新登录成功的重复账号
50+
51+
因此本轮规则是:
52+
53+
1. 普通 `ChatGPT 登录` 可以新增账号
54+
2. 从失败卡片触发 `重新登录` 时,成功后要把新 auth 内容回填到原文件名
55+
3. 临时生成的新 auth file 在回填后删除,不保留为重复资产
56+
57+
### 4. 回填过程只使用现有管理接口完成
58+
59+
当前实现不直接依赖 sidecar 内部文件路径,而是复用现有管理 API:
60+
61+
1. 比较 OAuth 前后的 auth file 名单
62+
2. 找到新的 `codex` auth file
63+
3. 下载其内容
64+
4. 删除旧文件
65+
5. 以上传方式用旧文件名重新写回
66+
6. 删除临时新文件
67+
68+
这样做的好处:
69+
70+
1. 不额外引入 sidecar 专用“rename / replace auth file”协议
71+
2. 继续复用现有 normalize 上传链路
72+
3. 桌面端和 sidecar 的职责边界清晰
73+
74+
## 当前边界
75+
76+
### 前端
77+
78+
1. Header 菜单提供 `ChatGPT 登录`
79+
2. 失败态 `codex auth-file` 卡片提供 `重新登录`
80+
3. 轮询 OAuth 状态并显示进行中 / 失败提示
81+
82+
### Wails
83+
84+
1. `StartCodexOAuth`
85+
2. `GetOAuthStatus`
86+
3. `FinalizeCodexOAuth`
87+
88+
### sidecar
89+
90+
1. 继续负责 OAuth 握手、回调接收与 token 落盘
91+
2. 不要求为 GetTokens 新增专门的 replace 接口
92+
93+
## 不照搬的参考做法
94+
95+
参考项目 `Cli-Proxy-API-Management-Center` 的以下能力值得借:
96+
97+
1. `start -> status -> callback` 的 OAuth 三段式接口
98+
2. WebUI 模式下的浏览器回调转发
99+
100+
但以下做法不适合直接照搬到 GetTokens:
101+
102+
1. 全局 `401 -> logout -> ProtectedRoute` 重定向
103+
2. 单独的管理后台登录体系
104+
105+
原因是 GetTokens 的核心对象是“账号资产”,不是“当前操作者会话”。

docs-linhay/memory/2026-04-26.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# 2026-04-26
22

3+
## Codex OAuth Bridge & Reauth
4+
- 账号池已补齐 `nolon/chatgpt(codex)` 最小 OAuth bridge:Wails 新增 `StartCodexOAuth``GetOAuthStatus``FinalizeCodexOAuth`,前端可从 Header 发起 `ChatGPT 登录`,也可从失败态 `codex` 卡片触发 `重新登录`
5+
- 本轮明确了一条稳定边界:GetTokens 不引入 Web 管理后台式全局登录态;`codex` 账号过期只在账号池内局部修复,不做全局 `401 -> logout -> route redirect`
6+
- 重新登录成功后的默认行为不是保留一新一旧两个账号,而是把新的 `codex` auth 内容回填到原文件名,再删除临时生成的新 auth file,避免账号池里残留重复资产。
7+
- 本轮验证通过:`go test ./internal/wailsapp ./internal/cliproxyapi``npm run typecheck``npm run test:unit`
8+
39
## Sidebar Version Label
410
- 新增 `ReleaseLabel` 构建注入字段,保留 `Version` 给自动更新比较使用,避免界面发布日期格式影响更新逻辑。
511
- `frontend/src/utils/version.ts` 将 Sidebar 版本展示统一格式化为 `YYYY.MM.DD.HH`

docs-linhay/spaces/account-pool/README.md

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,82 @@
1111
- 账号池开发计划归档到 `plans/`
1212
- 账号池截图归档到 `screenshots/`
1313
- 账号池多 agent 讨论归档到 `debate/`
14+
- `nolon/chatgpt(codex)` OAuth 登录与账号过期恢复集成
1415

1516
## 非目标
1617
- 在本次操作中直接定义账号池的详细产品方案或技术实现
1718
- 在仓库级 `dev/` 或其他 space 中重复维护同一份账号池范围说明
19+
- 为整个桌面应用引入独立的全局登录页或 Web 管理后台式会话系统
20+
21+
## 当前需求
22+
23+
### 目标
24+
25+
在当前账号池中,为 `nolon/chatgpt(codex)` 账号补齐两条闭环:
26+
27+
1. 应用内发起 OAuth 登录,替代纯手工导入 `auth.json`
28+
2. 当账号过期或失效时,用户可以直接从失败卡片触发重新登录,而不是只看到失败原因
29+
30+
### 已知边界
31+
32+
1. 当前 GetTokens 已能把 `nolon chatgptAccount` 类 auth 文件识别为 `codex`
33+
2. 当前导入链路已能把 legacy `codex/nolon` auth 清洗成 sidecar 真正消费的最小字段结构
34+
3. 当前失败原因已能从 sidecar `statusMessage` 透传到账号卡片
35+
4. 当前前端入口仍以“上传文件 / 粘贴 JSON / API Key 录入”为主,没有 OAuth bridge
36+
5. 当前桌面应用不是单一全局登录应用,不能把单个账号失效等同于整个应用登出
37+
38+
### BDD 场景
39+
40+
#### 场景 1:新增 ChatGPT 账号
41+
42+
- Given sidecar 已就绪,账号池页面可操作
43+
- When 用户点击 `ChatGPT 登录`
44+
- Then 应用调用 sidecar OAuth 起始接口并打开系统浏览器
45+
- And 前端显示登录进行中状态
46+
- When sidecar OAuth 流程完成
47+
- Then 账号池刷新并出现新的 `codex` 账号记录
48+
49+
#### 场景 2:过期账号重新登录
50+
51+
- Given 账号池中已有一个 `codex` auth-file 账号,状态异常且存在失败原因
52+
- When 用户点击该卡片上的 `重新登录`
53+
- Then 应用发起新的 OAuth 流程
54+
- When OAuth 成功且检测到新的 `codex` auth 文件
55+
- Then 应用将新 auth 内容回填到原账号资产
56+
- And 刷新后原账号 ID 仍以原文件名存在
57+
- And 临时生成的新 auth 文件不会作为重复账号残留
58+
59+
#### 场景 3:OAuth 失败或超时
60+
61+
- Given 用户已发起 `ChatGPT 登录``重新登录`
62+
- When sidecar 返回 `error` 状态或超时
63+
- Then 前端保留错误提示
64+
- And 不修改现有账号内容
65+
- And 用户可以再次触发登录
66+
67+
#### 场景 4:账号失效后的可恢复性
68+
69+
- Given `codex` auth-file 账号状态不是 `ACTIVE / CONFIGURED / DISABLED / LOCAL`
70+
- When 用户查看账号卡片
71+
- Then 卡片除失败原因外,还应暴露 `重新登录` 动作
72+
- And 该动作只作用于当前账号,不影响其他账号和应用整体路由
1873

1974
## 验收标准
2075
- 已存在 `docs-linhay/spaces/account-pool/README.md`
2176
- 已存在 `docs-linhay/spaces/account-pool/plans/`
2277
- 已存在 `docs-linhay/spaces/account-pool/screenshots/`
2378
- 已存在 `docs-linhay/spaces/account-pool/debate/`
2479
- 后续账号池相关文档默认优先落到该 space
80+
- 已定义 `codex` OAuth 登录与过期恢复的验收场景
81+
- 实现后至少覆盖后端 bridge 测试与前端账号动作测试
82+
- 过期 `codex` 账号不再只是显示失败原因,而是可直接触发重新登录
83+
- 成功重登后默认回填原账号资产,不新增重复账号
2584

2685
## 相关链接
2786
- [docs-linhay 文档入口](/Users/linhey/Desktop/linhay-open-sources/GetTokens/docs-linhay/README.md)
2887
- [spaces 结构治理](/Users/linhey/Desktop/linhay-open-sources/GetTokens/docs-linhay/dev/20260424-spaces-structure-governance.md)
2988

3089
## 当前状态
31-
- 状态:ready
90+
- 状态:in-progress
3291
- 最近更新:2026-04-26
33-
- 最近变更:`AccountDetailModal``RAW_SOURCE_DATA` 区块已支持点击 / 键盘触发复制,便于直接提取 auth 原文做排障和对比
92+
- 最近变更:启动 `nolon/chatgpt(codex)` OAuth 登录与过期恢复集成,先收敛为账号池内局部动作,不引入全局登录页
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# 20260426 Codex OAuth Regression Plan V01
2+
3+
## 目标
4+
5+
为账号池内的 `ChatGPT 登录 / codex 重新登录` 闭环提供一份可重复执行的回归方案,降低后续修改账号池、Wails bridge 或 sidecar OAuth 时的回归风险。
6+
7+
## 适用范围
8+
9+
1. `StartCodexOAuth`
10+
2. `GetOAuthStatus`
11+
3. `FinalizeCodexOAuth`
12+
4. 账号卡片失败态 `重新登录`
13+
5. 新 auth 回填原文件名
14+
15+
## 自动化回归
16+
17+
### Go
18+
19+
运行:
20+
21+
```bash
22+
go test ./internal/wailsapp ./internal/cliproxyapi
23+
```
24+
25+
关注点:
26+
27+
1. OAuth 起始接口是否仍正确桥接到 sidecar
28+
2. 回填逻辑是否仍保留原文件名
29+
3. 多个新 `codex` auth file 同时出现时,是否仍能拒绝歧义结果
30+
31+
### Frontend
32+
33+
运行:
34+
35+
```bash
36+
cd frontend
37+
npm run test:unit
38+
npm run typecheck
39+
npm run build
40+
```
41+
42+
关注点:
43+
44+
1. 失败态 `codex auth-file` 是否仍被识别为可重登
45+
2. 登录 / 重登横幅文案是否仍按场景区分
46+
3. 新增入口和卡片动作是否未破坏现有构建
47+
48+
## 手动回归
49+
50+
### 场景 1:新增 ChatGPT 账号
51+
52+
1. 打开账号池
53+
2. 点击 `ChatGPT 登录`
54+
3. 确认浏览器被拉起
55+
4. 完成授权
56+
5. 返回应用,确认出现“登录进行中”横幅
57+
6. 同步完成后确认出现“登录成功”横幅
58+
7. 确认账号池新增一个 `codex` auth-file 账号
59+
60+
### 场景 2:失败账号重新登录
61+
62+
1. 准备一个失败态 `codex` auth-file 账号
63+
2. 确认卡片显示失败原因和 `重新登录`
64+
3. 点击 `重新登录`
65+
4. 完成浏览器授权
66+
5. 返回应用并等待同步
67+
6. 确认原文件名对应的账号恢复,而不是出现一新一旧两个重复账号
68+
69+
### 场景 3:登录失败
70+
71+
1. 发起 `ChatGPT 登录`
72+
2. 在浏览器中取消授权或让流程超时
73+
3. 返回应用
74+
4. 确认出现错误横幅
75+
5. 确认账号池未新增错误资产
76+
6. 确认可再次点击登录
77+
78+
### 场景 4:异常歧义保护
79+
80+
1. 人为制造多个新的 `codex` auth file
81+
2. 触发失败账号 `重新登录`
82+
3. 确认应用不静默覆盖错误账号
83+
4. 确认错误被显式提示,而不是产生不可追踪的数据错配
84+
85+
## 通过标准
86+
87+
1. 自动化测试全部通过
88+
2. 新增登录和重登两条路径都能完成
89+
3. 回填后不残留重复账号
90+
4. 失败时能明确提示且不破坏原账号

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"build": "vite build",
99
"preview": "vite preview",
1010
"typecheck": "tsc --noEmit",
11-
"test:unit": "node --test src/utils/version.test.mjs src/features/accounts/tests/accountConfig.test.mjs src/features/accounts/tests/accountSelectors.test.mjs src/features/accounts/tests/accountSelection.test.mjs src/features/accounts/tests/accountTransfer.test.mjs src/features/accounts/tests/accountPresentation.test.mjs src/features/accounts/tests/accountCardInteractions.test.mjs src/components/biz/accountDetailClipboard.test.mjs"
11+
"test:unit": "node --test src/utils/version.test.mjs src/features/accounts/tests/accountConfig.test.mjs src/features/accounts/tests/accountSelectors.test.mjs src/features/accounts/tests/accountSelection.test.mjs src/features/accounts/tests/accountTransfer.test.mjs src/features/accounts/tests/accountPresentation.test.mjs src/features/accounts/tests/accountOAuth.test.mjs src/features/accounts/tests/accountCardInteractions.test.mjs src/components/biz/accountDetailClipboard.test.mjs"
1212
},
1313
"dependencies": {
1414
"react": "^18.3.1",

frontend/package.json.md5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
27a1890f55e32fa001a804da1bb7ebae
1+
8b028b4ca170b27b487153842f114243

0 commit comments

Comments
 (0)