mcpField
E
extension-object-storage

extension-object-storage

github.com/caffeinelabs云与 DevOps228.6k 次安装6 天前更新
代码仓库

关于 extension-object-storage

extension-object-storage 是用于 Caffeine AI 的 skill,将文件内容存储在链下,并在应用后端保存引用。它介绍基于 ExternalBlob 跟踪文件,以及前端上传、展示和下载流程。

主要功能

  1. 01链下文件存储
  2. 02ExternalBlob 文件引用
  3. 03上传时保留 MIME 类型和文件名
  4. 04通过代理 URL 展示文件
  5. 05按原文件名下载内容

适用场景

  1. 01从浏览器上传图片或文档
  2. 02在后端记录中保存文件引用
  3. 03内嵌展示图片或视频
  4. 04按原文件名下载文件

安装

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

$npx skills add https://github.com/caffeinelabs/skills --skill extension-object-storage

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

详细介绍

对象存储

面向 Caffeine AI 的对象存储扩展。

概述

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

必需设置清单

以下四个步骤均为必需。跳过任何一步都会导致上传时出现 403 Forbidden: Invalid payload。

  1. mops 依赖项 — 在 mops.toml 的 [dependencies] 下添加 caffeineai-object-storage。
  2. 调用混入 — 在 main.mo 中使用 include 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();

  // Track file references
  type Data = {
    id : Text;
    blob : Storage.ExternalBlob;
    name : Text;
    // other metadata
  };
};

错误做法:切勿自行实现存储方法

绝不要自行实现 _immutableObjectStorageCreateCertificate 或任何其他 _immutableObjectStorage* 方法。这些是平台保留的方法名,仅由 mops 包中的 MixinObjectStorage 混入提供。手写实现会产生错误的返回类型,并导致上传时出现 403 Forbidden: Invalid payload。

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

// WRONG: Do not write this yourself
public shared func _immutableObjectStorageCreateCertificate(fileHash : Text) : async Blob {
  CertifiedData.set(Blob.fromArray(hashBytes));
  Blob.fromArray([])
};

错误做法——自定义混入文件模仿平台结构:

// WRONG: Do not create 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-Type 和 Content-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);

// Conditional rendering
{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 类型和文件名
检测文件类型filename 或 mimeType 字段切勿检查 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后端 canister 缺少 _immutableObjectStorageCreateCertificate,或返回了错误类型在 mops.toml 中安装 caffeineai-object-storage,在 main.mo 中添加 include MixinObjectStorage(),然后重新部署
所有文件均返回 403 Forbidden: Invalid payload已安装 npm 包 @caffeineai/object-storage,但未安装 mops 依赖 caffeineai-object-storage添加 mops 依赖并重新构建后端
方法存在但仍返回 403手写的存根返回了错误类型(例如返回 Blob 或 (),而不是 record { method; blob_hash })移除自定义实现,改用平台 mixin
Forbidden: Owner does not have an account with the cashierCashier 注册问题(与此技能无关)重新部署后端 canister,以触发自动修复注册

评论

支持 Markdown