跳到主要内容

用 CloudBase 云函数代理 fal.ai FLUX 图片生成

一句话定义:用 @fal-ai/client 在 CloudBase 云函数里调 FLUX schnell 模型出图,通过 fetch 把生成的图片二进制拉回云函数,用 @cloudbase/node-sdk 写到云存储,前端通过 getTempFileURL 拿临时链接展示。

预计耗时:25 分钟 | 难度:入门

适用场景

  • 小程序/Web 想要 AI 出图(头像、商品图、营销素材、占位插画),不想自己跑模型
  • fal.ai 的 key 不能放前端,但又不想在前端和 fal 之间架一个独立服务器
  • 生成的图片想沉淀到自己的云存储,而不是只拿一个 fal CDN 链接(那个链接会过期或被清理)
  • 同一个云函数环境里,接下来还要把图片做后处理(裁剪、加水印、写库)

不适用:

  • 需要超低延迟、用户对生成时间敏感(本篇用同步 subscribe,一次出图通常 3-15 秒,云函数同步等待)
  • 一次出图量大(批量超过几十张),建议改 fal 的 queue 模式 + 数据库状态机,本文不展开
  • 想直接在浏览器里跑模型(WebGPU),那是另一条路线,不需要云函数

环境要求

依赖版本
Node.js(云函数运行时)≥ 18(自带 fetch,老版本要装 node-fetch)
@fal-ai/client^1.10.1(注意:@fal-ai/serverless-client 已 deprecated,不要用)
@cloudbase/node-sdk^3.18.1
@cloudbase/clilatest
云函数类型HTTP 触发(--httpFn)
公网出口调 fal.ai 需要公网,控制台「网络 → 公网访问」确认已开

需要准备:

  • 一个 fal.ai 账号,在 fal.ai dashboard 拿一个 API key
  • 一个 CloudBase 环境 ID,且开通了云函数和云存储

第一步:申请 fal.ai key,配置到云函数环境变量

  1. 打开 https://fal.ai/dashboard/keys ,点 Add Key,把生成的字符串复制下来(只显示一次)
  2. 这个 key 之后会以环境变量 FAL_KEY 注入到云函数,不要写到代码或 git 里
  3. 同步在 CloudBase 控制台「云函数 → 环境变量」预备好,这一步部署完函数再填也行

fal.ai 的 SDK 会自动从 process.env.FAL_KEY 读取 credentials,所以变量名必须叫 FAL_KEY,不要起别的名字。

第二步:写云函数

新建一个目录 fal-image-gen,初始化:

mkdir fal-image-gen && cd fal-image-gen
npm init -y
npm install --save @fal-ai/client @cloudbase/node-sdk

package.jsonmainindex.js(默认就是)。

index.js:

const { fal } = require('@fal-ai/client');
const tcb = require('@cloudbase/node-sdk');

// FAL_KEY 从环境变量读,代码里不留 key
fal.config({ credentials: process.env.FAL_KEY });

const app = tcb.init({
env: process.env.TCB_ENV || tcb.SYMBOL_CURRENT_ENV,
});

exports.main = async (event) => {
// event.body 是 HTTP 触发的请求体字符串,需要 JSON.parse
let body = {};
try {
body = typeof event.body === 'string' ? JSON.parse(event.body) : (event.body || {});
} catch (err) {
return httpJson(400, { error: 'invalid_json', message: err.message });
}

const prompt = body.prompt;
const imageSize = body.image_size || 'landscape_16_9';
const model = body.model || 'fal-ai/flux/schnell';

if (!prompt || typeof prompt !== 'string') {
return httpJson(400, { error: 'missing_prompt' });
}

// 1. 调 fal.ai 生成图片(同步等待结果)
let falResult;
try {
falResult = await fal.subscribe(model, {
input: { prompt, image_size: imageSize },
});
} catch (err) {
console.error('fal.subscribe failed', err);
return httpJson(502, { error: 'fal_failed', message: err.message });
}

const images = falResult?.data?.images || [];
if (images.length === 0) {
return httpJson(502, { error: 'no_image_returned' });
}
const imageUrl = images[0].url;

// 2. 用 fetch 把图片二进制拉回来
let buffer;
try {
const resp = await fetch(imageUrl);
if (!resp.ok) {
return httpJson(502, { error: 'fetch_image_failed', status: resp.status });
}
buffer = Buffer.from(await resp.arrayBuffer());
} catch (err) {
console.error('fetch image failed', err);
return httpJson(502, { error: 'fetch_image_failed', message: err.message });
}

// 3. 写到云存储,cloudPath 用日期 + 随机数,避免覆盖
const ext = guessExt(imageUrl) || 'png';
const cloudPath = `ai-images/${dateStr()}/${Date.now()}-${rand6()}.${ext}`;
let uploadResult;
try {
uploadResult = await app.uploadFile({
cloudPath,
fileContent: buffer,
});
} catch (err) {
console.error('uploadFile failed', err);
return httpJson(500, { error: 'upload_failed', message: err.message });
}

// 4. 拿临时链接(1 小时),前端直接用
const tmp = await app.getTempFileURL({
fileList: [{ fileID: uploadResult.fileID, maxAge: 3600 }],
});
const tempFileURL = tmp.fileList?.[0]?.tempFileURL;

return httpJson(200, {
fileID: uploadResult.fileID,
tempFileURL,
model,
prompt,
});
};

