Skip to content

feat: 添加一键排序 Antigravity 路由功能#138

Merged
Bowl42 merged 2 commits intomainfrom
feat/sort-antigravity-routes
Jan 19, 2026
Merged

feat: 添加一键排序 Antigravity 路由功能#138
Bowl42 merged 2 commits intomainfrom
feat/sort-antigravity-routes

Conversation

@Bowl42
Copy link
Collaborator

@Bowl42 Bowl42 commented Jan 19, 2026

Summary

  • 在 Routes 页面添加"一键排序 Antigravity"按钮
  • 根据 Claude 模型的重刷时间 (resetTime) 对 Antigravity 类型的路由进行排序,越早重刷的排在前面
  • 排序只影响 Antigravity 路由之间的顺序,非 Antigravity 路由位置保持不变

Test plan

  • 在 Routes 页面添加多个 Antigravity 类型的 Provider 路由
  • 确认"一键排序 Antigravity"按钮显示
  • 点击按钮后,Antigravity 路由按 resetTime 从早到晚排序
  • 确认非 Antigravity 路由位置不受影响

Summary by CodeRabbit

  • 新功能

    • 新增“一键排序 Antigravity”按钮,按 Claude 重置时间整理路由并支持即时列表更新(失败回滚)。
    • 新增手动“刷新配额”操作,可触发 Antigravity 配额刷新并显示进行状态。
  • 设置

    • 添加 Antigravity 配置:配额刷新间隔与自动排序开关,支持保存生效。
  • 界面

    • 提示和列表增强:并排展示 Claude 与 Image 配额、“最后更新时间”及提供者元信息。
  • 国际化

    • 补充中/英文相关文案。

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

新增 Antigravity 后台任务服务与前端配额上下文:后端引入 AntigravityTaskService、后台循环与 HTTP 管理接口;仓库与模型新增接口/设置键;前端新增配额刷新/排序按钮、乐观路由排序、配额展示与多语言键,及若干组件签名调整。

Changes

