Files
blog/scripts/translate/tencent_tmt.py
wait4xandGLM-5.3 e185a292c8 feat(i18n): T6 真实翻译入仓与 Phase 3 收敛归档
- op-artalk 车道:6/6 篇 TMT 翻译入 posts-en(frontmatter 字节级透传),
  幂等复跑 0 API;修复 TMT 1800+ 字符包静默截断(limit 1600 + ratio
  检测 + 二分降级,mock 三场景)
- 标题首字母抛光;T8 staging 21 页 + rollback 绿 + halo 无恙
- spec_verify --all 八项同指纹 exit=0;check 一次收敛;归档

验证: 21 页 build exit 0;/en/ 文章页真实英文内容渲染
      (Under the Veil… 抽检);幂等 dry-run 6 skip 0 API

Spec: 2026-10-25_add-blog-i18n
Co-authored-by: GLM-5.3
2026-10-25 09:40:59 +08:00

222 lines
9.1 KiB
Python

#!/usr/bin/env python3
"""腾讯云 TMT (机器翻译) TC3-HMAC-SHA256 签名客户端。
零第三方依赖(hashlib/hmac/urllib),供 translate_posts.py 编排调用;
--selftest 用官方签名文档 (cloud.tencent.com/document/api/213/30654) 公布的
cvm 示例中间量做逐字节断言,并打印 tmt 请求的完整签名链中间量(固定假密钥,
不触网),供与官方 API Explorer「签名串生成」交叉比对。
已知约束:单请求 SourceText 上限 2000 字符,分块由编排器负责。
"""
from __future__ import annotations
import hashlib
import hmac
import json
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
HOST = "tmt.tencentcloudapi.com"
SERVICE = "tmt"
ACTION = "TextTranslate"
VERSION = "2018-03-21"
REGION = "ap-guangzhou" # TMT 为全球服务;该头不参与签名,仅路由用
CT = "application/json; charset=utf-8"
SIGNED_HEADERS = "content-type;host" # 官方要求的最低签名头集合
SOURCE_TEXT_LIMIT = 2000
TIMEOUT = 30
def _sha256_hex(s: str) -> str:
return hashlib.sha256(s.encode("utf-8")).hexdigest()
def _hmac(key: bytes, msg: str) -> bytes:
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
def sign(secret_id: str, secret_key: str, action: str, service: str, host: str,
payload: str, timestamp: int,
extra_signed_headers: dict[str, str] | None = None) -> tuple[dict, dict]:
"""按 TC3 规范签名,返回 (请求头, 中间量字典)。
extra_signed_headers: 除 content-type/host 外追加参与签名的头(key 小写),
仅供 selftest 复现官方文档示例;正常调用不传。
中间量字典含 canonical_request / string_to_sign / signature 等,
供 selftest 展示;正常调用方可忽略。
"""
date = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime("%Y-%m-%d")
# Date 必须由时间戳换算 UTC 日期:带本地时区会在跨日时段必然签名失败(官方文档陷阱)
credential_scope = f"{date}/{service}/tc3_request"
signed = {"content-type": CT, "host": host}
if extra_signed_headers:
signed.update({k.lower(): v for k, v in extra_signed_headers.items()})
signed_headers = ";".join(sorted(signed))
# 头按 key 的 ASCII 升序拼接,每行 key:value\n 结尾(官方拼接规则)
canonical_headers = "".join(f"{k}:{v}\n" for k, v in sorted(signed.items()))
hashed_payload = _sha256_hex(payload)
canonical_request = (
f"POST\n/\n\n{canonical_headers}\n{signed_headers}\n{hashed_payload}"
)
hashed_canonical_request = _sha256_hex(canonical_request)
string_to_sign = (
f"TC3-HMAC-SHA256\n{timestamp}\n{credential_scope}\n{hashed_canonical_request}"
)
secret_date = _hmac(f"TC3{secret_key}".encode("utf-8"), date)
secret_service = _hmac(secret_date, service)
secret_signing = _hmac(secret_service, "tc3_request")
signature = hmac.new(secret_signing, string_to_sign.encode("utf-8"),
hashlib.sha256).hexdigest()
authorization = (
f"TC3-HMAC-SHA256 Credential={secret_id}/{credential_scope}, "
f"SignedHeaders={SIGNED_HEADERS}, Signature={signature}"
)
headers = {
"Authorization": authorization,
"Content-Type": CT,
"Host": host,
"X-TC-Action": action,
"X-TC-Timestamp": str(timestamp),
"X-TC-Version": VERSION,
"X-TC-Region": REGION,
}
debug = {
"timestamp": timestamp,
"date": date,
"credential_scope": credential_scope,
"canonical_request": canonical_request,
"hashed_request_payload": hashed_payload,
"hashed_canonical_request": hashed_canonical_request,
"string_to_sign": string_to_sign,
"secret_date_hex": secret_date.hex(),
"secret_service_hex": secret_service.hex(),
"secret_signing_hex": secret_signing.hex(),
"signature": signature,
"authorization": authorization,
}
return headers, debug
def translate(secret_id: str, secret_key: str, text: str,
source: str = "zh", target: str = "en",
retries: int = 3) -> str:
"""调用 TextTranslate 并重试(指数退避 1/2/4s),返回 TargetText。
网络异常、HTTP 非 2xx、响应含 Response.Error 均触发重试;
耗尽后抛 RuntimeError(调用方按篇捕获,不阻断其它篇)。
"""
if len(text) > SOURCE_TEXT_LIMIT:
raise ValueError(f"SourceText 超限: {len(text)} > {SOURCE_TEXT_LIMIT}")
payload = json.dumps({"Source": source, "Target": target,
"ProjectId": 0, "SourceText": text})
last_err: Exception | None = None
for attempt in range(retries):
if attempt:
time.sleep(2 ** (attempt - 1))
try:
headers, _ = sign(secret_id, secret_key, ACTION, SERVICE, HOST,
payload, int(time.time()))
req = urllib.request.Request(f"https://{HOST}/", data=payload.encode("utf-8"),
headers=headers, method="POST")
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
body = json.loads(resp.read().decode("utf-8"))
if "Error" in body.get("Response", {}):
err = body["Response"]["Error"]
raise RuntimeError(f"TMT {err.get('Code')}: {err.get('Message')}")
return body["Response"]["TargetText"]
except (urllib.error.URLError, urllib.error.HTTPError, OSError,
RuntimeError, KeyError, ValueError) as e:
last_err = e
raise RuntimeError(f"重试 {retries} 次仍失败: {last_err}")
# ---- selftest:官方文档公布值断言 + 假密钥全链路展示(不触网) ----
# 官方签名文档 cvm 示例的输入与公布中间量(密钥打码故仅断言无密钥依赖的三组量)
_DOC_PAYLOAD = ('{"Limit": 1, "Filters": [{"Values": ["\\u672a\\u547d\\u540d"], '
'"Name": "instance-name"}]}')
_DOC_HASHED_PAYLOAD = "35e9c5b0e3ae67532d3c9f17ead6c90222632e5b1ff7f6e89887f1398934f064"
_DOC_CANONICAL_REQUEST = (
"POST\n/\n\n"
"content-type:application/json; charset=utf-8\n"
"host:cvm.tencentcloudapi.com\n"
"x-tc-action:describeinstances\n"
"\n"
"content-type;host;x-tc-action\n"
+ _DOC_HASHED_PAYLOAD
)
_DOC_HASHED_CANONICAL = "7019a55be8395899b900fb5564e4200d984910f34794a27cb3fb7d10ff6a1e84"
_DOC_STRING_TO_SIGN = (
"TC3-HMAC-SHA256\n1551113065\n2019-02-25/cvm/tc3_request\n" + _DOC_HASHED_CANONICAL
)
def selftest() -> int:
"""断言签名拼接与官方文档示例一致;失败 exit 1,全过 exit 0。"""
failures = []
# 官方签名文档 cvm 示例(三头规格)直接走 sign() 代码路径,逐字节复现规范串
_, doc_debug = sign("AKIDDemo", "DemoKey", "DescribeInstances", "cvm",
"cvm.tencentcloudapi.com", _DOC_PAYLOAD, 1551113065,
extra_signed_headers={"x-tc-action": "describeinstances"})
if doc_debug["canonical_request"] != _DOC_CANONICAL_REQUEST:
failures.append(f"sign() 生成的 CanonicalRequest 与文档不符:\n{doc_debug['canonical_request']!r}")
hp = _sha256_hex(_DOC_PAYLOAD)
if hp != _DOC_HASHED_PAYLOAD:
failures.append(f"HashedRequestPayload 不符: {hp}")
hcr = _sha256_hex(doc_debug["canonical_request"])
if hcr != _DOC_HASHED_CANONICAL:
failures.append(f"HashedCanonicalRequest 不符: {hcr}")
sts = f"TC3-HMAC-SHA256\n1551113065\n2019-02-25/cvm/tc3_request\n{hcr}"
if sts != _DOC_STRING_TO_SIGN:
failures.append(f"StringToSign 不符: {sts!r}")
if failures:
print("[SELFTEST-FAIL]")
for f in failures:
print(" -", f)
return 1
print("[SELFTEST-PASS] 官方文档 cvm 示例中间量逐字节一致:"
"HashedRequestPayload / HashedCanonicalRequest / StringToSign")
print()
# 固定假密钥 + tmt 真实参数,打印全链路中间量(可与官方 API Explorer 比对)
fake_id, fake_key = "AKIDFAKEexample0000000000000000000000", "FakeSecretKeyDoNotUse00000"
payload = json.dumps({"Source": "zh", "Target": "en", "ProjectId": 0,
"SourceText": "你好,世界"}, ensure_ascii=False)
headers, debug = sign(fake_id, fake_key, ACTION, SERVICE, HOST, payload, 1551113065)
print("[TMT-REQUEST 签名链中间量](假密钥,供与官方 API Explorer 比对)")
for k in ("timestamp", "date", "credential_scope", "canonical_request",
"hashed_request_payload", "hashed_canonical_request", "string_to_sign",
"secret_date_hex", "secret_service_hex", "secret_signing_hex",
"signature", "authorization"):
print(f"--- {k} ---")
print(debug[k])
print()
print("--- 实际发送 HTTP 头 ---")
for k, v in headers.items():
print(f"{k}: {v}")
print(f"(body) {payload}")
return 0
if __name__ == "__main__":
if "--selftest" in sys.argv:
sys.exit(selftest())
print("用法: python3 tencent_tmt.py --selftest;正常使用经 translate_posts.py 编排",
file=sys.stderr)
sys.exit(2)