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 存储方案
|
## 三、S3 存储方案
|
||||||
|
|
||||||
### 核心选择:`S3ChatSource.uploadChatFileByBuffer()`
|
### 核心选择:`S3ChatSource.uploadChatFile()`
|
||||||
|
|
||||||
| 字段 | 值 |
|
| 字段 | 值 |
|
||||||
|------|----|
|
|------|----|
|
||||||
@@ -66,9 +66,9 @@ type UserChatItemValueItemType = {
|
|||||||
|
|
||||||
```
|
```
|
||||||
getS3ChatSource()
|
getS3ChatSource()
|
||||||
└── uploadChatFileByBuffer({ appId, chatId, uId, filename, buffer, contentType })
|
└── uploadChatFile({ appId, chatId, uId, filename, buffer, contentType })
|
||||||
└── getFileS3Key.chat(...) → key = "chat/{appId}/{uId}/{chatId}/{filename}"
|
└── getFileS3Key.chat(...) → key = "chat/{appId}/{uId}/{chatId}/{filename}"
|
||||||
└── uploadFileByBuffer({ key, buffer, contentType })
|
└── uploadFileByBody({ key, buffer, contentType })
|
||||||
├── MongoS3TTL.create({ expiredTime: +1h }) // 文件 1 小时后自动清理
|
├── MongoS3TTL.create({ expiredTime: +1h }) // 文件 1 小时后自动清理
|
||||||
├── client.uploadObject(...)
|
├── client.uploadObject(...)
|
||||||
└── returns { key, accessUrl } // accessUrl 预签名 2h
|
└── returns { key, accessUrl } // accessUrl 预签名 2h
|
||||||
@@ -327,7 +327,7 @@ for (const msg of msgs) {
|
|||||||
|
|
||||||
### 5.3 `wechat/fileHandler.ts` — 新建文件处理模块
|
### 5.3 `wechat/fileHandler.ts` — 新建文件处理模块
|
||||||
|
|
||||||
图片和文件**统一存入 S3 私有桶**(`chat` source),通过 `S3ChatSource.uploadChatFileByBuffer()` 写入,
|
图片和文件**统一存入 S3 私有桶**(`chat` source),通过 `S3ChatSource.uploadChatFile()` 写入,
|
||||||
上传完成后用 `createGetChatFileURL()` 生成预签名 URL 传给 workflow。
|
上传完成后用 `createGetChatFileURL()` 生成预签名 URL 传给 workflow。
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@@ -366,7 +366,7 @@ export async function downloadAndStoreMedia(params: {
|
|||||||
|
|
||||||
// 上传到 S3,返回 { key, accessUrl }
|
// 上传到 S3,返回 { key, accessUrl }
|
||||||
// accessUrl 是 createExternalUrl({ key, expiredHours: 2 }) 的预签名 URL
|
// accessUrl 是 createExternalUrl({ key, expiredHours: 2 }) 的预签名 URL
|
||||||
const { key, accessUrl } = await chatSource.uploadChatFileByBuffer({
|
const { key, accessUrl } = await chatSource.uploadChatFile({
|
||||||
appId,
|
appId,
|
||||||
chatId,
|
chatId,
|
||||||
uId,
|
uId,
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ description: 'FastGPT V4.14.10 更新说明'
|
|||||||
## 🚀 新增内容
|
## 🚀 新增内容
|
||||||
|
|
||||||
1. 增加 OpenSandbox docker 部署方案及适配,并支持通过挂载 volumn 进行数据持久化。
|
1. 增加 OpenSandbox docker 部署方案及适配,并支持通过挂载 volumn 进行数据持久化。
|
||||||
2. 飞书发布渠道,支持流输出。
|
2. 新增沙盒读取文件链接工具,可以直接让 AI 返回文件的访问链接。
|
||||||
3. 目录最大上限,可通过环境变量配置。
|
3. 飞书发布渠道,支持流输出。
|
||||||
4. rerank 模型上限配置,避免超出单条 document 上限导致 rerank 失败。
|
4. 目录最大上限,可通过环境变量配置。
|
||||||
5. 增加 LLM 梯度计量计费模式,同时统一计费推送方式。
|
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/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.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/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.en.mdx": "2026-03-30T10:05:42+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.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.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/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",
|
"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/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.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/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.en.mdx": "2026-03-30T10:05:42+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.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.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/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",
|
"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/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.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/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.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/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",
|
"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/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.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/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.en.mdx": "2026-03-30T10:05:42+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.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.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/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.en.mdx": "2026-03-30T10:05:42+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/google_search.mdx": "2026-03-30T10:05:42+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.en.mdx": "2026-03-30T10:05:42+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/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.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/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",
|
"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_ICON = 'core/app/sandbox/sandbox' as const;
|
||||||
export const SANDBOX_TOOL_NAME = 'sandbox_shell';
|
export const SANDBOX_TOOL_NAME = 'sandbox_shell';
|
||||||
export const SANDBOX_TOOL_DESCRIPTION =
|
export const SandboxShellToolSchema = z.object({
|
||||||
'在独立 Linux 环境中执行 shell 命令,支持文件操作、代码运行、包安装等';
|
command: z.string(),
|
||||||
|
timeout: z.number().optional()
|
||||||
// ---- 系统提示词(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 SANDBOX_SHELL_TOOL: ChatCompletionTool = {
|
export const SANDBOX_SHELL_TOOL: ChatCompletionTool = {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
function: {
|
function: {
|
||||||
name: SANDBOX_TOOL_NAME,
|
name: SANDBOX_TOOL_NAME,
|
||||||
description: SANDBOX_TOOL_DESCRIPTION,
|
description: '在独立 Linux 虚拟机环境中执行 shell 命令,支持文件操作、代码运行、包安装等',
|
||||||
parameters: {
|
parameters: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
@@ -47,7 +42,7 @@ export const SANDBOX_SHELL_TOOL: ChatCompletionTool = {
|
|||||||
timeout: {
|
timeout: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
description: '超时秒数',
|
description: '超时秒数',
|
||||||
max: 300,
|
max: 600,
|
||||||
min: 1
|
min: 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -56,10 +51,41 @@ export const SANDBOX_SHELL_TOOL: ChatCompletionTool = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SANDBOX_TOOLS: ChatCompletionTool[] = [SANDBOX_SHELL_TOOL];
|
export const SANDBOX_READ_FILE_TOOL_NAME: I18nStringType = {
|
||||||
|
'zh-CN': '虚拟机/获取文件链接',
|
||||||
// Zod Schema 用于参数验证
|
'zh-Hant': '虛擬機/獲取文件鏈接',
|
||||||
export const SandboxShellToolSchema = z.object({
|
en: 'Sandbox/Get File URL'
|
||||||
command: z.string(),
|
};
|
||||||
timeout: z.number().optional()
|
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 {
|
import {
|
||||||
SANDBOX_TOOL_NAME,
|
SANDBOX_GET_FILE_URL_TOOL,
|
||||||
SANDBOX_ICON,
|
SANDBOX_ICON,
|
||||||
SANDBOX_NAME,
|
SANDBOX_NAME,
|
||||||
SANDBOX_TOOL_DESCRIPTION
|
SANDBOX_READ_FILE_TOOL_NAME,
|
||||||
|
SANDBOX_SHELL_TOOL
|
||||||
} from '../../../ai/sandbox/constants';
|
} from '../../../ai/sandbox/constants';
|
||||||
import type { I18nStringType } from '../../../../common/i18n/type';
|
import type { I18nStringType } from '../../../../common/i18n/type';
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ export enum SubAppIds {
|
|||||||
model = 'model_agent',
|
model = 'model_agent',
|
||||||
fileRead = 'file_read',
|
fileRead = 'file_read',
|
||||||
datasetSearch = 'dataset_search',
|
datasetSearch = 'dataset_search',
|
||||||
sandboxTool = 'sandbox_shell'
|
sandboxTool = 'sandbox_shell',
|
||||||
|
sandboxGetFileUrl = 'sandbox_get_file_url'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const systemSubInfo: Record<
|
export const systemSubInfo: Record<
|
||||||
@@ -22,7 +24,12 @@ export const systemSubInfo: Record<
|
|||||||
[SubAppIds.sandboxTool]: {
|
[SubAppIds.sandboxTool]: {
|
||||||
name: SANDBOX_NAME,
|
name: SANDBOX_NAME,
|
||||||
avatar: SANDBOX_ICON,
|
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]: {
|
[SubAppIds.plan]: {
|
||||||
name: {
|
name: {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { MongoS3TTL } from '../schema';
|
|||||||
import { addHours, addMinutes, differenceInSeconds } from 'date-fns';
|
import { addHours, addMinutes, differenceInSeconds } from 'date-fns';
|
||||||
import { getLogger, LogCategories } from '../../logger';
|
import { getLogger, LogCategories } from '../../logger';
|
||||||
import { addS3DelJob } from '../mq';
|
import { addS3DelJob } from '../mq';
|
||||||
import { type UploadFileByBufferParams, UploadFileByBufferSchema } from '../type';
|
import { type UploadFileByBufferParams, UploadFileByBodySchema } from '../type';
|
||||||
import type { createStorage } from '@fastgpt-sdk/storage';
|
import type { createStorage } from '@fastgpt-sdk/storage';
|
||||||
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
import { parseFileExtensionFromUrl } from '@fastgpt/global/common/string/tools';
|
||||||
|
|
||||||
@@ -196,19 +196,30 @@ export class S3BaseBucket {
|
|||||||
return await this.client.generatePresignedGetUrl({ key, expiredSeconds: expires });
|
return await this.client.generatePresignedGetUrl({ key, expiredSeconds: expires });
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFileByBuffer(params: UploadFileByBufferParams) {
|
async uploadFileByBody(params: UploadFileByBufferParams) {
|
||||||
const { key, buffer, contentType } = UploadFileByBufferSchema.parse(params);
|
const {
|
||||||
|
key,
|
||||||
|
body,
|
||||||
|
filename,
|
||||||
|
contentType,
|
||||||
|
expiredTime = addHours(new Date(), 1)
|
||||||
|
} = UploadFileByBodySchema.parse(params);
|
||||||
|
|
||||||
await MongoS3TTL.create({
|
await MongoS3TTL.create({
|
||||||
minioKey: key,
|
minioKey: key,
|
||||||
bucketName: this.bucketName,
|
bucketName: this.bucketName,
|
||||||
expiredTime: addHours(new Date(), 1)
|
expiredTime
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.client.uploadObject({
|
await this.client.uploadObject({
|
||||||
key,
|
key,
|
||||||
body: buffer,
|
body,
|
||||||
contentType: contentType || 'application/octet-stream'
|
contentType: contentType || 'application/octet-stream',
|
||||||
|
metadata: {
|
||||||
|
contentDisposition: `attachment; filename="${encodeURIComponent(filename)}"`,
|
||||||
|
originFilename: encodeURIComponent(filename),
|
||||||
|
uploadTime: new Date().toISOString()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ export class S3ChatSource extends S3PrivateBucket {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadChatFileByBuffer(params: UploadFileParams) {
|
async uploadChatFile(params: UploadFileParams) {
|
||||||
const { appId, chatId, uId, filename, buffer, contentType } =
|
const { appId, chatId, uId, filename, body, contentType, expiredTime } =
|
||||||
UploadChatFileSchema.parse(params);
|
UploadChatFileSchema.parse(params);
|
||||||
const { fileKey } = getFileS3Key.chat({
|
const { fileKey } = getFileS3Key.chat({
|
||||||
appId,
|
appId,
|
||||||
@@ -97,10 +97,12 @@ export class S3ChatSource extends S3PrivateBucket {
|
|||||||
filename
|
filename
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.uploadFileByBuffer({
|
return this.uploadFileByBody({
|
||||||
key: fileKey,
|
key: fileKey,
|
||||||
buffer,
|
filename,
|
||||||
contentType
|
body,
|
||||||
|
contentType,
|
||||||
|
expiredTime
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
|
import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
|
||||||
|
import { UploadFileByBodySchema } from '../../type';
|
||||||
|
|
||||||
export const ChatFileUploadSchema = z.object({
|
export const ChatFileUploadSchema = z.object({
|
||||||
appId: ObjectIdSchema,
|
appId: ObjectIdSchema,
|
||||||
@@ -22,9 +23,10 @@ export const UploadChatFileSchema = z.object({
|
|||||||
appId: ObjectIdSchema,
|
appId: ObjectIdSchema,
|
||||||
chatId: z.string().nonempty(),
|
chatId: z.string().nonempty(),
|
||||||
uId: z.string().nonempty(),
|
uId: z.string().nonempty(),
|
||||||
filename: z.string().nonempty(),
|
filename: UploadFileByBodySchema.shape.filename,
|
||||||
buffer: z.instanceof(Buffer),
|
body: UploadFileByBodySchema.shape.body,
|
||||||
contentType: z.string().optional()
|
contentType: UploadFileByBodySchema.shape.contentType,
|
||||||
|
expiredTime: UploadFileByBodySchema.shape.expiredTime
|
||||||
});
|
});
|
||||||
|
|
||||||
export type UploadFileParams = z.infer<typeof UploadChatFileSchema>;
|
export type UploadFileParams = z.infer<typeof UploadChatFileSchema>;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { Mimes } from './constants';
|
import type { Mimes } from './constants';
|
||||||
import type { S3BaseBucket } from './buckets/base';
|
import type { S3BaseBucket } from './buckets/base';
|
||||||
|
import { Readable } from 'node:stream';
|
||||||
|
|
||||||
export const S3MetadataSchema = z.object({
|
export const S3MetadataSchema = z.object({
|
||||||
filename: z.string(),
|
filename: z.string(),
|
||||||
@@ -62,12 +63,14 @@ export const UploadImage2S3BucketParamsSchema = z.object({
|
|||||||
});
|
});
|
||||||
export type UploadImage2S3BucketParams = z.infer<typeof UploadImage2S3BucketParamsSchema>;
|
export type UploadImage2S3BucketParams = z.infer<typeof UploadImage2S3BucketParamsSchema>;
|
||||||
|
|
||||||
export const UploadFileByBufferSchema = z.object({
|
export const UploadFileByBodySchema = z.object({
|
||||||
buffer: z.instanceof(Buffer),
|
body: z.union([z.instanceof(Buffer), z.string(), z.instanceof(Readable)]),
|
||||||
contentType: z.string().optional(),
|
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 {
|
declare global {
|
||||||
var s3BucketMap: {
|
var s3BucketMap: {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ const SandboxInstanceSchema = new Schema({
|
|||||||
});
|
});
|
||||||
|
|
||||||
SandboxInstanceSchema.index(
|
SandboxInstanceSchema.index(
|
||||||
{ appId: 1, userId: 1, chatId: 1 },
|
{ provider: 1, appId: 1, userId: 1, chatId: 1 },
|
||||||
{
|
{
|
||||||
unique: true,
|
unique: true,
|
||||||
partialFilterExpression: {
|
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 { getErrText } from '@fastgpt/global/common/error/utils';
|
||||||
import { DatasetSearchToolSchema } from '../sub/dataset/utils';
|
import { DatasetSearchToolSchema } from '../sub/dataset/utils';
|
||||||
import { dispatchAgentDatasetSearch } from '../sub/dataset';
|
import { dispatchAgentDatasetSearch } from '../sub/dataset';
|
||||||
import { dispatchSandboxShell } from '../sub/sandbox';
|
|
||||||
import type { DispatchAgentModuleProps } from '..';
|
import type { DispatchAgentModuleProps } from '..';
|
||||||
import { getLLMModel } from '../../../../../ai/model';
|
import { getLLMModel } from '../../../../../ai/model';
|
||||||
import { getStepCallQuery, getStepDependon } from './dependon';
|
import { getStepCallQuery, getStepDependon } from './dependon';
|
||||||
@@ -33,8 +32,11 @@ import { dispatchApp, dispatchPlugin } from '../sub/app';
|
|||||||
import { getLogger, LogCategories } from '../../../../../../common/logger';
|
import { getLogger, LogCategories } from '../../../../../../common/logger';
|
||||||
import {
|
import {
|
||||||
SandboxShellToolSchema,
|
SandboxShellToolSchema,
|
||||||
SANDBOX_TOOL_NAME
|
SANDBOX_TOOL_NAME,
|
||||||
|
SANDBOX_GET_FILE_URL_TOOL_NAME,
|
||||||
|
SandboxGetFileUrlToolSchema
|
||||||
} from '@fastgpt/global/core/ai/sandbox/constants';
|
} from '@fastgpt/global/core/ai/sandbox/constants';
|
||||||
|
import { dispatchSandboxShell, dispatchSandboxGetFileUrl } from '../sub/sandbox';
|
||||||
|
|
||||||
type Response = {
|
type Response = {
|
||||||
stepResponse?: {
|
stepResponse?: {
|
||||||
@@ -406,6 +408,32 @@ export const masterCall = async ({
|
|||||||
usages: result.usages
|
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) {
|
if (toolId === SubAppIds.plan) {
|
||||||
try {
|
try {
|
||||||
const toolArgs = await PlanAgentParamsSchema.safeParseAsync(
|
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 type { ChatNodeUsageType } from '@fastgpt/global/support/wallet/bill/type';
|
||||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||||
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
|
||||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
|
||||||
import { getLogger, LogCategories } from '../../../../../../../common/logger';
|
|
||||||
import {
|
import {
|
||||||
SANDBOX_ICON,
|
SANDBOX_ICON,
|
||||||
SANDBOX_NAME,
|
SANDBOX_NAME,
|
||||||
SANDBOX_TOOL_NAME
|
SANDBOX_TOOL_NAME,
|
||||||
|
SANDBOX_GET_FILE_URL_TOOL_NAME
|
||||||
} from '@fastgpt/global/core/ai/sandbox/constants';
|
} from '@fastgpt/global/core/ai/sandbox/constants';
|
||||||
import { getErrText } from '@fastgpt/global/common/error/utils';
|
|
||||||
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
|
import { parseI18nString } from '@fastgpt/global/common/i18n/utils';
|
||||||
import type { localeType } from '@fastgpt/global/common/i18n/type';
|
import type { localeType } from '@fastgpt/global/common/i18n/type';
|
||||||
|
import { callSandboxTool } from '../../../../../../ai/sandbox/toolCall';
|
||||||
|
|
||||||
type SandboxShellParams = {
|
type SandboxDispatchParams = {
|
||||||
command: string;
|
|
||||||
timeout?: number;
|
|
||||||
appId: string;
|
appId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
lang?: localeType;
|
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 ({
|
export const dispatchSandboxShell = async ({
|
||||||
command,
|
command,
|
||||||
timeout,
|
timeout,
|
||||||
@@ -29,75 +60,57 @@ export const dispatchSandboxShell = async ({
|
|||||||
userId,
|
userId,
|
||||||
chatId,
|
chatId,
|
||||||
lang
|
lang
|
||||||
}: SandboxShellParams): Promise<{
|
}: SandboxDispatchParams & {
|
||||||
response: string;
|
command: string;
|
||||||
usages: ChatNodeUsageType[];
|
timeout?: number;
|
||||||
nodeResponse: ChatHistoryItemResType;
|
}): Promise<SandboxDispatchResult> => {
|
||||||
}> => {
|
const { input, response, durationSeconds } = await callSandboxTool({
|
||||||
const startTime = Date.now();
|
toolName: SANDBOX_TOOL_NAME,
|
||||||
const nodeId = getNanoid(6);
|
rawArgs: JSON.stringify({ command, timeout }),
|
||||||
const moduleName = parseI18nString(SANDBOX_NAME, lang);
|
appId,
|
||||||
|
userId,
|
||||||
|
chatId
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
return {
|
||||||
const sandboxInstance = await getSandboxClient({
|
response,
|
||||||
appId,
|
usages: [],
|
||||||
userId,
|
nodeResponse: buildNodeResponse({
|
||||||
chatId
|
toolId: SANDBOX_TOOL_NAME,
|
||||||
});
|
input,
|
||||||
|
|
||||||
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 {
|
|
||||||
response,
|
response,
|
||||||
usages: [],
|
durationSeconds,
|
||||||
nodeResponse: {
|
lang
|
||||||
nodeId,
|
})
|
||||||
id: nodeId,
|
};
|
||||||
moduleType: FlowNodeTypeEnum.tool,
|
};
|
||||||
moduleName,
|
|
||||||
moduleLogo: SANDBOX_ICON,
|
export const dispatchSandboxGetFileUrl = async ({
|
||||||
toolId: SANDBOX_TOOL_NAME,
|
paths,
|
||||||
toolInput: { command, timeout },
|
appId,
|
||||||
toolRes: response,
|
userId,
|
||||||
totalPoints: 0,
|
chatId,
|
||||||
runningTime: +((Date.now() - startTime) / 1000).toFixed(2)
|
lang
|
||||||
}
|
}: SandboxDispatchParams & {
|
||||||
};
|
paths: string[];
|
||||||
} catch (error) {
|
}): Promise<SandboxDispatchResult> => {
|
||||||
getLogger(LogCategories.MODULE.AI.AGENT).error('[Sandbox Shell] Execution failed', { error });
|
const { input, response, durationSeconds } = await callSandboxTool({
|
||||||
|
toolName: SANDBOX_GET_FILE_URL_TOOL_NAME,
|
||||||
const errorResponse = JSON.stringify({
|
rawArgs: JSON.stringify({ paths }),
|
||||||
stdout: '',
|
appId,
|
||||||
stderr: getErrText(error),
|
userId,
|
||||||
exitCode: -1
|
chatId
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
response: errorResponse,
|
response,
|
||||||
usages: [],
|
usages: [],
|
||||||
nodeResponse: {
|
nodeResponse: buildNodeResponse({
|
||||||
nodeId,
|
toolId: SANDBOX_GET_FILE_URL_TOOL_NAME,
|
||||||
id: nodeId,
|
input,
|
||||||
moduleType: FlowNodeTypeEnum.tool,
|
response,
|
||||||
moduleName,
|
durationSeconds,
|
||||||
moduleLogo: SANDBOX_ICON,
|
lang
|
||||||
toolInput: { command, timeout },
|
})
|
||||||
toolRes: errorResponse,
|
};
|
||||||
totalPoints: 0,
|
|
||||||
runningTime: +((Date.now() - startTime) / 1000).toFixed(2)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ Image:{{imgCount}}
|
|||||||
export const getSandboxToolWorkflowResponse = ({
|
export const getSandboxToolWorkflowResponse = ({
|
||||||
name,
|
name,
|
||||||
logo,
|
logo,
|
||||||
|
toolId = SANDBOX_TOOL_NAME,
|
||||||
input,
|
input,
|
||||||
response,
|
response,
|
||||||
durationSeconds
|
durationSeconds
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
logo: string;
|
logo: string;
|
||||||
|
toolId?: string;
|
||||||
input: Record<string, any>;
|
input: Record<string, any>;
|
||||||
response: string;
|
response: string;
|
||||||
durationSeconds: number;
|
durationSeconds: number;
|
||||||
@@ -36,7 +38,7 @@ export const getSandboxToolWorkflowResponse = ({
|
|||||||
moduleName: name,
|
moduleName: name,
|
||||||
moduleType: FlowNodeTypeEnum.tool,
|
moduleType: FlowNodeTypeEnum.tool,
|
||||||
moduleLogo: logo,
|
moduleLogo: logo,
|
||||||
toolId: SANDBOX_TOOL_NAME,
|
toolId,
|
||||||
toolInput: input,
|
toolInput: input,
|
||||||
toolRes: response,
|
toolRes: response,
|
||||||
totalPoints: 0,
|
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 { ToolCallChildrenInteractive } from '@fastgpt/global/core/workflow/template/system/interactive/type';
|
||||||
import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema';
|
import type { JsonSchemaPropertiesItemType } from '@fastgpt/global/core/app/jsonschema';
|
||||||
import {
|
import {
|
||||||
SANDBOX_SHELL_TOOL,
|
|
||||||
SandboxShellToolSchema,
|
|
||||||
SANDBOX_SYSTEM_PROMPT,
|
SANDBOX_SYSTEM_PROMPT,
|
||||||
SANDBOX_ICON,
|
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';
|
} from '@fastgpt/global/core/ai/sandbox/constants';
|
||||||
import { getSandboxClient } from '../../../../ai/sandbox/controller';
|
|
||||||
import { getSandboxToolWorkflowResponse } from './constants';
|
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 = {
|
type ResponseType = {
|
||||||
requestIds: string[];
|
requestIds: string[];
|
||||||
@@ -121,7 +121,7 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
|
|||||||
let finalMessages = messages;
|
let finalMessages = messages;
|
||||||
if (useAgentSandbox && global.feConfigs?.show_agent_sandbox) {
|
if (useAgentSandbox && global.feConfigs?.show_agent_sandbox) {
|
||||||
// 注入 sandbox_shell 工具
|
// 注入 sandbox_shell 工具
|
||||||
tools.push(SANDBOX_SHELL_TOOL);
|
tools.push(...SANDBOX_TOOLS);
|
||||||
|
|
||||||
// 追加提示词
|
// 追加提示词
|
||||||
const systemMessage = messages.find((m) => m.role === 'system');
|
const systemMessage = messages.find((m) => m.role === 'system');
|
||||||
@@ -135,10 +135,11 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getToolInfo = (name: string) => {
|
const getToolInfo = (name: string) => {
|
||||||
if (name === SANDBOX_TOOL_NAME) {
|
const systemTool = systemSubInfo[name];
|
||||||
|
if (systemTool) {
|
||||||
return {
|
return {
|
||||||
name: SANDBOX_NAME[workflowProps.lang || 'zh-CN'] || SANDBOX_TOOL_NAME,
|
name: parseI18nString(systemTool.name, workflowProps.lang),
|
||||||
avatar: SANDBOX_ICON
|
avatar: systemTool.avatar
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +241,6 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
|
|||||||
},
|
},
|
||||||
handleToolResponse: async ({ call, messages }) => {
|
handleToolResponse: async ({ call, messages }) => {
|
||||||
const tool = getToolInfo(call.function?.name);
|
const tool = getToolInfo(call.function?.name);
|
||||||
const startTime = Date.now();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
response,
|
response,
|
||||||
@@ -250,41 +250,29 @@ export const runToolCall = async (props: DispatchToolModuleProps): Promise<Respo
|
|||||||
interactive,
|
interactive,
|
||||||
stop
|
stop
|
||||||
} = await (async () => {
|
} = await (async () => {
|
||||||
// 拦截 sandbox_shell 调用
|
// 拦截 sandbox 工具调用
|
||||||
if (call.function?.name === SANDBOX_TOOL_NAME) {
|
if (
|
||||||
try {
|
call.function?.name === SANDBOX_TOOL_NAME ||
|
||||||
const params = SandboxShellToolSchema.parse(parseJsonArgs(call.function.arguments));
|
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({
|
const flowResponse = getSandboxToolWorkflowResponse({
|
||||||
appId: String(workflowProps.runningAppInfo.id),
|
name: tool.name,
|
||||||
userId: String(workflowProps.uid),
|
logo: SANDBOX_ICON,
|
||||||
chatId: workflowProps.chatId
|
toolId: call.function.name,
|
||||||
});
|
input,
|
||||||
const result = await instance.exec(params.command, params.timeout);
|
response,
|
||||||
|
durationSeconds
|
||||||
|
});
|
||||||
|
|
||||||
const stringToolResponse = JSON.stringify({
|
return { response, flowResponse };
|
||||||
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)}`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
const toolNode = tool?.rawData;
|
const toolNode = tool?.rawData;
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/json-schema-ref-parser": "^11.7.2",
|
"@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/otel": "catalog:",
|
||||||
"@fastgpt-sdk/storage": "catalog:",
|
"@fastgpt-sdk/storage": "catalog:",
|
||||||
"@fastgpt/global": "workspace:*",
|
"@fastgpt/global": "workspace:*",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const MyModal = ({
|
|||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
contentPx = '8',
|
contentPx = '8',
|
||||||
contentPy = '8',
|
contentPy = '8',
|
||||||
headerPx,
|
headerPx = 0,
|
||||||
...props
|
...props
|
||||||
}: MyModalProps) => {
|
}: MyModalProps) => {
|
||||||
const { isPc } = useSystem();
|
const { isPc } = useSystem();
|
||||||
@@ -99,7 +99,7 @@ const MyModal = ({
|
|||||||
fontWeight={'500'}
|
fontWeight={'500'}
|
||||||
mb={6}
|
mb={6}
|
||||||
py={0}
|
py={0}
|
||||||
px={headerPx ?? contentPx}
|
px={headerPx}
|
||||||
gap={3}
|
gap={3}
|
||||||
>
|
>
|
||||||
{iconSrc && (
|
{iconSrc && (
|
||||||
|
|||||||
Generated
+9
-9
@@ -250,8 +250,8 @@ importers:
|
|||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
'@fastgpt-sdk/sandbox-adapter':
|
'@fastgpt-sdk/sandbox-adapter':
|
||||||
specifier: ^0.0.33
|
specifier: ^0.0.34
|
||||||
version: 0.0.33
|
version: 0.0.34
|
||||||
'@fastgpt-sdk/storage':
|
'@fastgpt-sdk/storage':
|
||||||
specifier: 'catalog:'
|
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)
|
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':
|
'@fastgpt-sdk/plugin@0.3.8':
|
||||||
resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==}
|
resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==}
|
||||||
|
|
||||||
'@fastgpt-sdk/sandbox-adapter@0.0.33':
|
'@fastgpt-sdk/sandbox-adapter@0.0.34':
|
||||||
resolution: {integrity: sha512-XsfMn5rdxFMTyNuQjp97vJcQBeJIphZ+qtpVxcy4pABSk7B90Xf8nYCZ09OLTfcbql+flQ4P3C9HoDIi64ZMkA==}
|
resolution: {integrity: sha512-YXCwycqs2yByOPUMMjm2tf0BYUJfLR9D4bvHDv6xIbfKT5btT+hR1pujW5nVawXJDefYNsDfLy8dQ+IkMd21xQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@fastgpt-sdk/storage@0.6.15':
|
'@fastgpt-sdk/storage@0.6.15':
|
||||||
@@ -8801,8 +8801,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
|
resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
minimatch@10.2.4:
|
minimatch@10.2.5:
|
||||||
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
|
resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
|
||||||
engines: {node: 18 || 20 || >=22}
|
engines: {node: 18 || 20 || >=22}
|
||||||
|
|
||||||
minimatch@3.1.2:
|
minimatch@3.1.2:
|
||||||
@@ -13745,7 +13745,7 @@ snapshots:
|
|||||||
'@fortaine/fetch-event-source': 3.0.6
|
'@fortaine/fetch-event-source': 3.0.6
|
||||||
zod: 4.1.12
|
zod: 4.1.12
|
||||||
|
|
||||||
'@fastgpt-sdk/sandbox-adapter@0.0.33':
|
'@fastgpt-sdk/sandbox-adapter@0.0.34':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@e2b/code-interpreter': 2.4.0
|
'@e2b/code-interpreter': 2.4.0
|
||||||
|
|
||||||
@@ -19329,7 +19329,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
jackspeak: 4.2.3
|
jackspeak: 4.2.3
|
||||||
minimatch: 10.2.4
|
minimatch: 10.2.5
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 2.0.2
|
path-scurry: 2.0.2
|
||||||
@@ -21125,7 +21125,7 @@ snapshots:
|
|||||||
|
|
||||||
mimic-response@4.0.0: {}
|
mimic-response@4.0.0: {}
|
||||||
|
|
||||||
minimatch@10.2.4:
|
minimatch@10.2.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion: 5.0.5
|
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 { useSendCode } from '@/web/support/user/hooks/useSendCode';
|
||||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||||
|
import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
|
||||||
|
|
||||||
type FormType = {
|
type FormType = {
|
||||||
contact: string;
|
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
|
const placeholder = feConfigs?.bind_notification_method
|
||||||
?.map((item) => {
|
?.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);
|
res.setHeader('Content-Type', metadata.contentType);
|
||||||
}
|
}
|
||||||
if (metadata?.contentLength) {
|
if (metadata?.contentLength) {
|
||||||
res.setHeader('Content-Length', 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');
|
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
||||||
|
|
||||||
stream.pipe(res);
|
stream.pipe(res);
|
||||||
|
|||||||
Reference in New Issue
Block a user