204848 commited on
Commit
c7ba3dd
·
1 Parent(s): 0c46526

feat: 增强配置文件恢复机制,支持损坏文件的备份和恢复

Browse files
.gitignore CHANGED
@@ -73,3 +73,4 @@ deepseek2api旧版/
73
  chat.deepseek.com.har.txt
74
  chat.deepseek.com2.har.txt
75
  chat.deepseek.com/
 
 
73
  chat.deepseek.com.har.txt
74
  chat.deepseek.com2.har.txt
75
  chat.deepseek.com/
76
+ chat.deepseek.com3.har
internal/config/config_test.go CHANGED
@@ -4,6 +4,7 @@ import (
4
  "encoding/base64"
5
  "errors"
6
  "os"
 
7
  "strings"
8
  "testing"
9
  )
@@ -462,3 +463,110 @@ func TestAccountTestStatusIsRuntimeOnlyAndNotPersisted(t *testing.T) {
462
  t.Fatalf("expected test_status to stay out of persisted config, got: %s", content)
463
  }
464
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  "encoding/base64"
5
  "errors"
6
  "os"
7
+ "path/filepath"
8
  "strings"
9
  "testing"
10
  )
 
463
  t.Fatalf("expected test_status to stay out of persisted config, got: %s", content)
464
  }
465
  }
466
+
467
+ func TestLoadConfigRecoversFromCorruptFileWithBackup(t *testing.T) {
468
+ dir := t.TempDir()
469
+ path := dir + "/config.json"
470
+ bakPath := path + ".bak"
471
+ good := `{"keys":["backup-key"],"accounts":[{"email":"u@example.com","password":"p"}]}`
472
+ if err := os.WriteFile(bakPath, []byte(good), 0o644); err != nil {
473
+ t.Fatalf("write bak: %v", err)
474
+ }
475
+ if err := os.WriteFile(path, []byte(`{broken-json`), 0o644); err != nil {
476
+ t.Fatalf("write corrupt: %v", err)
477
+ }
478
+
479
+ cfg, err := loadConfigFromFile(path)
480
+ if err != nil {
481
+ t.Fatalf("expected recovery to avoid error, got: %v", err)
482
+ }
483
+ if len(cfg.Keys) != 1 || cfg.Keys[0] != "backup-key" {
484
+ t.Fatalf("expected recovery from backup key, got keys=%#v", cfg.Keys)
485
+ }
486
+ content, rerr := os.ReadFile(path)
487
+ if rerr != nil {
488
+ t.Fatalf("read recovered path: %v", rerr)
489
+ }
490
+ if !strings.Contains(string(content), "backup-key") {
491
+ t.Fatalf("expected path restored from backup, got: %s", content)
492
+ }
493
+ matches, _ := filepath.Glob(dir + "/config.json.corrupt.*")
494
+ if len(matches) == 0 {
495
+ t.Fatalf("expected corrupt snapshot to be preserved")
496
+ }
497
+ }
498
+
499
+ func TestLoadConfigResetsCorruptFileWithoutBackup(t *testing.T) {
500
+ dir := t.TempDir()
501
+ path := dir + "/config.json"
502
+ if err := os.WriteFile(path, []byte(`{broken`), 0o644); err != nil {
503
+ t.Fatalf("write corrupt: %v", err)
504
+ }
505
+
506
+ cfg, err := loadConfigFromFile(path)
507
+ if err != nil {
508
+ t.Fatalf("expected reset to avoid error, got: %v", err)
509
+ }
510
+ if len(cfg.Keys) != 0 || len(cfg.Accounts) != 0 {
511
+ t.Fatalf("expected empty config after reset, got keys=%d accounts=%d", len(cfg.Keys), len(cfg.Accounts))
512
+ }
513
+ content, rerr := os.ReadFile(path)
514
+ if rerr != nil {
515
+ t.Fatalf("read reset path: %v", rerr)
516
+ }
517
+ if strings.TrimSpace(string(content)) != "{}" {
518
+ t.Fatalf("expected path reset to {}, got: %s", content)
519
+ }
520
+ }
521
+
522
+ func TestLoadConfigResetsCorruptFileWithCorruptBackup(t *testing.T) {
523
+ dir := t.TempDir()
524
+ path := dir + "/config.json"
525
+ bakPath := path + ".bak"
526
+ if err := os.WriteFile(bakPath, []byte(`{also-broken`), 0o644); err != nil {
527
+ t.Fatalf("write corrupt bak: %v", err)
528
+ }
529
+ if err := os.WriteFile(path, []byte(`{broken`), 0o644); err != nil {
530
+ t.Fatalf("write corrupt: %v", err)
531
+ }
532
+
533
+ cfg, err := loadConfigFromFile(path)
534
+ if err != nil {
535
+ t.Fatalf("expected reset to avoid error, got: %v", err)
536
+ }
537
+ if len(cfg.Keys) != 0 {
538
+ t.Fatalf("expected empty config after reset, got keys=%d", len(cfg.Keys))
539
+ }
540
+ content, rerr := os.ReadFile(path)
541
+ if rerr != nil {
542
+ t.Fatalf("read reset path: %v", rerr)
543
+ }
544
+ if strings.TrimSpace(string(content)) != "{}" {
545
+ t.Fatalf("expected path reset to {}, got: %s", content)
546
+ }
547
+ }
548
+
549
+ func TestLoadConfigRefreshesBackupOnSuccessfulLoad(t *testing.T) {
550
+ dir := t.TempDir()
551
+ path := dir + "/config.json"
552
+ bakPath := path + ".bak"
553
+ good := `{"keys":["live-key"],"accounts":[]}`
554
+ if err := os.WriteFile(path, []byte(good), 0o644); err != nil {
555
+ t.Fatalf("write config: %v", err)
556
+ }
557
+
558
+ cfg, err := loadConfigFromFile(path)
559
+ if err != nil {
560
+ t.Fatalf("unexpected error: %v", err)
561
+ }
562
+ if len(cfg.Keys) != 1 || cfg.Keys[0] != "live-key" {
563
+ t.Fatalf("unexpected keys: %#v", cfg.Keys)
564
+ }
565
+ bak, rerr := os.ReadFile(bakPath)
566
+ if rerr != nil {
567
+ t.Fatalf("expected backup to be created: %v", rerr)
568
+ }
569
+ if !strings.Contains(string(bak), "live-key") {
570
+ t.Fatalf("expected backup to mirror live config, got: %s", bak)
571
+ }
572
+ }
internal/config/store.go CHANGED
@@ -130,7 +130,22 @@ func loadConfigFromFile(path string) (Config, error) {
130
  }
