Skip to content

Commit 0821d19

Browse files
committed
feat: complete codex routing workspace
1 parent dd6613a commit 0821d19

76 files changed

Lines changed: 6790 additions & 987 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
name: gettokens-codex-account-list
3+
description: GetTokens Codex 账号列表:账号请求顺序、路由探测、模型映射、OAuth 透传语义、openai-compatible 映射保存与浏览器预览。
4+
---
5+
6+
# GetTokens Codex Account List
7+
8+
当任务涉及 `frontend/src/features/codex/CodexAccountListFeature.tsx`、Codex 账号请求顺序、路由探测、模型映射、OAuth/auth-file 映射、openai-compatible provider 映射,或后端 `ProbeCodexAccountRouting` / OAuth model alias 时使用本 skill。
9+
10+
## 1. 业务边界
11+
- Codex 账号列表是请求调试与账号顺序工作台,不是账号创建页。
12+
- 账号来源统一展示,但语义保持分离:
13+
- `auth-file` / OAuth Codex
14+
- `codex-api-key`
15+
- `openai-compatible`
16+
- 禁用账号保留在排序中,但不参与运行时请求候选。
17+
- 请求测试顺序只来自当前可请求账号的拖拽顺序,从上到下执行。
18+
- 不再维护第二套独立策略顺序;允许/排除只过滤候选,不重排候选。
19+
20+
## 2. 前端结构
21+
- `CodexAccountListFeature.tsx` 保持为 controller:
22+
- Wails/browser 数据加载
23+
- 顺序保存
24+
- 路由探测调度
25+
- modal 打开/关闭与 hash 同步
26+
- 模型映射保存编排
27+
- UI 组件放在 `frontend/src/features/codex/components/`
28+
- `CodexRouteProbeCard.tsx`
29+
- `CodexAccountOrderRow.tsx`
30+
- `CodexAccountDetailModal.tsx`
31+
- `ModelCombobox.tsx`
32+
- `codexAccountPresentation.ts`
33+
- 纯模型逻辑放在 `frontend/src/features/codex/model/`
34+
- `codexAccountList.ts`:账号合并、排序、优先级更新
35+
- `codexModelMappings.ts`:OAuth/openai-compatible 模型映射归一
36+
- `codexRoutePolicy.ts`:候选过滤、探测日志、路由状态
37+
- 不新增 catch-all helper 文件;按账号、映射、路由策略拆分。
38+
39+
## 3. 模型映射语义
40+
- openai-compatible 映射方向固定为:真实模型 `models[].name` -> Codex 模型 `models[].alias || name`
41+
- openai-compatible 保存时按 `name + alias` 去重,允许同一个真实模型映射到多个 Codex alias。
42+
- OAuth/auth-file 默认原样穿透模型名,不展示同名 `model -> model` 映射。
43+
- OAuth/auth-file 只有配置显式 alias 后才关闭默认透传;保存空映射应删除 channel alias。
44+
- OAuth 映射按 provider/channel 生效,同一 `codex` channel 共享映射。
45+
- 模型选择使用项目自定义 combobox,不回退到原生 `datalist`
46+
47+
## 4. 路由探测语义
48+
- `ProbeCodexAccountRouting` 使用页面传入的候选约束发起最小 relay 请求。
49+
- 前端传给后端的 `orderAccountIDs` 必须是当前拖拽排序后的可请求账号 ID 列表。
50+
- `allowAccountIDs` 表示首选候选;`denyAccountIDs` 表示排除候选;`allowFallback` 只在设置允许账号后决定是否继续尝试其他未排除账号。
51+
- 探测结果需要同时展示:
52+
- 终端式流输出
53+
- 当前候选顺序
54+
- 最新命中账号
55+
- 对应账号行高亮
56+
- 连续测试应逐次追加结果,避免等待全部完成后才刷新 UI。
57+
58+
## 5. 浏览器预览
59+
- `#frame=codex&workspace=account-list` 必须可在普通浏览器预览。
60+
- 缺少 `window.go.main.App` 时使用 `previewData.ts`,不能让页面空白。
61+
- 浏览器预览中的排序、启停、模型映射保存是本地状态更新,并需要给出 preview-only 提示。
62+
- 视觉或交互调整要优先用浏览器预览快速验证;涉及真实 sidecar、Wails 绑定或账号命中时,再用桌面环境补验。
63+
64+
## 6. UI 规则
65+
- 保持 Swiss-industrial 风格:硬边框、黑白灰、紧凑高密度、monospace 辅助信息。
66+
- 账号行固定为单一请求顺序列表,不再额外渲染重复策略账号列表。
67+
- 账号行主体点击打开详情;嵌套按钮、switch、combobox、策略控件必须阻止冒泡。
68+
- 策略控件常驻行内:默认 / 允许 / 排除。
69+
- 路由探测卡片独立于账号顺序卡片;测试流常驻显示,不使用卡中卡文本模块。
70+
71+
## 7. 后端 / Wails 边界
72+
- 新增 Wails-facing 方法时必须同时检查:
73+
- `internal/wailsapp`
74+
- root `app.go`
75+
- root DTO / mapper
76+
- `frontend/wailsjs`
77+
- 账号 row id 到 sidecar auth id 的转换必须覆盖:
78+
- `auth-file:<name>`
79+
- `codex-api-key:<id>`
80+
- `openai-compatible:<name>`
81+
- 路由探测用的 loopback header 是调试口子;不要把它和持久化账号配置混在一起。
82+
83+
## 8. 验证
84+
- 前端结构或 UI 调整:
85+
- `npm --prefix frontend run typecheck`
86+
- `npm --prefix frontend run test:unit -- src/features/codex/codexAccountList.test.mjs`
87+
- 浏览器打开 `#frame=codex&workspace=account-list` 检查账号行、探测卡、详情 modal 和 combobox。
88+
- 后端、Wails 或 sidecar 探测调整:
89+
- `go test ./internal/wailsapp -run 'TestListOAuthModelAliases|TestUpdateOAuthModelAliases|TestProbeCodexAccountRouting|TestDetectCodexRoutingProbeHit|TestSidecarRelayRequest'`
90+
- 涉及公共 DTO 或绑定时重新生成 `frontend/wailsjs` 并跑类型检查。
91+
- 视觉截图放到 `docs-linhay/spaces/20260511-codex-account-list-tab/screenshots/<YYYYMMDD>/codex/`
92+
93+
## 9. 文档
94+
- 需求、验收与截图写入 `docs-linhay/spaces/20260511-codex-account-list-tab/README.md`
95+
- 技术拆分、沉淀结论写入 `docs-linhay/dev/`
96+
- 稳定决策和用户偏好写入 `docs-linhay/memory/YYYY-MM-DD.md`
97+
- 文档或记忆写回后运行 `qmd update``qmd embed`

