- Discover /creations/resourceConvert endpoint via browser inspection - Add resource_convert() method to api.py and api_complete.py - Extend MODEL_FORMAT_KEYS with fbx, stl, usdz, mp4, gif - Update get_model_urls() and download_model() with include_converted flag - Update CLI with --converted flag for formats and download commands - Update reverse engineering docs with native vs converted format tables
335 lines
9.9 KiB
Markdown
335 lines
9.9 KiB
Markdown
# 腾讯混元3D API 逆向工程文档
|
||
|
||
## 项目概述
|
||
|
||
本项目通过逆向工程破解了腾讯混元3D(https://3d.hunyuan.tencent.com/)的前端签名算法,实现了**纯 Python HTTP 调用**,无需浏览器环境即可使用图生3D、查询配额等功能。
|
||
|
||
---
|
||
|
||
## 核心成果
|
||
|
||
### 1. 签名算法破解
|
||
|
||
**算法位置**: webpack Chunk 3057, Module 47436
|
||
|
||
**签名流程**:
|
||
```
|
||
1. nonce 生成: 16次 Math.random(),从 62 字符集 (A-Za-z0-9) 选取
|
||
2. timestamp: Math.floor(Date.now() / 1000)(秒级时间戳)
|
||
3. 密钥派生: 硬编码字节数组 → XOR → 循环左移 → 置换 → 截断
|
||
4. 签名: HMAC-SHA256(排序后的查询参数字符串, 派生密钥) → Hex
|
||
```
|
||
|
||
**关键常量**:
|
||
```python
|
||
C = bytes([122, 59, 92, 165, 30, 79, 166, 139, 142, 129, 139, 89, 219, 131, 101, 204])
|
||
D = bytes([122, 59, 92, 45, 30, 79, 106, 139, 156, 13, 46, 63, 74, 91, 108, 125])
|
||
U = [3, 5, 2, 7, 1, 4, 6, 2, 5, 3, 1, 4, 2, 6, 3, 5] # 左移位数
|
||
M = [14, 11, 13, 9, 15, 10, 12, 8, 6, 3, 5, 1, 7, 2, 4, 0] # 置换表
|
||
```
|
||
|
||
**派生密钥**: `Hf6d6KFB3D`(10字符)
|
||
|
||
### 2. 签名范围(重要发现)
|
||
|
||
⚠️ **签名只包含 URL 查询参数,不包含请求体数据**
|
||
|
||
浏览器实际请求:
|
||
```
|
||
POST /api/3d/creations/generations?timestamp=xxx&nonce=yyy&sign=zzz
|
||
Body: {"sceneType":"playGround3D-2.0",...}
|
||
```
|
||
|
||
签名计算:
|
||
```python
|
||
# 只签查询参数(timestamp + nonce)
|
||
param_str = "nonce=yyy×tamp=xxx"
|
||
sign = HMAC-SHA256(param_str, key="Hf6d6KFB3D")
|
||
```
|
||
|
||
### 3. 图片上传方式演进
|
||
|
||
#### 方式一:浏览器自动化上传(原始方式)
|
||
通过 `generator.py` 在浏览器内完成上传,获取腾讯内部资源格式的 `resourceId`。
|
||
|
||
#### 方式二:纯 Python COS 直传(新方式)
|
||
通过 `cos_upload.py` 直接调用腾讯 COS API 上传文件,无需浏览器:
|
||
1. 调用 `/resource/genUploadInfo` 获取临时 STS 凭证(`encryptTmpSecretId` / `encryptTmpSecretKey` / `encryptToken`)
|
||
2. 使用 COS V1 签名算法生成 `Authorization` 请求头
|
||
3. 直接 `PUT` 上传到 `*.cos.accelerate.myqcloud.com`
|
||
4. 获得 `resourceUrl` 供后续图生3D API 使用
|
||
|
||
```python
|
||
from hunyuan3dweb.cos_upload import upload_image
|
||
|
||
resource_url = upload_image("/path/to/image.png")
|
||
```
|
||
|
||
---
|
||
|
||
## 文件说明
|
||
|
||
| 文件 | 说明 |
|
||
|------|------|
|
||
| `hunyuan3dweb/sign.py` | 签名算法纯 Python 实现 |
|
||
| `hunyuan3dweb/api.py` | 基础 API 客户端(图生3D、文生3D) |
|
||
| `hunyuan3dweb/api_complete.py` | 完整 API 客户端(所有生成模式) |
|
||
| `hunyuan3dweb/cos_upload.py` | 纯 Python COS 上传工具(无需浏览器) |
|
||
| `hunyuan3dweb/config.py` | 用户配置路径管理(cookie、profile) |
|
||
| `hunyuan3dweb/cli.py` | CLI 入口 |
|
||
| `hunyuan3dweb/browser/login.py` | 浏览器自动化登录工具 |
|
||
| `hunyuan3dweb/browser/generator.py` | 浏览器自动化图生3D |
|
||
| `hunyuan3dweb/browser/sniffer.py` | API 请求嗅探工具 |
|
||
|
||
---
|
||
|
||
## 使用流程
|
||
|
||
### 1. 登录获取 Cookie
|
||
|
||
```bash
|
||
hunyuan3dweb-login
|
||
# 按提示输入邮箱和验证码
|
||
# 登录状态自动保存到 ~/.config/hunyuan3dweb/profile
|
||
# Cookie 同时导出到 ~/.config/hunyuan3dweb/cookies.txt
|
||
```
|
||
|
||
**登录状态检测逻辑**(双重验证):
|
||
- 检查当前页面 URL 是否包含 `"login"`
|
||
- 检查页面上是否存在"登录"按钮
|
||
- 只有 URL 不含 login **且** 没有登录按钮时,才判定为已登录
|
||
|
||
### 2. 纯 Python 生成3D模型
|
||
|
||
```python
|
||
from hunyuan3dweb import Hunyuan3DAPI
|
||
|
||
# 自动从 ~/.config/hunyuan3dweb/cookies.txt 加载 cookie
|
||
api = Hunyuan3DAPI()
|
||
|
||
# 查询配额
|
||
quota = api.get_quota_info()
|
||
|
||
# 文生3D
|
||
result = api.generate_text("一只可爱的熊猫", title="熊猫模型")
|
||
|
||
# 查询状态
|
||
status = api.get_generation_status(result["creationsId"])
|
||
```
|
||
|
||
### 3. 本地图片上传 + 图生3D(纯 Python)
|
||
|
||
```python
|
||
from hunyuan3dweb.cos_upload import upload_image
|
||
from hunyuan3dweb import Hunyuan3DAPIComplete
|
||
|
||
# 上传本地图片到 COS
|
||
resource_url = upload_image("/path/to/image.png")
|
||
|
||
# 调用图生3D API
|
||
api = Hunyuan3DAPIComplete()
|
||
result = api.generate_from_image(resource_url, title="我的模型")
|
||
```
|
||
|
||
### 4. CLI 下载模型
|
||
|
||
```bash
|
||
# 列出某个创作所有可用的下载格式
|
||
hunyuan3dweb formats <creation_id>
|
||
|
||
# 包含转换格式(fbx, stl, usdz, mp4, gif)
|
||
hunyuan3dweb formats <creation_id> --converted
|
||
|
||
# 下载指定格式(默认 glb)
|
||
hunyuan3dweb download <creation_id> --format glb -o model.glb
|
||
|
||
# 下载转换格式(如 usdz)
|
||
hunyuan3dweb download <creation_id> --format usdz --converted -o model.usdz
|
||
```
|
||
|
||
---
|
||
|
||
## API 端点
|
||
|
||
| 功能 | 方法 | 端点 | 签名范围 |
|
||
|------|------|------|----------|
|
||
| 用户信息 | GET | `/getuserinfo` | 查询参数 |
|
||
| 配额查询 | POST | `/quotainfo` | 查询参数 |
|
||
| 生成3D | POST | `/creations/generations` | 查询参数 |
|
||
| 查询状态 | GET | `/creations/detail` | 查询参数 |
|
||
| 作品列表 | POST | `/creations/list` | 查询参数 |
|
||
| 作品数量 | POST | `/creations/count` | 查询参数 |
|
||
| 取消生成 | POST | `/creations/cancel` | 查询参数 |
|
||
| 资源转换 | POST | `/creations/resourceConvert` | 查询参数 |
|
||
| 上传凭证 | POST | `/resource/genUploadInfo` | 查询参数 |
|
||
| 资源审核 | POST | `/resource/review` | 查询参数 |
|
||
| 创建分享 | POST | `/share` | 查询参数 |
|
||
| 全局配置 | GET | `/config` | 查询参数 |
|
||
| 动画模板 | GET | `/workflow/action/templates` | 查询参数 |
|
||
|
||
### 模型下载格式
|
||
|
||
#### 原生格式(来自 `urlResult`)
|
||
|
||
`/creations/detail` 返回的 `urlResult` 字段包含 14 种原生格式键:
|
||
|
||
| 键名 | 说明 |
|
||
|-----|------|
|
||
| `glb` | 带 PBR 贴图的 GLB 二进制模型 |
|
||
| `obj` | OBJ 模型(通常打包在 zip 中) |
|
||
| `mtl` | MTL 材质文件 |
|
||
| `obj_url` | 独立 OBJ 文件 URL |
|
||
| `geometryGlb` | 纯几何 GLB(无材质) |
|
||
| `textureGlb` | GLB 纹理包 |
|
||
| `textureObj` | OBJ 纹理包 |
|
||
| `image_url` | 预览图/缩略图 |
|
||
| `pbrImage` | PBR 综合贴图 |
|
||
| `pbrMetallicImage` | PBR 金属度贴图 |
|
||
| `pbrRoughnessImage` | PBR 粗糙度贴图 |
|
||
| `pbrNormalImage` | PBR 法线贴图 |
|
||
| `invisible_wall` | 不可见碰撞墙 |
|
||
| `air_wall` | 空气墙(碰撞体) |
|
||
|
||
#### 转换格式(通过 `/creations/resourceConvert`)
|
||
|
||
以下格式由网站按需将 OBJ zip 转换生成:
|
||
|
||
| 键名 | 说明 | 来源 |
|
||
|-----|------|------|
|
||
| `fbx` | Autodesk FBX 格式 | OBJ zip 转换 |
|
||
| `stl` | STL 格式(3D打印) | OBJ zip 转换 |
|
||
| `usdz` | USDZ 格式(iOS AR) | OBJ zip 转换 |
|
||
| `mp4` | MP4 视频格式 | OBJ zip 转换 |
|
||
| `gif` | GIF 动图格式 | OBJ zip 转换 |
|
||
|
||
**转换接口示例**:
|
||
```python
|
||
POST /creations/resourceConvert
|
||
Body: {
|
||
"sourceResource": [{"format": "zip", "url": "<obj_zip_url>"}],
|
||
"targetFormatList": ["usdz", "fbx"]
|
||
}
|
||
Response: {"convertResult": [{"format": "usdz", "url": "..."}]}
|
||
```
|
||
|
||
---
|
||
|
||
## 技术细节
|
||
|
||
### 签名算法实现
|
||
|
||
```python
|
||
def derive_key(c: bytes) -> str:
|
||
"""从硬编码常量派生签名密钥"""
|
||
# 1. XOR with D
|
||
t = bytearray(16)
|
||
for i in range(16):
|
||
t[i] = c[i] ^ D[i]
|
||
|
||
# 2. 循环左移
|
||
o = bytearray(16)
|
||
for i in range(16):
|
||
n = U[i]
|
||
val = t[i]
|
||
o[i] = (val << n | val >> (8 - n)) & 0xFF
|
||
|
||
# 3. 置换
|
||
n = bytearray(16)
|
||
for i in range(16):
|
||
n[i] = o[M[i]]
|
||
|
||
# 4. 截断(找到第一个0字节)
|
||
try:
|
||
r = n.index(0)
|
||
except ValueError:
|
||
r = 16
|
||
|
||
return n[:r].decode('utf-8')
|
||
```
|
||
|
||
### COS V1 签名(用于直传)
|
||
|
||
```python
|
||
def _cos_v1_auth(method, pathname, query_params, headers, secret_id, secret_key, key_time):
|
||
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()
|
||
|
||
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={';'.join(sorted(headers.keys())).lower()}",
|
||
f"q-url-param-list={';'.join(sorted(query_params.keys())).lower()}",
|
||
f"q-signature={signature}",
|
||
])
|
||
```
|
||
|
||
### 请求签名
|
||
|
||
```python
|
||
def sign(params: dict) -> dict:
|
||
result = dict(params)
|
||
result["timestamp"] = int(time.time())
|
||
result["nonce"] = generate_nonce(16)
|
||
|
||
# 排序并拼接(JSON格式用于列表/字典)
|
||
sorted_items = sort_params(result)
|
||
param_str = join_params(sorted_items)
|
||
|
||
# HMAC-SHA256
|
||
key = derive_key(C)
|
||
signature = hmac.new(
|
||
key.encode('utf-8'),
|
||
param_str.encode('utf-8'),
|
||
hashlib.sha256
|
||
).hexdigest()
|
||
|
||
result["sign"] = signature
|
||
return result
|
||
```
|
||
|
||
---
|
||
|
||
## 验证结果
|
||
|
||
| 测试项 | 浏览器签名 | Python 签名 | 匹配 |
|
||
|--------|-----------|-------------|------|
|
||
| 固定参数 | `2214e55a...` | `2214e55a...` | ✅ |
|
||
| 配额查询 | 可用 | 可用 | ✅ |
|
||
| 生成请求 | 可用 | 可用 | ✅ |
|
||
| 状态查询 | 可用 | 可用 | ✅ |
|
||
| COS 直传 | 浏览器上传 | Python 上传 | ✅ |
|
||
|
||
---
|
||
|
||
## 限制与注意事项
|
||
|
||
1. **Cookie 有效期**: 登录状态会过期,需要定期重新登录
|
||
2. **配额限制**: 每个账号有生成配额限制(默认20次/天)
|
||
3. **风控检测**: 频繁调用可能触发风控
|
||
4. **并发安全**: 多个工具同时运行时,`login.py` 独享标准 profile 目录,`sniffer.py` / `generator.py` 会自动复制 profile 到临时目录启动,避免 Chromium `SingletonLock` 冲突
|
||
|
||
---
|
||
|
||
## 依赖
|
||
|
||
```
|
||
requests
|
||
cloakbrowser (用于登录和浏览器自动化)
|
||
```
|
||
|
||
---
|
||
|
||
## 法律声明
|
||
|
||
本项目仅供学习和研究使用。使用本代码需遵守腾讯混元3D的服务条款。请勿用于商业用途或大规模自动化调用。
|