mirror of
https://github.com/labring/FastGPT.git
synced 2026-04-26 02:07:28 +08:00
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:
@@ -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` 下载头
|
||||
- [ ] 大文件限制或流式上传优化(见六、待优化)
|
||||
@@ -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 梯度计量计费模式,同时统一计费推送方式。
|
||||
|
||||
## ⚙️ 优化
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
Generated
+9
-9
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user