翻译任务完整执行流程详解(GitHub 文档翻译工具项目)

翻译逻辑本身简单,问题都出在流程上:后台脚本拿到配置、拉取文件、调一次模型接口、把译文写回去,就算完成。上线跑了一周,问题全冒出来了——用户提交三个仓库的翻译任务,脚本并行拉起后互相抢占资源;翻译到一半断网,模型返回超时,整个任务从数据库里直接消失,谁也不知道丢到哪一步;更麻烦的是有人误提交了没翻译完的文件,PR 打开后文档结构直接乱掉。我意识到翻译本身只是最外层能力,真正难的是把”用户提交配置”到”PR 创建完成”这一段路径做成可观测、可断点恢复的流水线。最后我把整条链路拆成七个阶段:仓库发现、文件扫描、任务拆解、片段翻译、术语校对、合并与渲染、提交 PR。每一步都有明确的输入、输出、状态与失败处理,整体目标是”断点可恢复、可观测、可回滚”。

一、流程总览

在动手写代码之前,我先花了一整天把全链路画成一张状态图贴在工位墙上。原因很实际:这个工具后续要接 Webhook 自动翻译、要支持多人同时使用,如果没有一张全局图,任何一个人改一处状态流转,都可能把别人的流程搞坏。而且做断点恢复的前提就是”知道当前走到哪一步”,状态图就是恢复的依据。

用户提交配置
   ↓
仓库与文件扫描
   ↓
创建 TranslationJob(pending)
   ↓
拆分为 TranslationSegment(pending)
   ↓
拉取文件内容,按 Markdown 边界切分
   ↓
调度 AI 翻译:PENDING → RUNNING → DONE / FAILED
   ↓
合并片段,运行 Markdown 校验
   ↓
创建分支、提交译文
   ↓
打开 PR,任务状态 → completed

这张图也帮我提前发现了两个隐患:一是”创建任务”和”拆分为片段”如果合并成一步,大仓库一次要拉几千个文件,事务会长时间持锁;二是翻译阶段如果不给片段单独的状态,中断恢复就无从谈起。所以我把这两步刻意拆开,后面所有代码都围绕这张图落。整条链路的落地细节见下一节。

二、每一步详解

七步流程里,我踩过的坑大多集中在第二步到第五步。接下来按顺序讲每一步的输入、输出、状态与失败处理,代码片段只保留关键部分。

2.1 仓库与文件扫描

扫描这步我一开始用的是 Personal Access Token(PAT),结果接入组织仓库时发现 PAT 的权限粒度太粗,要么能读整个组织所有仓库,要么就什么都读不到,安全边界很难卡。后来换成 GitHub App 的 Installation Token,权限按安装到单个仓库来控制,正好匹配”用户勾选哪个仓库就只碰哪个仓库”的产品逻辑。

输入:installationId、用户勾选范围。

  • 用 Installation Token 调 GitHub API 拉仓库列表;
  • 对目标仓库按规则(content/**、docs/**、自定义 glob)拉树;
  • 过滤:跳过二进制、锁文件、已有目标语言文件。

输出:RepoFile[],含 path、sha、size。

这里最容易翻车的是过滤逻辑。一开始只过滤了二进制文件,结果把 package-lock.json、pnpm-lock.yaml 这些锁文件也当成文档送进了翻译队列,模型把哈希串改得面目全非。后来加上”锁文件一律跳过”的规则,翻译质量立刻上来了。sha 字段也值得存下来,后面做增量翻译就靠它判断文件有没有变过。

2.2 创建任务

为什么要把任务落进数据库而不是直接开一个后台 Promise?我第一版就是直接跑 Promise,结果用户关掉页面、服务重启,任务跟着内存一起没了。改用 Prisma 落库后,任务状态是持久的,任何时刻进程挂掉都能从库里找到恢复点。

const job = await prisma.translationJob.create({
  data: {
    repositoryId,
    sourceLang,
    targetLang,
    modelKey: "main",
    status: "PENDING",
  },
});

这段解决的是”任务从哪来、归谁管”的问题。落库之后有个额外的好处:PENDING 状态的 job 可以被人为重新拉起,运维时想重跑某个任务,改一行状态就行,不需要改任何业务代码。要注意的是 status 字段要设默认值,避免并发创建时读到空值。

2.3 拆分为片段

拆分策略直接决定翻译质量和失败成本。我最早按字符长度硬切,比如每 1000 字符一段,结果一个三行的代码块被拦腰切断,模型把断在中间的一行当成注释翻译了,合并回来语法直接报错。改成按 Markdown 结构切分后才稳定下来:

  • 标题:每个 H1/H2 单独成段;
  • 代码块:单独成段,原样保留;
  • 段落:长度 ≤ 1500 字;
  • Frontmatter:单独成段,仅翻译指定字段。

每个片段记录 sourceHash 用于增量判断。

按结构切分还有一重收益:每个片段的”语义边界”是完整的,模型不会拿到半个代码块或半个标题,翻译的上下文更干净。sourceHash 用来做增量判断,只有文件内容真的变了,对应的片段才会重新进队列,这是后面增量翻译的数据基础。

2.4 翻译调度

片段的状态机是我整段流程里反复改的地方。最初只有”待翻译/已翻译”两态,任务一中断就分不清哪些是翻译到一半的。后来收敛成 PENDING → RUNNING → DONE / FAILED 四个状态,调度器只认状态、不认”感觉”。

await prisma.translationSegment.update({
  where: { id },
  data: { status: "RUNNING", startedAt: new Date() },
});

这段把片段标记为运行中,防止调度器把同一个片段重复派发给两个并发任务。后面接的就是调用 OpenRouter client,失败时按”失败次数”决定重试或切档。切档这个动作我想了很久:主模型连续报错三次,就自动降级到备用模型,同时给片段打上 degraded 标记,宁可质量略降也不让整个任务卡死。

2.5 合并与渲染

翻译完成后所有片段按原始顺序拼回去,合并后必须做:

  1. Markdown 结构校验:标题层级连续;
  2. 链接完整:所有内部链接仍指向可访问路径;
  3. 代码块不变量:fence 数量为偶数;
  4. 术语表约束:专有名词未改变。

这四条校验不是拍脑袋定的,每一条都对应一次线上事故。最典型的是 fence 数量:模型偶尔会在代码块里多补一个反引号,三个变四个,合并后整个文档的代码块全错位,渲染出来的页面乱成一团。链接校验则堵住了”内部链接被翻译后指向不存在的路径”这种问题。校验失败时把任务标记为 review_required,等待人工。

2.6 提交分支与 PR

提交这步我一开始用 Octokit 手动拼参数,代码又长又容易错,后来直接用 gh 命令行,认证和错误处理都能复用现成的,代码量少了三分之一:

git checkout -b docs/translate-{targetLang}-{shortSha}
git add translated/
git commit -m "docs(translate): {targetLang} from {sourceLang} via app"
git push origin HEAD
gh pr create --title "..." --body "..."

这里有个容易踩的坑:commit message 里的 via app 前缀不能随便改,后面 Webhook 自动翻译的防循环逻辑就靠识别这段前缀来判断”这个 commit 是不是我们自己提交的”。PR 描述里附上任务链接、变更文件数、模型与版本,方便用户拿到 PR 后快速判断要不要合。

三、断点恢复

任务可能因任何原因中断:模型接口超时、服务器部署重启、GitHub API 限流。我第一版没有恢复机制,结果一次线上部署把三个进行中的任务全弄丢了,用户反馈翻译到一半的文档永远停在半截。要做到可恢复:

  • 片段状态字段细分:pending、running、done、failed;
  • 启动时扫描所有 running 且 startedAt < now - 5min 的片段,标记 failed 并允许重试;
  • PR 创建前再次校验,避免重复创建。

5 分钟这个阈值是我调过的:设太短,模型推理稍慢就被误杀重试;设太长,僵尸任务会一直占着配额。按当前模型档位的平均耗时,5 分钟能覆盖绝大多数正常翻译。PR 创建前再校验一次,防止网络抖动导致重复开 PR。

四、并发与速率

并发策略是我跟云资源较劲最多的地方。刚开始图省事,一个仓库的所有片段全部并行,结果 OpenRouter 限额被打爆,失败率直线上升。后来收敛成下面的约束:

资源 限速
GitHub API Installation 15000/h
OpenRouter 按模型档位限额
任务并发 同一仓库内顺序,不同仓库并行
数据库写 短事务,避免长锁

同一仓库内顺序执行、不同仓库并行,这个组合是我对比了几种方案后选中的:仓库内顺序保证了翻译结果不互相覆盖,跨仓库并行保证了用户同时提交多个任务时不用排队。GitHub API 用 Installation Token 能拿到安装级配额,实测日常扫描加建 PR 完全够用。数据库写全走短事务,避免大仓库扫描时长时间持锁拖垮其他用户。

五、可观测性

没有指标的日子我记忆深刻:用户报”翻译好慢”,我连是哪一步慢都说不出来。补齐埋点后,问题定位从猜变成查。每个阶段都要有日志与指标:

  • 任务耗时分布:扫描、翻译、提交各自 P50/P99;
  • 失败原因分类:网络、模型、配额、校验;
  • 成本:input/output token、模型、用户维度。

成本维度是我后来加的。OpenRouter 按 token 计费,没有成本归因,月底对账根本说不清钱花在哪。现在每次任务结束都把 token 用量按用户维度落表,谁用的多、哪个模型贵,一眼就能看出来。

六、回滚与撤销

回滚策略我坚持一个原则:工具只负责”提出建议”,不替用户做破坏性操作。删除 PR、撤销任务、保留草稿,三级动作的破坏力依次递减:

  • 删除 PR:调用 GitHub API 关 PR 并删除分支;
  • 撤销任务:把状态改 cancelled,不再调度;
  • 用户主动撤销:保留已生成译文到草稿,不直接推回 GitHub。

最后一档是我反复斟酌过的。用户点了撤销,其实很多时候只是想停一下,译文如果直接丢进回收站就浪费了。保留到草稿区,用户随时可以重新拉起,这个设计被不少用户表扬过。

七、与 Webhook 的关系

Webhook(push 事件)触发自动翻译时,仍走相同七步流程,区别在于:

  • 来源是 Webhook 而不是用户表单;
  • 默认开启增量翻译(仅翻译变更文件);
  • 提交 PR 时标注 auto-translate。

共用同一套流程的最大好处是维护成本低:修一次断点恢复的逻辑,手动翻译和自动翻译同时受益,不需要维护两套调度器。

八、边界场景

边界场景是我在正式发布前专门用一整天”找茬”列出来的,每一条都对应过真实事故:

  • 仓库巨大:拆成多个 job,避免单 job 超时;
  • 大量代码块:跳过翻译但保留结构;
  • 用户取消:立即停止调度,但允许已翻译片段保留;
  • 模型宕机:降级到备用模型,标记 degraded 状态。

到这里,翻译任务从提交到 PR 的完整链路就讲完了。七步流程的核心不在翻译本身,而在状态可恢复、过程可观测、操作可回滚,把这三点守住,工具才敢交给真实用户去用。

常见问题(FAQ)

Q1:任务被中断后从哪里继续?

从最后一个未完成的片段继续,状态由数据库记录。

Q2:失败任务要不要重试?

按失败类型:网络和限流自动重试,校验失败转人工。

Q3:自动翻译能关闭吗?

可以,仓库级和用户级都有开关。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部