2358 字
12 分钟
用 Cloudflare Workers 做了两个小工具:短链接和 AI 小助手

上一篇我用 Cloudflare Pages 发布了这个博客。Pages 只能放“提前做好的”静态网页,要让网站真的“干点活”(比如生成短链接、调用 AI),就要用到 Cloudflare Workers。

这两天我用 Workers 做了两个小工具,现在都放在博客的 工具页 里,可以直接试用:

  • 短链接 dreamer-link:把长网址变成短网址,还能看点击次数
  • AI 小助手 dreamer-ai:中英互译 + 简单问答

下面按顺序记录一下我是怎么做的,以及踩过的坑。

Workers 是什么#

可以把 Worker 理解成“一段放在 Cloudflare 机房里的 JavaScript 函数”。每次有人访问它的网址,Cloudflare 就在离访客最近的机房里运行这个函数,函数返回什么,访客就看到什么。

最小的 Worker 长这样:

export default {
async fetch(request, env, ctx) {
return new Response("Hello, Dreamer!");
},
};
  • request:这次请求的信息(网址、方法、请求头、请求体)
  • env:绑定(Bindings),比如数据库、AI,后面会讲
  • ctx:可以用 ctx.waitUntil() 让一些任务在返回响应之后继续在后台跑完

不用买服务器,免费版每天有 10 万次请求,学习完全够用。

工具一:短链接(Workers + KV)#

思路#

短链接的本质就是一张“对照表”:

短码原网址
AcsDNYhttps://blog.dogelover.online/posts/...

这张表我存在 Workers KV 里。KV 是 Cloudflare 的“键值存储”,就像一个超大的字典:用键(key)存,用键取。这里键是 6 位短码,值是一段 JSON:

{"url":"https://...","clicks":3,"created":"2026-10-08T12:00:00.000Z"}

Worker 只需要处理几种请求:

  1. POST /api/shorten:收到一个长网址,生成短码,存进 KV
  2. GET /xxxxxx:按短码查 KV,找到就跳转过去
  3. GET /api/stats/xxxxxx:查看点击次数

生成短链接#

核心就是一句 env.LINKS.put()。LINKS 是我给 KV 绑定起的名字,KV 只能存字符串,所以要先 JSON.stringify:

// POST /api/shorten(节选)
const check = validateUrl(body && body.url, selfUrl); // 只接受正常的 http/https 网址
if (!check.ok) return jsonResponse({ error: check.error }, 400);
const code = await createUniqueCode(env); // 生成一个没被占用的 6 位短码
const record = { url: check.url, clicks: 0, created: new Date().toISOString() };
await env.LINKS.put(code, JSON.stringify(record));
return jsonResponse({ short: `${selfUrl.origin}/${code}`, code, url: check.url }, 201);

跳转 + 点击计数#

访问短链接时,查到记录就返回 302 跳转。这里有两个我觉得挺有意思的小细节:

const record = await env.LINKS.get(code, { type: "json" });
if (!record || !record.url) return htmlResponse(notFoundPage(code), 404);
// 点击数 +1:放进 waitUntil,先让用户跳走,写 KV 在后台慢慢做
const updated = { ...record, clicks: (record.clicks || 0) + 1 };
ctx.waitUntil(
env.LINKS.put(code, JSON.stringify(updated)).catch((err) =>
console.warn("点击计数写入失败:", err)
)
);
return new Response(null, {
status: 302,
headers: { Location: record.url, "Cache-Control": "no-store" },
});
  • 为什么用 302 而不是 301? 301 是“永久跳转”,浏览器会把它缓存起来,下次直接跳,不再经过 Worker,点击就数不到了。302 是“临时跳转”,每次都会来问一遍。
  • 为什么用 ctx.waitUntil? 写 KV 需要一点时间,没必要让访客干等。先把跳转发出去,计数在后台写完就行。.catch 保证就算写入失败,跳转也不受影响。

免费额度要注意#

KV 免费版每天读取 10 万次,但写入只有 1,000 次。我这里“生成一个短链接”是一次写入,“每被点击一次”也是一次写入。自己用完全够,但如果哪天某个短链接被疯狂转发,计数可能当天就写不进去了(跳转本身不受影响,因为只用到读取)。

工具二:AI 小助手(Workers AI)#

绑定 AI#

Workers AI 让 Worker 可以直接调用 Cloudflare 机房里的大模型,不需要自己申请什么 API Key。只要在 Worker 的设置里加一个 AI 绑定,起名叫 AI,代码里就能用 env.AI.run(模型ID, 参数) 调用模型。

我用的主模型是 @cf/zai-org/glm-4.7-flash,备用模型是 @cf/qwen/qwen3-30b-a3b-fp8:主模型出错时自动换备用的再试一次。

const CONFIG = {
MODEL: "@cf/zai-org/glm-4.7-flash",
FALLBACK_MODEL: "@cf/qwen/qwen3-30b-a3b-fp8",
};
async function runAI(env, ctx, messages, { temperature, stream, meta }) {
const models = [CONFIG.MODEL, CONFIG.FALLBACK_MODEL];
let lastError = null;
for (const model of models) {
try {
const result = await env.AI.run(model, { messages, stream, temperature });
if (!stream) return jsonResponse({ ok: true, model, text: extractText(result) });
return sseResponse(convertStream(result, { model, ...meta }), ctx);
} catch (err) {
lastError = err;
if (isQuotaError(err)) break; // 额度用完了,换模型也没用
}
}
return jsonError(429, "今天的免费额度用完了,明天再来吧。");
}

messages 就是和 ChatGPT 类似的对话格式:一条 system 告诉 AI “你是谁、要怎么做”,后面是 user 和 assistant 轮流说话。翻译和问答其实是同一个模型,只是 system 提示词不一样。翻译方向(中→英还是英→中)我用代码先判断好,再写进提示词,比让 AI 自己猜稳定得多。

