#!/usr/bin/env python3 from __future__ import annotations import mimetypes import os import sys import time from pathlib import Path import httpx API_BASE = os.getenv("HUIHUI_API_BASE", "http://192.168.1.188:8000/api").rstrip("/") AVATAR_DIR = Path(os.getenv("HUIHUI_AVATAR_DIR", str(Path.home() / "Downloads" / "头像"))) ACCOUNT_START = os.getenv("HUIHUI_ACCOUNT_START", "13721560041") ACCOUNT_END = os.getenv("HUIHUI_ACCOUNT_END", "13721560199") POLL_SECONDS = int(os.getenv("HUIHUI_POLL_SECONDS", "10")) TIMEOUT_SECONDS = int(os.getenv("HUIHUI_TIMEOUT_SECONDS", "5400")) def load_avatar_files(expected_count: int) -> list[Path]: files = sorted(path for path in AVATAR_DIR.iterdir() if path.is_file()) if len(files) < expected_count: raise RuntimeError(f"头像文件不足: 需要 {expected_count} 个,实际只有 {len(files)} 个") return files[:expected_count] def fetch_target_users(client: httpx.Client) -> list[dict]: users: list[dict] = [] page = 1 page_size = 100 while True: resp = client.get( f"{API_BASE}/users", params={"page": page, "page_size": page_size, "keyword": "1372156"}, ) resp.raise_for_status() payload = resp.json()["data"] items = payload["items"] users.extend(items) if page * page_size >= payload["total"] or not items: break page += 1 targets = [u for u in users if ACCOUNT_START <= u["account"] <= ACCOUNT_END] targets.sort(key=lambda item: item["account"]) return targets def upload_avatar(client: httpx.Client, user: dict, avatar_path: Path) -> str: mime = mimetypes.guess_type(str(avatar_path))[0] or "application/octet-stream" with avatar_path.open("rb") as fp: resp = client.post( f"{API_BASE}/users/{user['id']}/upload-avatar", params={"sync_to_platform": "true"}, files={"file": (avatar_path.name, fp, mime)}, timeout=120, ) resp.raise_for_status() payload = resp.json() if payload.get("code") not in (0, 200): raise RuntimeError(payload.get("message") or f"上传失败: {payload}") return payload["data"]["avatar_url"] def main() -> int: started_at = time.time() processed: set[str] = set() with httpx.Client(timeout=30) as client: initial_targets = fetch_target_users(client) if not initial_targets: raise RuntimeError("没有找到目标手机号范围内的用户") avatar_files = load_avatar_files(len(initial_targets)) avatar_map = { user["account"]: avatar_files[idx] for idx, user in enumerate(initial_targets) } print( f"准备处理 {len(initial_targets)} 个用户,头像目录 {AVATAR_DIR},接口 {API_BASE}", flush=True, ) while len(processed) < len(initial_targets): if time.time() - started_at > TIMEOUT_SECONDS: pending = sorted(set(avatar_map) - processed) print(f"超时,仍有 {len(pending)} 个用户未完成: {', '.join(pending[:20])}", flush=True) return 2 targets = fetch_target_users(client) status_summary: dict[int, int] = {} for user in targets: status_summary[user["status"]] = status_summary.get(user["status"], 0) + 1 for user in targets: account = user["account"] if account in processed: continue if user["status"] != 2: continue avatar_url = upload_avatar(client, user, avatar_map[account]) processed.add(account) print( f"[{len(processed):03d}/{len(targets)}] {account} -> {avatar_map[account].name} -> {avatar_url}", flush=True, ) if len(processed) < len(initial_targets): print( f"等待登录中: 已完成 {len(processed)}/{len(initial_targets)},状态分布 {status_summary}", flush=True, ) time.sleep(POLL_SECONDS) print("全部头像替换完成", flush=True) return 0 if __name__ == "__main__": try: raise SystemExit(main()) except KeyboardInterrupt: print("已中断", file=sys.stderr) raise SystemExit(130)