feat: live remote key list with auto-upsert and safe group name extraction

- list_generated_keys now fetches live keys from upstream API, merges with
  local DB: remote keys with plaintext values are auto-upserted (by
  group_id+managed_prefix), remote-only keys shown as unimportable
- Use _fetch_remote_managed_prefixes to support custom key prefixes
- Group remote keys by (group_id, prefix), pick latest by key_id
- Extract _remote_group_name helper for safe group name parsing
  (handles dict group field from Meow upstream)
- Frontend excludes orphaned keys from importable list
- Backend import endpoint reconciles upstream before importing
This commit is contained in:
liumangmang
2026-06-01 14:53:40 +08:00
parent bea4344bb3
commit c8ba25f08e
2 changed files with 201 additions and 24 deletions
+22 -14
View File
@@ -418,10 +418,10 @@ def sync_account_priorities_for_upstream(db: Session, upstream_id: int) -> list[
return all_results
def _fetch_remote_managed_key_ids(db: Session, client, upstream_id: int) -> set[str]:
"""查询本地 distinct managed_prefix,分别拉远端活跃 Key ID 集合
def _fetch_remote_managed_prefixes(db: Session, upstream_id: int) -> list[str]:
"""查询本地 distinct managed_prefix。
返回全部找到的远端 Key ID(合并多个 prefix 的结果)
返回该上游所有已使用的 prefix 列表。空时回退 ["SmartUp"] 兼容旧数据
"""
prefixes = [
row[0] for row in
@@ -433,10 +433,16 @@ def _fetch_remote_managed_key_ids(db: Session, client, upstream_id: int) -> set[
.distinct()
.all()
]
if not prefixes:
prefixes = ["SmartUp"]
return prefixes if prefixes else ["SmartUp"]
def _fetch_remote_managed_key_ids(db: Session, client, upstream_id: int) -> set[str]:
"""查询本地 distinct managed_prefix,分别拉远端活跃 Key ID 集合。
返回全部找到的远端 Key ID(合并多个 prefix 的结果)。
"""
all_ids: set[str] = set()
for prefix in prefixes:
for prefix in _fetch_remote_managed_prefixes(db, upstream_id):
remote_keys = client.list_api_keys(search=prefix, status="active")
all_ids.update(str(k["id"]) for k in remote_keys if k.get("id"))
return all_ids
@@ -490,12 +496,13 @@ def reconcile_upstream_keys(
def reconcile_upstream_keys_full(db: Session, upstream_id: int) -> bool:
"""完整的 Key 对账:登录上游、拉取分组和远端 Key 列表调用 reconcile_upstream_keys。
"""完整的 Key 对账:拉取最新快照的分组 + 登录上游查远端 Key 列表调用 reconcile_upstream_keys。
活跃分组 ID 从最新快照获取(与调度器一致),而非调用 live API 避免格式不一致。
安全规则:
- 分组拉取成功 → 才允许分组级清理。
- 快照存在 → 才允许分组级清理。
- 远端 Key 列表拉取成功 → 才允许 key_id 级清理。
- 登录/分组拉取失败 → 不做任何删除/标记。
- 两者均失败 → 不做任何删除/标记。
支持自定义 managed_prefix:查询本地 distinct prefix,分别查远端。
"""
@@ -512,6 +519,12 @@ def reconcile_upstream_keys_full(db: Session, upstream_id: int) -> bool:
remote_key_ids: set[str] | None = None
now = datetime.now(timezone.utc)
# 从最新快照获取活跃分组 ID(与调度器 _sync_upstream_keys 一致)
groups = latest_rate_map(db, upstream_id)
if groups:
active_group_ids = set(groups.keys())
groups_fetched = True
try:
with UpstreamClient(
base_url=upstream.base_url,
@@ -521,11 +534,6 @@ def reconcile_upstream_keys_full(db: Session, upstream_id: int) -> bool:
timeout=float(upstream.timeout_seconds),
) as client:
client.login()
# 获取当前分组 ID 集合
groups = client.get_available_groups(upstream.groups_endpoint)
active_group_ids = {g.get("group_id") or g.get("id") or "" for g in groups if isinstance(g, dict)}
active_group_ids.discard("")
groups_fetched = True
# 获取远端 Key 列表(支持自定义 managed_prefix
remote_key_ids = _fetch_remote_managed_key_ids(db, client, upstream_id)
keys_fetched = True