从零搭建 AI 图床:Claude Code + Cloudflare R2 + OpenClaw 全流程保姆级指南

一个人、一句话、一个下午,用 Vibe Coding 搭出带图片编辑、文件夹管理、AI Agent 接入的完整图床系统。


目录


这篇文章讲什么

Image-Hosting/1773580969153-56a5787e-2c8e-495c-8eca-c49a5d62ba76.png
我想要一个自己的图床——能拖拽上传、能裁剪压缩、能管理文件夹、能让 AI 帮我自动传图。

市面上的图床要么付费、要么有审核、要么不支持 API。所以我决定自己搭一个。

整个过程我几乎没手写代码,全程 Vibe Coding——用自然语言告诉 Claude Code 我要什么,它帮我写。从后端 API 到前端 UI 到 AI Agent 接入,一个下午全搞定。

技术栈:

组件 技术 作用
对象存储 Cloudflare R2 存图片,免费额度很大
后端 API Cloudflare Worker 处理上传/删除/重命名
前端 UI 单文件 HTML 管理界面,Lightroom 风格暗色主题
AI 接入 Python / Node.js SDK 让 AI Agent 程序化管理图床
AI 工作流 OpenClaw AI Agent 平台,通过 Skill 调用图床

最终成果预览

Image-Hosting/1773580969153-56a5787e-2c8e-495c-8eca-c49a5d62ba76.png
Image-Hosting/1773580971113-51151ce4-c756-440c-bf38-76ae1529f08b.png
Image-Hosting/1773580970038-7ae0e570-6360-42b8-b9f7-f58ee8bf9916.png
Image-Hosting/1773580967899-68354a62-c7f3-4ef2-a80a-b0c47cbd048b.png
Image-Hosting/1773580967047-81477455-78b2-40f9-a6f9-51f5d85b9457.png
你会得到一个这样的系统:

  • 单文件 HTML 管理器(~1870 行):暗色主题、拖拽上传、图片网格、文件夹树、灯箱预览、图片编辑(裁剪/压缩/格式转换)、批量操作、一键复制链接
  • Cloudflare Worker API(~190 行):处理上传(FormData + JSON 两种模式)、列表(支持文件夹浏览)、删除、重命名/移动、防盗链
  • Python & Node.js SDK:让 AI Agent 或脚本通过代码管理图床
  • OpenClaw Skill:让 AI 用自然语言管理你的图床(”帮我把这张图上传到 blog 文件夹”)

你需要准备什么

工具 用途 获取方式
Cloudflare 账号 R2 存储 + Worker dash.cloudflare.com 免费注册
Node.js 18+ 运行 Wrangler CLI nodejs.org
Claude Code Vibe Coding 主力 npm install -g @anthropic-ai/claude-code
OpenClaw(可选) AI Agent 平台 openclaw.ai
一个域名(可选) 自定义图片链接 任意域名注册商

第一步:创建 Cloudflare R2 存储桶

1.1 开通 R2

登录 Cloudflare Dashboard → 左侧菜单找到 R2 对象存储 → 创建存储桶。

存储桶名称: my-image-bucket    # 随便起,记住就行
位置提示: 亚太地区              # 选离你近的

免费额度:R2 每月免费 10GB 存储 + 1000 万次读取 + 100 万次写入,个人图床完全够用。

1.2 初始化 Worker 项目

打开终端,用 Wrangler 创建项目:

# 安装 Wrangler(Cloudflare 的 CLI 工具)
npm install -g wrangler

# 登录 Cloudflare
wrangler login

# 创建项目
mkdir my-picbed && cd my-picbed
npm init -y

1.3 配置 wrangler.toml

创建 wrangler.toml:

name = "my-picbed"
main = "src/index.js"
compatibility_date = "2024-12-01"

[[r2_buckets]]
binding = "BUCKET"
bucket_name = "my-image-bucket"    # 改成你的存储桶名

1.4 设置认证令牌

# 生成一个随机令牌(或者用你自己想的密码)
wrangler secret put AUTH_TOKEN
# 输入你的令牌,比如: MySecretToken123

这个令牌用于保护上传/删除等写操作,防止别人乱传图片到你的桶里。


第二步:用 Claude Code Vibe 出 Worker 后端

这是 Vibe Coding 的精华部分。打开 Claude Code:

cd my-picbed
claude

2.1 第一版:基础 CRUD

给 Claude Code 的第一个 Prompt:

帮我写一个 Cloudflare Worker,作为图床的后端 API。

