概述
Nuxt 3 使用 Nitro 作为其服务端引擎,提供了基于文件的服务端路由,非常适合作为 Webhook 处理程序。ModelRiver 后端管道的 Webhooks 可以自然地与 Nuxt 的自动导入系统、Composables 和服务端中间件集成。
您将构建的内容:
- 一个用于接收 ModelRiver 发出的 AI 生成数据的 Nitro 服务端路由
- 使用 Nuxt 服务端中间件进行签名验证
- 具有回调 ModelRiver 功能的后台处理逻辑
- 一个用于基于 WebSocket 交付结果的 Vue Composable
快速开始
安装依赖
Bash
npx nuxi@latest init my-ai-appcd my-ai-appnpm install环境变量
Bash
# .envNUXT_MODELRIVER_API_KEY=mr_live_YOUR_API_KEYNUXT_MODELRIVER_WEBHOOK_SECRET=your_webhook_secretTYPESCRIPT
1// nuxt.config.ts2export default defineNuxtConfig({3 runtimeConfig: {4 modelriverApiKey: "",5 modelriverWebhookSecret: "",6 },7});Webhook 处理程序
服务端路由
TYPESCRIPT
1// server/api/webhooks/modelriver.post.ts2import crypto from "crypto";3 4function verifySignature(rawBody: string, timestamp: string, signature: string, secret: string): boolean {5 // 签名字符串:"<timestamp>.<原始请求体中 "data" 值的 JSON 文本>"(切勿重新编码)6 const start = rawBody.indexOf('"data":');7 const end = rawBody.lastIndexOf(',"timestamp":');8 if (start === -1 || end === -1) return false;9 10 const expected = crypto11 .createHmac("sha256", secret)12 .update(`${timestamp}.${rawBody.slice(start + '"data":'.length, end)}`)13 .digest("hex");14 15 const a = Buffer.from(signature);16 const b = Buffer.from(expected);17 return a.length === b.length && crypto.timingSafeEqual(a, b);18}19 20export default defineEventHandler(async (event) => {21 const config = useRuntimeConfig();22 const signature = getHeader(event, "x-modelriver-signature");23 const timestamp = getHeader(event, "x-modelriver-timestamp") ?? "";24 const rawBody = await readRawBody(event);25 26 // 1. 验证 Webhook 签名27 if (!signature || !rawBody || !verifySignature(rawBody, timestamp, signature, config.modelriverWebhookSecret)) {28 throw createError({ statusCode: 401, statusMessage: "Invalid signature" });29 }30 31 const payload = JSON.parse(rawBody).data; // 事件载荷位于 "data" 之下32 const { type, event: eventName, ai_response, callback_url, customer_data } = payload;33 34 // 2. 处理后端管道的工作流35 if (type === "task.ai_generated" && callback_url) {36 // 处理并回调37 await processAndCallback(eventName, ai_response, callback_url, customer_data, config);38 39 return { received: true };40 }41 42 // 标准 Webhook43 console.log("收到标准 Webhook:", payload.status);44 return { received: true };45});46 47async function processAndCallback(48 eventName: string,49 aiResponse: { data: Record<string, unknown> },50 callbackUrl: string,51 customerData: Record<string, unknown>,52 config: ReturnType<typeof useRuntimeConfig>53) {54 try {55 let enrichedData = { ...aiResponse.data };56 57 // 3. 您的自定义业务逻辑58 if (eventName === "content_ready") {59 enrichedData = {60 ...enrichedData,61 processed: true,62 saved_at: new Date().toISOString(),63 };64 }65 66 // 4. 回调 ModelRiver67 await $fetch(callbackUrl, {68 method: "POST",69 headers: {70 Authorization: `Bearer ${config.modelriverApiKey}`,71 "Content-Type": "application/json",72 },73 body: {74 data: enrichedData,75 task_id: `task_${Date.now()}`,76 },77 });78 79 console.log(`✅ 已针对事件发送回调: ${eventName}`);80 } catch (error) {81 console.error("处理错误:", error);82 83 await $fetch(callbackUrl, {84 method: "POST",85 headers: {86 Authorization: `Bearer ${config.modelriverApiKey}`,87 "Content-Type": "application/json",88 },89 body: {90 data: {91 processing_status: "failed",92 processing_error: error instanceof Error ? error.message : "未知错误",93 },94 },95 });96 }97}触发异步请求
服务端路由
TYPESCRIPT
1// server/api/ai/generate.post.ts2export default defineEventHandler(async (event) => {3 const config = useRuntimeConfig();4 const body = await readBody(event);5 6 const response = await $fetch("https://api.modelriver.com/v1/ai/async", {7 method: "POST",8 headers: {9 Authorization: `Bearer ${config.modelriverApiKey}`,10 "Content-Type": "application/json",11 },12 body: {13 model: "content_generator",14 messages: [{ role: "user", content: body.prompt }],15 metadata: body.metadata || {},16 },17 });18 19 return response;20});Vue Composable
TYPESCRIPT
1// composables/useEventDrivenAI.ts2export function useEventDrivenAI() {3 const result = ref<Record<string, unknown> | null>(null);4 const status = ref<"idle" | "processing" | "complete" | "error">("idle");5 6 async function generate(prompt: string) {7 status.value = "processing";8 result.value = null;9 10 try {11 const { channel_id, ws_token, websocket_channel } = await $fetch("/api/ai/generate", {12 method: "POST",13 body: { prompt },14 });15 16 // 通过 WebSocket 连接17 const ws = new WebSocket(18 `wss://api.modelriver.com/socket/websocket?token=${encodeURIComponent(ws_token)}`19 );20 21 ws.onopen = () => {22 ws.send(JSON.stringify({23 topic: websocket_channel,24 event: "phx_join",25 payload: {},26 ref: "1",27 }));28 };29 30 ws.onmessage = (event) => {31 const msg = JSON.parse(event.data);32 if (msg.event === "response") {33 result.value = msg.payload.data;34 status.value = "complete";35 ws.close();36 }37 };38 39 ws.onerror = () => {40 status.value = "error";41 };42 } catch (error) {43 status.value = "error";44 console.error("触发 AI 生成失败:", error);45 }46 }47 48 return { result, status, generate };49}最佳实践
- 使用
useRuntimeConfig():绝不硬编码 API 密钥;Nuxt 会在运行时安全地注入它们。 - 使用
$fetch发起回调:Nitro 内置的 fetch 会自动处理重试和错误解析。 - 读取原始 Body 进行验证:使用
readRawBody(event),并对从中截取的<timestamp>.<data 的 JSON 文本>计算 HMAC;切勿对解析后的请求体重新序列化。 - 保持 Webhook 路由简洁:将繁重的处理逻辑卸载到独立的服务端实用程序中。
- 使用 Composables:将 WebSocket 逻辑封装在可复用的 Vue Composables 中。
下一步
- Django 后端管道指南:Python 备选方案
- Webhooks 参考:重试政策和投递监控
- 后端管道 概述:架构和流程