mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-15 15:41:05 +00:00
Enhance file upload functionality and system tool integration (#5257)
* Enhance file upload functionality and system tool integration * Add supplementary documents and optimize the upload interface * Refactor file plugin types and update upload configurations * Refactor MinIO configuration variables and clean up API plugin handlers for improved readability and consistency * File name change * Refactor SystemTools component layout * fix i18n * fix * fix * fix
This commit is contained in:
BIN
docSite/assets/imgs/plugins/entry.png
Normal file
BIN
docSite/assets/imgs/plugins/entry.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 513 KiB |
BIN
docSite/assets/imgs/plugins/file.png
Normal file
BIN
docSite/assets/imgs/plugins/file.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 49 KiB |
@@ -1,5 +1,14 @@
|
||||
{
|
||||
"title": "系统插件",
|
||||
"description": "介绍如何使用和提交系统插件,以及各插件的填写说明",
|
||||
"pages": ["dev_system_tool","how_to_submit_system_plugin","searxng_plugin_guide","google_search_plugin_guide","bing_search_plugin","doc2x_plugin_guide"]
|
||||
"pages": [
|
||||
"dev_system_tool",
|
||||
"how_to_submit_system_plugin",
|
||||
"upload_system_tool",
|
||||
"searxng_plugin_guide",
|
||||
"google_search_plugin_guide",
|
||||
"bing_search_plugin",
|
||||
"doc2x_plugin_guide",
|
||||
"deepseek_plugin_guide"
|
||||
]
|
||||
}
|
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: 如何在线上传系统工具
|
||||
description: FastGPT 系统工具在线上传指南
|
||||
---
|
||||
|
||||
> 从 FastGPT 4.10.0 版本开始,系统管理员可以通过 Web 界面直接上传和更新系统工具,无需重新部署服务
|
||||
|
||||
## 权限要求
|
||||
|
||||
⚠️ **重要提示**:只有 **root 用户** 才能使用在线上传系统工具功能。
|
||||
|
||||
- 确保您已使用 `root` 账户登录 FastGPT
|
||||
- 普通用户无法看到"导入/更新"按钮和删除功能
|
||||
|
||||
## 支持的文件格式
|
||||
|
||||
- **文件类型**:`.js` 文件
|
||||
- **文件大小**:最大 10MB
|
||||
- **文件数量**:每次只能上传一个文件
|
||||
|
||||
## 上传步骤
|
||||
|
||||
### 1. 进入系统工具页面
|
||||
|
||||
1. 登录 FastGPT 管理后台
|
||||
2. 导航到:**工作台** → **系统工具**
|
||||
3. 确认页面右上角显示"导入/更新"按钮(只有 root 用户可见)
|
||||
|
||||

|
||||
|
||||
### 2. 准备工具文件
|
||||
|
||||
在上传之前,请确保您的 `.js` 文件是从 fastgpt-plugin 项目中通过 `bun run build` 命令打包后的 dist/tools/built-in 文件夹下得到的
|
||||
|
||||

|
||||
|
||||
### 3. 执行上传
|
||||
|
||||
1. 点击 **"导入/更新"** 按钮
|
||||
2. 在弹出的对话框中,点击文件选择区域
|
||||
3. 选择您准备好的 `.js` 工具文件
|
||||
4. 确认文件信息无误后,点击 **"确认导入"**
|
||||
|
||||
### 4. 上传过程
|
||||
|
||||
- 上传成功后会显示成功提示
|
||||
- 页面自动刷新,新工具会出现在工具列表中
|
||||
|
||||
## 功能特点
|
||||
|
||||
### 工具管理
|
||||
|
||||
- **查看工具**:所有用户都可以查看已安装的系统工具
|
||||
- **上传工具**:仅 root 用户可以上传新工具或更新现有工具
|
||||
- **删除工具**:仅 root 用户可以删除已上传的工具
|
||||
|
||||
### 工具类型识别
|
||||
|
||||
系统会根据工具的配置自动识别工具类型:
|
||||
|
||||
- 🔧 **工具 (tools)**
|
||||
- 🔍 **搜索 (search)**
|
||||
- 🎨 **多模态 (multimodal)**
|
||||
- 💬 **通讯 (communication)**
|
||||
- 📦 **其他 (other)**
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 上传失败,提示"文件内容存在错误"
|
||||
|
||||
**可能原因:**
|
||||
- fastgpt-plugin 项目不是最新的,导致打包的 `.js` 文件缺少正确的内容
|
||||
- 工具配置格式不正确
|
||||
|
||||
**解决方案:**
|
||||
1. 拉取最新的 fastgpt-plugin 项目重新进行 `bun run build` 获得打包后的 `.js` 文件
|
||||
2. 检查本地插件运行是否成功
|
||||
|
||||
### Q: 无法看到"导入/更新"按钮
|
||||
|
||||
**原因:** 当前用户不是 root 用户
|
||||
|
||||
**解决方案:** 使用 root 账户重新登录
|
||||
|
||||
### Q: 文件上传超时
|
||||
|
||||
**可能原因:**
|
||||
- 文件过大(超过 10MB)
|
||||
- 网络连接不稳定
|
||||
|
||||
**解决方案:**
|
||||
1. 确认文件大小在限制范围内
|
||||
2. 检查网络连接
|
||||
3. 尝试重新上传
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### 上传前检查
|
||||
|
||||
1. **代码测试**:在本地环境测试工具功能
|
||||
2. **格式验证**:确保符合 FastGPT 工具规范
|
||||
3. **文件大小**:保持文件在合理大小范围内
|
||||
|
||||
### 版本管理
|
||||
|
||||
- 建议为工具添加版本号注释
|
||||
- 更新工具时,先备份原有版本
|
||||
- 记录更新日志和功能变更
|
||||
|
||||
### 安全考虑
|
||||
|
||||
- 仅上传来源可信的工具文件
|
||||
- 避免包含敏感信息或凭据
|
||||
- 定期审查已安装的工具
|
||||
|
||||
### 存储方式
|
||||
|
||||
- 工具文件存储在 MinIO 中
|
||||
- 工具元数据保存在 MongoDB 中
|
||||
|
||||
---
|
||||
|
||||
通过在线上传功能,您可以快速部署和管理系统工具,提高 FastGPT 的扩展性和灵活性。如遇到问题,请参考上述常见问题或联系技术支持。
|
3
packages/global/core/app/plugin/type.d.ts
vendored
3
packages/global/core/app/plugin/type.d.ts
vendored
@@ -55,6 +55,9 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & {
|
||||
// Admin config
|
||||
inputList?: FlowNodeInputItemType['inputList'];
|
||||
hasSystemSecret?: boolean;
|
||||
|
||||
// Plugin source type
|
||||
toolSource?: 'uploaded' | 'built-in';
|
||||
};
|
||||
|
||||
export type SystemPluginTemplateListItemType = Omit<
|
||||
|
1
packages/global/core/workflow/type/node.d.ts
vendored
1
packages/global/core/workflow/type/node.d.ts
vendored
@@ -124,6 +124,7 @@ export type NodeTemplateListItemType = {
|
||||
instructions?: string; // 使用说明
|
||||
courseUrl?: string; // 教程链接
|
||||
sourceMember?: SourceMember;
|
||||
toolSource?: 'uploaded' | 'built-in'; // Plugin source type
|
||||
};
|
||||
|
||||
export type NodeTemplateListType = {
|
||||
|
56
packages/service/common/file/plugin/config.ts
Normal file
56
packages/service/common/file/plugin/config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { mimeTypes } from './utils';
|
||||
|
||||
export enum FilePluginTypeEnum {
|
||||
tool = 'tool'
|
||||
}
|
||||
|
||||
export enum PluginPathEnum {
|
||||
tools = 'plugin/tools'
|
||||
}
|
||||
|
||||
export const PLUGIN_TYPE_TO_PATH_MAP: Record<FilePluginTypeEnum, PluginPathEnum> = {
|
||||
[FilePluginTypeEnum.tool]: PluginPathEnum.tools
|
||||
};
|
||||
|
||||
export type UploadFileConfig = {
|
||||
maxFileSize: number;
|
||||
allowedExtensions?: string[];
|
||||
bucket: string;
|
||||
};
|
||||
|
||||
export const defaultUploadConfig: UploadFileConfig = {
|
||||
maxFileSize: process.env.UPLOAD_MAX_FILE_SIZE
|
||||
? parseInt(process.env.UPLOAD_MAX_FILE_SIZE)
|
||||
: 10 * 1024 * 1024,
|
||||
bucket: process.env.MINIO_UPLOAD_BUCKET || 'fastgpt-uploads',
|
||||
allowedExtensions: Object.keys(mimeTypes)
|
||||
};
|
||||
|
||||
export type FileMetadata = {
|
||||
fileId: string;
|
||||
originalFilename: string;
|
||||
contentType: string;
|
||||
size: number;
|
||||
uploadTime: Date;
|
||||
accessUrl: string;
|
||||
};
|
||||
|
||||
export type PresignedUrlInput = {
|
||||
filename: string;
|
||||
pluginType?: FilePluginTypeEnum;
|
||||
contentType?: string;
|
||||
metadata?: Record<string, string>;
|
||||
maxSize?: number;
|
||||
};
|
||||
|
||||
export type PresignedUrlResponse = {
|
||||
fileId: string;
|
||||
objectName: string;
|
||||
uploadUrl: string;
|
||||
formData: Record<string, string>;
|
||||
};
|
||||
|
||||
export type FileUploadInput = {
|
||||
buffer: Buffer;
|
||||
filename: string;
|
||||
};
|
107
packages/service/common/file/plugin/controller.ts
Normal file
107
packages/service/common/file/plugin/controller.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import {
|
||||
defaultUploadConfig,
|
||||
type UploadFileConfig,
|
||||
type PresignedUrlInput,
|
||||
type PresignedUrlResponse,
|
||||
FilePluginTypeEnum,
|
||||
PLUGIN_TYPE_TO_PATH_MAP
|
||||
} from './config';
|
||||
import { addLog } from '../../system/log';
|
||||
import { ensureBucket } from '../../minio/init';
|
||||
import { connectionMinio } from '../../minio/index';
|
||||
import { inferContentType } from './utils';
|
||||
|
||||
let globalConfig: UploadFileConfig = defaultUploadConfig;
|
||||
|
||||
export const initFileUploadService = async (config?: Partial<UploadFileConfig>) => {
|
||||
globalConfig = { ...defaultUploadConfig, ...config };
|
||||
|
||||
try {
|
||||
addLog.info(`Initializing upload bucket: ${globalConfig.bucket}`);
|
||||
await ensureBucket(globalConfig.bucket, true);
|
||||
addLog.info(`Upload bucket initialized successfully: ${globalConfig.bucket}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
addLog.error(`Failed to initialize upload bucket: ${globalConfig.bucket}`, error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const generateFileId = (): string => {
|
||||
return randomBytes(16).toString('hex');
|
||||
};
|
||||
|
||||
export const generateDownloadUrl = (objectName: string, config: UploadFileConfig): string => {
|
||||
const pathParts = objectName.split('/');
|
||||
const encodedParts = pathParts.map((part) => encodeURIComponent(part));
|
||||
const encodedObjectName = encodedParts.join('/');
|
||||
return `${config.bucket}/${encodedObjectName}`;
|
||||
};
|
||||
|
||||
//Generate a pre-signed URL for direct file upload
|
||||
export const generatePresignedUrl = async (
|
||||
input: PresignedUrlInput
|
||||
): Promise<PresignedUrlResponse> => {
|
||||
const currentConfig = { ...globalConfig };
|
||||
|
||||
const fileId = generateFileId();
|
||||
const pluginType = input.pluginType || FilePluginTypeEnum.tool;
|
||||
const pluginPath = PLUGIN_TYPE_TO_PATH_MAP[pluginType];
|
||||
const objectName = `${pluginPath}/${fileId}/${input.filename}`;
|
||||
const contentType = input.contentType || inferContentType(input.filename);
|
||||
const maxSize = input.maxSize || currentConfig.maxFileSize;
|
||||
|
||||
try {
|
||||
const policy = connectionMinio.newPostPolicy();
|
||||
|
||||
policy.setBucket(currentConfig.bucket);
|
||||
policy.setKey(objectName);
|
||||
policy.setContentType(contentType);
|
||||
policy.setContentLengthRange(1, maxSize);
|
||||
policy.setExpires(new Date(Date.now() + 10 * 60 * 1000));
|
||||
|
||||
const metadata = {
|
||||
'original-filename': encodeURIComponent(input.filename),
|
||||
'upload-time': new Date().toISOString(),
|
||||
'file-id': fileId,
|
||||
...input.metadata
|
||||
};
|
||||
policy.setUserMetaData(metadata);
|
||||
|
||||
const { postURL, formData } = await connectionMinio.presignedPostPolicy(policy);
|
||||
|
||||
const response: PresignedUrlResponse = {
|
||||
fileId,
|
||||
objectName,
|
||||
uploadUrl: postURL,
|
||||
formData
|
||||
};
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
addLog.error('Failed to generate presigned URL', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
return Promise.reject(`Failed to generate presigned URL: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
//Confirm the presigned URL upload is complete and save the file information to MongoDB
|
||||
export const confirmPresignedUpload = async (objectName: string, size: string): Promise<string> => {
|
||||
try {
|
||||
const currentConfig = { ...globalConfig };
|
||||
const stat = await connectionMinio.statObject(currentConfig.bucket, objectName);
|
||||
|
||||
if (stat.size !== Number(size)) {
|
||||
addLog.error(`File size mismatch. Expected: ${size}, Actual: ${stat.size}`);
|
||||
return Promise.reject(`File size mismatch. Expected: ${size}, Actual: ${stat.size}`);
|
||||
}
|
||||
|
||||
const accessUrl = generateDownloadUrl(objectName, currentConfig);
|
||||
|
||||
return accessUrl;
|
||||
} catch (error) {
|
||||
addLog.error('Failed to confirm presigned upload', error);
|
||||
return Promise.reject(`Failed to confirm presigned upload: ${error}`);
|
||||
}
|
||||
};
|
10
packages/service/common/file/plugin/utils.ts
Normal file
10
packages/service/common/file/plugin/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import * as path from 'path';
|
||||
|
||||
export const mimeTypes: Record<string, string> = {
|
||||
'.js': 'application/javascript'
|
||||
};
|
||||
|
||||
export const inferContentType = (filename: string): string => {
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
return mimeTypes[ext] || 'application/octet-stream';
|
||||
};
|
27
packages/service/common/minio/index.ts
Normal file
27
packages/service/common/minio/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Client } from 'minio';
|
||||
|
||||
export * from 'minio';
|
||||
export { Client };
|
||||
|
||||
export const S3_ENDPOINT = process.env.S3_ENDPOINT || 'localhost';
|
||||
export const S3_PORT = process.env.S3_PORT ? parseInt(process.env.S3_PORT) : 9000;
|
||||
export const S3_USE_SSL = process.env.S3_USE_SSL === 'true';
|
||||
export const S3_ACCESS_KEY = process.env.S3_ACCESS_KEY || 'minioadmin';
|
||||
export const S3_SECRET_KEY = process.env.S3_SECRET_KEY || 'minioadmin';
|
||||
|
||||
export const connectionMinio = (() => {
|
||||
if (!global.minioClient) {
|
||||
global.minioClient = new Client({
|
||||
endPoint: S3_ENDPOINT,
|
||||
port: S3_PORT,
|
||||
useSSL: S3_USE_SSL,
|
||||
accessKey: S3_ACCESS_KEY,
|
||||
secretKey: S3_SECRET_KEY
|
||||
});
|
||||
}
|
||||
return global.minioClient;
|
||||
})();
|
||||
|
||||
export const getMinioClient = () => connectionMinio;
|
||||
|
||||
export default connectionMinio;
|
39
packages/service/common/minio/init.ts
Normal file
39
packages/service/common/minio/init.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { connectionMinio } from './index';
|
||||
import { addLog } from '../system/log';
|
||||
import { retryFn } from '@fastgpt/global/common/system/utils';
|
||||
|
||||
export const ensureBucket = async (bucketName: string, isPublic: boolean = false) => {
|
||||
return retryFn(async () => {
|
||||
try {
|
||||
const bucketExists = await connectionMinio.bucketExists(bucketName);
|
||||
|
||||
if (!bucketExists) {
|
||||
addLog.info(`Creating bucket: ${bucketName}`);
|
||||
await connectionMinio.makeBucket(bucketName);
|
||||
}
|
||||
|
||||
if (isPublic) {
|
||||
// Set public read policy
|
||||
const policy = {
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Principal: '*',
|
||||
Action: ['s3:GetObject'],
|
||||
Resource: [`arn:aws:s3:::${bucketName}/*`]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await connectionMinio.setBucketPolicy(bucketName, JSON.stringify(policy));
|
||||
addLog.info(`Set public read policy for bucket: ${bucketName}`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
addLog.error(`Failed to ensure bucket ${bucketName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}, 3);
|
||||
};
|
5
packages/service/common/minio/type.d.ts
vendored
Normal file
5
packages/service/common/minio/type.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Client } from 'minio';
|
||||
|
||||
declare global {
|
||||
var minioClient: Client;
|
||||
}
|
@@ -391,7 +391,7 @@ function getCachedSystemPlugins() {
|
||||
return global.systemPlugins_cache;
|
||||
}
|
||||
|
||||
const cleanSystemPluginCache = () => {
|
||||
export const cleanSystemPluginCache = () => {
|
||||
global.systemPlugins_cache = undefined;
|
||||
};
|
||||
|
||||
@@ -439,22 +439,10 @@ export const getSystemPlugins = async (): Promise<SystemPluginTemplateItemType[]
|
||||
const inputs = versionList[0]?.inputs;
|
||||
|
||||
return {
|
||||
isActive: item.isActive,
|
||||
id: item.id,
|
||||
parentId: item.parentId,
|
||||
...item,
|
||||
isFolder: tools.some((tool) => tool.parentId === item.id),
|
||||
name: item.name,
|
||||
avatar: item.avatar,
|
||||
intro: item.intro,
|
||||
author: item.author,
|
||||
courseUrl: item.courseUrl,
|
||||
showStatus: true,
|
||||
weight: item.weight,
|
||||
templateType: item.templateType,
|
||||
originCost: item.originCost,
|
||||
currentCost: item.currentCost,
|
||||
hasTokenFee: item.hasTokenFee,
|
||||
pluginOrder: item.pluginOrder,
|
||||
toolSource: item.toolSource || 'built-in',
|
||||
|
||||
workflow: {
|
||||
nodes: [],
|
||||
|
@@ -29,6 +29,26 @@ export async function getSystemToolList() {
|
||||
return Promise.reject(res.body);
|
||||
}
|
||||
|
||||
export async function deleteSystemTool(toolId: string) {
|
||||
const res = await client.tool.delete({ body: { toolId } });
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.body;
|
||||
}
|
||||
|
||||
return Promise.reject(res.body);
|
||||
}
|
||||
|
||||
export async function uploadSystemTool(url: string) {
|
||||
const res = await client.tool.upload({ body: { url } });
|
||||
|
||||
if (res.status === 200) {
|
||||
return res.body;
|
||||
}
|
||||
|
||||
return Promise.reject(res.body);
|
||||
}
|
||||
|
||||
const runToolInstance = new RunToolWithStream({
|
||||
baseUrl: BASE_URL,
|
||||
token: TOKEN
|
||||
|
@@ -52,7 +52,8 @@
|
||||
"tiktoken": "1.0.17",
|
||||
"tunnel": "^0.0.6",
|
||||
"turndown": "^7.1.2",
|
||||
"winston": "^3.17.0"
|
||||
"winston": "^3.17.0",
|
||||
"minio": "^8.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.5.2",
|
||||
|
@@ -86,7 +86,7 @@ export const workflowNodeTemplateList: {
|
||||
export const defaultGroup: PluginGroupSchemaType = {
|
||||
groupId: 'systemPlugin',
|
||||
groupAvatar: 'core/app/type/pluginLight',
|
||||
groupName: i18nT('common:core.module.template.System Plugin'),
|
||||
groupName: i18nT('app:core.module.template.System Tools'),
|
||||
groupOrder: 0,
|
||||
groupTypes: systemPluginTemplateList
|
||||
};
|
||||
|
@@ -43,6 +43,7 @@
|
||||
"confirm_delete_folder_tip": "Confirm to delete this folder? All apps and corresponding conversation records under it will be deleted. Please confirm!",
|
||||
"copy_one_app": "Create Duplicate",
|
||||
"core.app.QG.Switch": "Enable guess what you want to ask",
|
||||
"core.module.template.System Tools": "System Tools",
|
||||
"core.dataset.import.Custom prompt": "Custom Prompt",
|
||||
"create_by_curl": "By CURL",
|
||||
"create_by_template": "By template",
|
||||
|
@@ -659,7 +659,6 @@
|
||||
"core.module.template.AI support tool tip": "Models that support function calls can better use tool calls.",
|
||||
"core.module.template.Basic Node": "Basic",
|
||||
"core.module.template.Query extension": "Question Optimization",
|
||||
"core.module.template.System Plugin": "System Plugin",
|
||||
"core.module.template.System input module": "System Input",
|
||||
"core.module.template.Team app": "Team",
|
||||
"core.module.template.UnKnow Module": "Unknown Module",
|
||||
@@ -1217,6 +1216,7 @@
|
||||
"support.wallet.usage.Total points": "AI Points Consumption",
|
||||
"support.wallet.usage.Usage Detail": "Usage Details",
|
||||
"support.wallet.usage.Whisper": "Voice Input",
|
||||
"sure_delete_tool_cannot_undo": "Are you sure to delete the tool? \nThis operation cannot be withdrawn",
|
||||
"sync_link": "Sync Link",
|
||||
"sync_success": "Synced Successfully",
|
||||
"system.Concat us": "Contact Us",
|
||||
|
@@ -13,11 +13,13 @@
|
||||
"Only_support_uploading_one_image": "Only support uploading one image",
|
||||
"Please select the image to upload": "Please select the image to upload",
|
||||
"Please wait for all files to upload": "Please wait for all files to be uploaded to complete",
|
||||
"common.upload_system_tools": "Upload system tools",
|
||||
"bucket_chat": "Conversation Files",
|
||||
"bucket_file": "Dataset Documents",
|
||||
"click_to_view_raw_source": "Click to View Original Source",
|
||||
"common.Some images failed to process": "Some images failed to process",
|
||||
"common.dataset_data_input_image_support_format": "Support .jpg, .jpeg, .png, .gif, .webp formats",
|
||||
"common.import_update": "Import/Update",
|
||||
"count.core.dataset.collection.Create Success": "{{count}} picture successfully imported",
|
||||
"delete_image": "Delete pictures",
|
||||
"file_name": "Filename",
|
||||
|
@@ -43,6 +43,7 @@
|
||||
"confirm_delete_folder_tip": "确认删除该文件夹?将会删除它下面所有应用及对应的聊天记录,请确认!",
|
||||
"copy_one_app": "创建副本",
|
||||
"core.app.QG.Switch": "启用猜你想问",
|
||||
"core.module.template.System Tools": "系统工具",
|
||||
"core.dataset.import.Custom prompt": "自定义提示词",
|
||||
"create_by_curl": "从 CURL 创建",
|
||||
"create_by_template": "从模板创建",
|
||||
|
@@ -659,7 +659,6 @@
|
||||
"core.module.template.AI support tool tip": "支持函数调用的模型,可以更好的使用工具调用。",
|
||||
"core.module.template.Basic Node": "基础功能",
|
||||
"core.module.template.Query extension": "问题优化",
|
||||
"core.module.template.System Plugin": "系统插件",
|
||||
"core.module.template.System input module": "系统输入",
|
||||
"core.module.template.Team app": "团队应用",
|
||||
"core.module.template.UnKnow Module": "未知模块",
|
||||
@@ -1218,6 +1217,7 @@
|
||||
"support.wallet.usage.Total points": "AI 积分消耗",
|
||||
"support.wallet.usage.Usage Detail": "使用详情",
|
||||
"support.wallet.usage.Whisper": "语音输入",
|
||||
"sure_delete_tool_cannot_undo": "是否确认删除该工具?该操作无法撤回",
|
||||
"sync_link": "同步链接",
|
||||
"sync_success": "同步成功",
|
||||
"system.Concat us": "联系我们",
|
||||
|
@@ -18,6 +18,8 @@
|
||||
"click_to_view_raw_source": "点击查看来源",
|
||||
"common.Some images failed to process": "部分图片处理失败",
|
||||
"common.dataset_data_input_image_support_format": "支持 .jpg, .jpeg, .png, .gif, .webp 格式",
|
||||
"common.import_update": "导入/更新",
|
||||
"common.upload_system_tools": "上传系统工具",
|
||||
"count.core.dataset.collection.Create Success": "成功导入 {{count}} 张图片",
|
||||
"delete_image": "删除图片",
|
||||
"file_name": "文件名",
|
||||
|
@@ -44,6 +44,7 @@
|
||||
"copy_one_app": "建立副本",
|
||||
"core.app.QG.Switch": "啟用猜你想問",
|
||||
"core.dataset.import.Custom prompt": "自訂提示詞",
|
||||
"core.module.template.System Tools": "系統工具",
|
||||
"create_by_curl": "從 CURL 建立",
|
||||
"create_by_template": "從範本建立",
|
||||
"create_copy_success": "建立副本成功",
|
||||
|
@@ -659,7 +659,6 @@
|
||||
"core.module.template.AI support tool tip": "支援函式呼叫的模型可以更好地使用工具呼叫。",
|
||||
"core.module.template.Basic Node": "基本功能",
|
||||
"core.module.template.Query extension": "問題最佳化",
|
||||
"core.module.template.System Plugin": "系統外掛",
|
||||
"core.module.template.System input module": "系統輸入模組",
|
||||
"core.module.template.Team app": "團隊應用程式",
|
||||
"core.module.template.UnKnow Module": "未知模組",
|
||||
@@ -1217,6 +1216,7 @@
|
||||
"support.wallet.usage.Total points": "AI 點數消耗",
|
||||
"support.wallet.usage.Usage Detail": "使用詳細資訊",
|
||||
"support.wallet.usage.Whisper": "語音輸入",
|
||||
"sure_delete_tool_cannot_undo": "是否確認刪除該工具?\n該操作無法撤回",
|
||||
"sync_link": "同步連結",
|
||||
"sync_success": "同步成功",
|
||||
"system.Concat us": "聯絡我們",
|
||||
|
@@ -13,11 +13,13 @@
|
||||
"Only_support_uploading_one_image": "僅支持上傳一張圖片",
|
||||
"Please select the image to upload": "請選擇要上傳的圖片",
|
||||
"Please wait for all files to upload": "請等待所有文件上傳完成",
|
||||
"common.upload_system_tools": "上傳系統工具",
|
||||
"bucket_chat": "對話檔案",
|
||||
"bucket_file": "知識庫檔案",
|
||||
"click_to_view_raw_source": "點選檢視原始來源",
|
||||
"common.Some images failed to process": "部分圖片處理失敗",
|
||||
"common.dataset_data_input_image_support_format": "支持 .jpg, .jpeg, .png, .gif, .webp 格式",
|
||||
"common.import_update": "導入/更新",
|
||||
"count.core.dataset.collection.Create Success": "成功導入 {{count}} 張圖片",
|
||||
"delete_image": "刪除圖片",
|
||||
"file_name": "檔案名稱",
|
||||
|
@@ -91,3 +91,10 @@ CONFIG_JSON_PATH=
|
||||
# Signoz
|
||||
SIGNOZ_BASE_URL=
|
||||
SIGNOZ_SERVICE_NAME=
|
||||
|
||||
# MINIO
|
||||
S3_ENDPOINT=localhost
|
||||
S3_PORT=9000
|
||||
S3_USE_SSL=false
|
||||
S3_ACCESS_KEY=minioadmin
|
||||
S3_SECRET_KEY=minioadmin
|
@@ -65,7 +65,8 @@
|
||||
"request-ip": "^3.3.0",
|
||||
"sass": "^1.58.3",
|
||||
"use-context-selector": "^1.4.4",
|
||||
"zod": "^3.24.2"
|
||||
"zod": "^3.24.2",
|
||||
"minio": "^8.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
|
@@ -2,28 +2,50 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { Box, Flex, HStack } from '@chakra-ui/react';
|
||||
import Avatar from '@fastgpt/web/components/common/Avatar';
|
||||
import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
|
||||
import { type NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node';
|
||||
import { type PluginGroupSchemaType } from '@fastgpt/service/core/app/plugin/type';
|
||||
import UseGuideModal from '@/components/common/Modal/UseGuideModal';
|
||||
|
||||
const PluginCard = ({
|
||||
item,
|
||||
groups
|
||||
groups,
|
||||
onDelete
|
||||
}: {
|
||||
item: NodeTemplateListItemType;
|
||||
groups: PluginGroupSchemaType[];
|
||||
onDelete?: (pluginId: string) => Promise<void>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const { openConfirm, ConfirmModal } = useConfirm({
|
||||
type: 'delete',
|
||||
content: t('common:sure_delete_tool_cannot_undo')
|
||||
});
|
||||
|
||||
const type = groups.reduce<string | undefined>((acc, group) => {
|
||||
const foundType = group.groupTypes.find((type) => type.typeId === item.templateType);
|
||||
return foundType ? foundType.typeName : acc;
|
||||
}, undefined);
|
||||
|
||||
const isUploadedPlugin = item.toolSource === 'uploaded';
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (onDelete && item.id) {
|
||||
await onDelete(item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteClick = () => {
|
||||
openConfirm(handleDelete)();
|
||||
};
|
||||
|
||||
return (
|
||||
<MyBox
|
||||
key={item.id}
|
||||
@@ -39,11 +61,31 @@ const PluginCard = ({
|
||||
position={'relative'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
_hover={{
|
||||
borderColor: 'primary.300',
|
||||
boxShadow: '1.5'
|
||||
}}
|
||||
>
|
||||
{/* Delete button with centered confirmation modal */}
|
||||
{isUploadedPlugin && (
|
||||
<MyIconButton
|
||||
icon="delete"
|
||||
position="absolute"
|
||||
bottom={3}
|
||||
right={4}
|
||||
color="blue.500"
|
||||
aria-label={t('common:Delete')}
|
||||
zIndex={1}
|
||||
opacity={isHovered ? 1 : 0}
|
||||
pointerEvents={isHovered ? 'auto' : 'none'}
|
||||
onClick={handleDeleteClick}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ConfirmModal />
|
||||
|
||||
<HStack>
|
||||
<Avatar src={item.avatar} borderRadius={'sm'} w={'1.5rem'} h={'1.5rem'} />
|
||||
<Box flex={'1 0 0'} color={'myGray.900'} fontWeight={500}>
|
||||
@@ -102,7 +144,10 @@ const PluginCard = ({
|
||||
</UseGuideModal>
|
||||
)}
|
||||
</Flex>
|
||||
<Box color={'myGray.500'}>{`by ${item.author || feConfigs.systemTitle}`}</Box>
|
||||
{/* Hide author info when showing delete button but maintain space */}
|
||||
<Box color={'myGray.500'} visibility={isUploadedPlugin && isHovered ? 'hidden' : 'visible'}>
|
||||
{`by ${item.author || feConfigs.systemTitle}`}
|
||||
</Box>
|
||||
</Flex>
|
||||
</MyBox>
|
||||
);
|
||||
|
@@ -0,0 +1,25 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { confirmPresignedUpload } from '@fastgpt/service/common/file/plugin/controller';
|
||||
|
||||
type RequestBody = {
|
||||
objectName: string;
|
||||
size: string;
|
||||
};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const { objectName, size }: RequestBody = req.body;
|
||||
|
||||
// Verify file upload and get access URL
|
||||
const accessUrl = await confirmPresignedUpload(objectName, size);
|
||||
|
||||
return accessUrl;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: true
|
||||
}
|
||||
};
|
@@ -0,0 +1,38 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import {
|
||||
generatePresignedUrl,
|
||||
initFileUploadService
|
||||
} from '@fastgpt/service/common/file/plugin/controller';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
|
||||
type RequestBody = {
|
||||
filename: string;
|
||||
contentType?: string;
|
||||
metadata?: Record<string, string>;
|
||||
maxSize?: number;
|
||||
expires?: number;
|
||||
};
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
await initFileUploadService({
|
||||
bucket: 'fastgpt-uploads',
|
||||
allowedExtensions: ['.js']
|
||||
});
|
||||
|
||||
const { filename, contentType, metadata, maxSize }: RequestBody = req.body;
|
||||
|
||||
if (!filename) {
|
||||
return Promise.reject('Filename is required');
|
||||
}
|
||||
|
||||
const presignedData = await generatePresignedUrl({
|
||||
filename,
|
||||
contentType,
|
||||
metadata,
|
||||
maxSize: maxSize || 10 * 1024 * 1024
|
||||
});
|
||||
|
||||
return presignedData;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
@@ -13,7 +13,7 @@ import { getSystemPlugins } from '@fastgpt/service/core/app/plugin/controller';
|
||||
|
||||
export type GetSystemPluginTemplatesBody = {
|
||||
searchKey?: string;
|
||||
parentId: ParentIdType;
|
||||
parentId?: ParentIdType;
|
||||
};
|
||||
|
||||
async function handler(
|
||||
@@ -35,7 +35,8 @@ async function handler(
|
||||
templateType: plugin.templateType ?? FlowNodeTemplateTypeEnum.other,
|
||||
flowNodeType: FlowNodeTypeEnum.tool,
|
||||
name: parseI18nString(plugin.name, lang),
|
||||
intro: parseI18nString(plugin.intro ?? '', lang)
|
||||
intro: parseI18nString(plugin.intro ?? '', lang),
|
||||
toolSource: plugin.toolSource
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (searchKey) {
|
||||
|
28
projects/app/src/pages/api/plugin/delete.ts
Normal file
28
projects/app/src/pages/api/plugin/delete.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { deleteSystemTool } from '@fastgpt/service/core/app/tool/api';
|
||||
import { cleanSystemPluginCache } from '@fastgpt/service/core/app/plugin/controller';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const toolId = (req.query.toolId as string) || req.body?.toolId;
|
||||
|
||||
if (!toolId) {
|
||||
return Promise.reject('ToolId is required');
|
||||
}
|
||||
|
||||
const actualToolId = toolId.includes('-') ? toolId.split('-').slice(1).join('-') : toolId;
|
||||
|
||||
const result = await deleteSystemTool(actualToolId);
|
||||
|
||||
cleanSystemPluginCache();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: true
|
||||
}
|
||||
};
|
26
projects/app/src/pages/api/plugin/upload.ts
Normal file
26
projects/app/src/pages/api/plugin/upload.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { NextAPI } from '@/service/middleware/entry';
|
||||
import { uploadSystemTool } from '@fastgpt/service/core/app/tool/api';
|
||||
import { cleanSystemPluginCache } from '@fastgpt/service/core/app/plugin/controller';
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
const { url } = req.body;
|
||||
|
||||
if (!url) {
|
||||
return Promise.reject('URL is required');
|
||||
}
|
||||
|
||||
const result = await uploadSystemTool(url);
|
||||
|
||||
cleanSystemPluginCache();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default NextAPI(handler);
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: true
|
||||
}
|
||||
};
|
@@ -1,9 +1,29 @@
|
||||
'use client';
|
||||
import DashboardContainer from '@/pageComponents/dashboard/Container';
|
||||
|
||||
import {
|
||||
Button,
|
||||
useDisclosure,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
VStack,
|
||||
HStack,
|
||||
Link
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
|
||||
import FileSelector, {
|
||||
type SelectFileItemType
|
||||
} from '@/pageComponents/dataset/detail/components/FileSelector';
|
||||
import PluginCard from '@/pageComponents/dashboard/SystemPlugin/ToolCard';
|
||||
import { serviceSideProps } from '@/web/common/i18n/utils';
|
||||
import { getSystemPlugTemplates } from '@/web/core/app/api/plugin';
|
||||
import {
|
||||
postUploadFileAndUrl,
|
||||
postPresignedUrl,
|
||||
postConfirmUpload,
|
||||
postS3UploadFile,
|
||||
postDeletePlugin
|
||||
} from '@/web/common/file/api';
|
||||
import { Box, Flex, Grid } from '@chakra-ui/react';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -13,18 +33,118 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
|
||||
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
|
||||
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import MyModal from '@fastgpt/web/components/common/MyModal';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { useUserStore } from '@/web/support/user/useUserStore';
|
||||
import { getDocPath } from '@/web/common/system/doc';
|
||||
|
||||
const SystemTools = () => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToast();
|
||||
const router = useRouter();
|
||||
const { type, pluginGroupId } = router.query as { type?: string; pluginGroupId?: string };
|
||||
const { isPc } = useSystem();
|
||||
const { userInfo } = useUserStore();
|
||||
|
||||
const isRoot = userInfo?.username === 'root';
|
||||
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [selectFiles, setSelectFiles] = useState<SelectFileItemType[]>([]);
|
||||
const [deletingPlugins, setDeletingPlugins] = useState<Set<string>>(new Set());
|
||||
|
||||
const { data: plugins = [], loading: isLoading } = useRequest2(getSystemPlugTemplates, {
|
||||
const {
|
||||
data: plugins = [],
|
||||
loading: isLoading,
|
||||
runAsync: refreshPlugins
|
||||
// refreshAsync: refreshPlugins
|
||||
} = useRequest2(getSystemPlugTemplates, {
|
||||
manual: false
|
||||
});
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const handleCloseUploadModal = () => {
|
||||
setSelectFiles([]);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const { run: handlePluginUpload, loading: uploadLoading } = useRequest2(
|
||||
async () => {
|
||||
const file = selectFiles[0];
|
||||
|
||||
const presignedData = await postPresignedUrl({
|
||||
filename: file.name,
|
||||
contentType: file.file.type,
|
||||
metadata: {
|
||||
size: String(file.file.size)
|
||||
}
|
||||
});
|
||||
|
||||
const formData = new FormData();
|
||||
Object.entries(presignedData.formData).forEach(([key, value]) => {
|
||||
formData.append(key, value);
|
||||
});
|
||||
formData.append('file', file.file);
|
||||
|
||||
await postS3UploadFile(presignedData.uploadUrl, formData, (progress) => {
|
||||
console.log('Upload progress:', progress);
|
||||
});
|
||||
|
||||
const fileUrl = await postConfirmUpload({
|
||||
objectName: presignedData.objectName,
|
||||
size: String(file.file.size)
|
||||
});
|
||||
|
||||
await postUploadFileAndUrl(fileUrl);
|
||||
await refreshPlugins({ parentId: null });
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: async () => {
|
||||
toast({
|
||||
title: t('common:import_success'),
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
setSelectFiles([]);
|
||||
onClose();
|
||||
// null means all tools
|
||||
},
|
||||
onError: (error) => {
|
||||
toast({
|
||||
title: t('common:import_failed'),
|
||||
description: error instanceof Error ? error.message : t('dataset:common.error.unKnow'),
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const handlePluginDelete = async (pluginId: string) => {
|
||||
setDeletingPlugins((prev) => new Set(prev).add(pluginId));
|
||||
|
||||
try {
|
||||
await postDeletePlugin(pluginId);
|
||||
toast({
|
||||
title: t('common:delete_success'),
|
||||
status: 'success'
|
||||
});
|
||||
|
||||
// null means all tools
|
||||
await refreshPlugins({ parentId: null });
|
||||
} catch (error) {
|
||||
Promise.reject(error);
|
||||
toast({
|
||||
title: t('common:delete_failed'),
|
||||
status: 'error'
|
||||
});
|
||||
} finally {
|
||||
setDeletingPlugins((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
newSet.delete(pluginId);
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const currentPlugins = useMemo(() => {
|
||||
return plugins
|
||||
@@ -59,17 +179,18 @@ const SystemTools = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBox isLoading={isLoading} h={'100%'}>
|
||||
<Box p={6} h={'100%'} overflowY={'auto'}>
|
||||
<Flex alignItems={'center'} justifyContent={'space-between'}>
|
||||
{isPc ? (
|
||||
<Box fontSize={'lg'} color={'myGray.900'} fontWeight={500}>
|
||||
{t('common:core.module.template.System Plugin')}
|
||||
{t('app:core.module.template.System Tools')}
|
||||
</Box>
|
||||
) : (
|
||||
MenuIcon
|
||||
)}
|
||||
|
||||
<Flex alignItems={'center'}>
|
||||
<Box flex={'0 0 200px'}>
|
||||
<SearchInput
|
||||
value={searchKey}
|
||||
@@ -77,6 +198,8 @@ const SystemTools = () => {
|
||||
placeholder={t('common:plugin.Search plugin')}
|
||||
/>
|
||||
</Box>
|
||||
{isRoot && <Button onClick={onOpen}>{t('file:common:import_update')}</Button>}
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Grid
|
||||
gridTemplateColumns={[
|
||||
@@ -91,12 +214,84 @@ const SystemTools = () => {
|
||||
py={5}
|
||||
>
|
||||
{filterPluginsByGroup.map((item) => (
|
||||
<PluginCard key={item.id} item={item} groups={pluginGroups} />
|
||||
<PluginCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
groups={pluginGroups}
|
||||
onDelete={isRoot ? handlePluginDelete : undefined}
|
||||
/>
|
||||
))}
|
||||
</Grid>
|
||||
{filterPluginsByGroup.length === 0 && <EmptyTip />}
|
||||
</Box>
|
||||
</MyBox>
|
||||
|
||||
<MyModal
|
||||
title={t('file:common.upload_system_tools')}
|
||||
isOpen={isOpen}
|
||||
onClose={handleCloseUploadModal}
|
||||
iconSrc="core/app/type/plugin"
|
||||
iconColor={'primary.600'}
|
||||
h={'auto'}
|
||||
>
|
||||
<ModalBody>
|
||||
<Flex justifyContent={'flex-end'} mb={3} fontSize={'sm'} fontWeight={500}>
|
||||
<Link
|
||||
display={'flex'}
|
||||
alignItems={'center'}
|
||||
gap={0.5}
|
||||
href={getDocPath('/docs/guide/plugins/upload_system_tool/')}
|
||||
color="primary.600"
|
||||
target="_blank"
|
||||
>
|
||||
<MyIcon name={'book'} w={'18px'} />
|
||||
{t('common:Instructions')}
|
||||
</Link>
|
||||
</Flex>
|
||||
<FileSelector
|
||||
maxCount={1}
|
||||
maxSize="10MB"
|
||||
fileType=".js"
|
||||
selectFiles={selectFiles}
|
||||
setSelectFiles={setSelectFiles}
|
||||
/>
|
||||
{/* File render */}
|
||||
{selectFiles.length > 0 && (
|
||||
<VStack mt={4} gap={2}>
|
||||
{selectFiles.map((item, index) => (
|
||||
<HStack key={index} w={'100%'}>
|
||||
<MyIcon name={item.icon as any} w={'1rem'} />
|
||||
<Box color={'myGray.900'}>{item.name}</Box>
|
||||
<Box fontSize={'xs'} color={'myGray.500'} flex={1}>
|
||||
{item.size}
|
||||
</Box>
|
||||
<MyIconButton
|
||||
icon="delete"
|
||||
hoverColor="red.500"
|
||||
hoverBg="red.50"
|
||||
onClick={() => {
|
||||
setSelectFiles(selectFiles.filter((_, i) => i !== index));
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="whiteBase" mr={2} onClick={handleCloseUploadModal}>
|
||||
{t('common:Close')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePluginUpload}
|
||||
isDisabled={selectFiles.length === 0}
|
||||
isLoading={uploadLoading}
|
||||
>
|
||||
{t('common:comfirm_import')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</MyModal>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</DashboardContainer>
|
||||
@@ -108,7 +303,7 @@ export default SystemTools;
|
||||
export async function getServerSideProps(content: any) {
|
||||
return {
|
||||
props: {
|
||||
...(await serviceSideProps(content, ['app']))
|
||||
...(await serviceSideProps(content, ['app', 'file']))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { GET, POST } from '@/web/common/api/request';
|
||||
import { DELETE, GET, POST } from '@/web/common/api/request';
|
||||
import type { UploadImgProps } from '@fastgpt/global/common/file/api.d';
|
||||
import { type AxiosProgressEvent } from 'axios';
|
||||
import type { PresignedUrlResponse } from '@fastgpt/service/common/file/plugin/config';
|
||||
|
||||
export const postUploadImg = (e: UploadImgProps) => POST<string>('/common/file/uploadImage', e);
|
||||
|
||||
@@ -18,3 +19,36 @@ export const postUploadFiles = (
|
||||
'Content-Type': 'multipart/form-data; charset=utf-8'
|
||||
}
|
||||
});
|
||||
|
||||
export const postS3UploadFile = (
|
||||
postURL: string,
|
||||
form: FormData,
|
||||
onUploadProgress: (progressEvent: AxiosProgressEvent) => void
|
||||
) =>
|
||||
POST(postURL, form, {
|
||||
timeout: 600000,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
onUploadProgress
|
||||
});
|
||||
|
||||
export const postPresignedUrl = (data: {
|
||||
filename: string;
|
||||
contentType?: string;
|
||||
metadata?: Record<string, string>;
|
||||
maxSize?: number;
|
||||
}) => POST<PresignedUrlResponse>('/common/file/plugin/presignedUrl', data);
|
||||
|
||||
export const postConfirmUpload = (data: { objectName: string; size: string }) =>
|
||||
POST<string>('/common/file/plugin/confirmUpload', data);
|
||||
|
||||
export const postUploadFileAndUrl = (url: string) =>
|
||||
POST<void>('/plugin/upload', {
|
||||
url: url
|
||||
});
|
||||
|
||||
export const postDeletePlugin = (toolId: string) =>
|
||||
DELETE<void>('/plugin/delete', {
|
||||
toolId
|
||||
});
|
||||
|
Reference in New Issue
Block a user