你已经创建了一个任务组,并在几毫秒内收到了 202 响应。翻译现在正在后台运行,每个语言区域对应一个独立任务。你当然可以轮询每个任务直到它完成——但你没必要只为了确认德语是否就绪,就专门维护一套轮询逻辑。你真正需要的,是每个语言区域一完成,服务器就立刻收到通知。
这正是 webhook 的作用。创建任务时传入 callbackUrl 后,Lingo.dev 会在每个任务进入终态时,将结果 POST 到这个 URL——每个语言区域一条 POST,完成即送达。 翻译顺利完成的语言区域会以 translation.completed 的形式携带数据送达;翻译失败的语言区域则会以 translation.failed 的形式携带错误送达。不管成功还是失败,你都会按语言逐一收到通知,无需主动查询。
本页会介绍这两种负载,以及该如何处理它们。投递的签名与重试机制和预配共用,相关细节都在 Webhook 签名验证 页面里;在你会用到的地方,我们也都附上了链接。
本页内容
投递如何工作#
组里的每个语言区域都是一个独立任务。只要其中任意一个进入终态,结果就会单独投递到你的 callbackUrl——Lingo.dev 不会等最慢的那个语言区域,也不会把整个任务组打包成一次调用。目标语言区域有十四个,就最多会收到十四条 POST;每种语言各自在完成时送达,先后顺序取决于它们实际完成的时间。
你可以在创建任务组时,通过请求级的 callbackUrl 指定投递地址;也可以在控制台中设置组织级默认值,让所有任务组继承。某个任务组上单独传入的 callbackUrl 会覆盖组织默认值。
仅支持 HTTPS
callbackUrl 必须使用 HTTPS。若你传入的是 HTTP URL,创建任务时会直接返回 400——webhook 本身带签名,而在明文链路上传送签名负载就失去意义了。
线上传输的负载有两种格式,通过它们的 type 字段区分:translation.completed 和 translation.failed。两者都会标明所属任务、任务组以及对应的语言区域,因此同一个处理器只需根据 type 做路由,就能更新正确的记录。
优雅处理未知事件类型
当前线上会发送 translation.completed 和 translation.failed。请把这个集合视为开放的——对已知类型按分支处理,其他类型直接忽略即可。这样即使未来新增事件类型,也不会把已部署的处理器搞挂。
成功完成时的负载#
任务成功完成时,负载里会带上翻译后的 data——结构与你通过获取任务拿到的结果完全一致,只不过这里不是你去轮询,而是系统主动推送给你。data 会完整映射你提交时的结构:所有字符串都会被翻译,所有非字符串值(数字、布尔值、null)都会原样保留,嵌套结构也不会变。
{
"type": "translation.completed",
"jobId": "ljb_A1b2C3d4E5f6G7h8",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"sourceLocale": "en",
"targetLocale": "de",
"data": {
"id": "course_101",
"title": "Einführung in maschinelles Lernen",
"steps": [
{ "heading": "Was ist ML?", "body": "Maschinelles Lernen ist ein Teilbereich der künstlichen Intelligenz." },
{ "heading": "Überwachtes Lernen", "body": "Trainieren eines Modells mit gelabelten Daten." }
],
"metadata": { "author": "Dr. Smith", "difficulty": "beginner" }
}
}| 字段 | 说明 |
|---|---|
type | translation.completed |
jobId | 已完成的任务(前缀为 ljb_) |
groupId | 所属任务组(前缀为 ljg_) |
sourceLocale | 你提交的源语言区域 |
targetLocale | 该负载所对应的目标语言区域 |
data | 翻译后的内容,结构与你提交的 data 保持一致 |
只要任务产出了结果,就不算失败——所以即使某个任务最终状态是 completed_with_warnings(已经产出结果,但某个可选的 pipeline 阶段发生了回退),它仍会以 translation.completed 的形式投递,并附带可用的 data。Webhook 只负责告诉你这个语言区域已经就绪;至于解释回退原因的逐步警告,则记录在 单个任务 里,你需要时可以通过 jobId 获取。
失败时的负载#
单个语言区域也可能失败——比如模型超时,或者所有已配置模型都不可用。当任务进入 failed 时,你依然会收到通知。此时负载类型为 translation.failed,并且会用一个 error 字符串来替代 data:
{
"type": "translation.failed",
"jobId": "ljb_C3d4E5f6G7h8I9j0",
"groupId": "ljg_A1b2C3d4E5f6G7h8",
"sourceLocale": "en",
"targetLocale": "ja",
"error": "Model timeout after 30 seconds"
}| 字段 | 说明 |
|---|---|
type | translation.failed |
jobId | 失败的任务 |
groupId | 所属任务组 |
sourceLocale | 你提交的源语言区域 |
targetLocale | 失败的目标语言区域 |
error | 便于人工阅读的失败描述 |
失败只影响单个语言区域。比如你提交了 de、fr 和 ja,其中某个 ja 失败了,它会作为一条独立的 translation.failed POST 送达;而 de 和 fr 仍会以 translation.completed 的形式送达——德语和法语翻译照常交付。任务组的部分失败状态会反映这种混合结果。若要恢复失败的语言区域,请仅针对该语言区域重新提交一个新任务,并使用新的幂等键。
如何处理 webhook#
一个谨慎的读者看到这里,第一反应往往是对的:我的处理器要做真正的业务操作——写数据库、刷新缓存、给已连接客户端做扇出——那岂不是会让连接一直挂着,最后把 webhook 拖到超时?
会,所以别让 Lingo.dev 等。先返回 200,再处理。 先立即确认已收到请求,等响应发出后再做真正的业务处理。处理器返回得快,投递就更稳;如果一直阻塞在下游操作上,就会引来原本完全没必要的重试。
app.post("/webhooks/translations", verifyWebhook, async (req, res) => {
// Acknowledge first - one POST per locale, the moment it lands.
res.status(200).send("ok");
const { type, jobId, groupId, targetLocale, data } = req.body;
if (type === "translation.completed") {
await db.content.update({
where: { groupId },
data: { [`content_${targetLocale}`]: data },
});
// Advance your own progress model - your UI can poll this or receive it over SSE.
await db.translationProgress.increment({
where: { groupId },
data: { completedLanguages: { increment: 1 } },
});
}
if (type === "translation.failed") {
console.error(`Translation failed: ${jobId} (${targetLocale})`, req.body.error);
}
});这页唯一没有展开定义的是 verifyWebhook 中间件。每次投递都会遵循 Standard Webhooks 规范进行签名,所以你不需要自己去反向摸索协议。如何校验签名,以及非 2xx 响应对应的重试节奏,都已在 Webhook 签名验证 页面完整说明,并与预配共用。在你信任任何负载之前,务必先把这层中间件接上:未经验证的请求体,本质上就是未经认证的请求体。
先验证,再信任请求体
你的端点是公开 URL;任何人都可以向它发起 POST。在根据任何负载采取动作之前,先基于原始请求体完成签名校验。具体做法——请求头、HMAC,以及 whsec_ 密钥——都在 签名验证 页面里。
什么时候不该用投递#
Webhook 是一种推送上的便利,不是你的记录事实来源。遇到下面两种情况,更合适的工具另有其人,而且都只差点开一个链接。
如果结果投递时你的端点正好不可用,平台会自动重试——即使所有重试都用尽,结果也不会丢。它仍然可以通过 jobId 获取;任务的 callbackStatus 会记录这次推送最终是否成功。至于具体的重试节奏,见 签名与投递 页面。Webhook 在大多数情况下帮你省掉轮询;而在少数异常情况下,底层始终都有任务记录可查。
如果你真正想要的是 UI 里的实时进度——比如随着语言区域陆续完成,计数器从 14 个里的 3 个跳到 4 个——而不是按语言区域向你的服务器发送回调,那你需要的是任务组 WebSocket,而不是 webhook。
