概述
Neon 提供了具有分支功能、可自动缩放至零以及支持 WebSocket 的无服务器 Postgres。其无服务器驱动程序可以在 Edge 环境中运行。通过将其与后端管道 结合使用,您可以构建完全无服务器的数据管道,让 AI 生成的数据直接落入 Postgres 数据库。
您将构建的内容:
- 一个使用 Neon 无服务器驱动程序的 Webhook 处理程序
- 用于存储 AI 生成内容的数据库架构 (Schema)
- 适用于生产负载的连接池 (Connection pooling) 配置
- 携带增强后的记录回调 ModelRiver
数据库架构 (Schema)
SQL
1-- 通过 Neon 控制台或 psql 创建2CREATE TABLE ai_content (3 id UUID DEFAULT gen_random_uuid() PRIMARY KEY,4 title TEXT NOT NULL,5 body TEXT,6 category TEXT DEFAULT 'general',7 metadata JSONB DEFAULT '{}',8 channel_id TEXT,9 event_name TEXT,10 created_at TIMESTAMPTZ DEFAULT now(),11 updated_at TIMESTAMPTZ DEFAULT now()12);13 14CREATE INDEX idx_ai_content_channel ON ai_content(channel_id);15CREATE INDEX idx_ai_content_event ON ai_content(event_name);16CREATE INDEX idx_ai_content_created ON ai_content(created_at DESC);Webhook 处理程序 (Node.js)
TYPESCRIPT
1// 使用 @neondatabase/serverless 驱动程序2import { neon } from "@neondatabase/serverless";3import crypto from "crypto";4 5const sql = neon(process.env.DATABASE_URL!);6 7function verifySignature(8 rawBody: string,9 timestamp: string,10 signature: string,11 secret: string12): boolean {13 // 签名字符串:"<timestamp>.<body.data 的原始 JSON 文本>"14 const start = rawBody.indexOf('"data":') + '"data":'.length;15 const end = rawBody.lastIndexOf(',"timestamp":');16 if (start < 7 || end < start) return false;17 18 const expected = crypto19 .createHmac("sha256", secret)20 .update(`${timestamp}.${rawBody.slice(start, end)}`)21 .digest("hex");22 return (23 signature.length === expected.length &&24 crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))25 );26}27 28export async function handleWebhook(req: Request): Promise<Response> {29 const signature = req.headers.get("x-modelriver-signature") ?? "";30 const timestamp = req.headers.get("x-modelriver-timestamp") ?? "";31 const rawBody = await req.text();32 33 // 1. 验证签名34 if (!verifySignature(rawBody, timestamp, signature, process.env.MODELRIVER_WEBHOOK_SECRET!)) {35 return new Response(JSON.stringify({ error: "Invalid signature" }), { status: 401 });36 }37 38 const payload = JSON.parse(rawBody).data; // 事件负载位于 body.data 下39 const { type, event, ai_response, callback_url, customer_data, channel_id } = payload;40 41 // 2. 处理后端管道的工作流42 if (type === "task.ai_generated" && callback_url) {43 try {44 const aiData = ai_response?.data ?? {};45 46 // 3. 写入 Neon47 const [record] = await sql`48 INSERT INTO ai_content (title, body, category, metadata, channel_id, event_name)49 VALUES (50 ${aiData.title ?? "Untitled"},51 ${aiData.description ?? aiData.body ?? ""},52 ${customer_data?.category ?? "general"},53 ${JSON.stringify({54 ...customer_data,55 ai_model: ai_response?.meta?.used_model,56 ai_provider: ai_response?.meta?.used_provider,57 })}::jsonb,58 ${channel_id},59 ${event}60 )61 RETURNING id, created_at62 `;63 64 // 4. 回调 ModelRiver65 await fetch(callback_url, {66 method: "POST",67 headers: {68 Authorization: `Bearer ${process.env.MODELRIVER_API_KEY}`,69 "Content-Type": "application/json",70 },71 body: JSON.stringify({72 data: {73 ...aiData,74 id: record.id,75 saved_at: record.created_at,76 },77 task_id: `neon_${record.id}`,78 metadata: {79 database: "neon",80 table: "ai_content",81 record_id: record.id,82 },83 }),84 });85 86 return new Response(JSON.stringify({ received: true }), { status: 200 });87 88 } catch (error: any) {89 console.error("错误:", error);90 91 await fetch(callback_url, {92 method: "POST",93 headers: {94 Authorization: `Bearer ${process.env.MODELRIVER_API_KEY}`,95 "Content-Type": "application/json",96 },97 body: JSON.stringify({98 data: {99 processing_status: "failed",100 processing_error: error.message,101 },102 }),103 });104 105 return new Response(JSON.stringify({ received: true }), { status: 200 });106 }107 }108 109 return new Response(JSON.stringify({ received: true }), { status: 200 });110}Vercel Edge Function 示例
Neon 的无服务器驱动程序可以在 Edge 环境中原生运行:
TYPESCRIPT
1// app/api/webhooks/modelriver/route.ts2import { NextRequest, NextResponse } from "next/server";3import { neon } from "@neondatabase/serverless";4 5export const runtime = "edge";6 7const sql = neon(process.env.DATABASE_URL!);8 9export async function POST(request: NextRequest) {10 const rawBody = await request.text();11 // ... 使用 rawBody 验证 x-modelriver-signature(见上文) ...12 13 const payload = JSON.parse(rawBody).data; // 事件负载位于 body.data 下14 15 if (payload.type === "task.ai_generated" && payload.callback_url) {16 const aiData = payload.ai_response?.data ?? {};17 18 const [record] = await sql`19 INSERT INTO ai_content (title, body, metadata)20 VALUES (${aiData.title}, ${aiData.description}, ${JSON.stringify(payload.customer_data)}::jsonb)21 RETURNING id, created_at22 `;23 24 await fetch(payload.callback_url, {25 method: "POST",26 headers: {27 Authorization: `Bearer ${process.env.MODELRIVER_API_KEY}`,28 "Content-Type": "application/json",29 },30 body: JSON.stringify({31 data: { ...aiData, id: record.id, saved_at: record.created_at },32 task_id: `neon_${record.id}`,33 }),34 });35 36 return NextResponse.json({ received: true });37 }38 39 return NextResponse.json({ received: true });40}Drizzle ORM 集成
用于类型安全的数据库访问:
TYPESCRIPT
1// db/schema.ts2import { pgTable, uuid, text, jsonb, timestamp } from "drizzle-orm/pg-core";3 4export const aiContent = pgTable("ai_content", {5 id: uuid("id").defaultRandom().primaryKey(),6 title: text("title").notNull(),7 body: text("body"),8 category: text("category").default("general"),9 metadata: jsonb("metadata").default({}),10 channelId: text("channel_id"),11 eventName: text("event_name"),12 createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),13 updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),14});15 16// 在您的 Webhook 处理程序中17import { drizzle } from "drizzle-orm/neon-http";18import { neon } from "@neondatabase/serverless";19import { aiContent } from "./db/schema";20 21const sql = neon(process.env.DATABASE_URL!);22const db = drizzle(sql);23 24const [record] = await db25 .insert(aiContent)26 .values({27 title: aiData.title,28 body: aiData.description,29 category: customerData?.category,30 metadata: customerData,31 channelId,32 eventName: event,33 })34 .returning();数据库分支 (Branching)
在不影响生产环境的情况下测试 AI 内容架构:
Bash
# 创建一个分支neonctl branches create --name ai-content-v2 # 获取分支的连接字符串neonctl connection-string ai-content-v2 # 针对该分支测试您的 Webhook 处理程序MODELRIVER_WEBHOOK_SECRET=... DATABASE_URL=<branch-url> npm run dev # 满意后进行合并最佳实践
- 使用无服务器驱动程序:
@neondatabase/serverless使用 WebSocket 以实现 Edge 兼容性。 - 使用带标签的模板字面量:Neon 的
sql函数会自动防止 SQL 注入。 - 将元数据用 JSONB 存储:灵活的架构,适应各种 AI 响应形式。
- 使用数据库分支:在投入生产之前安全地测试架构变更。
- 使用 Drizzle ORM:类型安全查询,并支持自动架构推断。
下一步
- Convex 后端管道指南:响应式数据库备选方案
- 后端管道 概述:架构和流程
- Webhooks 参考:重试政策和投递监控