.agents/skills/gettokens-domain-engineering/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ This skill unifies the technical rules for building, styling, and debugging GetT
7979

8080
## 3.1 Codex Workspace & Local Config Surfaces
8181
- **Codex Binary**: For Codex CLI binary version/source management, use the dedicated `gettokens-codex-binary-management` skill. Keep it as an independent binary-management business; do not merge it into account pool, local apply, usage, session, or routing flows.
82+
- **Codex Account List**: For Codex account request order, route probing, OAuth/auth-file model aliasing, openai-compatible model mappings, and `#frame=codex&workspace=account-list`, use the dedicated `gettokens-codex-account-list` skill. Keep request order semantics in that skill: draggable account order is the test order, while allow/deny only filters candidates.
8283
- **Browser Support**: New Codex workspace tabs must be usable in a normal browser preview when the interaction is layout/config-flow checkable. Do not let missing `window.go.main.App` make the page blank; provide explicit preview data and visible preview-only save behavior.
8384
- **Frame URL Rule**: Modal/detail layers opened from Codex workspaces should preserve the frame hash, for example `#frame=codex&workspace=<key>&detail=<id>`, when the surrounding feature already follows frame/detail routing. Closing a modal should remove only the detail marker.
8485
- **Wails Binding Boundary**: Any Wails-facing Codex method added under `internal/wailsapp` must also be exposed through root `app.go`, mirrored in root DTOs/mappers when needed, and regenerated into `frontend/wailsjs`. Frontend should import from generated bindings only after the root `main.App` method exists.

