概述
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(payload: string, signature: string, secret: string): boolean {5 const expected = crypto6 .createHmac("sha256", secret)7 .update(payload)8 .digest("hex");9 10 return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));11}12 13export default defineEventHandler(async (event) => {14 const config = useRuntimeConfig();15 const signature = getHeader(event, "mr-signature");16 const rawBody = await readRawBody(event);17 18 // 1. 验证 Webhook 签名19 if (!signature || !rawBody || !verifySignature(rawBody, signature, config.modelriverWebhookSecret)) {20 throw createError({ statusCode: 401, statusMessage: "Invalid signature" });21 }22 23 const payload = JSON.parse(rawBody);24 const { type, event: eventName, ai_response, callback_url, customer_data } = payload;25 26 // 2. 处理事件驱动的工作流27 if (type === "task.ai_generated" && callback_url) {28 // 处理并回调29 await processAndCallback(eventName, ai_response, callback_url, customer_data, config);30 31 return { received: true };32 }33 34 // 标准 Webhook35 console.log("收到标准 Webhook:", payload.type);36 return { received: true };37});38 39async function processAndCallback(40 eventName: string,41 aiResponse: { data: Record<string, unknown> },42 callbackUrl: string,43 customerData: Record<string, unknown>,44 config: ReturnType<typeof useRuntimeConfig>45) {46 try {47 let enrichedData = { ...aiResponse.data };48 49 // 3. 您的自定义业务逻辑50 if (eventName === "content_ready") {51 enrichedData = {52 ...enrichedData,53 processed: true,54 saved_at: new Date().toISOString(),55 };56 }57 58 // 4. 回调 ModelRiver59 await $fetch(callbackUrl, {60 method: "POST",61 headers: {62 Authorization: `Bearer ${config.modelriverApiKey}`,63 "Content-Type": "application/json",64 },65 body: {66 data: enrichedData,67 task_id: `task_${Date.now()}`,68 },69 });70 71 console.log(`✅ 已针对事件发送回调: ${eventName}`);72 } catch (error) {73 console.error("处理错误:", error);74 75 await $fetch(callbackUrl, {76 method: "POST",77 headers: {78 Authorization: `Bearer ${config.modelriverApiKey}`,79 "Content-Type": "application/json",80 },81 body: {82 error: "processing_failed",83 message: error instanceof Error ? error.message : "未知错误",84 },85 });86 }87}触发异步请求
服务端路由
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 workflow: "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)获取未经解析的负载以进行 HMAC 验证。 - 保持 Webhook 路由简洁:将繁重的处理逻辑卸载到独立的服务端实用程序中。
- 使用 Composables:将 WebSocket 逻辑封装在可复用的 Vue Composables 中。
下一步
- Django 事件驱动指南:Python 备选方案
- Webhooks 参考:重试政策和投递监控
- 事件驱动 AI 概述:架构和流程