feat: add pure Python COS upload and improve login detection

- feat(cos): add cos_upload.py for direct file upload without browser
  - implements COS V1 signature algorithm with temporary credentials
  - upload_image() pipeline: get_upload_info → sign → PUT to COS
- feat(api): auto-load cookies from file when cookies arg is omitted
  - both Hunyuan3DAPI and Hunyuan3DAPIComplete now fall back to
    ~/.config/hunyuan3dweb/cookies.txt automatically
- fix(login): strengthen login-state detection using both URL and DOM
  - checks "login" not in page.url AND no login button on page
- docs: update README / README_CN with COS upload examples

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-05-27 00:47:11 +08:00
parent 734d53dafb
commit bf4e1e5755
6 changed files with 206 additions and 12 deletions

View File

@@ -87,7 +87,7 @@ if result["status"] == "success":
### 2. Image-to-3D (Local File)
For local image files, use the browser automation tool. The pure API client cannot upload images directly because Tencent COS requires browser-side decryption of temporary credentials.
For local image files, you can use either the browser automation tool or the pure Python COS uploader to obtain a `resourceUrl` and then call the API.
```bash
hunyuan3dweb-generate /path/to/image.png wait
@@ -102,6 +102,18 @@ result = generate_3d("/path/to/image.png", wait_for_complete=True)
print(f"Model URL: {result.get('modelUrl')}")
```
Or via pure Python upload:
```python
from hunyuan3dweb.cos_upload import upload_image
from hunyuan3dweb import Hunyuan3DAPIComplete
resource_url = upload_image("/path/to/image.png")
api = Hunyuan3DAPIComplete()
result = api.generate_from_image(resource_url, title="My Model")
```
### 3. Image-to-3D (API with Existing resourceUrl)
If you already have a `resourceUrl` (e.g. from a previous browser upload), use the pure API client:
@@ -283,6 +295,7 @@ hunyuan3dweb-sniffer
| `hunyuan3dweb/api.py` | Basic API client (image2model, text2model) |
| `hunyuan3dweb/api_complete.py` | Full API client (all generation modes) |
| `hunyuan3dweb/sign.py` | Tencent Hunyuan 3D signing algorithm |
| `hunyuan3dweb/cos_upload.py` | Pure Python COS upload helper |
| `hunyuan3dweb/config.py` | User config path management |
| `hunyuan3dweb/cli.py` | CLI entry point |
| `hunyuan3dweb/browser/login.py` | Browser login tool |

View File

@@ -87,7 +87,7 @@ if result["status"] == "success":
### 2. 图生3D本地文件
本地图片文件通过浏览器自动化工具上传。纯 API 客户端无法直接上传图片,因为腾讯 COS 临时凭证需要浏览器端解密
本地图片文件通过浏览器自动化工具上传,也可使用纯 Python 的 COS 上传模块先拿到 `resourceUrl` 再走 API
```bash
hunyuan3dweb-generate /path/to/image.png wait
@@ -102,6 +102,18 @@ result = generate_3d("/path/to/image.png", wait_for_complete=True)
print(f"模型地址: {result.get('modelUrl')}")
```
或通过纯 Python 上传:
```python
from hunyuan3dweb.cos_upload import upload_image
from hunyuan3dweb import Hunyuan3DAPIComplete
resource_url = upload_image("/path/to/image.png")
api = Hunyuan3DAPIComplete()
result = api.generate_from_image(resource_url, title="我的模型")
```
### 3. 图生3DAPI已有 resourceUrl
如果你已经有 `resourceUrl`(例如之前通过浏览器上传过),可直接调用 API
@@ -283,6 +295,7 @@ hunyuan3dweb-sniffer
| `hunyuan3dweb/api.py` | 基础 API 客户端图生3D、文生3D |
| `hunyuan3dweb/api_complete.py` | 完整 API 客户端(所有生成模式) |
| `hunyuan3dweb/sign.py` | 腾讯混元3D 签名算法 |
| `hunyuan3dweb/cos_upload.py` | 纯 Python COS 上传工具 |
| `hunyuan3dweb/config.py` | 用户配置路径管理 |
| `hunyuan3dweb/cli.py` | CLI 入口 |
| `hunyuan3dweb/browser/login.py` | 浏览器登录工具 |

View File

@@ -34,8 +34,12 @@ class Hunyuan3DAPI:
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
})
if cookies:
self.set_cookies(cookies)
try:
cookie_value = cookies if cookies is not None else load_cookies_from_file()
except FileNotFoundError:
cookie_value = None
if cookie_value:
self.set_cookies(cookie_value)
def set_cookies(self, cookies: str):
"""设置Cookie"""

View File

@@ -57,8 +57,12 @@ class Hunyuan3DAPI:
"Referer": f"{BASE_URL}/",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
})
if cookies:
self.set_cookies(cookies)
try:
cookie_value = cookies if cookies is not None else load_cookies_from_file()
except FileNotFoundError:
cookie_value = None
if cookie_value:
self.set_cookies(cookie_value)
def set_cookies(self, cookies: str):
self.session.headers["Cookie"] = cookies

View File

