Conversation
📝 WalkthroughWalkthrough引入 Codex 提供程序类型与端到端支持(前端 UI、传输层 API、后台任务、数据库仓库、OAuth 回调服务器和适配器);移除 ClientTypeRoutesContent 中的 Antigravity 配额/排序专属逻辑;新增 Codex 批量配额上下文与多处 UI/路由适配与国际化文本。 Changes
Sequence Diagram(s)mermaid Frontend->>Transport: POST /api/codex/start-oauth CodexAPI->>Server: POST /oauth/token (Exchange) mermaid Scheduler->>TaskSvc: 定时调用 RefreshQuotas() Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 分钟 Possibly related PRs
Suggested reviewers
诗歌
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
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>
1c95cbc to
27b3fde
Compare
There was a problem hiding this comment.
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.isPending为true时,两个 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类型定义重复。此处定义的
ProviderTypeKey与web/src/pages/providers/types.ts中的定义重复。考虑从共享位置导入以保持 DRY 原则。♻️ 建议从共享类型文件导入
+ import { ProviderTypeKey } from '@/pages/providers/types'; - type ProviderTypeKey = 'antigravity' | 'kiro' | 'codex' | 'custom';如果
PROVIDER_TYPE_ORDER和PROVIDER_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
📒 Files selected for processing (48)
cmd/maxx/main.gointernal/adapter/provider/codex/adapter.gointernal/adapter/provider/codex/oauth.gointernal/adapter/provider/codex/service.gointernal/adapter/provider/codex/settings.gointernal/core/codex_oauth_server.gointernal/core/database.gointernal/core/server.gointernal/core/task.gointernal/domain/model.gointernal/handler/codex.gointernal/handler/project_proxy.gointernal/handler/static.gointernal/repository/interfaces.gointernal/repository/sqlite/codex_quota.gointernal/repository/sqlite/models.gointernal/service/admin.gointernal/service/antigravity_task.gointernal/service/codex_task.goweb/src/components/routes/ClientTypeRoutesContent.tsxweb/src/contexts/codex-quotas-context.tsxweb/src/hooks/queries/index.tsweb/src/hooks/queries/use-providers.tsweb/src/hooks/queries/use-settings.tsweb/src/index.cssweb/src/lib/theme.tsweb/src/lib/transport/http-transport.tsweb/src/lib/transport/index.tsweb/src/lib/transport/interface.tsweb/src/lib/transport/types.tsweb/src/locales/en.jsonweb/src/locales/zh.jsonweb/src/pages/client-routes/index.tsxweb/src/pages/documentation/index.tsxweb/src/pages/projects/tabs/overview.tsxweb/src/pages/providers/components/codex-provider-view.tsxweb/src/pages/providers/components/codex-token-import.tsxweb/src/pages/providers/components/provider-edit-flow.tsxweb/src/pages/providers/components/provider-row.tsxweb/src/pages/providers/components/select-type-step.tsxweb/src/pages/providers/create-layout.tsxweb/src/pages/providers/hooks/use-provider-navigation.tsweb/src/pages/providers/index.tsxweb/src/pages/providers/types.tsweb/src/pages/requests/index.tsxweb/src/pages/routes/form.tsxweb/src/pages/settings/index.tsxweb/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组件的导入,与现有的AntigravityProviderView和KiroProviderView导入保持一致。
418-436: LGTM!Codex provider 的条件分支实现正确,完全遵循了 Antigravity 和 Kiro provider 的既有模式:
- 渲染
CodexProviderView组件- 正确绑定
onDelete和onClose回调- 复用
DeleteConfirmModal组件处理删除确认流程internal/service/admin.go (1)
567-571: LGTM!Codex provider 类型的客户端支持配置正确:
- 遵循了
antigravity、kiro等其他 provider 类型的既有模式- 仅支持
ClientTypeCodex协议,符合 Codex 原生协议的设计意图- 注释清晰说明了支持范围
web/src/pages/routes/form.tsx (2)
10-17: LGTM!
PROVIDER_TYPE_ORDER和PROVIDER_TYPE_LABELS的更新正确添加了 Codex 支持,与 provider 分组逻辑保持一致。
46-47: LGTM!
groupedProviders对象正确初始化了codex空数组,确保 Codex 类型的 provider 能够被正确分组。web/src/hooks/queries/use-providers.ts (1)
131-142: LGTM!
useCodexBatchQuotashook 实现正确:
- 遵循了
useAntigravityBatchQuotas的既有模式- Query key 命名清晰且唯一
- 10 分钟的
refetchInterval和staleTime配置与其他配额 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: 设置集成和切换处理器实现正确。使用
useSettings和useUpdateSetting钩子进行设置管理的方式正确,乐观更新机制(在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: 日期格式化辅助函数实现完善。
formatDate和formatRelativeTime函数正确处理了空值和异常情况,使用 try-catch 确保不会因为无效日期而崩溃。
336-353: 缓存数据与手动刷新数据的合并逻辑清晰。使用
useMemo优先显示手动刷新的数据,否则使用缓存数据的方式合理。这确保了用户看到最新的数据,同时在没有手动刷新时也能显示缓存内容。internal/core/database.go (2)
46-46: Codex 仓库和组件集成正确。
CodexQuotaRepo、CodexHandler和CodexOAuthServer的添加遵循了现有的 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 配额信息提取函数实现正确。
getCodex5HQuotaInfo和getCodexWeekQuotaInfo函数正确处理了两种重置时间格式(resetAtUnix 时间戳和resetAfterSeconds),并计算剩余百分比。逻辑与现有的 Antigravity 配额函数保持一致。
333-393: Codex 配额 UI 渲染与 Antigravity 保持一致。5H 和 Week 配额的进度条显示逻辑与 Claude/Image 配额完全一致,包括颜色阈值(绿/黄/红)和最后更新时间显示。代码结构清晰,易于维护。
web/src/pages/requests/index.tsx (2)
44-53: Codex 类型支持添加正确。
ProviderTypeKey、PROVIDER_TYPE_ORDER和PROVIDER_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.goCodexUsageWindow、CodexRateLimitInfo、CodexUsageResponse对应internal/adapter/provider/codex/oauth.goCodexQuotaData和CodexBatchQuotaResult结构清晰,类型正确
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.
| // Retry request | ||
| upstreamReq, _ = http.NewRequestWithContext(ctx, "POST", upstreamURL, bytes.NewReader(requestBody)) | ||
| a.applyCodexHeaders(upstreamReq, req, accessToken, config.AccountID) |
There was a problem hiding this comment.
忽略的错误可能隐藏问题
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.
| // 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.
| // Calculate expiration time (with 60s buffer) | ||
| expiresAt := time.Now().Add(time.Duration(tokenResp.ExpiresIn-60) * time.Second) |
There was a problem hiding this comment.
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.
| // 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.
| eventChan := ctxutil.GetEventChan(ctx) | ||
| eventChan.SendResponseInfo(&domain.ResponseInfo{ | ||
| Status: resp.StatusCode, | ||
| Headers: flattenHeaders(resp.Header), | ||
| Body: string(body), | ||
| }) |
There was a problem hiding this comment.
潜在的空指针解引用
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.
| 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]", | ||
| }) | ||
|
|
There was a problem hiding this comment.
同样的空指针解引用风险
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.
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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) | ||
| } |
There was a problem hiding this comment.
配额写库失败需可观测
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.
| // 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.
| 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 }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find web/src -name "*settings*" -type fRepository: awsl-project/maxx
Length of output: 98
🏁 Script executed:
cat -n web/src/hooks/queries/use-settings.tsRepository: awsl-project/maxx
Length of output: 4880
🏁 Script executed:
rg -n "settingsKeys" web/src --type=ts --type=tsx | head -30Repository: awsl-project/maxx
Length of output: 88
🏁 Script executed:
rg -n "useSetting\(" web/src --type=ts --type=tsxRepository: awsl-project/maxx
Length of output: 88
🏁 Script executed:
rg -n "useSetting\(" web/src --type tsRepository: awsl-project/maxx
Length of output: 266
🏁 Script executed:
rg -n "useSetting" web/srcRepository: awsl-project/maxx
Length of output: 1919
保持 settingsKeys.detail 与列表缓存一致
当前乐观更新只更新了 settingsKeys.all,但 useSetting(key) 实际在代码中被使用(CopyAsCurlButton.tsx)。如果有组件通过 useSetting('proxy_port') 查询单个设置,在更新时会因为 settingsKeys.detail(key) 缓存未被同步而显示旧值。
需要在 onMutate、onError 和 onSettled 中同步更新/失效 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.
| refreshAntigravityQuotas(): Promise<{ success: boolean; refreshed: number }>; | ||
| sortAntigravityRoutes(): Promise<{ success: boolean }>; |
There was a problem hiding this comment.
🧩 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 -80Repository: 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 -100Repository: 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 -20Repository: 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 -100Repository: 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 -100Repository: 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.goRepository: 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.goRepository: 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.goRepository: 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.goRepository: 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.goRepository: 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.goRepository: 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 -50Repository: 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.goRepository: 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.
| code="POST http://localhost:9880/project/{project-slug}/v1/chat/completions" | ||
| id="openai-project" |
There was a problem hiding this comment.
文档示例前缀不一致: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.
| 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) { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "codex-token-import.tsx" -type fRepository: awsl-project/maxx
Length of output: 120
🏁 Script executed:
wc -l web/src/pages/providers/components/codex-token-import.tsxRepository: awsl-project/maxx
Length of output: 122
🏁 Script executed:
cat -n web/src/pages/providers/components/codex-token-import.tsxRepository: awsl-project/maxx
Length of output: 24342
处理 popup 拦截、interval 泄漏及错误态重试入口
当 window.open() 被 popup 拦截器阻止时会返回 null,代码未检查此情况导致 oauthWindowRef.current 为 null 但仍继续执行。同时,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.
Summary
Codex Provider
路由自动排序
Test plan
🤖 Generated with Claude Code
Summary by CodeRabbit
发布说明
新功能
改进
移除
✏️ Tip: You can customize this high-level summary in your review settings.