Cohort / File(s) 变更摘要
前端:路由列表与排序 UI
web/src/components/routes/ClientTypeRoutesContent.tsx
改为接收单一 props 对象;加入 Antigravity 配额上下文、i18n 与 ArrowUpDown 图标;新增 “Sort Antigravity” 按钮、handleSortAntigravity(按 Claude resetTime 排序)、DnD/乐观更新与回滚逻辑。
前端:Provider 行与配额展示
web/src/pages/providers/components/provider-row.tsx, web/src/pages/client-routes/components/provider-row.tsx
扩展 Claude 配额返回包含 lastUpdated;新增 Image 配额解析与显示、formatLastUpdated;UI 同时展示 Claude 与 Image 进度与更新时间。
前端:传输层与接口
web/src/lib/transport/interface.ts, web/src/lib/transport/http-transport.ts
Transport 接口新增 refreshAntigravityQuotas(),HttpTransport 新增对应 POST /api/antigravity/refresh-quotas 实现,返回 { success, refreshed }。
前端:Providers 页面刷新按钮
web/src/pages/providers/index.tsx
为 Antigravity 源添加 “Refresh quotas” 按钮、isRefreshingQuotas 状态,调用 transport.refreshAntigravityQuotas 并失效相关查询缓存。
前端:设置页 Antigravity 配置
web/src/pages/settings/index.tsx
新增 AntigravitySection(quota_refresh_interval 与 auto_sort_antigravity),含保存与切换逻辑;注意文件内出现重复的 AntigravitySection 声明。
前端:本地化
web/src/locales/en.json, web/src/locales/zh.json
新增 Antigravity 相关翻译键(refreshQuotas、sortAntigravity、quotaRefreshInterval、autoSortAntigravity 等)并做细微格式调整。
后端:任务服务与自动化
internal/service/antigravity_task.go, cmd/maxx/main.go, internal/core/task.go
新增 AntigravityTaskService(刷新配额、强制刷新、自动排序、广播事件、读取设置、活动节流);在 main 中创建并注入服务;在后台任务循环中注册定期刷新逻辑。
后端:HTTP 处理扩展
internal/handler/antigravity.go
AntigravityHandler 新增内部字段并提供 SetTaskService 注入方法;新增路由:POST /antigravity/refresh-quotas/antigravity/sort-routes,并实现相应处理器。
后端:仓库与模型变更
internal/repository/interfaces.go, internal/repository/sqlite/proxy_request.go, internal/domain/model.go
ProxyRequestRepository 新增 HasRecentRequests(since time.Time) 方法及 sqlite 实现;domain 新增设置键常量 SettingKeyQuotaRefreshIntervalSettingKeyAutoSortAntigravity(系统设置键)。
前端:路由组件签名变更
web/src/components/routes/*
ClientTypeRoutesContent 签名改为接收单个 props 对象,影响调用方;内部采用 wrapper/inner 以接入配额上下文与新交互。

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • whhjdi
  • awsl233777

Poem

🐇 我是小兔子,轻点那按钮,
后台悄悄刷配额,路由排成群,
乐观先上阵,失败温柔回退,
配额与排序同舞,界面更欢欣,
咕噜咕噜,代码跑得轻又敏 ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.90% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR标题准确地反映了主要改动:添加Antigravity路由的一键排序功能,与变更集的核心目标完全一致。

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0420bf9 and 5fcdb1f.

📒 Files selected for processing (16)
  • cmd/maxx/main.go
  • internal/core/task.go
  • internal/domain/model.go
  • internal/handler/antigravity.go
  • internal/repository/interfaces.go
  • internal/repository/sqlite/proxy_request.go
  • internal/service/antigravity_task.go
  • web/src/components/routes/ClientTypeRoutesContent.tsx
  • web/src/lib/transport/http-transport.ts
  • web/src/lib/transport/interface.ts
  • web/src/locales/en.json
  • web/src/locales/zh.json
  • web/src/pages/client-routes/components/provider-row.tsx
  • web/src/pages/providers/components/provider-row.tsx
  • web/src/pages/providers/index.tsx
  • web/src/pages/settings/index.tsx

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


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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2a567c4 and e254be3.

📒 Files selected for processing (3)
  • web/src/components/routes/ClientTypeRoutesContent.tsx
  • web/src/locales/en.json
  • web/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.

Comment on lines +158 to +161
// Check if there are any Antigravity routes
const hasAntigravityRoutes = useMemo(() => {
return items.some((item) => item.provider.type === 'antigravity' && item.route);
}, [items]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

按钮显隐不应受搜索过滤影响。
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.

Comment on lines +164 to +175
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;
}
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Comment on lines +179 to +239
// 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]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

排序基于过滤列表会破坏全局位置。
当搜索过滤生效时,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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 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

📥 Commits

Reviewing files that changed from the base of the PR and between e254be3 and 2c4770b.

📒 Files selected for processing (15)
  • cmd/maxx/main.go
  • internal/core/task.go
  • internal/domain/model.go
  • internal/handler/antigravity.go
  • internal/repository/interfaces.go
  • internal/repository/sqlite/proxy_request.go
  • internal/service/antigravity_task.go
  • web/src/lib/transport/http-transport.ts
  • web/src/lib/transport/interface.ts
  • web/src/locales/en.json
  • web/src/locales/zh.json
  • web/src/pages/client-routes/components/provider-row.tsx
  • web/src/pages/providers/components/provider-row.tsx
  • web/src/pages/providers/index.tsx
  • web/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.

Comment on lines +69 to +79
// 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
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

需鉴权: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.

Comment on lines +156 to +197
// 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)
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
// 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.

Comment on lines +404 to +409
async refreshAntigravityQuotas(): Promise<{ success: boolean; refreshed: number }> {
const { data } = await axios.post<{ success: boolean; refreshed: number }>(
'/api/antigravity/refresh-quotas',
);
return data;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n web/src/lib/transport/http-transport.ts | head -50

Repository: awsl-project/maxx

Length of output: 1421


🏁 Script executed:

wc -l web/src/lib/transport/http-transport.ts

Repository: awsl-project/maxx

Length of output: 104


🏁 Script executed:

cat -n web/src/lib/transport/http-transport.ts | tail -100

Repository: 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 2

Repository: awsl-project/maxx

Length of output: 1113


🏁 Script executed:

rg -n "this\.authToken|Authorization" web/src/lib/transport/http-transport.ts

Repository: awsl-project/maxx

Length of output: 227


🏁 Script executed:

rg -n "async.*\(\).*Promise" web/src/lib/transport/http-transport.ts | head -20

Repository: awsl-project/maxx

Length of output: 1241


🏁 Script executed:

rg -n "axios\." web/src/lib/transport/http-transport.ts -B 2 -A 2

Repository: awsl-project/maxx

Length of output: 2756


应使用 this.client 而非 axios 直接调用,以自动应用请求拦截器中的认证头。

该方法使用 axios.post 直接绕过了类中配置的请求拦截器(第 80-84 行),该拦截器负责在 authToken 存在时自动添加 Authorization 头。应改用 this.client.post 保持一致性,确保认证令牌在需要时被正确传递。

注意:该模式在类中其他多个方法中也存在(validateAntigravityTokenvalidateAntigravityTokens 等),建议检查所有这类调用。

🤖 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.

Comment on lines +198 to +211
// 格式化 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`;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

本地化遗漏:nowLast 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 翻译
@Bowl42 Bowl42 force-pushed the feat/sort-antigravity-routes branch from 2c4770b to 0420bf9 Compare January 19, 2026 12:30
- 新增后台任务服务 (AntigravityTaskService) 定期刷新配额
- 在 Provider 列表添加手动刷新额度按钮
- 显示 Image 模型额度 (alongside Claude quota)
- 添加 /antigravity/refresh-quotas API 端点
@Bowl42 Bowl42 force-pushed the feat/sort-antigravity-routes branch from 0420bf9 to 5fcdb1f Compare January 19, 2026 12:38
@Bowl42 Bowl42 merged commit e34a25c into main Jan 19, 2026
1 check passed
@Bowl42 Bowl42 deleted the feat/sort-antigravity-routes branch January 19, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants