fix: add stream close check, recover regression test assertions, and secure credentials type check

This commit is contained in:
SmartUp Developer
2026-07-02 19:18:10 +08:00
parent ece11d66e6
commit c20c9cfd26
3 changed files with 82 additions and 6 deletions
+4 -2
View File
@@ -1397,9 +1397,11 @@ def _sync_upstream_models_generator(wid: int, db: Session):
continue continue
# 1. 先安全修复 base_url (不依赖同步模型结果) # 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 = { 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 if k not in SENSITIVE_CREDENTIAL_KEYS
} }
current_creds["base_url"] = upstream_base_url current_creds["base_url"] = upstream_base_url
+71 -4
View File
@@ -70,6 +70,8 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch):
db_session.add(key2) db_session.add(key2)
db_session.commit() db_session.commit()
updates_recorded = []
class FakeClient: class FakeClient:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pass pass
@@ -79,13 +81,23 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch):
pass pass
def list_accounts(self): def list_accounts(self):
return [ 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"}} {"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}}
] ]
def extract_id(self, val): def extract_id(self, val):
return val["id"] return val["id"]
def update_account(self, aid, data): def update_account(self, aid, data):
pass updates_recorded.append((aid, data))
def sync_account_upstream_models(self, aid): def sync_account_upstream_models(self, aid):
if aid == "1": if aid == "1":
return ["gpt-4", "gpt-3.5"] 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"]["status"] == "failed"
assert items_map["2"]["model_count"] == 0 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: finally:
app.dependency_overrides.clear() app.dependency_overrides.clear()
@@ -163,6 +198,8 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch):
db_session.add(key2) db_session.add(key2)
db_session.commit() db_session.commit()
updates_recorded = []
class FakeClient: class FakeClient:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
pass pass
@@ -172,13 +209,23 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch):
pass pass
def list_accounts(self): def list_accounts(self):
return [ 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"}} {"id": "2", "name": "Account 2", "credentials": {"base_url": "old"}}
] ]
def extract_id(self, val): def extract_id(self, val):
return val["id"] return val["id"]
def update_account(self, aid, data): def update_account(self, aid, data):
pass updates_recorded.append((aid, data))
def sync_account_upstream_models(self, aid): def sync_account_upstream_models(self, aid):
if aid == "1": if aid == "1":
return ["gpt-4", "gpt-3.5"] 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]["event"] == "complete"
assert parsed_events[3]["data"]["success"] is False assert parsed_events[3]["data"]["success"] is False
assert len(parsed_events[3]["data"]["items"]) == 2 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: finally:
app.dependency_overrides.clear() app.dependency_overrides.clear()
+7
View File
@@ -1986,6 +1986,7 @@ async function triggerSyncUpstreamModels() {
syncModelsDialog.value = true syncModelsDialog.value = true
let hasProcessedStart = false let hasProcessedStart = false
let gotComplete = false
try { try {
const authStore = useAuthStore() const authStore = useAuthStore()
@@ -2040,6 +2041,7 @@ async function triggerSyncUpstreamModels() {
syncModelsResults.value.push(data) syncModelsResults.value.push(data)
} else if (event === 'complete') { } else if (event === 'complete') {
syncModelsMessage.value = data.message syncModelsMessage.value = data.message
gotComplete = true
if (data.success) { if (data.success) {
ElMessage.success('同步完成') ElMessage.success('同步完成')
} else { } else {
@@ -2061,6 +2063,7 @@ async function triggerSyncUpstreamModels() {
syncModelsResults.value.push(data) syncModelsResults.value.push(data)
} else if (event === 'complete') { } else if (event === 'complete') {
syncModelsMessage.value = data.message syncModelsMessage.value = data.message
gotComplete = true
if (data.success) { if (data.success) {
ElMessage.success('同步完成') ElMessage.success('同步完成')
} else { } else {
@@ -2070,6 +2073,10 @@ async function triggerSyncUpstreamModels() {
throw new Error(data.message || '流式同步出错') throw new Error(data.message || '流式同步出错')
} }
} }
if (!gotComplete) {
throw new Error('同步流异常中断,未获取到完整执行报告')
}
} catch (e: any) { } catch (e: any) {
ElMessage.error(e.message || '同步上游模型失败') ElMessage.error(e.message || '同步上游模型失败')
if (!hasProcessedStart || syncModelsResults.value.length === 0) { if (!hasProcessedStart || syncModelsResults.value.length === 0) {