docs: 完善 cloud-artifacts 文档并统一出口域名

- CLAUDE.md 加 cloud-artifacts 到 Workspace Packages 表和新增 HTML Artifact Hosting 段落
- docs.json 注册 cloud-artifacts 到运行模式 group
- README 加 Quickstart、架构图(含 Deno Deploy 代理层)、Security Considerations、Troubleshooting
- 统一出口域名为 https://cloud-artifacts.claude-code-best.win(wrangler.toml PUBLIC_URL、test.sh 默认 WORKER_URL、所有文档示例)
- test.sh expect() 加 [via body] fallback:经 Deno Deploy 代理(status 抹平为 200)时按 body 的 error 字段断言

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-20 13:58:17 +08:00
parent 617254b2b5
commit 1ac7d57904
4 changed files with 134 additions and 58 deletions

View File

@@ -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 ServerDocker 部署,含 Web UI— Web UI 已重构为 React + Vite + Radix UI支持 ACP agent 接入 |
| `packages/cloud-artifacts/` | 独立 Cloudflare Worker + R2 服务POST `/upload` HTML 上传返回 hash URLGET `/<7d\|30d>/<id>.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>/<id>.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`(入口)。

View File

@@ -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://<worker-domain>/<ttl-prefix>/<id>.html`,文件到期由 R2 lifecycle rule 自动删除。
## Quickstart
```bash
# 上传一份 html默认随机 ID + 7 天 TTL
echo '<h1>hello</h1>' > /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>/<id>.html │
│ lifecycle: │
│ 7d/ -> expire 7 days │
│ 30d/ -> expire 30 days │
└──────────────────────────┘
```
- 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 熵
- **POST /upload**Bearer 鉴权 → text/html 校验 → 10MB 上限 → ttl ∈ {7,30} → R2 put
- **GET /<7d\|30d>/<id>.html**Worker 从 R2 读 → 返回 `text/html; charset=utf-8` + `Cache-Control: public, max-age=86400`
- **TTL**R2 prefix + lifecycle rule 实现Worker 不参与过期处理(零额外代码
- **覆盖**:指定 `?hash=` 时,先删 `7d/<hash>.html``30d/<hash>.html` 旧 key再写新 key
- **ID**:默认 `nanoid(21)`126 bit 熵),可指定 `?hash=<custom-id>`
## 部署
## 为什么套一层 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://<worker-domain>/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": "<code>" }`
错误(统一 `{ "error": "<code>" }`,状态码见下
| 状态码 | 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 /<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
`ttl-prefix` 只能是 `7d``30d`(其他路径返回 404/not_found返回 `text/html; charset=utf-8` + `Cache-Control: public, max-age=86400`。任何人拿到 URL 都可访问hash 即秘密。
## 示例
```bash
# 上传(默认随机 ID + 7 天
echo '<h1>hello</h1>' > /tmp/t.html
curl -X POST "https://<worker-domain>/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://<worker-domain>/7d/V1StGXR8_Z5jdHi6B-myT.html","expiresAt":"..."}
# 自定义 hash + 30 天(再次上传同 hash 覆盖)
curl -X POST "https://<worker-domain>/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://<worker-domain>/7d/V1StGXR8_Z5jdHi6B-myT.html"
# 访问
curl "https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B-myT.html"
```
## 覆盖语义
@@ -108,14 +131,34 @@ curl "https://<worker-domain>/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.<your-domain>``alias.deno.net`,并在 Deno Deploy 项目里把上游设为 `https://<worker>.<account>.workers.dev`)。
- bucket 配两条 ruleprefix `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=<your-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 形如 `/<ttl>/<id>.html`hash21 字符 nanoId即唯一秘密。不要把 URL 贴到公开频道再期望它"私密"。
- **覆盖即写**:知道 hash 的任何持 token 者都能覆盖该 ID 的内容。若需要"创建后不可改"语义,应在客户端自行约束(不传 `?hash=`)。
- **不校验 HTML 内容**:上传的 html 会被原样返回,浏览器渲染时会执行其中的 `<script>`。本服务定位是"托管自己产出的 html",不要作为任意用户上传入口。
- **TTL 上限 30 天**lifecycle rule 是 prefix 级全局规则,所有对象最多保留 30 天,无法延长。
```bash
WORKER_URL=https://<worker-domain> \
TOKEN=<your-token> \
bash scripts/test.sh
```
## Troubleshooting
| 现象 | 原因 / 处理 |
|------|-------------|
| 所有请求返 HTTP 200 但业务出错 | 经 Deno Deploy 代理时正常现象,看 body 的 `error` 字段判断真实状态 |
| `curl``*.workers.dev` 超时 | 国内 DNS 污染 + 路由问题,走 `cloud-artifacts.claude-code-best.win` 出口或挂代理 |
| 响应 html 多一段 `<a href="/cdn-cgi/content...">``<script>` | Cloudflare 默认注入的 Browser InsightsRUM不影响内容渲染。要纯净响应dashboard → Workers & Pages → cloud-artifacts → 关 Web Analytics |
| 上传 413 但文件不到 10MB | 检查 `Content-Length` header 是否被中间层改写Worker 同时按 `Content-Length``arrayBuffer().byteLength` 双重校验 |
| `?ttl=14` 返 400 | 设计如此,只允许 7 或 30对应 R2 lifecycle prefix |
| `wrangler secret list` 看到 TOKEN 但上传 401 | token 值不一致。重新 `wrangler secret put TOKEN` 设正确值 |
## 依赖

View File

@@ -10,7 +10,7 @@
set -uo pipefail
WORKER_URL="${WORKER_URL:-https://cloud-artifacts.claude-code-best.workers.dev}"
WORKER_URL="${WORKER_URL:-https://cloud-artifacts.claude-code-best.win}"
TOKEN="${TOKEN:-cloud-artifacts}"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
@@ -26,17 +26,35 @@ echo '<!doctype html><title>t</title><h1>hello v2 (overwritten)</h1>' > "$TMP/v2
yes '<p>x</p>' | head -c 11000000 > "$TMP/big.html"
pass=0; fail=0
# expect: 主断言 status code如代理把所有 status 抹平为 200 但 body 仍是 error JSON
# 则按 body 中的 error 字段做 fallback 断言(标 [via body])。
expect() {
local label="$1" want_code="$2" resp="$3" code="$4" body="$5"
if [[ "$code" == "$want_code" ]]; then
printf "${G}✓ %s -> HTTP %s${D}\n" "$label" "$code"
[[ -n "$resp" ]] && printf " body: %s\n" "$body"
pass=$((pass+1))
else
printf "${R}✗ %s -> HTTP %s (expected %s)${D}\n" "$label" "$code" "$want_code"
printf " body: %s\n" "$body"
fail=$((fail+1))
return
fi
# 代理透传 fallbackHTTP 200 + body 是 {"error":"..."} JSON
if [[ "$code" == "200" && "$body" == {\"error\":* ]]; then
local want_error=""
case "$want_code" in
401) want_error="unauthorized" ;;
415) want_error="unsupported_media_type" ;;
413) want_error="payload_too_large" ;;
404) want_error="not_found" ;;
400) want_error="invalid_" ;; # invalid_ttl 或 invalid_hash前缀匹配
esac
if [[ -z "$want_error" ]] || echo "$body" | grep -q "\"error\":\"$want_error"; then
printf "${G}✓ %s -> HTTP 200 [via body] %s${D}\n" "$label" "$body"
pass=$((pass+1))
return
fi
fi
printf "${R}✗ %s -> HTTP %s (expected %s)${D}\n" "$label" "$code" "$want_code"
printf " body: %s\n" "$body"
fail=$((fail+1))
}
call() {

View File

@@ -7,7 +7,7 @@ binding = "BUCKET"
bucket_name = "cloud-artifacts"
[vars]
PUBLIC_URL = "https://artifacts.claude-code-best.win"
PUBLIC_URL = "https://cloud-artifacts.claude-code-best.win"
DEFAULT_TTL_DAYS = "7"
MAX_TTL_DAYS = "30"
MAX_BYTES = "10485760"