Conversation
|
Caution Review failedThe pull request is closed. 📝 WalkthroughWalkthrough新增 Antigravity 后台任务服务与前端配额上下文:后端引入 AntigravityTaskService、后台循环与 HTTP 管理接口;仓库与模型新增接口/设置键;前端新增配额刷新/排序按钮、乐观路由排序、配额展示与多语言键,及若干组件签名调整。 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User as 用户
participant UI as 浏览器 UI
participant QuotasCtx as AntigravityQuotasContext
participant Transport as HttpTransport
participant Handler as AntigravityHandler(API)
participant TaskSvc as AntigravityTaskService
participant Repo as Repositories/DB
participant Broad as Broadcaster/Event
User->>UI: 点击 "Sort Antigravity"
UI->>QuotasCtx: 读取 quotaByEmail
QuotasCtx-->>UI: 返回配额数据
UI->>UI: 计算新路由顺序并乐观更新界面
UI->>Transport: POST /api/antigravity/sort-routes (或批量更新)
Transport->>Handler: 转发请求
Handler->>TaskSvc: 调用 SortRoutes
TaskSvc->>Repo: 读取 routes/providers/quotas/设置/请求历史
Repo-->>TaskSvc: 返回数据
TaskSvc->>Repo: 批量更新 route positions
TaskSvc->>Broad: 广播 routes_updated
Broad-->>UI: 前端接收事件或 API 返回成功
alt 成功
UI->>UI: 保持乐观顺序
else 失败
UI->>UI: 回滚到原始顺序
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 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
📜 Recent review detailsConfiguration used: Organization UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (16)
✏️ Tip: You can disable this entire section by setting 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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@web/src/components/routes/ClientTypeRoutesContent.tsx`:
- Around line 158-161: hasAntigravityRoutes is computed from the filtered items
array (which is affected by searchQuery) so the Antigravity button can be hidden
incorrectly; change the useMemo to compute presence from the full/unfiltered
routes source (e.g. clientRoutes or the original routes array / props.routes)
instead of items, update the dependency array to reference that full route list
rather than items, and keep the same predicate (provider.type === 'antigravity'
&& route) so the button visibility is driven by the complete routes set.
- Around line 164-175: getClaudeResetTime currently uses try/catch around new
Date(...) but invalid dates do not throw — they produce "Invalid Date" which
yields NaN when sorted; replace the try/catch with explicit validation: after
creating const d = new Date(claudeModel.resetTime) (or use Date.parse on the raw
value), check if Number.isNaN(d.getTime()) (or d.toString() === 'Invalid Date')
and return null if invalid; ensure you still handle missing quota/models and
keep the same function signature and references to quotas and
claudeModel.resetTime.
- Around line 179-239: handleSortAntigravity currently computes indices and
position updates from the filtered items array (`items`), which causes global
position corruption when a search/filter is active; instead retrieve the
unfiltered full route list (e.g. from
`queryClient.getQueryData(routeKeys.list())` or the existing `allRoutes`
source), find Antigravity entries and their indices in that full list, sort
those Antigravity entries by `getClaudeResetTime`, write them back into the full
list at their original indices, compute `updates` from that full list (using
`route.id` -> position), then perform the optimistic update and API
`updatePositions.mutate(updates...)`; keep the UI rendering/filtering using
`items` only for display, not for computing positions. Ensure you reference
`handleSortAntigravity`, `items`, `getClaudeResetTime`, `queryClient`,
`routeKeys.list()`, and `updatePositions` when making the change.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
web/src/components/routes/ClientTypeRoutesContent.tsxweb/src/locales/en.jsonweb/src/locales/zh.json
🔇 Additional comments (6)
web/src/locales/en.json (1)
226-227: 新增 routes.sortAntigravity 翻译项 OK。
文案清晰,符合 Routes 区域命名习惯。web/src/locales/zh.json (1)
226-227: 新增 routes.sortAntigravity 中文翻译合理。
与英文 key 对齐,语义明确。web/src/components/routes/ClientTypeRoutesContent.tsx (4)
55-62: 外层 Provider 包装清晰。
通过AntigravityQuotasProvider包裹并抽出 Inner 组件,结构清楚且便于访问上下文。
339-353: 排序按钮交互合理。
仅在存在 Antigravity 路由时展示,且在更新中禁用,符合预期。
355-403: DND 列表与 Overlay 更新合理。
行内统计与请求数基于item.provider计算,避免错位。
411-479: “可用 Provider”卡片式入口清晰。
布局、hover 与禁用态处理完整,用户引导明确。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| // Check if there are any Antigravity routes | ||
| const hasAntigravityRoutes = useMemo(() => { | ||
| return items.some((item) => item.provider.type === 'antigravity' && item.route); | ||
| }, [items]); |
There was a problem hiding this comment.
按钮显隐不应受搜索过滤影响。
hasAntigravityRoutes 基于 items(受 searchQuery 过滤)计算,可能在仍存在 Antigravity 路由时隐藏按钮。建议基于完整路由集合(如 clientRoutes 或未过滤的 route 列表)计算。
🤖 Prompt for AI Agents
In `@web/src/components/routes/ClientTypeRoutesContent.tsx` around lines 158 -
161, hasAntigravityRoutes is computed from the filtered items array (which is
affected by searchQuery) so the Antigravity button can be hidden incorrectly;
change the useMemo to compute presence from the full/unfiltered routes source
(e.g. clientRoutes or the original routes array / props.routes) instead of
items, update the dependency array to reference that full route list rather than
items, and keep the same predicate (provider.type === 'antigravity' && route) so
the button visibility is driven by the complete routes set.
| const getClaudeResetTime = useCallback( | ||
| (providerId: number): Date | null => { | ||
| const quota = quotas?.[providerId]; | ||
| if (!quota || quota.isForbidden || !quota.models) return null; | ||
| const claudeModel = quota.models.find((m) => m.name.includes('claude')); | ||
| if (!claudeModel) return null; | ||
| try { | ||
| return new Date(claudeModel.resetTime); | ||
| } catch { | ||
| return null; | ||
| } | ||
| }, |
There was a problem hiding this comment.
Invalid Date 不会被 try/catch 捕获。
new Date(...) 对非法值不会抛错,会生成 Invalid Date,排序时可能得到 NaN。建议显式校验并返回 null。
建议修复
- try {
- return new Date(claudeModel.resetTime);
- } catch {
- return null;
- }
+ const date = new Date(claudeModel.resetTime);
+ return Number.isNaN(date.getTime()) ? null : date;📝 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.
| const getClaudeResetTime = useCallback( | |
| (providerId: number): Date | null => { | |
| const quota = quotas?.[providerId]; | |
| if (!quota || quota.isForbidden || !quota.models) return null; | |
| const claudeModel = quota.models.find((m) => m.name.includes('claude')); | |
| if (!claudeModel) return null; | |
| try { | |
| return new Date(claudeModel.resetTime); | |
| } catch { | |
| return null; | |
| } | |
| }, | |
| const getClaudeResetTime = useCallback( | |
| (providerId: number): Date | null => { | |
| const quota = quotas?.[providerId]; | |
| if (!quota || quota.isForbidden || !quota.models) return null; | |
| const claudeModel = quota.models.find((m) => m.name.includes('claude')); | |
| if (!claudeModel) return null; | |
| const date = new Date(claudeModel.resetTime); | |
| return Number.isNaN(date.getTime()) ? null : date; | |
| }, |
🤖 Prompt for AI Agents
In `@web/src/components/routes/ClientTypeRoutesContent.tsx` around lines 164 -
175, getClaudeResetTime currently uses try/catch around new Date(...) but
invalid dates do not throw — they produce "Invalid Date" which yields NaN when
sorted; replace the try/catch with explicit validation: after creating const d =
new Date(claudeModel.resetTime) (or use Date.parse on the raw value), check if
Number.isNaN(d.getTime()) (or d.toString() === 'Invalid Date') and return null
if invalid; ensure you still handle missing quota/models and keep the same
function signature and references to quotas and claudeModel.resetTime.
| // Sort Antigravity routes by resetTime (earliest first), keeping non-Antigravity routes in place | ||
| const handleSortAntigravity = useCallback(() => { | ||
| // Get indices of Antigravity items in the original list | ||
| const antigravityIndices: number[] = []; | ||
| const antigravityItems: ProviderConfigItem[] = []; | ||
|
|
||
| items.forEach((item, index) => { | ||
| if (item.provider.type === 'antigravity' && item.route) { | ||
| antigravityIndices.push(index); | ||
| antigravityItems.push(item); | ||
| } | ||
| }); | ||
|
|
||
| // Sort Antigravity items by resetTime (earliest first) | ||
| const sortedAntigravityItems = [...antigravityItems].sort((a, b) => { | ||
| const resetTimeA = getClaudeResetTime(a.provider.id); | ||
| const resetTimeB = getClaudeResetTime(b.provider.id); | ||
|
|
||
| // Items without resetTime go to the end | ||
| if (!resetTimeA && !resetTimeB) return 0; | ||
| if (!resetTimeA) return 1; | ||
| if (!resetTimeB) return -1; | ||
|
|
||
| return resetTimeA.getTime() - resetTimeB.getTime(); | ||
| }); | ||
|
|
||
| // Build new items array: put sorted Antigravity items back into their original positions | ||
| const newItems = [...items]; | ||
| antigravityIndices.forEach((originalIndex, sortedIndex) => { | ||
| newItems[originalIndex] = sortedAntigravityItems[sortedIndex]; | ||
| }); | ||
|
|
||
| // Update positions for all items | ||
| const updates: Record<number, number> = {}; | ||
| newItems.forEach((item, i) => { | ||
| if (item.route) { | ||
| updates[item.route.id] = i + 1; | ||
| } | ||
| }); | ||
|
|
||
| if (Object.keys(updates).length > 0) { | ||
| // Optimistic update | ||
| queryClient.setQueryData(routeKeys.list(), (oldRoutes: typeof allRoutes) => { | ||
| if (!oldRoutes) return oldRoutes; | ||
| return oldRoutes.map((route) => { | ||
| const newPosition = updates[route.id]; | ||
| if (newPosition !== undefined) { | ||
| return { ...route, position: newPosition }; | ||
| } | ||
| return route; | ||
| }); | ||
| }); | ||
|
|
||
| // Send API request | ||
| updatePositions.mutate(updates, { | ||
| onError: () => { | ||
| queryClient.invalidateQueries({ queryKey: routeKeys.list() }); | ||
| }, | ||
| }); | ||
| } | ||
| }, [items, getClaudeResetTime, queryClient, updatePositions]); |
There was a problem hiding this comment.
排序基于过滤列表会破坏全局位置。
当搜索过滤生效时,items 只包含子集,updates 用子集索引更新 position,可能把 Antigravity 路由挤到前面并与未展示路由发生位置冲突/乱序。建议基于未过滤的 route 列表排序与更新,仅展示时再做过滤。
建议修复思路(提取未过滤的 routeItems)
+ const routeItems = useMemo((): ProviderConfigItem[] => {
+ const allItems = providers.map((provider) => {
+ const route = clientRoutes.find((r) => Number(r.providerID) === Number(provider.id)) || null;
+ const isNative = (provider.supportedClientTypes || []).includes(clientType);
+ return {
+ id: `${clientType}-provider-${provider.id}`,
+ provider,
+ route,
+ enabled: route?.isEnabled ?? false,
+ isNative,
+ };
+ });
+ return allItems
+ .filter((item) => item.route)
+ .sort((a, b) => (a.route!.position - b.route!.position));
+ }, [providers, clientRoutes, clientType]);
+
const items = useMemo((): ProviderConfigItem[] => {
- const allItems = providers.map((provider) => {
- const route = clientRoutes.find((r) => Number(r.providerID) === Number(provider.id)) || null;
- const isNative = (provider.supportedClientTypes || []).includes(clientType);
- return { ... };
- });
-
- let filteredItems = allItems.filter((item) => item.route);
+ let filteredItems = routeItems;
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filteredItems = filteredItems.filter(
(item) =>
item.provider.name.toLowerCase().includes(query) ||
item.provider.type.toLowerCase().includes(query),
);
}
-
- return filteredItems.sort((a, b) => { ... });
+ return filteredItems;
- }, [providers, clientRoutes, clientType, searchQuery]);
+ }, [routeItems, searchQuery]);
- const hasAntigravityRoutes = useMemo(() => {
- return items.some((item) => item.provider.type === 'antigravity' && item.route);
- }, [items]);
+ const hasAntigravityRoutes = useMemo(() => {
+ return routeItems.some((item) => item.provider.type === 'antigravity' && item.route);
+ }, [routeItems]);
const handleSortAntigravity = useCallback(() => {
- const antigravityIndices: number[] = [];
- const antigravityItems: ProviderConfigItem[] = [];
- items.forEach((item, index) => { ... });
+ const antigravityIndices: number[] = [];
+ const antigravityItems: ProviderConfigItem[] = [];
+ routeItems.forEach((item, index) => { ... });
- const newItems = [...items];
+ const newItems = [...routeItems];
antigravityIndices.forEach((originalIndex, sortedIndex) => {
newItems[originalIndex] = sortedAntigravityItems[sortedIndex];
});
...
- }, [items, getClaudeResetTime, queryClient, updatePositions]);
+ }, [routeItems, getClaudeResetTime, queryClient, updatePositions]);🤖 Prompt for AI Agents
In `@web/src/components/routes/ClientTypeRoutesContent.tsx` around lines 179 -
239, handleSortAntigravity currently computes indices and position updates from
the filtered items array (`items`), which causes global position corruption when
a search/filter is active; instead retrieve the unfiltered full route list (e.g.
from `queryClient.getQueryData(routeKeys.list())` or the existing `allRoutes`
source), find Antigravity entries and their indices in that full list, sort
those Antigravity entries by `getClaudeResetTime`, write them back into the full
list at their original indices, compute `updates` from that full list (using
`route.id` -> position), then perform the optimistic update and API
`updatePositions.mutate(updates...)`; keep the UI rendering/filtering using
`items` only for display, not for computing positions. Ensure you reference
`handleSortAntigravity`, `items`, `getClaudeResetTime`, `queryClient`,
`routeKeys.list()`, and `updatePositions` when making the change.
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@internal/handler/antigravity.go`:
- Around line 69-79: The refresh-quotas and sort-routes endpoints are currently
callable anonymously—ensure admin-only access by adding an auth check before
invoking handlers: update the routing branch that dispatches to
handleForceRefreshQuotas and handleSortRoutes (and the other similar antigravity
handlers around the 456-481 range) to verify the requester is an authenticated
admin (e.g., call the existing auth middleware or validate an admin token/roles
from r.Context or Authorization header) and return 401/403 on failure;
alternatively, add the same admin permission check at the start of
handleForceRefreshQuotas and handleSortRoutes so they reject non-admin requests.
In `@internal/service/antigravity_task.go`:
- Around line 156-197: The Upsert call in saveQuotaToDB ignores its returned
error; update saveQuotaToDB (method on AntigravityTaskService) to capture the
error from s.quotaRepo.Upsert(domainQuota) and handle it—log a descriptive error
including email and projectID and the error value (use s.logger.Errorf or, if
the service has no logger, use log.Printf) so failures are recorded for
debugging; do not change control flow otherwise.
In `@web/src/lib/transport/http-transport.ts`:
- Around line 404-409: The refreshAntigravityQuotas method currently calls
axios.post directly and bypasses the class HTTP client and its request
interceptor that injects Authorization when authToken exists; change the
implementation of refreshAntigravityQuotas to call this.client.post<{ success:
boolean; refreshed: number }>('/api/antigravity/refresh-quotas') and return the
response data so the auth header from the interceptor is applied. Also scan for
other methods mentioned (validateAntigravityToken, validateAntigravityTokens,
etc.) that use axios directly and replace them with this.client to ensure
consistent authentication handling.
In `@web/src/pages/client-routes/components/provider-row.tsx`:
- Around line 198-211: The hardcoded English strings in formatLastUpdated (the
'now' return value) and the nearby "Last updated" usage should be replaced with
i18n keys; update formatLastUpdated to accept a translation function (e.g., t)
or import the project's translation helper and return t('provider.now') instead
of 'now', and replace the explicit "Last updated" text (see also the occurrences
around lines 461-463) with t('provider.lastUpdated'); add the corresponding keys
(provider.now, provider.lastUpdated) to the locale files for all supported
languages and ensure the component passes the translator into formatLastUpdated
if you change its signature.
🧹 Nitpick comments (3)
web/src/pages/providers/index.tsx (1)
2-2: 可选:补充刷新结果的用户反馈。
目前失败仅console.error,用户无感知;可考虑 toast/提示并展示refreshed数量。Also applies to: 25-130
web/src/pages/settings/index.tsx (1)
318-414: AntigravitySection 组件实现良好,结构与现有模式一致。组件整体逻辑清晰,状态管理模式与
DataRetentionSection保持一致。有一个小建议:当
updateSetting.mutateAsync失败时,用户不会看到任何错误反馈。考虑添加错误处理,向用户显示操作失败的提示。💡 可选改进:添加错误处理
+ const [error, setError] = useState<string | null>(null); + const handleAutoSortToggle = async (checked: boolean) => { + setError(null); + try { await updateSetting.mutateAsync({ key: 'auto_sort_antigravity', value: checked ? 'true' : 'false', }); + } catch (e) { + setError(t('common.saveFailed')); + } }; const handleSaveInterval = async () => { const intervalNum = parseInt(intervalDraft, 10); if (!isNaN(intervalNum) && intervalNum >= 0 && intervalDraft !== refreshInterval) { + setError(null); + try { await updateSetting.mutateAsync({ key: 'quota_refresh_interval', value: intervalDraft, }); + } catch (e) { + setError(t('common.saveFailed')); + } } };internal/service/antigravity_task.go (1)
63-110: RefreshQuotas 和 ForceRefreshQuotas 之间存在重复代码。两个方法在刷新成功后执行相同的逻辑(广播消息、检查并触发自动排序)。可以考虑提取公共逻辑以提高可维护性。
♻️ 可选重构:提取公共的刷新后处理逻辑
+// handlePostRefresh handles common post-refresh operations +func (s *AntigravityTaskService) handlePostRefresh(ctx context.Context) { + s.broadcaster.BroadcastMessage("quota_updated", nil) + + autoSortEnabled := s.isAutoSortEnabled() + log.Printf("[AntigravityTask] Auto-sort enabled: %v", autoSortEnabled) + if autoSortEnabled { + s.autoSortAntigravityRoutes(ctx) + } +} func (s *AntigravityTaskService) RefreshQuotas(ctx context.Context) bool { // ... activity check ... refreshed := s.refreshAllQuotas(ctx) if refreshed { - s.broadcaster.BroadcastMessage("quota_updated", nil) - autoSortEnabled := s.isAutoSortEnabled() - log.Printf("[AntigravityTask] Auto-sort enabled: %v", autoSortEnabled) - if autoSortEnabled { - s.autoSortAntigravityRoutes(ctx) - } + s.handlePostRefresh(ctx) } return refreshed } func (s *AntigravityTaskService) ForceRefreshQuotas(ctx context.Context) bool { refreshed := s.refreshAllQuotas(ctx) if refreshed { - s.broadcaster.BroadcastMessage("quota_updated", nil) - autoSortEnabled := s.isAutoSortEnabled() - log.Printf("[AntigravityTask] Auto-sort enabled: %v", autoSortEnabled) - if autoSortEnabled { - s.autoSortAntigravityRoutes(ctx) - } + s.handlePostRefresh(ctx) } return refreshed }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
cmd/maxx/main.gointernal/core/task.gointernal/domain/model.gointernal/handler/antigravity.gointernal/repository/interfaces.gointernal/repository/sqlite/proxy_request.gointernal/service/antigravity_task.goweb/src/lib/transport/http-transport.tsweb/src/lib/transport/interface.tsweb/src/locales/en.jsonweb/src/locales/zh.jsonweb/src/pages/client-routes/components/provider-row.tsxweb/src/pages/providers/components/provider-row.tsxweb/src/pages/providers/index.tsxweb/src/pages/settings/index.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- web/src/locales/en.json
🧰 Additional context used
🧬 Code graph analysis (6)
internal/repository/sqlite/proxy_request.go (3)
internal/repository/interfaces.go (1)
ProxyRequestRepository(62-83)internal/domain/model.go (1)
ProxyRequest(185-247)internal/repository/sqlite/models.go (2)
ProxyRequest(182-212)ProxyRequest(214-214)
internal/handler/antigravity.go (2)
internal/service/antigravity_task.go (1)
AntigravityTaskService(22-29)internal/adapter/provider/antigravity/service.go (1)
FetchQuotaForProvider(118-142)
web/src/pages/client-routes/components/provider-row.tsx (3)
web/src/lib/transport/types.ts (1)
AntigravityQuotaData(339-344)web/src/lib/transport/index.ts (1)
AntigravityQuotaData(46-46)web/src/lib/utils.ts (1)
cn(8-10)
internal/core/task.go (1)
internal/service/antigravity_task.go (1)
AntigravityTaskService(22-29)
web/src/pages/providers/components/provider-row.tsx (3)
web/src/lib/transport/types.ts (1)
AntigravityQuotaData(339-344)web/src/lib/transport/index.ts (1)
AntigravityQuotaData(46-46)web/src/lib/utils.ts (1)
cn(8-10)
web/src/pages/settings/index.tsx (2)
web/src/hooks/queries/use-settings.ts (2)
useSettings(15-20)useUpdateSetting(30-40)web/src/lib/transport/http-transport.ts (1)
updateSetting(332-337)
🔇 Additional comments (20)
internal/domain/model.go (1)
371-375: 新增系统设置键定义清晰。
命名与注释明确,方便前后端一致读取与配置。web/src/locales/zh.json (3)
164-165: 提供商配额相关文案清晰。
新增文本与按钮语义匹配,易于理解。
227-228: 路由页新增文案 OK。
“排序 Antigravity”相关文案简洁直观。
364-371: Antigravity 设置区文案完整。
新增描述覆盖刷新间隔与自动排序设置,表达清楚。web/src/pages/providers/components/provider-row.tsx (5)
43-53: Claude 配额解析扩展合理。
大小写无关匹配与 lastUpdated 的补充符合预期。
56-66: Image 配额解析逻辑清晰。
与 Claude 的处理方式一致,易维护。
151-152: imageInfo 接入合理。
从上下文读取并统一走 Antigravity 分支,逻辑清晰。
214-270: Antigravity 双配额展示逻辑清晰。
Claude/Image 的并排展示与重置时间提示一致性很好。
94-106: 时间戳单位已正确处理,无需修改。后端通过
time.Now().Unix()返回秒级 Unix 时间戳,前端在formatLastUpdated中正确乘以 1000 转换为毫秒。该模式在antigravity-provider-view.tsx中同样应用,确保了全栈一致性。不存在时间戳失真风险,建议的防御性检查不必要。internal/repository/interfaces.go (1)
81-82: 接口能力补充一致。
HasRecentRequests 的定义清晰,便于上层复用。internal/repository/sqlite/proxy_request.go (1)
190-198: HasRecentRequests 实现直观。
逻辑简洁,符合“是否存在最近请求”的语义。cmd/maxx/main.go (1)
177-196: 服务注入顺序清晰,逻辑顺畅。
WebSocket hub、任务服务和 handler 的绑定关系明确,便于统一广播与后台刷新。Also applies to: 265-265
web/src/pages/providers/index.tsx (1)
225-241: 按钮显示条件与加载态处理得当。
仅在 Antigravity 分组出现、禁用态与旋转图标明确。internal/handler/antigravity.go (1)
24-38: 新增注入点合理。
通过 setter 注入 task service 便于在不同入口复用后台能力。internal/core/task.go (1)
72-75: 后台刷新任务接入合理。
动态间隔 + 禁用状态退避检查的逻辑清晰。Also applies to: 136-154
web/src/pages/client-routes/components/provider-row.tsx (1)
147-170: Claude + Image 配额展示增强很到位。
辅助函数与 UI 组合清晰,信息密度提升。Also applies to: 236-236, 408-458
web/src/lib/transport/interface.ts (1)
133-133: 新增方法已在唯一的 Transport 实现中正确包含。代码库中仅有
HttpTransport一个 Transport 实现(factory.ts 明确说明所有环境都使用 HttpTransport),该实现已在 http-transport.ts 第 404 行正确实现了refreshAntigravityQuotas()方法,签名与接口一致。无需检查其他实现或测试 mock。internal/service/antigravity_task.go (3)
402-418: getClaudeResetTime 在存在多个 Claude 模型时的行为需确认。当前实现返回第一个匹配 "claude" 的模型的 reset time。如果配额数据中存在多个 Claude 模型(如 claude-3-opus、claude-3-sonnet 等),当前逻辑会返回遍历顺序中第一个找到的模型。
请确认这是预期行为,或者是否需要特定的模型优先级(例如优先使用 opus 或 sonnet)。
278-400: 排序逻辑正确,能够保持非 Antigravity 路由的位置不变。
sortAntigravityRoutesForScope方法正确实现了 PR 目标中描述的行为:
- 只对 Antigravity 路由按 resetTime 排序
- 非 Antigravity 路由位置保持不变
- 通过
originalIndices将排序后的路由放回原来的槽位
21-48: 服务结构设计合理,依赖注入清晰。
AntigravityTaskService通过构造函数注入所有依赖,便于测试和维护。
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| // POST /antigravity/refresh-quotas - 强制刷新所有配额 | ||
| if len(parts) >= 2 && parts[1] == "refresh-quotas" && r.Method == http.MethodPost { | ||
| h.handleForceRefreshQuotas(w, r) | ||
| return | ||
| } | ||
|
|
||
| // POST /antigravity/sort-routes - 手动排序路由 | ||
| if len(parts) >= 2 && parts[1] == "sort-routes" && r.Method == http.MethodPost { | ||
| h.handleSortRoutes(w, r) | ||
| return | ||
| } |
There was a problem hiding this comment.
需鉴权:refresh-quotas / sort-routes 当前可匿名调用。
这些是管理操作,但 /api/antigravity/ 在主路由里未加 auth;任何人都可触发刷新/排序。建议在路由层加管理员鉴权,或在 handler 内校验权限/令牌。
Also applies to: 456-481
🤖 Prompt for AI Agents
In `@internal/handler/antigravity.go` around lines 69 - 79, The refresh-quotas and
sort-routes endpoints are currently callable anonymously—ensure admin-only
access by adding an auth check before invoking handlers: update the routing
branch that dispatches to handleForceRefreshQuotas and handleSortRoutes (and the
other similar antigravity handlers around the 456-481 range) to verify the
requester is an authenticated admin (e.g., call the existing auth middleware or
validate an admin token/roles from r.Context or Authorization header) and return
401/403 on failure; alternatively, add the same admin permission check at the
start of handleForceRefreshQuotas and handleSortRoutes so they reject non-admin
requests.
| // saveQuotaToDB saves quota to database | ||
| func (s *AntigravityTaskService) saveQuotaToDB(email, projectID string, quota *antigravity.QuotaData) { | ||
| if s.quotaRepo == nil || email == "" { | ||
| return | ||
| } | ||
|
|
||
| var models []domain.AntigravityModelQuota | ||
| var subscriptionTier string | ||
| var isForbidden bool | ||
|
|
||
| if quota != nil { | ||
| models = make([]domain.AntigravityModelQuota, len(quota.Models)) | ||
| for i, m := range quota.Models { | ||
| models[i] = domain.AntigravityModelQuota{ | ||
| Name: m.Name, | ||
| Percentage: m.Percentage, | ||
| ResetTime: m.ResetTime, | ||
| } | ||
| } | ||
| subscriptionTier = quota.SubscriptionTier | ||
| isForbidden = quota.IsForbidden | ||
| } | ||
|
|
||
| // Try to preserve existing user info | ||
| var name, picture string | ||
| if existing, _ := s.quotaRepo.GetByEmail(email); existing != nil { | ||
| name = existing.Name | ||
| picture = existing.Picture | ||
| } | ||
|
|
||
| domainQuota := &domain.AntigravityQuota{ | ||
| Email: email, | ||
| Name: name, | ||
| Picture: picture, | ||
| GCPProjectID: projectID, | ||
| SubscriptionTier: subscriptionTier, | ||
| IsForbidden: isForbidden, | ||
| Models: models, | ||
| } | ||
|
|
||
| s.quotaRepo.Upsert(domainQuota) | ||
| } |
There was a problem hiding this comment.
saveQuotaToDB 中 Upsert 操作的错误被忽略。
Line 196 的 s.quotaRepo.Upsert(domainQuota) 调用没有处理返回的错误。虽然这是后台任务,但记录错误有助于排查问题。
🔧 建议的修复
- s.quotaRepo.Upsert(domainQuota)
+ if err := s.quotaRepo.Upsert(domainQuota); err != nil {
+ log.Printf("[AntigravityTask] Failed to upsert quota for email %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 quota to database | |
| func (s *AntigravityTaskService) saveQuotaToDB(email, projectID string, quota *antigravity.QuotaData) { | |
| if s.quotaRepo == nil || email == "" { | |
| return | |
| } | |
| var models []domain.AntigravityModelQuota | |
| var subscriptionTier string | |
| var isForbidden bool | |
| if quota != nil { | |
| models = make([]domain.AntigravityModelQuota, len(quota.Models)) | |
| for i, m := range quota.Models { | |
| models[i] = domain.AntigravityModelQuota{ | |
| Name: m.Name, | |
| Percentage: m.Percentage, | |
| ResetTime: m.ResetTime, | |
| } | |
| } | |
| subscriptionTier = quota.SubscriptionTier | |
| isForbidden = quota.IsForbidden | |
| } | |
| // Try to preserve existing user info | |
| var name, picture string | |
| if existing, _ := s.quotaRepo.GetByEmail(email); existing != nil { | |
| name = existing.Name | |
| picture = existing.Picture | |
| } | |
| domainQuota := &domain.AntigravityQuota{ | |
| Email: email, | |
| Name: name, | |
| Picture: picture, | |
| GCPProjectID: projectID, | |
| SubscriptionTier: subscriptionTier, | |
| IsForbidden: isForbidden, | |
| Models: models, | |
| } | |
| s.quotaRepo.Upsert(domainQuota) | |
| } | |
| // saveQuotaToDB saves quota to database | |
| func (s *AntigravityTaskService) saveQuotaToDB(email, projectID string, quota *antigravity.QuotaData) { | |
| if s.quotaRepo == nil || email == "" { | |
| return | |
| } | |
| var models []domain.AntigravityModelQuota | |
| var subscriptionTier string | |
| var isForbidden bool | |
| if quota != nil { | |
| models = make([]domain.AntigravityModelQuota, len(quota.Models)) | |
| for i, m := range quota.Models { | |
| models[i] = domain.AntigravityModelQuota{ | |
| Name: m.Name, | |
| Percentage: m.Percentage, | |
| ResetTime: m.ResetTime, | |
| } | |
| } | |
| subscriptionTier = quota.SubscriptionTier | |
| isForbidden = quota.IsForbidden | |
| } | |
| // Try to preserve existing user info | |
| var name, picture string | |
| if existing, _ := s.quotaRepo.GetByEmail(email); existing != nil { | |
| name = existing.Name | |
| picture = existing.Picture | |
| } | |
| domainQuota := &domain.AntigravityQuota{ | |
| Email: email, | |
| Name: name, | |
| Picture: picture, | |
| GCPProjectID: projectID, | |
| SubscriptionTier: subscriptionTier, | |
| IsForbidden: isForbidden, | |
| Models: models, | |
| } | |
| if err := s.quotaRepo.Upsert(domainQuota); err != nil { | |
| log.Printf("[AntigravityTask] Failed to upsert quota for email %s: %v", email, err) | |
| } | |
| } |
🤖 Prompt for AI Agents
In `@internal/service/antigravity_task.go` around lines 156 - 197, The Upsert call
in saveQuotaToDB ignores its returned error; update saveQuotaToDB (method on
AntigravityTaskService) to capture the error from
s.quotaRepo.Upsert(domainQuota) and handle it—log a descriptive error including
email and projectID and the error value (use s.logger.Errorf or, if the service
has no logger, use log.Printf) so failures are recorded for debugging; do not
change control flow otherwise.
| async refreshAntigravityQuotas(): Promise<{ success: boolean; refreshed: number }> { | ||
| const { data } = await axios.post<{ success: boolean; refreshed: number }>( | ||
| '/api/antigravity/refresh-quotas', | ||
| ); | ||
| return data; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n web/src/lib/transport/http-transport.ts | head -50Repository: awsl-project/maxx
Length of output: 1421
🏁 Script executed:
wc -l web/src/lib/transport/http-transport.tsRepository: awsl-project/maxx
Length of output: 104
🏁 Script executed:
cat -n web/src/lib/transport/http-transport.ts | tail -100Repository: awsl-project/maxx
Length of output: 3544
🏁 Script executed:
rg -n "class HttpTransport|authToken|refreshAntigravityQuotas" web/src/lib/transport/http-transport.ts -A 2 -B 2Repository: awsl-project/maxx
Length of output: 1113
🏁 Script executed:
rg -n "this\.authToken|Authorization" web/src/lib/transport/http-transport.tsRepository: awsl-project/maxx
Length of output: 227
🏁 Script executed:
rg -n "async.*\(\).*Promise" web/src/lib/transport/http-transport.ts | head -20Repository: awsl-project/maxx
Length of output: 1241
🏁 Script executed:
rg -n "axios\." web/src/lib/transport/http-transport.ts -B 2 -A 2Repository: awsl-project/maxx
Length of output: 2756
应使用 this.client 而非 axios 直接调用,以自动应用请求拦截器中的认证头。
该方法使用 axios.post 直接绕过了类中配置的请求拦截器(第 80-84 行),该拦截器负责在 authToken 存在时自动添加 Authorization 头。应改用 this.client.post 保持一致性,确保认证令牌在需要时被正确传递。
注意:该模式在类中其他多个方法中也存在(validateAntigravityToken、validateAntigravityTokens 等),建议检查所有这类调用。
🤖 Prompt for AI Agents
In `@web/src/lib/transport/http-transport.ts` around lines 404 - 409, The
refreshAntigravityQuotas method currently calls axios.post directly and bypasses
the class HTTP client and its request interceptor that injects Authorization
when authToken exists; change the implementation of refreshAntigravityQuotas to
call this.client.post<{ success: boolean; refreshed: number
}>('/api/antigravity/refresh-quotas') and return the response data so the auth
header from the interceptor is applied. Also scan for other methods mentioned
(validateAntigravityToken, validateAntigravityTokens, etc.) that use axios
directly and replace them with this.client to ensure consistent authentication
handling.
| // 格式化 lastUpdated 为相对时间 | ||
| function formatLastUpdated(timestamp: number): string { | ||
| if (!timestamp) return ''; | ||
| const now = Date.now(); | ||
| const diff = now - timestamp * 1000; | ||
| const minutes = Math.floor(diff / (1000 * 60)); | ||
|
|
||
| if (minutes < 1) return 'now'; | ||
| if (minutes < 60) return `${minutes}m`; | ||
| const hours = Math.floor(minutes / 60); | ||
| if (hours < 24) return `${hours}h`; | ||
| const days = Math.floor(hours / 24); | ||
| return `${days}d`; | ||
| } |
There was a problem hiding this comment.
本地化遗漏:now 与 Last updated。
这两个字符串为硬编码英文,建议改用 i18n key 并在多语言文件中补齐。
Also applies to: 461-463
🤖 Prompt for AI Agents
In `@web/src/pages/client-routes/components/provider-row.tsx` around lines 198 -
211, The hardcoded English strings in formatLastUpdated (the 'now' return value)
and the nearby "Last updated" usage should be replaced with i18n keys; update
formatLastUpdated to accept a translation function (e.g., t) or import the
project's translation helper and return t('provider.now') instead of 'now', and
replace the explicit "Last updated" text (see also the occurrences around lines
461-463) with t('provider.lastUpdated'); add the corresponding keys
(provider.now, provider.lastUpdated) to the locale files for all supported
languages and ensure the component passes the translator into formatLastUpdated
if you change its signature.
在 Routes 页面添加"一键排序 Antigravity"按钮,根据 Claude 模型的重刷时间 (resetTime) 对 Antigravity 类型的路由进行排序,越早重刷的排在前面。 - 仅当存在 Antigravity 路由时显示排序按钮 - 排序只影响 Antigravity 路由之间的顺序,非 Antigravity 路由位置不变 - 添加中英文 i18n 翻译
2c4770b to
0420bf9
Compare
- 新增后台任务服务 (AntigravityTaskService) 定期刷新配额 - 在 Provider 列表添加手动刷新额度按钮 - 显示 Image 模型额度 (alongside Claude quota) - 添加 /antigravity/refresh-quotas API 端点
0420bf9 to
5fcdb1f
Compare
Summary
Test plan
Summary by CodeRabbit
新功能
设置
界面
国际化
✏️ Tip: You can customize this high-level summary in your review settings.