Merge tag 'v0.1.90' into merge/upstream-v0.1.90

注册邮箱域名白名单策略上线,后台大数据场景性能大幅优化。

- 注册邮箱域名白名单:支持管理员配置允许注册的邮箱域名策略
- Keys 页面表单筛选:用户 /keys 页面支持按条件筛选 API Key
- Settings 页面分 Tab 拆分:管理后台设置页面按功能模块分 Tab 展示

- 后台大数据场景加载性能优化:仪表盘/用户/账号/Ops 页面大数据集加载显著提速
- Usage 大表分页优化:默认避免全量 COUNT(*),大幅降低分页查询耗时
- 消除重复的 normalizeAccountIDList,补充新增组件的单元测试
- 清理无用文件和过时文档,精简项目结构
- EmailVerifyView 硬编码英文字符串替换为 i18n 调用

- 修复 Anthropic 平台无限流重置时间的 429 误标记账号限流问题
- 修复自定义菜单页面管理员视角菜单不生效问题
- 修复 Ops 错误详情弹窗未展示真实上游 payload 的问题
- 修复充值/订阅菜单 icon 显示问题

# Conflicts:
#	.gitignore
#	backend/cmd/server/VERSION
#	backend/ent/group.go
#	backend/ent/runtime/runtime.go
#	backend/ent/schema/group.go
#	backend/go.sum
#	backend/internal/handler/admin/account_handler.go
#	backend/internal/handler/admin/dashboard_handler.go
#	backend/internal/pkg/usagestats/usage_log_types.go
#	backend/internal/repository/group_repo.go
#	backend/internal/repository/usage_log_repo.go
#	backend/internal/server/middleware/security_headers.go
#	backend/internal/server/router.go
#	backend/internal/service/account_usage_service.go
#	backend/internal/service/admin_service_bulk_update_test.go
#	backend/internal/service/dashboard_service.go
#	backend/internal/service/gateway_service.go
#	frontend/src/api/admin/dashboard.ts
#	frontend/src/components/account/BulkEditAccountModal.vue
#	frontend/src/components/charts/GroupDistributionChart.vue
#	frontend/src/components/layout/AppSidebar.vue
#	frontend/src/i18n/locales/en.ts
#	frontend/src/i18n/locales/zh.ts
#	frontend/src/views/admin/GroupsView.vue
#	frontend/src/views/admin/SettingsView.vue
#	frontend/src/views/admin/UsageView.vue
#	frontend/src/views/user/PurchaseSubscriptionView.vue
This commit is contained in:
erio
2026-03-04 19:58:38 +08:00
461 changed files with 63392 additions and 6617 deletions

View File

@@ -152,6 +152,7 @@ var claudeModels = []modelDef{
{ID: "claude-sonnet-4-5", DisplayName: "Claude Sonnet 4.5", CreatedAt: "2025-09-29T00:00:00Z"},
{ID: "claude-sonnet-4-5-thinking", DisplayName: "Claude Sonnet 4.5 Thinking", CreatedAt: "2025-09-29T00:00:00Z"},
{ID: "claude-opus-4-6", DisplayName: "Claude Opus 4.6", CreatedAt: "2026-02-05T00:00:00Z"},
{ID: "claude-opus-4-6-thinking", DisplayName: "Claude Opus 4.6 Thinking", CreatedAt: "2026-02-05T00:00:00Z"},
{ID: "claude-sonnet-4-6", DisplayName: "Claude Sonnet 4.6", CreatedAt: "2026-02-17T00:00:00Z"},
}
@@ -165,6 +166,8 @@ var geminiModels = []modelDef{
{ID: "gemini-3-pro-high", DisplayName: "Gemini 3 Pro High", CreatedAt: "2025-06-01T00:00:00Z"},
{ID: "gemini-3.1-pro-low", DisplayName: "Gemini 3.1 Pro Low", CreatedAt: "2026-02-19T00:00:00Z"},
{ID: "gemini-3.1-pro-high", DisplayName: "Gemini 3.1 Pro High", CreatedAt: "2026-02-19T00:00:00Z"},
{ID: "gemini-3.1-flash-image", DisplayName: "Gemini 3.1 Flash Image", CreatedAt: "2026-02-19T00:00:00Z"},
{ID: "gemini-3.1-flash-image-preview", DisplayName: "Gemini 3.1 Flash Image Preview", CreatedAt: "2026-02-19T00:00:00Z"},
{ID: "gemini-3-pro-preview", DisplayName: "Gemini 3 Pro Preview", CreatedAt: "2025-06-01T00:00:00Z"},
{ID: "gemini-3-pro-image", DisplayName: "Gemini 3 Pro Image", CreatedAt: "2025-06-01T00:00:00Z"},
}

View File

@@ -0,0 +1,26 @@
package antigravity
import "testing"
func TestDefaultModels_ContainsNewAndLegacyImageModels(t *testing.T) {
t.Parallel()
models := DefaultModels()
byID := make(map[string]ClaudeModel, len(models))
for _, m := range models {
byID[m.ID] = m
}
requiredIDs := []string{
"claude-opus-4-6-thinking",
"gemini-3.1-flash-image",
"gemini-3.1-flash-image-preview",
"gemini-3-pro-image", // legacy compatibility
}
for _, id := range requiredIDs {
if _, ok := byID[id]; !ok {
t.Fatalf("expected model %q to be exposed in DefaultModels", id)
}
}
}

