Skip to content

Commit a0ab301

Browse files
committed
Enhance file upload functionality and system tool integration
1 parent 59e2b2f commit a0ab301

File tree

23 files changed

+855
-67
lines changed

23 files changed

+855
-67
lines changed

packages/global/core/app/plugin/type.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ export type SystemPluginTemplateItemType = WorkflowTemplateType & {
5252
// Admin config
5353
inputList?: FlowNodeInputItemType['inputList'];
5454
hasSystemSecret?: boolean;
55+
56+
// Plugin source type
57+
toolSource?: 'uploaded' | 'built-in';
5558
};
5659

5760
export type SystemPluginTemplateListItemType = Omit<

packages/global/core/workflow/type/node.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export type NodeTemplateListItemType = {
123123
instructions?: string; // 使用说明
124124
courseUrl?: string; // 教程链接
125125
sourceMember?: SourceMember;
126+
toolSource?: 'uploaded' | 'built-in'; // Plugin source type
126127
};
127128

128129
export type NodeTemplateListType = {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { mimeTypes } from './utils';
2+
3+
// 插件类型枚举
4+
export enum PluginTypeEnum {
5+
tool = 'tool'
6+
}
7+
8+
// 插件路径枚举
9+
export enum PluginPathEnum {
10+
tools = 'plugin/tools'
11+
}
12+
13+
// 插件类型到路径的映射
14+
export const PLUGIN_TYPE_TO_PATH_MAP: Record<PluginTypeEnum, PluginPathEnum> = {
15+
[PluginTypeEnum.tool]: PluginPathEnum.tools
16+
};
17+
18+
export type UploadFileConfig = {
19+
maxFileSize: number; // 文件大小限制(字节)
20+
allowedExtensions?: string[]; // 允许的文件扩展名
21+
bucket: string; // 存储桶名称
22+
};
23+
24+
// 默认配置
25+
export const defaultUploadConfig: UploadFileConfig = {
26+
maxFileSize: process.env.UPLOAD_MAX_FILE_SIZE
27+
? parseInt(process.env.UPLOAD_MAX_FILE_SIZE)
28+
: 100 * 1024 * 1024, // 默认 100MB
29+
bucket: process.env.MINIO_UPLOAD_BUCKET || 'fastgpt-uploads',
30+
allowedExtensions: Object.keys(mimeTypes) // 从 mimeTypes 映射表中获取支持的扩展名
31+
};
32+
33+
export type FileMetadata = {
34+
fileId: string;
35+
originalFilename: string;
36+
contentType: string;
37+
size: number;
38+
uploadTime: Date;
39+
accessUrl: string;
40+
};
41+
42+
// 预签名URL请求输入类型
43+
export type PresignedUrlInput = {
44+
filename: string;
45+
pluginType?: PluginTypeEnum; // 插件类型,默认为 tool
46+
contentType?: string;
47+
metadata?: Record<string, string>;
48+
maxSize?: number;
49+
};
50+
51+
// 预签名URL响应类型
52+
export type PresignedUrlResponse = {
53+
fileId: string;
54+
objectName: string;
55+
uploadUrl: string;
56+
formData: Record<string, string>;
57+
};
58+
59+
// 保留原有类型以兼容现有代码
60+
export type FileUploadInput = {
61+
buffer: Buffer;
62+
filename: string;
63+
};
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { randomBytes } from 'crypto';
2+
import {
3+
defaultUploadConfig,
4+
type UploadFileConfig,
5+
type PresignedUrlInput,
6+
type PresignedUrlResponse,
7+
PluginTypeEnum,
8+
PLUGIN_TYPE_TO_PATH_MAP
9+
} from './config';
10+
import { addLog } from '../../system/log';
11+
import { ensureBucket } from '../../minio/init';
12+
import * as path from 'path';
13+
import { connectionMinio } from '../../minio/index';
14+
import { MongoFastGPTPlugin } from './schema';
15+
import { inferContentType } from './utils';
16+
17+
let globalConfig: UploadFileConfig = defaultUploadConfig;
18+
19+
export const initFileUploadService = async (config?: Partial<UploadFileConfig>) => {
20+
globalConfig = { ...defaultUploadConfig, ...config };
21+
22+
try {
23+
addLog.info(`Initializing upload bucket: ${globalConfig.bucket}`);
24+
await ensureBucket(globalConfig.bucket, true);
25+
addLog.info(`Upload bucket initialized successfully: ${globalConfig.bucket}`);
26+
return true;
27+
} catch (error) {
28+
addLog.error(`Failed to initialize upload bucket: ${globalConfig.bucket}`, error);
29+
throw error;
30+
}
31+
};
32+
33+
const generateFileId = (): string => {
34+
return randomBytes(16).toString('hex');
35+
};
36+
37+
export const generateDownloadUrl = (objectName: string, config: UploadFileConfig): string => {
38+
const pathParts = objectName.split('/');
39+
const encodedParts = pathParts.map((part) => encodeURIComponent(part));
40+
const encodedObjectName = encodedParts.join('/');
41+
return `${config.bucket}/${encodedObjectName}`;
42+
};
43+
44+
const validatePresignedInput = (input: PresignedUrlInput, config: UploadFileConfig): void => {
45+
if (!input.filename) {
46+
throw new Error('Filename is required');
47+
}
48+
49+
const maxSize = input.maxSize || config.maxFileSize;
50+
if (maxSize > config.maxFileSize) {
51+
throw new Error(`Max size exceeds limit of ${config.maxFileSize} bytes`);
52+
}
53+
54+
const ext = path.extname(input.filename).toLowerCase();
55+
if (config.allowedExtensions && !config.allowedExtensions.includes(ext)) {
56+
throw new Error(
57+
`File extension ${ext} is not allowed. Supported extensions: ${config.allowedExtensions.join(', ')}`
58+
);
59+
}
60+
};
61+
62+
//Generate a pre-signed URL for direct file upload
63+
export const generatePresignedUrl = async (
64+
input: PresignedUrlInput
65+
): Promise<PresignedUrlResponse> => {
66+
const currentConfig = { ...globalConfig };
67+
68+
validatePresignedInput(input, currentConfig);
69+
70+
const fileId = generateFileId();
71+
const pluginType = input.pluginType || PluginTypeEnum.tool;
72+
const pluginPath = PLUGIN_TYPE_TO_PATH_MAP[pluginType];
73+
const objectName = `${pluginPath}/${fileId}/${input.filename}`;
74+
const contentType = input.contentType || inferContentType(input.filename);
75+
const maxSize = input.maxSize || currentConfig.maxFileSize;
76+
77+
try {
78+
const policy = connectionMinio.newPostPolicy();
79+
policy.setBucket(currentConfig.bucket);
80+
policy.setKey(objectName);
81+
82+
policy.setContentType(contentType);
83+
84+
policy.setContentLengthRange(1, maxSize);
85+
86+
policy.setExpires(new Date(Date.now() + 10 * 60 * 1000));
87+
88+
const metadata = {
89+
'original-filename': encodeURIComponent(input.filename),
90+
'upload-time': new Date().toISOString(),
91+
'file-id': fileId,
92+
...input.metadata
93+
};
94+
policy.setUserMetaData(metadata);
95+
96+
const { postURL, formData } = await connectionMinio.presignedPostPolicy(policy);
97+
98+
const response: PresignedUrlResponse = {
99+
fileId,
100+
objectName,
101+
uploadUrl: postURL,
102+
formData
103+
};
104+
105+
console.log('Generated presigned URL successfully', response);
106+
107+
return response;
108+
} catch (error) {
109+
addLog.error('Failed to generate presigned URL', error);
110+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
111+
return Promise.reject(`Failed to generate presigned URL: ${errorMessage}`);
112+
}
113+
};
114+
115+
//Confirm the presigned URL upload is complete and save the file information to MongoDB
116+
export const confirmPresignedUpload = async (objectName: string, size: string): Promise<string> => {
117+
try {
118+
const currentConfig = { ...globalConfig };
119+
const stat = await connectionMinio.statObject(currentConfig.bucket, objectName);
120+
121+
if (stat.size !== Number(size)) {
122+
return Promise.reject(`File size mismatch. Expected: ${size}, Actual: ${stat.size}`);
123+
}
124+
125+
const accessUrl = generateDownloadUrl(objectName, currentConfig);
126+
127+
const pluginData = {
128+
url: accessUrl,
129+
type: 'tool' as const
130+
};
131+
132+
const result = await MongoFastGPTPlugin.create(pluginData);
133+
134+
return result.url;
135+
} catch (error) {
136+
addLog.error('Failed to confirm presigned upload', error);
137+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
138+
return Promise.reject(`Failed to confirm presigned upload: ${errorMessage}`);
139+
}
140+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Schema, getMongoModel } from '../../mongo';
2+
3+
export type FastGPTPluginSchema = {
4+
toolId?: string;
5+
type: 'tool';
6+
url: string;
7+
};
8+
9+
const collectionName = 'fastgpt_plugins';
10+
11+
const FastGPTPluginSchema = new Schema({
12+
toolId: {
13+
type: String,
14+
required: false
15+
},
16+
type: {
17+
type: String,
18+
required: true,
19+
enum: ['tool'],
20+
default: 'tool'
21+
},
22+
url: {
23+
type: String,
24+
required: true
25+
}
26+
});
27+
28+
export const MongoFastGPTPlugin = getMongoModel<FastGPTPluginSchema>(
29+
collectionName,
30+
FastGPTPluginSchema
31+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as path from 'path';
2+
3+
export const mimeTypes: Record<string, string> = {
4+
'.js': 'application/javascript'
5+
};
6+
7+
export const inferContentType = (filename: string): string => {
8+
const ext = path.extname(filename).toLowerCase();
9+
return mimeTypes[ext] || 'application/octet-stream';
10+
};
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Client } from 'minio';
2+
3+
export * from 'minio';
4+
export { Client };
5+
6+
export const MINIO_ENDPOINT = process.env.MINIO_ENDPOINT || 'localhost';
7+
export const MINIO_PORT = process.env.MINIO_PORT ? parseInt(process.env.MINIO_PORT) : 9000;
8+
export const MINIO_USE_SSL = process.env.MINIO_USE_SSL === 'true';
9+
export const MINIO_ACCESS_KEY = process.env.MINIO_ACCESS_KEY || 'minioadmin';
10+
export const MINIO_SECRET_KEY = process.env.MINIO_SECRET_KEY || 'minioadmin';
11+
12+
export const connectionMinio = (() => {
13+
if (!global.minioClient) {
14+
global.minioClient = new Client({
15+
endPoint: MINIO_ENDPOINT,
16+
port: MINIO_PORT,
17+
useSSL: MINIO_USE_SSL,
18+
accessKey: MINIO_ACCESS_KEY,
19+
secretKey: MINIO_SECRET_KEY
20+
});
21+
}
22+
return global.minioClient;
23+
})();
24+
25+
export const getMinioClient = () => connectionMinio;
26+
27+
export default connectionMinio;

packages/service/common/minio/init.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { connectionMinio } from './index';
2+
import { addLog } from '../system/log';
3+
import { retryFn } from '@fastgpt/global/common/system/utils';
4+
5+
export const initMinio = async () => {
6+
try {
7+
addLog.info('Connecting to MinIO...');
8+
9+
// Test connection by listing buckets
10+
await connectionMinio.listBuckets();
11+
12+
addLog.info('MinIO connected successfully');
13+
return true;
14+
} catch (error) {
15+
addLog.error('Failed to connect to MinIO:', error);
16+
return false;
17+
}
18+
};
19+
20+
export const ensureBucket = async (bucketName: string, isPublic: boolean = false) => {
21+
return retryFn(async () => {
22+
try {
23+
const bucketExists = await connectionMinio.bucketExists(bucketName);
24+
25+
if (!bucketExists) {
26+
addLog.info(`Creating bucket: ${bucketName}`);
27+
await connectionMinio.makeBucket(bucketName);
28+
}
29+
30+
if (isPublic) {
31+
// Set public read policy
32+
const policy = {
33+
Version: '2012-10-17',
34+
Statement: [
35+
{
36+
Effect: 'Allow',
37+
Principal: '*',
38+
Action: ['s3:GetObject'],
39+
Resource: [`arn:aws:s3:::${bucketName}/*`]
40+
}
41+
]
42+
};
43+
44+
await connectionMinio.setBucketPolicy(bucketName, JSON.stringify(policy));
45+
addLog.info(`Set public read policy for bucket: ${bucketName}`);
46+
}
47+
48+
return true;
49+
} catch (error) {
50+
addLog.error(`Failed to ensure bucket ${bucketName}:`, error);
51+
throw error;
52+
}
53+
}, 3);
54+
};
55+
56+
export const listBuckets = async () => {
57+
try {
58+
const buckets = await connectionMinio.listBuckets();
59+
return buckets;
60+
} catch (error) {
61+
addLog.error('Failed to list buckets:', error);
62+
throw error;
63+
}
64+
};
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { Client } from 'minio';
2+
3+
declare global {
4+
var minioClient: Client;
5+
}

0 commit comments

Comments
 (0)