Files
claude-code/packages/cloud-artifacts
claude-code-best 084e487943 feat: 新增 artifacts 功能 (#1278)
* 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>

* 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>

* docs: 修正 CLAUDE.md cloud-artifacts 引用死链

之前指向不存在的 docs/features/cloud-artifacts.md(用户未注册到 docs.json),
改为指向已存在的 packages/cloud-artifacts/README.md,并补充生产出口域名
与 Deno Deploy status 抹平副作用的说明。

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* chore: 同步 cloud-artifacts 测试默认 TOKEN 到新值

用户已通过 wrangler secret put 把生产 TOKEN 改为 claude-code-best,
test.sh 的默认值(之前用旧 token 作 fallback)和注释示例同步更新。
现在直接 bash scripts/test.sh 即可跑通(无需显式传 TOKEN)。

src/index.ts 不依赖具体 token 值(只读 env.TOKEN 做比较),
wrangler.toml 不含 secret,README/.dev.vars.example 用 <your-token>
占位符故无需改。

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* docs: add artifacts feature implementation plan

* feat(artifact): add cloud-artifacts config with token/URL defaults

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): add HTTP client with body-error parsing

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): add tool name, description, and prompt

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): add buildTool definition with file validation

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* test(artifact): add end-to-end tool tests for upload/error paths

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): export ArtifactTool from builtin-tools barrel

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): register ArtifactTool in tools list

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): add /use-artifacts bundled skill

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): add extractArtifacts message scanner

Scans Message[] for artifact tool_use/tool_result pairs, parses URL/id/expires
from the upload response string, and returns ArtifactInfo[] newest-first.

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* fix(artifact): scanner type narrowing and url regex

- Use double assertion (`as unknown as Record<string, unknown>`) at lines 30
  and 90 to fix TS2352 per project convention
- Tighten URL_REGEX to avoid capturing trailing punctuation (parens,
  quotes, commas) when URL is embedded in text
- Add test case for array-form tool_result content path

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): add ArtifactsMenu Ink component

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): add /artifacts slash command entry

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): register /artifacts command

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* fix(artifact): use setClipboard instead of pbcopy for cross-platform support

* fix(artifact): drop userFacingName override so display matches /artifacts

* fix(rcs): add resJson helper to resolve strict mode type errors in tests

Hono Response.json() returns Promise<unknown> under strict TypeScript,
causing 121 TS errors across middleware and routes test files.

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>

* fix(cloud-artifacts): add type stubs so tsc passes without worker-configuration.d.ts

The wrangler-generated worker-configuration.d.ts is gitignored, causing CI to
fail with missing ExportedHandler/Env/R2Bucket types. This file provides minimal
stubs for all Cloudflare Workers types used by the artifact upload Worker.

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>

* fix(query): shallow-copy messages before stripping toolUseResult

Previously the per-query cleanup mutated messagesForQuery entries in
place via `delete msg.toolUseResult`. Those entries are references
shared with mutableMessages (UI state), so the delete stripped the
field from the live message object. The next query can start within
milliseconds of tool_result creation — before the React UI commit
lands — so UserToolSuccessMessage's `!message.toolUseResult` check
returned null and tool.renderToolResultMessage was never called,
leaving tool-result rows blank.

Map to a stripped copy instead so mutableMessages keeps the original
for the UI. Downstream API transformations (applyToolResultBudget,
snip, microcompact) already build new arrays via .map(), so they
compose cleanly with this copy.

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* feat(artifact): show uploaded URL inline below ExecuteExtraTool

Deferred tools (shouldDefer: true) are invoked via SearchExtraTools →
ExecuteExtraTool, so their tool_result rows used to render blank —
the UI looked up ExecuteExtraTool, which had no renderToolResultMessage,
and returned null. Add a generic delegation in ExecuteTool that forwards
renderToolResultMessage to the inner tool when it defines one, unwrapping
the { result, tool_name } envelope and the params from the input shape.
All 28 deferred tools can now render their own UI by defining
renderToolResultMessage.

For ArtifactTool specifically, render the uploaded URL as an OSC 8
hyperlink (Link component) in warning color so it's visually prominent,
with the expiry timestamp on a second line and a separate error branch.
Also add `error: z.string().optional()` to outputSchema — zod's default
strip mode was dropping the field, so error states never reached the UI.

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* fix(cloud-artifacts): make Env stubs actually take effect in CI

The previous stub file (2e29e362) wrapped `interface Env` in
`declare global { ... }`, but the file has no top-level import/export so
it's a script, not a module. TS2669 forbids `declare global` in scripts,
and in .d.ts files that error is silently swallowed — so the Env stubs
were never merged into the global scope. Locally typecheck passed only
because worker-configuration.d.ts (gitignored) provided Env separately;
in CI / fresh clones, `BUCKET`, `MAX_BYTES`, `DEFAULT_TTL_DAYS`,
`PUBLIC_URL` were all missing on Env.

Drop the wrapper. Top-level `interface Env` in a script .d.ts is already
global ambient and merges with worker-configuration.d.ts via interface
declaration merging, so both environments typecheck cleanly.

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

---------