功能:
1. POST / — 上传图片(FormData 格式,字段 file + path)
2. GET /{key} — 获取图片,返回图片内容
3. GET /list — 列出所有图片(需要认证)
4. DELETE /{key} — 删除图片(需要认证)

认证方式:请求头 X-Auth-Token 与环境变量 AUTH_TOKEN 比较。
CORS:允许所有来源。
R2 Bucket 绑定名为 BUCKET。
文件放在 src/index.js。

Claude Code 会直接生成 src/index.js,一个完整可用的 Worker。

2.2 迭代加功能

Vibe Coding 的核心是对话式迭代。基础版跑通后,我继续跟 Claude Code 对话,逐步加功能:

加重命名:

给 Worker 加一个 PUT /rename 接口,接收 JSON { oldKey, newKey },
实现方式是:读取旧文件 → 写入新 key → 删除旧文件。

加防盗链:

给 GET 图片接口加防盗链检查。
检查 Referer 头,只允许以下域名访问:
- example.com(和所有子域名)
- localhost
- 127.0.0.1
如果 Referer 不在白名单里,返回 403。
没有 Referer 的请求(直接访问)放行。

加 AI Agent 接口:

POST 接口除了支持 FormData,还要支持 JSON 模式:
1. { url: "https://...", folder: "blog", filename: "photo.jpg" } — 从 URL 抓图上传
2. { base64: "...", contentType: "image/png", folder: "blog" } — base64 上传

这样 AI Agent 就能通过 JSON API 上传图片了。

加文件夹浏览:

GET /list 接口加参数支持:
- ?prefix=blog/&delimiter=/ — 列出 blog/ 文件夹下的文件和子文件夹
- 无参数时列出所有文件(兼容旧行为)
返回格式:{ files: [...], folders: [...], prefix: "blog/" }

每个 Prompt 就是一个需求,Claude Code 直接改代码。整个 Worker 从 50 行迭代到 190 行,全程没手写一行。

2.3 最终 Worker 架构

POST /           → 上传(FormData 前端用 / JSON AI Agent 用)
GET  /list       → 列出文件(支持文件夹浏览)
GET  /{key}      → 获取图片(带防盗链检查)
PUT  /rename     → 重命名/移动文件
DELETE /{key}    → 删除文件
OPTIONS *        → CORS 预检

核心代码结构(简化版):

export default {
  async fetch(request, env) {
    const corsHeaders = {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, X-Auth-Token",
    };

    // CORS 预检
    if (request.method === "OPTIONS") {
      return new Response(null, { headers: corsHeaders });
    }

    const url = new URL(request.url);
    const key = decodeURIComponent(url.pathname.slice(1));

    // 认证检查(写操作)
    const checkAuth = () => {
      const token = request.headers.get("X-Auth-Token");
      return token === env.AUTH_TOKEN;
    };

    // GET /list — 文件列表
    if (request.method === "GET" && key === "list") {
      if (!checkAuth()) return new Response("Unauthorized", { status: 401 });
      // ... 列出文件,支持 prefix 和 delimiter 参数
    }

    // GET /{key} — 获取图片(带防盗链)
    if (request.method === "GET" && key) {
      // 检查 Referer 是否在白名单
      const referer = request.headers.get("Referer");
      if (referer) {
        const allowedHosts = ["example.com", "localhost", "127.0.0.1"];
        const refHost = new URL(referer).hostname;
        if (!allowedHosts.some(h => refHost === h || refHost.endsWith("." + h))) {
          return new Response("Forbidden", { status: 403 });
        }
      }
      // ... 返回图片内容
    }

    // POST / — 上传(支持 FormData 和 JSON 两种模式)
    if (request.method === "POST") {
      if (!checkAuth()) return new Response("Unauthorized", { status: 401 });
      const ct = request.headers.get("Content-Type") || "";

      if (ct.includes("application/json")) {
        const body = await request.json();
        if (body.url) { /* 从 URL 抓图上传 */ }
        if (body.base64) { /* base64 解码上传 */ }
      } else {
        // FormData 模式(前端用)
      }
    }

    // PUT /rename — 重命名
    // DELETE /{key} — 删除
  }
};

第三步:Vibe 出 HTML 前端管理器

这是最大的一个 Prompt,我把所有想要的功能一次性描述清楚:

帮我做一个单文件 HTML 的图床管理工具 PicManager.html,要求:

【上传功能】
- 拖拽上传 + 点击选择文件
- 上传到当前浏览的文件夹
- 上传进度条

【文件管理】
- 图片网格展示,可调节大小
- 单选/多选/全选
- 重命名、移动、删除
- 批量操作(批量删除、批量复制链接)
- 搜索过滤
- 按时间/大小/名称排序