function httpJson(statusCode, data) {
return {
statusCode,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
};
}

function dateStr() {
const d = new Date();
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
const day = String(d.getUTCDate()).padStart(2, '0');
return `${d.getUTCFullYear()}-${m}-${day}`;
}

function rand6() {
return Math.random().toString(36).slice(2, 8);
}

function guessExt(url) {
const m = url.match(/\.([a-zA-Z0-9]{2,5})(\?|$)/);
return m ? m[1].toLowerCase() : null;
}

几个写法上的提醒:

  • fal.subscribe(...) 是同步等结果的快速调用,适合 schnell 这种几秒就返回的模型;如果换 fal-ai/flux-profal-ai/flux-kontext/dev 这种慢一些的,云函数超时要相应调大,见下文
  • falResult.data.images 是数组,默认只生成一张,但模型支持 num_images 参数
  • cloudPath 不要让用户输入直接拼接,且不能含 ..,否则 uploadFile 会拒绝
  • 这里 fetch 是 Node.js 18 内置的,云函数运行时选 18 及以上即可,不用装 node-fetch

第三步:部署云函数

先登录 CLI(本地交互登录,或 CI 用 API key):

# 本地
tcb login

# CI 环境
tcb login --apiKeyId <YOUR_API_KEY_ID> --apiKey <YOUR_API_KEY_SECRET>

部署成 HTTP 函数:

tcb fn deploy fal-image-gen --httpFn -e your-env-id

部署成功后,在控制台「云函数 → fal-image-gen」做两件事:

  1. 环境变量:加一个 FAL_KEY,值是第一步拿到的 fal.ai key
  2. 超时配置:fal schnell 通常 3-8 秒,但偶发会到 15 秒以上;默认 3 秒会 timeout,调到 60 秒以上

记下访问 URL,类似 https://your-env.service.tcloudbase.com/fal-image-gen

第四步:前端调用 + 展示

用最朴素的 fetch 调云函数,拿到 tempFileURL 直接给 <img>:

<button id="gen">生成图片</button>
<img id="preview" style="max-width: 600px" />

<script>
document.getElementById('gen').onclick = async () => {
const resp = await fetch('https://your-env.service.tcloudbase.com/fal-image-gen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: 'a calico cat sitting on a window sill at sunset, soft light',
image_size: 'landscape_16_9',
}),
});
const data = await resp.json();
if (data.tempFileURL) {
document.getElementById('preview').src = data.tempFileURL;
} else {
alert('生成失败:' + JSON.stringify(data));
}
};
</script>

注意临时链接的有效期由 maxAge 决定(本文设的 3600 秒);如果是要持久展示,前端每次拿 fileID 重新换链接,或者把 fileID 存数据库,真要展示时再调 getTempFileURL不要把 tempFileURL 直接存库,过期就 404。

运行验证

最直接的验证就是 curl:

curl -X POST 'https://your-env.service.tcloudbase.com/fal-image-gen' \
-H 'Content-Type: application/json' \
-d '{"prompt": "a calico cat at sunset", "image_size": "landscape_16_9"}'

预期返回:

{
"fileID": "cloud://your-env.xxxx-xxx/ai-images/2026-04-30/1714451234567-abc123.png",
"tempFileURL": "https://...service.tcloudbase.com/...?sign=...",
"model": "fal-ai/flux/schnell",
"prompt": "a calico cat at sunset"
}

tempFileURL 贴浏览器,应该能直接看到图。

控制台「云函数 → fal-image-gen → 日志」可以按 requestId 过滤,看 fal 的调用耗时和上传是否成功。

常见错误

错误信息原因修复
401 Unauthorized 或 fal SDK 抛 Authentication required没配 FAL_KEY,或 key 写错/被吊销控制台环境变量重新填一遍,然后 重新部署函数让变量生效;也可以直接在 fal dashboard 重新生成 key
fal SDK 抛 Model not found404模型 id 拼错,例如把 fal-ai/flux/schnell 写成 fal-ai/flux-schnell模型 id 用斜杠分段,正确的是 fal-ai/flux/schnellfal-ai/flux/devfal-ai/flux-profal-ai/flux-kontext/devfal-ai/flux-2-flex/edit
uploadFileINVALID_PARAMcloudPath invalidcloudPath..、绝对路径前导 /、特殊字符用相对路径,只用字母数字 - _ / .,不要让用户输入直接拼到 cloudPath
云函数 3 秒后断开,日志里看到 fal 还没返回云函数默认超时 3 秒,生图通常 5-15 秒控制台 → 函数配置 → 超时时间,改到 60 秒(schnell)或 120-300 秒(flux-pro / kontext)
JavaScript heap out of memory 或函数 OOM默认内存 256MB,大尺寸图(2K 以上)的 buffer 加上 SDK 占用容易超控制台 → 函数配置 → 内存,提到 512MB 或 1024MB
调用上游 getaddrinfo EAI_AGAIN fal.run云函数没开公网出口控制台「网络 → 公网访问」开启,或挂 NAT

错误码完整列表可以查 https://docs.cloudbase.net/error-code/

相关文档

下一步

把这个云函数包装成 MCP tool 喂给 部署 MCP Server 到云托管 里的 server,agent 在对话里说"画一张猫"就能直接出图;或者注册给 Mastra agent 当工具,由 agent 自己判断什么时候要配图。