用 OpenClaw 也有一阵子了,这玩意儿确实好用,但有一个问题一直让我有点不爽——图片生成。
OpenClaw 自带的 image_generate 工具是个好东西,你直接在对话里说"帮我画一只猫",它就能调用 API 出图。但问题是,它默认走的 OpenAI 或者 OpenRouter,这些要么付费,要么配置起来一堆弯弯绕绕。
我就想,能不能白嫖一个免费的?
这时候我注意到了 Agnes AI——一个提供免费 API 的新平台。Agnes Image 2.1-Flash 和 2.0-Flash 都是免费的,生成质量还不错。但问题是,OpenClaw 不认识 Agnes 啊,我得自己把它接进去。
于是就有了这篇文章——给 OpenClaw 写一个图片生成插件,到底有多简单?

插件是什么?先打个比方
如果你没用过 OpenClaw 的插件系统,我先用大白话解释一下。
OpenClaw 本身是一个框架,它提供了各种能力——对话、工具调用、图片生成、视频生成等等。但具体怎么调用哪个 API、用哪个模型,它不知道。插件就是干这个的——告诉 OpenClaw"某某能力可以用某某 API 来实现"。
我写的 agnes-image-gen 插件,就是告诉 OpenClaw:"图片生成和视频生成,用 Agnes AI 的免费 API 就行。"
两条路:extensions 和 plugins
OpenClaw 有两种加载插件的方式:
extensions/ | ~/.openclaw/extensions/ | |
plugins/ | ~/.openclaw/plugins/ | openclaw plugins install 安装 |
开发阶段建议用 extensions/,简单粗暴,改代码后重启就生效。
踩坑: 我第一次写的时候把插件放到了
plugins/目录,结果 OpenClaw 怎么都加载不了,折腾了半天才发现路径不对。血泪教训。
动手写一个:三个文件搞定
一个最简的插件只需要三个文件:
extensions/agnes-image-gen/├── openclaw.plugin.json # 插件身份证├── package.json # 包信息└── index.js # 插件逻辑第一步:manifest(插件身份证)
openclaw.plugin.json 是 OpenClaw 读取插件信息的入口,在加载你的代码之前就会读取它:
{ "id": "agnes-image-gen", "name": "Agnes Image Generation", "description": "Adds Agnes-Image-2.0-Flash + Agnes-Video-V2.0 (free API)", "contracts": { "imageGenerationProviders": ["agnes"], "videoGenerationProviders": ["agnes"] }, "activation": { "onStartup":true }, "configSchema": { "type": "object", "additionalProperties":false }}这里有几个关键字段:
• id— 插件的唯一标识,相当于身份证号。后面配置的时候也要用这个 id。• contracts— 声明这个插件提供了什么能力。我声明了imageGenerationProviders和videoGenerationProviders,id 都叫"agnes"。意思是"图片和视频生成,找我就行"。• activation.onStartup— 设为true表示随 OpenClaw 启动自动加载。
第二步:入口文件(核心逻辑)
index.js 是实际干活的地方。入口用 definePluginEntry 定义:
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";export default definePluginEntry({ id: "agnes-image-gen", name: "Agnes Image Generation", description: "Agnes-Image-2.0-Flash + 2.1-Flash 图片生成 (免费API)", register(api) { // 在这里注册 provider },});核心就是 register(api) 这个函数——在这里面调用 api.registerImageGenerationProvider(...) 或 api.registerVideoGenerationProvider(...),把你的 provider 注册进去。
第三步:注册图片生成 provider
api.registerImageGenerationProvider({ id: "agnes", label: "Agnes AI", defaultModel: "agnes-image-2.1-flash", models: ["agnes-image-2.0-flash", "agnes-image-2.1-flash"], // 检查是否配置了 API Key isConfigured: ({ cfg }) => !!resolveApiKey(cfg), async generateImage(req) { // 在这里调用 Agnes API const response = await fetch(`${baseUrl}/images/generations`, { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: req.model || "agnes-image-2.1-flash", prompt: req.prompt, size: req.size || "1024x768", extra_body: { response_format: "url" }, }), }); // ... 处理返回数据 },});generateImage(req) 是 OpenClaw 框架调用的方法,你只需要在里面调 API、拿结果、返回。框架负责把结果发给 agent,agent 再呈现给用户。
第四步:处理 Agnes API 的特殊性
Agnes 的 API 和 OpenAI 标准格式有一点不同——它不支持返回 base64 编码的图片,只返回图片的 URL。
所以需要在插件里多加一步:拿到 URL 后,fetch 下载回来,转成 buffer 再返回给框架。
async function downloadImage(imageUrl) { const response = await fetch(imageUrl, { signal: AbortSignal.timeout(60_000), }); if (!response.ok) return null; const buffer = Buffer.from(await response.arrayBuffer()); // 根据文件头判断格式 let mimeType = "image/png"; if (buffer[0] === 0xff && buffer[1] === 0xd8) { mimeType = "image/jpeg"; } else if (buffer.toString("ascii", 0, 4) === "RIFF") { mimeType = "image/webp"; } return { buffer, mimeType };}第五步:顺便把视频生成也加上
Agnes 还提供了免费的 agnes-video-v2.0 视频模型,我就顺手也加上了。视频生成比图片复杂一些——它是异步任务,需要创建任务 → 轮询结果 → 下载视频:
api.registerVideoGenerationProvider({ id: "agnes", label: "Agnes AI", defaultModel: "agnes-video-v2.0", defaultTimeoutMs: 900_000, // 15分钟超时 async generateVideo(req) { // 1. 创建视频任务 const createResp = await fetch(`${baseUrl}/videos`, { method: "POST", headers: { "Authorization": `Bearer ${apiKey}` }, body: JSON.stringify({ model: "agnes-video-v2.0", prompt: req.prompt, num_frames: 121, frame_rate: 24, width: 1152, height: 768, }), }); const taskData = await createResp.json(); // 2. 轮询等待完成 while (Date.now() < deadline) { await new Promise(r => setTimeout(r, 15000)); const statusResp = await fetch(pollUrl, { headers: { "Authorization": `Bearer ${apiKey}` }, }); const result = await statusResp.json(); if (result.status === "completed") { // 3. 下载视频 const videoResp = await fetch(result.remixed_from_video_id); const buffer = Buffer.from(await videoResp.arrayBuffer()); return { videos: [{ buffer, mimeType: "video/mp4" }] }; } } },});配置一下就能用了
插件写好后,重启 OpenClaw,然后在 openclaw.json 里把它设为默认的图片生成模型:
{ "agents": { "defaults": { "imageGenerationModel": { "primary": "agnes/agnes-image-2.1-flash", "fallbacks": [ "sensenova/sensenova-u1-fast" ] } } }}API Key 可以通过环境变量 AGNES_API_KEY 配置,或者在 openclaw.json 里写:
{ "models": { "providers": { "agnes": { "apiKey": "你的API_KEY", "baseUrl": "https://apihub.agnes-ai.cn/v1" } } }}配置好之后,在对话里直接说"帮我画一只穿着西装的猫"或者"生成一段海边的视频",OpenClaw 就会调用 Agnes 的免费 API 来生成。
踩坑记录:这些坑我替你踩过了
坑1:插件放错位置
问题: 放 plugins/ 目录下不加载。解决方案: 放 extensions/ 下。plugins/ 只能通过 openclaw plugins install 安装,手动放不会被扫描到。
坑2:Agnes 不支持 base64
问题: Agnes API 只返回 URL,不返回 base64 图片数据。OpenClaw 默认期望的是 base64。解决方案: 在插件里多一步——下载 URL 转 buffer。
坑3:视频轮询接口偶发连接重置
问题: 用 GET /v1/videos/{taskId} 轮询时,时不时 TLS 握手失败。解决方案: 改用 GET /agnesapi?video_id={video_id} 接口,这个接口更稳定。
坑4:视频URL藏在奇怪的字段里
问题: 任务完成后,返回的数据里视频 URL 不在常见的 url 或 output.url 字段,而是藏在 remixed_from_video_id 里。解决方案: 多看两眼返回数据,不要把字段名写死,多试几个可能性。
坑5:轮询时的网络抖动
问题: 轮询过程中偶发 TypeError(连接重置),一次失败就导致整个 Promise reject。解决方案: 加 try-catch 和重试逻辑,连续失败 10 次才放弃:
let consecutiveErrors = 0;while (Date.now() < deadline) { await new Promise(r => setTimeout(r, 15000)); try { const resp = await fetch(pollUrl, { /* ... */ }); // 处理成功情况 consecutiveErrors = 0; } catch (err) { consecutiveErrors++; if (consecutiveErrors >= 10) throw err; // 否则继续重试 }}写完之后的感觉
说实话,写完这个插件最大的感受不是"我好牛",而是"原来 OpenClaw 的插件系统设计得挺清晰的"。
你不需要理解 OpenClaw 的底层实现,不需要改它的源码,只需要写一个符合规范的插件,它就能自动发现、自动加载。manifest 声明能力,入口文件实现逻辑,框架负责调度。
而且最爽的是——现在我可以在对话里直接让 OpenClaw 生成图片和视频,完全免费。 输入"帮我画一张赛博朋克风格的城市夜景",等几秒钟,图就出来了。
如果你也在用 OpenClaw,不妨也试试自己写个插件——从图片生成这种单一能力入手,门槛其实很低。
附:插件完整源码
点阅读原文提取。
以上,如果觉得不错,随手点个赞、在看、转发三连吧,如果想第一时间收到推送,也可以给我个星标⭐~谢谢你看我的文章,我们,下次再见。
本文基于 OpenClaw 2026.7.1 版本
我的Openclaw教程
夜雨聆风