diff --git a/bun.lock b/bun.lock index a99575cd..e2f4277f 100644 --- a/bun.lock +++ b/bun.lock @@ -118,6 +118,18 @@ "typescript": "^5.0.0", }, }, + "modules/tool/packages/base64ToImage": { + "name": "@fastgpt-plugins/tool-base64to-image", + "dependencies": { + "zod": "^3.24.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, "modules/tool/packages/blackForestLab": { "name": "fastgpt-tools-blackForestLab", "dependencies": { @@ -391,6 +403,18 @@ "typescript": "^5.0.0", }, }, + "modules/tool/packages/searchInfinity": { + "name": "@fastgpt-plugins/tool-search-infinity", + "dependencies": { + "zod": "^3.24.2", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5.0.0", + }, + }, "modules/tool/packages/searchXNG": { "name": "fastgpt-tools-searchXNG", "dependencies": { @@ -631,10 +655,14 @@ "@fastgpt-plugins/tool-ali-model-studio": ["@fastgpt-plugins/tool-ali-model-studio@workspace:modules/tool/packages/aliModelStudio"], + "@fastgpt-plugins/tool-base64to-image": ["@fastgpt-plugins/tool-base64to-image@workspace:modules/tool/packages/base64ToImage"], + "@fastgpt-plugins/tool-link-memo": ["@fastgpt-plugins/tool-link-memo@workspace:modules/tool/packages/linkMemo"], "@fastgpt-plugins/tool-mineru": ["@fastgpt-plugins/tool-mineru@workspace:modules/tool/packages/mineru"], + "@fastgpt-plugins/tool-search-infinity": ["@fastgpt-plugins/tool-search-infinity@workspace:modules/tool/packages/searchInfinity"], + "@fastgpt-sdk/plugin": ["@fastgpt-sdk/plugin@workspace:sdk"], "@fortaine/fetch-event-source": ["@fortaine/fetch-event-source@3.0.6", "", {}, "sha512-621GAuLMvKtyZQ3IA6nlDWhV1V/7PGOTNIGLUifxt0KzM+dZIweJ6F3XvQF3QnqeNfS1N7WQ0Kil1Di/lhChEw=="], @@ -2047,10 +2075,14 @@ "@fast-csv/parse/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], + "@fastgpt-plugins/tool-base64to-image/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + "@fastgpt-plugins/tool-link-memo/@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="], "@fastgpt-plugins/tool-mineru/@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], + "@fastgpt-plugins/tool-search-infinity/@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], "@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], @@ -2209,10 +2241,14 @@ "zip-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "@fastgpt-plugins/tool-base64to-image/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + "@fastgpt-plugins/tool-link-memo/@types/bun/bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="], "@fastgpt-plugins/tool-mineru/@types/bun/bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + "@fastgpt-plugins/tool-search-infinity/@types/bun/bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + "@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], diff --git a/modules/tool/packages/base64ToImage/config.ts b/modules/tool/packages/base64ToImage/config.ts new file mode 100644 index 00000000..66755338 --- /dev/null +++ b/modules/tool/packages/base64ToImage/config.ts @@ -0,0 +1,50 @@ +import { defineTool } from '@tool/type'; +import { FlowNodeInputTypeEnum, WorkflowIOValueTypeEnum } from '@tool/type/fastgpt'; +import { ToolTypeEnum } from '@tool/type/tool'; + +export default defineTool({ + name: { + 'zh-CN': 'Base64 转图片', + en: 'Base64 to Image' + }, + type: ToolTypeEnum.tools, + description: { + 'zh-CN': '输入 Base64 编码的图片,输出图片可访问链接。', + en: 'Enter a Base64-encoded image and get a directly accessible image link.' + }, + toolDescription: 'Enter a Base64-encoded image and get a directly accessible image link.', + versionList: [ + { + value: '0.1.0', + description: 'Default version', + inputs: [ + { + key: 'base64', + label: 'Base64 字符串', + renderTypeList: [FlowNodeInputTypeEnum.input, FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string + } + ], + outputs: [ + { + valueType: WorkflowIOValueTypeEnum.string, + key: 'url', + label: '图片 URL', + description: '可访问的图片地址: http://example.com' + }, + { + valueType: WorkflowIOValueTypeEnum.string, + key: 'type', + label: 'MIME 类型', + description: 'MIME 类型' + }, + { + valueType: WorkflowIOValueTypeEnum.number, + key: 'size', + label: '图片大小(B)', + description: '图片大小(B)' + } + ] + } + ] +}); diff --git a/modules/tool/packages/base64ToImage/index.ts b/modules/tool/packages/base64ToImage/index.ts new file mode 100644 index 00000000..d698ed48 --- /dev/null +++ b/modules/tool/packages/base64ToImage/index.ts @@ -0,0 +1,10 @@ +import config from './config'; +import { InputType, OutputType, tool as toolCb } from './src'; +import { exportTool } from '@tool/utils/tool'; + +export default exportTool({ + toolCb, + InputType, + OutputType, + config +}); diff --git a/modules/tool/packages/base64ToImage/logo.svg b/modules/tool/packages/base64ToImage/logo.svg new file mode 100644 index 00000000..4725ae03 --- /dev/null +++ b/modules/tool/packages/base64ToImage/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/modules/tool/packages/base64ToImage/package.json b/modules/tool/packages/base64ToImage/package.json new file mode 100644 index 00000000..ea1cb5b0 --- /dev/null +++ b/modules/tool/packages/base64ToImage/package.json @@ -0,0 +1,17 @@ +{ + "name": "@fastgpt-plugins/tool-base64to-image", + "module": "index.ts", + "type": "module", + "scripts": { + "build": "bun ../../../../scripts/build.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "zod": "^3.24.2" + } +} diff --git a/modules/tool/packages/base64ToImage/src/index.ts b/modules/tool/packages/base64ToImage/src/index.ts new file mode 100644 index 00000000..cd5770bf --- /dev/null +++ b/modules/tool/packages/base64ToImage/src/index.ts @@ -0,0 +1,123 @@ +import { z } from 'zod'; +import { uploadFile } from '@tool/utils/uploadFile'; + +/** + * Detect image MIME type from base64 binary data by checking file signatures + * Supports JPEG, PNG, GIF, BMP, and WebP formats + */ +function detectImageMimeType(base64Data: string) { + try { + // Remove data URL prefix if exists and decode base64 + const base64Content = base64Data.replace(/^data:[^;]+;base64,/, ''); + const binaryString = atob(base64Content); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // Check for common image file signatures + // JPEG: FF D8 FF + if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) { + return 'image/jpeg'; + } + + // PNG: 89 50 4E 47 0D 0A 1A 0A + if ( + bytes.length >= 8 && + bytes[0] === 0x89 && + bytes[1] === 0x50 && + bytes[2] === 0x4e && + bytes[3] === 0x47 && + bytes[4] === 0x0d && + bytes[5] === 0x0a && + bytes[6] === 0x1a && + bytes[7] === 0x0a + ) { + return 'image/png'; + } + + // GIF: 47 49 46 38 (GIF8) + if ( + bytes.length >= 4 && + bytes[0] === 0x47 && + bytes[1] === 0x49 && + bytes[2] === 0x46 && + bytes[3] === 0x38 + ) { + return 'image/gif'; + } + + // BMP: 42 4D + if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) { + return 'image/bmp'; + } + + // WebP: RIFF + WEBP + if ( + bytes.length >= 12 && + bytes[0] === 0x52 && + bytes[1] === 0x49 && + bytes[2] === 0x46 && + bytes[3] === 0x46 && + bytes[8] === 0x57 && + bytes[9] === 0x45 && + bytes[10] === 0x42 && + bytes[11] === 0x50 + ) { + return 'image/webp'; + } + + // Default to PNG if no signature matches + return null; + } catch { + // If any error occurs during detection, default to PNG + return null; + } +} + +export const InputType = z.object({ + base64: z.string().nonempty() +}); + +export const OutputType = z.object({ + url: z.string(), + type: z.string(), + size: z.number() +}); + +/** + * Convert base64 image data to a file and return its URL, type, and size + * Supports both data URL format (with MIME type) and raw base64 (auto-detected) + */ +export async function tool({ + base64 +}: z.infer): Promise> { + // First try to get MIME type from data URL + const mime = (() => { + const match = base64.match(/^data:([^;]+);base64,/); + if (match?.[1]) { + return match[1]; + } + const detectedType = detectImageMimeType(base64); + if (!detectedType) { + throw new Error('Image Type unknown'); + } + return detectedType; + })(); + + const ext = (() => { + const m = mime.split('/')[1]; + return m && m.length > 0 ? m : 'png'; + })(); + + // Generate filename with appropriate extension + const filename = `image.${ext}`; + + const meta = await uploadFile({ base64, defaultFilename: filename }); + + return { + url: meta.accessUrl, + type: meta.contentType, + size: meta.size + }; +} diff --git a/modules/tool/packages/base64ToImage/test/index.test.ts b/modules/tool/packages/base64ToImage/test/index.test.ts new file mode 100644 index 00000000..26bcb189 --- /dev/null +++ b/modules/tool/packages/base64ToImage/test/index.test.ts @@ -0,0 +1,17 @@ +import { expect, test } from 'vitest'; +import tool from '..'; + +test(async () => { + expect(tool.name).toBeDefined(); + expect(tool.description).toBeDefined(); + expect(tool.cb).toBeDefined(); + + const v = tool.versionList?.[0]; + expect(v).toBeDefined(); + const inputKeys = (v?.inputs || []).map((i: any) => i.key); + const outputKeys = (v?.outputs || []).map((o: any) => o.key); + expect(inputKeys).toContain('base64'); + expect(outputKeys).toContain('url'); + expect(outputKeys).toContain('type'); + expect(outputKeys).toContain('size'); +});