Files
hunyuan3dweb/README_REVERSE_ENGINEERING_CN.md
KawasakiAkasei 5328c213fe feat: add on-demand format conversion support (FBX, STL, USDZ, MP4, GIF)
- 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
2026-05-27 13:42:29 +08:00

9.9 KiB
Raw Permalink Blame History

腾讯混元3D API 逆向工程文档

项目概述

本项目通过逆向工程破解了腾讯混元3Dhttps://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

关键常量:

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]  # 置换表

派生密钥: Hf6d6KFB3D10字符

2. 签名范围(重要发现)

⚠️ 签名只包含 URL 查询参数,不包含请求体数据

浏览器实际请求:

POST /api/3d/creations/generations?timestamp=xxx&nonce=yyy&sign=zzz
Body: {"sceneType":"playGround3D-2.0",...}

签名计算:

# 只签查询参数timestamp + nonce
param_str = "nonce=yyy&timestamp=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 使用
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 请求嗅探工具

使用流程

hunyuan3dweb-login
# 按提示输入邮箱和验证码
# 登录状态自动保存到 ~/.config/hunyuan3dweb/profile
# Cookie 同时导出到 ~/.config/hunyuan3dweb/cookies.txt

登录状态检测逻辑(双重验证):

  • 检查当前页面 URL 是否包含 "login"
  • 检查页面上是否存在"登录"按钮
  • 只有 URL 不含 login 没有登录按钮时,才判定为已登录

2. 纯 Python 生成3D模型

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

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 下载模型

# 列出某个创作所有可用的下载格式
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 转换

转换接口示例

POST /creations/resourceConvert
Body: {
  "sourceResource": [{"format": "zip", "url": "<obj_zip_url>"}],
  "targetFormatList": ["usdz", "fbx"]
}
Response: {"convertResult": [{"format": "usdz", "url": "..."}]}

技术细节

签名算法实现

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 签名(用于直传)

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}",
    ])

请求签名

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的服务条款。请勿用于商业用途或大规模自动化调用。