Files
hy3d/hunyuan3dweb/browser/sniffer.py
T
Claude 734d53dafb fix: prevent browser process leaks and improve robustness
- fix(browser): wrap context lifecycle in try/finally to ensure browser
  closes on exceptions and KeyboardInterrupt (login, sniffer, generator)
- fix(browser): replace time.sleep with Playwright native waits
  (wait_for, wait_for_timeout) for more reliable element interaction
- fix(browser): use parameterized page.evaluate instead of f-string
  JS injection in generator polling
- fix(api): add retry logic in wait_for_completion to survive transient
  network errors
- fix(config): add prepare_profile_dir to copy profile to temp dir,
  preventing Chromium SingletonLock conflicts when tools run concurrently
- fix(sniffer): stream API logs to tempfile instead of unbounded memory
  list to avoid OOM
- fix(api): specify encoding='utf-8' when loading cookies from file

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:08:46 +08:00

138 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""
腾讯混元3D API 拦截分析工具 (CloakBrowser)
利用持久化登录状态,自动捕获所有 API 请求和响应
"""
import json
import os
import shutil
import sys
import tempfile
import time
from datetime import datetime
from cloakbrowser import launch_persistent_context
from ..config import get_profile_dir, prepare_profile_dir
PROFILE_DIR = str(get_profile_dir())
API_LOG_FILE = "./api_requests.log.json"
def main():
# 使用临时文件缓冲日志,避免长时间嗅探导致内存无限增长
log_buffer = tempfile.NamedTemporaryFile(mode="w+", suffix=".jsonl", delete=False, encoding="utf-8")
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
log_buffer.write(json.dumps(entry, ensure_ascii=False) + "\n")
log_buffer.flush()
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)
log_buffer.write(json.dumps(entry, ensure_ascii=False) + "\n")
log_buffer.flush()
print(f"[RES] {response.status} {url}")
if not os.path.exists(PROFILE_DIR):
print(f"错误: 未找到持久化目录 {PROFILE_DIR}")
print("请先运行 hunyuan3dweb-login 完成登录")
sys.exit(1)
profile_dir, temp_dir = prepare_profile_dir()
context = None
try:
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()
finally:
if context is not None:
try:
context.close()
except Exception:
pass
if temp_dir is not None:
try:
shutil.rmtree(temp_dir)
except Exception:
pass
# 从临时文件读取并保存为 JSON 数组
try:
log_buffer.flush()
log_buffer.seek(0)
logs = [json.loads(line) for line in log_buffer if line.strip()]
finally:
log_buffer.close()
os.unlink(log_buffer.name)
# 保存日志
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)}")
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n已取消")
sys.exit(0)