mcpField
C
cloud-storage-web

cloud-storage-web

github.com/tencentcloudbase云与 DevOps791.0k 次安装1 天前更新
代码仓库

关于 cloud-storage-web

cloud-storage-web 是通过 `@cloudbase/js-sdk` 在浏览器中操作 CloudBase 存储的技能,涵盖上传、下载、临时 URL 和删除。它还说明存储桶前置条件,以及 NoSQL 与 PG 存储流程的差异。

主要功能

  1. 01上传浏览器文件
  2. 02生成文件访问 URL
  3. 03删除存储对象
  4. 04触发浏览器下载
  5. 05处理 NoSQL 与 PG 存储方式

适用场景

  1. 01为 Web 应用添加文件上传
  2. 02提供临时下载链接
  3. 03在浏览器流程中删除存储对象
  4. 04排查存储桶缺失或访问策略问题

MCP 相关

该技能指导 Agent 使用 CloudBase MCP 管理和查询工具检查或创建存储桶,并在浏览器上传前处理环境配置要求。

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/tencentcloudbase/skills --skill cloud-storage-web

安装后在 Claude Code 中输入/cloud-storage-web确认已加载。

附带文件LICENSE.md

详细介绍

同级技能(仅限本地)

同级 CloudBase 技能与此技能一起提供。使用 ../auth-tool-cloudbase/SKILL.md 这类本地相对路径。

如果当前环境中缺少引用的同级技能文件,请让用户安装完整的 CloudBase 插件(或缺少的技能)。不要通过 HTTP 将远程技能或协议 Markdown 获取到 Agent 上下文中。

Cloud Storage Web SDK

激活约定

以下情况优先使用此技能
  • 浏览器或 Web 应用需要通过 @cloudbase/js-sdk 上传、下载或管理 CloudBase 存储对象。
  • 请求提到前端代码中的 uploadFile、getTempFileURL、deleteFile 或 downloadFile。
编写代码前阅读(如果)
  • 任务涉及浏览器端存储,但仍需将其与小程序存储、后端存储管理或静态托管部署区分开来。
  • 请求可能受到安全域名或前端身份验证的阻碍。
然后还要阅读
  • Web 登录和身份 -> ../auth-web-cloudbase/SKILL.md
  • Web 应用通用设置 -> ../web-development/SKILL.md
  • 通过 MCP 工具直接管理存储 -> ../cloudbase-platform/SKILL.md
以下情况不要使用
  • 小程序文件 API。
  • 通过 MCP 直接管理后端或 Agent 端存储。
  • 使用 manageHosting(action="upload") 部署静态网站托管。
  • 数据库操作。
常见错误 / 注意事项
  • 浏览器代码上传文件时未配置安全域名。
  • 将此技能用于静态托管,而不是存储对象。
  • 混用浏览器 SDK 上传流程与服务器端文件管理任务。
  • 误以为临时下载 URL 是永久链接。
  • 忽略 STORAGE_NOT_EXIST;这表示目标存储桶/资源尚未就绪,而不是应该让浏览器上传代码伪造一个 URL。
  • 在本地 Vite 或开发服务器任务中,测试 app.uploadFile() 前忘记将当前浏览器的确切 host:port 加入白名单。
  • 将 CloudBase PG / pgstore 当作旧版 NoSQL CloudBase 存储。PG 环境使用独立的 pgstore 后端,其存储桶不会根据旧 NoSQL 存储桶自动创建。如果 pgstore 没有存储桶,每次上传都会返回 STORAGE_BUCKET_NOT_FOUND,随后 SDK 会发出 PUT https://undefined/(在 DevTools 中显示为 net::ERR_NAME_NOT_RESOLVED)。必须将存储桶存在视为硬性前提,就像 Supabase 一样:在 Supabase Storage 中,每次上传都必须指向一个已创建的存储桶;CloudBase PG 遵循相同模式。
