From 2635d37573472271798e6b5071d49064d9dc5e4d Mon Sep 17 00:00:00 2001 From: GcsSloop Date: Thu, 2 Jul 2026 16:15:27 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=B4=A6=E6=88=B7?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=87=8D=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/accounts/repository.go | 3 +- backend/internal/accounts/repository_test.go | 62 ++++++++++++++++++++ backend/internal/store/sqlite/store.go | 12 ++++ backend/internal/store/sqlite/store_test.go | 43 ++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/backend/internal/accounts/repository.go b/backend/internal/accounts/repository.go index 74caf69..f6f6b38 100644 --- a/backend/internal/accounts/repository.go +++ b/backend/internal/accounts/repository.go @@ -205,7 +205,7 @@ func (r *SQLiteRepository) Update(account Account) error { _, err = r.db.Exec( `UPDATE accounts - SET account_name = ?, source_icon = ?, base_url = ?, credential_ref = ?, account_driver = ?, usage_driver = ?, usage_config_json = ?, status = ?, priority = ?, is_active = ?, is_locked = ?, supports_responses = ?, skip_tls_verify = ?, cooldown_until = ?, cooldown_reason = ? + SET account_name = ?, source_icon = ?, base_url = ?, credential_ref = ?, account_driver = ?, usage_driver = ?, usage_config_json = ?, status = ?, priority = ?, is_locked = ?, supports_responses = ?, skip_tls_verify = ?, cooldown_until = ?, cooldown_reason = ? WHERE id = ?`, account.AccountName, account.SourceIcon, @@ -216,7 +216,6 @@ func (r *SQLiteRepository) Update(account Account) error { account.UsageConfigJSON, account.Status, account.Priority, - boolToInt(account.IsActive), boolToInt(account.IsLocked), boolToInt(account.SupportsResponses), boolToInt(account.SkipTLSVerify), diff --git a/backend/internal/accounts/repository_test.go b/backend/internal/accounts/repository_test.go index 80fbcc3..dc2239b 100644 --- a/backend/internal/accounts/repository_test.go +++ b/backend/internal/accounts/repository_test.go @@ -289,6 +289,68 @@ func TestSQLiteRepositorySetActiveKeepsSingleActiveAccount(t *testing.T) { } } +func TestSQLiteRepositoryUpdateDoesNotReactivateStaleAccount(t *testing.T) { + t.Parallel() + + store, err := sqlitestore.Open(filepath.Join(t.TempDir(), "router.sqlite")) + if err != nil { + t.Fatalf("Open returned error: %v", err) + } + t.Cleanup(func() { + _ = store.Close() + }) + + repo := accounts.NewSQLiteRepository(store.DB()) + for _, item := range []accounts.Account{ + {ProviderType: accounts.ProviderOpenAICompatible, AccountName: "a", AuthMode: accounts.AuthModeAPIKey, CredentialRef: "sk-a", Priority: 2, Status: accounts.StatusActive}, + {ProviderType: accounts.ProviderOpenAICompatible, AccountName: "b", AuthMode: accounts.AuthModeAPIKey, CredentialRef: "sk-b", Priority: 1, Status: accounts.StatusActive}, + } { + if err := repo.Create(item); err != nil { + t.Fatalf("Create returned error: %v", err) + } + } + + if err := repo.SetActive(1); err != nil { + t.Fatalf("SetActive(1) returned error: %v", err) + } + staleActive, err := repo.GetByID(1) + if err != nil { + t.Fatalf("GetByID returned error: %v", err) + } + if !staleActive.IsActive { + t.Fatal("stale account snapshot should start active") + } + if err := repo.SetActive(2); err != nil { + t.Fatalf("SetActive(2) returned error: %v", err) + } + + staleActive.AccountName = "a-renamed" + if err := repo.Update(staleActive); err != nil { + t.Fatalf("Update returned error: %v", err) + } + + items, err := repo.List() + if err != nil { + t.Fatalf("List returned error: %v", err) + } + if len(items) != 2 { + t.Fatalf("List returned %d items, want 2", len(items)) + } + byID := map[int64]accounts.Account{} + for _, item := range items { + byID[item.ID] = item + } + if byID[1].AccountName != "a-renamed" { + t.Fatalf("account id=1 name = %q, want a-renamed", byID[1].AccountName) + } + if byID[1].IsActive { + t.Fatal("stale update reactivated account id=1") + } + if !byID[2].IsActive { + t.Fatal("account id=2 should remain active") + } +} + func TestSQLiteRepositoryPersistsAccountLockState(t *testing.T) { t.Parallel() diff --git a/backend/internal/store/sqlite/store.go b/backend/internal/store/sqlite/store.go index 5d62e49..77cbe65 100644 --- a/backend/internal/store/sqlite/store.go +++ b/backend/internal/store/sqlite/store.go @@ -159,6 +159,18 @@ func (s *Store) migrate() error { if _, err := s.db.Exec(`UPDATE accounts SET status = 'active' WHERE status = 'cooldown'`); err != nil { return fmt.Errorf("normalize account cooldown status: %w", err) } + if _, err := s.db.Exec(`UPDATE accounts + SET is_active = 0 + WHERE is_active = 1 + AND id NOT IN ( + SELECT id + FROM accounts + WHERE is_active = 1 + ORDER BY priority DESC, id ASC + LIMIT 1 + )`); err != nil { + return fmt.Errorf("normalize duplicate active accounts: %w", err) + } if _, err := s.db.Exec(`UPDATE server_users SET username = name WHERE username = ''`); err != nil { return fmt.Errorf("backfill server user usernames: %w", err) } diff --git a/backend/internal/store/sqlite/store_test.go b/backend/internal/store/sqlite/store_test.go index 3548482..c01f138 100644 --- a/backend/internal/store/sqlite/store_test.go +++ b/backend/internal/store/sqlite/store_test.go @@ -94,6 +94,49 @@ func TestOpenAddsSnapshotMetadataColumns(t *testing.T) { } } +func TestOpenNormalizesDuplicateActiveAccounts(t *testing.T) { + t.Parallel() + + dbPath := filepath.Join(t.TempDir(), "codex-router.sqlite") + + store, err := sqlite.Open(dbPath) + if err != nil { + t.Fatalf("Open returned error: %v", err) + } + if _, err := store.DB().Exec(`INSERT INTO accounts (provider_type, account_name, source_icon, auth_mode, credential_ref, base_url, status, priority, is_active, supports_responses) + VALUES + ('openai-compatible', 'primary', 'openai', 'api_key', 'secret-a', 'https://a.example.test/v1', 'active', 10, 1, 1), + ('openai-compatible', 'secondary', 'openai', 'api_key', 'secret-b', 'https://b.example.test/v1', 'active', 20, 1, 1)`); err != nil { + t.Fatalf("seed duplicate active accounts returned error: %v", err) + } + if err := store.Close(); err != nil { + t.Fatalf("Close returned error: %v", err) + } + + reopened, err := sqlite.Open(dbPath) + if err != nil { + t.Fatalf("reopen returned error: %v", err) + } + t.Cleanup(func() { + _ = reopened.Close() + }) + + var activeCount int + if err := reopened.DB().QueryRow(`SELECT COUNT(*) FROM accounts WHERE is_active = 1`).Scan(&activeCount); err != nil { + t.Fatalf("count active accounts returned error: %v", err) + } + if activeCount != 1 { + t.Fatalf("active accounts = %d, want 1", activeCount) + } + var activeName string + if err := reopened.DB().QueryRow(`SELECT account_name FROM accounts WHERE is_active = 1`).Scan(&activeName); err != nil { + t.Fatalf("select active account returned error: %v", err) + } + if activeName != "secondary" { + t.Fatalf("active account = %q, want highest-priority secondary", activeName) + } +} + func TestOpenConfiguresSQLiteForBusyDesktopWorkload(t *testing.T) { t.Parallel() From a1bd278ec36d95892f290e313caadd2b3e80fa2b Mon Sep 17 00:00:00 2001 From: GcsSloop Date: Thu, 2 Jul 2026 16:15:55 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E5=85=BC=E5=AE=B9=E6=9C=80=E6=96=B0?= =?UTF-8?q?=20Codex=20=E7=94=A8=E9=87=8F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/providers/codex/adapter.go | 40 ++++++-- .../internal/providers/codex/adapter_test.go | 41 ++++++++ .../internal/usagedrv/builtin/builtin_test.go | 93 ++++++++++++++++++- .../usagedrv/builtin/openai_official.go | 67 ++++++++++--- references/openai-codex | 2 +- 5 files changed, 223 insertions(+), 20 deletions(-) create mode 100644 backend/internal/providers/codex/adapter_test.go diff --git a/backend/internal/providers/codex/adapter.go b/backend/internal/providers/codex/adapter.go index b0f4290..f657e4e 100644 --- a/backend/internal/providers/codex/adapter.go +++ b/backend/internal/providers/codex/adapter.go @@ -12,7 +12,7 @@ import ( const ( // Codex ChatGPT backend gates some models (e.g. gpt-5.5) behind minimum Codex versions. // Use a recent Codex CLI version string to avoid unnecessary model gating. - codexClientVersion = "0.125.0" + codexClientVersion = "0.142.5" codexOriginator = "codex_cli_rs" codexUserAgent = "codex_cli_rs/" + codexClientVersion + " (ai-gate)" ) @@ -62,15 +62,12 @@ func (a *Adapter) BuildResponsesEndpointRequest(ctx context.Context, credential } func (a *Adapter) BuildUsageRequest(ctx context.Context, credential string, accountID string) (*http.Request, error) { - base, err := url.Parse(a.baseURL) + usageURL, err := a.buildUsageURL() if err != nil { return nil, err } - base.Path = "/backend-api/wham/usage" - base.RawQuery = "" - base.Fragment = "" - req, err := http.NewRequestWithContext(ctx, http.MethodGet, base.String(), nil) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, usageURL, nil) if err != nil { return nil, err } @@ -82,6 +79,37 @@ func (a *Adapter) BuildUsageRequest(ctx context.Context, credential string, acco return req, nil } +func (a *Adapter) buildUsageURL() (string, error) { + base, err := url.Parse(a.baseURL) + if err != nil { + return "", err + } + base.RawQuery = "" + base.Fragment = "" + + if isChatGPTUsageBase(base) { + base.Path = chatGPTBackendPath(base.Path) + "/wham/usage" + return base.String(), nil + } + + base.Path = strings.TrimRight(base.Path, "/") + "/api/codex/usage" + return base.String(), nil +} + +func isChatGPTUsageBase(base *url.URL) bool { + host := strings.ToLower(base.Hostname()) + return host == "chatgpt.com" || + host == "chat.openai.com" || + strings.Contains(base.Path, "/backend-api") +} + +func chatGPTBackendPath(path string) string { + if idx := strings.Index(path, "/backend-api"); idx >= 0 { + return path[:idx] + "/backend-api" + } + return "/backend-api" +} + func strconvTimeID() string { return time.Now().UTC().Format("20060102T150405.000000000") } diff --git a/backend/internal/providers/codex/adapter_test.go b/backend/internal/providers/codex/adapter_test.go new file mode 100644 index 0000000..3500420 --- /dev/null +++ b/backend/internal/providers/codex/adapter_test.go @@ -0,0 +1,41 @@ +package codex + +import ( + "context" + "testing" +) + +func TestAdapterBuildUsageRequestUsesLatestClientVersionAndChatGPTUsagePath(t *testing.T) { + t.Parallel() + + req, err := NewAdapter("https://chatgpt.com/backend-api/codex").BuildUsageRequest( + context.Background(), + "token-1", + "acct-1", + ) + if err != nil { + t.Fatalf("BuildUsageRequest returned error: %v", err) + } + if req.URL.String() != "https://chatgpt.com/backend-api/wham/usage" { + t.Fatalf("url = %q, want ChatGPT WHAM usage endpoint", req.URL.String()) + } + if got := req.Header.Get("User-Agent"); got != "codex_cli_rs/0.142.5 (ai-gate)" { + t.Fatalf("User-Agent = %q, want latest Codex client version", got) + } +} + +func TestAdapterBuildUsageRequestUsesCodexAPIPathForNonChatGPTBase(t *testing.T) { + t.Parallel() + + req, err := NewAdapter("https://codex.example.test/root").BuildUsageRequest( + context.Background(), + "token-1", + "acct-1", + ) + if err != nil { + t.Fatalf("BuildUsageRequest returned error: %v", err) + } + if req.URL.String() != "https://codex.example.test/root/api/codex/usage" { + t.Fatalf("url = %q, want Codex API usage endpoint", req.URL.String()) + } +} diff --git a/backend/internal/usagedrv/builtin/builtin_test.go b/backend/internal/usagedrv/builtin/builtin_test.go index 2d35ac7..9f276e6 100644 --- a/backend/internal/usagedrv/builtin/builtin_test.go +++ b/backend/internal/usagedrv/builtin/builtin_test.go @@ -8,6 +8,7 @@ import ( "net/http/httptest" "strings" "testing" + "time" "github.com/gcssloop/codex-router/backend/internal/accountdrv" "github.com/gcssloop/codex-router/backend/internal/accounts" @@ -33,7 +34,7 @@ func TestOpenAIOfficialDriverFetchParsesUsageLimits(t *testing.T) { driver := builtin.NewOpenAIOfficialDriver(http.DefaultClient) result, err := driver.Fetch(context.Background(), accounts.Account{ ProviderType: accounts.ProviderOpenAIOfficial, - BaseURL: server.URL, + BaseURL: server.URL + "/backend-api/codex", }, accountdrv.ResolvedCredential{ AccessToken: "at-token", Metadata: map[string]any{"account_id": "acct-1"}, @@ -67,6 +68,96 @@ func TestOpenAIOfficialDriverFetchParsesUsageLimits(t *testing.T) { } } +func TestOpenAIOfficialDriverFetchParsesCurrentCodexRateLimits(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/backend-api/wham/usage" { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{ + "plan_type":"pro", + "rate_limit":{ + "allowed":true, + "limit_reached":false, + "primary_window":{ + "used_percent":52, + "limit_window_seconds":18000, + "reset_after_seconds":3600, + "reset_at":1767205800 + }, + "secondary_window":{ + "used_percent":51, + "limit_window_seconds":604800, + "reset_after_seconds":86400, + "reset_at":1783350300 + } + }, + "credits":{"has_credits":true,"unlimited":false,"balance":"0"}, + "additional_rate_limits":[ + { + "limit_name":"codex_other", + "metered_feature":"codex_other", + "rate_limit":{ + "allowed":true, + "limit_reached":false, + "primary_window":{ + "used_percent":88, + "limit_window_seconds":1800, + "reset_after_seconds":600, + "reset_at":1735693200 + } + } + } + ], + "rate_limit_reached_type":{"type":"workspace_member_usage_limit_reached"}, + "rate_limit_reset_credits":{"available_count":1} + }`)) + })) + defer server.Close() + + driver := builtin.NewOpenAIOfficialDriver(http.DefaultClient) + result, err := driver.Fetch(context.Background(), accounts.Account{ + ProviderType: accounts.ProviderOpenAIOfficial, + BaseURL: server.URL + "/backend-api/codex", + }, accountdrv.ResolvedCredential{ + AccessToken: "at-token", + Metadata: map[string]any{"account_id": "acct-1"}, + }) + if err != nil { + t.Fatalf("Fetch returned error: %v", err) + } + if result.Limits.PrimaryUsedPercent == nil || *result.Limits.PrimaryUsedPercent != 52 { + t.Fatalf("PrimaryUsedPercent = %#v, want 52", result.Limits.PrimaryUsedPercent) + } + if result.Limits.SecondaryUsedPercent == nil || *result.Limits.SecondaryUsedPercent != 51 { + t.Fatalf("SecondaryUsedPercent = %#v, want 51", result.Limits.SecondaryUsedPercent) + } + if result.Limits.RPMRemaining == nil || *result.Limits.RPMRemaining != 48 { + t.Fatalf("RPMRemaining = %#v, want 48", result.Limits.RPMRemaining) + } + if result.Limits.TPMRemaining == nil || *result.Limits.TPMRemaining != 49 { + t.Fatalf("TPMRemaining = %#v, want 49", result.Limits.TPMRemaining) + } + wantPrimaryReset := time.Unix(1767205800, 0).UTC() + if result.Limits.PrimaryResetsAt == nil || !result.Limits.PrimaryResetsAt.Equal(wantPrimaryReset) { + t.Fatalf("PrimaryResetsAt = %#v, want %s", result.Limits.PrimaryResetsAt, wantPrimaryReset) + } + wantSecondaryReset := time.Unix(1783350300, 0).UTC() + if result.Limits.SecondaryResetsAt == nil || !result.Limits.SecondaryResetsAt.Equal(wantSecondaryReset) { + t.Fatalf("SecondaryResetsAt = %#v, want %s", result.Limits.SecondaryResetsAt, wantSecondaryReset) + } + if got, ok := result.Meta["plan_type"].(string); !ok || got != "pro" { + t.Fatalf("meta.plan_type = %#v, want pro", result.Meta["plan_type"]) + } + reachedType, ok := result.Meta["rate_limit_reached_type"].(map[string]any) + if !ok || reachedType["type"] != "workspace_member_usage_limit_reached" { + t.Fatalf("meta.rate_limit_reached_type = %#v, want workspace_member_usage_limit_reached", result.Meta["rate_limit_reached_type"]) + } +} + func TestOpenAIOfficialDriverFetchClassifiesErrors(t *testing.T) { t.Parallel() diff --git a/backend/internal/usagedrv/builtin/openai_official.go b/backend/internal/usagedrv/builtin/openai_official.go index 7794a3c..10a1bad 100644 --- a/backend/internal/usagedrv/builtin/openai_official.go +++ b/backend/internal/usagedrv/builtin/openai_official.go @@ -124,10 +124,18 @@ func parseOfficialRawUsage(raw []byte) (usagedrv.RawUsageResult, bool) { return usagedrv.RawUsageResult{}, false } - rateLimit, _ := payload["rate_limit"].(map[string]any) - primary, _ := rateLimit["primary_window"].(map[string]any) - secondary, _ := rateLimit["secondary_window"].(map[string]any) - credits, _ := payload["credits"].(map[string]any) + rateLimit := asMap(payload["rate_limit"]) + credits := asMap(payload["credits"]) + if rateLimits := asMap(payload["rate_limits"]); len(rateLimits) > 0 { + if len(rateLimit) == 0 { + rateLimit = rateLimits + } + if len(credits) == 0 { + credits = asMap(rateLimits["credits"]) + } + } + primary := firstMap(rateLimit["primary_window"], rateLimit["primary"]) + secondary := firstMap(rateLimit["secondary_window"], rateLimit["secondary"]) primaryUsed := asFloat(primary["used_percent"]) secondaryUsed := asFloat(secondary["used_percent"]) @@ -143,8 +151,17 @@ func parseOfficialRawUsage(raw []byte) (usagedrv.RawUsageResult, bool) { primaryRemaining := maxFloat(100-primaryUsed, 0) secondaryRemaining := maxFloat(100-secondaryUsed, 0) - primaryResetsAt := unixSecondsPtr(int64(asFloat(primary["reset_at"]))) - secondaryResetsAt := unixSecondsPtr(int64(asFloat(secondary["reset_at"]))) + primaryResetsAt := unixSecondsPtr(int64(firstFloat(primary["reset_at"], primary["resets_at"]))) + secondaryResetsAt := unixSecondsPtr(int64(firstFloat(secondary["reset_at"], secondary["resets_at"]))) + meta := map[string]any{ + "allowed": allowed, + "limit_reached": limitReached, + "has_credits": hasCredits, + "unlimited": unlimited, + } + copyMetaValue(meta, payload, "plan_type") + copyMetaValue(meta, payload, "rate_limit_reached_type") + copyMetaValue(meta, payload, "rate_limit_reset_credits") return usagedrv.RawUsageResult{ Source: "remote", @@ -159,12 +176,7 @@ func parseOfficialRawUsage(raw []byte) (usagedrv.RawUsageResult, bool) { PrimaryResetsAt: primaryResetsAt, SecondaryResetsAt: secondaryResetsAt, }, - Meta: map[string]any{ - "allowed": allowed, - "limit_reached": limitReached, - "has_credits": hasCredits, - "unlimited": unlimited, - }, + Meta: meta, }, true } @@ -199,6 +211,37 @@ func unixSecondsPtr(seconds int64) *time.Time { return &value } +func asMap(value any) map[string]any { + if typed, ok := value.(map[string]any); ok { + return typed + } + return nil +} + +func firstMap(values ...any) map[string]any { + for _, value := range values { + if typed := asMap(value); len(typed) > 0 { + return typed + } + } + return nil +} + +func firstFloat(values ...any) float64 { + for _, value := range values { + if parsed := asFloat(value); parsed != 0 { + return parsed + } + } + return 0 +} + +func copyMetaValue(meta map[string]any, payload map[string]any, key string) { + if value, ok := payload[key]; ok { + meta[key] = value + } +} + func asFloat(value any) float64 { switch typed := value.(type) { case float64: diff --git a/references/openai-codex b/references/openai-codex index 637f7dd..26de830 160000 --- a/references/openai-codex +++ b/references/openai-codex @@ -1 +1 @@ -Subproject commit 637f7dd6d737f3961e6bf32fbb3861c4953269c5 +Subproject commit 26de83050b20f7e0ee211b9739e52ae00ce8032a From b333ec947a6c36dd76699be77bf785977ef9613b Mon Sep 17 00:00:00 2001 From: GcsSloop Date: Thu, 2 Jul 2026 16:16:02 +0800 Subject: [PATCH 3/4] =?UTF-8?q?chore:=20=E5=90=8C=E6=AD=A5=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=89=88=E6=9C=AC=201.6.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- desktop/package-lock.json | 4 ++-- desktop/package.json | 2 +- desktop/src-tauri/Cargo.lock | 2 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/desktop/package-lock.json b/desktop/package-lock.json index 857d75a..1618c60 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,12 +1,12 @@ { "name": "aigate-desktop", - "version": "1.6.1", + "version": "1.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aigate-desktop", - "version": "1.6.1", + "version": "1.6.2", "devDependencies": { "@tauri-apps/cli": "^2.8.0" } diff --git a/desktop/package.json b/desktop/package.json index f97a16a..237ebe9 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,7 +1,7 @@ { "name": "aigate-desktop", "private": true, - "version": "1.6.1", + "version": "1.6.2", "type": "module", "scripts": { "dev": "tauri dev", diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 56502b4..69bb6ea 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "aigate-desktop" -version = "1.6.1" +version = "1.6.2" dependencies = [ "arboard", "base64 0.22.1", diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 01f72a9..e2c236b 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aigate-desktop" -version = "1.6.1" +version = "1.6.2" edition = "2021" [build-dependencies] diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 180d71e..ca8282e 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "AI Gate", - "version": "1.6.1", + "version": "1.6.2", "identifier": "com.aigate.desktop", "build": { "frontendDist": "../../frontend/dist", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3befb9c..5a1ab48 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "aigate-frontend", - "version": "1.6.1", + "version": "1.6.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aigate-frontend", - "version": "1.6.1", + "version": "1.6.2", "dependencies": { "@ant-design/icons": "^6.1.0", "@dnd-kit/core": "^6.3.1", diff --git a/frontend/package.json b/frontend/package.json index a2d7bb8..bf48415 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "aigate-frontend", "private": true, - "version": "1.6.1", + "version": "1.6.2", "type": "module", "scripts": { "dev": "vite", From 2f10728099918cfb9035230e778c1c2777bbcf07 Mon Sep 17 00:00:00 2001 From: GcsSloop Date: Thu, 2 Jul 2026 16:49:03 +0800 Subject: [PATCH 4/4] =?UTF-8?q?test:=20=E7=A8=B3=E5=AE=9A=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E5=A4=87=E4=BB=BD=E8=8F=9C=E5=8D=95=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/features/settings/SettingsPage.test.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frontend/src/features/settings/SettingsPage.test.tsx b/frontend/src/features/settings/SettingsPage.test.tsx index a4efaeb..645efb5 100644 --- a/frontend/src/features/settings/SettingsPage.test.tsx +++ b/frontend/src/features/settings/SettingsPage.test.tsx @@ -48,6 +48,9 @@ describe("SettingsPage", () => { async function openBackupMenu(label = "备份操作 20260309-101500.000") { const trigger = await screen.findByRole("button", { name: label }); + await waitFor(() => { + expect(trigger).not.toBeDisabled(); + }); await act(async () => { fireEvent.click(trigger); }); @@ -364,6 +367,12 @@ describe("SettingsPage", () => { }), ); }); + await waitFor(() => { + expect( + fetchMock.mock.calls.filter(([input, init]) => String(input) === "/ai-router/api/settings/database/backups" && (!init?.method || init.method === "GET")) + .length, + ).toBeGreaterThanOrEqual(3); + }); await act(async () => { await openBackupMenu();