你创建了一个预配任务,并拿到了一个202:一个引擎 ID,以及 status: "in_progress"。现在,AI 代理正在后台抓取你的源内容,并将品牌语调、术语表条目和指令应用到这个引擎上。这个过程可能很快,也可能需要一些时间,取决于它要抓取多少链接。你当然可以保持一个实时 WebSocket连接,实时看它运行——但你大概并不想只是为了知道代理何时完成、以及它到底构建了什么,就一直维持着连接不断开。
Webhook 就是为此而生。创建任务时,如果你传入 callbackUrl,Lingo 会在任务结束的第一时间将最终结果 POST 到该 URL——引擎一准备好,你就会收到通知,并拿到已构建内容的完整清单。 成功完成的任务会以 provisioning.completed 的形式送达,其中包含 AI 创建的每一条记录的摘要。失败的任务则会以 provisioning.failed 的形式送达,并附带失败原因。无论结果如何,你的配置流程都会收到通知,无需主动查询。
本页会介绍这两种负载,以及该如何处理它们。投递带有签名,也会自动重试——这套机制与本地化共用,统一说明见Webhook 签名验证页面;在你会用到的地方,这里都会附上对应链接。
本页内容
投递如何运作#
一个预配任务只会结束一次。它一旦进入终态——要么所有源都已抓取并分析完成,要么运行被放弃——结果就会以单个 POST 的形式投递到你的 callbackUrl。本地化分组会拆成每个目标语言区域一个任务,因此每个任务都会各自回调;而预配任务就是一个任务,所以也只会有一次投递。
在创建任务时,通过 callbackUrl 设置投递目标。线上会传输两种负载格式,通过它们的 type 字段区分:provisioning.completed 和 provisioning.failed。两者都会标明所属的 jobId 和 engineId,因此同一个处理器就可以根据 type 做路由,并更新正确的记录。
仅支持 HTTPS
callbackUrl 必须使用 HTTPS。创建任务时,如果提供的是 HTTP URL,就会被拒绝——Webhook 是带签名的,而通过明文传输已签名负载,就失去了签名本身的意义。
优雅处理未知事件类型
目前线上传输的是 provisioning.completed 和 provisioning.failed。请把事件类型集合视为开放的——处理你已知的类型,忽略其余类型,这样未来新增事件类型时,也不会破坏已部署的处理器。
completed 负载#
任务完成后,负载会携带 summary——也就是你通过读取任务本应拿到的同一份清单,只不过现在是主动推送给你,而不是靠你轮询。它会列出 AI 在你的引擎上创建的每个品牌语调、术语表条目和指令,并列出过程中遇到的所有逐项失败。
{
"type": "provisioning.completed",
"jobId": "pjb_A1b2C3d4E5f6G7h8",
"engineId": "eng_X1y2Z3a4B5c6D7e8",
"summary": {
"brandVoices": { "count": 3, "ids": ["bv_A1b2C3d4", "bv_B2c3D4e5", "bv_C3d4E5f6"] },
"glossaryItems": { "count": 12, "ids": ["gi_A1b2C3d4", "..."] },
"instructions": { "count": 5, "ids": ["ins_A1b2C3d4", "..."] },
"errors": []
}
}| 字段 | 说明 |
|---|---|
type | provisioning.completed |
jobId | 已完成的预配任务(前缀为 pjb_) |
engineId | 它所配置的引擎(前缀为 eng_) |
summary | AI 在引擎上创建的内容——各组件的数量和 ID,以及 errors 中的逐项失败 |
summary 与任务中携带的是同一个对象,而它逐字段的含义——每个组件是什么、条目如何映射到语言区域、哪些内容会落入 errors——统一记录在 AI 会提取什么 页面。这里你只需要知道:completed 负载会把代理构建出的所有内容 ID 直接交给你,这样你的处理器就可以直接记录它们,或在仪表盘中展示它们,而无需重新获取任务。
即使 errors 数组非空,也仍会以 completed 送达。
逐项失败不会让整个任务失败。如果某个源无法抓取,或者某条记录无法创建,它会被写入 summary.errors,其余内容仍会应用到引擎上——而负载依然是 provisioning.completed,不是 provisioning.failed。completed 事件表示任务已完整跑完;请查看 errors 了解需要修复的内容。只有当整个运行完全没有产出任何可用引擎时,才会发送 provisioning.failed 负载。
failed 负载#
当一次运行没有产出任何可用结果时,预配任务就会失败——例如,所有源都抓取失败,导致代理没有任何内容可供分析。即便如此,你仍然会收到通知。该负载类型为 provisioning.failed,并且会用一个 error 字符串来替代摘要:
{
"type": "provisioning.failed",
"jobId": "pjb_A1b2C3d4E5f6G7h8",
"engineId": "eng_X1y2Z3a4B5c6D7e8",
"error": "All sources failed to crawl. No content available for analysis."
}| 字段 | 说明 |
|---|---|
type | provisioning.failed |
jobId | 失败的预配任务 |
engineId | 已创建但未完成配置的引擎 |
error | 说明任务为何无法完成的可读原因 |
细心的读者此时很可能会问:如果任务失败了,我是不是连引擎也一起丢了? 不会。此负载中的 engineId 与你在 202 中收到的是同一个引擎——它依然存在,并且在你发起调用的那一刻就已经创建好了,只是没有加上那次失败运行原本会补上的配置。失败只会让你失去提取结果,不会让你失去引擎。你可以调整提交的内容后重试,或者直接在仪表盘里手动配置该引擎。当任务因抓取失败时,问题通常出在源本身——源类型 页面介绍了什么样的源才值得接入。
如何处理 Webhook#
细心的读者看到这里,第一个反应其实是对的:我的处理器要做真正的工作——写数据库、发通知、刷新仪表盘——那不会让连接一直开着,最后把 Webhook 拖到超时吗?
会,所以别让 Lingo 等。先返回 200,再处理。 先确认已收到,再在响应发出后做真正的工作。完整的投递约定——为什么要先确认,以及如果不这么做,后续会按怎样的计划重试——都在 签名与投递 页面;下面的处理器示例展示了它在预配负载里的基本形态。
app.post("/webhooks/provisioning", verifyWebhook, async (req, res) => {
// Acknowledge first - the job ends once, so this fires once.
res.status(200).send("ok");
const { type, jobId, engineId } = req.body;
if (type === "provisioning.completed") {
const { summary } = req.body;
await db.engines.update({
where: { engineId },
data: {
status: "ready",
brandVoiceCount: summary.brandVoices.count,
glossaryCount: summary.glossaryItems.count,
instructionCount: summary.instructions.count,
},
});
}
if (type === "provisioning.failed") {
console.error(`Provisioning failed: ${jobId} (${engineId})`, req.body.error);
await db.engines.update({
where: { engineId },
data: { status: "needs_configuration" },
});
}
});本页唯一没有展开定义的是 verifyWebhook 中间件。每次投递都遵循 Standard Webhooks 规范进行签名——三个请求头、基于原始请求体计算的 HMAC,以及一个 whsec_ 密钥,该密钥会在你第一次提交带回调的任务时生成。预配和本地化回调都原样使用这套方案,因此相关说明统一放在 Webhook 签名验证 页面。请务必在信任负载之前先接入这层中间件——未经验证的请求体,就是未经认证的请求体。
先验证,再信任请求体
你的端点是一个公开 URL;任何人都可以向它 POST。在对任何负载采取行动之前——例如把某个引擎标记为就绪,或存储它声称已创建的 ID——都要先根据原始请求体验证签名。具体做法——请求头、HMAC,以及 whsec_ 密钥——请参见 签名验证 页面。
什么时候不该用投递#
Webhook 是一种推送层面的便利机制,不是系统记录的唯一来源。有两种情况更适合用别的方式,而且都只差一个链接。
如果结果投递时你的端点不可用,平台会按照 Lingo 所有 Webhook 共用的同一重试计划继续重试——而结果并不会被困在回调里。AI 创建的记录就是引擎的真实配置;completed 摘要只是对已经发生在真实引擎上的工作的报告,并不是唯一副本。所以,一段时间的宕机只会让你错过通知,不会让你失去引擎。重试计划本身请参见 签名与投递 页面。
如果你真正想要的是在引擎配置期间获得实时进度——比如在 UI 中展示“先抓取、再配置”的状态,而不是在结束时只向你的服务器发一次回调——那你需要的是预配任务 WebSocket,而不是 Webhook。它会在连接建立时推送快照,并在运行推进过程中持续发送进度事件;你可以在任何时候连接,即使任务已经结束也没问题。
