feat: sandbox readfile tool (#6679)

* feat: sandbox readfile tool

* perf: read stream

* fix: schema name

* update sdk version

* udpate enum

* perf: time
This commit is contained in:
Archer
2026-03-31 13:50:26 +08:00
committed by GitHub
parent 3f4400a500
commit b884631363
21 changed files with 796 additions and 197 deletions
@@ -0,0 +1,376 @@
# 虚拟机系统工具:获取文件链接 (sandbox_get_file_url)
## 一、需求概述
为虚拟机(Agent Sandbox)新增一个系统级工具 `sandbox_get_file_url`
1. Agent 调用该工具,传入虚拟机内的文件路径
2. 系统从虚拟机读取文件内容
3. 将文件上传到 S3(使用对话的 Chat Bucket 实例)
4. 返回 2 小时有效期的签名访问 URL
**工具返回格式**
```json
{
"url": "https://xxx.s3.amazonaws.com/...",
"expired": "2 hours",
"filename": "output.csv"
}
```
---
## 二、涉及文件
| 文件 | 变更类型 | 说明 |
|------|----------|------|
| `packages/global/core/ai/sandbox/constants.ts` | 修改 | 新增工具定义、Schema、更新系统提示词 |
| `packages/service/core/ai/sandbox/toolCall.ts` | 新增 | 沙盒工具统一执行层 `callSandboxTool()`,两条链路共享 |
| `packages/service/core/workflow/dispatch/ai/tool/toolCall.ts` | 修改 | 统一拦截所有 sandbox 工具,复用 `callSandboxTool` |
| `packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts` | 修改 | 新增 `dispatchSandboxGetFileUrl()`,复用 `callSandboxTool` |
| `packages/service/core/workflow/dispatch/ai/agent/master/call.ts` | 修改 | 新增工具拦截逻辑(Agent 模式) |
| `packages/service/common/s3/buckets/base.ts` | 修改 | `uploadFileByBody` 新增 `filename``expiredTime` 参数 |
| `packages/service/common/s3/sources/chat/index.ts` | 修改 | `uploadChatFile` 透传 `filename``expiredTime` |
| `packages/service/common/s3/type.ts` | 修改 | `UploadFileByBodySchema` 新增 `filename``expiredTime` 字段 |
| `packages/service/common/s3/sources/chat/type.ts` | 修改 | `UploadChatFileSchema` 新增 `expiredTime` 字段 |
| `projects/app/src/pages/api/system/file/[jwt].ts` | 修改 | 下载接口支持 `Content-Disposition` 响应头 |
> **架构说明**:新增 `callSandboxTool` 作为纯执行层,不绑定业务响应格式。两条调用链路(普通工作流 / Agent 模式)均复用该层,消除重复逻辑。
> **URL 生成**:文件上传到 S3 后,通过 `jwtSignS3ObjectKey(key, expiredAt)` 生成 JWT 签名的内网访问 URL(有效期 2 小时),不依赖 S3 签名 URL。
---
## 三、详细设计
### 3.1 工具定义(constants.ts
新增常量与 Schema
```typescript
export const SANDBOX_READ_FILE_TOOL_NAME: I18nStringType = {
'zh-CN': '虚拟机/获取文件链接',
'zh-Hant': '虛擬機/獲取文件鏈接',
en: 'Sandbox/Get File URL'
};
export const SANDBOX_GET_FILE_URL_TOOL_NAME = 'sandbox_get_file_url';
// 参数为 paths 数组,支持同时获取多个文件
export const SandboxGetFileUrlToolSchema = z.object({
paths: z.array(z.string())
});
export const SANDBOX_GET_FILE_URL_TOOL: ChatCompletionTool = {
type: 'function',
function: {
name: SANDBOX_GET_FILE_URL_TOOL_NAME,
description: '从虚拟机读取指定文件,上传至云存储,返回 2 小时有效期的访问链接',
parameters: {
type: 'object',
properties: {
paths: {
type: 'array',
items: {
type: 'string',
description: '文件访问路径,例如: output.csv'
},
description: '文件访问路径,例如: ["output.csv", "output.txt"]'
}
},
required: ['paths']
}
}
};
// 更新 SANDBOX_TOOLS(追加新工具)
export const SANDBOX_TOOLS: ChatCompletionTool[] = [SANDBOX_SHELL_TOOL, SANDBOX_GET_FILE_URL_TOOL];
```
更新系统提示词(`SANDBOX_SYSTEM_PROMPT`),追加工具说明:
```
- 若需要将生成的文件分享给用户,可使用 ${SANDBOX_GET_FILE_URL_TOOL_NAME} 工具获取文件的临时访问链接(有效期 2 小时)
```
---
### 3.2 沙盒工具统一执行层(toolCall.ts
新增 `packages/service/core/ai/sandbox/toolCall.ts`,作为所有沙盒工具的纯执行层,不绑定业务响应格式:
```typescript
type SandboxToolCallParams = {
toolName: string;
rawArgs: string;
appId: string;
userId: string;
chatId: string;
};
export type SandboxToolCallResult = {
input: Record<string, any>;
response: string;
durationSeconds: number;
};
export const callSandboxTool = async (params: SandboxToolCallParams): Promise<SandboxToolCallResult>
```
**`sandbox_get_file_url` 执行逻辑**
1. 解析 `paths` 数组参数
2.`Promise.all` 并发处理每个文件路径:
- 通过 `instance.provider.readFileStream(filePath)` 流式读取文件内容
- 聚合 chunks 为 `Buffer`
- 调用 `chatBucket.uploadChatFile({ ..., expiredTime: addHours(now, 2) })` 上传,TTL 设为 2 小时
-`jwtSignS3ObjectKey(key, addHours(now, 2))` 生成 JWT 签名访问 URL
3. 返回 `Array<{ fileUrl: string, filename: string }>`
**工具返回格式**JSON 序列化后作为 response):
```json
[
{ "fileUrl": "https://app.xxx.com/api/system/file/eyJ...", "filename": "output.csv" },
{ "fileUrl": "https://app.xxx.com/api/system/file/eyJ...", "filename": "report.txt" }
]
```
> **注意**URL 不再使用 S3 签名 URL`accessUrl`),而是通过 `jwtSignS3ObjectKey` 生成 JWT 签名的内部访问 URL,由 `/api/system/file/[jwt]` 接口代理下载。
---
### 3.3 S3 上传(uploadFileByBody 扩展)
扩展 `packages/service/common/s3/type.ts``UploadFileByBodySchema`,新增 `filename`(必填)和 `expiredTime`(可选)字段:
```typescript
export const UploadFileByBodySchema = z.object({
buffer: z.instanceof(Buffer),
contentType: z.string().optional(),
key: z.string().nonempty(),
filename: z.string().nonempty(), // 新增,用于 Content-Disposition
expiredTime: z.date().optional() // 新增,不传则默认 now + 1h
});
```
`uploadFileByBody` 使用 `expiredTime` 写入 MongoS3TTL,并将 `filename` 写入对象 metadata
```typescript
metadata: {
contentDisposition: `attachment; filename="${encodeURIComponent(filename)}"`,
originFilename: encodeURIComponent(filename),
uploadTime: new Date().toISOString()
}
```
`uploadChatFile` 同步透传 `filename``expiredTime` 到底层方法。
---
### 3.4 封装方法(sub/sandbox/index.ts
`dispatchSandboxGetFileUrl` 直接复用 `callSandboxTool`,与 `dispatchSandboxShell` 模式一致:
```typescript
export const dispatchSandboxGetFileUrl = async ({
filePath, // 保持参数名,内部转为 paths: [filePath]
appId,
userId,
chatId,
lang
}: SandboxDispatchParams & { filePath: string }): Promise<SandboxDispatchResult> => {
const { input, response, durationSeconds } = await callSandboxTool({
toolName: SANDBOX_GET_FILE_URL_TOOL_NAME,
rawArgs: JSON.stringify({ paths: [filePath] }),
appId,
userId,
chatId
});
return {
response,
usages: [],
nodeResponse: buildNodeResponse({ toolId: SANDBOX_GET_FILE_URL_TOOL_NAME, input, response, durationSeconds, lang })
};
};
```
---
### 3.5 工具拦截逻辑(两条链路)
两条链路均通过 `callSandboxTool` 统一处理,不再各自内联逻辑。
#### 3.5.1 普通工作流:toolCall.ts
`handleToolResponse` 中合并 `SANDBOX_TOOL_NAME``SANDBOX_GET_FILE_URL_TOOL_NAME` 到同一拦截块:
```typescript
if (
call.function?.name === SANDBOX_TOOL_NAME ||
call.function?.name === SANDBOX_GET_FILE_URL_TOOL_NAME
) {
const { input, response, durationSeconds } = await callSandboxTool({
toolName: call.function.name,
rawArgs: call.function.arguments ?? '',
appId: String(workflowProps.runningAppInfo.id),
userId: String(workflowProps.uid),
chatId: workflowProps.chatId
});
const flowResponse = getSandboxToolWorkflowResponse({
name: tool.name,
logo: SANDBOX_ICON,
toolId: call.function.name,
input,
response,
durationSeconds
});
return { response, flowResponse };
}
```
#### 3.5.2 Agent 模式:agent/master/call.ts
紧跟 `SANDBOX_TOOL_NAME` 拦截块之后,添加对 `SANDBOX_GET_FILE_URL_TOOL_NAME` 的处理,复用 `dispatchSandboxGetFileUrl`
```typescript
if (toolId === SANDBOX_GET_FILE_URL_TOOL_NAME) {
const toolParams = SandboxGetFileUrlToolSchema.safeParse(parseJsonArgs(call.function.arguments));
if (!toolParams.success) return { response: toolParams.error.message, usages: [] };
const result = await dispatchSandboxGetFileUrl({
filePath: toolParams.data.paths[0], // Agent 模式单文件路径
appId: runningAppInfo.id,
userId: props.uid,
chatId,
lang: props.lang
});
childrenResponses.push(result.nodeResponse);
return { response: result.response, usages: result.usages };
}
```
---
## 四、执行流程
### 4.1 普通工作流链路(toolCall.ts
```mermaid
sequenceDiagram
participant Tool as ToolCall 节点
participant Handler as toolCall.ts 拦截
participant CallLayer as callSandboxTool()
participant SandboxProvider as provider.readFileStream()
participant S3 as S3ChatSource.uploadChatFile()
participant DB as MongoS3TTL
Tool->>Handler: 调用 sandbox_get_file_url({ paths })
Handler->>CallLayer: callSandboxTool({ toolName, rawArgs, ... })
loop 每个 path(并发)
CallLayer->>SandboxProvider: readFileStream(filePath)
SandboxProvider-->>CallLayer: AsyncIterable<Uint8Array>
CallLayer->>CallLayer: Buffer.concat(chunks)
CallLayer->>S3: uploadChatFile({ filename, buffer, expiredTime: +2h })
S3->>DB: MongoS3TTL.create(expiredTime = now + 2h)
S3-->>CallLayer: { key }
CallLayer->>CallLayer: jwtSignS3ObjectKey(key, +2h) → fileUrl
end
CallLayer-->>Handler: { input, response: JSON([{fileUrl, filename}...]), durationSeconds }
Handler-->>Tool: response + flowResponse
```
### 4.2 Agent 模式链路(agent/master/call.ts
```mermaid
sequenceDiagram
participant Agent as Agent Master Call
participant Handler as master/call.ts 拦截
participant Dispatch as dispatchSandboxGetFileUrl()
participant CallLayer as callSandboxTool()
participant SandboxProvider as provider.readFileStream()
participant S3 as S3ChatSource.uploadChatFile()
Agent->>Handler: 调用 sandbox_get_file_url({ paths })
Handler->>Dispatch: dispatchSandboxGetFileUrl({ filePath, appId, ... })
Dispatch->>CallLayer: callSandboxTool({ toolName, rawArgs: {paths:[filePath]}, ... })
CallLayer->>SandboxProvider: readFileStream(filePath)
SandboxProvider-->>CallLayer: stream chunks
CallLayer->>S3: uploadChatFile({ expiredTime: +2h })
S3-->>CallLayer: { key }
CallLayer->>CallLayer: jwtSignS3ObjectKey(key, +2h)
CallLayer-->>Dispatch: { input, response, durationSeconds }
Dispatch-->>Handler: { response, nodeResponse }
Handler-->>Agent: { response, usages }
```
---
## 五、错误处理
| 错误场景 | 处理方式 |
|----------|----------|
| 文件不存在 | `readFileStream` 抛出异常,`callSandboxTool` 捕获后返回错误字符串,Agent 收到后可重试 |
| 沙盒不可用 | `getSandboxClient` 失败,返回错误信息 |
| S3 上传失败 | 捕获异常,返回错误信息 |
| 文件过大 | ⚠️ **当前未限制**,文件内容先全部读入内存再上传(见六、待优化) |
---
## 六、待优化
### 6.1 内存占用问题(当前实现缺陷)
当前实现将沙盒文件全量读入内存后再上传 S3:
```typescript
// 当前:全量内存聚合
const chunks: Uint8Array[] = [];
for await (const chunk of stream) {
chunks.push(chunk);
}
const fileBuffer = Buffer.concat(chunks); // 大文件 OOM 风险
```
**问题**:大文件会导致内存暴涨,Tool Call 场景下 AI 可以任意触发,存在 DoS 风险。
**优化方向一:文件大小限制**
在聚合 chunks 过程中累计字节数,超过阈值立即中止:
```typescript
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
let totalSize = 0;
for await (const chunk of stream) {
totalSize += chunk.byteLength;
if (totalSize > MAX_FILE_SIZE) {
throw new Error(`File too large (> ${MAX_FILE_SIZE / 1024 / 1024}MB)`);
}
chunks.push(chunk);
}
```
**优化方向二:直接流式上传 S3(推荐)**
绕过内存聚合,将 `readFileStream` 返回的 `AsyncIterable` 直接作为 S3 上传 body
```typescript
const stream = instance.provider.readFileStream(filePath);
// 需要 S3 client 支持传入 AsyncIterable/Stream 作为 body
await this.client.uploadObject({ key, body: stream, contentType: '...' });
```
可行性取决于底层 S3 client`@aws-sdk/client-s3``PutObjectCommand` 支持传入 `Readable` / `ReadableStream`)。需评估 `MinIO/custom` client 的支持情况后实施。
---
## 七、TODO
- [x] `packages/global/core/ai/sandbox/constants.ts`:新增 `SANDBOX_READ_FILE_TOOL_NAME``SANDBOX_GET_FILE_URL_TOOL_NAME``SANDBOX_GET_FILE_URL_TOOL``SandboxGetFileUrlToolSchema`(参数为 `paths: string[]`),更新 `SANDBOX_TOOLS``SANDBOX_SYSTEM_PROMPT`
- [x] `packages/service/core/ai/sandbox/toolCall.ts`:新增 `callSandboxTool()` 统一执行层,支持 `sandbox_shell``sandbox_get_file_url`
- [x] `packages/service/core/workflow/dispatch/ai/agent/sub/sandbox/index.ts`:重构为复用 `callSandboxTool`,新增 `dispatchSandboxGetFileUrl()`
- [x] `packages/service/core/workflow/dispatch/ai/tool/toolCall.ts`:合并拦截逻辑,复用 `callSandboxTool`
- [x] `packages/service/core/workflow/dispatch/ai/agent/master/call.ts`:新增 `SANDBOX_GET_FILE_URL_TOOL_NAME` 拦截逻辑
- [x] `packages/service/common/s3/type.ts``buckets/base.ts``sources/chat/index.ts``sources/chat/type.ts`:扩展 `filename``expiredTime` 参数
- [x] `projects/app/src/pages/api/system/file/[jwt].ts`:支持 `Content-Disposition` 下载头
- [ ] 大文件限制或流式上传优化(见六、待优化)
+5 -5
View File
@@ -53,7 +53,7 @@ type UserChatItemValueItemType = {
## 三、S3 存储方案
### 核心选择:`S3ChatSource.uploadChatFileByBuffer()`
### 核心选择:`S3ChatSource.uploadChatFile()`
| 字段 | 值 |
|------|----|
@@ -66,9 +66,9 @@ type UserChatItemValueItemType = {
```
getS3ChatSource()
└── uploadChatFileByBuffer({ appId, chatId, uId, filename, buffer, contentType })
└── uploadChatFile({ appId, chatId, uId, filename, buffer, contentType })
└── getFileS3Key.chat(...) → key = "chat/{appId}/{uId}/{chatId}/{filename}"
└── uploadFileByBuffer({ key, buffer, contentType })
└── uploadFileByBody({ key, buffer, contentType })
├── MongoS3TTL.create({ expiredTime: +1h }) // 文件 1 小时后自动清理
├── client.uploadObject(...)
└── returns { key, accessUrl } // accessUrl 预签名 2h
@@ -327,7 +327,7 @@ for (const msg of msgs) {
### 5.3 `wechat/fileHandler.ts` — 新建文件处理模块
图片和文件**统一存入 S3 私有桶**(`chat` source),通过 `S3ChatSource.uploadChatFileByBuffer()` 写入,
图片和文件**统一存入 S3 私有桶**(`chat` source),通过 `S3ChatSource.uploadChatFile()` 写入,
上传完成后用 `createGetChatFileURL()` 生成预签名 URL 传给 workflow。
```typescript
@@ -366,7 +366,7 @@ export async function downloadAndStoreMedia(params: {
// 上传到 S3,返回 { key, accessUrl }
// accessUrl 是 createExternalUrl({ key, expiredHours: 2 }) 的预签名 URL
const { key, accessUrl } = await chatSource.uploadChatFileByBuffer({
const { key, accessUrl } = await chatSource.uploadChatFile({
appId,
chatId,
uId,
@@ -11,10 +11,11 @@ description: 'FastGPT V4.14.10 更新说明'
## 🚀 新增内容
1. 增加 OpenSandbox docker 部署方案及适配,并支持通过挂载 volumn 进行数据持久化。
2. 飞书发布渠道,支持流输出
3. 目录最大上限,可通过环境变量配置
4. rerank 模型上限配置,避免超出单条 document 上限导致 rerank 失败
5. 增加 LLM 梯度计量计费模式,同时统一计费推送方式
2. 新增沙盒读取文件链接工具,可以直接让 AI 返回文件的访问链接
3. 飞书发布渠道,支持流输出
4. 目录最大上限,可通过环境变量配置
5. rerank 模型上限配置,避免超出单条 document 上限导致 rerank 失败
6. 增加 LLM 梯度计量计费模式,同时统一计费推送方式。
## ⚙️ 优化
+11 -11
View File
@@ -147,8 +147,8 @@
"document/content/docs/openapi/share.mdx": "2026-02-12T18:45:30+08:00",
"document/content/docs/self-host/config/json.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/config/json.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/config/model/intro.en.mdx": "2026-03-24T23:37:00+08:00",
"document/content/docs/self-host/config/model/intro.mdx": "2026-03-24T23:37:00+08:00",
"document/content/docs/self-host/config/model/intro.en.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/self-host/config/model/intro.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/self-host/config/model/minimax.en.mdx": "2026-03-19T09:32:57-05:00",
"document/content/docs/self-host/config/model/minimax.mdx": "2026-03-19T09:32:57-05:00",
"document/content/docs/self-host/config/model/siliconCloud.en.mdx": "2026-03-19T14:09:03+08:00",
@@ -171,8 +171,8 @@
"document/content/docs/self-host/custom-models/mineru.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/custom-models/ollama.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/custom-models/ollama.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/custom-models/xinference.en.mdx": "2026-03-24T23:37:00+08:00",
"document/content/docs/self-host/custom-models/xinference.mdx": "2026-03-24T23:37:00+08:00",
"document/content/docs/self-host/custom-models/xinference.en.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/self-host/custom-models/xinference.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/self-host/deploy/docker.en.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/deploy/docker.mdx": "2026-03-19T14:09:03+08:00",
"document/content/docs/self-host/deploy/sealos.en.mdx": "2026-03-03T17:39:47+08:00",
@@ -220,7 +220,7 @@
"document/content/docs/self-host/upgrading/4-14/4140.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4141.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-28T17:10:23+08:00",
"document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00",
@@ -387,14 +387,14 @@
"document/content/docs/use-cases/app-cases/dalle3.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/use-cases/app-cases/english_essay_correction_bot.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/use-cases/app-cases/feishu_webhook.en.mdx": "2026-03-28T17:14:28+08:00",
"document/content/docs/use-cases/app-cases/feishu_webhook.mdx": "2026-03-28T17:14:28+08:00",
"document/content/docs/use-cases/app-cases/feishu_webhook.en.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/use-cases/app-cases/feishu_webhook.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/use-cases/app-cases/fixingEvidence.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/use-cases/app-cases/fixingEvidence.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/use-cases/app-cases/google_search.en.mdx": "2026-03-28T17:14:28+08:00",
"document/content/docs/use-cases/app-cases/google_search.mdx": "2026-03-28T17:14:28+08:00",
"document/content/docs/use-cases/app-cases/lab_appointment.en.mdx": "2026-03-28T17:10:23+08:00",
"document/content/docs/use-cases/app-cases/lab_appointment.mdx": "2026-03-28T17:10:23+08:00",
"document/content/docs/use-cases/app-cases/google_search.en.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/use-cases/app-cases/google_search.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/use-cases/app-cases/lab_appointment.en.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/use-cases/app-cases/lab_appointment.mdx": "2026-03-30T10:05:42+08:00",
"document/content/docs/use-cases/app-cases/multi_turn_translation_bot.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/use-cases/app-cases/multi_turn_translation_bot.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/use-cases/app-cases/submit_application_template.en.mdx": "2026-03-03T17:39:47+08:00",
+43 -17
View File
@@ -26,20 +26,15 @@ export const SANDBOX_NAME: I18nStringType = {
};
export const SANDBOX_ICON = 'core/app/sandbox/sandbox' as const;
export const SANDBOX_TOOL_NAME = 'sandbox_shell';
export const SANDBOX_TOOL_DESCRIPTION =
'在独立 Linux 环境中执行 shell 命令,支持文件操作、代码运行、包安装等';
// ---- 系统提示词(useAgentSandbox=true 时追加) ----
export const SANDBOX_SYSTEM_PROMPT = `你拥有一个独立的 Linux 沙盒环境(Ubuntu 22.04),可通过 ${SANDBOX_TOOL_NAME} 工具执行命令:
- 预装:bash / python3 / node / bun / git / curl
- 可自行安装软件包(apt / pip / npm
- 生成的文件内容都保存在当前目录下即可`;
export const SandboxShellToolSchema = z.object({
command: z.string(),
timeout: z.number().optional()
});
export const SANDBOX_SHELL_TOOL: ChatCompletionTool = {
type: 'function',
function: {
name: SANDBOX_TOOL_NAME,
description: SANDBOX_TOOL_DESCRIPTION,
description: '在独立 Linux 虚拟机环境中执行 shell 命令,支持文件操作、代码运行、包安装等',
parameters: {
type: 'object',
properties: {
@@ -47,7 +42,7 @@ export const SANDBOX_SHELL_TOOL: ChatCompletionTool = {
timeout: {
type: 'number',
description: '超时秒数',
max: 300,
max: 600,
min: 1
}
},
@@ -56,10 +51,41 @@ export const SANDBOX_SHELL_TOOL: ChatCompletionTool = {
}
};
export const SANDBOX_TOOLS: ChatCompletionTool[] = [SANDBOX_SHELL_TOOL];
// Zod Schema 用于参数验证
export const SandboxShellToolSchema = z.object({
command: z.string(),
timeout: z.number().optional()
export const SANDBOX_READ_FILE_TOOL_NAME: I18nStringType = {
'zh-CN': '虚拟机/获取文件链接',
'zh-Hant': '虛擬機/獲取文件鏈接',
en: 'Sandbox/Get File URL'
};
export const SANDBOX_GET_FILE_URL_TOOL_NAME = 'sandbox_get_file_url';
export const SandboxGetFileUrlToolSchema = z.object({
paths: z.array(z.string())
});
export const SANDBOX_GET_FILE_URL_TOOL: ChatCompletionTool = {
type: 'function',
function: {
name: SANDBOX_GET_FILE_URL_TOOL_NAME,
description: '从虚拟机读取指定文件,上传至云存储,返回 2 小时有效期的访问链接',
parameters: {
type: 'object',
properties: {
paths: {
type: 'array',
items: {
type: 'string',
description: '文件访问路径,例如: output.csv'
},
description: '文件访问路径,例如: ["output.csv", "output.txt"]'
}
},
required: ['paths']
}
}
};
export const SANDBOX_TOOLS: ChatCompletionTool[] = [SANDBOX_SHELL_TOOL, SANDBOX_GET_FILE_URL_TOOL];
export const SANDBOX_SYSTEM_PROMPT = `你拥有一个独立的 Linux 沙盒环境(Ubuntu 22.04),可通过 ${SANDBOX_TOOL_NAME} 工具执行命令:
- 预装:bash / python3 / node / bun / git / curl
- 可自行安装软件包(apt / pip / npm
- 生成的文件内容都保存在当前目录下即可
- 若需要将生成的文件分享给用户,可使用 ${SANDBOX_GET_FILE_URL_TOOL_NAME} 工具获取文件的临时访问链接(有效期 2 小时)`;
@@ -1,8 +1,9 @@
import {
SANDBOX_TOOL_NAME,
SANDBOX_GET_FILE_URL_TOOL,
SANDBOX_ICON,
SANDBOX_NAME,
SANDBOX_TOOL_DESCRIPTION
SANDBOX_READ_FILE_TOOL_NAME,
SANDBOX_SHELL_TOOL
} from '../../../ai/sandbox/constants';
import type { I18nStringType } from '../../../../common/i18n/type';
@@ -12,7 +13,8 @@ export enum SubAppIds {
model = 'model_agent',
fileRead = 'file_read',
datasetSearch = 'dataset_search',
sandboxTool = 'sandbox_shell'
sandboxTool = 'sandbox_shell',
sandboxGetFileUrl = 'sandbox_get_file_url'
}
export const systemSubInfo: Record<
@@ -22,7 +24,12 @@ export const systemSubInfo: Record<
[SubAppIds.sandboxTool]: {
name: SANDBOX_NAME,
avatar: SANDBOX_ICON,
toolDescription: SANDBOX_TOOL_DESCRIPTION
toolDescription: SANDBOX_SHELL_TOOL.function.description!
},
[SubAppIds.sandboxGetFileUrl]: {
name: SANDBOX_READ_FILE_TOOL_NAME,
avatar: SANDBOX_ICON,
toolDescription: SANDBOX_GET_FILE_URL_TOOL.function.description!
},
[SubAppIds.plan]: {
name: {
+17 -6
View File
@@ -19,7 +19,7 @@ import { MongoS3TTL } from '../schema';
import { addHours, addMinutes, differenceInSeconds } from 'date-fns';
import { getLogger, LogCategories } from '../../logger';
import { addS3DelJob } from '../mq';
import { type UploadFileByBufferParams, UploadFileByBufferSchema } from '../type';
import { type UploadFileByBufferParams, UploadFileByBodySchema } from '../type';
import type { createStorage } from '@fastgpt-sdk/storage';
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
@@ -196,19 +196,30 @@ export class S3BaseBucket {
return await this.client.generatePresignedGetUrl({ key, expiredSeconds: expires });
}
async uploadFileByBuffer(params: UploadFileByBufferParams) {
const { key, buffer, contentType } = UploadFileByBufferSchema.parse(params);
async uploadFileByBody(params: UploadFileByBufferParams) {
const {
key,
body,
filename,
contentType,
expiredTime = addHours(new Date(), 1)
} = UploadFileByBodySchema.parse(params);
await MongoS3TTL.create({
minioKey: key,
bucketName: this.bucketName,
expiredTime: addHours(new Date(), 1)
expiredTime
});
await this.client.uploadObject({
key,
body: buffer,
contentType: contentType || 'application/octet-stream'
body,
contentType: contentType || 'application/octet-stream',
metadata: {
contentDisposition: `attachment; filename="${encodeURIComponent(filename)}"`,
originFilename: encodeURIComponent(filename),
uploadTime: new Date().toISOString()
}
});
return {
@@ -87,8 +87,8 @@ export class S3ChatSource extends S3PrivateBucket {
return key;
}
async uploadChatFileByBuffer(params: UploadFileParams) {
const { appId, chatId, uId, filename, buffer, contentType } =
async uploadChatFile(params: UploadFileParams) {
const { appId, chatId, uId, filename, body, contentType, expiredTime } =
UploadChatFileSchema.parse(params);
const { fileKey } = getFileS3Key.chat({
appId,
@@ -97,10 +97,12 @@ export class S3ChatSource extends S3PrivateBucket {
filename
});
return this.uploadFileByBuffer({
return this.uploadFileByBody({
key: fileKey,
buffer,
contentType
filename,
body,
contentType,
expiredTime
});
}
@@ -1,5 +1,6 @@
import { z } from 'zod';
import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
import { UploadFileByBodySchema } from '../../type';
export const ChatFileUploadSchema = z.object({
appId: ObjectIdSchema,
@@ -22,9 +23,10 @@ export const UploadChatFileSchema = z.object({
appId: ObjectIdSchema,
chatId: z.string().nonempty(),
uId: z.string().nonempty(),
filename: z.string().nonempty(),
buffer: z.instanceof(Buffer),
contentType: z.string().optional()
filename: UploadFileByBodySchema.shape.filename,
body: UploadFileByBodySchema.shape.body,
contentType: UploadFileByBodySchema.shape.contentType,
expiredTime: UploadFileByBodySchema.shape.expiredTime
});
export type UploadFileParams = z.infer<typeof UploadChatFileSchema>;
+7 -4
View File
@@ -1,6 +1,7 @@
import { z } from 'zod';
import type { Mimes } from './constants';
import type { S3BaseBucket } from './buckets/base';
import { Readable } from 'node:stream';
export const S3MetadataSchema = z.object({
filename: z.string(),
@@ -62,12 +63,14 @@ export const UploadImage2S3BucketParamsSchema = z.object({
});
export type UploadImage2S3BucketParams = z.infer<typeof UploadImage2S3BucketParamsSchema>;
export const UploadFileByBufferSchema = z.object({
buffer: z.instanceof(Buffer),
export const UploadFileByBodySchema = z.object({
body: z.union([z.instanceof(Buffer), z.string(), z.instanceof(Readable)]),
contentType: z.string().optional(),
key: z.string().nonempty()
key: z.string().nonempty(),
filename: z.string().nonempty(),
expiredTime: z.date().optional()
});
export type UploadFileByBufferParams = z.infer<typeof UploadFileByBufferSchema>;
export type UploadFileByBufferParams = z.infer<typeof UploadFileByBodySchema>;
declare global {
var s3BucketMap: {
+1 -1
View File
@@ -54,7 +54,7 @@ const SandboxInstanceSchema = new Schema({
});
SandboxInstanceSchema.index(
{ appId: 1, userId: 1, chatId: 1 },
{ provider: 1, appId: 1, userId: 1, chatId: 1 },
{
unique: true,
partialFilterExpression: {
@@ -0,0 +1,133 @@
import {
SANDBOX_TOOL_NAME,
SANDBOX_GET_FILE_URL_TOOL_NAME,
SandboxShellToolSchema,
SandboxGetFileUrlToolSchema
} from '@fastgpt/global/core/ai/sandbox/constants';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { parseJsonArgs } from '../utils';
import { getSandboxClient } from './controller';
import { getS3ChatSource } from '../../../common/s3/sources/chat';
import path from 'path';
import { jwtSignS3ObjectKey } from '../../../common/s3/utils';
import { addHours } from 'date-fns';
import { Readable } from 'stream';
import { getLogger } from '@fastgpt-sdk/otel';
import { LogCategories } from '../../../common/logger';
type SandboxToolCallParams = {
toolName: string;
rawArgs: string;
appId: string;
userId: string;
chatId: string;
};
export type SandboxToolCallResult = {
input: Record<string, any>;
response: string;
durationSeconds: number;
};
/**
* 纯沙盒工具执行层。
* 只负责调用沙盒、上传 S3 等底层操作,返回统一的执行结果,不绑定任何业务响应格式。
*/
export const callSandboxTool = async ({
toolName,
rawArgs,
appId,
userId,
chatId
}: SandboxToolCallParams): Promise<SandboxToolCallResult> => {
const startTime = Date.now();
const getDuration = () => +((Date.now() - startTime) / 1000).toFixed(2);
if (toolName === SANDBOX_TOOL_NAME) {
const parsed = SandboxShellToolSchema.safeParse(parseJsonArgs(rawArgs));
if (!parsed.success) {
return { input: {}, response: parsed.error.message, durationSeconds: getDuration() };
}
const { command, timeout } = parsed.data;
try {
const instance = await getSandboxClient({ appId, userId, chatId });
const result = await instance.exec(command, timeout);
return {
input: { command, timeout },
response: JSON.stringify({
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
}),
durationSeconds: getDuration()
};
} catch (error: any) {
getLogger(LogCategories.MODULE.AI.AGENT).error('[Sandbox Shell] Execution failed', { error });
return {
input: { command, timeout },
response: getErrText(error),
durationSeconds: getDuration()
};
}
}
if (toolName === SANDBOX_GET_FILE_URL_TOOL_NAME) {
const parsed = SandboxGetFileUrlToolSchema.safeParse(parseJsonArgs(rawArgs));
if (!parsed.success) {
return { input: {}, response: parsed.error.message, durationSeconds: getDuration() };
}
const { paths } = parsed.data;
try {
const instance = await getSandboxClient({ appId, userId, chatId });
const result = await Promise.all(
paths.map(async (url) => {
const filename = path.basename(url);
const stream = instance.provider.readFileStream(url);
const readable = Readable.from(stream); // AsyncIterable<Uint8Array> → Readable
const chatBucket = getS3ChatSource();
const expiredTime = addHours(new Date(), 2);
const { key } = await chatBucket.uploadChatFile({
appId,
chatId,
uId: userId,
filename,
body: readable,
expiredTime: expiredTime
});
const fileUrl = jwtSignS3ObjectKey(key, expiredTime);
return {
fileUrl,
filename
};
})
);
return {
input: { paths },
response: JSON.stringify(result),
durationSeconds: getDuration()
};
} catch (error) {
getLogger(LogCategories.MODULE.AI.AGENT).error('[Sandbox Get File URL] failed', { error });
return {
input: { paths },
response: `Get file URL error: ${getErrText(error)}`,
durationSeconds: getDuration()
};
}
}
return {
input: {},
response: `Unknown sandbox tool: ${toolName}`,
durationSeconds: getDuration()
};
};
@@ -14,7 +14,6 @@ import { dispatchTool } from '../sub/tool';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { DatasetSearchToolSchema } from '../sub/dataset/utils';
import { dispatchAgentDatasetSearch } from '../sub/dataset';
import { dispatchSandboxShell } from '../sub/sandbox';
import type { DispatchAgentModuleProps } from '..';
import { getLLMModel } from '../../../../../ai/model';
import { getStepCallQuery, getStepDependon } from './dependon';
@@ -33,8 +32,11 @@ import { dispatchApp, dispatchPlugin } from '../sub/app';
import { getLogger, LogCategories } from '../../../../../../common/logger';
import {
SandboxShellToolSchema,
SANDBOX_TOOL_NAME
SANDBOX_TOOL_NAME,
SANDBOX_GET_FILE_URL_TOOL_NAME,
SandboxGetFileUrlToolSchema
} from '@fastgpt/global/core/ai/sandbox/constants';
import { dispatchSandboxShell, dispatchSandboxGetFileUrl } from '../sub/sandbox';
type Response = {
stepResponse?: {
@@ -406,6 +408,32 @@ export const masterCall = async ({
usages: result.usages
};
}
if (toolId === SANDBOX_GET_FILE_URL_TOOL_NAME) {
const toolParams = SandboxGetFileUrlToolSchema.safeParse(
parseJsonArgs(call.function.arguments)
);
if (!toolParams.success) {
return {
response: toolParams.error.message,
usages: []
};
}
const result = await dispatchSandboxGetFileUrl({
paths: toolParams.data.paths,
appId: runningAppInfo.id,
userId: props.uid,
chatId,
lang: props.lang
});
childrenResponses.push(result.nodeResponse);
return {
response: result.response,
usages: result.usages
};
}
if (toolId === SubAppIds.plan) {
try {
const toolArgs = await PlanAgentParamsSchema.safeParseAsync(
@@ -1,27 +1,58 @@
import { getSandboxClient } from '../../../../../../ai/sandbox/controller';
import type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { getLogger, LogCategories } from '../../../../../../../common/logger';
import {
SANDBOX_ICON,
SANDBOX_NAME,
SANDBOX_TOOL_NAME
SANDBOX_TOOL_NAME,
SANDBOX_GET_FILE_URL_TOOL_NAME
} from '@fastgpt/global/core/ai/sandbox/constants';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
import type { localeType } from '@fastgpt/global/common/i18n/type';
import { callSandboxTool } from '../../../../../../ai/sandbox/toolCall';
type SandboxShellParams = {
command: string;
timeout?: number;
type SandboxDispatchParams = {
appId: string;
userId: string;
chatId: string;
lang?: localeType;
};
type SandboxDispatchResult = {
response: string;
usages: ChatNodeUsageType[];
nodeResponse: ChatHistoryItemResType;
};
const buildNodeResponse = ({
toolId,
input,
response,
durationSeconds,
lang
}: {
toolId: string;
input: Record<string, any>;
response: string;
durationSeconds: number;
lang?: localeType;
}): ChatHistoryItemResType => {
const nodeId = getNanoid(6);
return {
nodeId,
id: nodeId,
moduleType: FlowNodeTypeEnum.tool,
moduleName: parseI18nString(SANDBOX_NAME, lang),
moduleLogo: SANDBOX_ICON,
toolId,
toolInput: input,
toolRes: response,
totalPoints: 0,
runningTime: durationSeconds
};
};
export const dispatchSandboxShell = async ({
command,
timeout,
@@ -29,75 +60,57 @@ export const dispatchSandboxShell = async ({
userId,
chatId,
lang
}: SandboxShellParams): Promise<{
response: string;
usages: ChatNodeUsageType[];
nodeResponse: ChatHistoryItemResType;
}> => {
const startTime = Date.now();
const nodeId = getNanoid(6);
const moduleName = parseI18nString(SANDBOX_NAME, lang);
}: SandboxDispatchParams & {
command: string;
timeout?: number;
}): Promise<SandboxDispatchResult> => {
const { input, response, durationSeconds } = await callSandboxTool({
toolName: SANDBOX_TOOL_NAME,
rawArgs: JSON.stringify({ command, timeout }),
appId,
userId,
chatId
});
try {
const sandboxInstance = await getSandboxClient({
appId,
userId,
chatId
});
const result = await sandboxInstance.exec(command, timeout);
const response = JSON.stringify({
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
});
getLogger(LogCategories.MODULE.AI.AGENT).info('[Sandbox Shell] Command executed', {
command,
exitCode: result.exitCode,
stdoutLength: result.stdout?.length || 0,
stderrLength: result.stderr?.length || 0
});
return {
return {
response,
usages: [],
nodeResponse: buildNodeResponse({
toolId: SANDBOX_TOOL_NAME,
input,
response,
usages: [],
nodeResponse: {
nodeId,
id: nodeId,
moduleType: FlowNodeTypeEnum.tool,
moduleName,
moduleLogo: SANDBOX_ICON,
toolId: SANDBOX_TOOL_NAME,
toolInput: { command, timeout },
toolRes: response,
totalPoints: 0,
runningTime: +((Date.now() - startTime) / 1000).toFixed(2)
}
};
} catch (error) {
getLogger(LogCategories.MODULE.AI.AGENT).error('[Sandbox Shell] Execution failed', { error });
const errorResponse = JSON.stringify({
stdout: '',
stderr: getErrText(error),
exitCode: -1
});
return {
response: errorResponse,
usages: [],
nodeResponse: {
nodeId,
id: nodeId,
moduleType: FlowNodeTypeEnum.tool,
moduleName,
moduleLogo: SANDBOX_ICON,
toolInput: { command, timeout },
toolRes: errorResponse,
totalPoints: 0,
runningTime: +((Date.now() - startTime) / 1000).toFixed(2)
}
};
}
durationSeconds,
lang
})
};
};
export const dispatchSandboxGetFileUrl = async ({
paths,
appId,
userId,
chatId,
lang
}: SandboxDispatchParams & {
paths: string[];
}): Promise<SandboxDispatchResult> => {
const { input, response, durationSeconds } = await callSandboxTool({
toolName: SANDBOX_GET_FILE_URL_TOOL_NAME,
rawArgs: JSON.stringify({ paths }),
appId,
userId,
chatId
});
return {
response,
usages: [],
nodeResponse: buildNodeResponse({
toolId: SANDBOX_GET_FILE_URL_TOOL_NAME,
input,
response,
durationSeconds,
lang
})
};
};
@@ -20,12 +20,14 @@ Image{{imgCount}}
export const getSandboxToolWorkflowResponse = ({
name,
logo,
toolId = SANDBOX_TOOL_NAME,
input,
response,
durationSeconds
}: {
name: string;
logo: string;
toolId?: string;
input: Record<string, any>;
response: string;
durationSeconds: number;
@@ -36,7 +38,7 @@ export const getSandboxToolWorkflowResponse = ({
moduleName: name,
moduleType: FlowNodeTypeEnum.tool,
moduleLogo: logo,
toolId: SANDBOX_TOOL_NAME,
toolId,
toolInput: input,
toolRes: response,
totalPoints: 0,
@@ -18,16 +18,16 @@ import { runAgentCall } from '../../../../ai/llm/agentCall';
import type { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema';
import {
SANDBOX_SHELL_TOOL,
SandboxShellToolSchema,
SANDBOX_SYSTEM_PROMPT,
SANDBOX_ICON,
SANDBOX_NAME,
SANDBOX_TOOL_NAME
SANDBOX_TOOL_NAME,
SANDBOX_GET_FILE_URL_TOOL_NAME,
SANDBOX_TOOLS
} from '@fastgpt/global/core/ai/sandbox/constants';
import { getSandboxClient } from '../../../../ai/sandbox/controller';
import { getSandboxToolWorkflowResponse } from './constants';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { callSandboxTool } from '../../../../ai/sandbox/toolCall';
import { systemSubInfo } from '@fastgpt/global/core/workflow/node/agent/constants';
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
type ResponseType = {
requestIds: string[];
@@ -121,7 +121,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
let finalMessages = messages;
if (useAgentSandbox && global.feConfigs?.show_agent_sandbox) {
// 注入 sandbox_shell 工具
tools.push(SANDBOX_SHELL_TOOL);
tools.push(...SANDBOX_TOOLS);
// 追加提示词
const systemMessage = messages.find((m) => m.role === 'system');
@@ -135,10 +135,11 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
}
const getToolInfo = (name: string) => {
if (name === SANDBOX_TOOL_NAME) {
const systemTool = systemSubInfo[name];
if (systemTool) {
return {
name: SANDBOX_NAME[workflowProps.lang || 'zh-CN'] || SANDBOX_TOOL_NAME,
avatar: SANDBOX_ICON
name: parseI18nString(systemTool.name, workflowProps.lang),
avatar: systemTool.avatar
};
}
@@ -240,7 +241,6 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
},
handleToolResponse: async ({ call, messages }) => {
const tool = getToolInfo(call.function?.name);
const startTime = Date.now();
const {
response,
@@ -250,41 +250,29 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
interactive,
stop
} = await (async () => {
// 拦截 sandbox_shell 调用
if (call.function?.name === SANDBOX_TOOL_NAME) {
try {
const params = SandboxShellToolSchema.parse(parseJsonArgs(call.function.arguments));
// 拦截 sandbox 工具调用
if (
call.function?.name === SANDBOX_TOOL_NAME ||
call.function?.name === SANDBOX_GET_FILE_URL_TOOL_NAME
) {
const { input, response, durationSeconds } = await callSandboxTool({
toolName: call.function.name,
rawArgs: call.function.arguments ?? '',
appId: String(workflowProps.runningAppInfo.id),
userId: String(workflowProps.uid),
chatId: workflowProps.chatId
});
const instance = await getSandboxClient({
appId: String(workflowProps.runningAppInfo.id),
userId: String(workflowProps.uid),
chatId: workflowProps.chatId
});
const result = await instance.exec(params.command, params.timeout);
const flowResponse = getSandboxToolWorkflowResponse({
name: tool.name,
logo: SANDBOX_ICON,
toolId: call.function.name,
input,
response,
durationSeconds
});
const stringToolResponse = JSON.stringify({
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
});
const flowResponse = getSandboxToolWorkflowResponse({
name: tool.name,
logo: SANDBOX_ICON,
input: params,
response: stringToolResponse,
durationSeconds: +((Date.now() - startTime) / 1000).toFixed(2)
});
return {
response: stringToolResponse,
flowResponse
};
} catch (error) {
return {
response: `Sandbox execution error: ${getErrText(error)}`
};
}
return { response, flowResponse };
} else {
const toolNode = tool?.rawData;
+1 -1
View File
@@ -8,7 +8,7 @@
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.2",
"@fastgpt-sdk/sandbox-adapter": "^0.0.33",
"@fastgpt-sdk/sandbox-adapter": "^0.0.34",
"@fastgpt-sdk/otel": "catalog:",
"@fastgpt-sdk/storage": "catalog:",
"@fastgpt/global": "workspace:*",
@@ -43,7 +43,7 @@ const MyModal = ({
showCloseButton = true,
contentPx = '8',
contentPy = '8',
headerPx,
headerPx = 0,
...props
}: MyModalProps) => {
const { isPc } = useSystem();
@@ -99,7 +99,7 @@ const MyModal = ({
fontWeight={'500'}
mb={6}
py={0}
px={headerPx ?? contentPx}
px={headerPx}
gap={3}
>
{iconSrc && (
+9 -9
View File
@@ -250,8 +250,8 @@ importers:
specifier: 'catalog:'
version: 0.1.0
'@fastgpt-sdk/sandbox-adapter':
specifier: ^0.0.33
version: 0.0.33
specifier: ^0.0.34
version: 0.0.34
'@fastgpt-sdk/storage':
specifier: 'catalog:'
version: 0.6.15(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(proxy-agent@6.5.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)
@@ -2735,8 +2735,8 @@ packages:
'@fastgpt-sdk/plugin@0.3.8':
resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==}
'@fastgpt-sdk/sandbox-adapter@0.0.33':
resolution: {integrity: sha512-XsfMn5rdxFMTyNuQjp97vJcQBeJIphZ+qtpVxcy4pABSk7B90Xf8nYCZ09OLTfcbql+flQ4P3C9HoDIi64ZMkA==}
'@fastgpt-sdk/sandbox-adapter@0.0.34':
resolution: {integrity: sha512-YXCwycqs2yByOPUMMjm2tf0BYUJfLR9D4bvHDv6xIbfKT5btT+hR1pujW5nVawXJDefYNsDfLy8dQ+IkMd21xQ==}
engines: {node: '>=18'}
'@fastgpt-sdk/storage@0.6.15':
@@ -8801,8 +8801,8 @@ packages:
resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
minimatch@10.2.5:
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.2:
@@ -13745,7 +13745,7 @@ snapshots:
'@fortaine/fetch-event-source': 3.0.6
zod: 4.1.12
'@fastgpt-sdk/sandbox-adapter@0.0.33':
'@fastgpt-sdk/sandbox-adapter@0.0.34':
dependencies:
'@e2b/code-interpreter': 2.4.0
@@ -19329,7 +19329,7 @@ snapshots:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.2.3
minimatch: 10.2.4
minimatch: 10.2.5
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.2
@@ -21125,7 +21125,7 @@ snapshots:
mimic-response@4.0.0: {}
minimatch@10.2.4:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.5
@@ -9,6 +9,7 @@ import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
type FormType = {
contact: string;
@@ -61,7 +62,7 @@ const UpdateContactModal = ({
}
);
const { SendCodeBox } = useSendCode({ type: 'bindNotification' });
const { SendCodeBox } = useSendCode({ type: UserAuthTypeEnum.bindNotification });
const placeholder = feConfigs?.bind_notification_method
?.map((item) => {
@@ -40,12 +40,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
if (metadata) {
if (metadata?.contentType) {
res.setHeader('Content-Type', metadata.contentType);
}
if (metadata?.contentLength) {
res.setHeader('Content-Length', metadata.contentLength);
}
if (metadata?.filename) {
res.setHeader(
'Content-Disposition',
`attachment; filename="${encodeURIComponent(metadata.filename)}"`
);
}
res.setHeader('Cache-Control', 'public, max-age=31536000');
stream.pipe(res);