131
  var cfg Config
132
  if err := json.Unmarshal(content, &cfg); err != nil {
133
- return Config{}, err
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  }
135
  cfg.NormalizeCredentials()
136
  cfg.DropInvalidAccounts()
@@ -139,6 +154,13 @@ func loadConfigFromFile(path string) (Config, error) {
139
  _ = overwriteFile(path, b, 0o644)
140
  }
141
  }
 
 
 
 
 
 
 
142
  return cfg, nil
143
  }
144
 
 
130
  }
131
  var cfg Config
132
  if err := json.Unmarshal(content, &cfg); err != nil {
133
+ // 配置文件损坏:保留损坏文件、尝试从 .bak 恢复、恢复失败则重置为 {}
134
+ // 不返回错误,避免单点配置损坏导致整个服务启动失败(例如
135
+ // HuggingFace Space 重启瞬间 config.json 被截断成半截 JSON)。
136
+ Logger.Error("[config] config file is corrupt, attempting recovery", "path", path, "error", err)
137
+ if !recoverCorruptConfig(path, content) {
138
+ return Config{}, nil
139
+ }
140
+ content, err = os.ReadFile(path)
141
+ if err != nil {
142
+ Logger.Error("[config] re-read after recovery failed, starting with empty config", "path", path, "error", err)
143
+ return Config{}, nil
144
+ }
145
+ if err := json.Unmarshal(content, &cfg); err != nil {
146
+ Logger.Error("[config] recovered config still invalid, starting with empty config", "path", path, "error", err)
147
+ return Config{}, nil
148
+ }
149
  }
150
  cfg.NormalizeCredentials()
151
  cfg.DropInvalidAccounts()
 
154
  _ = overwriteFile(path, b, 0o644)
155
  }
156
  }
