fix: restore api_key from local key_value when syncing upstream models for apikey accounts

This commit is contained in:
SmartUp Developer
2026-07-02 23:38:57 +08:00
parent a0c96ed5da
commit 9997b9d56a
2 changed files with 105 additions and 14 deletions
+20
View File
@@ -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:
+85 -14
View File
@@ -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()