Cloudflare Workers 开发指南:路由、反向代理、KV 存储和 TCP Socket

Cloudflare Workers 是部署在 Cloudflare 全球边缘节点上的无服务器运行环境,写 JavaScript/TypeScript 即可,无需管理服务器。 快速开始 npm create cloudflare@latest my-worker cd my-worker npm run dev # 本地调试 npm run deploy # 部署基础结构 export default { async fetch(request, env, ctx) { return new Response("hello world"); }, };路由处理 export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/api/user") { return Response.json({ name: "soulock" }); } return new Response("404", { status: 404 }); }, };获取 query 参数: const name = url.searchParams.get("name");获取请求体: const data = await request.json();反向代理 export default { async fetch(request) { const url = new URL(request.url); const target = "https://example.com"; const targetUrl = new URL(url.pathname + url.search, target); const headers = new Headers(request.headers); headers.set("Host", targetUrl.host); const resp = await fetch(targetUrl.toString(), { method: request.method, headers, body: request.body, redirect: "follow", }); return new Response(resp.body, { status: resp.status, headers: resp.headers, }); }, };流式返回(AI 接口必须用这种写法): // 正确:保持流式 return new Response(resp.body, resp);// 错误:会阻塞到完整响应 const text = await resp.text();CORS 处理 function corsHeaders() { return { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "*", "Access-Control-Allow-Headers": "*", }; }export default { async fetch(request) { if (request.method === "OPTIONS") { return new Response(null, { headers: corsHeaders() }); } const resp = await fetch("https://example.com"); return new Response(resp.body, { status: resp.status, headers: { ...Object.fromEntries(resp.headers), ...corsHeaders() }, }); }, };环境变量 # wrangler.toml [vars] API_KEY = "xxx"代码里用 env.API_KEY。生产环境推荐用 secret(不写入代码): wrangler secret put API_KEYKV 存储 [[kv_namespaces]] binding = "MY_KV" id = "xxxx"await env.MY_KV.put("key", "value"); const value = await env.MY_KV.get("key");D1 数据库(SQLite) wrangler d1 create mydb[[d1_databases]] binding = "DB" database_name = "mydb" database_id = "xxxx"const result = await env.DB.prepare("SELECT * FROM users").all();定时任务 export default { async scheduled(event, env, ctx) { // 定时执行的逻辑 }, };[triggers] crons = ["*/5 * * * *"]TCP Socket Workers 支持主动发起 TCP 连接(不能监听 TCP): import { connect } from "cloudflare:sockets";const socket = connect({ hostname: "example.com", port: 80 }); const writer = socket.writable.getWriter(); await writer.write(new TextEncoder().encode("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")); writer.releaseLock(); return new Response(socket.readable);常见用途:WebSocket → TCP 隧道,代理 Redis、PostgreSQL 等 TCP 协议。 Workers 不能替代大流量 TCP 服务(有连接数和执行时间限制),适合轻量中转。 推荐:Hono 框架 npm i honoimport { Hono } from "hono";const app = new Hono();app.get("/", (c) => c.text("hello")); app.get("/user", (c) => c.json({ name: "soulock" }));// 反向代理 app.all("*", async (c) => { const url = new URL(c.req.url); const target = "https://example.com" + url.pathname + url.search; const resp = await fetch(target, { method: c.req.method, headers: c.req.raw.headers, body: c.req.raw.body }); return new Response(resp.body, resp); });export default app;适合与不适合的场景适合 不适合API 网关 / 鉴权 大型 CPU 运算AI 接口中转 ffmpeg / 视频编码Webhook / Bot Puppeteer反向代理 本地文件读写短链接 / 边缘缓存 高并发大流量 VPNWorkers 没有 fs、child_process、net 等 Node.js 原生模块,运行在沙盒环境中。

Cloudflare Workers 入门:从 hello world 到 KV / D1

