mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-18 23:25:10 +08:00
Merge pull request #137 from CJackHwang/codex/optimize-configuration-file-management
Make account `test_status` runtime-only (in-memory cache)
This commit is contained in:
@@ -17,6 +17,7 @@ type ConfigStore interface {
|
|||||||
FindAccount(identifier string) (config.Account, bool)
|
FindAccount(identifier string) (config.Account, bool)
|
||||||
UpdateAccountToken(identifier, token string) error
|
UpdateAccountToken(identifier, token string) error
|
||||||
UpdateAccountTestStatus(identifier, status string) error
|
UpdateAccountTestStatus(identifier, status string) error
|
||||||
|
AccountTestStatus(identifier string) (string, bool)
|
||||||
Update(mutator func(*config.Config) error) error
|
Update(mutator func(*config.Config) error) error
|
||||||
ExportJSONAndBase64() (string, string, error)
|
ExportJSONAndBase64() (string, string, error)
|
||||||
IsEnvBacked() bool
|
IsEnvBacked() bool
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
items := make([]map[string]any, 0, end-start)
|
items := make([]map[string]any, 0, end-start)
|
||||||
for _, acc := range accounts[start:end] {
|
for _, acc := range accounts[start:end] {
|
||||||
|
testStatus, _ := h.Store.AccountTestStatus(acc.Identifier())
|
||||||
token := strings.TrimSpace(acc.Token)
|
token := strings.TrimSpace(acc.Token)
|
||||||
preview := ""
|
preview := ""
|
||||||
if token != "" {
|
if token != "" {
|
||||||
@@ -70,7 +71,7 @@ func (h *Handler) listAccounts(w http.ResponseWriter, r *http.Request) {
|
|||||||
"has_password": acc.Password != "",
|
"has_password": acc.Password != "",
|
||||||
"has_token": token != "",
|
"has_token": token != "",
|
||||||
"token_preview": preview,
|
"token_preview": preview,
|
||||||
"test_status": acc.TestStatus,
|
"test_status": testStatus,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
|
writeJSON(w, http.StatusOK, map[string]any{"items": items, "total": total, "page": page, "page_size": pageSize, "total_pages": totalPages})
|
||||||
|
|||||||
@@ -93,8 +93,9 @@ func TestTestAccount_BatchModeOnlyCreatesSession(t *testing.T) {
|
|||||||
if updated.Token != "new-token" {
|
if updated.Token != "new-token" {
|
||||||
t.Fatalf("expected refreshed token to be persisted, got %q", updated.Token)
|
t.Fatalf("expected refreshed token to be persisted, got %q", updated.Token)
|
||||||
}
|
}
|
||||||
if updated.TestStatus != "ok" {
|
testStatus, ok := store.AccountTestStatus("batch@example.com")
|
||||||
t.Fatalf("expected test status ok, got %q", updated.TestStatus)
|
if !ok || testStatus != "ok" {
|
||||||
|
t.Fatalf("expected runtime test status ok, got %q (ok=%v)", testStatus, ok)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ type Config struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
Mobile string `json:"mobile,omitempty"`
|
Mobile string `json:"mobile,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
Token string `json:"token,omitempty"`
|
Token string `json:"token,omitempty"`
|
||||||
TestStatus string `json:"test_status,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) ClearAccountTokens() {
|
func (c *Config) ClearAccountTokens() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package config
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -147,3 +148,39 @@ func TestLoadConfigOnVercelWithoutConfigFileFallsBackToMemory(t *testing.T) {
|
|||||||
t.Fatalf("expected empty bootstrap config, got keys=%d accounts=%d", len(cfg.Keys), len(cfg.Accounts))
|
t.Fatalf("expected empty bootstrap config, got keys=%d accounts=%d", len(cfg.Keys), len(cfg.Accounts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccountTestStatusIsRuntimeOnlyAndNotPersisted(t *testing.T) {
|
||||||
|
tmp, err := os.CreateTemp(t.TempDir(), "config-*.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create temp config: %v", err)
|
||||||
|
}
|
||||||
|
defer tmp.Close()
|
||||||
|
if _, err := tmp.WriteString(`{
|
||||||
|
"accounts":[{"email":"u@example.com","password":"p","test_status":"ok"}]
|
||||||
|
}`); err != nil {
|
||||||
|
t.Fatalf("write temp config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Setenv("DS2API_CONFIG_JSON", "")
|
||||||
|
t.Setenv("CONFIG_JSON", "")
|
||||||
|
t.Setenv("DS2API_CONFIG_PATH", tmp.Name())
|
||||||
|
|
||||||
|
store := LoadStore()
|
||||||
|
if got, ok := store.AccountTestStatus("u@example.com"); ok || got != "" {
|
||||||
|
t.Fatalf("expected no runtime status loaded from config, got %q", got)
|
||||||
|
}
|
||||||
|
if err := store.UpdateAccountTestStatus("u@example.com", "ok"); err != nil {
|
||||||
|
t.Fatalf("update test status: %v", err)
|
||||||
|
}
|
||||||
|
if got, ok := store.AccountTestStatus("u@example.com"); !ok || got != "ok" {
|
||||||
|
t.Fatalf("expected runtime status to be available, got %q (ok=%v)", got, ok)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(tmp.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read config: %v", err)
|
||||||
|
}
|
||||||
|
if strings.Contains(string(content), "test_status") {
|
||||||
|
t.Fatalf("expected test_status to stay out of persisted config, got: %s", content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Store struct {
|
|||||||
fromEnv bool
|
fromEnv bool
|
||||||
keyMap map[string]struct{} // O(1) API key lookup index
|
keyMap map[string]struct{} // O(1) API key lookup index
|
||||||
accMap map[string]int // O(1) account lookup: identifier -> slice index
|
accMap map[string]int // O(1) account lookup: identifier -> slice index
|
||||||
|
accTest map[string]string // runtime-only account test status cache
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadStore() *Store {
|
func LoadStore() *Store {
|
||||||
@@ -58,6 +59,11 @@ func loadConfig() (Config, bool, error) {
|
|||||||
return Config{}, false, err
|
return Config{}, false, err
|
||||||
}
|
}
|
||||||
cfg.DropInvalidAccounts()
|
cfg.DropInvalidAccounts()
|
||||||
|
if strings.Contains(string(content), `"test_status"`) && !IsVercel() {
|
||||||
|
if b, err := json.MarshalIndent(cfg, "", " "); err == nil {
|
||||||
|
_ = os.WriteFile(ConfigPath(), b, 0o644)
|
||||||
|
}
|
||||||
|
}
|
||||||
if IsVercel() {
|
if IsVercel() {
|
||||||
// Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors.
|
// Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors.
|
||||||
return cfg, true, nil
|
return cfg, true, nil
|
||||||
@@ -108,8 +114,19 @@ func (s *Store) UpdateAccountTestStatus(identifier, status string) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return errors.New("account not found")
|
return errors.New("account not found")
|
||||||
}
|
}
|
||||||
s.cfg.Accounts[idx].TestStatus = status
|
s.setAccountTestStatusLocked(s.cfg.Accounts[idx], status, identifier)
|
||||||
return s.saveLocked()
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) AccountTestStatus(identifier string) (string, bool) {
|
||||||
|
identifier = strings.TrimSpace(identifier)
|
||||||
|
if identifier == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
s.mu.RLock()
|
||||||
|
defer s.mu.RUnlock()
|
||||||
|
status, ok := s.accTest[identifier]
|
||||||
|
return status, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) UpdateAccountToken(identifier, token string) error {
|
func (s *Store) UpdateAccountToken(identifier, token string) error {
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ package config
|
|||||||
|
|
||||||
// rebuildIndexes must be called with the lock already held (or during init).
|
// rebuildIndexes must be called with the lock already held (or during init).
|
||||||
func (s *Store) rebuildIndexes() {
|
func (s *Store) rebuildIndexes() {
|
||||||
|
prevStatus := s.accTest
|
||||||
s.keyMap = make(map[string]struct{}, len(s.cfg.Keys))
|
s.keyMap = make(map[string]struct{}, len(s.cfg.Keys))
|
||||||
for _, k := range s.cfg.Keys {
|
for _, k := range s.cfg.Keys {
|
||||||
s.keyMap[k] = struct{}{}
|
s.keyMap[k] = struct{}{}
|
||||||
}
|
}
|
||||||
s.accMap = make(map[string]int, len(s.cfg.Accounts))
|
s.accMap = make(map[string]int, len(s.cfg.Accounts))
|
||||||
|
s.accTest = make(map[string]string, len(s.cfg.Accounts))
|
||||||
for i, acc := range s.cfg.Accounts {
|
for i, acc := range s.cfg.Accounts {
|
||||||
id := acc.Identifier()
|
id := acc.Identifier()
|
||||||
if id != "" {
|
if id != "" {
|
||||||
s.accMap[id] = i
|
s.accMap[id] = i
|
||||||
|
if status, ok := prevStatus[id]; ok {
|
||||||
|
s.setAccountTestStatusLocked(acc, status, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,3 +34,22 @@ func (s *Store) findAccountIndexLocked(identifier string) (int, bool) {
|
|||||||
}
|
}
|
||||||
return -1, false
|
return -1, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) setAccountTestStatusLocked(acc Account, status, hintedIdentifier string) {
|
||||||
|
status = lower(status)
|
||||||
|
if status == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if id := acc.Identifier(); id != "" {
|
||||||
|
s.accTest[id] = status
|
||||||
|
}
|
||||||
|
if email := acc.Email; email != "" {
|
||||||
|
s.accTest[email] = status
|
||||||
|
}
|
||||||
|
if mobile := CanonicalMobileKey(acc.Mobile); mobile != "" {
|
||||||
|
s.accTest[mobile] = status
|
||||||
|
}
|
||||||
|
if hintedIdentifier = lower(hintedIdentifier); hintedIdentifier != "" {
|
||||||
|
s.accTest[hintedIdentifier] = status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user