AGENTS.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ Git `worktree` 治理:
9898
2. 涉及文档写回、memory 写回、`qmd update` / `qmd embed` 同步时,优先使用 `gettokens-ops-governance`
9999
3. 涉及 AGENTS 级长期治理规则时,优先使用 `gettokens-ops-governance`;若用户明确说“整理”,同时使用 `gettokens-session-skill-distill`
100100
4. 涉及账号池、quota、视觉系统、前端调试归因或 CLIProxyAPI fork 维护时,优先使用 `gettokens-domain-engineering`
101-
5. 涉及“主控 agent 监督、subagent 实做、直到完整需求闭环才停止”的执行模式时,优先使用 `gettokens-ops-governance` 中的 `Subagent Delivery Loop`
102-
6. 若用户希望用显式 skill 名称触发该模式,使用 `gettokens-subagent-supervision`;它是监督交付模式的轻量触发入口。
101+
5. 涉及 Codex 账号列表、请求顺序、路由探测、OAuth/openai-compatible 模型映射时,优先使用 `gettokens-codex-account-list`
102+
6. 涉及“主控 agent 监督、subagent 实做、直到完整需求闭环才停止”的执行模式时,优先使用 `gettokens-ops-governance` 中的 `Subagent Delivery Loop`
103+
7. 若用户希望用显式 skill 名称触发该模式,使用 `gettokens-subagent-supervision`;它是监督交付模式的轻量触发入口。
103104

104105
## 5. 记忆系统规则(必须)
105106

app.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,40 @@ func (a *App) UpdateAccountPriority(input UpdateAccountPriorityInput) error {
151151
})
152152
}
153153

