|
| 1 | +# Workflow 输入文件上下文统一设计 |
| 2 | + |
| 3 | +## 1. 背景 |
| 4 | + |
| 5 | +Workflow 输入文件目前以 URL 字符串贯穿 AI Chat、ToolCall、AgentV2、ReadFiles 和 |
| 6 | +Sandbox。URL 同时承担模型访问地址、服务端下载地址、文件类型推断和去重标识,导致同源 |
| 7 | +绝对 URL、相对下载短链和外部 URL 容易被错误转换。 |
| 8 | + |
| 9 | +尤其需要避免以下行为: |
| 10 | + |
| 11 | +- 根据 `requestOrigin`、`FE_DOMAIN` 或 URL 前缀推断文件权限; |
| 12 | +- 将用户输入的相对 URL 交给内部 Axios; |
| 13 | +- 让模型或节点通过新 URL 扩大本轮 Workflow 可读取的文件集合; |
| 14 | +- 使用模型 URL 作为私有对象的服务端读取来源。 |
| 15 | + |
| 16 | +本期引入请求级、只读的 `WorkflowFileContext`。它在 `dispatchWorkFlow` 入口统一处理当前 |
| 17 | +query 和 histories 已携带的文件,下游消费者只能解析入口已登记的文件。 |
| 18 | + |
| 19 | +## 2. 本期范围 |
| 20 | + |
| 21 | +### 2.1 包含 |
| 22 | + |
| 23 | +- 当前 query 中的文件; |
| 24 | +- histories 中 Human 消息携带的文件; |
| 25 | +- AI Chat、ToolCall、AgentV2、ReadFiles 和 Sandbox 对上述文件的消费; |
| 26 | +- 私有 chat object key 的归属校验; |
| 27 | +- 外部绝对 HTTP(S) URL 的 SSRF 防护读取; |
| 28 | +- 请求级文件数量和单文件大小限制。 |
| 29 | + |
| 30 | +### 2.2 不包含 |
| 31 | + |
| 32 | +- Workflow 运行期间由普通节点、Plugin、Interactive 或 Dataset Search 新产生的文件; |
| 33 | +- Workflow 文件变量的存储结构迁移; |
| 34 | +- 跨请求持久化 `WorkflowFileRef`; |
| 35 | +- 修改聊天数据库中的现有文件格式; |
| 36 | +- 通过 signed alias 反推业务归属; |
| 37 | +- Context clone、分支注册和合并。 |
| 38 | + |
| 39 | +## 3. 核心原则 |
| 40 | + |
| 41 | +### 3.1 key 是私有文件的可信来源 |
| 42 | + |
| 43 | +私有文件必须携带 `key`。Workflow 入口使用 `parseChatFileS3Key` 或 |
| 44 | +`isAuthorizedChatFileS3Key` 校验 key 是否属于根 Workflow: |
| 45 | + |
| 46 | +```text |
| 47 | +key |
| 48 | + -> sourceType |
| 49 | + -> sourceId |
| 50 | + -> uid |
| 51 | + -> chatId |
| 52 | + -> 与根 Workflow 鉴权上下文逐项匹配 |
| 53 | +``` |
| 54 | + |
| 55 | +匹配使用根 `dispatchWorkFlow` 的 `runningAppInfo`、`uid` 和 `chatId`。Child Workflow 不得 |
| 56 | +使用 child app 的身份重新解释父 Workflow 输入文件。 |
| 57 | + |
| 58 | +归属匹配失败时直接拒绝。不能把失败的私有 key 降级成外部 URL。 |
| 59 | + |
| 60 | +### 3.2 signed URL 只用于模型访问 |
| 61 | + |
| 62 | +signed URL 不参与权限判断。私有 key 校验通过后,每个 key 在一次 Workflow 中最多签发 |
| 63 | +一次两小时有效的绝对 HTTP(S) URL,并在请求内复用。 |
| 64 | + |
| 65 | +如果文件域名配置无法生成绝对 HTTP(S) URL,入口应报配置错误,不能生成相对 URL。 |
| 66 | + |
| 67 | +### 3.3 服务端读取与模型 URL 分离 |
| 68 | + |
| 69 | +- 私有 chat object:FastGPT 服务端通过 S3 client 直接读取; |
| 70 | +- 外部 HTTP(S) URL:FastGPT 服务端通过带 SSRF 防护的 Axios 读取; |
| 71 | +- 模型只接收 `modelUrl`; |
| 72 | +- 相对 URL、protocol-relative URL 和非 HTTP(S) 协议一律不能进入读取链路。 |
| 73 | + |
| 74 | +### 3.4 Context 只读 |
| 75 | + |
| 76 | +只有 Workflow 入口可以登记文件。节点、模型工具参数和下游消费者只能查询已登记 Ref, |
| 77 | +不能在查询失败时自动注册新外链。 |
| 78 | + |
| 79 | +## 4. 数据模型 |
| 80 | + |
| 81 | +```ts |
| 82 | +export type WorkflowFileSource = |
| 83 | + | { |
| 84 | + type: 'chatObject'; |
| 85 | + objectKey: string; |
| 86 | + } |
| 87 | + | { |
| 88 | + type: 'externalHttp'; |
| 89 | + url: string; |
| 90 | + }; |
| 91 | + |
| 92 | +export type WorkflowFileRef = { |
| 93 | + id: string; |
| 94 | + name: string; |
| 95 | + type: ChatFileTypeEnum; |
| 96 | + modelUrl: string; |
| 97 | + source: WorkflowFileSource; |
| 98 | +}; |
| 99 | + |
| 100 | +export type WorkflowFileLimits = { |
| 101 | + maxFiles: number; |
| 102 | + maxBytesPerFile: number; |
| 103 | +}; |
| 104 | + |
| 105 | +export type WorkflowFileContext = { |
| 106 | + resolve: (urlOrId: string) => WorkflowFileRef | undefined; |
| 107 | + resolveChatFile: (url: string) => UserChatItemFileItemType | undefined; |
| 108 | + getIdentity: (urlOrId: string) => string | undefined; |
| 109 | + read: (target: string | WorkflowFileRef) => Promise<WorkflowFileReadResult>; |
| 110 | + limits: WorkflowFileLimits; |
| 111 | +}; |
| 112 | +``` |
| 113 | + |
| 114 | +Context 不暴露 `register`、`clone` 或文件枚举接口。内部至少维护: |
| 115 | + |
| 116 | +- `byId`:服务端生成 file id 到 Ref; |
| 117 | +- `byRuntimeUrl`:本轮 `modelUrl` 到 Ref; |
| 118 | +- `byIdentity`:私有 key 或完整外部 URL 到 Ref,用于去重。 |
| 119 | + |
| 120 | +## 5. Workflow 入口 |
| 121 | + |
| 122 | +入口处理顺序: |
| 123 | + |
| 124 | +```text |
| 125 | +完成 Workflow/Chat 鉴权 |
| 126 | + -> 计算根 Workflow 文件 scope |
| 127 | + -> 计算 maxFiles/maxBytesPerFile |
| 128 | + -> 扫描 query + histories 文件 |
| 129 | + -> 校验 chat object key 归属 |
| 130 | + -> 校验外链必须为绝对 HTTP(S) |
| 131 | + -> 对私有 key 按请求缓存签发两小时 modelUrl |
| 132 | + -> 按 key/完整外链 URL 去重并建立只读 Context |
| 133 | + -> 将 FileContext 挂载到 WorkflowContext |
| 134 | + -> 开始节点调度 |
| 135 | +``` |
| 136 | + |
| 137 | +### 5.1 Query |
| 138 | + |
| 139 | +- 带 key:完整校验 `sourceType/sourceId/uid/chatId`,失败则拒绝本轮 Workflow; |
| 140 | +- 无 key:只允许绝对 HTTP(S) URL,并继续应用 `fileUrlWhitelist`; |
| 141 | +- 其他 URL:拒绝本轮 Workflow。 |
| 142 | + |
| 143 | +### 5.2 Histories |
| 144 | + |
| 145 | +- 带 key:使用根 Workflow scope 校验并签发 URL; |
| 146 | +- 无 key的绝对 HTTP(S) URL:登记为外链; |
| 147 | +- 无 key的相对或非法 URL:不登记,使用该文件时返回不可用,不能交给内部 Axios; |
| 148 | +- 无关历史文件不可用时,不能阻塞整轮 Workflow。 |
| 149 | + |
| 150 | +入口只登记来源和签发 URL,不下载所有历史文件正文。正文继续按消费者需求懒加载。 |
| 151 | + |
| 152 | +## 6. 读取规则 |
| 153 | + |
| 154 | +### 6.1 私有对象 |
| 155 | + |
| 156 | +`WorkflowFileContext.read` 根据 Ref 中已校验的 bucket/objectKey 使用 S3 client: |
| 157 | + |
| 158 | +1. 先读取 metadata; |
| 159 | +2. `contentLength` 超过 `maxBytesPerFile` 时拒绝; |
| 160 | +3. 下载流再次按累计字节数限制; |
| 161 | +4. 返回 Buffer、filename 和 contentType。 |
| 162 | + |
| 163 | +### 6.2 外部 URL |
| 164 | + |
| 165 | +外部 URL 始终使用带 SSRF 防护的 Axios: |
| 166 | + |
| 167 | +- 只允许 `http:` 和 `https:`; |
| 168 | +- 保留 `fileUrlWhitelist`; |
| 169 | +- SSRF Axios 负责 DNS、metadata 地址和逐跳重定向检查; |
| 170 | +- 先检查 `Content-Length`,流式下载时再次限制累计字节数; |
| 171 | +- 禁止使用 `pickOutboundAxios` 或任何内部相对路径 Axios。 |
| 172 | + |
| 173 | +### 6.3 大小限制 |
| 174 | + |
| 175 | +`dispatchWorkFlow` 接受可选的 `maxFileSize`(字节)。调用入口可以根据团队套餐和系统限制 |
| 176 | +计算后传入;本期未传入时固定使用 2GB。 |
| 177 | + |
| 178 | +该限制约束 FastGPT 服务端读取。模型提供商直接拉取外部 `modelUrl` 时,不经过 FastGPT |
| 179 | +下载链路,因此不计入服务端读取限制。 |
| 180 | + |
| 181 | +## 7. 消费者 |
| 182 | + |
| 183 | +### 7.1 AI Chat |
| 184 | + |
| 185 | +- 普通文档 URL 必须先命中 Context,再由统一读取器获取正文; |
| 186 | +- 图片、音频、视频默认使用 `modelUrl`; |
| 187 | +- LLM 层不感知 Workflow Context;`shouldLoadMediaAsBase64` 为真时统一使用 AI 层已有的 |
| 188 | + 下载与 Base64 转换能力,否则把绝对 `modelUrl` 直接交给模型。 |
| 189 | + |
| 190 | +### 7.2 ToolCall 和 AgentV2 read_file |
| 191 | + |
| 192 | +- 模型只提交 file id; |
| 193 | +- file id 必须命中节点已知文件映射,并最终命中 WorkflowFileContext; |
| 194 | +- 未知 id/URL 返回文件不可用,不能注册新外链; |
| 195 | +- 文档解析继续复用 `core/chat/fileContext.ts`。 |
| 196 | + |
| 197 | +### 7.3 ReadFiles |
| 198 | + |
| 199 | +ReadFiles 的字符串 URL 输入保持兼容,但只有命中 WorkflowFileContext 的 URL 才能读取。 |
| 200 | + |
| 201 | +### 7.4 Sandbox |
| 202 | + |
| 203 | +Workflow 显式向 Sandbox 文件注入步骤传入读取函数。该函数通过 WorkflowFileContext 读取 |
| 204 | +Buffer 后写入 Sandbox;Sandbox 通用模块不引用 Workflow,也不使用 `requestOrigin`、相对 URL |
| 205 | +或内部 Axios。没有注入读取函数的普通外链仍走带 SSRF 防护的 Axios。 |
| 206 | + |
| 207 | +### 7.5 Child、Loop 和 Parallel |
| 208 | + |
| 209 | +本期 Context 创建后保持只读,因此根 Workflow、Child、Loop 和 Parallel 共享同一请求级 |
| 210 | +Context,不需要 clone 或 merge。下游只拿实际传入的 URL/file id 查询,Context 不提供枚举。 |
| 211 | + |
| 212 | +## 8. 兼容策略 |
| 213 | + |
| 214 | +- 文件类型、名称和 key 等运行态元数据统一由 WorkflowFileContext 提供,不再维护额外的 |
| 215 | + query URL Map; |
| 216 | +- WorkflowFileContext 复用 Workflow 自己的 AsyncLocalStorage,并由 Workflow 调用方显式传给 |
| 217 | + Chat/Sandbox;`core/ai` 和 `core/chat` 不得反向引用 `core/workflow`; |
| 218 | +- 字符串 URL 运行值继续保留; |
| 219 | +- keyless 历史绝对 URL 按外链处理; |
| 220 | +- keyless 历史相对 URL 不再通过内部 Axios 读取; |
| 221 | +- AgentV2 的 `normalizeAgentServerFileUrl` 和 `internalUrl` 过渡实现应删除; |
| 222 | +- Workflow 以外的普通聊天文件链路暂不纳入本期 Context,但共享读取函数不能再接受相对 URL。 |
| 223 | + |
| 224 | +## 9. 测试要求 |
| 225 | + |
| 226 | +### 9.1 Context |
| 227 | + |
| 228 | +- query/history 私有 key 归属校验成功; |
| 229 | +- sourceType/sourceId/uid/chatId 任一不匹配时拒绝; |
| 230 | +- 同一个 key 在 query/history 中只签发一次并只创建一个 Ref; |
| 231 | +- 外部绝对 HTTP(S) URL 可登记; |
| 232 | +- 相对 URL、protocol-relative、`file:`、`data:`、`ws:` 被拒绝或跳过; |
| 233 | +- `fileUrlWhitelist` 继续生效; |
| 234 | +- `resolve` 不会自动登记未知绝对 URL。 |
| 235 | + |
| 236 | +### 9.2 读取 |
| 237 | + |
| 238 | +- 私有对象通过 S3 client 读取; |
| 239 | +- 外链通过 SSRF Axios 读取; |
| 240 | +- 私有对象和外链均执行 header/metadata 与流式双重大小限制; |
| 241 | +- 私有对象读取不调用 HTTP; |
| 242 | +- 相对 URL 不进入任何 Axios。 |
| 243 | + |
| 244 | +### 9.3 消费者 |
| 245 | + |
| 246 | +- AI Chat、ToolCall、AgentV2、ReadFiles 使用同一 Context; |
| 247 | +- Agent history 和当前输入都保留绝对 modelUrl; |
| 248 | +- read_file 未知 id 无法读取; |
| 249 | +- Sandbox 注入通过 Context Buffer; |
| 250 | +- 不可用的无关 history 文件不阻塞本轮请求。 |
| 251 | + |
| 252 | +## 10. TODO |
| 253 | + |
| 254 | +- [x] 新增只读 `WorkflowFileContext` 和入口准备函数; |
| 255 | +- [x] 为 `dispatchWorkFlow` 增加 `maxFileSize`,创建并挂载 Context; |
| 256 | +- [x] `parseUrlToFileType` 优先读取 Context 文件元数据; |
| 257 | +- [x] 统一 `core/chat/fileContext.ts` 的 Workflow 文件读取; |
| 258 | +- [x] 保持 AI Chat 多模态 Base64 与 Workflow Context 解耦; |
| 259 | +- [x] 迁移 ToolCall、AgentV2 和 ReadFiles; |
| 260 | +- [x] 迁移 Sandbox 文件注入; |
| 261 | +- [x] 删除 AgentV2 `requestOrigin/internalUrl` 过渡逻辑; |
| 262 | +- [x] 补齐 Context、读取器和消费者测试; |
| 263 | +- [x] 运行全量测试;Admin 既有批处理性能用例在全量并发下超时,隔离复跑 31/31 通过。 |
| 264 | + |
| 265 | +## 11. 相关代码 |
| 266 | + |
| 267 | +- `packages/service/core/workflow/utils/context.ts` |
| 268 | +- `packages/service/core/workflow/utils/fileContext.ts` |
| 269 | +- `packages/service/core/workflow/dispatch/index.ts` |
| 270 | +- `packages/service/core/chat/fileContext.ts` |
| 271 | +- `packages/service/core/workflow/dispatch/ai/chat/chatMessages.ts` |
| 272 | +- `packages/service/core/workflow/dispatch/ai/toolcall/tools/file.ts` |
| 273 | +- `packages/service/core/workflow/dispatch/ai/agent/adapter/userContext.ts` |
| 274 | +- `packages/service/core/workflow/dispatch/ai/agent/sub/file/index.ts` |
| 275 | +- `packages/service/core/ai/sandbox/application/file.ts` |
| 276 | +- `packages/service/core/workflow/dispatch/tools/readFiles.ts` |
| 277 | +- `packages/service/common/api/axios.ts` |
| 278 | +- `packages/service/common/s3/sources/chat/key.ts` |
0 commit comments