Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docSite/assets/imgs/plugins/entry.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docSite/assets/imgs/plugins/file.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 11 additions & 2 deletions document/content/docs/introduction/guide/plugins/meta.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
title: 如何在线上传系统工具
description: FastGPT 系统工具在线上传指南
---

> 从 FastGPT 4.10.0 版本开始,系统管理员可以通过 Web 界面直接上传和更新系统工具,无需重新部署服务

## 权限要求

⚠️ **重要提示**:只有 **root 用户** 才能使用在线上传系统工具功能。

- 确保您已使用 `root` 账户登录 FastGPT
- 普通用户无法看到"导入/更新"按钮和删除功能

## 支持的文件格式

- **文件类型**:`.js` 文件
- **文件大小**:最大 10MB
- **文件数量**:每次只能上传一个文件

## 上传步骤

### 1. 进入系统工具页面

1. 登录 FastGPT 管理后台
2. 导航到:**工作台** → **系统工具**
3. 确认页面右上角显示"导入/更新"按钮(只有 root 用户可见)

![](/imgs/plugins/entry.png)

### 2. 准备工具文件

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

![](/imgs/plugins/file.png)

### 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 changes: 3 additions & 0 deletions packages/global/core/app/plugin/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<
Expand Down
1 change: 1 addition & 0 deletions packages/global/core/workflow/type/node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export type NodeTemplateListItemType = {
instructions?: string; // 使用说明
courseUrl?: string; // 教程链接
sourceMember?: SourceMember;
toolSource?: 'uploaded' | 'built-in'; // Plugin source type
};

export type NodeTemplateListType = {
Expand Down
56 changes: 56 additions & 0 deletions packages/service/common/file/plugin/config.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 107 additions & 0 deletions packages/service/common/file/plugin/controller.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 10 additions & 0 deletions packages/service/common/file/plugin/utils.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 27 additions & 0 deletions packages/service/common/minio/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading