feat: track imported accounts per upstream key across platforms

Add upstream_key_account_links table mapping generated keys to remote
accounts per website/platform, surface imported_accounts on key
responses, and update website sync/routers to manage the links.
This commit is contained in:
liujing
2026-07-23 17:52:58 +08:00
parent ee89cd6119
commit df7e400e7b
17 changed files with 1081 additions and 537 deletions
+101 -2
View File
@@ -87,6 +87,11 @@ def test_import_upstream_key_creates_sub2api_account_management_apikey(monkeypat
def __exit__(self, exc_type, exc, tb):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
return [{"id": "101", "name": "Existing", "platform": "anthropic", "group_ids": [7]}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append((endpoint, body))
return {"id": 101, "name": body["name"]}
@@ -147,6 +152,10 @@ def test_import_upstream_key_idempotent_skips_already_imported(monkeypatch, db_s
return self
def __exit__(self, exc_type, exc, tb):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
return [{"id": "101", "name": "Existing", "platform": "anthropic", "group_ids": [7]}]
def create_account(self, body, endpoint="/accounts"):
nonlocal create_called
create_called = True
@@ -177,7 +186,7 @@ def test_import_upstream_key_idempotent_skips_already_imported(monkeypatch, db_s
assert len(response.items) == 1
assert response.items[0].status == "exists"
assert response.items[0].account_id == "101"
assert response.items[0].message == "导入过,已跳过"
assert response.items[0].message == "存在并已对齐目标分组"
# success 应为 true(没有 failed
assert response.success
@@ -214,12 +223,14 @@ def test_sync_imported_upstream_keys_efficient_and_correct(monkeypatch, db_sessi
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
nonlocal list_accounts_calls
list_accounts_calls += 1
# 远端只有 A1 (id: 101),没有 A2 (id: 102)
return [
{"id": "101", "name": "SmartUp-A1"}
{"id": "101", "name": "SmartUp-A1", "platform": "anthropic", "group_ids": [7]}
]
@staticmethod
def extract_id(data):
@@ -269,6 +280,8 @@ def test_sync_imported_upstream_keys_check_failed_on_exception(monkeypatch, db_s
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
raise Exception("List accounts failure")
@staticmethod
@@ -305,6 +318,10 @@ def test_import_rebuilds_when_remote_deleted(monkeypatch, db_session):
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def list_accounts(self):
return []
def account_exists(self, account_id):
return False
def create_account(self, body, endpoint="/accounts"):
@@ -345,6 +362,10 @@ def test_import_skips_on_check_failed(monkeypatch, db_session):
return self
def __exit__(self, *a):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "openai"}]
def list_accounts(self):
return None
def account_exists(self, account_id):
return None
def create_account(self, body, endpoint="/accounts"):
@@ -380,6 +401,8 @@ def test_import_upstream_key_with_custom_concurrency_and_priority(monkeypatch, d
return self
def __exit__(self, exc_type, exc, tb):
return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "openai"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append((endpoint, body))
return {"id": 202, "name": body["name"]}
@@ -440,6 +463,8 @@ def test_import_platform_from_snapshot(monkeypatch, db_session):
pass
def __enter__(self): return self
def __exit__(self, *args): return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append(body)
return {"id": 101, "name": body["name"]}
@@ -491,6 +516,8 @@ def test_import_platform_fallback_to_name(monkeypatch, db_session):
pass
def __enter__(self): return self
def __exit__(self, *args): return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "anthropic"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append(body)
return {"id": 101, "name": body["name"]}
@@ -542,6 +569,8 @@ def test_import_platform_manual_override(monkeypatch, db_session):
pass
def __enter__(self): return self
def __exit__(self, *args): return False
def get_groups(self, endpoint="/groups"):
return [{"id": "7", "name": "Target", "platform": "gemini"}]
def create_account(self, body, endpoint="/accounts"):
account_bodies.append(body)
return {"id": 101, "name": body["name"]}
@@ -564,3 +593,73 @@ def test_import_platform_manual_override(monkeypatch, db_session):
)
assert len(account_bodies) == 1
assert account_bodies[0]["platform"] == "gemini"
def test_same_key_can_be_imported_into_two_platform_groups(monkeypatch, db_session):
"""同一 Key 的不同平台映射独立创建,迁移其中一平台不影响另一平台。"""
website, generated = seed_account_import_rows(db_session)
class FakeClient:
accounts = {}
next_id = 100
def __init__(self, **kwargs):
pass
def __enter__(self):
return self
def __exit__(self, *args):
return False
def get_groups(self, endpoint="/groups"):
return [
{"id": "7", "name": "OpenAI", "platform": "openai"},
{"id": "8", "name": "Claude", "platform": "anthropic"},
{"id": "9", "name": "OpenAI 2", "platform": "openai"},
]
def list_accounts(self):
return list(self.accounts.values())
def create_account(self, body, endpoint="/accounts"):
type(self).next_id += 1
account = {"id": str(type(self).next_id), **body}
type(self).accounts[account["id"]] = account
return account
def update_account(self, account_id, body):
type(self).accounts[str(account_id)].update(body)
return type(self).accounts[str(account_id)]
@staticmethod
def extract_id(data):
return str(data.get("id"))
monkeypatch.setattr(websites_router, "Sub2ApiWebsiteClient", FakeClient)
monkeypatch.setattr(websites_router, "reconcile_upstream_keys_full", lambda db, uid: None)
def do_import(target_group_id):
return websites_router.import_upstream_keys_as_accounts(
website.id,
ImportAccountsRequest(
upstream_key_ids=[generated.id],
target_group_map={"vip": target_group_id},
auto_priority_by_rate=False,
),
db_session,
object(),
)
first = do_import("7")
second = do_import("8")
third = do_import("9")
assert first.items[0].status == "created"
assert second.items[0].status == "created"
assert third.items[0].status == "exists"
assert third.items[0].platform == "openai"
links = db_session.query(websites_router.UpstreamKeyAccountLink).filter_by(
upstream_key_id=generated.id,
website_id=website.id,
status="active",
).all()
assert {(link.platform, link.target_group) for link in links} == {
("openai", "9"),
("anthropic", "8"),
}
assert len(FakeClient.accounts) == 2