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
5 changes: 4 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"minio": "^8.0.5",
"mongoose": "^8.16.4",
"nanoid": "^5.1.5",
"node-cron": "^4.2.1",
"undici": "^7.13.0",
"uuid": "^11.1.0",
"zod": "^3.24.3",
Expand Down Expand Up @@ -459,7 +460,7 @@
},
"sdk": {
"name": "@fastgpt-sdk/plugin",
"version": "0.1.16",
"version": "0.1.18",
"dependencies": {
"@fortaine/fetch-event-source": "^3.0.6",
"@ts-rest/core": "^3.52.1",
Expand Down Expand Up @@ -1647,6 +1648,8 @@

"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],

"node-cron": ["node-cron@4.2.1", "", {}, "sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg=="],

"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],

"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
Expand Down
69 changes: 0 additions & 69 deletions modules/tool/api/delete.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
import { s } from '@/router/init';
import { contract } from '@/contract';
import { MongoPluginModel, pluginTypeEnum } from '@/mongo/models/plugins';
import { mongoSessionRun } from '@/mongo/utils';
import { downloadTool } from '@tool/controller';
import { refreshSyncKey } from '@/cache';
import { MongoPluginModel, pluginTypeEnum } from '@/mongo/models/plugins';
import { refreshVersionKey } from '@/cache';
import { SystemCacheKeyEnum } from '@/cache/type';
import { mongoSessionRun } from '@/mongo/utils';
import { addLog } from '@/utils/log';
import { pluginFileS3Server } from '@/s3';

export const uploadToolHandler = s.route(contract.tool.upload, async ({ body }) => {
export default s.route(contract.tool.upload.confirmUpload, async ({ body }) => {
const { objectName } = body;

await mongoSessionRun(async (session) => {
const toolId = await downloadTool(objectName, 'uploaded');
await MongoPluginModel.updateOne(
const toolId = await downloadTool(objectName);
const oldTool = await MongoPluginModel.findOneAndUpdate(
{
toolId
},
{
$set: {
objectName,
type: pluginTypeEnum.Enum.tool
}
objectName,
type: pluginTypeEnum.Enum.tool
},
{
session,
upsert: true
}
);

await refreshSyncKey(SystemCacheKeyEnum.systemTool);
if (oldTool?.objectName) pluginFileS3Server.removeFile(oldTool.objectName);
await refreshVersionKey(SystemCacheKeyEnum.systemTool);
addLog.info(`Upload tool success: ${toolId}`);
});

Expand Down
31 changes: 31 additions & 0 deletions modules/tool/api/upload/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { contract } from '@/contract';
import { MongoPluginModel } from '@/mongo/models/plugins';
import { mongoSessionRun } from '@/mongo/utils';
import { s } from '@/router/init';
import { pluginFileS3Server } from '@/s3';
import { refreshVersionKey } from '@/cache';
import { SystemCacheKeyEnum } from '@/cache/type';

export default s.route(contract.tool.upload.delete, async ({ query: { toolId: rawToolId } }) => {
const toolId = rawToolId.split('-').slice(1).join('-');
await mongoSessionRun(async (session) => {
const result = await MongoPluginModel.findOneAndDelete({ toolId }).session(session);
if (!result) {
return {
status: 404,
body: {
error: `Tool with toolId ${toolId} not found in MongoDB`
}
};
}
await pluginFileS3Server.removeFile(result.objectName);
await refreshVersionKey(SystemCacheKeyEnum.systemTool);
});

return {
status: 200,
body: {
message: 'Tool deleted successfully'
}
};
});
16 changes: 16 additions & 0 deletions modules/tool/api/upload/getUploadURL.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { s } from '@/router/init';
import { contract } from '@/contract';
import { pluginFileS3Server } from '@/s3';
import { UploadToolsS3Path } from '@tool/constants';
import { mimeMap } from '@/s3/const';

export default s.route(contract.tool.upload.getUploadURL, async ({ query: { filename } }) => {
return {
status: 200,
body: await pluginFileS3Server.generateUploadPresignedURL({
filepath: UploadToolsS3Path,
contentType: mimeMap['.js'],
filename
})
};
});
11 changes: 11 additions & 0 deletions modules/tool/api/upload/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { contract } from '@/contract';
import { s } from '@/router/init';
import confirmUpload from './confirmUpload';
import getUploadURL from './getUploadURL';
import deleteHandler from './delete';

export default s.router(contract.tool.upload, {
confirmUpload,
getUploadURL,
delete: deleteHandler
});
76 changes: 50 additions & 26 deletions modules/tool/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,28 @@ import z from 'zod';
import { c } from '@/contract/init';
import { ToolListItemSchema, type ToolListItemType, ToolTypeListSchema } from './type/api';

export const toolContract = c.router(
export const toolUploadContract = c.router(
{
list: {
path: '/list',
method: 'GET',
description: 'Get tools list',
responses: {
200: c.type<Array<ToolListItemType>>()
}
},
getTool: {
path: '/get',
method: 'GET',
description: 'Get a tool',
getUploadURL: {
path: '/getUploadURL',
query: z.object({
toolId: z.string()
filename: z.string()
}),
responses: {
200: ToolListItemSchema
}
},
getType: {
path: '/getType',
200: z.object({
postURL: z.string(),
formData: z.record(z.any()),
objectName: z.string()
})
},
method: 'GET',
description: 'Get tool type',
responses: {
200: ToolTypeListSchema
}
description: 'Get presigned upload URL'
},
delete: {
path: '/delete',
method: 'DELETE',
description: 'Delete a tool',
body: z.object({
query: z.object({
toolId: z.string()
}),
responses: {
Expand All @@ -50,8 +38,8 @@ export const toolContract = c.router(
})
}
},
upload: {
path: '/upload',
confirmUpload: {
path: '/confirmUpload',
method: 'POST',
description: 'Upload and install a tool plugin',
body: z.object({
Expand All @@ -64,6 +52,42 @@ export const toolContract = c.router(
}
}
},
{
pathPrefix: '/upload'
}
);

export const toolContract = c.router(
{
list: {
path: '/list',
method: 'GET',
description: 'Get tools list',
responses: {
200: c.type<Array<ToolListItemType>>()
}
},
getTool: {
path: '/get',
method: 'GET',
description: 'Get a tool',
query: z.object({
toolId: z.string()
}),
responses: {
200: ToolListItemSchema
}
},
getType: {
path: '/getType',
method: 'GET',
description: 'Get tool type',
responses: {
200: ToolTypeListSchema
}
},
upload: toolUploadContract
},
{
pathPrefix: '/tool'
}
Expand Down
39 changes: 19 additions & 20 deletions modules/tool/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { builtinTools, uploadedTools } from './constants';
import type { ToolSetType, ToolType } from './type';
import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs';
import { Readable } from 'stream';
import * as fs from 'fs';
import { initUploadedTool } from '@tool/init';
import path from 'path';
import { addLog } from '@/utils/log';
import { getErrText } from './utils/err';
import { pluginFileS3Server } from '@/s3/config';
import { pluginFileS3Server } from '@/s3';
import { UploadedToolBaseURL } from './utils';

export function getTool(toolId: string): ToolType | undefined {
const tools = [...builtinTools, ...uploadedTools];
Expand All @@ -28,56 +28,55 @@ export function getToolType(): z.infer<typeof ToolTypeListSchema> {
}

export async function refreshUploadedTools() {
addLog.info('refreshUploadedTools');
addLog.info('Refreshing uploaded tools');
const existsFiles = uploadedTools.map((item) => item.toolDirName);

const tools = await MongoPluginModel.find({
type: pluginTypeEnum.Enum.tool
}).lean();

const deleteFiles = existsFiles.filter(
(item) => !tools.find((tool) => tool.objectName.split('/')[1] === item.split('/')[1])
(item) => !tools.find((tool) => tool.objectName.split('/').pop() === item.split('/').pop())
);

const newFiles = tools.filter((item) => !existsFiles.includes(item.objectName.split('/')[1]));
const newFiles = tools.filter((item) => !existsFiles.includes(item.objectName.split('/').pop()!));

const removeFile = async (file: string) => {
if (fs.existsSync(file)) {
await fs.promises.unlink(file);
}
};

// merge remove and download steps into one Promise.all
await Promise.all([
...deleteFiles.map((item) => fs.promises.unlink(item)),
...deleteFiles.map((item) =>
removeFile(path.join(UploadedToolBaseURL, item.split('/').pop()!))
),
...newFiles.map((tool) => downloadTool(tool.objectName))
]);

await initUploadedTool();
return uploadedTools;
}

export async function downloadTool(objectName: string, dir: string = 'uploaded') {
export async function downloadTool(objectName: string) {
const filename = objectName.split('/').pop() as string;
async function extractToolIdFromFile(filePath: string) {
const rootMod = (await import(filePath)).default as ToolSetType;
return rootMod.toolId;
}

try {
const fullUrl = await global._pluginFileS3Server.generateExternalUrl(objectName);
const response = await fetch(fullUrl, {
signal: AbortSignal.timeout(30000)
});

if (!response.ok) {
return Promise.reject(`Download failed: ${response.status} ${response.statusText}`);
}

const filename = objectName.split('/')[1];
const uploadPath = path.join(process.cwd(), 'dist', 'tools', dir);
const uploadPath = path.join(process.cwd(), 'dist', 'tools', 'uploaded');

// Upload folder exists
if (!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath, { recursive: true });
}

// Write file
const filepath = path.join(uploadPath, filename);
await pipeline(Readable.fromWeb(response.body as any), createWriteStream(filepath));
await pipeline(await pluginFileS3Server.getFile(objectName), createWriteStream(filepath));

const extractedToolId = await extractToolIdFromFile(filepath);
if (!extractedToolId) return Promise.reject('Failed to extract toolId from file');

Expand Down
Loading