Commit b8846313 by Archer Committed by GitHub

feat: sandbox readfile tool (#6679)

* feat: sandbox readfile tool

* perf: read stream

* fix: schema name

* update sdk version

* udpate enum

* perf: time
parent 3f4400a5
...@@ -53,7 +53,7 @@ type UserChatItemValueItemType = { ...@@ -53,7 +53,7 @@ type UserChatItemValueItemType = {
## 三、S3 存储方案 ## 三、S3 存储方案
### 核心选择:`S3ChatSource.uploadChatFileByBuffer()` ### 核心选择:`S3ChatSource.uploadChatFile()`
| 字段 | 值 | | 字段 | 值 |
|------|----| |------|----|
...@@ -66,9 +66,9 @@ type UserChatItemValueItemType = { ...@@ -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) { ...@@ -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: { ...@@ -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 更新说明' ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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 = { ...@@ -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 = { ...@@ -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 = { ...@@ -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 小时)`;
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 { ...@@ -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< ...@@ -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'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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
}); });
} }
......
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({ ...@@ -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>;
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({ ...@@ -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({ ...@@ -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: {
......
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'; ...@@ -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'; ...@@ -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 ({ ...@@ -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(
......
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 ({ ...@@ -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);
try {
const sandboxInstance = await getSandboxClient({
appId, appId,
userId, userId,
chatId 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, response,
usages: [], usages: [],
nodeResponse: { nodeResponse: buildNodeResponse({
nodeId,
id: nodeId,
moduleType: FlowNodeTypeEnum.tool,
moduleName,
moduleLogo: SANDBOX_ICON,
toolId: SANDBOX_TOOL_NAME, toolId: SANDBOX_TOOL_NAME,
toolInput: { command, timeout }, input,
toolRes: response, response,
totalPoints: 0, durationSeconds,
runningTime: +((Date.now() - startTime) / 1000).toFixed(2) lang
} })
}; };
} catch (error) { };
getLogger(LogCategories.MODULE.AI.AGENT).error('[Sandbox Shell] Execution failed', { error });
const errorResponse = JSON.stringify({ export const dispatchSandboxGetFileUrl = async ({
stdout: '', paths,
stderr: getErrText(error), appId,
exitCode: -1 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 { 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}} ...@@ -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 = ({ ...@@ -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'; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 instance = await getSandboxClient({ const { input, response, durationSeconds } = await callSandboxTool({
toolName: call.function.name,
rawArgs: call.function.arguments ?? '',
appId: String(workflowProps.runningAppInfo.id), appId: String(workflowProps.runningAppInfo.id),
userId: String(workflowProps.uid), userId: String(workflowProps.uid),
chatId: workflowProps.chatId chatId: workflowProps.chatId
}); });
const result = await instance.exec(params.command, params.timeout);
const stringToolResponse = JSON.stringify({
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode
});
const flowResponse = getSandboxToolWorkflowResponse({ const flowResponse = getSandboxToolWorkflowResponse({
name: tool.name, name: tool.name,
logo: SANDBOX_ICON, logo: SANDBOX_ICON,
input: params, toolId: call.function.name,
response: stringToolResponse, input,
durationSeconds: +((Date.now() - startTime) / 1000).toFixed(2) response,
durationSeconds
}); });
return { return { response, flowResponse };
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 @@ ...@@ -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 = ({ ...@@ -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 = ({ ...@@ -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 && (
......
...@@ -250,8 +250,8 @@ importers: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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'; ...@@ -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 = ({ ...@@ -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) ...@@ -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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment