Skip to content

feat: 添加 Codex Provider 支持和路由自动排序功能#158

Merged
Bowl42 merged 1 commit intomainfrom
feat/auto-sort-routes-ui
Jan 26, 2026
Merged

feat: 添加 Codex Provider 支持和路由自动排序功能#158
Bowl42 merged 1 commit intomainfrom
feat/auto-sort-routes-ui

Conversation

@Bowl42
Copy link
Collaborator

@Bowl42 Bowl42 commented Jan 26, 2026

Summary

Codex Provider

  • 新增 Codex OAuth 认证流程和 Token 导入
  • 添加 Codex 配额查询和缓存机制(5H/Week 限制)
  • 支持 Codex provider 的创建、编辑和删除
  • 在 Provider 列表页显示 Codex 配额进度条

路由自动排序

  • 在 Provider 列表页添加 Auto-Sort Switch(Antigravity/Codex 区域)
  • 在 Routes 页面 tabs bar 添加手动排序按钮
  • 将 Settings 页面的 "Antigravity Settings" 改名为 "Quota Settings"
  • 添加 useUpdateSetting 乐观更新,防止 Switch 切换闪烁

Test plan

  • 创建 Codex Provider(OAuth 或 Token 导入)
  • 验证 Codex 配额显示正确(5H/Week 进度条)
  • 在 Provider 列表页测试 Auto-Sort Switch
  • 在 Routes 页面测试手动排序按钮
  • 验证 Settings 页面 "Quota Settings" 显示正确

🤖 Generated with Claude Code

Summary by CodeRabbit

发布说明

  • 新功能

    • 支持 Codex 供应商:新增 Codex 提供商类型、配额展示、OAuth 导入与令牌导入体验。
    • 批量获取与刷新 Codex 配额,提供手动刷新按钮和配额上下限展示。
    • 在路由与提供商列表中加入 Codex 的排序与显示控件。
  • 改进

    • 设置中加入 Codex 自动排序开关与更统一的配额设置说明。
    • 设置保存现在采用乐观更新,界面响应更即时。
  • 移除

    • 移除部分 Antigravity 专用的配额与排序控件,保留通用路由/拖拽操作。

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

引入 Codex 提供程序类型与端到端支持(前端 UI、传输层 API、后台任务、数据库仓库、OAuth 回调服务器和适配器);移除 ClientTypeRoutesContent 中的 Antigravity 配额/排序专属逻辑;新增 Codex 批量配额上下文与多处 UI/路由适配与国际化文本。

Changes

