diff --git a/CLAUDE.md b/CLAUDE.md index 834711a9d..93e204079 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,6 +172,7 @@ bun run docs:dev | `packages/acp-link/` | ACP 代理服务器(WebSocket → ACP agent 桥接) | | `packages/mcp-client/` | MCP 客户端库 | | `packages/remote-control-server/` | 自托管 Remote Control Server(Docker 部署,含 Web UI)— Web UI 已重构为 React + Vite + Radix UI,支持 ACP agent 接入 | +| `packages/cloud-artifacts/` | 独立 Cloudflare Worker + R2 服务:POST `/upload` HTML 上传返回 hash URL,GET `/<7d\|30d>/.html` 由 Worker 代理读取;R2 lifecycle rule 自动 7/30 天过期 | | `packages/audio-capture-napi/` | 原生音频捕获(已恢复) | | `packages/color-diff-napi/` | 颜色差异计算(完整实现,11 tests) | | `packages/image-processor-napi/` | 图像处理(已恢复) | @@ -188,6 +189,11 @@ bun run docs:dev - CLI 快速路径: `claude remote-control` / `claude rc` / `claude bridge`。 - 详见 `docs/features/remote-control-self-hosting.md`。 +### HTML Artifact Hosting + +- **`packages/cloud-artifacts/`** — 独立 Cloudflare Worker + R2 服务,类似 `remote-control-server/` 的"独立部署服务"定位,**不被主 CLI import**。Worker 处理 `POST /upload`(Bearer token 鉴权 + text/html 校验 + 10MB 上限 + ttl∈{7,30})和 `GET /<7d|30d>/.html`(从 R2 读 + Cache-Control: max-age=86400)。R2 用 prefix + lifecycle rule 实现 TTL(`7d/` 删 7 天、`30d/` 删 30 天),Worker 不参与过期处理。ID 默认 `nanoid(21)`(126 bit 熵),可指定 `?hash=` 自定义 ID(覆盖语义:先删 7d/30d prefix 旧 key 再写新 key)。Worker 用 `wrangler types` 生成的全局 `Env` 类型(`worker-configuration.d.ts`,已 gitignore),不依赖 `@cloudflare/workers-types`。部署用 `npm create cloudflare@latest` 初始化 + `bun run setup`(创建 bucket + lifecycle + secret)+ `bun run deploy`。 +- 详见 `docs/features/cloud-artifacts.md`。 + ### ACP Protocol (Agent Client Protocol) - **`src/services/acp/`** — ACP agent 实现,包含 `agent.ts`(AcpAgent 类)、`bridge.ts`(Claude Code ↔ ACP 桥接)、`permissions.ts`(权限处理)、`entry.ts`(入口)。 diff --git a/packages/cloud-artifacts/README.md b/packages/cloud-artifacts/README.md index c7e8e54e5..12532c9f7 100644 --- a/packages/cloud-artifacts/README.md +++ b/packages/cloud-artifacts/README.md @@ -1,41 +1,66 @@ # cloud-artifacts -独立的 Cloudflare Worker + R2 服务,用于托管 HTML artifact。 +> **生产出口**:`https://cloud-artifacts.claude-code-best.win` +> +> 服务端(CLI / RCS 后台)通过单一 bearer token 上传 HTML,得到一个公开可访问的 URL。 +> 文件到期由 R2 lifecycle rule 自动删除(默认 7 天,最长 30 天)。 -服务端(CLI / RCS 后台)通过单一 bearer token 上传 HTML,得到一个公开可访问的 CDN URL。**POST 上传和 GET 访问都走 Worker**,URL 形如 `https:////.html`,文件到期由 R2 lifecycle rule 自动删除。 +## Quickstart + +```bash +# 上传一份 html(默认随机 ID + 7 天 TTL) +echo '

hello

