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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user