【文件夹管理】
- 左侧文件夹树
- 面包屑导航
- 创建/删除文件夹
- 文件夹间移动文件

【图片预览】
- 点击放大(灯箱模式)
- 左右翻页

【图片编辑】
- 裁剪(自由/1:1/4:3/16:9)
- 压缩(质量滑块)
- 调整尺寸
- 格式转换(JPG/PNG/WebP)
- 保存为新文件或覆盖原文件

【链接复制】
- 一键复制:原始 URL、Markdown、HTML、BBCode

【UI 设计】
- Lightroom 风格暗色主题
- 响应式布局

【配置】
- Worker URL、Auth Token、自定义域名存在 localStorage
- 首次使用弹出设置窗口

图片裁剪用 Cropper.js(CDN 引入)。
其他零依赖,纯原生 JS。

Claude Code 一次性生成了 1870 行的完整单文件应用。拖拽上传、灯箱预览、图片编辑、文件夹树——全部可用。

迭代修 Bug

生成的代码不可能一次完美,但 Vibe Coding 的好处是修 Bug 也用自然语言:

图片编辑器加载图片失败了,控制台报 CORS 错误。
能不能改成先通过 Worker API fetch 图片,
然后转成 blob URL 再给编辑器用?
这样就绕过了 canvas 的跨域安全策略。

Claude Code 理解了问题本质,把 <img crossOrigin="anonymous" src="..."> 改成了 blob URL 方案:

// 修复前:直接加载外部 URL,canvas 会报 CORS
img.crossOrigin = 'anonymous';
img.src = imageUrl;

// 修复后:通过 Worker 中转,完全绕过 CORS
const res = await fetch(workerUrl + '/' + encodeURIComponent(key));
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;

这就是 Vibe Coding 解 Bug 的体验——你描述现象和期望,AI 找到根因并修复。


第四步:部署上线

4.1 部署 Worker

wrangler deploy

部署成功后会得到一个 URL,类似:

https://my-picbed.your-account.workers.dev

4.2 使用前端

直接在浏览器打开 PicManager.html(双击就行,不需要服务器)。

首次使用会弹出设置窗口,填入:
– Worker URL: https://my-picbed.your-account.workers.dev
– Auth Token: 你之前设置的令牌
– 自定义域名: (可选,后面讲)

点保存,就能开始用了。


第五步:接入自定义域名(可选)

默认的 workers.dev URL 太长,可以绑定自己的域名。

5.1 在 Cloudflare 添加域名

确保你的域名在 Cloudflare DNS 管理下(nameserver 指向 Cloudflare)。

5.2 添加自定义域

Cloudflare Dashboard → Workers → 你的 Worker → 设置 → 域和路由 → 添加 → 自定义域。

输入你想用的子域名,比如 img.example.com。

Cloudflare 会自动配置 DNS 记录和 SSL 证书。等几分钟生效后,你的图片就可以通过类似 https://img.example.com/blog/photo.jpg 这样的短链接访问了。

5.3 更新前端配置

在 PicManager 设置里填入自定义域名,这样复制的链接会自动使用短域名。


第六步:让 AI Agent 管理你的图床

这是整个项目最有趣的部分——让 AI 通过自然语言管理你的图床。

6.1 Python SDK

先给 AI Agent 准备一个 SDK(还是让 Claude Code 写的):

帮我写一个 Python SDK picmanager.py,封装图床 API。
功能:upload_from_url, upload_file, upload_base64, list_all,
list_folder, search, rename, move, delete, url, markdown。
依赖 requests 库。

使用示例:

from picmanager import PicManager

pm = PicManager(
    worker_url="https://my-picbed.your-account.workers.dev",
    auth_token="your-token",
    custom_domain="https://img.example.com"
)

# 从 URL 抓图上传
result = pm.upload_from_url("https://example.com/photo.jpg", folder="blog")
print(result["url"])  # https://img.example.com/blog/photo.jpg

# 上传本地文件
result = pm.upload_file("/path/to/image.png", folder="avatars")

# 获取 Markdown 引用
md = pm.markdown("blog/photo.jpg", alt="博客配图")
# ![博客配图](https://img.example.com/blog/photo.jpg)

# 列出所有图片
images = pm.list_all()

# 搜索
results = pm.search("photo")

# 获取统计
info = pm.info()
# {"total_files": 42, "total_size_human": "128.3 MB", ...}

6.2 Node.js SDK

同样让 Claude Code 写一个 Node.js 版本,零依赖(使用 Node 18+ 内置 fetch):