最简检查清单
  • 确认调用方是浏览器/Web 应用。
  • 仅初始化一次 Web SDK。
  • 测试上传前,确认当前环境中存在 CloudBase 存储。如果环境没有默认存储桶,请使用可用的 MCP 管理/查询工具检查或创建/选择存储桶。在 PG / pgstore 环境中,DescribeEnvs 返回的旧 NoSQL 存储桶不算可用的 pgstore 存储桶;进行任何浏览器上传前,必须显式创建一个。旧 NoSQL 存储桶仍可用于已指向它的旧版 app.uploadFile() 流程——PG 和 NoSQL 存储可以共存;此技能适用于两者。
  • 检查安全域名/CORS 要求。
  • 编写代码前选择正确的存储方法。
本地开发流程

当应用运行在本地浏览器源站上,且必须从前端上传文件时:

  1. 使用 queryEnv 并将 action="domains",检查当前安全域名白名单。
  2. 将浏览器源站转换为 CloudBase 白名单条目格式:
    • 浏览器源站 http://127.0.0.1:4173 -> 白名单条目 127.0.0.1:4173
    • 浏览器源站 http://localhost:5173 -> 白名单条目 localhost:5173
  3. 如果缺少当前主机的确切条目,请先调用 envDomainManagement 并将 action="create",添加该主机条目,再使用 app.uploadFile()。
  4. 如果运行时端口可能在不同运行间发生变化,不要假定任何固定的默认端口列表就足够。请重新检查测试或最终验证时实际使用的浏览器源站,然后添加确切的 host:port。
  5. 告知用户安全域名变更可能需要几分钟才能生效;请轮询 queryEnv(action="domains"),而不是盲目休眠一段固定的长时间。
  6. 只有完成上述步骤后,才能实现并测试浏览器端的 app.uploadFile() 流程。

如果 app.uploadFile() 返回 STORAGE_NOT_EXIST,请停止修改前端代码,先修复环境侧的存储资源。重新检查环境存储列表;如果任务允许,请创建或选择一个可用的存储桶,然后重试相同的 SDK 上传流程。

如果任务使用浏览器端文件上传,请将此视为前置要求,而非可选的收尾工作。

上传代码前必须确认存储桶存在

与 Supabase Storage 一样,CloudBase Storage 要求目标存储桶在客户端上传前必须已存在。旧版 CloudBase NoSQL 存储(STORAGE_NOT_EXIST)和新版 PG / pgstore 后端(STORAGE_BUCKET_NOT_FOUND)都遵循这一要求。

与 Supabase 的对应关系:

步骤SupabaseCloudBase
创建存储桶supabase.storage.createBucket('covers', { public: true })(在管理端使用服务角色)在 PG 模式下,通过 PG 存储 HTTP API / CLI / 控制台 / 在适当情况下通过 SQL 在 storage.buckets 中创建存储桶。浏览器 SDK 无法创建存储桶。
上传supabase.storage.from('covers').upload('a.png', file)PG 模式:app.storage.from('covers').upload('a.png', file) — from(bucketName) 用于指定 pgstore 存储桶。
非 PG 模式:app.storage.from().upload('covers/a.png', file) — 存储桶名称作为路径的第一段。
存储桶不存在错误Bucket not found浏览器会收到 STORAGE_BUCKET_NOT_FOUND(PG)或 STORAGE_NOT_EXIST(NoSQL),随后发起 PUT https://undefined/,因为 SDK 仍会尝试 PUT 一个缺少 metadata.url 的对象。

