mcpField
C
cloud-storage-web

cloud-storage-web

github.com/tencentcloudbase云与 DevOps1.2k46 次安装今天更新
代码仓库

关于 cloud-storage-web

通过 Web SDK 在浏览器中执行 CloudBase 存储操作的技能,涵盖上传、下载、生成 URL 和删除文件,并强调实现上传前检查存储桶和访问控制。

主要功能

  1. 01浏览器文件上传
  2. 02临时及签名 URL 处理
  3. 03文件删除与下载
  4. 04存储桶和存储模式检查
  5. 05安全域名与 RLS 指引

适用场景

  1. 01从 Web 应用上传文件
  2. 02为存储对象生成下载链接
  3. 03排查存储桶缺失问题
  4. 04上传前配置存储权限

MCP 相关

该技能介绍通过 `@cloudbase/js-sdk` 操作浏览器存储,并将 MCP 工具直接管理存储的任务区分开来,后者应转向 CloudBase 平台技能。

安装

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

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

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

详细介绍

同级技能(仅限本地)

同级 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) 上传,并使用 app.storage.from('covers').createSignedUrl(path, expiresIn) 获取访问 URL。

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/storage.md · webv3-pg/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.buckets / storage.objects 上的 PostgreSQL 行级安全策略(RLS)**实施——与 Supabase Storage 完全相同。这些表已授予 anon、authenticated 和 service_role 权限;RLS 是权限控制门槛。传统存储权限标签(READONLY / PRIVATE / CUSTOM)和 JSON 存储安全规则不适用。默认 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 存储安全规则(managePermissions / ModifyStorageSafeRule)相互独立。在 PG 模式中,始终通过 PG SQL 配置存储 RLS,而不是使用旧版安全规则 API。
  • 如果未配置这些策略,浏览器在 PG 模式下调用 app.storage.from('covers').upload() 时会收到 STORAGE_PERMISSION_DENIED。
  • 重新应用策略时,在 DO $$ 块中使用 IF NOT 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