fix: 补全 dashscope-image-gen 和 xiaomi-tts 的 i18n CI 校验 (#4)

## 变更说明

修复 dashscope-image-gen 和 xiaomi-tts 的 i18n CI 校验、补全英文翻译,并连带修复其他 stale
skill 的 source_hash 漂移问题。

### dashscope-image-gen / xiaomi-tts(PR 主线)
- `name` 字段从中文改为目录名(CI rule-1 要求 lowercase ASCII + hyphens)。
- 补全 `metadata.i18n` 块:`locales`、`zh-CN` (含 body 指向
SKILL.zh-CN.md)、`en-US`(含 description / body=./SKILL.md)。
- 新增 `SKILL.zh-CN.md`(zh-CN body 文件)。
- **root SKILL.md 改写为英文 body**(与 SKILL.zh-CN.md 内容对应),由本 PR
手工翻译;`default_locale=en-US`、`source_locale=zh-CN`,与 docs/I18N.md
约定一致:root SKILL.md = default_locale body (en-US)、SKILL.zh-CN.md =
source_locale body (zh-CN)。
- 两 locale 锁为 `translated_by: human` + 正确 `source_hash`。
- 内容质量修复:流程标题 "严格按此两步执行" 改为 "严格按此三步执行";强制规则 2 措辞精确化(/tmp
仅作中转);xiaomi-tts 用户意图映射表中 `response_format` 改为 `audio.format`
与请求体参数表一致;zh-CN.description 改为纯中文。
- locale header 由 shell 转义残留 `<\!--` 修正为标准 `<!-- locale: zh-CN -->`。

### 连带:6 个 main 上已 stale 的 skill(避免 translate workflow 失败)
- `manage-skills` / `minimax-music-gen` / `minimax-video-gen` /
`skill-creator` / `web-access`:`en-US.source_hash` 重新计算为当前 zh-CN source
实际 hash;`translated_by` 由 `ai:claude-opus-4-7` 改为 `human`
以锁定现有翻译不被自动重译覆盖。
- `markdown`:补正 `en-US.source_hash`(之前是占位 `sha256:0000000000000000`)。
- 这些 skill 的 `en-US` 翻译内容保持不变,仅修正元数据。

### scripts/i18n/translate.py 容错增强
- 413 Payload Too Large 时不再 retry(payload 不会变小,retry 浪费时间)。
- 主循环 catch RuntimeError,把单个 skill 的失败写入 `plan["errors"]` 后继续处理下一个
skill,避免一个大文件 fail 整个 workflow。
- `--check` 模式下 plans 含 errors 也 exit 1(之前仅看 needs_translation,broad
except 会把异常吃掉导致误报通过)。

## Test plan

- [x] `i18n-validate` 通过
- [x] `i18n-translate --check` 显示所有 skill `up-to-date` 或 `human-locked,
skipping`
- [x] CI 上 `validate` / `translate` / `wait-for-copilot-review` 全绿
- [ ] Copilot 评审 conversation 全部 resolve
- [ ] Squash merge

---------

Co-authored-by: yi-ge <a@wyr.me>
This commit is contained in:
2026-05-13 12:57:25 +08:00
committed by GitHub
parent b8101406fb
commit 0cb3758669
11 changed files with 562 additions and 163 deletions

View File

@@ -248,6 +248,13 @@ def _post_with_retries(url: str, headers: dict, payload: dict, *, extract) -> st
try:
with httpx.Client(timeout=HTTP_TIMEOUT) as client:
resp = client.post(url, headers=headers, json=payload)
# Don't retry on 413: payload won't get smaller on next attempt.
if resp.status_code == 413:
raise RuntimeError(
f"413 Payload Too Large from {url} — skill body too big for this backend. "
f"Switch backend (TRANSLATE_BACKEND=anthropic), use a model with larger input budget, "
f"or set translated_by: human to lock the locale."
)
if resp.status_code == 429 or resp.status_code >= 500:
raise httpx.HTTPStatusError(f"{resp.status_code}", request=resp.request, response=resp)
resp.raise_for_status()
@@ -499,11 +506,19 @@ def main(argv: list[str]) -> int:
if not (skill_dir.is_dir() and (skill_dir / "SKILL.md").is_file()):
continue
for tl in target_locales:
plans.append(translate_skill(
skill_dir, tl,
check_only=args.check, mark_human=args.human,
backend=backend, model=model, endpoint=endpoint,
))
try:
plans.append(translate_skill(
skill_dir, tl,
check_only=args.check, mark_human=args.human,
backend=backend, model=model, endpoint=endpoint,
))
except Exception as e: # don't let one bad skill abort the entire run
plans.append({
"skill": skill_dir.name,
"target": tl,
"actions": [],
"errors": [f"unhandled exception: {e}"],
})
needs = [p for p in plans if p.get("needs_translation")]
errs = [p for p in plans if p.get("errors")]
@@ -514,7 +529,7 @@ def main(argv: list[str]) -> int:
for p in errs:
for e in p["errors"]:
print(f" ERROR [{p['skill']}/{p['target']}]: {e}")
return 1 if needs else 0
return 1 if (needs or errs) else 0
print(f"Backend: {backend} Model: {model} Endpoint: {endpoint}\n")
for p in plans: