Skip to content

文件 I/O

插件运行在沙箱中,无法直接触碰文件系统。它通过 FengYuClient.files.* 请求文件访问,宿主将其转换为 /api/plugin-runtime/{id}/files/** 下的调用。宿主为每个被授权的路径签发一个不透明的 FileRef(id 为 ref_<uuid>),把上传快照进一个临时目录树,并仅在派发 worker RPC 的那一刻把 FileRef 改写为绝对路径。权限把守着每一个 endpoint。

授权模型

一个 FileRef 是一个不透明的句柄:

ts
interface FileRef { id: string; name: string; kind: 'file'|'directory'; access: 'read'|'write'|'read-write'; size: number }
  • id(例如 ref_3f2a...)是 UI 能看到的唯一值。
  • 授权存放在一个以 id 为键、按插件 id 划分作用域的内存 ConcurrentHashMap 中。
  • 在 RPC 派发时,宿主遍历 params,找出任何形如 {id:"ref_..."} 的值,通过授权表解析,并在发送给 worker 之前将其改写为绝对文件系统路径。worker 收到的是真实路径,永远不会是原始上传字节,也永远不会是其他插件的 ref。

Endpoint

所有五个 endpoint 都位于基址 /api/plugin-runtime/{id}/files 之下。每一个都由插件清单中声明的权限把守。

方法 + 路径Body权限返回
POST /uploadmultipart filefiles.readFileRef(单个被上传文件,快照进 temp)
POST /upload-directorymultipart files + paths[];可选 access=read-writefiles.readread-write 同时要求 files.read + files.writeFileRef(由上传的目录树在 temp 中重建的目录)
POST /nativeJSON {path, kind, access}files.read 和/或 files.writeFileRef(仅桌面端——Electron 原生对话框返回一个原生路径,宿主将其包装为 ref)
POST /output(无)files.writeFileRef(一个新分配的可写输出目录)
GET /export/{ref}files.write被授权目录的 zip,以流式下载

/native 仅在 Electron 桌面外壳下才有意义,在那里 ctx.desktop.pickFile / pickDirectory 会给出真实的 OS 路径;在浏览器中,请改用 /upload/upload-directory。工作目录请求在桌面端授予原生目录 read-write 权限,在 Web 端创建可写的上传工作副本。请求的 access 必须与插件实际持有的权限一致。

一个需要某项权限但插件未声明该权限的请求会返回 403。参见 常见陷阱

AI 聊天会为每个兼容的后端插件分别创建作用域隔离的文件/目录授权。文件是只读输入;所选目录仅对同时声明 files.readfiles.write 的插件授予 read-write,只读插件获得隔离快照。在最新一条用户消息中直接输入的本机绝对路径也走相同流程,但始终只读。FileRef 仅在聊天会话中有效(不会持久化;重启后清除)。

临时存储与清理

上传与输出目录位于按插件划分的临时根目录下:

${java.io.tmpdir}/fengyu/runtime-files/<pluginId>/<uuid>/{in|out}/...
  • 宿主把每一次上传快照进一个全新的 <uuid>/in/ 目录树。上传树内的符号链接会被拒绝,越过快照根的路径穿越会被阻止。
  • POST /output 分配一个全新的 <uuid>/out/ 目录。
  • 授权仅保存在内存中——它们在宿主重启后不会留存。

清理

  • 整个 runtime-files 目录树会在宿主关停时由一个 @PreDestroy 钩子删除。
  • 没有定时清扫。 一个长时间运行的宿主会持续累积被授权的文件,直到进程退出。不要指望单个文件在会话中途被回收。

UI 如何使用

FengYuClient.files.* 辅助方法封装了这些 endpoint,因此 UI 永远不会自己拼装 multipart:

js
const file   = await fengyu.files.open({ extensions: ['xlsx'] })      // → 底层是 POST /upload
const inDir  = await fengyu.files.inputDirectory()                    // → POST /upload-directory
const project = await fengyu.files.workspaceDirectory()               // → 可写的所选项目/工作副本
const outDir = await fengyu.files.outputDirectory()                   // → POST /output  (需要 files.write)
await fengyu.files.export(outDir)                                     // → GET  /export/{ref}

workspaceDirectory() 需要 files.write;桌面端返回用户选择的原生目录,浏览器端返回可写的上传工作副本。请把每个 FileRef 直接传进 RPC,不要只提取其 id,这样宿主才能识别并改写完整的 {id, kind, access} 对象:

js
import { createPluginRpc } from './generated/fengyu-rpc'
const rpc = createPluginRpc(fengyu)

const analysis = await rpc.analyze({ filePath: file })
await rpc.excelExecute({ outputDir: outDir, filePrefix: 'q3-' })

此流程的端到端讲解见 官方插件——Excel

下一步

Released under the GPL-3.0 License.