157
+ // 加载成功后刷新可用备份(仅当当前文件合法时才覆盖备份,避免用损坏
158
+ // 文件覆盖已知好备份)。Vercel 文件系统为临时/只读,跳过。
159
+ if !IsVercel() {
160
+ if backupErr := backupConfigFile(path, content); backupErr != nil {
161
+ Logger.Warn("[config] failed to refresh config backup", "path", path, "error", backupErr)
162
+ }
163
+ }
164
  return cfg, nil
165
  }
166
 
internal/config/store_env_writeback.go CHANGED
@@ -6,6 +6,7 @@ import (
6
  "os"
7
  "path/filepath"
8
  "strings"
 
9
  )
10
 
11
  func envWritebackEnabled() bool {
@@ -96,3 +97,59 @@ func writeConfigBytes(path string, b []byte) error {
96
  }
97
  return nil
98
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  "os"
7
  "path/filepath"
8
  "strings"
9
+ "time"
10
  )
11
 
12
  func envWritebackEnabled() bool {
 
97
  }
98
  return nil
99
  }
100
+
101
+ // backupConfigFile 把当前合法的配置内容快照到 <path>.bak。仅在主配置解析
102
+ // 成功后调用,确保 .bak 永远是已知合法的副本,供 recoverCorruptConfig 使用。
103
+ func backupConfigFile(path string, content []byte) error {
104
+ if len(content) == 0 {
105
+ return nil
106
+ }
107
+ return overwriteFile(path+".bak", content, 0o644)
108
+ }
109
+
110
+ // resetConfigToEmpty 把 path 重置为 {},用于损坏文件无可恢复备份时的兜底。
111
+ func resetConfigToEmpty(path string) {
112
+ if werr := writeConfigBytes(path, []byte("{}")); werr != nil {
113
+ Logger.Warn("[config] failed to reset config to empty", "path", path, "error", werr)
114
+ }
115
+ }
116
+
117
+ // recoverCorruptConfig 处理主配置文件解析失败的情况:
118
+ // 1. 将损坏文件另存为 <path>.corrupt.<unix> 便于排查
119
+ // 2. 尝试用 <path>.bak 恢复;备份不存在或也损坏则把 path 重置为 {}
120
+ //
121
+ // 返回 true 表示 path 现在指向一个合法的(可能是空的)配置文件。这样避免
122
+ // 单点配置损坏导致整个服务启动失败(例如 HuggingFace Space 重启瞬间
123
+ // config.json 被截断成半截 JSON)。
124
+ func recoverCorruptConfig(path string, corruptContent []byte) bool {
125
+ corruptPath := fmt.Sprintf("%s.corrupt.%d", path, time.Now().Unix())
126
+ if werr := overwriteFile(corruptPath, corruptContent, 0o644); werr != nil {
127
+ Logger.Warn("[config] failed to preserve corrupt config for inspection", "path", corruptPath, "error", werr)
128
+ }
129
+
130
+ bakPath := path + ".bak"
131
+ bakContent, rerr := os.ReadFile(bakPath)
132
+ if rerr != nil {
133
+ Logger.Warn("[config] no backup available, resetting config to empty", "path", path, "bak_path", bakPath, "error", rerr)
134
+ resetConfigToEmpty(path)
135
+ return true
136
+ }
137
+ if len(strings.TrimSpace(string(bakContent))) == 0 {
138
+ Logger.Warn("[config] backup is empty, resetting config to empty", "path", path, "bak_path", bakPath)
139
+ resetConfigToEmpty(path)
140
+ return true
141
+ }
142
+ var probe Config
143
+ if perr := json.Unmarshal(bakContent, &probe); perr != nil {
144
+ Logger.Warn("[config] backup is also corrupt, resetting config to empty", "path", path, "bak_path", bakPath, "error", perr)
145
+ resetConfigToEmpty(path)
146
+ return true
147
+ }
148
+ if werr := writeConfigBytes(path, bakContent); werr != nil {
149
+ Logger.Error("[config] failed to restore config from backup, resetting to empty", "path", path, "bak_path", bakPath, "error", werr)
150
+ resetConfigToEmpty(path)
151
+ return true
152
+ }
153
+ Logger.Info("[config] recovered config from backup", "path", path, "bak_path", bakPath)
154
+ return true
155
+ }