diff --git a/internal/account/pool_test.go b/internal/account/pool_test.go index 59ea32b..37109ff 100644 --- a/internal/account/pool_test.go +++ b/internal/account/pool_test.go @@ -194,7 +194,7 @@ func TestPoolAccountConcurrencyAliasEnv(t *testing.T) { } } -func TestPoolSupportsTokenOnlyAccount(t *testing.T) { +func TestPoolDropsLegacyTokenOnlyAccountOnLoad(t *testing.T) { t.Setenv("DS2API_ACCOUNT_MAX_INFLIGHT", "1") t.Setenv("DS2API_CONFIG_JSON", `{ "keys":["k1"], @@ -203,19 +203,15 @@ func TestPoolSupportsTokenOnlyAccount(t *testing.T) { pool := NewPool(config.LoadStore()) status := pool.Status() - if got, ok := status["total"].(int); !ok || got != 1 { + if got, ok := status["total"].(int); !ok || got != 0 { t.Fatalf("unexpected total in pool status: %#v", status["total"]) } - if got, ok := status["available"].(int); !ok || got != 1 { + if got, ok := status["available"].(int); !ok || got != 0 { t.Fatalf("unexpected available in pool status: %#v", status["available"]) } - acc, ok := pool.Acquire("", nil) - if !ok { - t.Fatalf("expected acquire success for token-only account") - } - if acc.Token != "token-only-account" { - t.Fatalf("unexpected token on acquired account: %q", acc.Token) + if _, ok := pool.Acquire("", nil); ok { + t.Fatalf("expected acquire to fail for token-only account") } } diff --git a/internal/admin/handler_accounts_identifier_test.go b/internal/admin/handler_accounts_identifier_test.go index b6f63ca..7cac96b 100644 --- a/internal/admin/handler_accounts_identifier_test.go +++ b/internal/admin/handler_accounts_identifier_test.go @@ -6,7 +6,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "strings" "testing" "github.com/go-chi/chi/v5" @@ -26,9 +25,9 @@ func newAdminTestHandler(t *testing.T, raw string) *Handler { } } -func TestListAccountsIncludesTokenOnlyIdentifier(t *testing.T) { +func TestListAccountsUsesEmailIdentifier(t *testing.T) { h := newAdminTestHandler(t, `{ - "accounts":[{"token":"token-only-account"}] + "accounts":[{"email":"u@example.com","password":"pwd"}] }`) req := httptest.NewRequest(http.MethodGet, "/admin/accounts?page=1&page_size=10", nil) @@ -49,38 +48,8 @@ func TestListAccountsIncludesTokenOnlyIdentifier(t *testing.T) { } first, _ := items[0].(map[string]any) identifier, _ := first["identifier"].(string) - if identifier == "" { - t.Fatalf("expected non-empty identifier: %#v", first) - } - if !strings.HasPrefix(identifier, "token:") { - t.Fatalf("expected token synthetic identifier, got %q", identifier) - } -} - -func TestDeleteAccountSupportsTokenOnlyIdentifier(t *testing.T) { - h := newAdminTestHandler(t, `{ - "accounts":[{"token":"token-only-account"}] - }`) - accounts := h.Store.Accounts() - if len(accounts) != 1 { - t.Fatalf("expected 1 account, got %d", len(accounts)) - } - id := accounts[0].Identifier() - if id == "" { - t.Fatal("expected token-only synthetic identifier") - } - - r := chi.NewRouter() - r.Delete("/admin/accounts/{identifier}", h.deleteAccount) - req := httptest.NewRequest(http.MethodDelete, "/admin/accounts/"+url.PathEscape(id), nil) - rec := httptest.NewRecorder() - r.ServeHTTP(rec, req) - - if rec.Code != http.StatusOK { - t.Fatalf("unexpected status: %d body=%s", rec.Code, rec.Body.String()) - } - if got := len(h.Store.Accounts()); got != 0 { - t.Fatalf("expected account removed, remaining=%d", got) + if identifier != "u@example.com" { + t.Fatalf("expected email identifier, got %q", identifier) } } @@ -142,11 +111,10 @@ func TestAddAccountRejectsCanonicalMobileDuplicate(t *testing.T) { } } -func TestFindAccountByIdentifierSupportsMobileAndTokenOnly(t *testing.T) { +func TestFindAccountByIdentifierSupportsMobile(t *testing.T) { h := newAdminTestHandler(t, `{ "accounts":[ - {"email":"u@example.com","mobile":"13800138000","password":"pwd"}, - {"token":"token-only-account"} + {"email":"u@example.com","mobile":"13800138000","password":"pwd"} ] }`) @@ -165,21 +133,4 @@ func TestFindAccountByIdentifierSupportsMobileAndTokenOnly(t *testing.T) { t.Fatalf("unexpected account by +86 mobile: %#v", accByMobileWithCountryCode) } - tokenOnlyID := "" - for _, acc := range h.Store.Accounts() { - if strings.TrimSpace(acc.Email) == "" && strings.TrimSpace(acc.Mobile) == "" { - tokenOnlyID = acc.Identifier() - break - } - } - if tokenOnlyID == "" { - t.Fatal("expected token-only account identifier") - } - accByTokenOnly, ok := findAccountByIdentifier(h.Store, tokenOnlyID) - if !ok { - t.Fatalf("expected find by token-only id=%q", tokenOnlyID) - } - if accByTokenOnly.Token != "token-only-account" { - t.Fatalf("unexpected token-only account: %#v", accByTokenOnly) - } } diff --git a/internal/admin/handler_accounts_testing.go b/internal/admin/handler_accounts_testing.go index e528de0..2a8a447 100644 --- a/internal/admin/handler_accounts_testing.go +++ b/internal/admin/handler_accounts_testing.go @@ -105,18 +105,14 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me } _ = h.Store.UpdateAccountTestStatus(identifier, status) }() - token := strings.TrimSpace(acc.Token) - if token == "" { - newToken, err := h.DS.Login(ctx, acc) - if err != nil { - result["message"] = "登录失败: " + err.Error() - return result - } - token = newToken - if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil { - result["message"] = "登录成功但写入配置失败: " + err.Error() - return result - } + token, err := h.DS.Login(ctx, acc) + if err != nil { + result["message"] = "登录失败: " + err.Error() + return result + } + if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil { + result["message"] = "登录成功但写入运行时 token 失败: " + err.Error() + return result } authCtx := &authn.RequestAuth{UseConfigToken: false, DeepSeekToken: token} sessionID, err := h.DS.CreateSession(ctx, authCtx, 1) @@ -129,7 +125,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me token = newToken authCtx.DeepSeekToken = token if err := h.Store.UpdateAccountToken(acc.Identifier(), token); err != nil { - result["message"] = "刷新 token 成功但写入配置失败: " + err.Error() + result["message"] = "刷新 token 成功但写入运行时 token 失败: " + err.Error() return result } sessionID, err = h.DS.CreateSession(ctx, authCtx, 1) @@ -147,7 +143,7 @@ func (h *Handler) testAccount(ctx context.Context, acc config.Account, model, me if strings.TrimSpace(message) == "" { result["success"] = true - result["message"] = "API 测试成功(仅会话创建)" + result["message"] = "Token 刷新成功(登录与会话创建成功)" result["response_time"] = int(time.Since(start).Milliseconds()) return result } @@ -246,20 +242,16 @@ func (h *Handler) deleteAllSessions(w http.ResponseWriter, r *http.Request) { return } - // 获取 token - token := strings.TrimSpace(acc.Token) - if token == "" { - newToken, err := h.DS.Login(r.Context(), acc) - if err != nil { - writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "登录失败: " + err.Error()}) - return - } - token = newToken - _ = h.Store.UpdateAccountToken(acc.Identifier(), token) + // 每次先登录刷新一次 token,避免使用过期 token。 + token, err := h.DS.Login(r.Context(), acc) + if err != nil { + writeJSON(w, http.StatusOK, map[string]any{"success": false, "message": "登录失败: " + err.Error()}) + return } + _ = h.Store.UpdateAccountToken(acc.Identifier(), token) // 删除所有会话 - err := h.DS.DeleteAllSessionsForToken(r.Context(), token) + err = h.DS.DeleteAllSessionsForToken(r.Context(), token) if err != nil { // token 可能过期,尝试重新登录并重试一次 newToken, loginErr := h.DS.Login(r.Context(), acc) diff --git a/internal/admin/handler_accounts_testing_test.go b/internal/admin/handler_accounts_testing_test.go index e80eefe..b07afaa 100644 --- a/internal/admin/handler_accounts_testing_test.go +++ b/internal/admin/handler_accounts_testing_test.go @@ -77,7 +77,7 @@ func TestTestAccount_BatchModeOnlyCreatesSession(t *testing.T) { t.Fatalf("expected success=true, got %#v", result) } msg, _ := result["message"].(string) - if !strings.Contains(msg, "仅会话创建") { + if !strings.Contains(msg, "Token 刷新成功") { t.Fatalf("expected session-only success message, got %q", msg) } if ds.loginCalls != 1 || ds.createSessionCalls != 1 { @@ -118,8 +118,8 @@ func TestDeleteAllSessions_RetryWithReloginOnDeleteFailure(t *testing.T) { if ok, _ := resp["success"].(bool); !ok { t.Fatalf("expected success response, got %#v", resp) } - if ds.loginCalls != 1 { - t.Fatalf("expected relogin once, got %d", ds.loginCalls) + if ds.loginCalls != 2 { + t.Fatalf("expected initial login plus relogin, got %d", ds.loginCalls) } if ds.deleteAllSessionsCalls != 2 { t.Fatalf("expected delete called twice, got %d", ds.deleteAllSessionsCalls) diff --git a/internal/admin/handler_config_import.go b/internal/admin/handler_config_import.go index 2b88d45..b9dd1f6 100644 --- a/internal/admin/handler_config_import.go +++ b/internal/admin/handler_config_import.go @@ -43,6 +43,7 @@ func (h *Handler) configImport(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]any{"detail": err.Error()}) return } + incoming.ClearAccountTokens() importedKeys, importedAccounts := 0, 0 err = h.Store.Update(func(c *config.Config) error { @@ -180,6 +181,7 @@ func (h *Handler) configImport(w http.ResponseWriter, r *http.Request) { func (h *Handler) computeSyncHash() string { snap := h.Store.Snapshot().Clone() + snap.ClearAccountTokens() snap.VercelSyncHash = "" snap.VercelSyncTime = 0 b, _ := json.Marshal(snap) diff --git a/internal/admin/handler_config_write.go b/internal/admin/handler_config_write.go index e09edfe..bfc6296 100644 --- a/internal/admin/handler_config_write.go +++ b/internal/admin/handler_config_write.go @@ -50,9 +50,6 @@ func (h *Handler) updateConfig(w http.ResponseWriter, r *http.Request) { if strings.TrimSpace(acc.Password) == "" { acc.Password = prev.Password } - if strings.TrimSpace(acc.Token) == "" { - acc.Token = prev.Token - } } seen[key] = struct{}{} accounts = append(accounts, acc) diff --git a/internal/admin/helpers.go b/internal/admin/helpers.go index af27676..6d5a292 100644 --- a/internal/admin/helpers.go +++ b/internal/admin/helpers.go @@ -65,7 +65,6 @@ func toAccount(m map[string]any) config.Account { Email: email, Mobile: mobile, Password: fieldString(m, "password"), - Token: fieldString(m, "token"), } } diff --git a/internal/admin/helpers_edge_test.go b/internal/admin/helpers_edge_test.go index 0b2a0ab..17bb3d7 100644 --- a/internal/admin/helpers_edge_test.go +++ b/internal/admin/helpers_edge_test.go @@ -188,8 +188,8 @@ func TestToAccountAllFields(t *testing.T) { if acc.Password != "secret" { t.Fatalf("unexpected password: %q", acc.Password) } - if acc.Token != "tok123" { - t.Fatalf("unexpected token: %q", acc.Token) + if acc.Token != "" { + t.Fatalf("expected token to be ignored, got %q", acc.Token) } } diff --git a/internal/admin/token_runtime_http_test.go b/internal/admin/token_runtime_http_test.go new file mode 100644 index 0000000..e23c1aa --- /dev/null +++ b/internal/admin/token_runtime_http_test.go @@ -0,0 +1,109 @@ +package admin + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/go-chi/chi/v5" + + "ds2api/internal/account" + "ds2api/internal/config" +) + +func newHTTPAdminHarness(t *testing.T, rawConfig string, ds DeepSeekCaller) http.Handler { + t.Helper() + t.Setenv("DS2API_CONFIG_JSON", rawConfig) + t.Setenv("CONFIG_JSON", "") + store := config.LoadStore() + h := &Handler{ + Store: store, + Pool: account.NewPool(store), + DS: ds, + } + r := chi.NewRouter() + RegisterRoutes(r, h) + return r +} + +func adminReq(method, path string, body []byte) *http.Request { + req := httptest.NewRequest(method, path, bytes.NewReader(body)) + req.Header.Set("Authorization", "Bearer admin") + req.Header.Set("Content-Type", "application/json") + return req +} + +func TestConfigImportIgnoresTokenFieldInPayload(t *testing.T) { + ds := &testingDSMock{} + router := newHTTPAdminHarness(t, `{"accounts":[]}`, ds) + + payload := []byte(`{ + "mode":"replace", + "config":{ + "accounts":[{"email":"u@example.com","password":"pwd","token":"expired-token"}] + } + }`) + rec := httptest.NewRecorder() + router.ServeHTTP(rec, adminReq(http.MethodPost, "/config/import", payload)) + if rec.Code != http.StatusOK { + t.Fatalf("import status=%d body=%s", rec.Code, rec.Body.String()) + } + + readRec := httptest.NewRecorder() + router.ServeHTTP(readRec, adminReq(http.MethodGet, "/config", nil)) + if readRec.Code != http.StatusOK { + t.Fatalf("get config status=%d body=%s", readRec.Code, readRec.Body.String()) + } + var data map[string]any + if err := json.Unmarshal(readRec.Body.Bytes(), &data); err != nil { + t.Fatalf("decode config response: %v", err) + } + accounts, _ := data["accounts"].([]any) + if len(accounts) != 1 { + t.Fatalf("expected one account, got %d", len(accounts)) + } + accountMap, _ := accounts[0].(map[string]any) + if hasToken, _ := accountMap["has_token"].(bool); hasToken { + t.Fatalf("expected imported token to be ignored, account=%#v", accountMap) + } +} + +func TestAccountTestRefreshesRuntimeTokenButExportOmitsToken(t *testing.T) { + ds := &testingDSMock{} + router := newHTTPAdminHarness(t, `{ + "accounts":[{"email":"batch@example.com","password":"pwd","token":"stale-token"}] + }`, ds) + + rec := httptest.NewRecorder() + router.ServeHTTP(rec, adminReq(http.MethodPost, "/accounts/test", []byte(`{"identifier":"batch@example.com"}`))) + if rec.Code != http.StatusOK { + t.Fatalf("test account status=%d body=%s", rec.Code, rec.Body.String()) + } + var testResp map[string]any + if err := json.Unmarshal(rec.Body.Bytes(), &testResp); err != nil { + t.Fatalf("decode test response: %v", err) + } + if ok, _ := testResp["success"].(bool); !ok { + t.Fatalf("expected test success, got %#v", testResp) + } + if ds.loginCalls < 1 { + t.Fatalf("expected login to be called at least once, got %d", ds.loginCalls) + } + + exportRec := httptest.NewRecorder() + router.ServeHTTP(exportRec, adminReq(http.MethodGet, "/config/export", nil)) + if exportRec.Code != http.StatusOK { + t.Fatalf("export status=%d body=%s", exportRec.Code, exportRec.Body.String()) + } + var exportResp map[string]any + if err := json.Unmarshal(exportRec.Body.Bytes(), &exportResp); err != nil { + t.Fatalf("decode export response: %v", err) + } + exportJSON, _ := exportResp["json"].(string) + if strings.Contains(exportJSON, `"token"`) { + t.Fatalf("expected export json to omit tokens, got %s", exportJSON) + } +} diff --git a/internal/auth/request_test.go b/internal/auth/request_test.go index 2f70e3f..f8cb40f 100644 --- a/internal/auth/request_test.go +++ b/internal/auth/request_test.go @@ -58,7 +58,7 @@ func TestDetermineWithXAPIKeyManagedKeyAcquiresAccount(t *testing.T) { if auth.AccountID != "acc@example.com" { t.Fatalf("unexpected account id: %q", auth.AccountID) } - if auth.DeepSeekToken != "account-token" { + if auth.DeepSeekToken != "fresh-token" { t.Fatalf("unexpected account token: %q", auth.DeepSeekToken) } if auth.CallerID == "" { diff --git a/internal/config/account.go b/internal/config/account.go index 3d6fa7d..bebb70e 100644 --- a/internal/config/account.go +++ b/internal/config/account.go @@ -1,10 +1,6 @@ package config -import ( - "crypto/sha256" - "encoding/hex" - "strings" -) +import "strings" func (a Account) Identifier() string { if strings.TrimSpace(a.Email) != "" { @@ -13,12 +9,5 @@ func (a Account) Identifier() string { if mobile := NormalizeMobileForStorage(a.Mobile); mobile != "" { return mobile } - // Backward compatibility: old configs may contain token-only accounts. - // Use a stable non-sensitive synthetic id so they can still join the pool. - token := strings.TrimSpace(a.Token) - if token == "" { - return "" - } - sum := sha256.Sum256([]byte(token)) - return "token:" + hex.EncodeToString(sum[:8]) + return "" } diff --git a/internal/config/config.go b/internal/config/config.go index 0d541ed..8c50f8e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -12,8 +12,8 @@ type Config struct { Toolcall ToolcallConfig `json:"toolcall,omitempty"` Responses ResponsesConfig `json:"responses,omitempty"` Embeddings EmbeddingsConfig `json:"embeddings,omitempty"` - AutoDelete AutoDeleteConfig `json:"auto_delete"` - VercelSyncHash string `json:"_vercel_sync_hash,omitempty"` + AutoDelete AutoDeleteConfig `json:"auto_delete"` + VercelSyncHash string `json:"_vercel_sync_hash,omitempty"` VercelSyncTime int64 `json:"_vercel_sync_time,omitempty"` AdditionalFields map[string]any `json:"-"` } @@ -26,6 +26,32 @@ type Account struct { TestStatus string `json:"test_status,omitempty"` } +func (c *Config) ClearAccountTokens() { + if c == nil { + return + } + for i := range c.Accounts { + c.Accounts[i].Token = "" + } +} + +// DropInvalidAccounts removes accounts that cannot be addressed by admin APIs +// (no email and no normalizable mobile). This prevents legacy token-only +// records from becoming orphaned empty entries after token stripping. +func (c *Config) DropInvalidAccounts() { + if c == nil || len(c.Accounts) == 0 { + return + } + kept := make([]Account, 0, len(c.Accounts)) + for _, acc := range c.Accounts { + if acc.Identifier() == "" { + continue + } + kept = append(kept, acc) + } + c.Accounts = kept +} + type CompatConfig struct { WideInputStrictOutput *bool `json:"wide_input_strict_output,omitempty"` } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a409fd7..b87b118 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2,25 +2,21 @@ package config import ( "encoding/base64" - "strings" "testing" ) -func TestAccountIdentifierFallsBackToTokenHash(t *testing.T) { +func TestAccountIdentifierRequiresEmailOrMobile(t *testing.T) { acc := Account{Token: "example-token-value"} id := acc.Identifier() - if !strings.HasPrefix(id, "token:") { - t.Fatalf("expected token-prefixed identifier, got %q", id) - } - if len(id) != len("token:")+16 { - t.Fatalf("unexpected identifier length: %d (%q)", len(id), id) + if id != "" { + t.Fatalf("expected empty identifier when only token is present, got %q", id) } } -func TestStoreFindAccountWithTokenOnlyIdentifier(t *testing.T) { +func TestLoadStoreClearsTokensFromConfigInput(t *testing.T) { t.Setenv("DS2API_CONFIG_JSON", `{ "keys":["k1"], - "accounts":[{"token":"token-only-account"}] + "accounts":[{"email":"u@example.com","password":"p","token":"token-only-account"}] }`) store := LoadStore() @@ -28,22 +24,35 @@ func TestStoreFindAccountWithTokenOnlyIdentifier(t *testing.T) { if len(accounts) != 1 { t.Fatalf("expected 1 account, got %d", len(accounts)) } - id := accounts[0].Identifier() - if id == "" { - t.Fatalf("expected synthetic identifier for token-only account") - } - found, ok := store.FindAccount(id) - if !ok { - t.Fatalf("expected FindAccount to locate token-only account by synthetic id") - } - if found.Token != "token-only-account" { - t.Fatalf("unexpected token value: %q", found.Token) + if accounts[0].Token != "" { + t.Fatalf("expected token to be cleared after loading, got %q", accounts[0].Token) } } -func TestStoreUpdateAccountTokenKeepsOldAndNewIdentifierResolvable(t *testing.T) { +func TestLoadStoreDropsLegacyTokenOnlyAccounts(t *testing.T) { t.Setenv("DS2API_CONFIG_JSON", `{ - "accounts":[{"token":"old-token"}] + "accounts":[ + {"token":"legacy-token-only"}, + {"email":"u@example.com","password":"p","token":"runtime-token"} + ] + }`) + + store := LoadStore() + accounts := store.Accounts() + if len(accounts) != 1 { + t.Fatalf("expected token-only account to be dropped, got %d accounts", len(accounts)) + } + if accounts[0].Identifier() != "u@example.com" { + t.Fatalf("unexpected remaining account: %#v", accounts[0]) + } + if accounts[0].Token != "" { + t.Fatalf("expected persisted token to be cleared, got %q", accounts[0].Token) + } +} + +func TestStoreUpdateAccountTokenKeepsIdentifierResolvable(t *testing.T) { + t.Setenv("DS2API_CONFIG_JSON", `{ + "accounts":[{"email":"user@example.com","password":"p"}] }`) store := LoadStore() @@ -52,23 +61,12 @@ func TestStoreUpdateAccountTokenKeepsOldAndNewIdentifierResolvable(t *testing.T) t.Fatalf("expected 1 account, got %d", len(before)) } oldID := before[0].Identifier() - if oldID == "" { - t.Fatal("expected old identifier") - } if err := store.UpdateAccountToken(oldID, "new-token"); err != nil { t.Fatalf("update token failed: %v", err) } - after := store.Accounts() - newID := after[0].Identifier() - if newID == "" || newID == oldID { - t.Fatalf("expected changed identifier, old=%q new=%q", oldID, newID) - } - if got, ok := store.FindAccount(newID); !ok || got.Token != "new-token" { - t.Fatalf("expected find by new identifier") - } if got, ok := store.FindAccount(oldID); !ok || got.Token != "new-token" { - t.Fatalf("expected find by old identifier alias") + t.Fatalf("expected find by stable account identifier") } } diff --git a/internal/config/store.go b/internal/config/store.go index 7a09cdc..10e26f4 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -39,6 +39,8 @@ func loadConfig() (Config, bool, error) { } if rawCfg != "" { cfg, err := parseConfigString(rawCfg) + cfg.ClearAccountTokens() + cfg.DropInvalidAccounts() return cfg, true, err } @@ -55,6 +57,8 @@ func loadConfig() (Config, bool, error) { if err := json.Unmarshal(content, &cfg); err != nil { return Config{}, false, err } + cfg.ClearAccountTokens() + cfg.DropInvalidAccounts() if IsVercel() { // Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors. return cfg, true, nil @@ -161,7 +165,9 @@ func (s *Store) Save() error { Logger.Info("[save_config] source from env, skip write") return nil } - b, err := json.MarshalIndent(s.cfg, "", " ") + persistCfg := s.cfg.Clone() + persistCfg.ClearAccountTokens() + b, err := json.MarshalIndent(persistCfg, "", " ") if err != nil { return err } @@ -173,7 +179,9 @@ func (s *Store) saveLocked() error { Logger.Info("[save_config] source from env, skip write") return nil } - b, err := json.MarshalIndent(s.cfg, "", " ") + persistCfg := s.cfg.Clone() + persistCfg.ClearAccountTokens() + b, err := json.MarshalIndent(persistCfg, "", " ") if err != nil { return err } @@ -197,7 +205,9 @@ func (s *Store) SetVercelSync(hash string, ts int64) error { func (s *Store) ExportJSONAndBase64() (string, string, error) { s.mu.RLock() defer s.mu.RUnlock() - b, err := json.Marshal(s.cfg) + exportCfg := s.cfg.Clone() + exportCfg.ClearAccountTokens() + b, err := json.Marshal(exportCfg) if err != nil { return "", "", err } diff --git a/webui/src/locales/en.json b/webui/src/locales/en.json index 35eea1f..2f0adf0 100644 --- a/webui/src/locales/en.json +++ b/webui/src/locales/en.json @@ -49,8 +49,8 @@ "delete": "Delete", "copy": "Copy", "generate": "Generate", - "test": "Test", - "testing": "Testing...", + "test": "Refresh token", + "testing": "Refreshing...", "loading": "Loading..." }, "messages": { @@ -93,8 +93,8 @@ "deleteKeyConfirm": "Are you sure you want to delete this API key?", "deleteAccountConfirm": "Are you sure you want to delete this account?", "invalidIdentifier": "Invalid account identifier. Operation aborted.", - "testAllConfirm": "Test API connectivity for all accounts?", - "testAllCompleted": "Completed: {success}/{total} available", + "testAllConfirm": "Refresh all account tokens and verify login?", + "testAllCompleted": "Completed: {success}/{total} refreshed", "testFailed": "Test failed: {error}", "available": "Available", "inUse": "In use", @@ -110,9 +110,9 @@ "noApiKeys": "No API keys found.", "accountsTitle": "DeepSeek Accounts", "accountsDesc": "Manage the DeepSeek account pool", - "testAll": "Test all", + "testAll": "Refresh all tokens", "addAccount": "Add account", - "testingAllAccounts": "Testing all accounts...", + "testingAllAccounts": "Refreshing tokens for all accounts...", "sessionActive": "Session active", "reauthRequired": "Re-auth required", "testStatusFailed": "Last test failed", @@ -150,7 +150,7 @@ "missingApiKey": "Please provide an API key.", "requestFailed": "Request failed.", "networkError": "Network error: {error}", - "testSuccess": "{account}: Test successful ({time}ms)", + "testSuccess": "{account}: Token refresh successful ({time}ms)", "config": "Configuration", "modelLabel": "Model", "streamMode": "Streaming", diff --git a/webui/src/locales/zh.json b/webui/src/locales/zh.json index 29ebc7f..3a3a6e8 100644 --- a/webui/src/locales/zh.json +++ b/webui/src/locales/zh.json @@ -49,8 +49,8 @@ "delete": "删除", "copy": "复制", "generate": "生成", - "test": "测试", - "testing": "正在测试...", + "test": "刷新 Token", + "testing": "正在刷新...", "loading": "加载中..." }, "messages": { @@ -93,8 +93,8 @@ "deleteKeyConfirm": "确定要删除此 API 密钥吗?", "deleteAccountConfirm": "确定要删除此账号吗?", "invalidIdentifier": "账号标识无效,无法执行操作", - "testAllConfirm": "测试所有账号的 API 连通性?", - "testAllCompleted": "完成:{success}/{total} 可用", + "testAllConfirm": "刷新所有账号 Token 并验证登录?", + "testAllCompleted": "完成:{success}/{total} 刷新成功", "testFailed": "测试失败: {error}", "available": "可用", "inUse": "正在使用", @@ -110,9 +110,9 @@ "noApiKeys": "未找到 API 密钥", "accountsTitle": "DeepSeek 账号", "accountsDesc": "管理 DeepSeek 账号池", - "testAll": "测试全部", + "testAll": "刷新全部 Token", "addAccount": "添加账号", - "testingAllAccounts": "正在测试所有账号...", + "testingAllAccounts": "正在刷新所有账号 Token...", "sessionActive": "已建立会话", "reauthRequired": "需重新登录", "testStatusFailed": "上次测试失败", @@ -150,7 +150,7 @@ "missingApiKey": "请提供 API 密钥", "requestFailed": "请求失败", "networkError": "网络错误: {error}", - "testSuccess": "{account}: 测试成功 ({time}ms)", + "testSuccess": "{account}: Token 刷新成功 ({time}ms)", "config": "配置", "modelLabel": "模型", "streamMode": "流式模式",