From 3b60e3c8f982864ac873974edbb697db683ffc77 Mon Sep 17 00:00:00 2001 From: "CJACK." Date: Tue, 31 Mar 2026 01:26:43 +0800 Subject: [PATCH] fix(sse): trim stream output from CONTENT_FILTER onward --- internal/config/config_test.go | 31 +++++++++++++++++++++++++++++ internal/config/store.go | 29 +++++++++++++++++++-------- internal/sse/content_filter_leak.go | 3 ++- internal/sse/line_test.go | 10 ++++++++++ 4 files changed, 64 insertions(+), 9 deletions(-) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 30a2e80..a489093 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -154,6 +154,37 @@ func TestEnvBackedStoreWritebackDoesNotBootstrapOnInvalidEnvJSON(t *testing.T) { } } +func TestEnvBackedStoreWritebackFallsBackToPersistedFileOnInvalidEnvJSON(t *testing.T) { + tmp, err := os.CreateTemp(t.TempDir(), "config-*.json") + if err != nil { + t.Fatalf("create temp config: %v", err) + } + path := tmp.Name() + if _, err := tmp.WriteString(`{"keys":["file-key"],"accounts":[{"email":"persisted@example.com","password":"p"}]}`); err != nil { + t.Fatalf("write temp config: %v", err) + } + _ = tmp.Close() + + t.Setenv("DS2API_CONFIG_JSON", "{invalid-json") + t.Setenv("CONFIG_JSON", "") + t.Setenv("DS2API_CONFIG_PATH", path) + t.Setenv("DS2API_ENV_WRITEBACK", "1") + + cfg, fromEnv, loadErr := loadConfig() + if loadErr != nil { + t.Fatalf("expected fallback to persisted file, got error: %v", loadErr) + } + if fromEnv { + t.Fatalf("expected fallback to file-backed mode") + } + if len(cfg.Keys) != 1 || cfg.Keys[0] != "file-key" { + t.Fatalf("unexpected keys after fallback: %#v", cfg.Keys) + } + if len(cfg.Accounts) != 1 || cfg.Accounts[0].Email != "persisted@example.com" { + t.Fatalf("unexpected accounts after fallback: %#v", cfg.Accounts) + } +} + func TestRuntimeTokenRefreshIntervalHoursDefaultsToSix(t *testing.T) { t.Setenv("DS2API_CONFIG_JSON", `{ "keys":["k1"], diff --git a/internal/config/store.go b/internal/config/store.go index 9375bcf..310ea91 100644 --- a/internal/config/store.go +++ b/internal/config/store.go @@ -41,6 +41,11 @@ func loadConfig() (Config, bool, error) { if rawCfg != "" { cfg, err := parseConfigString(rawCfg) if err != nil { + if !IsVercel() && envWritebackEnabled() { + if fileCfg, fileErr := loadConfigFromFile(ConfigPath()); fileErr == nil { + return fileCfg, false, nil + } + } return cfg, true, err } cfg.ClearAccountTokens() @@ -66,7 +71,7 @@ func loadConfig() (Config, bool, error) { return cfg, true, err } - content, err := os.ReadFile(ConfigPath()) + cfg, err := loadConfigFromFile(ConfigPath()) if err != nil { if IsVercel() { // Vercel one-click deploy may start without a writable/present config file. @@ -75,21 +80,29 @@ func loadConfig() (Config, bool, error) { } return Config{}, false, err } + if IsVercel() { + // Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors. + return cfg, true, nil + } + return cfg, false, nil +} + +func loadConfigFromFile(path string) (Config, error) { + content, err := os.ReadFile(path) + if err != nil { + return Config{}, err + } var cfg Config if err := json.Unmarshal(content, &cfg); err != nil { - return Config{}, false, err + return Config{}, err } cfg.DropInvalidAccounts() if strings.Contains(string(content), `"test_status"`) && !IsVercel() { if b, err := json.MarshalIndent(cfg, "", " "); err == nil { - _ = os.WriteFile(ConfigPath(), b, 0o644) + _ = os.WriteFile(path, b, 0o644) } } - if IsVercel() { - // Vercel filesystem is ephemeral/read-only for runtime writes; avoid save errors. - return cfg, true, nil - } - return cfg, false, nil + return cfg, nil } func (s *Store) Snapshot() Config { diff --git a/internal/sse/content_filter_leak.go b/internal/sse/content_filter_leak.go index 87dff7b..f73e40c 100644 --- a/internal/sse/content_filter_leak.go +++ b/internal/sse/content_filter_leak.go @@ -22,7 +22,8 @@ func stripLeakedContentFilterSuffix(text string) string { if text == "" { return text } - idx := strings.Index(strings.ToUpper(text), "CONTENT_FILTER") + upperText := strings.ToUpper(text) + idx := strings.Index(upperText, "CONTENT_FILTER") if idx < 0 { return text } diff --git a/internal/sse/line_test.go b/internal/sse/line_test.go index 8e27f73..4e1d22a 100644 --- a/internal/sse/line_test.go +++ b/internal/sse/line_test.go @@ -55,3 +55,13 @@ func TestParseDeepSeekContentLineDropsPureLeakedContentFilterChunk(t *testing.T) t.Fatalf("expected empty parts, got %#v", res.Parts) } } + +func TestParseDeepSeekContentLineTrimsFromContentFilterKeyword(t *testing.T) { + res := ParseDeepSeekContentLine([]byte(`data: {"p":"response/content","v":"模型会在命中 CONTENT_FILTER 时返回拒绝原因。"}`), false, "text") + if !res.Parsed || res.Stop { + t.Fatalf("expected parsed non-stop result: %#v", res) + } + if len(res.Parts) != 1 || res.Parts[0].Text != "模型会在命中" { + t.Fatalf("unexpected parts after filter: %#v", res.Parts) + } +}