任何需要在浏览器中上传的任务,都必须先完成以下步骤:

  1. 先列出已有存储桶。对于 PG / pgstore,旧版 NoSQL 存储桶(DescribeEnvs.Storages[] 返回的 6d63-…-1409864723 格式)不是有效的 pgstore 存储桶 — 不要假设它可用。
  2. 如果上传目标(例如 covers)没有可用的存储桶,请先通过 PG 存储管理入口创建一个,再修改前端上传代码。在代码中添加 covers 路径前缀不会自动创建存储桶。
  3. 创建存储桶后,上传方式取决于环境:
    • PG / pgstore:app.storage.from('covers').upload('<file>', file) — 将存储桶名称传入 from()
    • 非 PG(NoSQL):app.storage.from().upload('covers/<file>', file) — 存储桶名称作为路径的第一段
  4. 如果在 DevTools 中看到请求 https://undefined/ 时出现 net::ERR_NAME_NOT_RESOLVED,说明 SDK 正在响应缺少的 metadata.url 字段 — 几乎总是因为存储桶不存在,或 SDK 请求在上游被拒绝。先在 DevTools 中检查失败的 POST .../v1/storages/get-objects-upload-info 响应;code 字段(例如 STORAGE_BUCKET_NOT_FOUND、STORAGE_CONTENT_LENGTH_REQUIRED、INVALID_PARAM)会明确指出需要修复的问题。

不要悄悄忽略上传失败。如果 uploadCoverImage() 拒绝,父级 createArticle() 也必须拒绝 — 绝不能使用伪造的 URL 或占位符继续执行 db.from(...).insert(...),也绝不能让 UI 显示成功提示。

⚠️ PG 模式上传:请使用 app.storage.from('bucket'),不要使用 app.uploadFile()

在 PG / pgstore 环境中,上传时请使用 app.storage.from('covers').upload(key, file),获取访问 URL 时请使用 app.storage.from('covers').createSignedUrl(path, expiresIn)。

PG 模式下不要使用旧版 NoSQL API:

  • ❌ app.uploadFile() — 这是旧 NoSQL 的上传 API
  • ❌ app.getTempFileURL() — 这是旧 NoSQL 的获取 URL 方式
  • ❌ app.storage.from().upload('covers/file', file) — 没有传入 bucket 名

请改用:

  • ✅ app.storage.from('covers').upload('file', file) — PG 模式上传
  • ✅ app.storage.from('covers').createSignedUrl('file', 3600) — 获取签名 URL(返回 fullSignedURL 字段)

不同模式的返回结构不同(v3 SDK)——请复制正确的列:

调用传统模式(from() 无参,cloud:// fileID)PG 模式(from('bucket'),bucket 内对象名)
upload(path, file){ data: { id, path, fullPath } };upsert 默认 true{ data: { id, ... } };upsert 默认 false
createSignedUrl(path, expiresIn)await → { data: { signedUrl } }await → { data: { fullSignedURL } }
getPublicUrl(path)await → { data: { publicUrl } }同步调用(不 await) → { data: { publicUrl } }

来源:webv3/classic/storage.md · webv3/storage.md(原始 Markdown)。

PG 模式 URL 解析:公开桶直链与签名 URL
Bucket 类型URL 策略代码
公开桶(storage.buckets.public = true)直链,无需登录态,可直接用于 <img src>app.storage.from('covers').getPublicUrl('a.png') → { data: { publicUrl } }
私有桶签名 URL,带过期时间app.storage.from('covers').createSignedUrl('a.png', 3600)
  • 公开桶直链能否访问取决于 storage.objects 的 RLS SELECT 策略是否允许 anon——建桶 SQL 与策略模板见 postgresql-development-cloudbase/references/storage-pg.md "Public-read bucket template"。
  • 展示层应使用 onerror 兜底(直链被策略拦截时降级到签名 URL),不要硬依赖单一取址流程。
  • 业务表只存 bucket + key(或 SDK 解析出的最终 URL),不要在浏览器中手动拼接 URL。
创建 Bucket 后:storage RLS(PG / pgstore 环境必须配置)

在 PG / pgstore 环境中,storage 访问控制通过 **storage.buckets / storage.objects 上的 PostgreSQL 行级安全(RLS)**实现——与 Supabase Storage 完全相同。这些表已授权给 anon、authenticated 和 service_role;RLS 是权限控制关卡。传统 storage 权限标签(READONLY / PRIVATE / CUSTOM)和 JSON storage 安全规则不适用。默认 RLS 策略为全部拒绝,因此,即使 bucket 已创建,浏览器调用 app.storage.from('covers').upload() 仍会失败并返回 STORAGE_PERMISSION_DENIED,除非你配置策略。

创建 bucket 后,请使用 managePgDatabase(action="execute", confirm=true) 运行以下 SQL:

ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;

-- Allow authenticated users to upload files
CREATE POLICY "authenticated_upload" ON storage.objects
  FOR INSERT TO authenticated
  WITH CHECK (true);

-- Allow authenticated users to read/download files
CREATE POLICY "authenticated_read" ON storage.objects
  FOR SELECT TO authenticated
  USING (true);

-- Optional: allow users to update/delete their own files
CREATE POLICY "users_manage_own" ON storage.objects
  FOR UPDATE TO authenticated
  USING (auth.uid() = owner_id)
  WITH CHECK (auth.uid() = owner_id);

要点:

  • storage.objects RLS 与 CloudBase 旧版 NoSQL storage 安全规则(managePermissions / ModifyStorageSafeRule)相互独立。在 PG 模式下,始终通过 PG SQL 配置 storage RLS,而非使用旧版安全规则 API。
  • 如果未配置这些策略,浏览器在 PG 模式下调用 app.storage.from('covers').upload() 时会收到 STORAGE_PERMISSION_DENIED。
  • 重新应用策略时,请在 DO $$ 块中使用 IF NOT EXISTS,避免重跑时出现 "policy already exists" 错误。

概览

使用此技能通过 CloudBase Web SDK 执行浏览器端云存储操作。

典型任务:

  • 从浏览器上传文件
  • 生成临时下载 URL
  • 删除文件
  • 触发浏览器下载

SDK 初始化

初始化参考:webv3/initialization.md

import cloudbase from "@cloudbase/js-sdk";

const app = cloudbase.init({
  env: "your-env-id",
  accessKey: import.meta.env.VITE_PUBLISHABLE_KEY, // publishable key — auto-provisioned, see below
});

初始化规则:

  • **自动配置 Publishable key(请勿询问用户):**浏览器端存储请求属于数据面调用,需要 Publishable accessKey。编写客户端代码前,调用 queryAppAuth(action="getPublishableKey");如果返回为空,则调用 manageAppAuth(action="ensurePublishableKey");然后将密钥写入 .env.local,变量名为 VITE_PUBLISHABLE_KEY(如果文件不存在则创建),并通过 import.meta.env.VITE_PUBLISHABLE_KEY 读取。切勿将密钥硬编码到源文件中。只有在两次 MCP 调用都失败时,才回退到控制台(https://tcb.cloud.tencent.com/dev?envId={env}#/env/apikey)。
  • 使用同步初始化,并共享同一个 app 实例。
  • 不要在每个组件中重复初始化。
  • 如果操作依赖用户身份,请在存储操作前处理身份验证。

方法路由

  • 从浏览器上传 → app.uploadFile()
  • 获取临时预览/下载 URL → app.getTempFileURL()
  • 删除现有文件 → app.deleteFile()
  • 触发浏览器下载 → app.downloadFile()

上传

const result = await app.uploadFile({
  cloudPath: "uploads/avatar.jpg",
  filePath: selectedFile
});
上传规则
  • cloudPath 必须包含文件名。
  • 使用 / 创建文件夹结构。
  • 在 CloudBase PG / pgstore 环境中,from(bucketName) 参数用作 bucket 名称(例如 from('covers')),而 upload(key, file) 接受不带 bucket 前缀的 key。bucket 必须已存在。这与 Supabase Storage 的模式相同——切勿上传到尚未创建的 bucket。
  • 上传前验证文件类型和大小。
  • 如果文件较大且用户体验需要,显示上传进度。
  • 在本地开发源站上,先确认环境安全域名中已存在准确的前端源站,再判断上传路径是否可用。
  • 请按照 queryEnv(action="domains") 返回的白名单条目格式进行匹配,该格式通常是 host:port,而不是完整的 http://... URL。
  • 如果环境中没有存储 bucket,或 SDK 返回 STORAGE_NOT_EXIST / STORAGE_BUCKET_NOT_FOUND,请使用 CloudBase 管理/MCP 存储工具创建或选择 bucket,然后重试。不要将此情况视为可忽略的上传成功。
  • app.uploadFile() 成功后,不要通过拼接 envId、bucket 域名或 cloudPath 伪造看似公开的 URL。请使用返回的 fileID 调用 app.getTempFileURL(),并存储或显示 SDK 解析出的 URL。
进度示例
await app.uploadFile({
  cloudPath: "uploads/avatar.jpg",
  filePath: selectedFile,
  onUploadProgress: ({ loaded, total }) => {
    const percent = Math.round((loaded * 100) / total);
    console.log(percent);
  }
});

临时 URL

const result = await app.getTempFileURL({
  fileList: [
    {
      fileID: "cloud://env-id/uploads/avatar.jpg",
      maxAge: 3600
    }
  ]
});

当浏览器需要预览或下载私有文件,且不能暴露永久公开链接时,请使用临时 URL。

典型的上传 + 预览流程:

const uploadResult = await app.uploadFile({
  cloudPath: "uploads/avatar.jpg",
  filePath: selectedFile
});

const tempUrlResult = await app.getTempFileURL({
  fileList: [{ fileID: uploadResult.fileID, maxAge: 3600 }]
});

const previewUrl = tempUrlResult.fileList?.[0]?.tempFileURL || tempUrlResult.fileList?.[0]?.download_url;
if (!previewUrl) {
  throw new Error("Failed to resolve temporary file URL after upload");
}

删除文件

await app.deleteFile({
  fileList: ["cloud://env-id/uploads/old-avatar.jpg"]
});

在确认删除成功前,务必检查每个文件的处理结果。

下载文件

await app.downloadFile({
  fileID: "cloud://env-id/uploads/report.pdf"
});

浏览器发起下载时使用此方法。对于程序化渲染或预览,优先使用 getTempFileURL()。

安全域名提醒

为避免 CORS 问题,请将前端域名添加到 CloudBase 安全域名中。在启用 MCP 的工作流中,优先在编写浏览器上传代码前使用工具检查并更新域名。

{ "tool": "queryEnv", "action": "domains" }

确定要添加的域名时,请使用实际的浏览器源站。如果页面运行在自定义域名或本地开发端口上,请添加准确的 host:port 值,不要根据硬编码列表猜测。

{
  "tool": "envDomainManagement",
  "action": "create",
  "domains": ["<actual-browser-host>:<actual-browser-port>"]
}

将真实浏览器源站与 queryEnv(action="domains") 返回的白名单条目格式进行匹配。对于本地 Vite 和预览服务器,端口可能会在不同运行期间发生变化,因此不要假设某个固定默认端口始终可用。

典型示例:

  • <your-local-host>:<actual-port>
  • <your-custom-domain>

最佳实践

  1. 使用清晰的文件夹结构,例如 uploads/、avatars/、documents/。
  2. 在上传前于浏览器中验证文件大小和类型。
  3. 使用过期时间合理的临时 URL。
  4. 清理过时文件,避免遗留孤立的存储对象。
  5. 将特权批量管理任务交由后端或 MCP 工作流处理,而不是让浏览器直接访问。

错误处理

try {
  const result = await app.uploadFile({
    cloudPath: "uploads/file.jpg",
    filePath: selectedFile
  });
  console.log(result.fileID);
} catch (error) {
  console.error("Storage operation failed:", error);
}

评论

支持 Markdown