feat: 新增 cloud-artifacts 包(Cloudflare Worker HTML artifact 托管)

POST /upload 鉴权上传 HTML 到 R2 返回 hash URL,GET /<7d|30d>/<id>.html
由 Worker 代理读取并直出 text/html。R2 lifecycle rule 自动 7/30 天删除。
独立服务,不被主 CLI 引用(类似 packages/remote-control-server/ 定位)。

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-20 13:34:39 +08:00
parent 5d74071ebf
commit 617254b2b5
10 changed files with 791 additions and 1 deletions

View File

@@ -0,0 +1,150 @@
# cloud-artifacts
独立的 Cloudflare Worker + R2 服务,用于托管 HTML artifact。
服务端CLI / RCS 后台)通过单一 bearer token 上传 HTML得到一个公开可访问的 CDN URL。**POST 上传和 GET 访问都走 Worker**URL 形如 `https://<worker-domain>/<ttl-prefix>/<id>.html`,文件到期由 R2 lifecycle rule 自动删除。
## 架构
```
客户端 --POST /upload--> Worker (鉴权 + 校验 + R2 put) --返回 {id, url, expiresAt}
客户端 --GET html-------> Worker (R2 get → text/html) --直出 html带 Cache-Control: max-age=86400
R2 lifecycle rule: 7d/ 删 7 天30d/ 删 30 天
```
- Worker 处理 `POST /upload``GET /<prefix>/<id>.html`
- R2 key 形如 `7d/<id>.html``30d/<id>.html`
- URL 形如 `https://<worker-domain>/7d/<id>.html`hash 本身是秘密21 字符 nanoId126 bit 熵)
## 部署
前置:本机已 `npx wrangler login` 登录目标 Cloudflare 账号,且账号下有一个用于 Worker custom domain 的域名zone
```bash
cd packages/cloud-artifacts
bun install # 在 monorepo 根执行也行workspace 自动识别)
cp .dev.vars.example .dev.vars # 填本地 dev 用的 TOKEN仅 wrangler dev 读)
bun run setup # 创建 bucket + 加 lifecycle rule + 设生产 TOKEN secret
# 绑定 Worker custom domain在 Cloudflare dashboard
# Workers & Pages > cloud-artifacts > Settings > Domains & Routes > Add > Custom Domain
# 填入你的 domain如 artifacts.example.com
# 改 wrangler.toml 中 [vars] PUBLIC_URL 为上一步的 domain如 https://artifacts.example.com
bun run deploy
```
## API
### `POST /upload`
| Header / Query | 必填 | 说明 |
|----------------|------|------|
| `Authorization: Bearer <TOKEN>` | 是 | 与 Worker secret `TOKEN` 完全相等 |
| `Content-Type: text/html` | 是 | 不接受其他类型 |
| `?ttl=7\|30` | 否 | 默认 7**只允许 7 或 30**(与 R2 lifecycle prefix 对应) |
| `?hash=<custom-id>` | 否 | 自定义 ID校验 `^[A-Za-z0-9_-]{1,128}$`;指定时覆盖同 ID 旧版本 |
| body | 是 | 原始 HTML`--data-binary @file.html`≤10MB |
成功 200
```json
{
"id": "V1StGXR8_Z5jdHi6B-myT",
"url": "https://<worker-domain>/7d/V1StGXR8_Z5jdHi6B-myT.html",
"expiresAt": "2026-06-27T10:00:00.000Z"
}
```
错误(统一 `{ "error": "<code>" }`
| 状态码 | error code | 触发条件 |
|--------|------------|----------|
| 400 | `invalid_ttl` | `ttl` 非 7 或 30 |
| 400 | `invalid_hash` | `hash` 不匹配 `^[A-Za-z0-9_-]{1,128}$` |
| 401 | `unauthorized` | 缺 Authorization / token 不匹配 |
| 404 | `not_found` | 非 `/upload` 路径或 GET 路径不匹配 `/<7d\|30d>/<id>.html` |
| 413 | `payload_too_large` | body > 10MB |
| 415 | `unsupported_media_type` | Content-Type 非 `text/html` |
### `GET /<ttl-prefix>/<id>.html`
由 Worker 处理:解析路径 → R2 get → 返回 `text/html; charset=utf-8` + `Cache-Control: public, max-age=86400`。任何人拿到 URL 都可访问hash 即秘密。
`ttl-prefix` 只能是 `7d``30d`(其他路径返回 404
## 示例
```bash
# 上传(默认随机 ID + 7 天)
echo '<h1>hello</h1>' > /tmp/t.html
curl -X POST "https://<worker-domain>/upload" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/html" \
--data-binary @/tmp/t.html
# -> {"id":"V1StGXR8_Z5jdHi6B-myT","url":"https://<worker-domain>/7d/V1StGXR8_Z5jdHi6B-myT.html","expiresAt":"..."}
# 自定义 hash + 30 天(再次上传同 hash 覆盖)
curl -X POST "https://<worker-domain>/upload?ttl=30&hash=my-report" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/html" \
--data-binary @/tmp/report.html
# 访问(公开 URL走 Worker → R2
curl "https://<worker-domain>/7d/V1StGXR8_Z5jdHi6B-myT.html"
```
## 覆盖语义
指定 `?hash=` 时:
1. 校验 hash 字符集(`^[A-Za-z0-9_-]{1,128}$`
2. 删除 `7d/<hash>.html``30d/<hash>.html` 两个 keyR2 delete 不存在的 key 不报错,零成本)
3.`?ttl=` 写入新 key
4. 返回新的 `expiresAt`
不指定 `?hash=` 时:用 `nanoid(21)` 随机 ID几乎不可能碰撞不做碰撞检查。
## TTL 落地
R2 不支持 per-object TTL本服务用 prefix + R2 lifecycle rule 模拟:
- bucket 配两条 ruleprefix `7d/` 删 7 天前对象、prefix `30d/` 删 30 天前对象
-`scripts/setup.sh``wrangler r2 bucket lifecycle add` 自动配置
- Worker 完全不参与过期处理,零额外代码
- 因此 `?ttl=` 只能取 `7``30`,对应这两个 prefix其他值会写到无 lifecycle 的 prefix → 永久存储,故拒绝)
## 本地开发
```bash
cp .dev.vars.example .dev.vars
# 编辑 .dev.vars 填 TOKEN
bun run dev # wrangler dev启动本地 Miniflare + 本地 R2 模拟
curl -X POST "http://localhost:8787/upload" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: text/html" \
--data-binary @/tmp/t.html
```
## 测试
`scripts/test.sh` 覆盖 7 个错误用例 + 3 个成功用例 + R2 写入验证:
```bash
WORKER_URL=https://<worker-domain> \
TOKEN=<your-token> \
bash scripts/test.sh
```
## 依赖
- `wrangler` ^4 — Cloudflare Workers CLI
- `nanoid` ^5 — ID 生成(纯 ESMWorker 兼容)
## 不被主 CLI 引用
这是独立 Cloudflare Worker 服务,类似 `packages/remote-control-server/` 的定位。Monorepo 根 `package.json``workspaces: ["packages/*", ...]` 自动识别本包,但主 CLI 不会 import 它。