Initial commit
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
腾讯混元3D 图生3D 完整自动化脚本
|
||||
使用 cloakbrowser 在浏览器内完成所有操作(包括签名生成)
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from datetime import datetime
|
||||
from cloakbrowser import launch_persistent_context
|
||||
|
||||
from ..config import get_profile_dir
|
||||
|
||||
PROFILE_DIR = str(get_profile_dir())
|
||||
|
||||
|
||||
def generate_3d(image_path, wait_for_complete=False, timeout=300):
|
||||
"""
|
||||
上传图片并触发生成3D模型
|
||||
|
||||
Args:
|
||||
image_path: 图片路径
|
||||
wait_for_complete: 是否等待生成完成
|
||||
timeout: 最长等待时间(秒)
|
||||
|
||||
Returns:
|
||||
dict: 包含 creationsId 和状态信息
|
||||
"""
|
||||
if not os.path.exists(image_path):
|
||||
raise FileNotFoundError(f"图片不存在: {image_path}")
|
||||
|
||||
if not os.path.exists(PROFILE_DIR):
|
||||
raise FileNotFoundError(f"未找到登录状态目录: {PROFILE_DIR}")
|
||||
|
||||
context = launch_persistent_context(PROFILE_DIR, headless=True)
|
||||
page = context.new_page()
|
||||
|
||||
# 用于存储结果
|
||||
result = {"creationsId": None, "status": None, "modelUrl": None}
|
||||
|
||||
# 监听响应
|
||||
def handle_response(response):
|
||||
url = response.url
|
||||
if "creations/generations" in url and response.status == 200:
|
||||
try:
|
||||
body = response.json()
|
||||
if "creationsId" in body:
|
||||
result["creationsId"] = body["creationsId"]
|
||||
print(f"✅ 生成已触发,creationsId: {body['creationsId']}")
|
||||
except:
|
||||
pass
|
||||
elif "creations/detail" in url and response.status == 200:
|
||||
try:
|
||||
body = response.json()
|
||||
result["status"] = body.get("status")
|
||||
if "result" in body and isinstance(body["result"], list) and len(body["result"]) > 0:
|
||||
model_data = body["result"][0]
|
||||
if "modelUrl" in model_data:
|
||||
result["modelUrl"] = model_data["modelUrl"]
|
||||
except:
|
||||
pass
|
||||
|
||||
page.on("response", handle_response)
|
||||
|
||||
try:
|
||||
print("[1/6] 打开首页...")
|
||||
page.goto("https://3d.hunyuan.tencent.com/")
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
print("[2/6] 点击 AI创作...")
|
||||
page.locator("button").filter(has_text="AI创作").first.click()
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
print("[3/6] 点击 图生3D...")
|
||||
page.locator("text=图生3D").first.click()
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
print("[4/6] 点击 单张图片...")
|
||||
page.locator("text=单张图片").first.click()
|
||||
page.wait_for_timeout(2000)
|
||||
|
||||
print("[5/6] 上传图片...")
|
||||
page.locator('input[type=file]').first.set_input_files(image_path)
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
print("[6/6] 点击 立即生成...")
|
||||
page.locator("text=立即生成").first.click()
|
||||
|
||||
# 等待生成触发
|
||||
page.wait_for_timeout(5000)
|
||||
|
||||
if not result["creationsId"]:
|
||||
print("⚠️ 未获取到 creationsId,可能生成失败")
|
||||
return result
|
||||
|
||||
if wait_for_complete:
|
||||
print(f"\n⏳ 等待生成完成(最长 {timeout} 秒)...")
|
||||
start = time.time()
|
||||
last_status = None
|
||||
while time.time() - start < timeout:
|
||||
# 轮询状态
|
||||
status_result = page.evaluate(f'''
|
||||
async () => {{
|
||||
const resp = await fetch('https://3d.hunyuan.tencent.com/api/3d/creations/detail?creationsId={result["creationsId"]}', {{
|
||||
headers: {{'X-Source': 'web', 'Referer': 'https://3d.hunyuan.tencent.com/'}}
|
||||
}});
|
||||
return await resp.json();
|
||||
}}
|
||||
''')
|
||||
|
||||
status = status_result.get("status", "unknown")
|
||||
progress = status_result.get("progress", 0)
|
||||
|
||||
if status != last_status:
|
||||
print(f" 状态: {status} (进度: {progress}%)")
|
||||
last_status = status
|
||||
|
||||
if status == "success":
|
||||
# 提取模型URL
|
||||
if "result" in status_result and len(status_result["result"]) > 0:
|
||||
model_data = status_result["result"][0]
|
||||
result["modelUrl"] = model_data.get("modelUrl")
|
||||
result["previewUrl"] = model_data.get("previewUrl")
|
||||
print(f"\n✅ 生成完成!")
|
||||
break
|
||||
elif status == "fail":
|
||||
print(f"\n❌ 生成失败")
|
||||
break
|
||||
|
||||
time.sleep(3)
|
||||
else:
|
||||
print(f"\n⏰ 超时,当前状态: {last_status}")
|
||||
|
||||
return result
|
||||
|
||||
finally:
|
||||
context.close()
|
||||
|
||||
|
||||
def get_quota():
|
||||
"""获取当前配额信息"""
|
||||
context = launch_persistent_context(PROFILE_DIR, headless=True)
|
||||
page = context.new_page()
|
||||
|
||||
try:
|
||||
page.goto("https://3d.hunyuan.tencent.com/")
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
result = page.evaluate('''
|
||||
async () => {
|
||||
const resp = await fetch('https://3d.hunyuan.tencent.com/api/3d/quotainfo', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Source': 'web', 'Referer': 'https://3d.hunyuan.tencent.com/'},
|
||||
body: '{"sceneType":"3dCreations"}'
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
''')
|
||||
return result
|
||||
finally:
|
||||
context.close()
|
||||
|
||||
|
||||
def get_creations_list():
|
||||
"""获取作品列表"""
|
||||
context = launch_persistent_context(PROFILE_DIR, headless=True)
|
||||
page = context.new_page()
|
||||
|
||||
try:
|
||||
page.goto("https://3d.hunyuan.tencent.com/")
|
||||
page.wait_for_timeout(3000)
|
||||
|
||||
result = page.evaluate('''
|
||||
async () => {
|
||||
const resp = await fetch('https://3d.hunyuan.tencent.com/api/3d/creations/list', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json', 'X-Source': 'web', 'Referer': 'https://3d.hunyuan.tencent.com/'},
|
||||
body: '{"page":1,"pageSize":20}'
|
||||
});
|
||||
return await resp.json();
|
||||
}
|
||||
''')
|
||||
return result
|
||||
finally:
|
||||
context.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("用法:")
|
||||
print(f" python {sys.argv[0]} quota # 查询配额")
|
||||
print(f" python {sys.argv[0]} list # 查询作品列表")
|
||||
print(f" python {sys.argv[0]} generate <图片路径> [wait] # 生成3D模型")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
|
||||
if cmd == "quota":
|
||||
quota = get_quota()
|
||||
print(json.dumps(quota, indent=2, ensure_ascii=False))
|
||||
|
||||
elif cmd == "list":
|
||||
creations = get_creations_list()
|
||||
print(json.dumps(creations, indent=2, ensure_ascii=False))
|
||||
|
||||
elif cmd == "generate":
|
||||
if len(sys.argv) < 3:
|
||||
print("请提供图片路径")
|
||||
sys.exit(1)
|
||||
|
||||
image_path = sys.argv[2]
|
||||
wait = len(sys.argv) > 3 and sys.argv[3] == "wait"
|
||||
|
||||
result = generate_3d(image_path, wait_for_complete=wait)
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
|
||||
else:
|
||||
print(f"未知命令: {cmd}")
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
腾讯混元3D 邮箱验证码登录 CLI 工具 (CloakBrowser 持久化版本)
|
||||
第一次输入邮箱回车后自动发送验证码
|
||||
第二次输入验证码后回车自动点击登录按钮
|
||||
登录状态会自动保存到 ./hunyuan3d_profile,下次运行无需重新登录
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from cloakbrowser import launch_persistent_context
|
||||
from ..config import get_profile_dir, get_cookie_path
|
||||
|
||||
PROFILE_DIR = str(get_profile_dir())
|
||||
|
||||
|
||||
def _extract_and_save_cookies(context):
|
||||
"""从浏览器上下文提取 Cookie 并保存到文件"""
|
||||
try:
|
||||
cookies = context.cookies()
|
||||
relevant_names = {"hunyuan_user", "hunyuan_token", "hunyuan_source", "hy_user"}
|
||||
cookie_dict = {c["name"]: c["value"] for c in cookies if c["name"] in relevant_names}
|
||||
if cookie_dict:
|
||||
cookie_str = "; ".join(f"{k}={v}" for k, v in cookie_dict.items())
|
||||
cookie_path = get_cookie_path()
|
||||
cookie_path.write_text(cookie_str, encoding="utf-8")
|
||||
print(f"Cookie 已自动保存到: {cookie_path}")
|
||||
return True
|
||||
else:
|
||||
print("警告: 未找到 hunyuan 相关 Cookie,API 客户端可能无法使用")
|
||||
except Exception as e:
|
||||
print(f"自动提取 Cookie 失败: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
# 使用持久化上下文,cookie 和登录状态会保存到 PROFILE_DIR
|
||||
context = launch_persistent_context(PROFILE_DIR, headless=False)
|
||||
page = context.new_page()
|
||||
|
||||
print("正在打开腾讯混元3D...")
|
||||
page.goto("https://3d.hunyuan.tencent.com/")
|
||||
time.sleep(2)
|
||||
|
||||
# 检查是否已经是登录状态(没有登录按钮说明已登录)
|
||||
login_btn = page.locator("button").filter(has_text="登录").first
|
||||
if login_btn.count() == 0:
|
||||
print("检测到已有登录状态,无需重新登录。")
|
||||
print(f"当前页面: {page.url}")
|
||||
else:
|
||||
# 未登录,走登录流程
|
||||
login_btn.click()
|
||||
time.sleep(1.5)
|
||||
|
||||
# 切换到邮箱登录
|
||||
email_tab = page.locator("text=邮箱").first
|
||||
if email_tab.count() > 0:
|
||||
email_tab.click()
|
||||
time.sleep(1.5)
|
||||
else:
|
||||
print("未找到邮箱登录选项")
|
||||
context.close()
|
||||
return
|
||||
|
||||
# 定位元素
|
||||
email_input = page.locator('input[placeholder="请输入邮箱地址"]')
|
||||
code_input = page.locator('input[type="number"][placeholder="请输入邮箱验证码"]')
|
||||
send_code_btn = page.locator("a.hyc-email-login__send-code")
|
||||
login_submit_btn = page.locator("button.hyc-email-login__btn")
|
||||
checkbox = page.locator(".t-checkbox__former")
|
||||
|
||||
# 第一次交互:输入邮箱并发送验证码
|
||||
print("\n============================================")
|
||||
email = input("请输入邮箱地址,按回车发送验证码: ").strip()
|
||||
if not email:
|
||||
print("邮箱不能为空,退出")
|
||||
context.close()
|
||||
return
|
||||
|
||||
email_input.fill(email)
|
||||
time.sleep(0.5)
|
||||
|
||||
# 勾选协议(如果未勾选)
|
||||
if checkbox.count() > 0:
|
||||
is_checked = checkbox.evaluate("el => el.checked")
|
||||
if not is_checked:
|
||||
checkbox.evaluate("el => el.click()")
|
||||
time.sleep(0.3)
|
||||
|
||||
if send_code_btn.count() > 0:
|
||||
send_code_btn.click()
|
||||
print("已点击发送验证码,请查收邮件...")
|
||||
else:
|
||||
print("未找到发送验证码按钮")
|
||||
context.close()
|
||||
return
|
||||
|
||||
# 第二次交互:输入验证码并登录
|
||||
print("\n============================================")
|
||||
code = input("请输入邮箱验证码,按回车登录: ").strip()
|
||||
if not code:
|
||||
print("验证码不能为空,退出")
|
||||
context.close()
|
||||
return
|
||||
|
||||
code_input.fill(code)
|
||||
time.sleep(0.5)
|
||||
|
||||
# 点击登录
|
||||
if login_submit_btn.count() > 0:
|
||||
login_submit_btn.click()
|
||||
print("已点击登录按钮,等待跳转...")
|
||||
else:
|
||||
print("未找到登录提交按钮")
|
||||
context.close()
|
||||
return
|
||||
|
||||
# 等待登录成功跳转
|
||||
try:
|
||||
page.wait_for_url(lambda url: "login" not in url, timeout=30000)
|
||||
print(f"\n登录成功!当前页面: {page.url}")
|
||||
print(f"登录状态已保存到: {os.path.abspath(PROFILE_DIR)}")
|
||||
_extract_and_save_cookies(context)
|
||||
except Exception:
|
||||
print("\n登录可能仍在处理中,或出现错误。请检查浏览器状态。")
|
||||
|
||||
# 保持浏览器打开
|
||||
print("\n按 Enter 键关闭浏览器并退出...")
|
||||
input()
|
||||
context.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n已取消")
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
腾讯混元3D API 拦截分析工具 (CloakBrowser)
|
||||
利用持久化登录状态,自动捕获所有 API 请求和响应
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from cloakbrowser import launch_persistent_context
|
||||
|
||||
from ..config import get_profile_dir
|
||||
|
||||
PROFILE_DIR = str(get_profile_dir())
|
||||
API_LOG_FILE = "./api_requests.log.json"
|
||||
|
||||
|
||||
def main():
|
||||
logs = []
|
||||
|
||||
def log_request(request):
|
||||
url = request.url
|
||||
# 只关注同域 API 和关键第三方接口
|
||||
if "/api/" in url or "hunyuan" in url:
|
||||
entry = {
|
||||
"time": datetime.now().isoformat(),
|
||||
"type": "request",
|
||||
"method": request.method,
|
||||
"url": url,
|
||||
"headers": dict(request.headers) if hasattr(request, "headers") else {},
|
||||
}
|
||||
# 尝试获取 POST body
|
||||
if request.method in ("POST", "PUT", "PATCH") and hasattr(request, "post_data"):
|
||||
try:
|
||||
entry["body"] = request.post_data
|
||||
except Exception:
|
||||
pass
|
||||
logs.append(entry)
|
||||
print(f"[REQ] {request.method} {url}")
|
||||
|
||||
def log_response(response):
|
||||
url = response.url
|
||||
if "/api/" in url or "hunyuan" in url:
|
||||
entry = {
|
||||
"time": datetime.now().isoformat(),
|
||||
"type": "response",
|
||||
"status": response.status,
|
||||
"url": url,
|
||||
}
|
||||
# 尝试读取响应体
|
||||
try:
|
||||
# 只读取 JSON 响应
|
||||
content_type = response.headers.get("content-type", "")
|
||||
if "json" in content_type:
|
||||
body = response.json()
|
||||
entry["body"] = body
|
||||
else:
|
||||
text = response.text()
|
||||
# 限制文本长度,避免过大
|
||||
entry["body_preview"] = text[:500]
|
||||
except Exception as e:
|
||||
entry["body_error"] = str(e)
|
||||
logs.append(entry)
|
||||
print(f"[RES] {response.status} {url}")
|
||||
|
||||
if not os.path.exists(PROFILE_DIR):
|
||||
print(f"错误: 未找到持久化目录 {PROFILE_DIR}")
|
||||
print("请先运行 hunyuan3dweb-login 完成登录")
|
||||
sys.exit(1)
|
||||
|
||||
context = launch_persistent_context(PROFILE_DIR, headless=False)
|
||||
page = context.new_page()
|
||||
|
||||
page.on("request", log_request)
|
||||
page.on("response", log_response)
|
||||
|
||||
print("正在打开腾讯混元3D并捕获 API...")
|
||||
page.goto("https://3d.hunyuan.tencent.com/")
|
||||
time.sleep(3)
|
||||
|
||||
# 检查是否已登录
|
||||
login_btn = page.locator("button").filter(has_text="登录").first
|
||||
if login_btn.count() > 0:
|
||||
print("\n警告: 当前未检测到登录状态,API 可能返回未授权")
|
||||
else:
|
||||
print("\n已检测到登录状态,开始捕获 API...")
|
||||
|
||||
print("\n你可以手动在浏览器中操作(切换页面、生成3D等)")
|
||||
print("所有 API 请求会实时打印在终端中")
|
||||
print("按 Enter 停止捕获并保存结果...\n")
|
||||
input()
|
||||
|
||||
# 保存日志
|
||||
with open(API_LOG_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(logs, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"\n共捕获 {len([l for l in logs if l['type'] == 'request'])} 个请求")
|
||||
print(f"结果已保存到: {os.path.abspath(API_LOG_FILE)}")
|
||||
|
||||
context.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n已取消")
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user