# TamaBox-v4 迁移 Cloudflare Workers + D1 可行性评估 - 日期:2026-09-27 - 评估对象:将 TamaBox-v4(Go + Flamego + GORM + PG/Redis)改造为 **TypeScript + Cloudflare Workers + D1** 的 serverless 形态 - 参照项目:[OpenListTeam/OpenList-Worker](https://github.com/OpenListTeam/OpenList-Worker) - 范围:**只读评估,未改动任何源码** --- ## 0. 结论速览 | 项 | 结论 | |---|---| | 技术可行性 | 可行 —— 没有理论上的不可能 | | 工程性价比 | 🔴 **低** —— 是「整体重写」,不是「改造」 | | 主要阻碍 | ① 图像管线(cgo WebP / cwebp 子进程 / freetype 中文渲染)② 进程内有状态子系统 ③ PG 专有 SQL | | 建议 | **方案 A(不迁移)**;若必须上边缘,走**方案 B(静态+只读下沉)** | 一句话:**OpenList 是「无状态代理」,TamaBox 是「有状态 Web 应用」,两者的边缘适配成本差一个数量级。** --- ## 1. 先看清参照物:OpenList-Worker 为什么能做成 | 维度 | OpenList(Go 原版) | OpenList-Worker(TS) | |---|---|---| | 仓库体积 | 9.8 MB | 21.7 MB(**比原版还大**) | | 语言 | Go | TypeScript | | 代码量 | — | 416 个 `.ts`,约 2.6 MB 源码 | | Star | 24,800 | 372 | | 立项 | — | 2025-08-23(约一年) | | 维护方 | 官方团队 | 官方团队 | 即使是**官方团队 + 24.8k star** 的项目,重写一年后 Worker 版代码量反而超过原版。而更关键的是**应用形态**: **OpenList 的本质是无状态的文件列表 + 反代:** - 没有多用户会话状态(只有一个管理员) - 没有服务端可变状态(限流 / 封禁 / 去重全都不需要) - 存储是「可插拔后端」,官方甚至支持降级到 `map` / KV,并明说 *serverless 下禁止回退到内存* - 没有图像处理、没有服务端模板渲染 —— 前端是现成 SPA,Worker 只负责注入 CDN 地址 所以它干的其实是「**把无状态代理逻辑搬到边缘**」—— 这正是边缘计算最擅长的形态。 **TamaBox 恰恰相反,它是一个有状态的多用户 Web 应用。** --- ## 2. TamaBox 的形态画像 代码规模:**34,664 行 Go** | 模块 | LOC | 说明 | |---|---|---| | `route/` | 7,645 | 业务逻辑(vote 1297 / share 970 / profile 606 …) | | `internal/db` | 6,950 | GORM 模型 + 仓储,15 张表 | | `internal/i18n` | 3,689 | 四语言字典(以数据为主) | | `internal/security` | 2,675 | 审核、自动封禁、IP 情报 | | `internal/route` | 1,653 | 中间件与路由装配 | | `internal/template` | 825 | 模板函数 / Markdown / minify | | `internal/shared` | 758 | IP、去重、并发工具 | | `internal/conf` | 587 | 配置(ini + 单环境变量) | | `internal/cmd` | 482 | CLI 子命令(web/censor/uid/ban/restore) | | `internal/storage` | 440 | R2 / OSS 上传 + WebP 转换 | | 模板 | 42 个 html | 服务端 `html/template` 渲染 | | 中间件 | — | Session / CSRF / Redis cache / reCAPTCHA | --- ## 3. 分层可行性矩阵 ### P0 —— 硬阻断(必须重构,无法直搬) | # | 阻断点 | 位置 | 为什么在 Workers 上不行 | |---|---|---|---| | 1 | **WebP 编码是 cgo** | `internal/storage/s3.go:16,23,192` | `chai2010/webp` 依赖 libwebp 的 C 代码(`//go:build cgo`)。Workers 是 V8 isolate,跑不了 cgo → 只能换 `@jsquash/webp`(WASM) | | 2 | **GIF→WebP 走 `cwebp` 子进程** | `internal/storage/s3.go:200-233` | `exec.LookPath("cwebp")` + `os.CreateTemp`。Workers 无子进程、无可写磁盘 → **完全不可实现** | | 3 | **分享图 freetype 渲染中文** | `route/question/share.go:121-131, 387-720` | `os.ReadFile(font_path)` 读磁盘 OTF + freetype 光栅化 + jpeg 编码。必须整体重写;且大图中文字形光栅化是 CPU 密集,易撞 Workers CPU 上限 | | 4 | **进程内状态**(Session / 缓存 / 限流 / 封禁 / 去重 / 统计) | `internal/route/route.go:386-395`、`internal/cache`、`internal/security/autoban`、`internal/shared/duplicate.go`、`internal/route/ratelimit.go` | Workers 请求间无共享可变状态 → 全部要迁 Durable Objects / KV / D1,成本明显上升 | | 5 | **cache 中间件硬编码 Redis** | `internal/route/route.go:386-395` | 无条件走 `cacheRedis.Initer()`,不判断配置是否为空。Workers 无法用原生 TCP Redis | | 6 | **启动期副作用** | `internal/cmd/web.go:57-105`、`internal/db/db.go:58-127` | `ListenAndServe` 常驻监听 + `AutoMigrate` + `fixPostgresSequences` + backfill。serverless 无常驻启动流程 | | 7 | **常驻 goroutine 定时任务** | `ratelimit.go:41`、`autoban.go:250`、`duplicate.go:34`、`share.go:207` | `for { sleep; cleanup }` 模式 → 需改 Cron Triggers + DO alarm | | 8 | **PG 专有 SQL** | `db.go:211-234`(序列 `nextval/setval`)、`follow_ups.go:68`(`FOR UPDATE`)、各处 `datatypes.JSON`(JSONB) | D1 是 SQLite:**无序列、无 `FOR UPDATE`、无 JSONB** | | 9 | **SMTP 发信** | `internal/mail/mail.go:129-147` | Workers 禁止裸 TCP 25/465/587 → 须换 Resend / SendGrid 等 HTTP 邮件 API | | 10 | **服务端 Go 模板** | `templates/` 42 个 + `internal/template/template.go:29-229` | `html/template` 的**上下文敏感转义**是安全边界,迁到 JS 模板引擎需谨慎等价实现 | | 11 | **OTel/Uptrace gRPC 上报** | `internal/tracing/tracing.go:15-27`、`internal/cmd/web.go:41-48` | Workers 无 gRPC、无后台 batch 上报协程 | ### P1 —— 需换库 / 改写(有明确对应物) - **S3/R2 上传** → `@aws-sdk/client-s3`(R2 兼容 S3 协议,可行) - **七牛 / 阿里云文本审核** → `fetch` 直调或 JS SDK(阿里云 `tea`/`darabonba` 运行时需重做) - **Excel 导出 `excelize`** → `exceljs` - **CSV 导出** `encoding/csv` → 手写(简单) - **QR `skip2/go-qrcode`** → `qrcode` JS 库 - **Markdown `blackfriday` + 自研 XSS 白名单** → `marked` + `DOMPurify`(**白名单规则需逐条复刻**,属安全关键) - **i18n 的 `fmt` 风格 `%s` 占位** → 需实现等价格式化 - **软删除 + 唯一索引语义** → SQLite 下槽位行为需重新设计 ### P2 —— 基本可直接移植 - `internal/imagecheck`(纯标准库像素校验) - `internal/captcha/math.go`(**JSON 算数题,非图片、无字体**) - `internal/i18n` 字典(纯数据) - CSRF(无状态 HMAC,本身可移植) - `internal/security/ipinfo`(离线签名,零网络、零 DNS) - `internal/shared/ip.go`(IP 解析) - `bodylimit` / `ipblacklist` - `embed.FS`(转 Assets / 模块) > 补充:`internal/template/minify.go` 当前**未被路由装配实际使用**,迁移时可直接丢弃。 --- ## 4. 成本量化 - 源码 **34.6k 行 Go** → 等价重写预计 **40k+ 行 TS** - `templates/` **42 个模板 + 25 个模板函数**全部重写 - 全部测试重写(含 `route/vote/vote_stats_test.go` 这类带 DB 的门控测试) - 参考标定:OpenList 官方团队 + 24.8k star,重写约 **一年**,产出 416 个 TS 文件 / 2.6 MB - TamaBox 的形态比 OpenList **更「重」**(有状态 + 关系型 DB + 图像 + 模板),且为**单人维护** --- ## 5. 容易被忽略的坑 1. **中国大陆可用性**:Workers 是海外节点。文本审核走七牛/阿里云(国内 API),海外边缘回源国内延迟高;面向大陆用户还有备案/合规问题(cf. 项目已有 `edgeone-noicp-cf-tunnel-guide`、`edgeone-origin-whitelist-guide`) 2. **D1 限额**:SQLite 单库上限(付费 10 GB)、写并发受限;`questions` / `vote_submissions` 属写密集 3. **Workers CPU 限额**:免费 10 ms、付费 30 s/请求 且**按 CPU 计费**。分享图渲染、Excel 导出都吃 CPU 4. **Durable Objects 是付费项**,按请求 + 时长计费 → 把 Session / 限流 / 封禁搬过去后,成本模型从「一台 1C1G」变成「按量」 5. **当前生产是 1C1G 小服务器**:迁移的收益(免运维 / 弹性)对这个体量并不显著 --- ## 6. 建议方案 ### 方案 A(推荐):不迁移 形态不匹配 + 单人维护 + 当前 1C1G 成本已很低。收益(免运维、弹性)不足以覆盖重写成本与风险。 ### 方案 B(若确要上边缘,渐进式) 只把**无状态**的部分下沉边缘: - **下沉**:静态资源(`static/`、模板产物)+ 只读接口(公开提问页、投票公开页、`/explore`)→ EdgeOne Pages / Workers - **保留源站**:写路径、图像处理、审核、邮件、导出 → 继续跑 Go 项目已有 EdgeOne 回源白名单脚本,这套「边缘 + 源站」形态其实已在用。**风险最低、可随时回退。** ### 方案 C(完全重写) 仅当能接受**功能裁剪**(砍分享图 / Excel 导出 / 聊天 / 投票表单)且投入 2–3 个月专职。且建议优先 **EdgeOne(腾讯云,有大陆节点)** 而非纯 Cloudflare Workers。 --- ## 附:评估依据 - 全部源码只读探查:`internal/**`(110 个 Go 文件)、`route/**`(22 个)、`templates/**`(42 个模板) - 依赖清单:`go.mod`(含 `chai2010/webp`、`disintegration/imaging`、`golang/freetype`、`xuri/excelize`、`uptrace/uptrace-go` 等) - 对照数据来自 GitHub API 与 OpenList-Worker `README` / 提交历史