View File

@@ -14,6 +14,9 @@ import (
"net/url"
"strings"
"time"
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyurl"
"github.com/Wei-Shaw/sub2api/internal/pkg/proxyutil"
)
// NewAPIRequestWithURL 使用指定的 base URL 创建 Antigravity API 请求v1internal 端点)
@@ -149,22 +152,26 @@ type Client struct {
httpClient *http.Client
}
func NewClient(proxyURL string) *Client {
func NewClient(proxyURL string) (*Client, error) {
client := &http.Client{
Timeout: 30 * time.Second,
}
if strings.TrimSpace(proxyURL) != "" {
if proxyURLParsed, err := url.Parse(proxyURL); err == nil {
client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyURLParsed),
}
_, parsed, err := proxyurl.Parse(proxyURL)
if err != nil {
return nil, err
}
if parsed != nil {
transport := &http.Transport{}
if err := proxyutil.ConfigureTransportProxy(transport, parsed); err != nil {
return nil, fmt.Errorf("configure proxy: %w", err)
}
client.Transport = transport
}
return &Client{
httpClient: client,
}
}, nil
}
// isConnectionError 判断是否为连接错误网络超时、DNS 失败、连接拒绝)

View File

@@ -228,8 +228,20 @@ func TestGetTier_两者都为nil(t *testing.T) {
// NewClient
// ---------------------------------------------------------------------------
func mustNewClient(t *testing.T, proxyURL string) *Client {
t.Helper()
client, err := NewClient(proxyURL)
if err != nil {
t.Fatalf("NewClient(%q) failed: %v", proxyURL, err)
}
return client
}
func TestNewClient_无代理(t *testing.T) {
client := NewClient("")
client, err := NewClient("")
if err != nil {
t.Fatalf("NewClient 返回错误: %v", err)
}
if client == nil {
t.Fatal("NewClient 返回 nil")
}
@@ -246,7 +258,10 @@ func TestNewClient_无代理(t *testing.T) {
}
func TestNewClient_有代理(t *testing.T) {
client := NewClient("http://proxy.example.com:8080")
client, err := NewClient("http://proxy.example.com:8080")
if err != nil {
t.Fatalf("NewClient 返回错误: %v", err)
}
if client == nil {
t.Fatal("NewClient 返回 nil")
}
@@ -256,7 +271,10 @@ func TestNewClient_有代理(t *testing.T) {
}
func TestNewClient_空格代理(t *testing.T) {
client := NewClient(" ")
client, err := NewClient(" ")
if err != nil {
t.Fatalf("NewClient 返回错误: %v", err)
}
if client == nil {
t.Fatal("NewClient 返回 nil")
}
@@ -267,15 +285,13 @@ func TestNewClient_空格代理(t *testing.T) {
}
func TestNewClient_无效代理URL(t *testing.T) {
// 无效 URL 时 url.Parse 不一定返回错误Go 的 url.Parse 很宽容),
// 但 ://invalid 会导致解析错误
client := NewClient("://invalid")
if client == nil {
t.Fatal("NewClient 返回 nil")
// 无效 URL 应返回 error
_, err := NewClient("://invalid")
if err == nil {
t.Fatal("无效代理 URL 应返回错误")
}
// 无效 URL 解析失败时Transport 应保持 nil
if client.httpClient.Transport != nil {
t.Error("无效代理 URL 时 Transport 应为 nil")
if !strings.Contains(err.Error(), "invalid proxy URL") {
t.Errorf("错误信息应包含 'invalid proxy URL': got %s", err.Error())
}
}
@@ -499,7 +515,7 @@ func TestClient_ExchangeCode_无ClientSecret(t *testing.T) {
defaultClientSecret = ""
t.Cleanup(func() { defaultClientSecret = old })
client := NewClient("")
client := mustNewClient(t, "")
_, err := client.ExchangeCode(context.Background(), "code", "verifier")
if err == nil {
t.Fatal("缺少 client_secret 时应返回错误")
@@ -602,7 +618,7 @@ func TestClient_RefreshToken_无ClientSecret(t *testing.T) {
defaultClientSecret = ""
t.Cleanup(func() { defaultClientSecret = old })
client := NewClient("")
client := mustNewClient(t, "")
_, err := client.RefreshToken(context.Background(), "refresh-tok")
if err == nil {
t.Fatal("缺少 client_secret 时应返回错误")
@@ -1242,7 +1258,7 @@ func TestClient_LoadCodeAssist_Success_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
resp, rawResp, err := client.LoadCodeAssist(context.Background(), "test-token")
if err != nil {
t.Fatalf("LoadCodeAssist 失败: %v", err)
@@ -1277,7 +1293,7 @@ func TestClient_LoadCodeAssist_HTTPError_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
_, _, err := client.LoadCodeAssist(context.Background(), "bad-token")
if err == nil {
t.Fatal("服务器返回 403 时应返回错误")
@@ -1300,7 +1316,7 @@ func TestClient_LoadCodeAssist_InvalidJSON_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
_, _, err := client.LoadCodeAssist(context.Background(), "token")
if err == nil {
t.Fatal("无效 JSON 响应应返回错误")
@@ -1333,7 +1349,7 @@ func TestClient_LoadCodeAssist_URLFallback_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := NewClient("")
client := mustNewClient(t, "")
resp, _, err := client.LoadCodeAssist(context.Background(), "token")
if err != nil {
t.Fatalf("LoadCodeAssist 应在 fallback 后成功: %v", err)
@@ -1361,7 +1377,7 @@ func TestClient_LoadCodeAssist_AllURLsFail_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := NewClient("")
client := mustNewClient(t, "")
_, _, err := client.LoadCodeAssist(context.Background(), "token")
if err == nil {
t.Fatal("所有 URL 都失败时应返回错误")
@@ -1377,7 +1393,7 @@ func TestClient_LoadCodeAssist_ContextCanceled_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
ctx, cancel := context.WithCancel(context.Background())
cancel()
@@ -1441,7 +1457,7 @@ func TestClient_FetchAvailableModels_Success_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
resp, rawResp, err := client.FetchAvailableModels(context.Background(), "test-token", "project-abc")
if err != nil {
t.Fatalf("FetchAvailableModels 失败: %v", err)
@@ -1496,7 +1512,7 @@ func TestClient_FetchAvailableModels_HTTPError_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "bad-token", "proj")
if err == nil {
t.Fatal("服务器返回 403 时应返回错误")
@@ -1516,7 +1532,7 @@ func TestClient_FetchAvailableModels_InvalidJSON_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
if err == nil {
t.Fatal("无效 JSON 响应应返回错误")
@@ -1546,7 +1562,7 @@ func TestClient_FetchAvailableModels_URLFallback_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := NewClient("")
client := mustNewClient(t, "")
resp, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
if err != nil {
t.Fatalf("FetchAvailableModels 应在 fallback 后成功: %v", err)
@@ -1574,7 +1590,7 @@ func TestClient_FetchAvailableModels_AllURLsFail_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := NewClient("")
client := mustNewClient(t, "")
_, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
if err == nil {
t.Fatal("所有 URL 都失败时应返回错误")
@@ -1590,7 +1606,7 @@ func TestClient_FetchAvailableModels_ContextCanceled_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
ctx, cancel := context.WithCancel(context.Background())
cancel()
@@ -1610,7 +1626,7 @@ func TestClient_FetchAvailableModels_EmptyModels_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server.URL})
client := NewClient("")
client := mustNewClient(t, "")
resp, rawResp, err := client.FetchAvailableModels(context.Background(), "token", "proj")
if err != nil {
t.Fatalf("FetchAvailableModels 失败: %v", err)
@@ -1646,7 +1662,7 @@ func TestClient_LoadCodeAssist_408Fallback_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := NewClient("")
client := mustNewClient(t, "")
resp, _, err := client.LoadCodeAssist(context.Background(), "token")
if err != nil {
t.Fatalf("LoadCodeAssist 应在 408 fallback 后成功: %v", err)
@@ -1672,7 +1688,7 @@ func TestClient_FetchAvailableModels_404Fallback_RealCall(t *testing.T) {
withMockBaseURLs(t, []string{server1.URL, server2.URL})
client := NewClient("")
client := mustNewClient(t, "")
resp, _, err := client.FetchAvailableModels(context.Background(), "token", "proj")
if err != nil {
t.Fatalf("FetchAvailableModels 应在 404 fallback 后成功: %v", err)

View File

@@ -70,7 +70,7 @@ type GeminiGenerationConfig struct {
ImageConfig *GeminiImageConfig `json:"imageConfig,omitempty"`
}
// GeminiImageConfig Gemini 图片生成配置(gemini-3-pro-image 支持)
// GeminiImageConfig Gemini 图片生成配置gemini-3-pro-image / gemini-3.1-flash-image 等图片模型支持)
type GeminiImageConfig struct {
AspectRatio string `json:"aspectRatio,omitempty"` // "1:1", "16:9", "9:16", "4:3", "3:4"
ImageSize string `json:"imageSize,omitempty"` // "1K", "2K", "4K"

View File

@@ -612,14 +612,14 @@ func TestBuildAuthorizationURL_参数验证(t *testing.T) {
expectedParams := map[string]string{
"client_id": ClientID,
"redirect_uri": RedirectURI,
"response_type": "code",
"scope": Scopes,
"state": state,
"code_challenge": codeChallenge,
"code_challenge_method": "S256",
"access_type": "offline",
"prompt": "consent",
"redirect_uri": RedirectURI,
"response_type": "code",
"scope": Scopes,
"state": state,
"code_challenge": codeChallenge,
"code_challenge_method": "S256",
"access_type": "offline",
"prompt": "consent",
"include_granted_scopes": "true",
}