HouYunFei commited on
Commit
7de5f87
·
1 Parent(s): d4e898c

feat: refactor access token fetching to handle multiple accounts and improve error reporting

Browse files
Files changed (1) hide show
  1. services/sub2api_service.py +49 -41
services/sub2api_service.py CHANGED
@@ -6,7 +6,6 @@ import json
6
  import threading
7
  import time
8
  import uuid
9
- from concurrent.futures import ThreadPoolExecutor, as_completed
10
  from datetime import datetime, timezone
11
  from pathlib import Path
12
  from threading import Lock
@@ -309,12 +308,11 @@ def list_remote_accounts(server: dict) -> list[dict]:
309
  if not isinstance(account, dict):
310
  continue
311
  credentials = account.get("credentials") if isinstance(account.get("credentials"), dict) else {}
312
- access_token = _extract_access_token(credentials)
313
- if not access_token:
314
- continue
315
  account_id = account.get("id")
 
 
316
  items.append({
317
- "id": str(account_id) if account_id is not None else _clean(credentials.get("chatgpt_account_id")),
318
  "name": _clean(account.get("name")),
319
  "email": _clean(credentials.get("email")) or _clean(account.get("name")),
320
  "plan_type": _clean(credentials.get("plan_type")),
@@ -387,16 +385,20 @@ def list_remote_groups(server: dict) -> list[dict]:
387
  return items
388
 
389
 
390
- def _fetch_access_token_for_account(server: dict, account_id: str) -> tuple[str, dict]:
391
- """Return (access_token, account_meta) for a single sub2api account id."""
392
  base_url = _clean(server.get("base_url"))
393
  headers = _auth_headers(server)
 
 
 
394
 
395
  session = Session(verify=True)
396
  try:
397
  response = session.get(
398
- f"{base_url.rstrip('/')}/api/v1/admin/accounts/{account_id}",
399
  headers=headers,
 
400
  timeout=30,
401
  )
402
  if not response.ok:
@@ -405,17 +407,28 @@ def _fetch_access_token_for_account(server: dict, account_id: str) -> tuple[str,
405
  finally:
406
  session.close()
407
 
408
- account = _unwrap_envelope(payload)
409
- if not isinstance(account, dict):
410
- account = payload if isinstance(payload, dict) else {}
411
- credentials = account.get("credentials") if isinstance(account.get("credentials"), dict) else {}
412
- access_token = _extract_access_token(credentials)
413
- if not access_token:
414
- raise RuntimeError("missing access_token")
415
- return access_token, {
416
- "email": _clean(credentials.get("email")),
417
- "plan_type": _clean(credentials.get("plan_type")),
418
- }
 
 
 
 
 
 
 
 
 
 
 
419
 
420
 
421
  class Sub2APIImportService:
@@ -472,28 +485,23 @@ class Sub2APIImportService:
472
  def _run_import(self, server_id: str, server: dict, account_ids: list[str]) -> None:
473
  self._update_job(server_id, status="running")
474
 
475
- tokens: list[str] = []
476
- max_workers = min(8, max(1, len(account_ids)))
477
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
478
- future_map = {
479
- executor.submit(_fetch_access_token_for_account, server, account_id): account_id
480
- for account_id in account_ids
481
- }
482
- for future in as_completed(future_map):
483
- account_id = future_map[future]
484
- try:
485
- token, _meta = future.result()
486
- tokens.append(token)
487
- except Exception as exc:
488
- self._append_error(server_id, account_id, str(exc) or "unknown error")
489
-
490
- current = self._config.get_import_job(server_id) or {}
491
- failed = len(current.get("errors") or [])
492
- self._update_job(
493
- server_id,
494
- completed=int(current.get("completed") or 0) + 1,
495
- failed=failed,
496
- )
497
 
498
  if not tokens:
499
  current = self._config.get_import_job(server_id) or {}
 
6
  import threading
7
  import time
8
  import uuid
 
9
  from datetime import datetime, timezone
10
  from pathlib import Path
11
  from threading import Lock
 
308
  if not isinstance(account, dict):
309
  continue
310
  credentials = account.get("credentials") if isinstance(account.get("credentials"), dict) else {}
 
 
 
311
  account_id = account.get("id")
312
+ if account_id is None:
313
+ continue
314
  items.append({
315
+ "id": str(account_id),
316
  "name": _clean(account.get("name")),
317
  "email": _clean(credentials.get("email")) or _clean(account.get("name")),
318
  "plan_type": _clean(credentials.get("plan_type")),
 
385
  return items
386
 
387
 
388
+ def _fetch_access_tokens_for_accounts(server: dict, account_ids: list[str]) -> tuple[list[str], list[dict]]:
389
+ """Return exported access tokens and per-account errors from sub2api."""
390
  base_url = _clean(server.get("base_url"))
391
  headers = _auth_headers(server)
392
+ ids = [_clean(item) for item in account_ids if _clean(item)]
393
+ if not ids:
394
+ return [], []
395
 
396
  session = Session(verify=True)
397
  try:
398
  response = session.get(
399
+ f"{base_url.rstrip('/')}/api/v1/admin/accounts/data",
400
  headers=headers,
401
+ params={"ids": ",".join(ids), "timezone": "Asia/Shanghai"},
402
  timeout=30,
403
  )
404
  if not response.ok:
 
407
  finally:
408
  session.close()
409
 
410
+ data = _unwrap_envelope(payload)
411
+ accounts = data.get("accounts") if isinstance(data, dict) else None
412
+ if not isinstance(accounts, list):
413
+ raise RuntimeError("invalid export payload")
414
+
415
+ tokens: list[str] = []
416
+ errors: list[dict] = []
417
+ for account in accounts:
418
+ if not isinstance(account, dict):
419
+ continue
420
+ credentials = account.get("credentials") if isinstance(account.get("credentials"), dict) else {}
421
+ token = _extract_access_token(credentials)
422
+ account_id = _clean(account.get("id")) or _clean(credentials.get("chatgpt_account_id")) or _clean(account.get("name"))
423
+ if token:
424
+ tokens.append(token)
425
+ else:
426
+ errors.append({"name": account_id or _clean(account.get("name")) or "unknown", "error": "missing access_token"})
427
+
428
+ if len(accounts) < len(ids):
429
+ errors.append({"name": ",".join(ids), "error": f"exported {len(accounts)}/{len(ids)} accounts"})
430
+
431
+ return tokens, errors
432
 
433
 
434
  class Sub2APIImportService:
 
485
  def _run_import(self, server_id: str, server: dict, account_ids: list[str]) -> None:
486
  self._update_job(server_id, status="running")
487
 
488
+ try:
489
+ tokens, errors = _fetch_access_tokens_for_accounts(server, account_ids)
490
+ except Exception as exc:
491
+ message = str(exc) or "unknown error"
492
+ for account_id in account_ids:
493
+ self._append_error(server_id, account_id, message)
494
+ tokens = []
495
+ else:
496
+ for error in errors:
497
+ self._append_error(server_id, _clean(error.get("name")), _clean(error.get("error")) or "unknown error")
498
+
499
+ current = self._config.get_import_job(server_id) or {}
500
+ self._update_job(
501
+ server_id,
502
+ completed=len(account_ids),
503
+ failed=len(current.get("errors") or []),
504
+ )
 
 
 
 
 
505
 
506
  if not tokens:
507
  current = self._config.get_import_job(server_id) or {}