feat: complete huihui square avatar workflows

This commit is contained in:
stefanfeng
2026-07-24 14:04:21 +08:00
parent 2ef58a44b8
commit e3bda469bb
68 changed files with 8062 additions and 132 deletions

View File

@@ -0,0 +1,128 @@
#!/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)

View File

@@ -0,0 +1,73 @@
#!/usr/bin/env python3
"""Repair nickname fields for the imported phone segment.
This fixes users whose nickname was overwritten back to the phone number by
setting nickname = real_name for the target account range.
"""
from __future__ import annotations
import os
import sys
import pymysql
DB_HOST = os.getenv("DB_HOST", "127.0.0.1")
DB_PORT = int(os.getenv("DB_PORT", "3306"))
DB_USER = os.getenv("DB_USER", "aivirtual")
DB_PASSWORD = os.getenv("DB_PASSWORD", "AiVirtual2024")
DB_NAME = os.getenv("DB_NAME", "ai_virtual_news")
ACCOUNT_START = os.getenv("ACCOUNT_START", "13721560041")
ACCOUNT_END = os.getenv("ACCOUNT_END", "13721560199")
def main() -> int:
conn = pymysql.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
database=DB_NAME,
charset="utf8mb4",
autocommit=False,
)
try:
with conn.cursor() as cur:
cur.execute(
"""
SELECT id, account, nickname, real_name
FROM virtual_users
WHERE account BETWEEN %s AND %s
ORDER BY account
""",
(ACCOUNT_START, ACCOUNT_END),
)
rows = cur.fetchall()
if not rows:
print("No matching users found.")
return 1
cur.execute(
"""
UPDATE virtual_users
SET nickname = real_name
WHERE account BETWEEN %s AND %s
AND real_name IS NOT NULL
AND real_name <> ''
AND nickname <> real_name
""",
(ACCOUNT_START, ACCOUNT_END),
)
updated = cur.rowcount
conn.commit()
print(f"Checked {len(rows)} users, repaired {updated} nicknames.")
return 0
finally:
conn.close()
if __name__ == "__main__":
raise SystemExit(main())