Add AIServer, character art pipeline, and Art package boot integration.

Bring in MaleExplorer assets/shaders, model tooling and Hunyuan updates, plus boot/YooAsset Art package support.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
udx
2026-09-21 17:06:59 +08:00
co-authored by Cursor
parent b7a0bbcb89
commit 6550b8df4b
121 changed files with 313260 additions and 113 deletions
+98 -13
View File
@@ -1,6 +1,7 @@
"""WeChat login session for https://3d.hunyuan.tencent.com via Playwright.
CLI 友好:同步阻塞扫码登录(非后台线程)。
未登录 / 凭证失效 / 额度满换号 时弹出有头 Edge 供用户扫码。
"""
from __future__ import annotations
@@ -11,7 +12,7 @@ from pathlib import Path
from typing import Any, Callable, Optional
HUNYUAN_URL = "https://3d.hunyuan.tencent.com/"
LOGIN_TIMEOUT_SEC = 180
LOGIN_TIMEOUT_SEC = 300
UpdateFn = Callable[..., None]
@@ -140,32 +141,87 @@ def _looks_logged_in(page) -> bool:
has_generate = bool(page.get_by_text("立即生成").count())
except Exception:
pass
return bool((interesting or storage_hit) and has_generate)
# 有生成入口,且 cookie/storage 任一命中,或页面上已无「登录」
if has_generate and (interesting or storage_hit):
return True
if has_generate and not _page_shows_login_cta(page):
return True
return bool(interesting and storage_hit)
def _page_shows_login_cta(page) -> bool:
try:
if page.get_by_text("登录", exact=True).count():
return page.get_by_text("登录", exact=True).first.is_visible(timeout=300)
except Exception:
pass
return False
def _try_site_logout(page) -> None:
"""尽量在官网侧退出,方便额度满后换号扫码。"""
for label in ("退出登录", "退出", "登出", "注销"):
if _click_first(page, (label,), exact=False):
page.wait_for_timeout(800)
break
# 常见头像/用户菜单再点一次退出
try:
avatar = page.locator(
'[class*="avatar"], [class*="user-info"], [class*="UserInfo"]'
).first
if avatar.count() and avatar.is_visible(timeout=500):
avatar.click(timeout=2000)
page.wait_for_timeout(400)
_click_first(page, ("退出登录", "退出", "登出"), exact=False)
except Exception:
pass
def start_wechat_login_blocking(
timeout_sec: int = LOGIN_TIMEOUT_SEC,
*,
update: Optional[UpdateFn] = None,
reason: str = "auth",
clear_existing: bool = False,
) -> dict:
"""打开有头浏览器供微信扫码,阻塞直到成功 / 超时 / 失败。"""
"""打开有头浏览器供微信扫码,阻塞直到成功 / 超时 / 失败。
reason:
- auth: 未登录或凭证失效
- quota: 当前账号额度满,需换号
clear_existing: 先清本地会话;quota 场景会再尝试官网退出。
"""
from playwright.sync_api import sync_playwright
def _emit(event: str, **payload: Any) -> None:
if update:
update(event=event, **payload)
if clear_existing or reason == "quota":
logout()
if reason == "quota":
message = (
"当前混元账号额度/次数已满,请在弹出的浏览器中退出旧号并用微信扫码登录其他账号"
)
else:
message = "请在弹出的浏览器窗口中用微信扫码登录混元3D"
_write_status(
logged_in=False,
state="waiting_scan",
message="请在弹出的浏览器窗口中用微信扫码登录",
message=message,
reason=reason,
)
_emit(
"login_waiting",
stage="请在弹出的浏览器窗口中用微信扫码登录",
stage=message,
progress=5,
timeout_sec=timeout_sec,
reason=reason,
)
# 同步打到 stderr,方便 Agent/终端注意弹窗
print(f"[hunyuan] {message}(最长等待 {timeout_sec}s)", flush=True)
try:
with sync_playwright() as playwright:
@@ -177,8 +233,15 @@ def start_wechat_login_blocking(
)
page = context.new_page()
page.goto(HUNYUAN_URL, wait_until="domcontentloaded", timeout=60000)
page.wait_for_timeout(2500)
page.wait_for_timeout(2000)
if reason == "quota":
_try_site_logout(page)
page.goto(HUNYUAN_URL, wait_until="domcontentloaded", timeout=60000)
page.wait_for_timeout(1500)
_click_first(page, ("登录", "立即登录"), exact=True)
page.wait_for_timeout(800)
_click_first(page, ("微信", "微信登录", "微信扫码"), exact=False)
deadline = time.monotonic() + timeout_sec
@@ -187,15 +250,22 @@ def start_wechat_login_blocking(
session_dir().mkdir(parents=True, exist_ok=True)
context.storage_state(path=str(storage_state_path()))
result = _write_status(
logged_in=True, state="ready", message="微信登录成功"
logged_in=True,
state="ready",
message="微信登录成功",
reason=reason,
)
browser.close()
_emit("login_ok", stage="微信登录成功", progress=15, **result)
print("[hunyuan] 微信登录成功,会话已保存", flush=True)
return result
page.wait_for_timeout(1500)
result = _write_status(
logged_in=False, state="timeout", message="微信登录超时,请重试"
logged_in=False,
state="timeout",
message="微信登录超时,请重试",
reason=reason,
)
browser.close()
_emit("login_failed", stage="微信登录超时", progress=0, **result)
@@ -204,7 +274,7 @@ def start_wechat_login_blocking(
raise
except Exception as exc:
result = _write_status(
logged_in=False, state="error", message=f"登录失败:{exc}"
logged_in=False, state="error", message=f"登录失败:{exc}", reason=reason
)
_emit("login_failed", stage=str(exc), progress=0, **result)
raise RuntimeError(f"登录失败:{exc}") from exc
@@ -215,9 +285,14 @@ def ensure_login(
interactive: bool = True,
timeout_sec: int = LOGIN_TIMEOUT_SEC,
update: Optional[UpdateFn] = None,
force: bool = False,
reason: str = "auth",
) -> dict:
"""若无会话则弹微信登录;interactive=False 且未登录则抛错。"""
if is_logged_in():
"""若无会话则弹微信登录;interactive=False 且未登录则抛错。
force=True 时忽略本地凭证,强制打开登录页(换号 / 重登)。
"""
if is_logged_in() and not force:
current = status()
if update:
update(
@@ -234,7 +309,17 @@ def ensure_login(
if update:
update(
event="login_required",
stage="需要微信登录混元3D",
stage=(
"当前账号额度已满,需要换号登录"
if reason == "quota"
else "需要微信登录混元3D"
),
progress=2,
reason=reason,
)
return start_wechat_login_blocking(timeout_sec=timeout_sec, update=update)
return start_wechat_login_blocking(
timeout_sec=timeout_sec,
update=update,
reason=reason,
clear_existing=force or reason == "quota",
)