import { PicManager } from './picmanager.mjs';

const pm = new PicManager({
  workerUrl: 'https://my-picbed.your-account.workers.dev',
  authToken: 'your-token',
  customDomain: 'https://img.example.com'
});

const result = await pm.uploadFromUrl('https://example.com/photo.jpg', { folder: 'blog' });
console.log(result.url);

6.3 OpenClaw 接入

OpenClaw 是一个 AI Agent 平台,支持本地运行和云端部署。它可以通过 Skill 扩展能力。

什么是 OpenClaw Skill?

Skill 是一个文件夹,里面有一个 SKILL.md 文件,用 Markdown 告诉 AI Agent “你能做什么、怎么做”。Agent 读到这个文件,就学会了一项新技能。

创建 PicManager Skill

~/.openclaw/workspace/skills/picmanager/
├── SKILL.md          # 技能描述(Agent 的说明书)
├── cli.mjs           # CLI 命令行工具
├── picmanager.mjs    # Node.js SDK
└── .env              # 配置(Worker URL + Token)

SKILL.md(核心文件,告诉 Agent 怎么用图床):

---
name: picmanager
description: 管理图床——上传、列出、搜索、重命名、移动、删除图片,获取图片链接。
---

# PicManager — 图床管理技能

你可以通过此技能管理用户的图床。

## 使用方法

通过 system.run 调用 CLI 工具:

\```bash
(cd ~/.openclaw/workspace/skills/picmanager && set -a && source .env && set +a && node cli.mjs <command>)
\```

### 可用命令

- upload-url <url> [--folder <f>] — 从 URL 抓图上传
- upload-file <path> [--folder <f>] — 上传本地文件
- list — 列出所有图片
- list-folder [path] — 列出文件夹内容
- search <keyword> — 搜索图片
- rename <old> <new> — 重命名
- move <key> <folder> — 移动文件
- delete <key> — 删除文件
- url <key> — 获取访问链接
- markdown <key> [alt] — 获取 Markdown 引用
- info — 获取统计信息

cli.mjs(Agent 实际调用的命令行工具):

#!/usr/bin/env node
import { PicManager } from './picmanager.mjs';

const [,, cmd, ...args] = process.argv;

function getPM() {
  const workerUrl = process.env.PICMANAGER_WORKER_URL;
  const authToken = process.env.PICMANAGER_AUTH_TOKEN;
  if (!workerUrl || !authToken) {
    console.error(JSON.stringify({ error: '请设置环境变量' }));
    process.exit(1);
  }
  return new PicManager({
    workerUrl,
    authToken,
    customDomain: process.env.PICMANAGER_CUSTOM_DOMAIN || ''
  });
}

// ... switch(cmd) 分发到 SDK 各方法

.env(敏感配置,不进版本控制):

PICMANAGER_WORKER_URL=https://my-picbed.your-account.workers.dev
PICMANAGER_AUTH_TOKEN=your-token-here
PICMANAGER_CUSTOM_DOMAIN=https://img.example.com

OpenClaw 本地端 vs 云端

对比项 本地端 (Desktop) 云端 (Server)
运行环境 你的电脑 远程服务器
安装方式 下载桌面应用或 npm install Docker 部署或进程管理
Skill 目录 ~/.openclaw/workspace/skills/ 同路径(服务器上)
特有能力 浏览器控制、屏幕截图、本地文件访问 7×24 在线、定时任务、Webhook 触发
图床场景 手动对话:”帮我上传这张图” 自动化:”监控文件夹,新图自动上传”

本地端典型用法:

你在写博客时,对 OpenClaw 说:

“帮我把桌面上的 screenshot.png 上传到 blog-2024 文件夹,然后给我 Markdown 链接”

Agent 执行:

node cli.mjs upload-file ~/Desktop/screenshot.png --folder blog-2024
node cli.mjs markdown blog-2024/screenshot.png "截图"

返回:

![截图](https://img.example.com/blog-2024/screenshot.png)

云端典型用法:

在服务器上配置 OpenClaw,结合定时任务或 Webhook:

  • 定时清理 30 天前的临时图片
  • 接收 Webhook 后自动从 URL 抓图并上传
  • 批量处理:把某个文件夹的图片全部移动到归档目录
  • 与其他 Skill 联动:写完文章后自动提取图片 URL 并上传到自己的图床
# 示例:定时清理脚本(可作为 cron 或 OpenClaw 定时任务)
node cli.mjs list | node -e "
  const imgs = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
  const cutoff = Date.now() - 30*24*60*60*1000;
  const old = imgs.filter(i => new Date(i.uploaded) < cutoff && i.key.startsWith('temp/'));
  old.forEach(i => console.log(i.key));
"
# 然后对每个 key 执行 delete

踩过的坑与解决方案

坑 1:图片编辑器 CORS 报错

现象:在 HTML 前端的图片编辑器里,裁剪后导出 canvas 报 “Tainted canvases may not be exported”。

原因:<img> 标签设置 crossOrigin="anonymous" 加载外部图片时,如果服务器的 CORS 头不完美,canvas 会被标记为”受污染”,禁止 toBlob()/toDataURL()。

解决:不用 <img> 直接加载外部 URL,改成先通过 Worker API 把图片 fetch 下来,转成 blob URL:

const res = await fetch(workerUrl + '/' + encodeURIComponent(key));
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
img.src = blobUrl;  // blob: 协议不受 CORS 限制

坑 2:R2 没有”文件夹”概念

现象:想实现文件夹管理,但 R2(和所有对象存储)没有真正的文件夹。

解决:用 key 中的 / 模拟文件夹结构。blog/photo.jpg 就是”blog 文件夹下的 photo.jpg”。

R2 的 list() 方法支持 prefix 和 delimiter 参数:

// 列出 blog/ 下的内容
const list = await env.BUCKET.list({ prefix: "blog/", delimiter: "/" });
// list.objects → 该文件夹下的文件
// list.delimitedPrefixes → 子文件夹列表

创建空文件夹:上传一个 0 字节的 .folder 占位文件。

坑 3:防盗链要考虑子域名

现象:白名单写了 example.com,但 blog.example.com 被拦截了。

解决:检查时同时匹配精确域名和子域名:

allowedHosts.some(h => refHost === h || refHost.endsWith("." + h))

坑 4:单文件 HTML 的配置管理

现象:不想把 Token 硬编码在 HTML 里,但又不想引入构建工具。

解决:用 localStorage 存配置,首次打开弹出设置窗口。配置存在用户浏览器本地,HTML 文件本身不含任何敏感信息,可以放心分享。


完整文件清单

my-picbed/
├── src/
│   └── index.js              # Cloudflare Worker(后端 API,~190 行)
├── wrangler.toml              # Wrangler 部署配置
├── PicManager.html            # 前端管理器(单文件,~1870 行)
├── picmanager.py              # Python SDK(依赖 requests)
├── picmanager.mjs             # Node.js SDK(零依赖)
└── package.json

# OpenClaw Skill(如果用 OpenClaw)
~/.openclaw/workspace/skills/picmanager/
├── SKILL.md                   # 技能描述
├── cli.mjs                    # CLI 命令行工具
├── picmanager.mjs             # Node.js SDK(同上)
└── .env                       # 配置(敏感信息)

写在最后

Vibe Coding 的体验

整个项目从零到完成,我做的事情是:

  1. 描述需求(用自然语言)
  2. 测试效果(在浏览器里点点看)
  3. 反馈问题(”这里有 Bug”、”加个功能”)

没有查文档、没有写模板代码、没有调 CSS 像素。Claude Code 处理了所有技术细节——从 Cloudflare Worker 的 API 设计到 Cropper.js 的集成到 R2 文件夹模拟的实现。

这不是说”AI 取代了编程”,而是编程的接口变了。以前的接口是键盘和 IDE,现在多了一个自然语言的接口。你需要知道自己要什么(产品思维),但不需要知道每一行怎么写。

费用

项目 费用
Cloudflare R2 免费(10GB 以内)
Cloudflare Worker 免费(10 万次/天)
域名 看你选什么,十几块到几百块/年
Claude Code Anthropic API 费用,这个项目大概花了几美元
OpenClaw 免费版可用

总计:几美元 + 域名费(可选),得到一个完全自主可控的图床系统。

扩展方向

  • 图片 CDN 优化:Cloudflare 自带全球 CDN,R2 的读取本身就很快
  • 水印功能:在 Worker 里用 WebAssembly 加水印
  • 图片转换:上传时自动转 WebP 节省空间
  • 多用户:给不同用户分配不同的文件夹和令牌
  • 接入更多 AI:除了 OpenClaw,任何能发 HTTP 请求的 AI Agent 都能用 JSON API

本文是一次真实的 Vibe Coding 实践记录。所有代码均由 Claude Code 生成,作者负责需求描述和方向把控。


了解 ~/jintaoblog/朝夕见闻志⚡️ 的更多信息

订阅后即可通过电子邮件收到最新文章。