流式输出(SSE)#

如果等 AI 把整段话写完再返回,用户要盯着空白页面等好几秒。所以我开了 stream: true,让模型“边想边吐字”,Worker 再用 SSE(Server-Sent Events) 一点一点推给网页:

return new Response(readable, {
headers: {
"content-type": "text/event-stream; charset=utf-8",
"cache-control": "no-cache",
},
});

网页收到的是一行一行的 data: {...},每行带几个新字,拼起来就是打字机效果。

免费额度:Neurons#

Workers AI 按 Neurons 计量,免费版每天 10,000 Neurons,北京时间每天早上 8 点(UTC 0 点)重置。我做的这种短翻译和短问答,一次只消耗一点点。

让我比较放心的一点是:免费版额度用完后只会报错,不会扣钱。我在代码里把这个错误识别出来,换成了一句中文提示“今天的免费额度用完了”,而不是甩给用户一堆英文报错。

把两个工具放进博客:CORS#

一开始两个工具各有各的网页,分别在 dreamer-link.….workers.dev 和 dreamer-ai.….workers.dev。我想把它们都放进博客的 工具页,网页用博客的样式,背后还是调用这两个 Worker。

结果第一次预览,工具页只显示“暂时连不上”。原因就是 跨域(CORS)。

为什么会被拦#

博客在 blog.dogelover.online(以及原来的 dreamer-home.pages.dev),Worker 在 workers.dev,对浏览器来说这是两个不同的网站。出于安全考虑,浏览器默认不让一个网站的页面去读另一个网站接口返回的内容,除非对方明确说“我允许你”。

而且像我这种发 JSON 的 POST 请求,浏览器会先偷偷发一个 OPTIONS 预检请求,问 Worker:“blog.dogelover.online 想用 POST 调你,行不行?” Worker 回答“行”,真正的请求才会发出去。

我的做法:白名单#

我在两个 Worker 里都加了一份允许名单,只放行我的博客、博客的预览地址和本机调试地址:

CORS_ORIGINS: ["https://blog.dogelover.online", "https://dreamer-home.pages.dev"],
function isAllowedOrigin(origin) {
if (!origin) return false;
if (CONFIG.CORS_ORIGINS.includes(origin)) return true;
// 博客的 Pages 预览地址,例如 https://3f2a1b4c.dreamer-home.pages.dev
if (/^https:\/\/[a-z0-9-]+\.dreamer-home\.pages\.dev$/.test(origin)) return true;
// 本机调试:http://localhost:4321 之类
if (/^http:\/\/(localhost|127\.0\.0\.1)(:\d{1,5})?$/.test(origin)) return true;
return false;
}
function corsHeaders(request) {
const origin = request.headers.get("origin");
const headers = { Vary: "Origin" };
if (isAllowedOrigin(origin)) {
headers["Access-Control-Allow-Origin"] = origin;
headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS";
headers["Access-Control-Allow-Headers"] = "Content-Type";
headers["Access-Control-Max-Age"] = "86400"; // 预检结果缓存 1 天
}
return headers;
}
// 在 fetch 入口最前面:预检请求直接回 204
if (request.method === "OPTIONS") return preflightResponse(request);

这里我没有偷懒写 Access-Control-Allow-Origin: *(允许所有网站)。如果写了 *,别人把我的工具页照抄一份放到他自己的网站上,就能白嫖我的 AI 额度。用白名单的话,别的网站在浏览器里就调不动了。

小提醒:CORS 只是浏览器的规矩。如果有人直接用 curl 之类的工具调接口,CORS 拦不住,所以 Worker 里该做的输入检查(网址格式、长度限制)还是要做。

另外,发布顺序也有讲究:先更新 Worker(加上白名单),再更新博客。反过来的话,新工具页上线的那几分钟里会一直显示“连不上”。

踩过的坑#

  1. 在控制台粘贴代码出现乱码。 我是在 Cloudflare 控制台的在线编辑器里直接粘代码的。有一次代码里的中文注释粘进去变成了一堆乱码(mojibake),部署后网页上的中文也全乱了。后来换成从一个 UTF-8 编码的网页里复制代码,就正常了。教训:粘贴完先扫一眼中文,再点部署。
  2. 绑定要在 Settings 里加。 代码里写了 env.LINKS、env.AI,不代表它们就存在。必须到 Worker 的 Settings → Bindings 里手动添加 KV 命名空间和 Workers AI,并且名字要和代码里一模一样(大小写也算)。没绑定时 env.LINKS 是 undefined,会直接报错。我在 AI 小助手里加了一个 /api/health 接口,能直接看到“AI 绑定:已绑定 / 未绑定”,排查起来方便很多。
  3. AI 不太听话。 我让它“用一句话介绍”,它有时还是会写两段,还带了个外卖和便利店的比喻 😂。提示词写得越具体,效果越好,这个还得慢慢调。

小结#

这次学到的东西:

  • Workers:一段跑在 Cloudflare 机房里的函数
  • KV:简单的键值存储,读多写少的场景很合适,注意每天 1,000 次写入
  • Workers AI:通过绑定调用大模型,env.AI.run() 一行搞定,免费每天 10,000 Neurons,超了只报错不扣钱
  • SSE 流式输出:让 AI 回答有打字机效果
  • CORS:跨网站调用接口要对方放行,用白名单比用 * 安全

两个工具都在 工具页,欢迎来试试~

用 Cloudflare Workers 做了两个小工具:短链接和 AI 小助手
https://blog.dogelover.online/posts/workers-shortlink-and-ai/
作者
Dreamer
发布于
2026-10-08
许可协议
CC BY-NC-SA 4.0