Cohort / File(s) 变更摘要
前端:路由与 Provider 列表
web/src/components/routes/ClientTypeRoutesContent.tsx, web/src/pages/client-routes/index.tsx, web/src/pages/routes/form.tsx, web/src/pages/requests/index.tsx, web/src/pages/stats/index.tsx
添加 codex 为 provider 类型并在排序/分组/标签中加入;移除 Antigravity 专属配额上下文与排序逻辑(相关按钮/函数/导入被删除);在 Global 视图加入排序控件(Codex/Antigravity)并调整标签布局(Global/Projects)。
前端:Providers 页面与组件
web/src/pages/providers/*, web/src/pages/providers/components/*, web/src/pages/providers/create-layout.tsx, web/src/pages/providers/hooks/use-provider-navigation.ts, web/src/pages/providers/types.ts
新增 CodexProviderView、CodexTokenImport、在 ProviderRow 中显示 Codex 配额视图;将创建流程支持 codex(导航、选择步骤、创建路由);引入 Codex 颜色与图标;添加 Codex 相关类型/表单选项。
前端:Hooks / Context / Queries
web/src/contexts/codex-quotas-context.tsx, web/src/hooks/queries/use-providers.ts, web/src/hooks/queries/index.ts, web/src/hooks/queries/use-settings.ts
新增 Codex 批量配额查询 hook 与上下文提供器;use-settings 实现乐观更新(onMutate/onError/onSettled);导出扩展的钩子集合。
前端:传输层类型与实现
web/src/lib/transport/types.ts, web/src/lib/transport/interface.ts, web/src/lib/transport/http-transport.ts, web/src/lib/transport/index.ts
在类型层与 Transport 接口中加入大量 Codex 类型与方法(validate/startOAuth/refreshProviderInfo/getUsage/getBatchQuotas/refreshQuotas/sortCodexRoutes)并新增 sortAntigravityRoutes;HttpTransport 实现相应 HTTP 调用(部分声明重复需注意)。
前端:本地化与主题
web/src/locales/en.json, web/src/locales/zh.json, web/src/lib/theme.ts, web/src/index.css
添加 Codex 相关国际化键;将 theme/provider 类型包含 codex;新增 provider 颜色 token --provider-codex
后端:Codex 推送 & OAuth & Handler
internal/handler/codex.go, internal/adapter/provider/codex/*, internal/adapter/provider/codex/oauth.go, internal/adapter/provider/codex/adapter.go, internal/adapter/provider/codex/service.go, internal/adapter/provider/codex/settings.go
新增 Codex HTTP handler(token 验证、OAuth 启动/回调、刷新 provider、获取使用量、批量配额、强制刷新/排序等端点);实现 OAuth 管理、PKCE、令牌刷新、配额解析与归一化;新增 Codex 适配器(转发上游、token 缓存与刷新、流/非流处理、事件/指标上报)。
后端:数据库与仓库
internal/repository/interfaces.go, internal/repository/sqlite/codex_quota.go, internal/repository/sqlite/models.go, internal/domain/model.go
新增 CodexQuota 域模型、数据库表模型与 SQLite 仓库实现(Upsert/GetByEmail/List/Delete);在域模型中加入 ProviderConfigCodex 与 CodexQuota 相关结构;新增 SettingKeyAutoSortCodex。
后端:服务与任务
internal/service/codex_task.go, internal/service/admin.go, internal/core/task.go
新增 CodexTaskService(刷新配额、强制刷新、自动排序路由)并在后台任务调度中加入跑动逻辑;在 admin 服务中将 provider.type=codex 映射为支持 Codex 客户端类型。
后端:服务器生命周期与路由
internal/core/codex_oauth_server.go, internal/core/database.go, internal/core/server.go, cmd/maxx/main.go
新增 CodexOAuthServer(回调 HTTP 服务)并在 server 启停中管理其生命周期;在数据库/服务器组件中注入 CodexQuotaRepo、CodexHandler、CodexOAuthServer;在主入口中注册 Codex 任务服务与 handler。
其他:文档与代理路径
web/src/pages/documentation/index.tsx, web/src/pages/projects/tabs/overview.tsx, internal/handler/project_proxy.go, internal/handler/static.go
代理路径改为强制 /project/{slug}/... 前缀(文档示例与项目配置 URL 同步);project proxy 路径解析更严格化。

Sequence Diagram(s)

mermaid
sequenceDiagram
autonumber
participant Frontend as 前端 (Browser)
participant Transport as HttpTransport
participant Server as 后端 (CodexHandler)
participant DB as DB/QuotaRepo
participant CodexAPI as Codex 外部 API
participant WS as WebSocket 广播

Frontend->>Transport: POST /api/codex/start-oauth
Transport->>Server: HTTP 请求转发
Server->>Server: OAuthManager.CreateSession(state, pkce)
Server->>Frontend: { authURL, state } 返回
note right of Frontend: 用户在弹窗完成授权,回调触发后端收到 code

CodexAPI->>Server: POST /oauth/token (Exchange)
Server->>Server: ParseIDToken、保存 tokens、upsert quota
Server->>WS: 广播 codex_oauth_result
WS->>Frontend: 前端接收 OAuth 结果并完成 provider 创建

mermaid
sequenceDiagram
autonumber
participant Scheduler as Background Scheduler
participant TaskSvc as CodexTaskService
participant ProviderRepo as ProviderRepository
participant RouteRepo as RouteRepository
participant QuotaRepo as CodexQuotaRepository
participant ServerWS as WebSocket 广播

Scheduler->>TaskSvc: 定时调用 RefreshQuotas()
TaskSvc->>ProviderRepo: 列出 Codex providers
TaskSvc->>CodexAPI: 获取每个 provider 使用/配额
CodexAPI->>TaskSvc: 返回 quota 数据
TaskSvc->>QuotaRepo: Upsert(quota)
TaskSvc->>RouteRepo: 根据 quota 计算并更新 route 顺序
TaskSvc->>ServerWS: 广播配额/路由变更

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 分钟

Possibly related PRs

Suggested reviewers

  • awsl233777
  • lxw15337674

诗歌

🐰 Codex 跳跃入林间,代码铺就新天边,
旧有重排悄然散,OAuth 风帆迎朝宴,
配额刷新声声脆,路由重整步步连,
兔子鼓掌庆一刻,代码与梦共缱绻。

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.46% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题为中文,描述添加Codex Provider支持和路由自动排序功能,与changeset的主要变更(添加Codex provider、路由自动排序开关、UI优化)高度相关且准确反映核心改动。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Codex Provider:
- 新增 Codex OAuth 认证流程和 Token 导入
- 添加 Codex 配额查询和缓存机制
- 支持 Codex provider 的创建、编辑和删除
- 在 Provider 列表页显示 Codex 配额进度条(5H/Week)

路由自动排序:
- 在 Provider 列表页添加 Auto-Sort Switch(Antigravity/Codex)
- 在 Routes 页面 tabs bar 添加手动排序按钮
- 将 Settings 页面的 "Antigravity Settings" 改名为 "Quota Settings"
- 添加 useUpdateSetting 乐观更新,防止 Switch 切换闪烁

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@Bowl42 Bowl42 force-pushed the feat/auto-sort-routes-ui branch from 1c95cbc to 27b3fde Compare January 26, 2026 06:07
@Bowl42 Bowl42 changed the title feat: 添加路由自动排序开关和优化 UI feat: 添加 Codex Provider 支持和路由自动排序功能 Jan 26, 2026
@solarhell solarhell self-requested a review January 26, 2026 06:08
@Bowl42 Bowl42 merged commit 4769dd6 into main Jan 26, 2026
1 of 2 checks passed
@Bowl42 Bowl42 deleted the feat/auto-sort-routes-ui branch January 26, 2026 06:16
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🤖 Fix all issues with AI agents
In `@internal/adapter/provider/codex/adapter.go`:
- Around line 264-273: handleStreamResponse currently calls
ctxutil.GetEventChan(ctx) and uses eventChan.SendResponseInfo without checking
for nil, risking a nil pointer dereference; add a defensive nil check after
obtaining eventChan in handleStreamResponse (or ensure ctxutil.GetEventChan
always returns a non-nil implementation) and if nil either return an error or
create/obtain a no-op implementation before calling SendResponseInfo and
subsequent Send... methods so all uses of eventChan (eventChan.SendResponseInfo,
etc.) are guarded.
- Around line 135-137: The call to http.NewRequestWithContext currently ignores
its error; in the function/method that constructs upstreamReq (where
upstreamReq, requestBody, ctx, upstreamURL are used) capture the returned error
instead of using `_`, check it, and handle it by returning or logging an
appropriate error (including context like upstreamURL and the original error)
before calling a.applyCodexHeaders; update references to upstreamReq to use the
validated request and ensure the handler returns an error response if request
construction fails.
- Around line 206-207: The current calculation of expiresAt uses
tokenResp.ExpiresIn-60 which can be negative if ExpiresIn < 60; update the logic
around tokenResp.ExpiresIn to clamp or adjust the TTL before building expiresAt:
compute a safe ttl (e.g. ttl := tokenResp.ExpiresIn - 60; if ttl <= 0 then
fallback to a positive value such as tokenResp.ExpiresIn/2 or 1) and then set
expiresAt = time.Now().Add(time.Duration(ttl) * time.Second). Update the code
where expiresAt is computed (look for the tokenResp.ExpiresIn and expiresAt
variables) so it never uses a negative duration.
- Around line 236-241: The code calls ctxutil.GetEventChan(ctx) and immediately
invokes eventChan.SendResponseInfo without a nil check, which can panic if
GetEventChan returns nil; update the block around ctxutil.GetEventChan(ctx) so
it mirrors the nil-check pattern used earlier (lines ~102-109): assign eventChan
:= ctxutil.GetEventChan(ctx), if eventChan != nil then call
eventChan.SendResponseInfo(&domain.ResponseInfo{Status: resp.StatusCode,
Headers: flattenHeaders(resp.Header), Body: string(body)}), otherwise skip
sending (or log) to avoid dereferencing a nil pointer.

In `@internal/core/codex_oauth_server.go`:
- Around line 14-20: The isRunning boolean on CodexOAuthServer is currently
accessed from multiple goroutines without synchronization; protect it to avoid
data races by either replacing isRunning with an uint32 and using
atomic.LoadUint32/atomic.StoreUint32 (and atomic.CompareAndSwapUint32 where
needed) or adding a sync.Mutex/sync.RWMutex on the CodexOAuthServer struct and
locking around all reads/writes to isRunning (e.g., in Start, Stop and any
status checks); update the Start method's goroutine and any Stop/IsRunning
checks to use the chosen atomic or mutex operations so all accesses are
synchronized.

In `@internal/repository/sqlite/codex_quota.go`:
- Around line 18-53: The Upsert in CodexQuotaRepository.Upsert currently does an
UPDATE-then-INSERT which breaks when a row exists but is soft-deleted and is
racy under concurrency; change it to a single atomic upsert using GORM's
OnConflict on the unique email column via
r.db.gorm.Clauses(clause.OnConflict{Columns: []clause.Column{{Name:"email"}},
DoUpdates: ...}) so the INSERT will update the existing record (and clear/set
DeletedAt=0, UpdatedAt, AccountID, PlanType, IsForbidden and the JSON windows)
instead of failing on unique constraint; ensure you build the model via
r.toModel(quota), set CreatedAt/UpdatedAt as needed, capture the resulting ID
and timestamps back into quota, and remove the separate UPDATE-then-INSERT flow
to avoid the race.

In `@internal/service/codex_task.go`:
- Around line 49-60: In GetRefreshInterval ensure negative configured values are
rejected: after parsing val into interval (in the GetRefreshInterval method of
CodexTaskService), add a lower-bound check (e.g. if interval < 0) and return
defaultCodexQuotaRefreshInterval (or clamp to 0 if desired) instead of returning
a negative value that would break time.Duration/timers; update the branch that
handles strconv.Atoi errors so negatives are treated the same as parse failures.
- Around line 116-170: The code in refreshAllQuotas ignores providerRepo.Update
errors (using `_ = s.providerRepo.Update(provider)`), which can drop persisted
rotated refresh tokens and cause repeated refresh failures; change this to check
the returned error from s.providerRepo.Update(provider) inside the
refreshAllQuotas method, log the error with context (provider.ID, account/email,
token rotation info), and decide whether to stop processing that provider
(continue) or escalate (return/emit alert) when the update fails so the new
token is not lost; reference the local vars tokenResp, config, provider and the
providerRepo.Update call to locate where to add the error handling and logging.
- Around line 192-216: The saveQuotaToDB function currently ignores the error
returned by s.quotaRepo.Upsert(quota); change it to capture that error and emit
an observability log containing the error and contextual identifiers (email,
accountID, planType, isForbidden) so failures to persist quota are visible; use
the service logger (e.g. s.logger) or the standard logger to record a clear
message including the Upsert error and relevant fields, and consider returning
or metrics incrementing if applicable.

In `@web/src/hooks/queries/use-settings.ts`:
- Around line 36-60: The optimistic update only updates settingsKeys.all but not
per-key caches, causing useSetting(key) consumers to see stale values; update
the code in the mutation handlers (onMutate, onError, onSettled) to also
set/restore/invalidate the per-key cache via settingsKeys.detail(key): in
onMutate after setting settingsKeys.all, call
queryClient.setQueryData(settingsKeys.detail(key), value) or the appropriate
object shape, return previous per-key value in the context; in onError restore
the per-key cache from context.previousDetail; and in onSettled call
queryClient.invalidateQueries({ queryKey: settingsKeys.detail(key) }) in
addition to invalidating settingsKeys.all so both list and detail caches stay in
sync for useSetting.

In `@web/src/lib/transport/interface.ts`:
- Around line 141-142: The return type for refreshAntigravityQuotas() is
incorrect: change its signature so the refreshed field is a boolean (not number)
to match the backend ForceRefreshQuotas() and the existing refreshCodexQuotas()
shape; update the interface declaration for refreshAntigravityQuotas() to return
Promise<{ success: boolean; refreshed: boolean }>, ensuring the method name
refreshAntigravityQuotas and the analogous refreshCodexQuotas are consistent
with backend bool semantics.

In `@web/src/pages/documentation/index.tsx`:
- Around line 238-239: Update the Gemini example URLs to match the new
OpenAI-style prefix by replacing the old Gemini path with the
/project/{project-slug}/v1/... prefix used in the OpenAI snippet (the code
element with code="POST
http://localhost:9880/project/{project-slug}/v1/chat/completions" and
id="openai-project"); locate the Gemini examples elsewhere in this file (the
other example blocks referenced in the review) and change their POST endpoints
to use /project/{project-slug}/v1/chat/completions (and any other v1 paths) so
all documentation examples are consistent.

In `@web/src/pages/providers/components/codex-token-import.tsx`:
- Around line 45-109: The popup handling and interval cleanup are incomplete:
update handleOAuth to check the result of window.open (oauthWindowRef) and if
null setOAuthStatus('error') and setError('Popup blocked') to allow retry; store
the interval ID in a ref (create oauthIntervalRef) instead of a local variable
and clear it whenever the OAuth flow ends, on window close, on component unmount
(inside the existing useEffect cleanup) and when cancelling by setting
oauthState to null; ensure the useEffect that subscribes to 'codex_oauth_result'
also clears oauthIntervalRef.current (clearInterval) when processing a matching
result; and update the UI logic to show the retry button when oauthStatus is
'error' (same visibility as 'idle') so users can re-run handleOAuth.
🧹 Nitpick comments (16)
internal/adapter/provider/codex/settings.go (1)

4-35: 考虑将 OAuth / Codex 端点与 Client ID 改为可配置

目前全部写死为常量,后续如果需要自部署调整端点、切换 Client ID 或进行灰度,会比较被动。建议支持环境变量覆盖并保留默认值。

web/src/index.css (1)

81-87: Codex 客户端颜色可与 provider-codex 对齐

已新增 --provider-codex,但 --client-codex 仍指向 --provider-openai。若期望 Codex 有独立色值,建议同步调整映射。

🎨 建议修改
-  --client-codex: var(--provider-openai);
+  --client-codex: var(--provider-codex);

Also applies to: 245-248

web/src/pages/routes/form.tsx (1)

8-8: 建议考虑从中央类型定义导入。

ProviderTypeKey 类型在 web/src/pages/providers/types.ts 中已有定义。当前的重复定义可能导致维护时遗漏同步更新。

♻️ 建议的重构
+import type { ProviderTypeKey } from '@/pages/providers/types';
-type ProviderTypeKey = 'antigravity' | 'kiro' | 'codex' | 'custom';
web/src/pages/providers/index.tsx (1)

270-321: Switch 禁用状态可能影响不相关的控件。

updateSetting.isPendingtrue 时,两个 Switch(Antigravity 和 Codex)都会被禁用,即使只有其中一个正在更新。这可能导致不必要的 UI 阻塞。

♻️ 建议的优化方案

可以通过跟踪当前正在更新的设置 key 来精确禁用对应的 Switch:

+ const [pendingSettingKey, setPendingSettingKey] = useState<string | null>(null);

  const handleToggleAutoSortAntigravity = (checked: boolean) => {
+   setPendingSettingKey('auto_sort_antigravity');
    updateSetting.mutate({
      key: 'auto_sort_antigravity',
      value: checked ? 'true' : 'false',
+   }, {
+     onSettled: () => setPendingSettingKey(null),
    });
  };

  // Antigravity Switch
  <Switch
    checked={autoSortAntigravity}
    onCheckedChange={handleToggleAutoSortAntigravity}
-   disabled={updateSetting.isPending}
+   disabled={pendingSettingKey === 'auto_sort_antigravity'}
  />
internal/core/codex_oauth_server.go (1)

50-55: w.Write 的错误被忽略。

虽然健康检查端点的写入失败通常不会造成严重问题,但忽略错误可能掩盖底层问题。

♻️ 建议处理或显式忽略错误
 mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
 	w.Header().Set("Content-Type", "application/json")
 	w.WriteHeader(http.StatusOK)
-	w.Write([]byte(`{"status":"ok","service":"codex-oauth"}`))
+	_, _ = w.Write([]byte(`{"status":"ok","service":"codex-oauth"}`))
 })
web/src/pages/providers/components/codex-provider-view.tsx (2)

204-218: 模型映射的优先级计算可能产生冲突。

priority: providerMappings.length * 10 + 1000 的计算方式可能在并发添加时产生相同的优先级值。

♻️ 建议使用时间戳或更可靠的方式
- priority: providerMappings.length * 10 + 1000,
+ priority: Date.now(),

或者考虑在后端自动分配优先级。


455-462: 刷新按钮使用非标准样式类。

className="btn bg-muted hover:bg-accent ..." 使用了 btn 类,这不是 shadcn/ui 的标准类名。建议使用统一的 Button 组件。

♻️ 建议使用 Button 组件
- <button
+ <Button
    onClick={handleRefresh}
    disabled={loading}
-   className="btn bg-muted hover:bg-accent text-foreground flex items-center gap-2 text-sm"
+   variant="outline"
+   size="sm"
+   className="flex items-center gap-2"
  >
    <RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
    {t('providers.refresh')}
- </button>
+ </Button>

同样适用于 Line 549-556 的 Usage 刷新按钮。

web/src/pages/providers/components/provider-row.tsx (1)

275-275: 注释中的 "Imagen" 应为 "Image"。

注释写的是 "显示 Claude 和 Imagen Quota",但根据 getImageQuotaInfo 函数和 UI 显示,应该是 "Image"。

✏️ 建议修正注释
- {/* 对于 Antigravity,显示 Claude 和 Imagen Quota */}
+ {/* 对于 Antigravity,显示 Claude 和 Image Quota */}
web/src/pages/requests/index.tsx (1)