' > /tmp/t.html +curl -X POST "https://cloud-artifacts.claude-code-best.win/upload" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: text/html" \ + --data-binary @/tmp/t.html +# {"id":"V1StGXR8_Z5jdHi6B-myT", +# "url":"https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B-myT.html", +# "expiresAt":"2026-06-27T10:00:00.000Z"} + +# 任何人拿到 url 都能访问 +curl "https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B-myT.html" +``` ## 架构 ``` -客户端 --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 天 + ┌──────────────────────────┐ +客户端 --POST /upload----▶│ Deno Deploy 边缘代理 │ + │ cloud-artifacts.ccb.win │ + └────────────┬─────────────┘ + │ 透传 + ▼ + ┌──────────────────────────┐ + │ Cloudflare Worker │ + │ - 鉴权 + MIME + 大小校验 │ + │ - ttl∈{7,30} + hash 校验 │ + │ - R2 put / R2 get │ + └────────────┬─────────────┘ + │ + ▼ + ┌──────────────────────────┐ + │ R2 bucket │ + │ key: <7d|30d>/.html │ + │ lifecycle: │ + │ 7d/ -> expire 7 days │ + │ 30d/ -> expire 30 days │ + └──────────────────────────┘ ``` -- Worker 处理 `POST /upload` 和 `GET //.html` -- R2 key 形如 `7d/.html` 或 `30d/.html` -- URL 形如 `https:///7d/.html`,hash 本身是秘密(21 字符 nanoId,126 bit 熵) +- **POST /upload**:Bearer 鉴权 → text/html 校验 → 10MB 上限 → ttl ∈ {7,30} → R2 put +- **GET /<7d\|30d>/.html**:Worker 从 R2 读 → 返回 `text/html; charset=utf-8` + `Cache-Control: public, max-age=86400` +- **TTL**:R2 prefix + lifecycle rule 实现,Worker 不参与过期处理(零额外代码) +- **覆盖**:指定 `?hash=` 时,先删 `7d/.html` 和 `30d/.html` 旧 key,再写新 key +- **ID**:默认 `nanoid(21)`(126 bit 熵),可指定 `?hash=` -## 部署 +## 为什么套一层 Deno Deploy -前置:本机已 `npx wrangler login` 登录目标 Cloudflare 账号,且账号下有一个用于 Worker custom domain 的域名(zone)。 +国内直连 Cloudflare Workers 边缘节点延迟高、丢包严重(DNS 污染 + 路由问题)。在 `cloud-artifacts.claude-code-best.win` 上套 Deno Deploy 边缘代理后: -```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 -``` +- 国内访问延迟显著降低(Deno Deploy 在国内可达性好) +- POST/GET body 完整透传 +- **副作用**:Deno Deploy 代理会把上游 HTTP status code 抹平为 200(但 body 内的 `{error: ...}` 字段完整保留)。客户端若依赖 status code 判断错误类型,应改为解析 body 中的 `error` 字段。直连 Worker 自身(如 `*.workers.dev`)时 status code 正常透传。 ## API @@ -54,14 +79,14 @@ bun run deploy ```json { "id": "V1StGXR8_Z5jdHi6B-myT", - "url": "https:///7d/V1StGXR8_Z5jdHi6B-myT.html", + "url": "https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B-myT.html", "expiresAt": "2026-06-27T10:00:00.000Z" } ``` -错误(统一 `{ "error": "" }`): +错误(统一 `{ "error": "" }`,状态码见下): -| 状态码 | error code | 触发条件 | +| 状态码(直连) | error code | 触发条件 | |--------|------------|----------| | 400 | `invalid_ttl` | `ttl` 非 7 或 30 | | 400 | `invalid_hash` | `hash` 不匹配 `^[A-Za-z0-9_-]{1,128}$` | @@ -70,31 +95,29 @@ bun run deploy | 413 | `payload_too_large` | body > 10MB | | 415 | `unsupported_media_type` | Content-Type 非 `text/html` | +> **经 Deno Deploy 代理时**:以上所有错误状态码统一返回 **200**,但 body 仍是上表中的 `{error: ...}` JSON。客户端解析逻辑应以 body 的 `error` 字段为准。 + ### `GET //.html` -由 Worker 处理:解析路径 → R2 get → 返回 `text/html; charset=utf-8` + `Cache-Control: public, max-age=86400`。任何人拿到 URL 都可访问,hash 即秘密。 - -`ttl-prefix` 只能是 `7d` 或 `30d`(其他路径返回 404)。 +`ttl-prefix` 只能是 `7d` 或 `30d`(其他路径返回 404/not_found)。返回 `text/html; charset=utf-8` + `Cache-Control: public, max-age=86400`。任何人拿到 URL 都可访问,hash 即秘密。 ## 示例 ```bash -# 上传(默认随机 ID + 7 天) -echo '