Cloudflare Workers 本质上是"边缘 JS/TS 函数"——写一段代码,部署到全球 300 多个节点,用户就近访问。适合做 API 代理、Webhook、AI 接口中转、鉴权网关,比自建 Nginx 或买 VPS 省事。 项目起步 一个命令拉起脚手架: npm create cloudflare@latest my-worker cd my-worker npm run dev # 本地起 wrangler dev npm run deploy # 部署到生产第一次部署会让你登录: npx wrangler login最小骨架 新版模块化写法: export default { async fetch(request, env, ctx) { return new Response("hello world"); }, };访问 https://<name>.<account>.workers.dev 就能看到。 路由分发 原生没有路由框架,URL 手动分: export default { async fetch(request) { const url = new URL(request.url); if (url.pathname === "/api/user") { return Response.json({ name: "alice", age: 18 }); } if (url.pathname === "/health") { return new Response("ok"); } return new Response("Not Found", { status: 404 }); }, };要更完善的路由体验就上 Hono: import { Hono } from "hono"; const app = new Hono(); app.get("/api/user", (c) => c.json({ name: "alice" })); export default app;读取请求 // query string const url = new URL(request.url); const name = url.searchParams.get("name");// JSON body const data = await request.json();// 表单 const form = await request.formData();转发外部 API(最常见用法) export default { async fetch(request, env) { const resp = await fetch("https://api.openai.com/v1/models", { headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` }, }); return new Response(resp.body, resp); }, };常见用途:藏 API Key、破跨域、加限流、加缓存、AI 接口中转。Key 千万别写死在代码里,走 wrangler secret。 KV:类 Redis 键值 wrangler.toml: [[kv_namespaces]] binding = "MY_KV" id = "xxxxxxxx"代码: await env.MY_KV.put("username", "alice"); const v = await env.MY_KV.get("username"); await env.MY_KV.delete("username");KV 是最终一致(全球复制有几秒延迟),做缓存 / 配置 / session 都很合适。 D1:SQLite [[d1_databases]] binding = "DB" database_name = "mydb" database_id = "xxxxxxxx"用法: const rows = await env.DB .prepare("SELECT * FROM users WHERE id = ?") .bind(userId) .all();参数化查询是标配,别拼字符串——同样有 SQL 注入。 环境变量 vs Secret 普通变量写 wrangler.toml: [vars] APP_NAME = "my-app"敏感 key 用 secret,不进代码库: npx wrangler secret put OPENAI_API_KEY两种都通过 env.XXX 访问。 免费额度和局限免费 10 万次请求/天,付费 10 美元/月起 单请求 CPU 时间限制(免费 10ms,付费 50ms+,最高 5 分钟) 不能开 socket、不能持久保存本地文件 Node.js 内置模块要靠 nodejs_compat 兼容标志一句话总结 Workers = 全球边缘 JavaScript。npm create cloudflare@latest 起步,fetch 转发是 90% 用例,配上 KV/D1 就能扛住一个中等 API 项目。

Node 内置 SQLite 报 statement has been finalized 的原因

Node 22 开始有了内置的 node:sqlite,同步 API 用起来非常顺。但启动时挂: ExperimentalWarning: SQLite is an experimental feature Failed to start server Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) ... code: 'ERR_INVALID_STATE'这个错的字面意思很明确:你在一个已经 finalize() 过的 prepared statement 上继续调用 all() / get() / iterate()。 三种最常见的成因 1. 手动过早 finalize const stmt = db.prepare("SELECT * FROM devices"); try { return stmt.all(); } finally { stmt.finalize(); }看上去正确。但如果 stmt.all() 返回的是迭代器(iterate())而不是数组(all()),外层还在懒消费,finalize 一走就崩。用 all() 拿到具体数组的写法没这个问题。 2. 用了 using 自动 finalize(Node 22 的新语法) export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.iterate(); // ← 迭代器,出了作用域 stmt 就被 finalize 了 }using 借用了 TC39 的 Explicit Resource Management 提案,出作用域时会自动调 [Symbol.dispose](),也就是 finalize()。返回迭代器给外部继续用,外部一 next() 就炸。 规则:using + iterate() 不能混,要么用 all() 一次拿全部再返回,要么调用方也 using。 3. 模块级缓存了 stmt // 模块顶部 const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); }看着挺合理——避免每次 prepare 的开销。但代码其它地方可能:单元测试用 db.close() 关掉了数据库(stmt 一起 finalize) 有热重载/HMR 重新 import 了模块(旧 stmt 引用还留着) 显式调用了 readStmt.finalize() 想清理,然后忘了之后再走这个函数就 ERR_INVALID_STATE。 推荐写法 每次现 prepare,让 GC 管理: export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 立刻取完,不返回 iterator }现代 SQLite 的 prepare 开销很小,不需要为性能提前优化。 要缓存的话,配套复用而不是复用 + finalize: const cache = new Map<string, ReturnType<typeof db.prepare>>();function s(sql: string) { let stmt = cache.get(sql); if (!stmt) { stmt = db.prepare(sql); cache.set(sql, stmt); } return stmt; }// 用: s("SELECT * FROM device_states").all();不在缓存生命周期外手动 finalize。数据库 close 时统一 cache.clear()。 要用 using 就一次性拿完数据: export function readDeviceStates() { using stmt = db.prepare("SELECT * FROM device_states"); return stmt.all(); // 数组,可以安全跨作用域 }一句话总结 statement has been finalized = 你正在用一个死掉的 stmt。别返回迭代器给作用域外、别在模块级缓存又手动 finalize。改成"每次 prepare + all()"最省心。

Node.js SQLite statement has been finalized 错误:原因与修复