154+
func (a *App) ProbeCodexAccountRouting(input ProbeCodexAccountRoutingInput) (*CodexAccountRoutingProbeResult, error) {
155+
result, err := a.core.ProbeCodexAccountRouting(wailsapp.ProbeCodexAccountRoutingInput{
156+
Model: input.Model,
157+
Attempts: input.Attempts,
158+
AllowAccountIDs: append([]string(nil), input.AllowAccountIDs...),
159+
DenyAccountIDs: append([]string(nil), input.DenyAccountIDs...),
160+
OrderAccountIDs: append([]string(nil), input.OrderAccountIDs...),
161+
AllowFallback: input.AllowFallback,
162+
})
163+
if err != nil {
164+
return nil, err
165+
}
166+
attempts := make([]CodexAccountRoutingProbeAttempt, 0, len(result.Attempts))
167+
for _, attempt := range result.Attempts {
168+
attempts = append(attempts, CodexAccountRoutingProbeAttempt{
169+
Index: attempt.Index,
170+
Success: attempt.Success,
171+
StatusCode: attempt.StatusCode,
172+
AccountID: attempt.AccountID,
173+
AccountLabel: attempt.AccountLabel,
174+
Provider: attempt.Provider,
175+
Message: attempt.Message,
176+
Evidence: attempt.Evidence,
177+
ResponseBody: attempt.ResponseBody,
178+
StartedAt: attempt.StartedAt,
179+
FinishedAt: attempt.FinishedAt,
180+
})
181+
}
182+
return &CodexAccountRoutingProbeResult{
183+
Model: result.Model,
184+
Attempts: attempts,
185+
}, nil
186+
}
187+
154188
func (a *App) UploadAuthFiles(files []UploadFilePayload) error {
155189
payload := make([]wailsapp.UploadFilePayload, 0, len(files))
156190
for _, file := range files {
@@ -166,6 +200,35 @@ func (a *App) GetAuthFileModels(name string) ([]map[string]interface{}, error) {
166200
return a.core.GetAuthFileModels(name)
167201
}
168202

203+
func (a *App) ListOAuthModelAliases(channel string) ([]OpenAICompatibleModel, error) {
204+
items, err := a.core.ListOAuthModelAliases(channel)
205+
if err != nil {
206+
return nil, err
207+
}
208+
out := make([]OpenAICompatibleModel, 0, len(items))
209+
for _, item := range items {
210+
out = append(out, OpenAICompatibleModel{
211+
Name: item.Name,
212+
Alias: item.Alias,
213+
})
214+
}
215+
return out, nil
216+
}
217+
218+
func (a *App) UpdateOAuthModelAliases(input UpdateOAuthModelAliasesInput) error {
219+
models := make([]wailsapp.OpenAICompatibleModel, 0, len(input.Models))
220+
for _, model := range input.Models {
221+
models = append(models, wailsapp.OpenAICompatibleModel{
222+
Name: model.Name,
223+
Alias: model.Alias,
224+
})
225+
}
226+
return a.core.UpdateOAuthModelAliases(wailsapp.UpdateOAuthModelAliasesInput{
227+
Channel: input.Channel,
228+
Models: models,
229+
})
230+
}
231+
169232
func (a *App) DownloadAuthFile(name string) (*DownloadFileResponse, error) {
170233
result, err := a.core.DownloadAuthFile(name)
171234
if err != nil {
@@ -358,6 +421,7 @@ func (a *App) GetCodexSkillsSnapshot() (*CodexSkillsSnapshot, error) {
358421
func (a *App) SaveCodexSkillEnabled(input SaveCodexSkillEnabledInput) (*SaveCodexSkillEnabledResult, error) {
359422
result, err := a.core.SaveCodexSkillEnabled(wailsapp.SaveCodexSkillEnabledInput{
360423
Path: input.Path,
424+
Name: input.Name,
361425
Enabled: input.Enabled,
362426
})
363427
if err != nil {
@@ -369,6 +433,45 @@ func (a *App) SaveCodexSkillEnabled(input SaveCodexSkillEnabledInput) (*SaveCode
369433
}, nil
370434
}
371435

436+
func (a *App) GetCodexSkillFilePreview(input GetCodexSkillFilePreviewInput) (*GetCodexSkillFilePreviewResult, error) {
437+
result, err := a.core.GetCodexSkillFilePreview(wailsapp.GetCodexSkillFilePreviewInput{
438+
SkillPath: input.SkillPath,
439+
FilePath: input.FilePath,
440+
})
441+
if err != nil {
442+
return nil, err
443+
}
444+
return &GetCodexSkillFilePreviewResult{
445+
Path: result.Path,
446+
Content: result.Content,
447+
Previewable: result.Previewable,
448+
}, nil
449+
}
450+
451+
func (a *App) RemoveCodexSkill(input RemoveCodexSkillInput) (*RemoveCodexSkillResult, error) {
452+
result, err := a.core.RemoveCodexSkill(wailsapp.RemoveCodexSkillInput{
453+
Path: input.Path,
454+
})
455+
if err != nil {
456+
return nil, err
457+
}
458+
return &RemoveCodexSkillResult{
459+
ConfigPath: result.ConfigPath,
460+
RemovedPath: result.RemovedPath,
461+
Preview: result.Preview,
462+
}, nil
463+
}
464+
465+
func (a *App) OpenCodexSkillInFinder(input OpenCodexSkillInFinderInput) (*OpenCodexSkillInFinderResult, error) {
466+
result, err := a.core.OpenCodexSkillInFinder(wailsapp.OpenCodexSkillInFinderInput{
467+
Path: input.Path,
468+
})
469+
if err != nil {
470+
return nil, err
471+
}
472+
return &OpenCodexSkillInFinderResult{Path: result.Path}, nil
473+
}
474+
372475
func (a *App) GetCodexMcpServers() (*CodexMcpServersSnapshot, error) {
373476
result, err := a.core.GetCodexMcpServers()
374477
if err != nil {

app_mappers.go

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,12 @@ func mapCodexSkillsSnapshot(result *wailsapp.CodexSkillsSnapshot) *CodexSkillsSn
155155
for _, skill := range result.Skills {
156156
files := make([]CodexSkillFile, 0, len(skill.Files))
157157
for _, file := range skill.Files {
158-
files = append(files, CodexSkillFile{Path: file.Path, Kind: file.Kind})
158+
files = append(files, CodexSkillFile{
159+
Path: file.Path,
160+
Kind: file.Kind,
161+
Content: file.Content,
162+
Previewable: file.Previewable,
163+
})
159164
}
160165
skills = append(skills, CodexSkillRecord{
161166
ID: skill.ID,
@@ -200,45 +205,99 @@ func mapCodexMcpServersSnapshot(result *wailsapp.CodexMcpServersSnapshot) *Codex
200205
}
201206

202207
func mapCodexMcpServer(server wailsapp.CodexMcpServer) CodexMcpServer {
203-
env := make([]CodexMcpEnvRow, 0, len(server.Env))
204-
for _, row := range server.Env {
205-
env = append(env, CodexMcpEnvRow{Key: row.Key, Value: row.Value})
206-
}
207208
return CodexMcpServer{
208-
ID: server.ID,
209-
Label: server.Label,
210-
Enabled: server.Enabled,
211-
Transport: server.Transport,
212-
Command: server.Command,
213-
Args: append([]string(nil), server.Args...),
214-
URL: server.URL,
215-
Env: env,
216-
BearerTokenEnvVar: server.BearerTokenEnvVar,
217-
SourcePath: server.SourcePath,
218-
Status: server.Status,
219-
Warnings: append([]string(nil), server.Warnings...),
209+
ID: server.ID,
210+
Label: server.Label,
211+
Enabled: server.Enabled,
212+
Transport: server.Transport,
213+
Command: server.Command,
214+
Args: append([]string(nil), server.Args...),
215+
Env: mapCodexMcpEnvRows(server.Env),
216+
EnvVarsRaw: server.EnvVarsRaw,
217+
Cwd: server.Cwd,
218+
URL: server.URL,
219+
BearerTokenEnvVar: server.BearerTokenEnvVar,
220+
HTTPHeaders: mapCodexMcpEnvRows(server.HTTPHeaders),
221+
EnvHTTPHeaders: mapCodexMcpEnvRows(server.EnvHTTPHeaders),
222+
ExperimentalEnvironment: server.ExperimentalEnvironment,
223+
Required: server.Required,
224+
SupportsParallelToolCalls: server.SupportsParallelToolCalls,
225+
StartupTimeoutSec: server.StartupTimeoutSec,
226+
ToolTimeoutSec: server.ToolTimeoutSec,
227+
DefaultToolsApprovalMode: server.DefaultToolsApprovalMode,
228+
EnabledTools: append([]string(nil), server.EnabledTools...),
229+
DisabledTools: append([]string(nil), server.DisabledTools...),
230+
Scopes: append([]string(nil), server.Scopes...),
231+
OAuthResource: server.OAuthResource,
232+
Tools: mapCodexMcpToolRows(server.Tools),
233+
SourcePath: server.SourcePath,
234+
Status: server.Status,
235+
Warnings: append([]string(nil), server.Warnings...),
220236
}
221237
}
222238

223239
func mapWailsCodexMcpServer(server CodexMcpServer) wailsapp.CodexMcpServer {
224-
env := make([]wailsapp.CodexMcpEnvRow, 0, len(server.Env))
225-
for _, row := range server.Env {
226-
env = append(env, wailsapp.CodexMcpEnvRow{Key: row.Key, Value: row.Value})
227-
}
228240
return wailsapp.CodexMcpServer{
229-
ID: server.ID,
230-
Label: server.Label,
231-
Enabled: server.Enabled,
232-
Transport: server.Transport,
233-
Command: server.Command,
234-
Args: append([]string(nil), server.Args...),
235-
URL: server.URL,
236-
Env: env,
237-
BearerTokenEnvVar: server.BearerTokenEnvVar,
238-
SourcePath: server.SourcePath,
239-
Status: server.Status,
240-
Warnings: append([]string(nil), server.Warnings...),
241+
ID: server.ID,
242+
Label: server.Label,
243+
Enabled: server.Enabled,
244+
Transport: server.Transport,
245+
Command: server.Command,
246+
Args: append([]string(nil), server.Args...),
247+
Env: mapWailsCodexMcpEnvRows(server.Env),
248+
EnvVarsRaw: server.EnvVarsRaw,
249+
Cwd: server.Cwd,
250+
URL: server.URL,
251+
BearerTokenEnvVar: server.BearerTokenEnvVar,
252+
HTTPHeaders: mapWailsCodexMcpEnvRows(server.HTTPHeaders),
253+
EnvHTTPHeaders: mapWailsCodexMcpEnvRows(server.EnvHTTPHeaders),
254+
ExperimentalEnvironment: server.ExperimentalEnvironment,
255+
Required: server.Required,
256+
SupportsParallelToolCalls: server.SupportsParallelToolCalls,
257+
StartupTimeoutSec: server.StartupTimeoutSec,
258+
ToolTimeoutSec: server.ToolTimeoutSec,
259+
DefaultToolsApprovalMode: server.DefaultToolsApprovalMode,
260+
EnabledTools: append([]string(nil), server.EnabledTools...),
261+
DisabledTools: append([]string(nil), server.DisabledTools...),
262+
Scopes: append([]string(nil), server.Scopes...),
263+
OAuthResource: server.OAuthResource,
264+
Tools: mapWailsCodexMcpToolRows(server.Tools),
265+
SourcePath: server.SourcePath,
266+
Status: server.Status,
267+
Warnings: append([]string(nil), server.Warnings...),
268+
}
269+
}
270+
271+
func mapCodexMcpEnvRows(rows []wailsapp.CodexMcpEnvRow) []CodexMcpEnvRow {
272+
result := make([]CodexMcpEnvRow, 0, len(rows))
273+
for _, row := range rows {
274+
result = append(result, CodexMcpEnvRow{Key: row.Key, Value: row.Value})
275+
}
276+
return result
277+
}
278+
279+
func mapWailsCodexMcpEnvRows(rows []CodexMcpEnvRow) []wailsapp.CodexMcpEnvRow {
280+
result := make([]wailsapp.CodexMcpEnvRow, 0, len(rows))
281+
for _, row := range rows {
282+
result = append(result, wailsapp.CodexMcpEnvRow{Key: row.Key, Value: row.Value})
241283
}
284+
return result
285+
}
286+
287+
func mapCodexMcpToolRows(rows []wailsapp.CodexMcpToolRow) []CodexMcpToolRow {
288+
result := make([]CodexMcpToolRow, 0, len(rows))
289+
for _, row := range rows {
290+
result = append(result, CodexMcpToolRow{Name: row.Name, ApprovalMode: row.ApprovalMode})
291+
}
292+
return result
293+
}
294+
295+
func mapWailsCodexMcpToolRows(rows []CodexMcpToolRow) []wailsapp.CodexMcpToolRow {
296+
result := make([]wailsapp.CodexMcpToolRow, 0, len(rows))
297+
for _, row := range rows {
298+
result = append(result, wailsapp.CodexMcpToolRow{Name: row.Name, ApprovalMode: row.ApprovalMode})
299+
}
300+
return result
242301
}
243302

244303
func mapCodexMcpSaveResult(result *wailsapp.SaveCodexMcpServerResult) *SaveCodexMcpServerResult {

0 commit comments

Comments
 (0)