Co-authored-by: glm-5.2 <zai-org@claude-code-best.win>
2026-06-20 19:52:08 +08:00
..
2026-06-20 19:52:08 +08:00

cloud-artifacts

生产出口https://cloud-artifacts.claude-code-best.win

服务端CLI / RCS 后台)通过单一 bearer token 上传 HTML得到一个公开可访问的 URL。 文件到期由 R2 lifecycle rule 自动删除(默认 7 天,最长 30 天)。

Quickstart

# 上传一份 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----▶│  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 │
                          └──────────────────────────┘
  • POST /uploadBearer 鉴权 → text/html 校验 → 10MB 上限 → ttl ∈ {7,30} → R2 put
  • GET /<7d|30d>/.htmlWorker 从 R2 读 → 返回 text/html; charset=utf-8 + Cache-Control: public, max-age=86400
  • TTLR2 prefix + lifecycle rule 实现Worker 不参与过期处理(零额外代码)
  • 覆盖:指定 ?hash= 时,先删 7d/<hash>.html30d/<hash>.html 旧 key再写新 key
  • ID:默认 nanoid(21)126 bit 熵),可指定 ?hash=<custom-id>

为什么套一层 Deno Deploy

国内直连 Cloudflare Workers 边缘节点延迟高、丢包严重DNS 污染 + 路由问题)。在 cloud-artifacts.claude-code-best.win 上套 Deno Deploy 边缘代理后:

  • 国内访问延迟显著降低Deno Deploy 在国内可达性好)
  • POST/GET body 完整透传
  • 副作用Deno Deploy 代理会把上游 HTTP status code 抹平为 200但 body 内的 {error: ...} 字段完整保留)。客户端若依赖 status code 判断错误类型,应改为解析 body 中的 error 字段。直连 Worker 自身(如 *.workers.dev)时 status code 正常透传。

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

{
  "id": "V1StGXR8_Z5jdHi6B-myT",
  "url": "https://cloud-artifacts.claude-code-best.win/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

经 Deno Deploy 代理时:以上所有错误状态码统一返回 200,但 body 仍是上表中的 {error: ...} JSON。客户端解析逻辑应以 body 的 error 字段为准。

GET /<ttl-prefix>/<id>.html

ttl-prefix 只能是 7d30d(其他路径返回 404/not_found。返回 text/html; charset=utf-8 + Cache-Control: public, max-age=86400。任何人拿到 URL 都可访问hash 即秘密。

示例

# 默认随机 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

# 自定义 hash + 30 天(再次上传同 hash 覆盖)
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

# 访问
curl "https://cloud-artifacts.claude-code-best.win/7d/V1StGXR8_Z5jdHi6B-myT.html"

覆盖语义

指定 ?hash= 时:

  1. 校验 hash 字符集(^[A-Za-z0-9_-]{1,128}$
  2. 删除 7d/<hash>.html30d/<hash>.html 两个 keyR2 delete 不存在的 key 不报错,零成本)
  3. ?ttl= 写入新 key
  4. 返回新的 expiresAt

不指定 ?hash= 时:用 nanoid(21) 随机 ID几乎不可能碰撞不做碰撞检查。

部署

前置:本机已 npx wrangler login 登录目标 Cloudflare 账号。Deno Deploy 代理层由部署者另配CNAME cloud-artifacts.<your-domain>alias.deno.net,并在 Deno Deploy 项目里把上游设为 https://<worker>.<account>.workers.dev)。

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])。

WORKER_URL=https://cloud-artifacts.claude-code-best.win \
TOKEN=<your-token> \
bash scripts/test.sh

本地开发

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

安全注意事项

  • TOKEN 是上传侧唯一鉴权:值泄露后任何人可上传/覆盖。生产应使用 ≥32 字符的随机串,定期轮换(wrangler secret put TOKEN 即时生效,无需 redeploy
  • GET 完全公开URL 形如 /<ttl>/<id>.htmlhash21 字符 nanoId即唯一秘密。不要把 URL 贴到公开频道再期望它"私密"。
  • 覆盖即写:知道 hash 的任何持 token 者都能覆盖该 ID 的内容。若需要"创建后不可改"语义,应在客户端自行约束(不传 ?hash=)。
  • 不校验 HTML 内容:上传的 html 会被原样返回,浏览器渲染时会执行其中的 <script>。本服务定位是"托管自己产出的 html",不要作为任意用户上传入口。
  • TTL 上限 30 天lifecycle rule 是 prefix 级全局规则,所有对象最多保留 30 天,无法延长。

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-LengtharrayBuffer().byteLength 双重校验
?ttl=14 返 400 设计如此,只允许 7 或 30对应 R2 lifecycle prefix
wrangler secret list 看到 TOKEN 但上传 401 token 值不一致。重新 wrangler secret put TOKEN 设正确值

依赖

  • wrangler ^4 — Cloudflare Workers CLI
  • nanoid ^5 — ID 生成(纯 ESMWorker 兼容)

不被主 CLI 引用

这是独立 Cloudflare Worker 服务,类似 packages/remote-control-server/ 的定位。Monorepo 根 package.jsonworkspaces: ["packages/*", ...] 自动识别本包,但主 CLI 不会 import 它。