hello

' > /tmp/t.html -curl -X POST "https:///upload" \ +# 默认随机 ID + 7 天 +curl -X POST "https://cloud-artifacts.claude-code-best.win/upload" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: text/html" \ --data-binary @/tmp/t.html -# -> {"id":"V1StGXR8_Z5jdHi6B-myT","url":"https:///7d/V1StGXR8_Z5jdHi6B-myT.html","expiresAt":"..."} # 自定义 hash + 30 天(再次上传同 hash 覆盖) -curl -X POST "https:///upload?ttl=30&hash=my-report" \ +curl -X POST "https://cloud-artifacts.claude-code-best.win/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:///7d/V1StGXR8_Z5jdHi6B-myT.html" +# 访问 +curl "https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B-myT.html" ``` ## 覆盖语义 @@ -108,14 +131,34 @@ curl "https:///7d/V1StGXR8_Z5jdHi6B-myT.html" 不指定 `?hash=` 时:用 `nanoid(21)` 随机 ID,几乎不可能碰撞,不做碰撞检查。 -## TTL 落地 +## 部署 -R2 不支持 per-object TTL,本服务用 prefix + R2 lifecycle rule 模拟: +前置:本机已 `npx wrangler login` 登录目标 Cloudflare 账号。Deno Deploy 代理层由部署者另配(CNAME `cloud-artifacts.` → `alias.deno.net`,并在 Deno Deploy 项目里把上游设为 `https://..workers.dev`)。 -- bucket 配两条 rule:prefix `7d/` 删 7 天前对象、prefix `30d/` 删 30 天前对象 -- 由 `scripts/setup.sh` 调 `wrangler r2 bucket lifecycle add` 自动配置 -- Worker 完全不参与过期处理,零额外代码 -- 因此 `?ttl=` 只能取 `7` 或 `30`,对应这两个 prefix(其他值会写到无 lifecycle 的 prefix → 永久存储,故拒绝) +```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 + +# 改 wrangler.toml 中 [vars] PUBLIC_URL 为对外出口域名(生产用 https://cloud-artifacts.claude-code-best.win) + +bun run deploy +``` + +## 测试 + +`scripts/test.sh` 覆盖 7 个错误用例 + 3 个成功用例 + R2 写入验证。**支持双模式**:直连 Worker 时按 HTTP status code 断言;经 Deno Deploy 代理(status 抹平为 200)时自动按 body 的 `error` 字段断言(标记 `[via body]`)。 + +```bash +WORKER_URL=https://cloud-artifacts.claude-code-best.win \ +TOKEN= \ +bash scripts/test.sh +``` ## 本地开发 @@ -130,15 +173,24 @@ curl -X POST "http://localhost:8787/upload" \ --data-binary @/tmp/t.html ``` -## 测试 +## 安全注意事项 -`scripts/test.sh` 覆盖 7 个错误用例 + 3 个成功用例 + R2 写入验证: +- **TOKEN 是上传侧唯一鉴权**:值泄露后任何人可上传/覆盖。生产应使用 ≥32 字符的随机串,定期轮换(`wrangler secret put TOKEN` 即时生效,无需 redeploy)。 +- **GET 完全公开**:URL 形如 `//.html`,hash(21 字符 nanoId)即唯一秘密。不要把 URL 贴到公开频道再期望它"私密"。 +- **覆盖即写**:知道 hash 的任何持 token 者都能覆盖该 ID 的内容。若需要"创建后不可改"语义,应在客户端自行约束(不传 `?hash=`)。 +- **不校验 HTML 内容**:上传的 html 会被原样返回,浏览器渲染时会执行其中的 `