44-44: ProviderTypeKey 类型定义重复。

此处定义的 ProviderTypeKeyweb/src/pages/providers/types.ts 中的定义重复。考虑从共享位置导入以保持 DRY 原则。

♻️ 建议从共享类型文件导入
+ import { ProviderTypeKey } from '@/pages/providers/types';

- type ProviderTypeKey = 'antigravity' | 'kiro' | 'codex' | 'custom';

如果 PROVIDER_TYPE_ORDERPROVIDER_TYPE_LABELS 也需要在多处使用,可以考虑将它们也移到共享类型文件中。

web/src/components/routes/ClientTypeRoutesContent.tsx (1)

49-49: 建议统一使用共享的类型定义。

此处定义的 ProviderTypeKey 类型与 web/src/pages/providers/types.ts:14 中的定义重复。建议从共享位置导入此类型,以避免两处定义不同步的风险。

♻️ 建议的修改
+import type { ProviderTypeKey } from '@/pages/providers/types';
-type ProviderTypeKey = 'antigravity' | 'kiro' | 'codex' | 'custom';
web/src/pages/client-routes/index.tsx (1)

61-83: 建议添加用户可见的错误反馈。

当前排序操作失败时仅通过 console.error 记录日志,用户无法得知操作失败。建议添加 toast 通知或其他用户反馈机制。

♻️ 建议添加错误提示
+import { toast } from '@/components/ui'; // 或使用项目中的通知组件

 const handleSortAntigravity = async () => {
   setIsSorting(true);
   try {
     await transport.sortAntigravityRoutes();
     queryClient.invalidateQueries({ queryKey: routeKeys.list() });
+    toast.success(t('routes.sortSuccess'));
   } catch (error) {
     console.error('Failed to sort Antigravity routes:', error);
+    toast.error(t('routes.sortError'));
   } finally {
     setIsSorting(false);
   }
 };
web/src/contexts/codex-quotas-context.tsx (1)

26-28: 建议使用 useCallback 优化 getQuotaForProvider

getQuotaForProvider 函数在每次渲染时都会重新创建,可能导致使用此函数作为依赖的子组件不必要的重新渲染。

