From 9997b9d56a6f8ca02aa6959fafe212bfd3908957 Mon Sep 17 00:00:00 2001 From: SmartUp Developer Date: Thu, 2 Jul 2026 23:38:57 +0800 Subject: [PATCH] fix: restore api_key from local key_value when syncing upstream models for apikey accounts --- backend/app/routers/websites.py | 20 ++++++ backend/test_sync_upstream_models.py | 99 ++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 14 deletions(-) diff --git a/backend/app/routers/websites.py b/backend/app/routers/websites.py index 0496f30..c184b74 100644 --- a/backend/app/routers/websites.py +++ b/backend/app/routers/websites.py @@ -1433,6 +1433,26 @@ def _sync_upstream_models_generator(wid: int, db: Session): } current_creds["base_url"] = upstream_base_url + # 对 SmartUp 导入的 apikey 类型账号,必须从本地 key_value 补回 api_key; + # 若 key_value 为空则禁止继续写回,避免写坏账号。 + db_key = cand.get("db_key") + acc_type = remote_acc.get("type", "") + if acc_type == "apikey": + local_key_value = (db_key.key_value or "").strip() if db_key else "" + if not local_key_value: + item = SyncUpstreamModelsItem( + account_id=aid, + account_name=acc_name, + model_count=0, + models=[], + status="failed", + message="本地 key_value 为空,跳过处理避免写坏账号 api_key" + ) + items.append(item) + yield "item", item.model_dump() + continue + current_creds["api_key"] = local_key_value + try: c.update_account(aid, {"credentials": current_creds}) except Exception as e: diff --git a/backend/test_sync_upstream_models.py b/backend/test_sync_upstream_models.py index c410b5b..b7d242e 100644 --- a/backend/test_sync_upstream_models.py +++ b/backend/test_sync_upstream_models.py @@ -84,15 +84,16 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch): { "id": "1", "name": "Account 1", + "type": "apikey", "credentials": { "base_url": "old", "access_token": "secret_access_token", # Sensitive credential - "api_key": "some_api_key", # Sensitive credential + "api_key": "old_api_key", # Will be overwritten by local key_value "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", "type": "apikey", "credentials": {"base_url": "old"}} ] def extract_id(self, val): return val["id"] @@ -136,19 +137,19 @@ def test_sync_upstream_models_original_json_endpoint(db_session, monkeypatch): acc1_updates = [up for up in updates_recorded if up[0] == "1"] assert len(acc1_updates) == 2 - # 1. First update to fix base_url + # 1. First update to fix base_url (api_key restored from local key_value="val1") 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 "access_token" not in payload1 # Sensitive: still filtered + assert payload1["api_key"] == "val1" # Restored from db key_value assert payload1["openai_capabilities"] == "some_capabilities" assert payload1["compact_model_mapping"] == "some_mapping" - # 2. Second update to write back model_mapping + # 2. Second update to write back model_mapping (api_key still present) 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["api_key"] == "val1" # Restored from db key_value 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"} @@ -212,15 +213,16 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch): { "id": "1", "name": "Account 1", + "type": "apikey", "credentials": { "base_url": "old", "access_token": "secret_access_token", # Sensitive credential - "api_key": "some_api_key", # Sensitive credential + "api_key": "old_api_key", # Will be overwritten by local key_value "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", "type": "apikey", "credentials": {"base_url": "old"}} ] def extract_id(self, val): return val["id"] @@ -275,19 +277,19 @@ def test_sync_upstream_models_streaming_endpoint(db_session, monkeypatch): acc1_updates = [up for up in updates_recorded if up[0] == "1"] assert len(acc1_updates) == 2 - # First update to fix base_url + # First update to fix base_url (api_key restored from local key_value="val1") 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 "access_token" not in payload1 # Sensitive: still filtered + assert payload1["api_key"] == "val1" # Restored from db key_value assert payload1["openai_capabilities"] == "some_capabilities" assert payload1["compact_model_mapping"] == "some_mapping" - # Second update to write back model_mapping + # Second update to write back model_mapping (api_key still present) 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["api_key"] == "val1" # Restored from db key_value 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"} @@ -337,3 +339,72 @@ def test_sync_upstream_models_error_handling(db_session, monkeypatch): finally: app.dependency_overrides.clear() + + +def test_sync_upstream_models_empty_key_value_skips_account(db_session, monkeypatch): + """本地 key_value 为空时,不允许写回 credentials,跳过该账号并报 failed。""" + w = Website( + name="wangwang888", + site_type="sub2api", + base_url="https://wangwang.top", + enabled=True, + auth_config_json="{}", + timeout_seconds=30 + ) + db_session.add(w) + up = Upstream(id=1, name="U1", base_url="http://up1.api", enabled=True) + db_session.add(up) + db_session.commit() + + # key_value 故意留空 + key1 = UpstreamGeneratedKey( + id=1, + upstream_id=up.id, + key_name="k1", + key_value="", # 空 + group_id="g1", + group_name="grp1", + status="active", + imported_website_id=w.id, + imported_account_id="1" + ) + db_session.add(key1) + db_session.commit() + + update_calls = [] + + class FakeClient: + def __init__(self, *args, **kwargs): pass + def __enter__(self): return self + def __exit__(self, *args): pass + def list_accounts(self): + return [{"id": "1", "name": "Acc1", "type": "apikey", "credentials": {"base_url": "old", "api_key": "x"}}] + def extract_id(self, val): return val["id"] + def update_account(self, aid, data): + update_calls.append((aid, data)) + def sync_account_upstream_models(self, aid): + return ["gpt-4"] + + monkeypatch.setattr("app.routers.websites._client", lambda website: FakeClient()) + + from fastapi.testclient import TestClient + from app.main import app + from app.database import get_db + from app.utils.auth import get_current_user + + app.dependency_overrides[get_db] = lambda: db_session + app.dependency_overrides[get_current_user] = lambda: None + + try: + client = TestClient(app) + resp = client.post(f"/api/websites/{w.id}/accounts/sync-upstream-models") + assert resp.status_code == 200 + data = resp.json() + assert data["success"] is False + assert len(data["items"]) == 1 + assert data["items"][0]["status"] == "failed" + assert "key_value" in data["items"][0]["message"] or "api_key" in data["items"][0]["message"] + # 最关键:没有调用 update_account(因为在写回之前已提前退出) + assert len(update_calls) == 0, f"不应调用 update_account,实际调用:{update_calls}" + finally: + app.dependency_overrides.clear()