Error: statement has been finalized at readDeviceStates (src/lib/database.ts:76:38) { code: 'ERR_INVALID_STATE' }这个错误来自 Node.js 22+ 的内置 node:sqlite 模块,对已经调用了 finalize() 的 StatementSync 对象再次操作时抛出。 原因一:全局缓存 Statement 被意外 finalize // 错误写法:模块级共享 statement const readStmt = db.prepare("SELECT * FROM device_states");export function readDeviceStates() { return readStmt.all(); // 若 readStmt 被 finalize 则崩溃 }如果在某处调用了 readStmt.finalize(),后续所有使用都会报错。 原因二:using 关键字自动释放 Node.js SQLite 支持 using 声明(Explicit Resource Management),离开作用域后自动调用 finalize(): // 错误:返回迭代器后 stmt 已被 finalize function getRows() { using stmt = db.prepare("SELECT * FROM logs"); return stmt.iterate(); // 离开函数后 stmt 自动 finalize,迭代器失效 }修复:每次使用时重新 prepare // 正确写法:每次都 prepare,不缓存 export function readDeviceStates() { const stmt = db.prepare("SELECT * FROM device_states"); const result = stmt.all(); stmt.finalize(); return result; }或者更简洁,不手动调用 finalize(Node.js 会在 GC 时自动释放): export function readDeviceStates() { return db.prepare("SELECT * FROM device_states").all(); }需要性能优化时:用对象包装 如果 prepare 开销较大,可以在模块初始化时准备,但确保 finalize 只在明确不再使用时调用: class DeviceRepository { private readStmt: StatementSync; constructor(private db: DatabaseSync) { this.readStmt = db.prepare("SELECT * FROM device_states"); } getAll() { return this.readStmt.all(); } close() { this.readStmt.finalize(); this.db.close(); } }生命周期由对象管理,close() 明确释放资源。 ExperimentalWarning 的处理 (node:2436) ExperimentalWarning: SQLite is an experimental featurenode:sqlite 在 Node.js 22 中是实验性功能,可以用以下方式消除警告: node --no-experimental-warnings server.js或在代码中: process.removeAllListeners('warning');生产环境建议关注 Node.js 版本更新,等待 SQLite 模块稳定。

平台积分体系设计:Credits(余额)与 Points(积分)的区别与应用

Credits vs Points 是两套独立系统特性 Credits Points本质 消费余额(货币) 生态贡献积分可消费 是(API 调用扣费) 否可提现 是(换回 USDC/法币) 否是否等价法币 基本等价 不等价数量变化 实时增减 只增(或少量减)未来用途 功能消费 Token 空投、VIP、治理Credits:平台货币 用户充值后获得 Credits,Credits 直接用于消费: 充值 100 USDT = 获得 100 Credits 调用 API 消费 2 Credits → 余额 98 Credits 提现 50 Credits → 取回约 50 USDTCredits 必须与真实价值挂钩,用户信任建立在 1 Credit ≈ 1 USD 的基础上。 Points:生态积分 Points 不直接等于钱,是平台的"长期价值记录": 充值 100 USDT → +100 Points API 消费 20 USD → +20 Points 邀请好友充值 → +邀请奖励 Points 每日活跃 → +少量 PointsPoints 积累后可以用于:VIP 等级解锁 折扣和返佣 Token 空投快照 DAO 治理权重 锁仓质押防止套利:提现扣回 Points 如果充值得 Points、提现不扣,用户会利用套利: 1. 充值 1000 USDT → 得 1000 Credits + 1000 Points 2. 立刻提现 1000 Credits 3. 净得:0 Credits(本金拿回)+ 1000 Points(免费)解决方案:提现时扣回相应 Points: 提现 1000 Credits → 余额 -1000 Credits,同时 Points -1000或者更稳健:Points 只按实际消费累计,充值/提现不影响 Points: 充值 → Credits+,Points 不变 消费 1 USD → Points +1 提现 → Credits-,Points 不变数据库设计 CREATE TABLE user_balance ( user_id BIGINT PRIMARY KEY, credits DECIMAL(18, 6) DEFAULT 0, -- 可消费余额 points BIGINT DEFAULT 0 -- 累计积分 );-- 消费记录 CREATE TABLE consumption_log ( id BIGINT AUTO_INCREMENT, user_id BIGINT, credits_used DECIMAL(18, 6), points_earned INT, action VARCHAR(100), created_at DATETIME );Points 用整数(无小数),Credits 用高精度小数(避免浮点误差)。 Credits 扣费保证原子性 Credits 扣费和 Points 增加应在同一事务中完成: @Transactional public void consumeCredits(Long userId, BigDecimal amount, String action) { // 检查余额 UserBalance balance = balanceMapper.selectForUpdate(userId); if (balance.getCredits().compareTo(amount) < 0) { throw new InsufficientCreditsException(); } // 扣 Credits balanceMapper.deductCredits(userId, amount); // 加 Points(按消费比例) long points = amount.longValue(); balanceMapper.addPoints(userId, points); // 记录消费日志 consumptionLogMapper.insert(userId, amount, points, action); }SELECT FOR UPDATE 防止并发扣款导致余额超扣。 首版设计建议Credits 优先实现,这是核心盈利结构 Points 首版只记录,不设兑换规则(避免承诺后反悔) 后期发 Token 时,用 Points 快照作为分配依据 积分比例(1 Credits 消费 = 多少 Points)后期可调,但不能追溯修改历史记录