From 471b1c3eeb3a0efec261dffdfe48277aab5af2c3 Mon Sep 17 00:00:00 2001
From: ianshaw
Date: Sun, 4 Jan 2026 22:26:33 -0800
Subject: [PATCH] =?UTF-8?q?fix(frontend):=20=E4=BF=AE=E5=A4=8D=E9=87=8D?=
=?UTF-8?q?=E6=9E=84=E6=97=B6=E9=81=97=E6=BC=8F=E7=9A=84=20SVG=20=E5=9B=BE?=
=?UTF-8?q?=E6=A0=87=EF=BC=8C=E5=88=9B=E5=BB=BA=E7=BB=9F=E4=B8=80=E5=9B=BE?=
=?UTF-8?q?=E6=A0=87=E7=AE=A1=E7=90=86=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 创建 Icon.vue 统一管理 SVG 图标(20+ 常用图标)
- 修复 AccountActionMenu 中被错误替换为 emoji 的图标
- 修复 ProfileView 和 GroupsView 中的 emoji 图标
- 图标支持 size/strokeWidth 属性,便于复用
---
.../service/antigravity_gateway_service.go | 61 +++++++++++++++--
.../admin/account/AccountActionMenu.vue | 34 ++++++++--
frontend/src/components/icons/Icon.vue | 66 +++++++++++++++++++
frontend/src/components/icons/index.ts | 1 +
frontend/src/views/admin/GroupsView.vue | 5 +-
frontend/src/views/user/ProfileView.vue | 3 +-
6 files changed, 156 insertions(+), 14 deletions(-)
create mode 100644 frontend/src/components/icons/Icon.vue
create mode 100644 frontend/src/components/icons/index.ts
diff --git a/backend/internal/service/antigravity_gateway_service.go b/backend/internal/service/antigravity_gateway_service.go
index cfbb85d2..35aa3937 100644
--- a/backend/internal/service/antigravity_gateway_service.go
+++ b/backend/internal/service/antigravity_gateway_service.go
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"log"
+ mathrand "math/rand"
"net/http"
"strings"
"sync/atomic"
@@ -405,6 +406,14 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
// 重试循环
var resp *http.Response
for attempt := 1; attempt <= antigravityMaxRetries; attempt++ {
+ // 检查 context 是否已取消(客户端断开连接)
+ select {
+ case <-ctx.Done():
+ log.Printf("%s status=context_canceled error=%v", prefix, ctx.Err())
+ return nil, ctx.Err()
+ default:
+ }
+
upstreamReq, err := antigravity.NewAPIRequest(ctx, action, accessToken, geminiBody)
if err != nil {
return nil, err
@@ -414,7 +423,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
if err != nil {
if attempt < antigravityMaxRetries {
log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err)
- sleepAntigravityBackoff(attempt)
+ if !sleepAntigravityBackoffWithContext(ctx, attempt) {
+ log.Printf("%s status=context_canceled_during_backoff", prefix)
+ return nil, ctx.Err()
+ }
continue
}
log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err)
@@ -427,7 +439,10 @@ func (s *AntigravityGatewayService) Forward(ctx context.Context, c *gin.Context,
if attempt < antigravityMaxRetries {
log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries)
- sleepAntigravityBackoff(attempt)
+ if !sleepAntigravityBackoffWithContext(ctx, attempt) {
+ log.Printf("%s status=context_canceled_during_backoff", prefix)
+ return nil, ctx.Err()
+ }
continue
}
// 所有重试都失败,标记限流状态
@@ -901,6 +916,14 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
// 重试循环
var resp *http.Response
for attempt := 1; attempt <= antigravityMaxRetries; attempt++ {
+ // 检查 context 是否已取消(客户端断开连接)
+ select {
+ case <-ctx.Done():
+ log.Printf("%s status=context_canceled error=%v", prefix, ctx.Err())
+ return nil, ctx.Err()
+ default:
+ }
+
upstreamReq, err := antigravity.NewAPIRequest(ctx, upstreamAction, accessToken, wrappedBody)
if err != nil {
return nil, err
@@ -910,7 +933,10 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
if err != nil {
if attempt < antigravityMaxRetries {
log.Printf("%s status=request_failed retry=%d/%d error=%v", prefix, attempt, antigravityMaxRetries, err)
- sleepAntigravityBackoff(attempt)
+ if !sleepAntigravityBackoffWithContext(ctx, attempt) {
+ log.Printf("%s status=context_canceled_during_backoff", prefix)
+ return nil, ctx.Err()
+ }
continue
}
log.Printf("%s status=request_failed retries_exhausted error=%v", prefix, err)
@@ -923,7 +949,10 @@ func (s *AntigravityGatewayService) ForwardGemini(ctx context.Context, c *gin.Co
if attempt < antigravityMaxRetries {
log.Printf("%s status=%d retry=%d/%d", prefix, resp.StatusCode, attempt, antigravityMaxRetries)
- sleepAntigravityBackoff(attempt)
+ if !sleepAntigravityBackoffWithContext(ctx, attempt) {
+ log.Printf("%s status=context_canceled_during_backoff", prefix)
+ return nil, ctx.Err()
+ }
continue
}
// 所有重试都失败,标记限流状态
@@ -1062,6 +1091,30 @@ func sleepAntigravityBackoff(attempt int) {
sleepGeminiBackoff(attempt) // 复用 Gemini 的退避逻辑
}
+// sleepAntigravityBackoffWithContext 带 context 取消检查的退避等待
+// 返回 true 表示正常完成等待,false 表示 context 已取消
+func sleepAntigravityBackoffWithContext(ctx context.Context, attempt int) bool {
+ delay := geminiRetryBaseDelay * time.Duration(1< geminiRetryMaxDelay {
+ delay = geminiRetryMaxDelay
+ }
+
+ // +/- 20% jitter
+ r := mathrand.New(mathrand.NewSource(time.Now().UnixNano()))
+ jitter := time.Duration(float64(delay) * 0.2 * (r.Float64()*2 - 1))
+ sleepFor := delay + jitter
+ if sleepFor < 0 {
+ sleepFor = 0
+ }
+
+ select {
+ case <-ctx.Done():
+ return false
+ case <-time.After(sleepFor):
+ return true
+ }
+}
+
func (s *AntigravityGatewayService) handleUpstreamError(ctx context.Context, prefix string, account *Account, statusCode int, headers http.Header, body []byte) {
// 429 使用 Gemini 格式解析(从 body 解析重置时间)
if statusCode == 429 {
diff --git a/frontend/src/components/admin/account/AccountActionMenu.vue b/frontend/src/components/admin/account/AccountActionMenu.vue
index ebf574d5..980fd352 100644
--- a/frontend/src/components/admin/account/AccountActionMenu.vue
+++ b/frontend/src/components/admin/account/AccountActionMenu.vue
@@ -3,15 +3,33 @@
-
-
+
+
-
-
+
+
-
-
+
+
@@ -21,10 +39,12 @@
\ No newline at end of file
+
diff --git a/frontend/src/components/icons/Icon.vue b/frontend/src/components/icons/Icon.vue
new file mode 100644
index 00000000..8632ef5f
--- /dev/null
+++ b/frontend/src/components/icons/Icon.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
diff --git a/frontend/src/components/icons/index.ts b/frontend/src/components/icons/index.ts
new file mode 100644
index 00000000..ea5ccfd4
--- /dev/null
+++ b/frontend/src/components/icons/index.ts
@@ -0,0 +1 @@
+export { default as Icon } from './Icon.vue'
diff --git a/frontend/src/views/admin/GroupsView.vue b/frontend/src/views/admin/GroupsView.vue
index ffa43edf..cb50afc6 100644
--- a/frontend/src/views/admin/GroupsView.vue
+++ b/frontend/src/views/admin/GroupsView.vue
@@ -336,7 +336,7 @@
- 💡 {{ t('admin.groups.exclusiveTooltip.example') }}
+ {{ t('admin.groups.exclusiveTooltip.example') }}
{{ t('admin.groups.exclusiveTooltip.exampleContent') }}
@@ -531,7 +531,7 @@
- 💡 {{ t('admin.groups.exclusiveTooltip.example') }}
+ {{ t('admin.groups.exclusiveTooltip.example') }}
{{ t('admin.groups.exclusiveTooltip.exampleContent') }}
@@ -691,6 +691,7 @@ import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
import EmptyState from '@/components/common/EmptyState.vue'
import Select from '@/components/common/Select.vue'
import PlatformIcon from '@/components/common/PlatformIcon.vue'
+import { Icon } from '@/components/icons'
const { t } = useI18n()
const appStore = useAppStore()
diff --git a/frontend/src/views/user/ProfileView.vue b/frontend/src/views/user/ProfileView.vue
index eaf98b77..a85ea5ca 100644
--- a/frontend/src/views/user/ProfileView.vue
+++ b/frontend/src/views/user/ProfileView.vue
@@ -9,7 +9,7 @@
-
💬
+
{{ t('common.contactSupport') }}
{{ contactInfo }}
@@ -27,6 +27,7 @@ import StatCard from '@/components/common/StatCard.vue'
import ProfileInfoCard from '@/components/user/profile/ProfileInfoCard.vue'
import ProfileEditForm from '@/components/user/profile/ProfileEditForm.vue'
import ProfilePasswordForm from '@/components/user/profile/ProfilePasswordForm.vue'
+import { Icon } from '@/components/icons'
const { t } = useI18n(); const authStore = useAuthStore(); const user = computed(() => authStore.user)
const contactInfo = ref('')