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:
+24
-111
@@ -13,8 +13,6 @@ from app.models.snapshot import UpstreamRateSnapshot
|
||||
from app.models.website import Website, WebsiteGroupBinding
|
||||
from app.routers.websites import (
|
||||
_normalize_platform,
|
||||
_detect_platform,
|
||||
_resolve_platform,
|
||||
organize_website_groups,
|
||||
import_upstream_keys_as_accounts,
|
||||
)
|
||||
@@ -49,88 +47,8 @@ def test_normalize_platform_unknown_preserved():
|
||||
|
||||
# ── 单元测试:_detect_platform ───────────────────────
|
||||
|
||||
def test_detect_platform_grok_in_name():
|
||||
assert _detect_platform("My Grok Key") == "grok"
|
||||
|
||||
|
||||
def test_detect_platform_grok_lowercase():
|
||||
assert _detect_platform("grok-group") == "grok"
|
||||
|
||||
|
||||
def test_detect_platform_xai_in_name():
|
||||
assert _detect_platform("Chat xAI Key") == "grok"
|
||||
|
||||
|
||||
def test_detect_platform_gemini_still_works():
|
||||
assert _detect_platform("Gemini-Pro") == "gemini"
|
||||
|
||||
|
||||
def test_detect_platform_claude_still_works():
|
||||
assert _detect_platform("Claude-v2") == "anthropic"
|
||||
|
||||
|
||||
def test_detect_platform_anthropic_still_works():
|
||||
assert _detect_platform("anthropic-key") == "anthropic"
|
||||
|
||||
|
||||
def test_detect_platform_antigravity_still_works():
|
||||
assert _detect_platform("antigravity-key") == "antigravity"
|
||||
|
||||
|
||||
def test_detect_platform_no_match_fallback():
|
||||
assert _detect_platform("abc") == "openai"
|
||||
assert _detect_platform("abc", "anthropic") == "anthropic"
|
||||
|
||||
|
||||
def test_detect_platform_fallback_xai_normalized():
|
||||
"""fallback='xai' 且未匹配任何关键词 → 规范化返回 grok。"""
|
||||
assert _detect_platform("abc", "xai") == "grok"
|
||||
|
||||
|
||||
# ── 单元测试:_resolve_platform ───────────────────────
|
||||
|
||||
def test_resolve_platform_snapshot_grok():
|
||||
"""快照 platform=grok → 返回 grok。"""
|
||||
result = _resolve_platform("some key name", {"platform": "grok"}, "openai")
|
||||
assert result == "grok"
|
||||
|
||||
|
||||
def test_resolve_platform_snapshot_grok_mixed_case():
|
||||
"""快照 platform=Grok → 统一小写返回 grok。"""
|
||||
result = _resolve_platform("some key name", {"platform": "Grok"}, "openai")
|
||||
assert result == "grok"
|
||||
|
||||
|
||||
def test_resolve_platform_snapshot_xai_normalized():
|
||||
"""快照 platform=xai → 规范化为 grok。"""
|
||||
result = _resolve_platform("some key name", {"platform": "xai"}, "openai")
|
||||
assert result == "grok"
|
||||
|
||||
|
||||
def test_resolve_platform_name_grok_without_snapshot():
|
||||
"""无快照平台时,名称中包含 grok → 返回 grok。"""
|
||||
result = _resolve_platform("My-Grok-Key", {}, "openai")
|
||||
assert result == "grok"
|
||||
|
||||
|
||||
def test_resolve_platform_name_xai_without_snapshot():
|
||||
"""无快照平台时,名称中包含 xai → 返回 grok。"""
|
||||
result = _resolve_platform("Chat-xAI", {}, "openai")
|
||||
assert result == "grok"
|
||||
|
||||
|
||||
def test_resolve_platform_snapshot_overrides_name():
|
||||
"""快照有平台时,应优先于名称识别。"""
|
||||
result = _resolve_platform("claude-key", {"platform": "grok"}, "openai")
|
||||
assert result == "grok", "快照平台应覆盖名称识别"
|
||||
|
||||
|
||||
def test_resolve_platform_gemini_snapshot_still_works():
|
||||
"""快照 platform=gemini → 返回 gemini(不受 grok 影响)。"""
|
||||
result = _resolve_platform("xxx", {"platform": "gemini"}, "openai")
|
||||
assert result == "gemini"
|
||||
|
||||
|
||||
# ── 集成测试夹具 ──────────────────────────────────────
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -168,6 +86,7 @@ def test_organize_groups_creates_account_with_grok_platform(db_session, monkeypa
|
||||
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,
|
||||
platform="grok",
|
||||
)
|
||||
db_session.add(b1)
|
||||
|
||||
@@ -193,7 +112,7 @@ def test_organize_groups_creates_account_with_grok_platform(db_session, monkeypa
|
||||
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 get_groups(self, *a, **kw): return [{"id": "TG1", "name": "TG1-Group", "platform": "grok"}]
|
||||
def list_accounts(self): return []
|
||||
def extract_id(self, val): return val.get("id") if isinstance(val, dict) else str(val)
|
||||
def create_account(self, body):
|
||||
@@ -225,6 +144,7 @@ def test_organize_groups_detects_grok_from_group_name(db_session, monkeypatch):
|
||||
website_id=w.id, target_group_id="TG1", target_group_name="TG1-Group",
|
||||
source_groups_json=json.dumps([{"upstream_id": u1.id, "group_id": "Grok01"}]),
|
||||
enabled=True,
|
||||
platform="grok",
|
||||
)
|
||||
db_session.add(b1)
|
||||
|
||||
@@ -241,7 +161,7 @@ def test_organize_groups_detects_grok_from_group_name(db_session, monkeypatch):
|
||||
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 get_groups(self, *a, **kw): return [{"id": "TG1", "name": "TG1-Group", "platform": "grok"}]
|
||||
def list_accounts(self): return []
|
||||
def extract_id(self, val): return val.get("id") if isinstance(val, dict) else str(val)
|
||||
def create_account(self, body):
|
||||
@@ -257,8 +177,8 @@ def test_organize_groups_detects_grok_from_group_name(db_session, monkeypatch):
|
||||
assert created_bodies[0]["platform"] == "grok"
|
||||
|
||||
|
||||
def test_organize_groups_corrects_platform_to_grok(db_session, monkeypatch):
|
||||
"""已导入账号平台为 openai,快照为 grok → 修正平台为 grok。"""
|
||||
def test_organize_groups_grok_platform_creates_independent_account(db_session, monkeypatch):
|
||||
"""binding platform=grok 但有旧 openai 账号时 → 创建独立 grok 账号,不修改旧账号。"""
|
||||
w = Website(
|
||||
name="W1", site_type="sub2api", base_url="http://w1",
|
||||
enabled=True, auth_config_json="{}", timeout_seconds=30,
|
||||
@@ -273,58 +193,51 @@ def test_organize_groups_corrects_platform_to_grok(db_session, monkeypatch):
|
||||
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,
|
||||
platform="grok",
|
||||
)
|
||||
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", status="imported",
|
||||
imported_website_id=w.id, imported_account_id="ACC-G1",
|
||||
imported_target_group_id="TG1",
|
||||
key_name="Key-G1", key_value="sk-g1", status="created",
|
||||
)
|
||||
db_session.add(k1)
|
||||
|
||||
snapshot = UpstreamRateSnapshot(
|
||||
upstream_id=u1.id,
|
||||
snapshot_json=json.dumps({
|
||||
"groups": {"G1": {"group_name": "G1-Group", "rate": 0.1, "platform": "grok"}}
|
||||
}),
|
||||
)
|
||||
db_session.add(snapshot)
|
||||
db_session.commit()
|
||||
|
||||
created_bodies = []
|
||||
updated_accounts = []
|
||||
mock_platform = "openai"
|
||||
|
||||
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 get_groups(self, *a, **kw): return [{"id": "TG1", "name": "TG1-Group", "platform": "grok"}]
|
||||
def list_accounts(self):
|
||||
return [{"id": "ACC-G1", "name": "SmartUp-G1", "group_ids": ["TG1"], "platform": mock_platform}]
|
||||
return [{"id": "ACC-OLD", "name": "Old-OpenAI", "group_ids": ["TG1"], "platform": "openai"}]
|
||||
def extract_id(self, val): return val.get("id") if isinstance(val, dict) else str(val)
|
||||
def create_account(self, body):
|
||||
created_bodies.append(body)
|
||||
acc_id = f"NEW-{body['platform']}-{len(created_bodies)}"
|
||||
return {"id": acc_id, "name": body["name"], "group_ids": body["group_ids"], "platform": body["platform"]}
|
||||
def update_account(self, account_id, body):
|
||||
nonlocal mock_platform
|
||||
updated_accounts.append((account_id, body))
|
||||
if "platform" in body:
|
||||
mock_platform = body["platform"]
|
||||
return {"id": account_id, "platform": body.get("platform")}
|
||||
return {"id": account_id}
|
||||
|
||||
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", MockClient)
|
||||
monkeypatch.setattr("app.routers.websites.sync_account_priorities_for_website", lambda db, wid: [])
|
||||
|
||||
response = organize_website_groups(wid=w.id, db=db_session)
|
||||
assert response.success is True
|
||||
assert len(updated_accounts) == 1
|
||||
assert updated_accounts[0][1]["platform"] == "grok"
|
||||
assert "平台从 openai 修正为 grok" in response.items[0].message
|
||||
assert len(updated_accounts) == 0
|
||||
assert len(created_bodies) == 1
|
||||
assert created_bodies[0]["platform"] == "grok"
|
||||
assert response.items[0].status == "created"
|
||||
|
||||
|
||||
# ── 手动导入:Grok default_platform ───────────────────
|
||||
|
||||
def test_import_upstream_key_with_grok_platform(monkeypatch, db_session):
|
||||
"""手动导入时 default_platform=grok → 创建账号的 platform = grok。"""
|
||||
def test_import_upstream_key_uses_target_group_platform(monkeypatch, db_session):
|
||||
"""手动导入只使用目标网站分组平台,忽略请求中的平台字段。"""
|
||||
website = Website(
|
||||
name="My Sub2API", site_type="sub2api", base_url="http://sub2api.local",
|
||||
api_prefix="/api/v1", auth_type="api_key",
|
||||
@@ -358,7 +271,7 @@ def test_import_upstream_key_with_grok_platform(monkeypatch, db_session):
|
||||
def account_exists(self, account_id): return True
|
||||
@staticmethod
|
||||
def extract_id(data): return str(data.get("id"))
|
||||
def get_groups(self, **kw): return []
|
||||
def get_groups(self, **kw): return [{"id": "7", "name": "Grok", "platform": "grok"}]
|
||||
def list_accounts(self): return []
|
||||
|
||||
monkeypatch.setattr("app.routers.websites.Sub2ApiWebsiteClient", FakeClient)
|
||||
@@ -371,7 +284,7 @@ def test_import_upstream_key_with_grok_platform(monkeypatch, db_session):
|
||||
ImportAccountsRequest(
|
||||
upstream_key_ids=[generated.id],
|
||||
target_group_map={"vip": "7"},
|
||||
default_platform="grok",
|
||||
default_platform="openai",
|
||||
platform_mode="manual",
|
||||
),
|
||||
db_session,
|
||||
|
||||
Reference in New Issue
Block a user