概述
Supabase 在一个平台中结合了 Postgres、实时订阅和 Edge Functions。这使其成为后端管道的理想选择:您的 Edge Function 接收 Webhook,直接将 AI 数据写入 Postgres,然后 Supabase Realtime 将变更推送到已连接的客户端。
您将构建的内容:
- 一个用于接收 ModelRiver Webhooks 的 Supabase Edge Function
- 用于存储 AI 生成内容的数据库架构 (Schema)
- 用于即时前端更新的实时订阅
- 携带增强后的数据库记录回调 ModelRiver
数据库架构 (Schema)
SQL
1-- supabase/migrations/001_ai_content.sql2create 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 source text default 'modelriver',11 created_at timestamptz default now(),12 updated_at timestamptz default now()13);14 15-- 启用实时功能16alter publication supabase_realtime add table ai_content;17 18-- 行级安全性 (RLS)19alter table ai_content enable row level security;20 21create policy "用户可以查看自己的内容"22 on ai_content for select23 using (metadata->>'user_id' = auth.uid()::text);Edge Function Webhook 处理程序
TYPESCRIPT
1// supabase/functions/modelriver-webhook/index.ts2import { serve } from "https://deno.land/[email protected]/http/server.ts";3import { createClient } from "https://esm.sh/@supabase/supabase-js@2";4 5const supabase = createClient(6 Deno.env.get("SUPABASE_URL")!,7 Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!8);9 10serve(async (req) => {11 if (req.method !== "POST") {12 return new Response("Method not allowed", { status: 405 });13 }14 15 const signature = req.headers.get("x-modelriver-signature") ?? "";16 const timestamp = req.headers.get("x-modelriver-timestamp") ?? "";17 const rawBody = await req.text();18 const webhookSecret = Deno.env.get("MODELRIVER_WEBHOOK_SECRET") ?? "";19 20 // 1. 验证签名:对 "<timestamp>.<body.data 的原始 JSON 文本>" 计算 HMAC-SHA25621 const start = rawBody.indexOf('"data":') + '"data":'.length;22 const end = rawBody.lastIndexOf(',"timestamp":');23 const key = await crypto.subtle.importKey(24 "raw",25 new TextEncoder().encode(webhookSecret),26 { name: "HMAC", hash: "SHA-256" },27 false,28 ["sign"]29 );30 const sig = await crypto.subtle.sign(31 "HMAC",32 key,33 new TextEncoder().encode(`${timestamp}.${rawBody.slice(start, end)}`)34 );35 const expected = Array.from(new Uint8Array(sig))36 .map((b) => b.toString(16).padStart(2, "0"))37 .join("");38 39 // 常量时间比较40 let diff = expected.length ^ signature.length;41 for (let i = 0; i < expected.length; i++) {42 diff |= expected.charCodeAt(i) ^ signature.charCodeAt(i);43 }44 45 if (start < 7 || end < start || diff !== 0) {46 return new Response(JSON.stringify({ error: "Invalid signature" }), { status: 401 });47 }48 49 const payload = JSON.parse(rawBody).data; // 事件负载位于 body.data 下50 const { type, event, ai_response, callback_url, customer_data, channel_id } = payload;51 52 // 2. 处理后端管道的工作流53 if (type === "task.ai_generated" && callback_url) {54 try {55 const aiData = ai_response?.data ?? {};56 57 // 3. 写入 Supabase58 const { data: record, error } = await supabase59 .from("ai_content")60 .insert({61 title: aiData.title ?? "Untitled",62 body: aiData.description ?? aiData.body ?? "",63 category: customer_data?.category ?? "general",64 metadata: {65 ...customer_data,66 ai_model: ai_response?.meta?.used_model,67 ai_provider: ai_response?.meta?.used_provider,68 },69 channel_id,70 event_name: event,71 })72 .select()73 .single();74 75 if (error) throw error;76 77 // 4. 携带数据库记录回调 ModelRiver78 const callbackResponse = await fetch(callback_url, {79 method: "POST",80 headers: {81 Authorization: `Bearer ${Deno.env.get("MODELRIVER_API_KEY")}`,82 "Content-Type": "application/json",83 },84 body: JSON.stringify({85 data: {86 ...aiData,87 id: record.id,88 saved_at: record.created_at,89 supabase_url: `${Deno.env.get("SUPABASE_URL")}/rest/v1/ai_content?id=eq.${record.id}`,90 },91 task_id: `supabase_${record.id}`,92 metadata: {93 database: "supabase",94 table: "ai_content",95 record_id: record.id,96 },97 }),98 });99 100 if (!callbackResponse.ok) {101 throw new Error(`回调失败: ${callbackResponse.status}`);102 }103 104 return new Response(JSON.stringify({ received: true }), { status: 200 });105 106 } catch (error) {107 console.error("错误:", error);108 109 // 发送错误回调110 await fetch(callback_url, {111 method: "POST",112 headers: {113 Authorization: `Bearer ${Deno.env.get("MODELRIVER_API_KEY")}`,114 "Content-Type": "application/json",115 },116 body: JSON.stringify({117 data: {118 processing_status: "failed",119 processing_error: error.message,120 },121 }),122 });123 124 return new Response(JSON.stringify({ received: true }), { status: 200 });125 }126 }127 128 return new Response(JSON.stringify({ received: true }), { status: 200 });129});部署 Edge Function
Bash
supabase functions deploy modelriver-webhook设置环境变量 (Secrets)
Bash
supabase secrets set MODELRIVER_API_KEY=mr_live_YOUR_API_KEYsupabase secrets set MODELRIVER_WEBHOOK_SECRET=your_webhook_secret带有实时功能的前端
使用 Supabase Realtime 订阅实时监听新记录:
TYPESCRIPT
1// React 示例2import { createClient } from "@supabase/supabase-js";3import { useEffect, useState } from "react";4 5const supabase = createClient(6 process.env.NEXT_PUBLIC_SUPABASE_URL!,7 process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!8);9 10export function AiContentFeed() {11 const [items, setItems] = useState<any[]>([]);12 13 useEffect(() => {14 // 订阅新的 AI 内容15 const channel = supabase16 .channel("ai_content_changes")17 .on(18 "postgres_changes",19 { event: "INSERT", schema: "public", table: "ai_content" },20 (payload) => {21 setItems((prev) => [payload.new, ...prev]);22 }23 )24 .subscribe();25 26 return () => {27 supabase.removeChannel(channel);28 };29 }, []);30 31 return (32 <div>33 {items.map((item) => (34 <div key={item.id} className="p-4 border rounded mb-2">35 <h3 className="font-bold">{item.title}</h3>36 <p>{item.body}</p>37 <span className="text-xs text-zinc-500">{item.created_at}</span>38 </div>39 ))}40 </div>41 );42}最佳实践
- 为 Webhooks 使用 Edge Functions:全球分布式、低延迟的 Webhook 处理。
- 在您的表上启用 Realtime:客户端无需轮询即可获得即时更新。
- 使用行级安全性 (RLS):根据用户身份限制对 AI 生成内容的访问。
- 在 Edge Functions 中使用 Service Role Key:绕过 RLS 进行服务端写入。
- 将元数据存储为 JSONB:灵活的架构,适应各种 AI 响应形式。
下一步
- PlanetScale 后端管道指南:MySQL 备选方案
- Supabase 向量集成:用于嵌入 (embeddings) 的 pgvector
- 后端管道 概述:架构和流程