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>
This commit is contained in:
Claude
2026-05-24 23:08:46 +08:00
parent b2f549af01
commit 734d53dafb
6 changed files with 255 additions and 180 deletions
+50 -23
View File
@@ -6,19 +6,22 @@
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
from ..config import get_profile_dir, prepare_profile_dir
PROFILE_DIR = str(get_profile_dir())
API_LOG_FILE = "./api_requests.log.json"
def main():
logs = []
# 使用临时文件缓冲日志,避免长时间嗅探导致内存无限增长
log_buffer = tempfile.NamedTemporaryFile(mode="w+", suffix=".jsonl", delete=False, encoding="utf-8")
def log_request(request):
url = request.url
@@ -37,7 +40,8 @@ def main():
entry["body"] = request.post_data
except Exception:
pass
logs.append(entry)
log_buffer.write(json.dumps(entry, ensure_ascii=False) + "\n")
log_buffer.flush()
print(f"[REQ] {request.method} {url}")
def log_response(response):
@@ -62,7 +66,8 @@ def main():
entry["body_preview"] = text[:500]
except Exception as e:
entry["body_error"] = str(e)
logs.append(entry)
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):
@@ -70,27 +75,51 @@ def main():
print("请先运行 hunyuan3dweb-login 完成登录")
sys.exit(1)
context = launch_persistent_context(PROFILE_DIR, headless=False)
page = context.new_page()
profile_dir, temp_dir = prepare_profile_dir()
page.on("request", log_request)
page.on("response", log_response)
context = None
try:
context = launch_persistent_context(profile_dir, headless=False)
page = context.new_page()
print("正在打开腾讯混元3D并捕获 API...")
page.goto("https://3d.hunyuan.tencent.com/")
time.sleep(3)
page.on("request", log_request)
page.on("response", log_response)
# 检查是否已登录
login_btn = page.locator("button").filter(has_text="登录").first
if login_btn.count() > 0:
print("\n警告: 当前未检测到登录状态,API 可能返回未授权")
else:
print("\n已检测到登录状态,开始捕获 API...")
print("正在打开腾讯混元3D并捕获 API...")
page.goto("https://3d.hunyuan.tencent.com/")
time.sleep(3)
print("\n你可以手动在浏览器中操作(切换页面、生成3D等)")
print("所有 API 请求会实时打印在终端中")
print("按 Enter 停止捕获并保存结果...\n")
input()
# 检查是否已登录
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:
@@ -99,8 +128,6 @@ def main():
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: