FileAccessor

FileAccessor 是插件访问平台托管文件的读写桥接接口。它允许命令处理器通过平台文件 ID 打开上传的文件,并将生成的输出持久化为新的平台文件——无需接触对象存储客户端、租户 Bucket 解析或凭据管理。这些关注点由宿主负责;accessor 暴露一个稳定的、带审计副作用的操作面。

这是每个需要处理上传、生成报告、转换文档或产出派生产物的插件能力背后的文件 SPI。如果你的插件需要操作原始文件字节,这就是规范契约——而非直接访问对象存储。

概念

accessor 以宿主桥接接口的形式交付给插件代码,与 AiProviderAccessorBackgroundAccessor 采用相同模式。插件不自动装配存储客户端——而是声明 FileAccessor 依赖,在命令执行时通过约定的 CommandContext.settings key 获取宿主绑定的实例:

String SETTINGS_KEY = "__fileAccessor";

宿主实现将所有调用解析到当前租户上下文:适用哪个存储后端、租户拥有哪个 Bucket 和前缀、哪些访问凭据在作用域内,以及生成的文件如何在平台文件注册表中被追踪。

这种拆分使插件文件 I/O 在构造上天然实现存储安全隔离。插件描述意图;平台决定身份、位置和协议。

接口签名

package com.auraboot.framework.plugin.extension;

import java.io.InputStream;

public interface FileAccessor {

    /** FileAccessor 在命令 settings map 中的约定 key */
    String SETTINGS_KEY = "__fileAccessor";

    /**
     * 通过平台文件 ID 打开文件。
     *
     * @param fileId 上传 API 返回的平台文件 pid/id
     * @return 可读文件流;调用方必须关闭
     */
    InputStream open(String fileId);

    /**
     * 将生成的字节持久化为平台文件。
     *
     * @param originalName 建议的下载文件名
     * @param contentType  MIME 类型
     * @param bytes        文件内容
     * @return 已保存的平台文件元数据
     */
    SavedFile save(String originalName, String contentType, byte[] bytes);

    /** 返回给插件处理器的已保存平台文件元数据 */
    record SavedFile(String fileId, String originalName, long size, String url) {}
}

SavedFile 是不可变的。fileId 是稳定的平台 ID,可存储在记录或返回给调用方。url 是预签名或代理 URL,有效期由平台配置的 TTL 决定——不要长期存储它;在提供下载时重新解析。

在命令处理器中获取 accessor

典型模式是:命令表单包含一个文件上传字段,其值为平台文件 ID。处理器打开文件、处理后保存派生结果,并将输出文件 ID 写回上下文数据。

@Extension
public class ConvertReportHandler implements CommandHandlerExtension {

    @Override
    public String getCommandType() {
        return "docs:report.convert";
    }

    @Override
    public Object execute(CommandContext ctx) throws Exception {
        FileAccessor files =
            (FileAccessor) ctx.settings().get(FileAccessor.SETTINGS_KEY);

        // 1. 通过平台文件 ID 读取上传的源文件
        String sourceFileId = (String) ctx.payload().get("sourceFileId");
        byte[] converted;
        try (InputStream in = files.open(sourceFileId)) {
            converted = convertToTargetFormat(in);  // 插件业务逻辑
        }

        // 2. 将派生输出持久化为新的平台文件
        FileAccessor.SavedFile output = files.save(
            "converted-report.pdf",
            "application/pdf",
            converted
        );

        // 3. 将输出元数据写回以供后续流水线阶段使用
        ctx.payload().put("outputFileId", output.fileId());
        ctx.payload().put("outputUrl",    output.url());
        ctx.payload().put("outputSize",   output.size());

        return output.fileId();
    }
}

三点注意:

  • 无存储客户端自动装配。 插件永远看不到 Bucket 名称、凭据或存储 SDK 类。
  • 必须关闭 InputStream open(fileId) 返回由平台存储支撑的活跃流;泄漏它会在高负载下耗尽连接。使用 try-with-resources。
  • url 是短暂的。 在领域记录中存储 fileId,而非 URL。如果你的用例需要,在下载时重新解析 URL。

使用场景与规则

使用场景推荐方式
在命令中处理上传的文件使用 try-with-resources 调用 open(fileId);fileId 来自表单字段
产出生成产物(PDF、CSV、图片)调用 save(name, contentType, bytes) 后存储返回的 fileId
多步链式转换保存每个中间结果;在步骤之间传递 fileId
向用户提供下载链接请求时获取 SavedFile.url;不要缓存该 URL
跨租户访问文件不允许;accessor 的作用域限定于当前租户上下文
不缓冲地流式处理大文件open() 流增量读取;在可能的情况下避免在内存中缓冲整个文件

错误语义

两个方法在失败时均抛出非受检异常,因为存储调用跨越网络和租户凭据边界。

  • 文件未找到open(fileId) 在文件 ID 不存在于租户文件注册表或后端存储中无对应对象时抛出。不要吞掉此异常;将其呈现给用户以便重新上传。
  • 访问被拒绝 — 文件存在但属于不同的租户上下文。平台在 accessor 层强制执行此限制;插件永远不会看到跨租户字节。
  • 存储后端不可用 — 对象存储不可达。异常通过命令流水线传播,作为瞬时错误呈现。不要实现插件侧重试;平台的重试和熔断策略适用于宿主层。
  • 负载过大save(...) 拒绝超过租户配置文件大小上限的字节数组。在调用前进行校验或分割。

只捕获可恢复的情况。将存储异常包装为通用消息会向平台监控面隐藏租户配额和后端健康信号。

审计与关联

每次 opensave 调用都会记录到平台的文件审计追踪中,以 fileIdtenantIdcommandType 和命令流水线的关联 ID 为键。无需额外的元数据 API——宿主在命令执行上下文中自动桥接审计关联信息。

不要在处理器中记录原始文件字节或文件 URL;平台的审计存储处理溯源。重复记录 URL 会在 URL 过期后产生陈旧引用。

常见陷阱

模式为何会出问题
直接自动装配对象存储客户端绕过租户隔离、凭据轮转和审计;不会通过平台代码审查
在领域记录中存储 SavedFile.urlURL 有时间限制;存储的 URL 静默过期,破坏下载功能
不关闭 open() 返回的 InputStream泄漏存储连接;高负载下表现为超时失败
对来自不同租户的文件 ID 调用 open()在 accessor 层被拒绝访问;通过显式交接的命令链实现跨实体流程
为大文件缓冲整个 open()并发命令执行时的内存压力;尽可能增量流式处理
save() 视为幂等每次调用创建具有新 fileId 的新平台文件条目;如果命令可重试,保存前进行去重

与其他 SPI 的关系

  • CommandHandlerExtension — 如何参与命令流水线;FileAccessorexecute 中获取和使用。
  • AiProviderAccessor — 用于 LLM 提供商调用的相同宿主桥接模式;在文档 AI 流水线中常与 FileAccessor 一起使用。
  • BackgroundAccessor — 用于后台任务系统字段的相同宿主桥接模式。

企业版说明 — 已签名文件产物溯源(生成文件的密码学保管链)、静态加密租户密钥选项(每租户 KMS 支撑的存储加密)以及长期归档层路由(根据保留策略将生成产物自动分层至冷存储)是商业版专有能力。社区版与商业版的 FileAccessor 接口完全相同;两者之间的差异体现在围绕其构建的运营和合规层面。

后续步骤