From a0c96ed5da52b6d050f80ff386e6e0d781488b6e Mon Sep 17 00:00:00 2001 From: SmartUp Developer Date: Thu, 2 Jul 2026 23:28:28 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20close=20platform-check=20silent-success?= =?UTF-8?q?=20loophole=20=E2=80=94=20raise=20WebsiteError=20on=20None=20li?= =?UTF-8?q?st,=20missing=20account,=20or=20stale=20platform=20after=20upda?= =?UTF-8?q?te?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/websites.py | 22 +++--- backend/test_organize_groups.py | 121 ++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+), 9 deletions(-) diff --git a/backend/app/routers/websites.py b/backend/app/routers/websites.py index 51c030f..0496f30 100644 --- a/backend/app/routers/websites.py +++ b/backend/app/routers/websites.py @@ -48,7 +48,7 @@ from app.schemas.website import ( SyncUpstreamModelsResponse, ) -from app.services.website_client import Sub2ApiWebsiteClient, _extract_id +from app.services.website_client import Sub2ApiWebsiteClient, WebsiteError, _extract_id from app.services.website_sync import ( binding_sources, sync_binding, @@ -1826,15 +1826,19 @@ def organize_website_groups( remote_acc["group_ids"] = new_group_ids if "platform" in update_payload: # 主动核对防线:防止子站忽略 platform 修改而发生静默失败 - fresh_accs = c.list_accounts() or [] + fresh_accs = c.list_accounts() + if fresh_accs is None: + raise WebsiteError("复查失败:拉取远端账号列表返回空,无法核对平台修改是否生效") fresh_acc = next((a for a in fresh_accs if str(c.extract_id(a)) == str(old_account_id)), None) - if fresh_acc: - remote_account_map[str(old_account_id)] = fresh_acc - new_platform = fresh_acc.get("platform") - if str(new_platform).lower() != str(platform).lower(): - raise WebsiteError( - f"远端账号不支持修改平台属性,尝试将平台从 {remote_platform} 修改为 {platform} 失败" - ) + if not fresh_acc: + raise WebsiteError(f"复查失败:在远端账号列表中未找到要更新的账号 {old_account_id}") + + remote_account_map[str(old_account_id)] = fresh_acc + new_platform = fresh_acc.get("platform") + if str(new_platform).lower() != str(platform).lower(): + raise WebsiteError( + f"远端账号不支持修改平台属性,尝试将平台从 {remote_platform} 修改为 {platform} 失败" + ) remote_acc["platform"] = platform msg_parts = [] diff --git a/backend/test_organize_groups.py b/backend/test_organize_groups.py index 127b6f7..ac11de4 100644 --- a/backend/test_organize_groups.py +++ b/backend/test_organize_groups.py @@ -582,3 +582,124 @@ def test_organize_groups_corrects_mismatched_platform_instead_of_recreating(db_s assert updated_accounts[0][0] == "ACC-G1" assert updated_accounts[0][1]["platform"] == "anthropic" assert "平台从 openai 修正为 anthropic" in response.items[0].message + + +def _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn): + """公用工厂:搭建平台错配场景并注入可定制的 list_accounts。""" + from app.models.snapshot import UpstreamRateSnapshot + + w = Website( + name="W-PlatChk", + site_type="sub2api", + base_url="http://wp", + enabled=True, + auth_config_json="{}", + timeout_seconds=30, + ) + u1 = Upstream(name="U-PlatChk", base_url="http://up") + db_session.add_all([w, u1]) + db_session.commit() + db_session.refresh(w) + db_session.refresh(u1) + + b1 = WebsiteGroupBinding( + website_id=w.id, + target_group_id="TG1", + target_group_name="TG1-Group", + source_groups_json=json.dumps([{"upstream_id": u1.id, "group_id": "G1"}]), + enabled=True, + ) + db_session.add(b1) + + k1 = UpstreamGeneratedKey( + upstream_id=u1.id, + group_id="G1", + group_name="G1-Group", + key_name="Key-G1", + key_value="sk-g1-secret", + status="imported", + imported_website_id=w.id, + imported_account_id="ACC-G1", + imported_target_group_id="TG1", + ) + db_session.add(k1) + + snapshot = UpstreamRateSnapshot( + upstream_id=u1.id, + snapshot_json=json.dumps({ + "groups": { + "G1": {"group_name": "G1-Group", "rate": 0.1, "platform": "anthropic"} + } + }), + ) + db_session.add(snapshot) + db_session.commit() + + call_count = {"n": 0} + + class MockClient: + def __init__(self, **kwargs): pass + def __enter__(self): return self + def __exit__(self, *a): pass + def get_groups(self, *a, **kw): + return [{"id": "TG1", "name": "TG1-Group"}] + def list_accounts(self): + call_count["n"] += 1 + return list_accounts_fn(call_count["n"]) + def extract_id(self, val): + return val.get("id") if isinstance(val, dict) else str(val) + def update_account(self, account_id, body): + return {"id": account_id} + + monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient) + monkeypatch.setattr( + "app.routers.websites.sync_account_priorities_for_website", lambda db, wid: [] + ) + return w + + +def test_organize_groups_platform_update_remote_silently_ignores(db_session, monkeypatch): + """复查时远端仍返回旧平台值 → 必须报错,不能静默认为成功。""" + + def list_accounts_fn(call_n): + # 第 1 次初始列表;第 2 次复查:仍是 openai(忽略了 platform 写入) + return [{"id": "ACC-G1", "name": "SmartUp-G1", "group_ids": ["TG1"], "platform": "openai"}] + + w = _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn) + response = organize_website_groups(wid=w.id, db=db_session) + + failed_items = [item for item in response.items if item.status == "failed"] + assert len(failed_items) == 1, f"期望 1 个 failed 项,实际:{response.items}" + assert "不支持修改平台属性" in failed_items[0].message + + +def test_organize_groups_platform_update_list_returns_none(db_session, monkeypatch): + """复查时 list_accounts() 返回 None → 必须报错,不能静默成功。""" + + def list_accounts_fn(call_n): + if call_n == 1: + return [{"id": "ACC-G1", "name": "SmartUp-G1", "group_ids": ["TG1"], "platform": "openai"}] + return None # 复查时列表拉取失败 + + w = _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn) + response = organize_website_groups(wid=w.id, db=db_session) + + failed_items = [item for item in response.items if item.status == "failed"] + assert len(failed_items) == 1, f"期望 1 个 failed 项,实际:{response.items}" + assert "复查失败" in failed_items[0].message + + +def test_organize_groups_platform_update_account_missing_from_list(db_session, monkeypatch): + """复查时账号从列表中消失 → 必须报错,不能静默成功。""" + + def list_accounts_fn(call_n): + if call_n == 1: + return [{"id": "ACC-G1", "name": "SmartUp-G1", "group_ids": ["TG1"], "platform": "openai"}] + return [] # 复查时账号消失 + + w = _seed_platform_mismatch(db_session, monkeypatch, list_accounts_fn) + response = organize_website_groups(wid=w.id, db=db_session) + + failed_items = [item for item in response.items if item.status == "failed"] + assert len(failed_items) == 1, f"期望 1 个 failed 项,实际:{response.items}" + assert "复查失败" in failed_items[0].message