Supabase 实战:用 Edge Functions + Huggingface.js + Storage 自动生成图片描述(Database Webhooks 驱动)
导读
本文围绕仓库中 huggingface-image-captioning 示例函数 展开,讲解如何把 Supabase Edge Functions、Supabase Storage 和 Database Webhooks 组合起来,搭建一个“图片一上传即自动生成文字描述并落库”的自动化服务。读完本文,你将掌握 Webhook 载荷解析、Storage 签名 URL 生成、Huggingface.js 推理调用,以及基于 @supabase/server 的服务端管理员客户端写法,可直接在自有项目中复刻这套事件驱动 AI 工作流。
整体架构:四块积木拼出一个自动流水线
该示例的核心思路是“事件触发、函数中转、AI 推理、结果落库”,由四个 Supabase/Hugging Face 能力拼接而成:
- Supabase Edge Functions:基于 Deno 运行、用 TypeScript 编写、通过 Supabase CLI 部署的无服务器函数,负责接收 Webhook 并编排整个流程;
- Huggingface.js:Hugging Face 官方 JS 库,用统一接口调用 10 万+ 机器学习模型。本示例通过
HfInference调用图像转文本(image-to-text)模型完成描述生成; - Supabase Storage:存放用户上传图片的存储桶;函数通过它生成带时效的签名 URL 供模型拉取图片;
- Database Webhooks:数据库变更触发器。当
storage.objects表插入新记录(即有文件上传成功)时,自动向 Edge Function 发送 HTTP 请求,无需任何额外编排代码。
从事件流来看,整条链路是:
- 用户把图片上传到名为
images的 Storage bucket,Postgres 的storage.objects表插入一行; - 预先配置的 Database Webhook 捕获到
INSERT事件,把包含该文件元数据的 JSON 载荷 POST 到已部署的huggingface-image-captioning函数; - 函数解析载荷,用管理员权限为刚上传的对象生成 60 秒有效的签名 URL,并抓取图片二进制数据;
- 函数调用 Hugging Face 上的视觉描述模型,得到一句话文字描述;
- 函数把
storage.objects的主键和生成的描述写入image_caption表,整个流程对调用方完全异步。
核心实现逐行拆解:index.ts
该示例的全部逻辑浓缩在 index.ts 一个文件里,完整约 46 行,下面分段展开。
依赖导入与推理客户端初始化
import { HfInference } from 'npm:@huggingface/inference@^4'
import { withSupabase } from 'npm:@supabase/server@^1'
import { Database } from './types.ts'
npm:@huggingface/inference@^4:Huggingface.js 推理库的 Deno 兼容导入写法,直接使用 npm 远程模块,无需安装依赖;npm:@supabase/server@^1:Supabase 官方服务端工具包,withSupabase包装器会在请求处理前自动解析 JWT、初始化带权限的 Supabase 客户端,并把ctx注入到 handler;Database类型来自同目录的types.ts,提供storage、public两个 schema 的完整表结构类型,让整条链路的字段访问都有类型保障。
随后是推理客户端与函数入口的日志输出:
console.log('Hello from `huggingface-image-captioning` function!')
const hf = new HfInference(Deno.env.get('HUGGINGFACE_ACCESS_TOKEN'))
HUGGINGFACE_ACCESS_TOKEN 从环境变量读取,需要在函数部署后用 supabase secrets set 配置。Hugging Face 的推理接口(Inference API)通常要求提供 Access Token,你可以在 Hugging Face 账户设置中创建 token 并把它绑定为 Supabase 项目 secret。
Webhook 载荷类型定义
type SoRecord = Database['storage']['Tables']['objects']['Row']
interface WebhookPayload {
type: 'INSERT' | 'UPDATE' | 'DELETE'
table: string
record: SoRecord
schema: 'public'
old_record: null | SoRecord
}
SoRecord 直接取自生成的类型文件 types.ts 中 storage.objects 表的 Row 类型,其字段包括 bucket_id、id、name、metadata、path_tokens、owner 等。从该结构可以看出,storage.objects 就是 Storage 的元数据表——每次文件上传都会在其中产生一条记录。WebhookPayload 则描述了 Supabase Database Webhooks 发送的 JSON 结构,包含事件类型、触发表、变更后的新记录 record 与变更前记录 old_record。
请求处理:签名 URL → 拉图 → 推理 → 落库
// Deploy with verify_jwt = false.
export default {
fetch: withSupabase<Database>({ auth: 'secret' }, async (req, ctx) => {
const payload: WebhookPayload = await req.json()
const soRecord = payload.record
代码注释与 config.toml 中 [functions.huggingface-image-captioning] verify_jwt = false 的设置互相印证:由于该函数是被数据库 Webhook 以服务端到服务端方式调用的,而非来自浏览器客户端,因此关闭了 JWT 校验,这也是 Webhook 类 Edge Function 的通用部署要求。
withSupabase<Database>({ auth: 'secret' }, handler) 是核心。仓库中另一个示例 og-image-with-storage-cdn 也采用相同的 withSupabase({ auth: 'secret' }, ...) 调用模式,说明这是服务端函数获得管理员权限的标准写法。传入 auth: 'secret' 后,handler 的 ctx.supabaseAdmin 即绑定 service role(服务角色密钥)的管理员客户端,可绕过 RLS 直接读写数据表,这正是本函数能够在无用户上下文的情况下向 image_caption 表写入记录的前提。
// Construct image url from storage
const { data, error } = await ctx.supabaseAdmin.storage
.from(soRecord.bucket_id!)
.select(...)
接着逻辑分四步展开(结合代码结构与 Storage API 语义):
- 生成签名 URL:以
soRecord.bucket_id选中存储桶,用path_tokens拼接出对象路径,调用createSignedUrl(path, 60)生成 60 秒有效的临时访问地址。path_tokens是路径分段数组(如["folder", "photo.jpg"]),用join('/')还原为完整对象键; - 拉取图片二进制:
await fetch(signedUrl)得到响应后取.blob(),作为推理接口的输入数据; - 调用视觉模型:
const imgDesc = await hf.imageToText({ data: await (await fetch(signedUrl)).blob(), model: 'nlpconnect/vit-gpt2-image-captioning', })hf.imageToText是 Huggingface.js 的图像转文本方法,model指向公开模型nlpconnect/vit-gpt2-image-captioning——这是一个基于 Vision Transformer + GPT-2 的经典图像描述生成模型。返回的imgDesc.generated_text即模型生成的图片描述文本。如需更换模型,只需替换该 model 标识; - 写回数据库:
以await ctx.supabaseAdmin .from('image_caption') .insert({ id: soRecord.id!, caption: imgDesc.generated_text }) .throwOnError()storage.objects的记录主键soRecord.id作为image_caption.id,把描述写入caption列。.throwOnError()会在写入失败时抛出异常,让函数返回非 2xx 状态以便 Webhook 重试。最后return Response.json({ status: 'ok' })通知 Webhook 处理成功。
前置数据准备:bucket 与 image_caption 表
按 README 的 Setup 步骤,需要先在数据库中准备两个对象:
- 创建 Storage bucket
images:在项目 Dashboard 的 Storage → Buckets 中新建名为images的存储桶。参考仓库 config.toml 中本地开发配置,示例还把该 bucket 设成了public = true并绑定objects_path,用于把本地./buckets/images目录同步为存储内容; - 建表
image_caption,仅两列:id:uuid类型,外键引用storage.objects.id,把每一条描述与具体的存储对象一一关联;caption:text类型,存放模型生成的描述文本。
建表时建议同时为 storage.objects 的插入事件配置触发器映射逻辑(即在 Webhook 表选择中按需勾选相关事件类型)。注意在仓库 migrations 目录中并没有为该示例提供建表迁移脚本,说明表结构需要按上述步骤在 Dashboard 或你自己的迁移中创建,字段定义与 types.ts 中 image_caption 表的 Row/Insert 类型保持一致(id: string、caption: string)。
生成 TypeScript 类型
为保证 withSupabase<Database> 的泛型与 ctx.supabaseAdmin.from('image_caption') 的调用链类型完备,需要基于远端数据库生成类型文件:
supabase gen types typescript --project-id=your-project-ref --schema=storage,public > supabase/functions/huggingface-image-captioning/types.ts
参数说明:
--project-id:替换为你的项目 ref(Dashboard 项目设置中可查);--schema=storage,public:关键点。本函数同时访问 Storage 的objects表与业务侧的image_caption表,因此必须同时导出storage与public两个 schema,缺一不可。
生成结果即仓库中的 types.ts:public schema 下只有 image_caption 一张表,而 storage schema 下包含 buckets、migrations、objects 三张表及多个辅助函数(如 search、can_insert_object、foldername 等),完整描述了 Storage 内部的数据结构。
部署函数并配置 Webhook
部署 Edge Function
在项目根目录执行:
supabase functions deploy huggingface-image-captioning
由于函数走 Webhook 触发,务必确认 config.toml 中对该函数设置了:
[functions.huggingface-image-captioning]
verify_jwt = false
随后配置密钥环境变量:
supabase secrets set HUGGINGFACE_ACCESS_TOKEN=your_hf_token
创建 Database Webhook
在 Dashboard 的 Database → Webhooks(即 README 中提到的 Database Hooks)页面新建 Webhook,核心配置要点:
- Source Table 选择
storage.objects,事件类型选择INSERT(对应“图片上传完成”这一时机,与函数内payload.type === 'INSERT'的语义匹配); - HTTP 方法选 POST,URL 填已部署函数的完整调用地址;
- HTTP Headers 中配置
Authorization: Bearer <anon key>(本地开发时可用supabase functions serve --no-verify-jwt绕过签名); - 事件载荷会自动携带表名、schema、新旧记录等字段,即
index.ts中WebhookPayload所声明的结构。
此后每当有新图片上传到 images bucket,Webhook 就会把 storage.objects 的新记录推送给函数,函数依次完成签名 URL 生成、模型推理与结果回写,全程无需客户端轮询或手动触发。
本地开发调试建议
结合 examples/edge-functions/README.md 提供的通用流程,本地调试该函数可参考以下步骤:
supabase start # 启动本地栈(需 Docker)
cp supabase/.env.local.example supabase/.env.local
# 在 .env.local 中填入 HUGGINGFACE_ACCESS_TOKEN 等变量
supabase functions serve --env-file supabase/.env.local --no-verify-jwt
--no-verify-jwt 与 config.toml 中的 verify_jwt = false 相呼应,都是为了方便事件源(Webhook 或本地 curl)不经鉴权直接调用函数。调试时可以先向函数手动 POST 一条模拟的 storage.objects 记录,观察控制台日志中的 Hello from ... 与推理结果,再回到 Dashboard 验证 image_caption 表是否出现了对应行。需要注意,本地调试时 Storage 的对象必须真实存在于本地栈中,签名 URL 才能正确拉到图片数据。
边界与注意点(依据源码推断)
从源码结构与官方设计模式可以总结出以下工程注意点,供扩展该示例时参考:
- 幂等性:示例直接以
storage.objects.id作为image_caption.id主键。若同一对象被重复触发(如 Webhook 重试),INSERT 会因主键冲突失败。生产环境建议改为 upsert 或先查后写,保证事件重放安全; - 模型时延与超时:
imageToText的耗时取决于所选模型与图片大小,Edge Functions 对执行时长有限制,超大图片应考虑先用 Storage 的图片处理能力压缩后再送推理; - Token 管理:推理消耗 Hugging Face 账户配额,
HUGGINGFACE_ACCESS_TOKEN属于机密,只能通过supabase secrets set注入,切勿写死在代码或提交到仓库; - 权限模型:全程使用
ctx.supabaseAdmin(service role),跳过 RLS,意味着该函数只应通过verify_jwt = false的 Webhook 入口暴露,避免被外部直接滥用。
小结
通过这个 示例 可以看到,事件驱动的 AI 能力接入并不复杂:storage.objects 的 INSERT Webhook 充当触发器,一个约 46 行的 Deno 函数借助 @supabase/server 的管理员客户端与 Huggingface.js 完成“取图 → 推理 → 落库”闭环,而模型本身可以随时通过 model 参数换成任意具备 image-to-text 能力的 Hugging Face 模型。这一模式同样可推广到语音转写、OCR、审核等其它“存储触发 + 模型推理”场景,值得作为你构建 Supabase AI 应用的基础模板。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00