关于 cloud-storage-web
cloud-storage-web 是通过 `@cloudbase/js-sdk` 在浏览器中操作 CloudBase 存储的技能,涵盖上传、下载、临时 URL 和删除。它还说明存储桶前置条件,以及 NoSQL 与 PG 存储流程的差异。
主要功能
- 01上传浏览器文件
- 02生成文件访问 URL
- 03删除存储对象
- 04触发浏览器下载
- 05处理 NoSQL 与 PG 存储方式
适用场景
- 01为 Web 应用添加文件上传
- 02提供临时下载链接
- 03在浏览器流程中删除存储对象
- 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 要求。
- 编写代码前选择正确的存储方法。
本地开发流程
当应用运行在本地浏览器源站上,且必须从前端上传文件时:
- 使用
queryEnv并将action="domains",检查当前安全域名白名单。 - 将浏览器源站转换为 CloudBase 白名单条目格式:
- 浏览器源站
http://127.0.0.1:4173-> 白名单条目127.0.0.1:4173 - 浏览器源站
http://localhost:5173-> 白名单条目localhost:5173
- 浏览器源站
- 如果缺少当前主机的确切条目,请先调用
envDomainManagement并将action="create",添加该主机条目,再使用app.uploadFile()。 - 如果运行时端口可能在不同运行间发生变化,不要假定任何固定的默认端口列表就足够。请重新检查测试或最终验证时实际使用的浏览器源站,然后添加确切的
host:port。 - 告知用户安全域名变更可能需要几分钟才能生效;请轮询
queryEnv(action="domains"),而不是盲目休眠一段固定的长时间。 - 只有完成上述步骤后,才能实现并测试浏览器端的
app.uploadFile()流程。
如果 app.uploadFile() 返回 STORAGE_NOT_EXIST,请停止修改前端代码,先修复环境侧的存储资源。重新检查环境存储列表;如果任务允许,请创建或选择一个可用的存储桶,然后重试相同的 SDK 上传流程。
如果任务使用浏览器端文件上传,请将此视为前置要求,而非可选的收尾工作。
上传代码前必须确认存储桶存在
与 Supabase Storage 一样,CloudBase Storage 要求目标存储桶在客户端上传前必须已存在。旧版 CloudBase NoSQL 存储(STORAGE_NOT_EXIST)和新版 PG / pgstore 后端(STORAGE_BUCKET_NOT_FOUND)都遵循这一要求。
与 Supabase 的对应关系:
| 步骤 | Supabase | CloudBase |
|---|---|---|
| 创建存储桶 | 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 的对象。 |
任何需要在浏览器中上传的任务,都必须先完成以下步骤:
- 先列出已有存储桶。对于 PG / pgstore,旧版 NoSQL 存储桶(
DescribeEnvs.Storages[]返回的6d63-…-1409864723格式)不是有效的 pgstore 存储桶 — 不要假设它可用。 - 如果上传目标(例如
covers)没有可用的存储桶,请先通过 PG 存储管理入口创建一个,再修改前端上传代码。在代码中添加covers路径前缀不会自动创建存储桶。 - 创建存储桶后,上传方式取决于环境:
- PG / pgstore:
app.storage.from('covers').upload('<file>', file)— 将存储桶名称传入from() - 非 PG(NoSQL):
app.storage.from().upload('covers/<file>', file)— 存储桶名称作为路径的第一段
- PG / pgstore:
- 如果在 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.objectsRLS 与 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>
最佳实践
- 使用清晰的文件夹结构,例如
uploads/、avatars/、documents/。 - 在上传前于浏览器中验证文件大小和类型。
- 使用过期时间合理的临时 URL。
- 清理过时文件,避免遗留孤立的存储对象。
- 将特权批量管理任务交由后端或 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);
}