♻️ 建议的优化
+import { createContext, useContext, useCallback, type ReactNode } from 'react';

 export function CodexQuotasProvider({ children, enabled = true }: CodexQuotasProviderProps) {
   const { data: quotas, isLoading } = useCodexBatchQuotas(enabled);

-  const getQuotaForProvider = (providerId: number): CodexQuotaData | undefined => {
-    return quotas?.[providerId];
-  };
+  const getQuotaForProvider = useCallback(
+    (providerId: number): CodexQuotaData | undefined => {
+      return quotas?.[providerId];
+    },
+    [quotas]
+  );

   return (
internal/repository/sqlite/codex_quota.go (1)

76-83: 删除时间戳建议复用 toTimestamp 保持一致

Line 77 直接用 UnixMilli,与其他写入路径不一致,建议统一以避免格式偏差。

♻️ 建议调整
- now := time.Now().UnixMilli()
+ now := toTimestamp(time.Now())
internal/adapter/provider/codex/oauth.go (2)

222-247: ID Token 签名验证缺失需注意

当前实现未验证 JWT 签名,仅解析 payload。虽然注释中已说明这一点,但在安全敏感场景下,建议后续增加签名验证。当前因 token 直接从 OpenAI HTTPS 端点获取,风险可控。


276-420: 考虑简化 API 响应结构体

codexUsageAPIResponse 结构体为处理 snake_case 和 camelCase 字段重复定义了大量字段,代码冗长。虽然当前实现可行,但可考虑使用自定义 UnmarshalJSON 方法或 mapstructure 库来简化处理。

当前方案的优点是显式且易于理解,如果 API 响应格式稳定,可保持现状。

internal/handler/codex.go (1)

345-413: Token 刷新逻辑重复

GetProviderUsage 中的 token 刷新逻辑与 GetBatchQuotas(lines 499-516)中的实现高度相似。建议提取为共享的辅助方法以减少代码重复。

当前实现功能正确,可作为后续重构考虑。

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1c95cbc and 27b3fde.

📒 Files selected for processing (48)
  • cmd/maxx/main.go
  • internal/adapter/provider/codex/adapter.go
  • internal/adapter/provider/codex/oauth.go
  • internal/adapter/provider/codex/service.go
  • internal/adapter/provider/codex/settings.go
  • internal/core/codex_oauth_server.go
  • internal/core/database.go
  • internal/core/server.go
  • internal/core/task.go
  • internal/domain/model.go
  • internal/handler/codex.go
  • internal/handler/project_proxy.go
  • internal/handler/static.go
  • internal/repository/interfaces.go
  • internal/repository/sqlite/codex_quota.go
  • internal/repository/sqlite/models.go
  • internal/service/admin.go
  • internal/service/antigravity_task.go
  • internal/service/codex_task.go
  • web/src/components/routes/ClientTypeRoutesContent.tsx
  • web/src/contexts/codex-quotas-context.tsx
  • web/src/hooks/queries/index.ts
  • web/src/hooks/queries/use-providers.ts
  • web/src/hooks/queries/use-settings.ts
  • web/src/index.css
  • web/src/lib/theme.ts
  • web/src/lib/transport/http-transport.ts
  • web/src/lib/transport/index.ts
  • web/src/lib/transport/interface.ts
  • web/src/lib/transport/types.ts
  • web/src/locales/en.json
  • web/src/locales/zh.json
  • web/src/pages/client-routes/index.tsx
  • web/src/pages/documentation/index.tsx
  • web/src/pages/projects/tabs/overview.tsx
  • web/src/pages/providers/components/codex-provider-view.tsx
  • web/src/pages/providers/components/codex-token-import.tsx
  • web/src/pages/providers/components/provider-edit-flow.tsx
  • web/src/pages/providers/components/provider-row.tsx
  • web/src/pages/providers/components/select-type-step.tsx
  • web/src/pages/providers/create-layout.tsx
  • web/src/pages/providers/hooks/use-provider-navigation.ts
  • web/src/pages/providers/index.tsx
  • web/src/pages/providers/types.ts
  • web/src/pages/requests/index.tsx
  • web/src/pages/routes/form.tsx
  • web/src/pages/settings/index.tsx
  • web/src/pages/stats/index.tsx
✅ Files skipped from review due to trivial changes (1)
  • internal/service/antigravity_task.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • web/src/pages/settings/index.tsx
  • web/src/locales/en.json
🧰 Additional context used
🧬 Code graph analysis (25)
internal/repository/sqlite/models.go (1)
internal/domain/model.go (1)
  • CodexQuota (532-560)
internal/repository/interfaces.go (2)
internal/domain/model.go (1)
  • CodexQuota (532-560)
internal/repository/sqlite/models.go (2)
  • CodexQuota (180-189)
  • CodexQuota (191-191)
internal/core/server.go (2)
internal/handler/codex.go (1)
  • CodexHandler (20-25)
internal/core/codex_oauth_server.go (1)
  • CodexOAuthServer (16-20)
web/src/pages/routes/form.tsx (1)
web/src/pages/providers/types.ts (1)
  • ProviderTypeKey (15-15)
web/src/hooks/queries/use-providers.ts (3)
web/src/hooks/queries/index.ts (2)
  • useCodexBatchQuotas (18-18)
  • providerKeys (7-7)
web/src/lib/transport/index.ts (1)
  • getTransport (105-105)
web/src/lib/transport/factory.ts (1)
  • getTransport (79-102)
internal/core/codex_oauth_server.go (2)
internal/handler/codex.go (1)
  • CodexHandler (20-25)
internal/adapter/provider/codex/settings.go (1)
  • OAuthCallbackPort (16-16)
web/src/pages/requests/index.tsx (1)
web/src/pages/providers/types.ts (1)
  • ProviderTypeKey (15-15)
web/src/pages/providers/components/codex-token-import.tsx (7)
web/src/pages/providers/hooks/use-provider-navigation.ts (1)
  • useProviderNavigation (3-15)
web/src/lib/transport/http-transport.ts (1)
  • createProvider (108-111)
web/src/hooks/queries/index.ts (1)
  • useCreateProvider (10-10)
web/src/hooks/queries/use-providers.ts (1)
  • useCreateProvider (37-47)
internal/adapter/provider/codex/service.go (1)
  • CodexTokenValidationResult (15-29)
web/src/lib/transport/types.ts (3)
  • CodexTokenValidationResult (479-493)
  • CodexOAuthResult (495-510)
  • CreateProviderData (73-79)
web/src/pages/providers/types.ts (1)
  • CODEX_COLOR (88-88)
web/src/contexts/codex-quotas-context.tsx (4)
web/src/lib/transport/index.ts (1)
  • CodexQuotaData (64-64)
web/src/lib/transport/types.ts (1)
  • CodexQuotaData (534-543)
web/src/hooks/queries/index.ts (1)
  • useCodexBatchQuotas (18-18)
web/src/hooks/queries/use-providers.ts (1)
  • useCodexBatchQuotas (133-142)
web/src/hooks/queries/use-settings.ts (2)
web/src/lib/query-client.ts (1)
  • queryClient (3-14)
web/src/hooks/queries/index.ts (1)
  • settingsKeys (87-87)
web/src/pages/providers/index.tsx (3)
web/src/hooks/queries/use-settings.ts (2)
  • useSettings (15-20)
  • useUpdateSetting (30-63)
web/src/pages/providers/types.ts (1)
  • ProviderTypeKey (15-15)
web/src/contexts/codex-quotas-context.tsx (1)
  • CodexQuotasProvider (23-35)
internal/core/task.go (1)
internal/service/codex_task.go (1)
  • CodexTaskService (21-28)
web/src/pages/providers/types.ts (1)
web/src/lib/theme.ts (1)
  • getProviderColorVar (540-542)
internal/domain/model.go (2)
internal/repository/sqlite/models.go (2)
  • ModelMapping (149-161)
  • ModelMapping (163-163)
web/src/lib/transport/types.ts (2)
  • ModelMapping (413-429)
  • ProviderConfigCodex (38-51)
web/src/components/routes/ClientTypeRoutesContent.tsx (1)
web/src/pages/providers/types.ts (1)
  • ProviderTypeKey (15-15)
web/src/pages/providers/components/provider-row.tsx (4)
web/src/lib/transport/index.ts (1)
  • CodexQuotaData (64-64)
web/src/lib/transport/types.ts (1)
  • CodexQuotaData (534-543)
web/src/contexts/codex-quotas-context.tsx (1)
  • useCodexQuotaFromContext (46-49)
web/src/lib/utils.ts (1)
  • cn (8-10)
web/src/lib/transport/types.ts (4)
internal/domain/model.go (3)
  • ProviderConfigCodex (72-108)
  • ProviderConfig (110-115)
  • CodexRateLimitInfo (524-529)
internal/adapter/provider/codex/service.go (1)
  • CodexTokenValidationResult (15-29)
internal/adapter/provider/codex/oauth.go (3)
  • CodexUsageWindow (254-259)
  • CodexRateLimitInfo (262-267)
  • CodexUsageResponse (270-274)
internal/handler/codex.go (1)
  • CodexBatchQuotaResult (458-460)
web/src/lib/transport/http-transport.ts (3)
internal/adapter/provider/codex/service.go (1)
  • CodexTokenValidationResult (15-29)
web/src/lib/transport/types.ts (3)
  • CodexTokenValidationResult (479-493)
  • CodexUsageResponse (527-531)
  • CodexQuotaData (534-543)
internal/adapter/provider/codex/oauth.go (1)
  • CodexUsageResponse (270-274)
internal/service/admin.go (4)
internal/domain/model.go (2)
  • ClientType (6-6)
  • ClientTypeCodex (10-10)
web/src/lib/theme.ts (1)
  • ClientType (26-26)
web/src/lib/transport/index.ts (1)
  • ClientType (8-8)
web/src/lib/transport/types.ts (1)
  • ClientType (8-8)
web/src/pages/providers/components/provider-edit-flow.tsx (1)
web/src/pages/providers/components/codex-provider-view.tsx (1)
  • CodexProviderView (320-634)
web/src/pages/providers/components/select-type-step.tsx (4)
web/src/pages/providers/hooks/use-provider-navigation.ts (1)
  • useProviderNavigation (3-15)
web/src/pages/providers/types.ts (1)
  • PROVIDER_TYPE_CONFIGS (31-72)
web/src/components/ui/index.ts (1)
  • Button (2-2)
web/src/components/ui/button.tsx (1)
  • Button (58-58)
internal/repository/sqlite/codex_quota.go (4)
internal/repository/sqlite/db.go (1)
  • DB (17-20)
internal/repository/interfaces.go (1)
  • CodexQuotaRepository (149-158)
internal/domain/model.go (2)
  • CodexQuota (532-560)
  • CodexQuotaWindow (516-521)
internal/repository/sqlite/models.go (6)
  • CodexQuota (180-189)
  • CodexQuota (191-191)
  • SoftDeleteModel (37-40)
  • BaseModel (30-34)
  • LongText (17-17)
  • LongText (20-27)
internal/adapter/provider/codex/oauth.go (4)
internal/adapter/provider/codex/settings.go (7)
  • OAuthClientID (10-10)
  • OAuthRedirectURI (17-17)
  • OAuthScopes (13-13)
  • OpenAIAuthURL (6-6)
  • OpenAITokenURL (7-7)
  • CodexUsageURL (23-23)
  • CodexUserAgent (29-29)
web/src/lib/transport/index.ts (3)
  • CodexUsageWindow (61-61)
  • CodexRateLimitInfo (62-62)
  • CodexUsageResponse (63-63)
web/src/lib/transport/types.ts (3)
  • CodexUsageWindow (513-518)
  • CodexRateLimitInfo (520-525)
  • CodexUsageResponse (527-531)
internal/domain/model.go (1)
  • CodexRateLimitInfo (524-529)
internal/adapter/provider/codex/service.go (1)
internal/adapter/provider/codex/oauth.go (5)
  • CodexUsageWindow (254-259)
  • RefreshAccessToken (186-220)
  • ParseIDToken (224-247)
  • PKCEChallenge (18-21)
  • GeneratePKCEChallenge (107-125)
web/src/lib/transport/interface.ts (3)
internal/adapter/provider/codex/service.go (1)
  • CodexTokenValidationResult (15-29)
web/src/lib/transport/types.ts (3)
  • CodexTokenValidationResult (479-493)
  • CodexUsageResponse (527-531)
  • CodexQuotaData (534-543)
internal/adapter/provider/codex/oauth.go (1)
  • CodexUsageResponse (270-274)
🔇 Additional comments (69)
web/src/pages/projects/tabs/overview.tsx (1)

49-49: 路径拼接更新符合新路由前缀,OK。

web/src/pages/providers/hooks/use-provider-navigation.ts (1)

11-11: 新增 Codex 跳转入口实现清晰。

internal/handler/static.go (1)

326-330: 前缀判断与新的 /project/ 路由一致,OK。

internal/repository/sqlite/models.go (1)

179-191: CodexQuota 模型与自动迁移注册保持一致,OK。

Also applies to: 377-377

internal/handler/project_proxy.go (1)

32-70: 请确认是否需要兼容旧的 /{slug}/... 路径。
这是一次显式的行为变更;如果已有外部客户端依赖旧路径,建议提供临时兼容或 308 重定向。

✅ 可选兼容示例(如需)
-  // Must start with /project/
-  if !strings.HasPrefix(path, "/project/") {
-    return "", "", false
-  }
-
-  // Remove /project/ prefix and split
-  path = strings.TrimPrefix(path, "/project/")
+  // Accept new /project/ prefix; optionally fall back to legacy /{slug}/...
+  if strings.HasPrefix(path, "/project/") {
+    path = strings.TrimPrefix(path, "/project/")
+  } else if strings.HasPrefix(path, "/") {
+    // Legacy format: /{slug}/...
+    path = strings.TrimPrefix(path, "/")
+  } else {
+    return "", "", false
+  }
web/src/pages/stats/index.tsx (1)

684-686: Provider 排序加入 codex 很好

类型排序与新增 Provider 保持一致,逻辑清晰。

web/src/pages/providers/create-layout.tsx (1)

6-17: Codex 创建流程路由接入正常

新增 codex 路由与现有创建流程一致,符合预期。

web/src/hooks/queries/index.ts (1)

6-19: 导出 useCodexBatchQuotas 没问题

新增导出与现有 Provider hooks 风格一致。

web/src/pages/providers/components/provider-edit-flow.tsx (2)

41-41: LGTM!

正确添加了 CodexProviderView 组件的导入,与现有的 AntigravityProviderViewKiroProviderView 导入保持一致。


418-436: LGTM!

Codex provider 的条件分支实现正确,完全遵循了 Antigravity 和 Kiro provider 的既有模式:

  • 渲染 CodexProviderView 组件
  • 正确绑定 onDeleteonClose 回调
  • 复用 DeleteConfirmModal 组件处理删除确认流程
internal/service/admin.go (1)

567-571: LGTM!

Codex provider 类型的客户端支持配置正确:

  • 遵循了 antigravitykiro 等其他 provider 类型的既有模式
  • 仅支持 ClientTypeCodex 协议,符合 Codex 原生协议的设计意图
  • 注释清晰说明了支持范围
web/src/pages/routes/form.tsx (2)

10-17: LGTM!

PROVIDER_TYPE_ORDERPROVIDER_TYPE_LABELS 的更新正确添加了 Codex 支持,与 provider 分组逻辑保持一致。


46-47: LGTM!

groupedProviders 对象正确初始化了 codex 空数组,确保 Codex 类型的 provider 能够被正确分组。

web/src/hooks/queries/use-providers.ts (1)

131-142: LGTM!

useCodexBatchQuotas hook 实现正确:

  • 遵循了 useAntigravityBatchQuotas 的既有模式
  • Query key 命名清晰且唯一
  • 10 分钟的 refetchIntervalstaleTime 配置与其他配额 hook 保持一致
  • enabled 参数支持条件性数据获取
internal/core/server.go (3)

78-78: LGTM!

Codex API 路由注册正确,遵循了 /api/antigravity//api/kiro/ 的既有模式。


136-141: LGTM!

Codex OAuth 回调服务器的启动逻辑实现正确:

  • 双重 nil 检查(Components 和 CodexOAuthServer)确保在组件未配置时安全跳过
  • 错误仅记录日志而不阻止主服务器启动,符合可选组件的处理模式

199-206: LGTM!

Codex OAuth 回调服务器的关闭逻辑实现正确:

  • 2 秒超时与 pprof manager 的关闭超时一致
  • 正确使用了独立的 context 和 cancel 函数
  • 错误处理与其他组件保持一致
web/src/lib/theme.ts (1)

20-21: LGTM!

ProviderType 类型正确扩展以包含 'kiro''codex',使 getProviderColorVar('codex') 等函数能够类型安全地使用。CSS 变量 --provider-codex 已在 web/src/index.css 第 81 行定义,Tailwind 自定义属性也在第 247 行配置完成,整个实现链路完整。

internal/core/task.go (1)

24-26: Codex 后台刷新接入清晰

依赖注入与启动逻辑与现有 Antigravity 任务一致,便于维护与理解。

Also applies to: 64-67, 196-214

internal/repository/interfaces.go (1)

149-158: CodexQuotaRepository 接口定义清楚

方法集合与现有配额仓储接口保持一致,后续实现/替换成本低。

web/src/lib/transport/index.ts (1)

57-64: Codex 类型导出补全 OK

入口导出齐全,便于调用方统一引用类型。

web/src/pages/providers/components/select-type-step.tsx (1)

10-31: Codex 入口接入一致

新增入口与导航分支清晰,UI 状态与既有类型保持一致。

Also applies to: 143-171

internal/domain/model.go (1)

72-108: Codex 配置与配额模型补充完整

新增结构与系统设置键定义清晰,和现有领域模型风格保持一致。

Also applies to: 114-115, 449-450, 515-560

web/src/pages/providers/index.tsx (2)

32-50: 设置集成和切换处理器实现正确。

使用 useSettingsuseUpdateSetting 钩子进行设置管理的方式正确,乐观更新机制(在 useUpdateSetting 中实现)可以防止 Switch 切换时的界面闪烁。


249-339: CodexQuotasProvider 嵌套和 UI 结构正确。

CodexQuotasProvider 正确嵌套在 AntigravityQuotasProvider 内部,确保两种配额上下文都可用。Antigravity 和 Codex 的 UI 控件遵循一致的模式,代码结构清晰。

internal/core/codex_oauth_server.go (2)

63-72: 服务器启动错误处理和生命周期管理合理。

使用 goroutine 启动服务器并正确处理 http.ErrServerClosed 的方式是正确的。Start 方法在返回前设置 isRunning = true 可以确保调用者知道服务器已启动。


76-94: 优雅关闭实现正确。

使用带超时的 context 进行优雅关闭,并在超时后调用 Close() 作为后备方案,这是标准的最佳实践。

web/src/pages/providers/components/codex-provider-view.tsx (2)

66-105: 日期格式化辅助函数实现完善。

formatDateformatRelativeTime 函数正确处理了空值和异常情况,使用 try-catch 确保不会因为无效日期而崩溃。


336-353: 缓存数据与手动刷新数据的合并逻辑清晰。

使用 useMemo 优先显示手动刷新的数据,否则使用缓存数据的方式合理。这确保了用户看到最新的数据,同时在没有手动刷新时也能显示缓存内容。

internal/core/database.go (2)

46-46: Codex 仓库和组件集成正确。

CodexQuotaRepoCodexHandlerCodexOAuthServer 的添加遵循了现有的 Antigravity 和 Kiro 的模式,依赖注入链完整且一致。

Also applies to: 76-77, 110-110, 142-142


340-341: CodexHandler 和 CodexOAuthServer 的初始化和装配正确。

组件初始化顺序正确:先创建 codexHandler,再将其传入 NewCodexOAuthServer。组件在 ServerComponents 结构中正确赋值。

Also applies to: 359-360

web/src/pages/providers/components/provider-row.tsx (2)

141-192: Codex 配额信息提取函数实现正确。

getCodex5HQuotaInfogetCodexWeekQuotaInfo 函数正确处理了两种重置时间格式(resetAt Unix 时间戳和 resetAfterSeconds),并计算剩余百分比。逻辑与现有的 Antigravity 配额函数保持一致。


333-393: Codex 配额 UI 渲染与 Antigravity 保持一致。

5H 和 Week 配额的进度条显示逻辑与 Claude/Image 配额完全一致,包括颜色阈值(绿/黄/红)和最后更新时间显示。代码结构清晰,易于维护。

web/src/pages/requests/index.tsx (2)

44-53: Codex 类型支持添加正确。

ProviderTypeKeyPROVIDER_TYPE_ORDERPROVIDER_TYPE_LABELS 的更新保持了与其他 provider 类型的一致性。


762-768: Provider 分组逻辑正确扩展。

Codex group 的初始化与其他 provider 类型保持一致,分组逻辑正确处理了未知类型(归入 custom)。

web/src/components/routes/ClientTypeRoutesContent.tsx (2)

151-189: LGTM!

groupedAvailableProviders 的实现正确地添加了 Codex 类型支持,分组逻辑和搜索过滤功能保持一致。


46-46: 保留 AntigravityQuotasProvider,该上下文仍被子组件使用。

ProviderRowContent 组件(通过 SortableProviderRow 使用)在第 235 行使用 useAntigravityQuotaFromContext(provider.id) 来获取 Antigravity 配额信息,用于显示 Claude 和 Image 模型的额度百分比和重置时间。因此,AntigravityQuotasProvider 仍然是必要的,不应移除。

web/src/lib/transport/interface.ts (1)

156-164: LGTM!

Codex API 部分结构清晰,方法命名与现有的 Antigravity/Kiro API 保持一致的风格。类型导入和方法签名正确。

web/src/pages/client-routes/index.tsx (2)

42-59: LGTM!

useMemo 的实现高效地检测 Antigravity 和 Codex 路由的存在,使用了提前退出优化(if (hasAntigravity && hasCodex) break),依赖数组正确包含了所有相关依赖。


171-201: LGTM!

排序按钮的条件渲染逻辑正确:仅在查看全局路由(selectedProjectId === '0')且存在相应类型路由时显示。UI 反馈(animate-pulse)在排序进行时提供了良好的视觉提示。

web/src/lib/transport/types.ts (3)

38-58: LGTM!

ProviderConfigCodex 接口定义与后端 Go 结构体 internal/domain/model.go 中的定义保持一致,字段命名和可选性正确。ProviderConfig 的扩展也正确地添加了 codex 配置项。


477-548: LGTM!

Codex 相关类型定义完整且与后端 Go 结构体保持一致:

  • CodexTokenValidationResult 对应 internal/adapter/provider/codex/service.go
  • CodexUsageWindowCodexRateLimitInfoCodexUsageResponse 对应 internal/adapter/provider/codex/oauth.go
  • CodexQuotaDataCodexBatchQuotaResult 结构清晰,类型正确

306-306: LGTM!

codex_oauth_result 已正确添加到 WSMessageType 联合类型中,与现有的 antigravity_oauth_result 模式一致。

web/src/contexts/codex-quotas-context.tsx (1)

37-48: LGTM!

提供了两种访问 context 的方式是个好的设计:

  • useCodexQuotasContext() 在 Provider 外部使用时抛出错误,适用于必须在 Provider 内的场景
  • useCodexQuotaFromContext() 返回 undefined 而非抛出错误,提供了更灵活的访问方式
cmd/maxx/main.go (4)

219-237: LGTM!

Codex 任务服务的创建和注册遵循了与 Antigravity 相同的模式,依赖注入正确,后台任务配置完整。


321-322: LGTM!

Codex Handler 的创建、任务服务注入和 HTTP 路由注册均正确,遵循了现有的 Antigravity/Kiro Handler 模式。

Also applies to: 339-339


436-439: LGTM!

Codex OAuth 服务器在优雅关闭流程中正确停止,位于 HTTP 服务器关闭之前,确保了正确的关闭顺序。


375-379: 端口 1455 是硬编码的,无法配置。

CodexOAuthServer 的端口 1455 在 internal/adapter/provider/codex/settings.go 中硬编码为常量 OAuthCallbackPort,这是必需的,因为 OpenAI 要求固定的回调地址 http://localhost:1455/auth/callback。与 Antigravity OAuth 无冲突:Antigravity 使用 OAuthManager 进行内存会话管理,不监听独立端口。

internal/repository/sqlite/codex_quota.go (2)

56-74: 查询与列表逻辑清晰

软删除过滤 + updated_at 倒序返回符合使用场景。


86-128: Domain/Model 映射覆盖完整

窗口 JSON、禁用标记与时间字段处理到位。

internal/service/codex_task.go (1)

62-113: 刷新/广播/排序流程整体清晰

手动与定时刷新路径一致,自动排序与辅助函数职责明确。

Also applies to: 180-190, 218-229, 231-298, 300-445, 448-481

web/src/pages/providers/types.ts (2)

4-56: Codex Provider 配置集成顺畅

类型扩展、图标/颜色与展示信息补充齐全。


215-232: 创建流程类型同步更新

ProviderFormData 与 CreateStep 覆盖 codex-import,避免表单状态遗漏。

web/src/lib/transport/http-transport.ts (1)

39-41: Codex 与排序 API 封装一致

新增方法与现有 transport 风格一致,返回类型清晰。

Also applies to: 430-433, 479-524

web/src/locales/zh.json (1)

174-195: Codex 与配额相关文案补全到位

新增的排序/配额/订阅条目覆盖 UI 所需语义。

Also applies to: 279-281, 464-477

internal/adapter/provider/codex/service.go (2)

14-84: Token 校验流程清晰

refresh → 解析 ID token → 填充字段顺序明确,便于前端消费。


86-208: OAuth 会话管理实现完善

状态生成、PKCE 会话存储与过期清理都齐全。

internal/adapter/provider/codex/oauth.go (5)

17-55: LGTM!

OAuth 相关结构体定义清晰,CodexAuthInfo 中使用 any 类型处理订阅时间的多种格式(string/float64/int64)是合理的设计选择。


57-104: LGTM!

IDTokenClaims 的 getter 方法提供了合理的回退逻辑,formatSubscriptionTime 正确处理了多种输入类型。


106-144: LGTM!

PKCE 实现遵循 RFC 7636 规范:使用 crypto/rand 生成 32 字节(256 位熵)的 code_verifier,采用 SHA256 和 S256 方法生成 code_challenge。OAuth URL 构建包含所有必需参数。


146-220: LGTM!

Token 交换和刷新函数实现正确:15秒超时合理,错误处理完善,响应解析规范。


528-564: LGTM!

FetchUsage 函数实现规范:正确设置 Authorization、User-Agent 和可选的 Chatgpt-Account-Id 请求头,15秒超时合理,错误处理完善。

internal/adapter/provider/codex/adapter.go (3)

21-72: LGTM!

Adapter 初始化逻辑正确:工厂注册模式清晰,token 缓存初始化正确处理了过期 token 的情况。


370-413: LGTM!

HTTP 客户端配置合理,600秒超时适用于流式响应。响应头复制正确跳过了 hop-by-hop 头部。


459-503: LGTM!

请求头处理逻辑清晰:客户端头部透传优先,必需头部强制覆盖,Codex 特定头部仅在客户端未提供时设置默认值。

internal/handler/codex.go (4)

52-107: LGTM!

路由处理虽然使用手动字符串解析,但对于有限的端点数量是可接受的。路由顺序正确(/providers/quotas/provider/:id 之前)。


189-257: LGTM!

OAuth 回调正确验证 state 参数防止 CSRF 攻击,token 交换使用固定的 redirect URI。ID token 解析失败时静默处理是合理的,因为声明信息是补充性的。


465-534: LGTM!

批量配额获取设计合理:优先返回数据库缓存(即使过期),API 调用作为后备。单个 provider 失败不影响整体结果,适合批量操作场景。


659-776: LGTM!

OAuth 结果页面设计简洁,内联 CSS 无外部依赖。window.close() 在某些浏览器中可能不生效(如果窗口非脚本打开),但这是已知的浏览器行为,提供的文字提示已足够。

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +135 to +137
// Retry request
upstreamReq, _ = http.NewRequestWithContext(ctx, "POST", upstreamURL, bytes.NewReader(requestBody))
a.applyCodexHeaders(upstreamReq, req, accessToken, config.AccountID)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

忽略的错误可能隐藏问题

Line 136 中 http.NewRequestWithContext 的错误被忽略(使用 _)。虽然此处失败的可能性很低(因为相同参数在之前已成功调用),但建议添加错误处理以便于调试。

🔧 建议的修复
-		upstreamReq, _ = http.NewRequestWithContext(ctx, "POST", upstreamURL, bytes.NewReader(requestBody))
+		upstreamReq, err = http.NewRequestWithContext(ctx, "POST", upstreamURL, bytes.NewReader(requestBody))
+		if err != nil {
+			return domain.NewProxyErrorWithMessage(err, true, "failed to create retry request")
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Retry request
upstreamReq, _ = http.NewRequestWithContext(ctx, "POST", upstreamURL, bytes.NewReader(requestBody))
a.applyCodexHeaders(upstreamReq, req, accessToken, config.AccountID)
// Retry request
upstreamReq, err = http.NewRequestWithContext(ctx, "POST", upstreamURL, bytes.NewReader(requestBody))
if err != nil {
return domain.NewProxyErrorWithMessage(err, true, "failed to create retry request")
}
a.applyCodexHeaders(upstreamReq, req, accessToken, config.AccountID)
🤖 Prompt for AI Agents
In `@internal/adapter/provider/codex/adapter.go` around lines 135 - 137, The call
to http.NewRequestWithContext currently ignores its error; in the
function/method that constructs upstreamReq (where upstreamReq, requestBody,
ctx, upstreamURL are used) capture the returned error instead of using `_`,
check it, and handle it by returning or logging an appropriate error (including
context like upstreamURL and the original error) before calling
a.applyCodexHeaders; update references to upstreamReq to use the validated
request and ensure the handler returns an error response if request construction
fails.

Comment on lines +206 to +207
// Calculate expiration time (with 60s buffer)
expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-60) * time.Second)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ExpiresIn 小于 60 时可能导致负数时长

如果 tokenResp.ExpiresIn 小于 60 秒,ExpiresIn-60 会产生负数,导致 expiresAt 被设置为过去的时间。这可能触发持续的 token 刷新尝试。

🔧 建议的修复
-	expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-60) * time.Second)
+	bufferSeconds := 60
+	if tokenResp.ExpiresIn < bufferSeconds {
+		bufferSeconds = 0
+	}
+	expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-bufferSeconds) * time.Second)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Calculate expiration time (with 60s buffer)
expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-60) * time.Second)
// Calculate expiration time (with 60s buffer)
bufferSeconds := 60
if tokenResp.ExpiresIn < bufferSeconds {
bufferSeconds = 0
}
expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-bufferSeconds) * time.Second)
🤖 Prompt for AI Agents
In `@internal/adapter/provider/codex/adapter.go` around lines 206 - 207, The
current calculation of expiresAt uses tokenResp.ExpiresIn-60 which can be
negative if ExpiresIn < 60; update the logic around tokenResp.ExpiresIn to clamp
or adjust the TTL before building expiresAt: compute a safe ttl (e.g. ttl :=
tokenResp.ExpiresIn - 60; if ttl <= 0 then fallback to a positive value such as
tokenResp.ExpiresIn/2 or 1) and then set expiresAt =
time.Now().Add(time.Duration(ttl) * time.Second). Update the code where
expiresAt is computed (look for the tokenResp.ExpiresIn and expiresAt variables)
so it never uses a negative duration.