@@ -56,9 +56,12 @@ def main():
page.goto("https://3d.hunyuan.tencent.com/", timeout=60000)
page.wait_for_timeout(2000)
# 检查是否已经是登录状态(没有登录按钮说明已登录)
# 检查是否已经是登录状态URL 不含 login 且页面上没有登录按钮
is_login_page = "login" in page.url
login_btn = page.locator("button").filter(has_text="登录").first
if login_btn.count() == 0:
has_login_btn = login_btn.count() > 0
if not is_login_page and not has_login_btn:
print("检测到已有登录状态,无需重新登录。")
print(f"当前页面: {page.url}")
_extract_and_save_cookies(context)
@@ -70,6 +73,10 @@ def main():
login_btn.click()
page.locator("text=邮箱").wait_for(state="visible", timeout=10000)
# 定位元素
email_input = page.locator('input[placeholder="请输入邮箱地址"]')
code_input = page.locator('input[type="number"][placeholder="请输入邮箱验证码"]')
# 切换到邮箱登录
email_tab = page.locator("text=邮箱").first
if email_tab.count() > 0:
@@ -78,10 +85,6 @@ def main():
else:
print("未找到邮箱登录选项")
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")

157
hunyuan3dweb/cos_upload.py Normal file
View File

@@ -0,0 +1,157 @@
#!/usr/bin/env python3
"""
Pure Python COS upload for Tencent Hunyuan 3D.
Uses the API to get temporary credentials, then directly uploads to COS.
"""
import hashlib
import hmac
import mimetypes
from pathlib import Path
from urllib.parse import quote
import requests
from . import Hunyuan3DAPIComplete
from .api import load_cookies_from_file
def _cam_safe_url_encode(s):
return quote(str(s), safe="")
def _get_object_keys(obj):
return sorted(obj.keys(), key=lambda x: x.lower())
def _obj2str(obj, lower_case_key=False):
items = []
for key in _get_object_keys(obj):
val = obj[key]
if val is None:
val = ""
else:
val = str(val)
encoded_key = _cam_safe_url_encode(key).lower() if lower_case_key else _cam_safe_url_encode(key)
encoded_val = _cam_safe_url_encode(val) or ""
items.append(f"{encoded_key}={encoded_val}")
return "&".join(items)
def _cos_v1_auth(method, pathname, query_params, headers, secret_id, secret_key, key_time):
"""Generate COS V1 authorization header."""
sign_key = hmac.new(secret_key.encode("utf-8"), key_time.encode("utf-8"), hashlib.sha1).hexdigest()
query_str = _obj2str(query_params, True)
headers_str = _obj2str(headers, True)
format_string = f"{method}\n{pathname}\n{query_str}\n{headers_str}\n"
format_string_sha1 = hashlib.sha1(format_string.encode("utf-8")).hexdigest()
string_to_sign = f"sha1\n{key_time}\n{format_string_sha1}\n"
signature = hmac.new(sign_key.encode("utf-8"), string_to_sign.encode("utf-8"), hashlib.sha1).hexdigest()
q_header_list = ";".join(_get_object_keys(headers)).lower()
q_url_param_list = ";".join(_get_object_keys(query_params)).lower()
return "&".join([
"q-sign-algorithm=sha1",
f"q-ak={secret_id}",
f"q-sign-time={key_time}",
f"q-key-time={key_time}",
f"q-header-list={q_header_list}",
f"q-url-param-list={q_url_param_list}",
f"q-signature={signature}",
])
def _guess_content_type(image_path):
content_type, _ = mimetypes.guess_type(str(image_path))
return content_type or "application/octet-stream"
def upload_file_to_cos(image_path, upload_info, use_accelerate=True):
"""
Upload a local file to Tencent COS using temporary credentials from get_upload_info.
Args:
image_path: Local file path
upload_info: Response from get_upload_info API
use_accelerate: Use COS global accelerate endpoint
Returns:
requests.Response
"""
bucket = upload_info["bucketName"]
region = upload_info["region"]
location = upload_info["location"]
secret_id = upload_info["encryptTmpSecretId"]
secret_key = upload_info["encryptTmpSecretKey"]
token = upload_info["encryptToken"]
start_time = upload_info["startTime"]
expired_time = upload_info["expiredTime"]
host = f"{bucket}.cos.accelerate.myqcloud.com" if use_accelerate else f"{bucket}.cos.{region}.myqcloud.com"
key_time = f"{start_time};{expired_time}"
file_size = Path(image_path).stat().st_size
content_type = _guess_content_type(image_path)
headers_for_sign = {
"content-length": file_size,
"content-type": content_type,
"host": host,
}
auth = _cos_v1_auth(
method="put",
pathname=f"/{location}",
query_params={},
headers=headers_for_sign,
secret_id=secret_id,
secret_key=secret_key,
key_time=key_time,
)
upload_url = f"https://{host}/{location}"
with open(image_path, "rb") as f:
resp = requests.put(
upload_url,
data=f,
headers={
"Authorization": auth,
"x-cos-security-token": token,
"Content-Type": content_type,
"Content-Length": str(file_size),
"Host": host,
},
timeout=120,
)
return resp
def upload_image(image_path, use_accelerate=True):
"""
Full pipeline: get upload credentials from Hunyuan3D API -> upload to COS.
Args:
image_path: Local image file path
use_accelerate: Use COS global accelerate endpoint
Returns:
str: resourceUrl for subsequent 3D generation
"""
try:
cookies = load_cookies_from_file()
except FileNotFoundError as exc:
raise RuntimeError("Cookies file not found. Please run 'hunyuan3dweb-login' first.") from exc
api = Hunyuan3DAPIComplete(cookies=cookies)
filename = Path(image_path).name
upload_info = api.get_upload_info(filename)
resp = upload_file_to_cos(image_path, upload_info, use_accelerate)
resp.raise_for_status()
return upload_info["resourceUrl"]