FastGPT 反向调用(Invoke)机制设计解析:插件安全访问宿主能力的令牌与权限体系
【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT
FastGPT 将插件(FastGPT-Plugin / 系统工具)与宿主主服务解耦为独立的微服务运行时,而插件在运行过程中往往需要"反向"访问宿主的文件存储、模型调用、知识库检索与用户上下文等能力。这篇技术指南以 FastGPT 仓库中的 反向调用处理器设计文档 为核心,结合 invoke.ts 源码与对应测试,完整剖析 InvokeToken 的签发、权限授权、接口实现与安全边界,帮助你理解并在插件开发中正确使用这套"插件调宿主"的授权机制。
一、反向调用是什么:解决的问题与适用场景
在 FastGPT 的插件体系中,常规调用方向是"FastGPT 主服务 → FastGPT Plugin 服务 → 插件代码":主服务通过插件运行时接口把工具调用转发到插件,插件执行完再把结果返回(见 system-tool-development.mdx 对插件运行模型的描述)。
但插件在执行过程中经常需要反过来使用宿主能力,例如:
- 把生成的文件上传到当前对话的文件目录,供用户在聊天界面中下载;
- 读取当前运行上下文的用户信息(账号、组织、群组),实现个性化逻辑;
- 获取当前团队的企微企业访问凭证等企业级集成能力。
反向调用(Invoke)正是为此设计的:FastGPT 主服务向插件暴露一组受保护的 HTTP 接口,插件携带一张短期、限权的InvokeToken来调用它们,从而实现"插件声明权限 → 用户安装授权 → 运行时按权限放行"的安全闭环。设计文档在 packages/service/support/invoke/DESIGN.md 中把这条链路概括为四步。
二、整体流程:从权限声明到结果返回的四步闭环
原设计文档给出了反向调用的核心流程骨架,本文结合源码将其展开为完整的时序链路:
1. 插件声明权限
插件开发者在自己插件的元信息中声明需要哪些宿主能力(如file-upload:allow表示允许调用宿主文件上传、userInfo:read表示允许读取用户信息)。该权限清单会随插件进入安装解析流程,并在插件市场/团队安装页面展示给用户确认——确认安装即视为授权。
从源码看,权限清单的数据模型贯穿全链路:
- packages/global/sdk/fastgpt-plugin.ts 从
@fastgpt-plugin/sdk-client导出PluginPermissionEnum与PluginPermissionEnumSchema,并组合出PluginPermissionListSchema(即权限字符串数组 schema); - 团队工具详情中通过
confirmedPermissions字段记录"安装时确认过的权限清单"(见 packages/global/openapi/core/plugin/team/tool/api.ts); - 系统工具详情 schema 同样带有
permissions: z.array(PluginPermissionEnumSchema).optional()字段(见 packages/global/core/app/tool/systemTool/type/base.ts),该字段会在运行时被填入签发的令牌载荷中。
2. FastGPT 签发 InvokeToken
当工作流/Agent 真正执行某个插件工具时,主服务为该次调用构建一个"会话上下文",并签发一张 JWT 格式的 InvokeToken,随插件运行时的systemVar注入到插件进程。令牌载荷中携带:应用 ID、对话 ID、用户 ID、团队 ID、成员 ID 以及该工具被授权的权限清单。
设计文档中标注的过期时间为 30 分钟,而当前源码中的实际常量为INVOKE_TOKEN_EXPIRES_IN = 60 * 60(1 小时,见 invoke.ts)——部署或阅读旧版文档时需注意这一差异,签发细节以仓库代码为准。
3. 插件发送 invoke 请求并携带 token
插件在运行时通过 SDK 提供的ctx.invoke.uploadFile()等封装发起请求(见 system-tool-development.mdx),HTTP 请求头采用标准的Authorization: Bearer <invokeToken>格式(schema 定义见 packages/global/openapi/plugin/invoke.ts)。
4. FastGPT 校验令牌与权限,返回结果
FastGPT 的 invoke API 端点首先从请求头解析 token,用InvokeProcessor.getInstanceFromToken(token)完成 JWT 验签与载荷解析,再在具体处理逻辑中通过assertPermission()校验所需权限,通过后执行宿主能力并返回结果。
三、InvokeToken 的数据结构与校验实现
3.1 会话载荷(Session)
令牌的 payload 不是任意自定义对象,而是经过 zod schema 强校验的会话结构。定义见 packages/service/support/invoke/type.ts:
export const InvokeSessionSchema = z.object({ appId: z.string().nonempty(), chatId: z.string().nonempty(), uId: z.string().nonempty(), teamId: z.string().nonempty(), tmbId: z.string().nonempty(), permissions: PluginPermissionListSchema.default([]) });各字段含义:
| 字段 | 含义 |
|---|---|
appId | 当前应用(App)ID,文件上传时作为 S3 对象归属的 sourceId |
chatId | 当前对话 ID,决定文件上传到哪个对话目录 |
uId | 用户标识,用于文件归属与权限隔离 |
teamId | 团队 ID,用户信息查询时定位团队与组织 |
tmbId | 团队成员 ID,用于解析具体成员信息 |
permissions | 该令牌被授予的权限清单,缺省为空数组(即默认无任何能力) |
3.2 签发与验签:JWT + 服务端密钥
InvokeProcessor类封装了令牌的全生命周期(invoke.ts):
public generateToken(): string { const session = InvokeSessionSchema.parse(this._session); return jwt.sign(session, InvokeProcessor.jwtSecret, { expiresIn: INVOKE_TOKEN_EXPIRES_IN }); } static getInstanceFromToken(token?: string): InvokeProcessor { if (!token) throw ERROR_ENUM.unAuthorization; try { const payload = jwt.verify(token, this.jwtSecret); const session = InvokeSessionSchema.parse(payload); return new InvokeProcessor(session); } catch (error) { throw ERROR_ENUM.unAuthorization; } }关键设计点:
- 无状态校验:服务端不维护令牌会话,任何持有令牌的进程都可在过期前凭
jwt.verify通过验签,适合插件运行时与主服务分离的微服务架构; - 载荷二次校验:验签通过后还会用
InvokeSessionSchema重新解析载荷,防止签名有效但结构被篡改或缺失字段的情况; - 统一失败语义:token 缺失、签名错误、过期、载荷非法统一抛
unAuthorization错误,避免向插件泄露过多内部信息; - 服务端密钥:签名密钥来自环境变量
INVOKE_TOKEN_SECRET,启动校验要求长度至少 32 字符(见 packages/service/env.ts)。
3.3 权限断言:最小授权执行
每个宿主能力在执行前都会先断言权限:
private assertPermission(permission: PluginPermissionEnumType) { const { permissions } = InvokeSessionSchema.parse(this._session); if (!permissions.includes(permission)) { throw ERROR_ENUM.unAuthorization; } }也就是说,令牌上没写的能力,即便接口被直接调用也会被拒绝。这是"确认安装即授权、运行时按授权执行"原则在代码层的落地。
四、反向调用接口详解
4.1 文件上传:POST /api/invoke/fileUpload
能力说明:插件通过该接口把文件上传到当前对话的文件目录,并返回可访问 URL 与 S3 对象 key。对应的 OpenAPI 契约(请求/响应 schema)定义在 packages/global/openapi/plugin/invoke.ts,HTTP 端点在 projects/app/src/pages/api/invoke/fileUpload.ts。
请求要点:
- 方法必须是
POST,且Content-Type必须是multipart/form-data; - 携带
Authorization: Bearer <invokeToken>; - 表单字段:
file(二进制文件)+ 可选的fileName(自定义文件名,不传时使用原始文件名); - 所需权限:
PluginPermissionEnum['file-upload:allow']。
响应结构(经InvokeFileUploadResponseSchema校验后返回):
| 字段 | 说明 |
|---|---|
url | 上传后的文件访问 URL |
key | 私有 S3 对象 key,用于后续持久化等场景 |
filename | 最终文件名 |
contentType | 文件 MIME 类型 |
type | 聊天文件类型(image/audio/video/file) |
4.2 用户信息读取:POST /api/invoke/userInfo
能力说明:插件通过该接口读取当前运行上下文的用户信息,包括账号、联系方式、成员名称、所属组织与群组。契约见 packages/global/openapi/plugin/invoke.ts,端点在 projects/app/src/pages/api/invoke/userInfo.ts。
请求要点:仅需携带Authorization: Bearer <invokeToken>,所需权限为PluginPermissionEnum['userInfo:read']。响应包含:
{ username: string; // 账号 contact?: string | null; // 联系方式 memberName?: string | null;// 成员名称 orgs: { pathId: string; name: string }[]; // 所属组织(含路径 ID) groups: { name: string }[]; // 所属群组 }4.3 企微企业访问凭证:POST /api/invoke/wecom/corpToken
除上述两类外,反向调用体系还覆盖企业微信集成:插件可凭 invoke token 换取当前运行团队的企微企业短期访问凭证(accessToken与expiresIn,见 invoke.ts 契约定义)。这进一步说明反向调用是一组统一的宿主能力开放面,而非单一接口。
五、文件上传的底层实现:S3 存储与文件类型识别
以文件上传为例,深入handleFileUpload(invoke.ts)可以看到宿主能力的具体实现:
const result = await getS3ChatSource().uploadChatFile({ sourceType: ChatSourceTypeEnum.app, sourceId: appId, chatId, uId, filename, body, contentType, expiredTime: addHours(new Date(), 365) }); await removeS3TTL({ key: result.key, bucketName: 'private' });实现要点:
- 归属路径:文件以
app → chat → user三级维度组织,上传到"对话文件目录",与聊天场景的文件生命周期对齐; - TTL 与持久化:上传时先带 365 小时的过期时间,随后调用
removeS3TTL移除临时 TTL,使其成为持久对象——既防止脏数据堆积,又保证对话期间文件长期可访问; - 文件类型判定:优先依据
Content-Type(image/、audio/、video/前缀),其次依据扩展名与 FastGPT 内置的图片/音频/视频扩展名清单匹配,均不命中时归为普通文件(ChatFileTypeEnum.file)。
对应的单测 packages/service/test/support/invoke/invoke.test.ts 验证了两个关键行为:
- 上传成功路径:
uploadChatFile收到正确的sourceType/sourceId/chatId/uId与 365 小时后的过期时间,且随后调用了removeS3TTL移除 TTL,最终返回url/key/filename/contentType/type完整结果; - 失败路径:缺少文件内容时直接抛错,且不会触发上传与 TTL 移除。
六、用户信息读取的底层实现:多源聚合
handleGetUserInfo(invoke.ts)展示了反向调用如何把多个内部数据源聚合成一次对外响应:
- 通过
getUserDetail({ tmbId })获取账号与成员信息; - 通过
getOrgsByTmbId+MongoOrgModel查询组织名称与路径 ID; - 通过
getGroupsByTmbId查询群组,其中默认群组(DefaultGroupName)会显示为团队名,提升可读性; - 通过
MongoTeam查询团队名称。
对外暴露的是一个精简、可序列化的用户上下文快照,插件无需理解 FastGPT 内部的成员/组织/群组数据模型。
七、令牌在运行时的注入:插件如何拿到 InvokeToken
在 FastGPT 主服务侧,InvokeToken 由工具调用链路统一签发。以工作流中的工具节点为例,packages/service/core/workflow/dispatch/child/runTool.ts 中:
const invokeToken = appId ? new InvokeProcessor({ appId, chatId, uId, teamId: String(runningUserInfo.teamId), tmbId: String(runningUserInfo.tmbId), permissions: tool.permissions ?? [] }).generateToken() : undefined;随后通过systemVar.invokeToken注入插件运行时(Agent 工具的对应实现见 packages/service/core/workflow/dispatch/ai/agent/sub/tool/index.ts)。两个关键细节:
- 权限直接取自工具配置:
permissions: tool.permissions ?? []说明令牌权限来自该工具被授权时记录的权限清单,与"确认安装即授权"的流程闭环对应; - 无 appId 时不签发:当工具不在具体应用上下文中运行时(如纯调试场景),
invokeToken为空字符串,插件自然无法反向调用宿主能力,实现了按场景收紧权限。
插件侧则通过@fastgpt-plugin/sdk-factory提供的ctx.invoke.uploadFile()等封装发起请求(见 system-tool-development.mdx),本地 debug 时该调用使用虚拟实现、默认输出到.fastgpt-plugin-debug/uploads(见 system-tool-development.mdx),方便开发者脱离宿主环境独立调试。
八、安全边界与部署要求
综合设计文档、源码与测试,反向调用机制的安全边界可归纳为以下几点:
- 显式授权:权限必须在安装时声明、展示并确认(
confirmedPermissions记录),运行时拒绝一切未授权能力; - 短期令牌:JWT 自带过期时间(当前源码为 1 小时),令牌泄漏的暴露窗口有限;令牌随每次工具调用重新签发,无需跨调用持久化;
- 强制服务端密钥:
INVOKE_TOKEN_SECRET必须显式配置且至少 32 字符,生产环境缺失该变量会导致服务环境初始化失败(见 packages/service/env.ts 与 packages/service/test/env.test.ts 中的校验用例);仅 vitest 测试环境提供固定测试默认值(见 packages/service/env.util.ts); - 请求头标准鉴权:使用
Authorization: Bearer传递令牌,避免 token 出现在 URL 查询参数中造成日志泄漏; - 校验失败统一拒绝:验签、结构校验、权限断言任一环节失败均抛
unAuthorization,不给攻击者探测空间。
九、总结与源码索引
反向调用是 FastGPT 插件安全模型的核心一环:以"权限声明 → 安装授权 → 令牌签发 → 运行时断言"的闭环,让脱离主进程运行的插件也能安全、可控地使用宿主能力。设计与实现集中在以下文件,可继续深入阅读:
- 设计文档:packages/service/support/invoke/DESIGN.md
- 核心处理器:packages/service/support/invoke/invoke.ts
- 会话/上传 Schema:packages/service/support/invoke/type.ts
- OpenAPI 契约(含 wecom 凭证):packages/global/openapi/plugin/invoke.ts
- HTTP 端点:fileUpload.ts、userInfo.ts
- 权限枚举与 SDK 导出:packages/global/sdk/fastgpt-plugin.ts
- 运行时令牌签发:runTool.ts、agent/tool/index.ts
- 测试用例:packages/service/test/support/invoke/invoke.test.ts、packages/service/test/env.test.ts
对于插件开发者,实践要点是:在插件元信息中准确声明所需权限、在插件代码中统一通过ctx.invoke.uploadFile()等 SDK 封装访问宿主能力、部署 FastGPT 时务必配置强随机且不小于 32 字符的INVOKE_TOKEN_SECRET,即可安全地使用这套反向调用能力。
【免费下载链接】FastGPTFastGPT is a knowledge-based platform built on the LLMs, offers a comprehensive suite of out-of-the-box capabilities such as data processing, RAG retrieval, and visual AI workflow orchestration, letting you easily develop and deploy complex question-answering systems without the need for extensive setup or configuration.项目地址: https://gitcode.com/GitHub_Trending/fa/FastGPT
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考