Comment on lines +236 to +241
eventChan := ctxutil.GetEventChan(ctx)
eventChan.SendResponseInfo(&domain.ResponseInfo{
Status: resp.StatusCode,
Headers: flattenHeaders(resp.Header),
Body: string(body),
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

潜在的空指针解引用

eventChan 从 context 获取后未进行 nil 检查就直接调用方法。如果 ctxutil.GetEventChan(ctx) 返回 nil,将导致 panic。

对比 Line 102-109 中的实现有 nil 检查,此处应保持一致。

🔧 建议的修复
 	// Send events via EventChannel
 	eventChan := ctxutil.GetEventChan(ctx)
+	if eventChan == nil {
+		// Write response without event tracking
+		copyResponseHeaders(w.Header(), resp.Header)
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(resp.StatusCode)
+		_, _ = w.Write(body)
+		return nil
+	}
 	eventChan.SendResponseInfo(&domain.ResponseInfo{
🤖 Prompt for AI Agents
In `@internal/adapter/provider/codex/adapter.go` around lines 236 - 241, The code
calls ctxutil.GetEventChan(ctx) and immediately invokes
eventChan.SendResponseInfo without a nil check, which can panic if GetEventChan
returns nil; update the block around ctxutil.GetEventChan(ctx) so it mirrors the
nil-check pattern used earlier (lines ~102-109): assign eventChan :=
ctxutil.GetEventChan(ctx), if eventChan != nil then call
eventChan.SendResponseInfo(&domain.ResponseInfo{Status: resp.StatusCode,
Headers: flattenHeaders(resp.Header), Body: string(body)}), otherwise skip
sending (or log) to avoid dereferencing a nil pointer.

Comment on lines +264 to +273
func (a *CodexAdapter) handleStreamResponse(ctx context.Context, w http.ResponseWriter, resp *http.Response) error {
eventChan := ctxutil.GetEventChan(ctx)

// Send initial response info
eventChan.SendResponseInfo(&domain.ResponseInfo{
Status: resp.StatusCode,
Headers: flattenHeaders(resp.Header),
Body: "[streaming]",
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

同样的空指针解引用风险

handleStreamResponse 中也存在相同问题,eventChan 未进行 nil 检查。建议添加防御性检查或确保调用方总是提供有效的 event channel。

🤖 Prompt for AI Agents
In `@internal/adapter/provider/codex/adapter.go` around lines 264 - 273,
handleStreamResponse currently calls ctxutil.GetEventChan(ctx) and uses
eventChan.SendResponseInfo without checking for nil, risking a nil pointer
dereference; add a defensive nil check after obtaining eventChan in
handleStreamResponse (or ensure ctxutil.GetEventChan always returns a non-nil
implementation) and if nil either return an error or create/obtain a no-op
implementation before calling SendResponseInfo and subsequent Send... methods so
all uses of eventChan (eventChan.SendResponseInfo, etc.) are guarded.

Comment on lines +14 to +20
// CodexOAuthServer handles OAuth callbacks on localhost:1455
// This is required because OpenAI uses a fixed redirect URI
type CodexOAuthServer struct {
codexHandler *handler.CodexHandler
httpServer *http.Server
isRunning bool
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

isRunning 字段存在数据竞争风险。

isRunning 字段在多个 goroutine 中被访问(Start 中的 goroutine 和主 goroutine),但没有同步保护。这可能导致数据竞争。

🔒️ 建议使用 sync.Mutex 或 atomic 保护
 import (
 	"context"
 	"fmt"
 	"log"
 	"net/http"
+	"sync"
 	"time"

 	"github.com/awsl-project/maxx/internal/adapter/provider/codex"
 	"github.com/awsl-project/maxx/internal/handler"
 )

 type CodexOAuthServer struct {
 	codexHandler *handler.CodexHandler
 	httpServer   *http.Server
-	isRunning    bool
+	isRunning    bool
+	mu           sync.Mutex
 }

 func (s *CodexOAuthServer) Start(ctx context.Context) error {
+	s.mu.Lock()
 	if s.isRunning {
+		s.mu.Unlock()
 		log.Printf("[CodexOAuth] Server already running")
 		return nil
 	}
+	s.mu.Unlock()
 	// ... rest of Start
+	s.mu.Lock()
 	s.isRunning = true
+	s.mu.Unlock()
 	// ...
 }

 func (s *CodexOAuthServer) IsRunning() bool {
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	return s.isRunning
 }
🤖 Prompt for AI Agents
In `@internal/core/codex_oauth_server.go` around lines 14 - 20, The isRunning
boolean on CodexOAuthServer is currently accessed from multiple goroutines
without synchronization; protect it to avoid data races by either replacing
isRunning with an uint32 and using atomic.LoadUint32/atomic.StoreUint32 (and
atomic.CompareAndSwapUint32 where needed) or adding a sync.Mutex/sync.RWMutex on
the CodexOAuthServer struct and locking around all reads/writes to isRunning
(e.g., in Start, Stop and any status checks); update the Start method's
goroutine and any Stop/IsRunning checks to use the chosen atomic or mutex
operations so all accesses are synchronized.

Comment on lines +192 to +216
// saveQuotaToDB saves Codex quota to database
func (s *CodexTaskService) saveQuotaToDB(email, accountID, planType string, usage *codex.CodexUsageResponse, isForbidden bool) {
if s.quotaRepo == nil || email == "" {
return
}

quota := &domain.CodexQuota{
Email: email,
AccountID: accountID,
PlanType: planType,
IsForbidden: isForbidden,
}

if usage != nil {
if usage.RateLimit != nil {
quota.PrimaryWindow = convertCodexWindow(usage.RateLimit.PrimaryWindow)
quota.SecondaryWindow = convertCodexWindow(usage.RateLimit.SecondaryWindow)
}
if usage.CodeReviewRateLimit != nil {
quota.CodeReviewWindow = convertCodexWindow(usage.CodeReviewRateLimit.PrimaryWindow)
}
}

s.quotaRepo.Upsert(quota)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

配额写库失败需可观测

Line 215 忽略 Upsert 错误会导致刷新静默失败,建议至少记录日志。

🛠️ 建议修复
- s.quotaRepo.Upsert(quota)
+ if err := s.quotaRepo.Upsert(quota); err != nil {
+   log.Printf("[CodexTask] Failed to upsert quota for %s: %v", email, err)
+ }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// saveQuotaToDB saves Codex quota to database
func (s *CodexTaskService) saveQuotaToDB(email, accountID, planType string, usage *codex.CodexUsageResponse, isForbidden bool) {
if s.quotaRepo == nil || email == "" {
return
}
quota := &domain.CodexQuota{
Email: email,
AccountID: accountID,
PlanType: planType,
IsForbidden: isForbidden,
}
if usage != nil {
if usage.RateLimit != nil {
quota.PrimaryWindow = convertCodexWindow(usage.RateLimit.PrimaryWindow)
quota.SecondaryWindow = convertCodexWindow(usage.RateLimit.SecondaryWindow)
}
if usage.CodeReviewRateLimit != nil {
quota.CodeReviewWindow = convertCodexWindow(usage.CodeReviewRateLimit.PrimaryWindow)
}
}
s.quotaRepo.Upsert(quota)
}
// saveQuotaToDB saves Codex quota to database
func (s *CodexTaskService) saveQuotaToDB(email, accountID, planType string, usage *codex.CodexUsageResponse, isForbidden bool) {
if s.quotaRepo == nil || email == "" {
return
}
quota := &domain.CodexQuota{
Email: email,
AccountID: accountID,
PlanType: planType,
IsForbidden: isForbidden,
}
if usage != nil {
if usage.RateLimit != nil {
quota.PrimaryWindow = convertCodexWindow(usage.RateLimit.PrimaryWindow)
quota.SecondaryWindow = convertCodexWindow(usage.RateLimit.SecondaryWindow)
}
if usage.CodeReviewRateLimit != nil {
quota.CodeReviewWindow = convertCodexWindow(usage.CodeReviewRateLimit.PrimaryWindow)
}
}
if err := s.quotaRepo.Upsert(quota); err != nil {
log.Printf("[CodexTask] Failed to upsert quota for %s: %v", email, err)
}
}
🤖 Prompt for AI Agents
In `@internal/service/codex_task.go` around lines 192 - 216, The saveQuotaToDB
function currently ignores the error returned by s.quotaRepo.Upsert(quota);
change it to capture that error and emit an observability log containing the
error and contextual identifiers (email, accountID, planType, isForbidden) so
failures to persist quota are visible; use the service logger (e.g. s.logger) or
the standard logger to record a clear message including the Upsert error and
relevant fields, and consider returning or metrics incrementing if applicable.

Comment on lines +36 to 60
onMutate: async ({ key, value }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: settingsKeys.all });

// Snapshot the previous value
const previousSettings = queryClient.getQueryData<Record<string, string>>(settingsKeys.all);

// Optimistically update to the new value
if (previousSettings) {
queryClient.setQueryData(settingsKeys.all, {
...previousSettings,
[key]: value,
});
}

return { previousSettings };
},
onError: (_err, _variables, context) => {
// If the mutation fails, use the context returned from onMutate to roll back
if (context?.previousSettings) {
queryClient.setQueryData(settingsKeys.all, context.previousSettings);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: settingsKeys.all });
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find web/src -name "*settings*" -type f

Repository: awsl-project/maxx

Length of output: 98


🏁 Script executed:

cat -n web/src/hooks/queries/use-settings.ts

Repository: awsl-project/maxx

Length of output: 4880


🏁 Script executed:

rg -n "settingsKeys" web/src --type=ts --type=tsx | head -30

Repository: awsl-project/maxx

Length of output: 88


🏁 Script executed:

rg -n "useSetting\(" web/src --type=ts --type=tsx

Repository: awsl-project/maxx

Length of output: 88


🏁 Script executed:

rg -n "useSetting\(" web/src --type ts

Repository: awsl-project/maxx

Length of output: 266


🏁 Script executed:

rg -n "useSetting" web/src

Repository: awsl-project/maxx

Length of output: 1919


保持 settingsKeys.detail 与列表缓存一致

当前乐观更新只更新了 settingsKeys.all,但 useSetting(key) 实际在代码中被使用(CopyAsCurlButton.tsx)。如果有组件通过 useSetting('proxy_port') 查询单个设置,在更新时会因为 settingsKeys.detail(key) 缓存未被同步而显示旧值。

需要在 onMutateonErroronSettled 中同步更新/失效 settingsKeys.detail(key),保持缓存一致。

🔧 建议修改
     onMutate: async ({ key, value }) => {
       // Cancel any outgoing refetches
       await queryClient.cancelQueries({ queryKey: settingsKeys.all });
+      await queryClient.cancelQueries({ queryKey: settingsKeys.detail(key) });

       // Snapshot the previous value
       const previousSettings = queryClient.getQueryData<Record<string, string>>(settingsKeys.all);

       // Optimistically update to the new value
       if (previousSettings) {
         queryClient.setQueryData(settingsKeys.all, {
           ...previousSettings,
           [key]: value,
         });
       }
+      queryClient.setQueryData(settingsKeys.detail(key), value);

       return { previousSettings };
     },
-    onError: (_err, _variables, context) => {
+    onError: (_err, variables, context) => {
       // If the mutation fails, use the context returned from onMutate to roll back
       if (context?.previousSettings) {
         queryClient.setQueryData(settingsKeys.all, context.previousSettings);
+        if (variables?.key) {
+          queryClient.setQueryData(
+            settingsKeys.detail(variables.key),
+            context.previousSettings[variables.key]
+          );
+        }
       }
     },
-    onSettled: () => {
+    onSettled: (_data, _err, variables) => {
       queryClient.invalidateQueries({ queryKey: settingsKeys.all });
+      if (variables?.key) {
+        queryClient.invalidateQueries({ queryKey: settingsKeys.detail(variables.key) });
+      }
     },
🤖 Prompt for AI Agents
In `@web/src/hooks/queries/use-settings.ts` around lines 36 - 60, The optimistic
update only updates settingsKeys.all but not per-key caches, causing
useSetting(key) consumers to see stale values; update the code in the mutation
handlers (onMutate, onError, onSettled) to also set/restore/invalidate the
per-key cache via settingsKeys.detail(key): in onMutate after setting
settingsKeys.all, call queryClient.setQueryData(settingsKeys.detail(key), value)
or the appropriate object shape, return previous per-key value in the context;
in onError restore the per-key cache from context.previousDetail; and in
onSettled call queryClient.invalidateQueries({ queryKey:
settingsKeys.detail(key) }) in addition to invalidating settingsKeys.all so both
list and detail caches stay in sync for useSetting.

Comment on lines 141 to +142
refreshAntigravityQuotas(): Promise<{ success: boolean; refreshed: number }>;
sortAntigravityRoutes(): Promise<{ success: boolean }>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's look at the interface file to see the actual return types
cat -n web/src/lib/transport/interface.ts | sed -n '135,170p'

Repository: awsl-project/maxx

Length of output: 2040


🏁 Script executed:

#!/bin/bash
# Let's search for backend implementations to confirm the design intent
find . -name "*.go" -type f | xargs rg -n "refreshAntigravityQuotas|refreshCodexQuotas" -A3 -B1 2>/dev/null | head -80

Repository: awsl-project/maxx

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Check if there are TypeScript type definitions that correspond to these return types
rg -n "refreshed.*number|refreshed.*boolean" --type=ts -B2 -A2 2>/dev/null | head -100

Repository: awsl-project/maxx

Length of output: 1974


🏁 Script executed:

#!/bin/bash
# Let's search for the backend API implementations to understand the design intent
find . -name "*.go" -type f | xargs rg -l "refresh.*quota" 2>/dev/null | head -20

Repository: awsl-project/maxx

Length of output: 194


🏁 Script executed:

#!/bin/bash
# Check the actual backend implementations for these endpoints
rg -n "refresh-quotas" -A10 -B2 --type=go 2>/dev/null | head -100

Repository: awsl-project/maxx

Length of output: 4536


🏁 Script executed:

#!/bin/bash
# Look for return type definitions in the Go backend
rg -n "type.*RefreshQuota.*struct\|refreshed.*int\|refreshed.*bool" --type=go -A5 -B2 2>/dev/null | head -100

Repository: awsl-project/maxx

Length of output: 43


🏁 Script executed:

#!/bin/bash
# Look at the antigravity handler refresh-quotas implementation
sed -n '150,180p' internal/handler/antigravity.go

Repository: awsl-project/maxx

Length of output: 965


🏁 Script executed:

#!/bin/bash
# Look at the codex handler refresh-quotas implementation more completely
sed -n '432,460p' internal/handler/codex.go

Repository: awsl-project/maxx

Length of output: 1044


🏁 Script executed:

#!/bin/bash
# Check what ForceRefreshQuotas returns in the service layer for both APIs
rg -n "func.*ForceRefreshQuotas" -A15 internal/service/antigravity_task.go internal/service/codex_task.go

Repository: awsl-project/maxx

Length of output: 2138


🏁 Script executed:

#!/bin/bash
# Look at the antigravity handler's handleForceRefreshQuotas implementation
rg -n "handleForceRefreshQuotas" -A12 internal/handler/antigravity.go

Repository: awsl-project/maxx

Length of output: 1024


🏁 Script executed:

#!/bin/bash
# Check what refreshAllQuotas returns for antigravity
rg -n "func.*refreshAllQuotas" -A10 internal/service/antigravity_task.go

Repository: awsl-project/maxx

Length of output: 500


🏁 Script executed:

#!/bin/bash
# Get more context on refreshAllQuotas to see what it's actually counting
sed -n '118,160p' internal/service/antigravity_task.go

Repository: awsl-project/maxx

Length of output: 1244


🏁 Script executed:

#!/bin/bash
# Check if antigravity refreshAllQuotas returns a count instead of bool
rg -n "refreshAllQuotas.*{" -A35 internal/service/antigravity_task.go | head -50

Repository: awsl-project/maxx

Length of output: 162


🏁 Script executed:

#!/bin/bash
# Let's verify what the codex refreshAllQuotas returns
sed -n '110,145p' internal/service/codex_task.go

Repository: awsl-project/maxx

Length of output: 1002


🏁 Script executed:

#!/bin/bash
# Double-check both implementations side by side to be absolutely sure
echo "=== ANTIGRAVITY ===" && \
rg -n "refreshAllQuotas" -A2 internal/service/antigravity_task.go | grep -E "return|bool" && \
echo "" && \
echo "=== CODEX ===" && \
rg -n "refreshAllQuotas" -A2 internal/service/codex_task.go | grep -E "return|bool"

Repository: awsl-project/maxx

Length of output: 272


修正返回类型:refreshAntigravityQuotas()refreshed 字段类型错误。

refreshAntigravityQuotas() 返回类型中 refreshed: number 应改为 refreshed: boolean。后端两个服务的 ForceRefreshQuotas() 方法都返回 bool 类型,表示是否成功刷新,而非刷新数量。当前 TypeScript 接口声明与后端实现不匹配。

建议将第 141 行改为:

refreshAntigravityQuotas(): Promise<{ success: boolean; refreshed: boolean }>;

以保持与 refreshCodexQuotas() 和后端实现的一致性。

🤖 Prompt for AI Agents
In `@web/src/lib/transport/interface.ts` around lines 141 - 142, The return type
for refreshAntigravityQuotas() is incorrect: change its signature so the
refreshed field is a boolean (not number) to match the backend
ForceRefreshQuotas() and the existing refreshCodexQuotas() shape; update the
interface declaration for refreshAntigravityQuotas() to return Promise<{
success: boolean; refreshed: boolean }>, ensuring the method name
refreshAntigravityQuotas and the analogous refreshCodexQuotas are consistent
with backend bool semantics.

Comment on lines +238 to 239
code="POST http://localhost:9880/project/{project-slug}/v1/chat/completions"
id="openai-project"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

文档示例前缀不一致:Gemini 仍是旧路径。
OpenAI 示例已改为 /project/,Gemini 处建议同步更新以避免误导。

📌 建议更新 Gemini 示例
-                code="POST http://localhost:9880/{project-slug}/v1beta/models/{model}:generateContent"
+                code="POST http://localhost:9880/project/{project-slug}/v1beta/models/{model}:generateContent"

Also applies to: 418-419

🤖 Prompt for AI Agents
In `@web/src/pages/documentation/index.tsx` around lines 238 - 239, Update the
Gemini example URLs to match the new OpenAI-style prefix by replacing the old
Gemini path with the /project/{project-slug}/v1/... prefix used in the OpenAI
snippet (the code element with code="POST
http://localhost:9880/project/{project-slug}/v1/chat/completions" and
id="openai-project"); locate the Gemini examples elsewhere in this file (the
other example blocks referenced in the review) and change their POST endpoints
to use /project/{project-slug}/v1/chat/completions (and any other v1 paths) so
all documentation examples are consistent.

Comment on lines +45 to +109
const oauthWindowRef = useRef<Window | null>(null);

// Subscribe to OAuth result messages via WebSocket
useEffect(() => {
const transport = getTransport();
const unsubscribe = transport.subscribe<CodexOAuthResult>('codex_oauth_result', (result) => {
// Only handle results that match our current OAuth state
if (result.state === oauthState) {
// Close the OAuth window if it's still open
if (oauthWindowRef.current && !oauthWindowRef.current.closed) {
oauthWindowRef.current.close();
}

if (result.success && result.refreshToken) {
// OAuth succeeded, save result for user confirmation
setOAuthStatus('success');
setOAuthResult(result);
} else {
// OAuth failed
setOAuthStatus('error');
setError(result.error || 'OAuth authorization failed');
}
}
});

return () => unsubscribe();
}, [oauthState]);

// Handle OAuth flow
const handleOAuth = async () => {
setOAuthStatus('waiting');
setError(null);

try {
// Request OAuth URL from backend
const { authURL, state } = await getTransport().startCodexOAuth();
setOAuthState(state);

// Open OAuth window
const width = 600;
const height = 700;
const left = window.screenX + (window.outerWidth - width) / 2;
const top = window.screenY + (window.outerHeight - height) / 2;

oauthWindowRef.current = window.open(
authURL,
'Codex OAuth',
`width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`,
);

// Monitor window closure
const checkWindowClosed = setInterval(() => {
if (oauthWindowRef.current?.closed) {
clearInterval(checkWindowClosed);
// If still waiting when window closes, assume user cancelled
setOAuthStatus((current) => {
if (current === 'waiting') {
setOAuthState(null);
return 'idle';
}
return current;
});
}
}, 500);
} catch (err) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "codex-token-import.tsx" -type f

Repository: awsl-project/maxx

Length of output: 120


🏁 Script executed:

wc -l web/src/pages/providers/components/codex-token-import.tsx

Repository: awsl-project/maxx

Length of output: 122


🏁 Script executed:

cat -n web/src/pages/providers/components/codex-token-import.tsx

Repository: awsl-project/maxx

Length of output: 24342


处理 popup 拦截、interval 泄漏及错误态重试入口

window.open() 被 popup 拦截器阻止时会返回 null,代码未检查此情况导致 oauthWindowRef.currentnull 但仍继续执行。同时,setInterval() 存储在局部变量(第 96 行)而非 ref,仅在窗口关闭时清理(第 98 行),若组件卸载或 oauthState 变化时 interval 会泄漏。此外,OAuth 失败时状态设为 error(第 64、110 行)但界面未显示重试按钮,用户只能看到通用错误消息无法重新尝试。

需补齐以下修复:

  • 添加 oauthIntervalRef 保存 interval ID,在 useEffect 清理函数中清理
  • 检查 window.open() 返回值,若为 null 则设置错误状态
  • 修改 OAuth UI 使 error 状态也能显示重试按钮(与 idle 状态同时显示)
  • 在取消处理器中清理 interval
🔧 修复方案
-  const oauthWindowRef = useRef<Window | null>(null);
+  const oauthWindowRef = useRef<Window | null>(null);
+  const oauthIntervalRef = useRef<number | null>(null);
 
-    return () => unsubscribe();
+    return () => {
+      if (oauthIntervalRef.current !== null) {
+        window.clearInterval(oauthIntervalRef.current);
+        oauthIntervalRef.current = null;
+      }
+      unsubscribe();
+    };
   }, [oauthState]);
 
-      oauthWindowRef.current = window.open(
+      const oauthWindow = window.open(
         authURL,
         'Codex OAuth',
         `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`,
       );
+      if (!oauthWindow) {
+        setOAuthStatus('error');
+        setError('Popup blocked. Please enable popups and retry.');
+        return;
+      }
+      oauthWindowRef.current = oauthWindow;
 
-      const checkWindowClosed = setInterval(() => {
+      if (oauthIntervalRef.current !== null) {
+        window.clearInterval(oauthIntervalRef.current);
+      }
+      oauthIntervalRef.current = window.setInterval(() => {
         if (oauthWindowRef.current?.closed) {
-          clearInterval(checkWindowClosed);
+          if (oauthIntervalRef.current !== null) {
+            window.clearInterval(oauthIntervalRef.current);
+            oauthIntervalRef.current = null;
+          }
           setOAuthStatus((current) => {
             if (current === 'waiting') {
               setOAuthState(null);
@@ -330,6 +344,10 @@
                       onClick={() => {
                         setOAuthStatus('idle');
                         setOAuthState(null);
+                        if (oauthIntervalRef.current !== null) {
+                          window.clearInterval(oauthIntervalRef.current);
+                          oauthIntervalRef.current = null;
+                        }
                         if (oauthWindowRef.current && !oauthWindowRef.current.closed) {
                           oauthWindowRef.current.close();
                         }
-                {oauthStatus === 'idle' && (
+                {(oauthStatus === 'idle' || oauthStatus === 'error') && (
🤖 Prompt for AI Agents
In `@web/src/pages/providers/components/codex-token-import.tsx` around lines 45 -
109, The popup handling and interval cleanup are incomplete: update handleOAuth
to check the result of window.open (oauthWindowRef) and if null
setOAuthStatus('error') and setError('Popup blocked') to allow retry; store the
interval ID in a ref (create oauthIntervalRef) instead of a local variable and
clear it whenever the OAuth flow ends, on window close, on component unmount
(inside the existing useEffect cleanup) and when cancelling by setting
oauthState to null; ensure the useEffect that subscribes to 'codex_oauth_result'
also clears oauthIntervalRef.current (clearInterval) when processing a matching
result; and update the UI logic to show the retry button when oauthStatus is
'error' (same visibility as 'idle') so users can re-run handleOAuth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants