From c20c9cfd26f8c9116d47c0b76212bc22cf71c2dd Mon Sep 17 00:00:00 2001 From: SmartUp Developer Date: Thu, 2 Jul 2026 19:18:10 +0800 Subject: [PATCH] fix: add stream close check, recover regression test assertions, and secure credentials type check --- backend/app/routers/websites.py | 6 ++- backend/test_sync_upstream_models.py | 75 ++++++++++++++++++++++++++-- frontend/src/views/Websites.vue | 7 +++ 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/backend/app/routers/websites.py b/backend/app/routers/websites.py index 2d316b9..4ea08c5 100644 --- a/backend/app/routers/websites.py +++ b/backend/app/routers/websites.py @@ -1397,9 +1397,11 @@ def _sync_upstream_models_generator(wid: int, db: Session): continue # 1. 先安全修复 base_url (不依赖同步模型结果) - # 在 remote_acc.get("credentials") 基础上过滤掉敏感字段,保留其他非敏感 credentials (例如 compact_model_mapping) + raw_creds = remote_acc.get("credentials") + if not isinstance(raw_creds, dict): + raw_creds = {} current_creds = { - k: v for k, v in (remote_acc.get("credentials") or {}).items() + k: v for k, v in raw_creds.items() if k not in SENSITIVE_CREDENTIAL_KEYS } current_creds["base_url"] = upstream_base_url diff --git a/backend/test_sync_upstream_models.py b/backend/test_sync_upstream_models.py index c1ddccb..40206c9 100644 --- a/backend/test_sync_upstream_models.py +++ b/backend/test_sync_upstream_models.py @@ -70,6 +70,8 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch): db_session.add(key2) db_session.commit() + updates_recorded = [] + class FakeClient: def __init__(self, *args, **kwargs): pass @@ -79,13 +81,23 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch): pass def list_accounts(self): return [ - {"id": "1", "name": "Account 1", "credentials": {"base_url": "old"}}, + { + "id": "1", + "name": "Account 1", + "credentials": { + "base_url": "old", + "access_token": "secret_access_token", # Sensitive credential + "api_key": "some_api_key", # Sensitive credential + "openai_capabilities": "some_capabilities", # Non-sensitive field + "compact_model_mapping": "some_mapping", # Non-sensitive field + } + }, {"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}} ] def extract_id(self, val): return val["id"] def update_account(self, aid, data): - pass + updates_recorded.append((aid, data)) def sync_account_upstream_models(self, aid): if aid == "1": return ["gpt-4", "gpt-3.5"] @@ -118,6 +130,29 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch): assert items_map["2"]["status"] == "failed" assert items_map["2"]["model_count"] == 0 + + # Verify update payload regression safety + # Account 1 should be updated twice (first to fix base_url, second to set model_mapping) + acc1_updates = [up for up in updates_recorded if up[0] == "1"] + assert len(acc1_updates) == 2 + + # 1. First update to fix base_url + payload1 = acc1_updates[0][1]["credentials"] + assert payload1["base_url"] == "http://up1.api" + assert "access_token" not in payload1 + assert "api_key" not in payload1 + assert payload1["openai_capabilities"] == "some_capabilities" + assert payload1["compact_model_mapping"] == "some_mapping" + + # 2. Second update to write back model_mapping + payload2 = acc1_updates[1][1]["credentials"] + assert payload2["base_url"] == "http://up1.api" + assert "access_token" not in payload2 + assert "api_key" not in payload2 + assert payload2["openai_capabilities"] == "some_capabilities" + assert payload2["compact_model_mapping"] == "some_mapping" + assert payload2["model_mapping"] == {"gpt-3.5": "gpt-3.5", "gpt-4": "gpt-4"} + finally: app.dependency_overrides.clear() @@ -163,6 +198,8 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch): db_session.add(key2) db_session.commit() + updates_recorded = [] + class FakeClient: def __init__(self, *args, **kwargs): pass @@ -172,13 +209,23 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch): pass def list_accounts(self): return [ - {"id": "1", "name": "Account 1", "credentials": {"base_url": "old"}}, + { + "id": "1", + "name": "Account 1", + "credentials": { + "base_url": "old", + "access_token": "secret_access_token", # Sensitive credential + "api_key": "some_api_key", # Sensitive credential + "openai_capabilities": "some_capabilities", # Non-sensitive field + "compact_model_mapping": "some_mapping", # Non-sensitive field + } + }, {"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}} ] def extract_id(self, val): return val["id"] def update_account(self, aid, data): - pass + updates_recorded.append((aid, data)) def sync_account_upstream_models(self, aid): if aid == "1": return ["gpt-4", "gpt-3.5"] @@ -223,6 +270,26 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch): assert parsed_events[3]["event"] == "complete" assert parsed_events[3]["data"]["success"] is False assert len(parsed_events[3]["data"]["items"]) == 2 + + # Verify update payload regression safety for stream + acc1_updates = [up for up in updates_recorded if up[0] == "1"] + assert len(acc1_updates) == 2 + + # First update to fix base_url + payload1 = acc1_updates[0][1]["credentials"] + assert payload1["base_url"] == "http://up1.api" + assert "access_token" not in payload1 + assert "api_key" not in payload1 + assert payload1["openai_capabilities"] == "some_capabilities" + + # Second update to write back model_mapping + payload2 = acc1_updates[1][1]["credentials"] + assert payload2["base_url"] == "http://up1.api" + assert "access_token" not in payload2 + assert "api_key" not in payload2 + assert payload2["openai_capabilities"] == "some_capabilities" + assert payload2["model_mapping"] == {"gpt-3.5": "gpt-3.5", "gpt-4": "gpt-4"} + finally: app.dependency_overrides.clear() diff --git a/frontend/src/views/Websites.vue b/frontend/src/views/Websites.vue index a189840..0918ae0 100644 --- a/frontend/src/views/Websites.vue +++ b/frontend/src/views/Websites.vue @@ -1986,6 +1986,7 @@ async function triggerSyncUpstreamModels() { syncModelsDialog.value = true let hasProcessedStart = false + let gotComplete = false try { const authStore = useAuthStore() @@ -2040,6 +2041,7 @@ async function triggerSyncUpstreamModels() { syncModelsResults.value.push(data) } else if (event === 'complete') { syncModelsMessage.value = data.message + gotComplete = true if (data.success) { ElMessage.success('同步完成') } else { @@ -2061,6 +2063,7 @@ async function triggerSyncUpstreamModels() { syncModelsResults.value.push(data) } else if (event === 'complete') { syncModelsMessage.value = data.message + gotComplete = true if (data.success) { ElMessage.success('同步完成') } else { @@ -2070,6 +2073,10 @@ async function triggerSyncUpstreamModels() { throw new Error(data.message || '流式同步出错') } } + + if (!gotComplete) { + throw new Error('同步流异常中断,未获取到完整执行报告') + } } catch (e: any) { ElMessage.error(e.message || '同步上游模型失败') if (!hasProcessedStart || syncModelsResults.value.length === 0) {