文档

Nuxt.js 的后端管道

在 Nuxt 服务端路由中接收 AI Webhooks,运行自定义服务端逻辑,并回调 ModelRiver:利用 Nitro 内置的事件处理能力。

概述

Nuxt 3 使用 Nitro 作为其服务端引擎,提供了基于文件的服务端路由,非常适合作为 Webhook 处理程序。ModelRiver 后端管道的 Webhooks 可以自然地与 Nuxt 的自动导入系统、Composables 和服务端中间件集成。

您将构建的内容:

  • 一个用于接收 ModelRiver 发出的 AI 生成数据的 Nitro 服务端路由
  • 使用 Nuxt 服务端中间件进行签名验证
  • 具有回调 ModelRiver 功能的后台处理逻辑
  • 一个用于基于 WebSocket 交付结果的 Vue Composable

快速开始

安装依赖

Bash
npx nuxi@latest init my-ai-app
cd my-ai-app
npm install

环境变量

Bash
# .env
NUXT_MODELRIVER_API_KEY=mr_live_YOUR_API_KEY
NUXT_MODELRIVER_WEBHOOK_SECRET=your_webhook_secret
TYPESCRIPT
1// nuxt.config.ts
2export default defineNuxtConfig({
3 runtimeConfig: {
4 modelriverApiKey: "",
5 modelriverWebhookSecret: "",
6 },
7});

Webhook 处理程序

服务端路由

TYPESCRIPT
1// server/api/webhooks/modelriver.post.ts
2import 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 = crypto
11 .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 // 标准 Webhook
43 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. 回调 ModelRiver
67 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.ts
2export 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.ts
2export 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}

最佳实践

  1. 使用 useRuntimeConfig():绝不硬编码 API 密钥;Nuxt 会在运行时安全地注入它们。
  2. 使用 $fetch 发起回调:Nitro 内置的 fetch 会自动处理重试和错误解析。
  3. 读取原始 Body 进行验证:使用 readRawBody(event),并对从中截取的 <timestamp>.<data 的 JSON 文本> 计算 HMAC;切勿对解析后的请求体重新序列化。
  4. 保持 Webhook 路由简洁:将繁重的处理逻辑卸载到独立的服务端实用程序中。
  5. 使用 Composables:将 WebSocket 逻辑封装在可复用的 Vue Composables 中。

下一步