extension-object-storage

extension-object-storage

通用文件/对象存储,适用于图片、视频、文档及其他批量数据。非常适合图片库、视频库及其他文件或对象管理场景。支持超出互联网计算机限制的大文件,并提供浏览器缓存的HTTP URL访问。

0Star
0Fork
更新于 2026/7/13
SKILL.md
readonly只读
name
extension-object-storage
description

通用文件/对象存储,适用于图片、视频、文档及其他批量数据。非常适合图片库、视频库及其他文件或对象管理场景。支持超出互联网计算机限制的大文件,并提供浏览器缓存的HTTP URL访问。

version
1.1.0

对象存储

Caffeine AI 的对象存储扩展。

概述

本技能提供链下文件/对象存储,并带有链上引用。MixinObjectStorage 混合器为文件操作提供基础设施;您可以使用 Storage.ExternalBlob 在自己的数据结构中跟踪上传的文件。

必需设置清单

所有四个步骤都是强制性的。跳过任何一步都会在上传时导致 403 Forbidden: Invalid payload

  1. mops 依赖 — 在 mops.toml[dependencies] 下添加 caffeineai-object-storage
  2. 混合器调用 — 在 main.moinclude MixinObjectStorage()(从 "mo:caffeineai-object-storage/Mixin" 导入)。
  3. Storage.ExternalBlob 类型 — 每个表示文件的数据字段必须使用 Storage.ExternalBlob,绝不能使用 Text
  4. 前端 npm 包 — 安装 @caffeineai/object-storage,并在调用处使用 ExternalBlob.fromBytes(bytes, file.type, file.name)

关键:前端包(@caffeineai/object-storage)在没有后端 mops 包(caffeineai-object-storage)的情况下无法工作。仅安装 npm 包而不安装 mops 包会导致静默上传失败(存储网关返回 403)。您必须同时安装两者。

后端

文件内容存储在链下。后端使用 mo:caffeineai-object-storage/Storage 中的 Storage.ExternalBlob 类型管理对外部文件的引用。前端处理实际的上传/下载;后端仅存储引用。

关键:任何表示文件、图片、照片、文档或媒体的数据字段必须使用 Storage.ExternalBlob 作为其类型——绝不能使用 Text。使用 Text 会破坏上传/下载代理。接受文件上传的方法参数也必须使用 Storage.ExternalBlob,而不是 Text

正确:

blob : Storage.ExternalBlob

错误:

blobId : Text
imageUrl : Text
fileRef : Text

模块 API

您从 mo:caffeineai-object-storage/Storage 中使用的唯一类型是 ExternalBlob(即 Blob)。Storage.mo 中的所有其他函数都是 MixinObjectStorage 使用的内部基础设施——不要直接调用它们。

main.mo 中设置

include MixinObjectStorage() 必须放在 main.mo 中,而不是自定义混合器文件中。您自己的文件跟踪逻辑放在单独的混合器中。

import MixinObjectStorage "mo:caffeineai-object-storage/Mixin";
import Storage "mo:caffeineai-object-storage/Storage";

actor {
  include MixinObjectStorage();

   // 跟踪文件引用
  type Data = {
        id: Text;
        blob: Storage.ExternalBlob;
        name: Text;
        // 其他元数据
    };
};

错误:不要自己实现存储方法

切勿自己创建 _immutableObjectStorageCreateCertificate 或任何其他 _immutableObjectStorage* 方法的实现。这些是平台保留的方法名,仅由 mops 包中的 MixinObjectStorage 混合器提供。手写实现会产生错误的返回类型,并在上传时导致 403 Forbidden: Invalid payload

错误——在 main.mo 中内联存根:

// 错误:不要自己编写
public shared func _immutableObjectStorageCreateCertificate(fileHash : Text) : async Blob {
  CertifiedData.set(Blob.fromArray(hashBytes));
  Blob.fromArray([])
};

错误——模仿平台形状的自定义混合器文件:

// 错误:不要创建 src/backend/mixins/object-storage-api.mo
import ObjectStorageMixin "mixins/object-storage-api";
include ObjectStorageMixin();

正确的导入路径始终是 "mo:caffeineai-object-storage/Mixin"——一个 mops 包,绝不是相对路径。任何像 "mixins/object-storage-api""./ObjectStorage" 的相对导入都是错误的。

平台混合器产生的正确签名是:

_immutableObjectStorageCreateCertificate : (blobHash : Text) -> async record { method : Text; blob_hash : Text }

任何其他返回类型(Blob()Text 等)都会导致网关验证失败。

前端

后端的 Blob 字段在前端表示为 ExternalBlob

import { ExternalBlob } from "@caffeineai/object-storage";
import type { FileRecord } from "@caffeineai/object-storage";

ExternalBlob API

class ExternalBlob {
  getBytes(): Promise<Uint8Array<ArrayBuffer>>;
  getDirectURL(): string;
  static fromURL(url: string): ExternalBlob;
  static fromBytes(
    blob: Uint8Array<ArrayBuffer>,
    contentType?: string,
    filename?: string,
  ): ExternalBlob;
  withUploadProgress(onProgress: (percentage: number) => void): ExternalBlob;
}

上传文件

将浏览器的 File 类型和名称传递给 fromBytes,以便网关 blob 树存储 Content-TypeContent-Disposition(原始文件名)。同时将 file.name 传递给后端,以便应用记录保留文件名用于列表和 UI。

const handleUpload = async (file: File) => {
  const bytes = new Uint8Array(await file.arrayBuffer());
  const blob = ExternalBlob.fromBytes(bytes, file.type, file.name).withUploadProgress((pct) => {
    setProgress(pct);
  });

  await actor.uploadFile(file.name, blob);
};

网关 GET/HEAD 响应通过 Content-Disposition 回显存储的文件名。保留后端的 filename 字段用于查询和显示,无需访问网关。

显示文件

使用 getDirectURL() 进行内联显示(图片、视频)。这会返回一个不透明的代理 URL——它没有文件扩展名,因此切勿检查 URL 来确定文件类型。

<img src={record.blob.getDirectURL()} alt={record.filename} />

文件类型检测

关键:切勿通过检查 getDirectURL() 返回的 URL 来检测文件类型。这些是不带扩展名的不透明代理 URL。相反,使用后端记录中的 filename 字段:

const isImage = (filename: string) =>
  /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico)$/i.test(filename);

// 条件渲染
{isImage(record.filename) ? (
  <img src={record.blob.getDirectURL()} alt={record.filename} />
) : (
  <div>{record.filename}</div>
)}

如果后端也返回 mimeType 字段,优先使用它:

const isImage = (mimeType?: string) => mimeType?.startsWith("image/");

下载文件

对于保留原始文件名的下载,使用 getBytes() 创建可下载的链接:

const handleDownload = async (record: FileRecord) => {
  const bytes = await record.blob.getBytes();
  const blob = new Blob([bytes]);
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = record.filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
};

使用 getDirectURL() 进行内联显示,使用 getBytes() 进行另存为下载。

总结

用例 方法 说明
显示图片/视频 blob.getDirectURL() 流式传输,缓存
下载并保留文件名 blob.getBytes() 包装为 Blob + 锚点
从浏览器上传 ExternalBlob.fromBytes(bytes, file.type, file.name) 网关头部中的 MIME + 文件名
检测文件类型 filenamemimeType 字段 切勿检查 URL

验证设置

确认后端已安装 mops 依赖。检查 src/backend/mops.toml

[dependencies]
caffeineai-object-storage = "0.1.2"

如果 [dependencies] 中缺少 caffeineai-object-storage,无论前端做什么,对象存储都无法工作。添加它,运行 mops install,然后重新构建。

故障排除

错误 原因 修复
PUT /v1/blob-tree/ 返回 403 Forbidden: Invalid payload 后端容器缺少 _immutableObjectStorageCreateCertificate 或返回错误类型 在 mops.toml 中安装 caffeineai-object-storage,在 main.mo 中添加 include MixinObjectStorage(),重新部署
403 Forbidden: Invalid payload(所有文件) 已安装 @caffeineai/object-storage npm 包但未安装 caffeineai-object-storage mops 包 添加 mops 依赖并重新构建后端
方法存在但仍返回 403 手写存根返回错误类型(例如 Blob() 而不是 record { method; blob_hash } 移除自定义实现,改用平台混合器
Forbidden: Owner does not have an account with the cashier 收银员注册问题(与本技能无关) 重新部署后端容器以触发自愈注册