mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-21 08:27:42 +08:00
fix(sse): trim stream output from CONTENT_FILTER onward
This commit is contained in:
@@ -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) {
|
func TestRuntimeTokenRefreshIntervalHoursDefaultsToSix(t *testing.T) {
|
||||||
t.Setenv("DS2API_CONFIG_JSON", `{
|
t.Setenv("DS2API_CONFIG_JSON", `{
|
||||||
"keys":["k1"],
|
"keys":["k1"],
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ func loadConfig() (Config, bool, error) {
|
|||||||
if rawCfg != "" {
|
if rawCfg != "" {
|
||||||
cfg, err := parseConfigString(rawCfg)
|
cfg, err := parseConfigString(rawCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if !IsVercel() && envWritebackEnabled() {
|
||||||
|
if fileCfg, fileErr := loadConfigFromFile(ConfigPath()); fileErr == nil {
|
||||||
|
return fileCfg, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
return cfg, true, err
|
return cfg, true, err
|
||||||
}
|
}
|
||||||
cfg.ClearAccountTokens()
|
cfg.ClearAccountTokens()
|
||||||
@@ -66,7 +71,7 @@ func loadConfig() (Config, bool, error) {
|
|||||||
return cfg, true, err
|
return cfg, true, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content, err := os.ReadFile(ConfigPath())
|
cfg, err := loadConfigFromFile(ConfigPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if IsVercel() {
|
if IsVercel() {
|
||||||
// Vercel one-click deploy may start without a writable/present config file.
|
// Vercel one-click deploy may start without a writable/present config file.
|
||||||
@@ -75,16 +80,6 @@ func loadConfig() (Config, bool, error) {
|
|||||||
}
|
}
|
||||||
return Config{}, false, err
|
return Config{}, false, err
|
||||||
}
|
}
|
||||||
var cfg Config
|
|
||||||
if err := json.Unmarshal(content, &cfg); err != nil {
|
|
||||||
return Config{}, false, err
|
|
||||||
}
|
|
||||||
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
|
||||||
@@ -92,6 +87,24 @@ func loadConfig() (Config, bool, error) {
|
|||||||
return cfg, false, 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{}, err
|
||||||
|
}
|
||||||
|
cfg.DropInvalidAccounts()
|
||||||
|
if strings.Contains(string(content), `"test_status"`) && !IsVercel() {
|
||||||
|
if b, err := json.MarshalIndent(cfg, "", " "); err == nil {
|
||||||
|
_ = os.WriteFile(path, b, 0o644)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) Snapshot() Config {
|
func (s *Store) Snapshot() Config {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ func stripLeakedContentFilterSuffix(text string) string {
|
|||||||
if text == "" {
|
if text == "" {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
idx := strings.Index(strings.ToUpper(text), "CONTENT_FILTER")
|
upperText := strings.ToUpper(text)
|
||||||
|
idx := strings.Index(upperText, "CONTENT_FILTER")
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,3 +55,13 @@ func TestParseDeepSeekContentLineDropsPureLeakedContentFilterChunk(t *testing.T)
|
|||||||
t.Fatalf("expected empty parts, got %#v", res.Parts)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user