From 13c44016597687f7fbe65077ab1e5059f29bb0ac Mon Sep 17 00:00:00 2001 From: Finley Ge Date: Mon, 3 Nov 2025 14:16:28 +0800 Subject: [PATCH 1/6] feat: implement S3-based model provider avatars - Add model avatar upload functionality to S3 storage - Initialize model avatars during service startup after S3 setup - Update provider API to dynamically generate avatar URLs - Support existing avatar paths (plugins/, model/) for backward compatibility - Auto-upload 35 model provider logos to /system/plugin/models/{provider}/logo Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude Co-Authored-By: Happy --- modules/model/api/provider.ts | 36 +++++++++++++++- modules/model/avatars.ts | 79 +++++++++++++++++++++++++++++++++++ runtime/index.ts | 4 ++ 3 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 modules/model/avatars.ts diff --git a/modules/model/api/provider.ts b/modules/model/api/provider.ts index 97eaef7b..a99801ac 100644 --- a/modules/model/api/provider.ts +++ b/modules/model/api/provider.ts @@ -1,13 +1,47 @@ import { s } from '@/router/init'; import { contract } from '@/contract'; import { aiproxyIdMap, ModelProviders } from '../constants'; +import { publicS3Server } from '@/s3'; +import { getModelAvatarUrl } from '../avatars'; export const getProvidersHandler = s.route(contract.model.getProviders, async () => { + // Convert avatar paths to full URLs + const aiproxyIdMapWithUrls = Object.fromEntries( + await Promise.all( + Object.entries(aiproxyIdMap).map(async ([key, value]) => { + let avatarUrl = value.avatar; + + // If no avatar is set, try to generate one from the provider + if (!avatarUrl) { + try { + avatarUrl = await getModelAvatarUrl(value.provider); + } catch (error) { + // If avatar generation fails, leave as undefined + avatarUrl = undefined; + } + } else { + // Convert existing S3 paths to full URLs + if (value.avatar.startsWith('plugins/') || value.avatar.startsWith('model/')) { + avatarUrl = await publicS3Server.generateExternalUrl(value.avatar); + } + } + + return [ + key, + { + ...value, + avatar: avatarUrl + } + ]; + }) + ) + ); + return { status: 200, body: { modelProviders: ModelProviders, - aiproxyIdMap + aiproxyIdMap: aiproxyIdMapWithUrls } }; }); diff --git a/modules/model/avatars.ts b/modules/model/avatars.ts new file mode 100644 index 00000000..f80978fa --- /dev/null +++ b/modules/model/avatars.ts @@ -0,0 +1,79 @@ +import { glob } from 'fs/promises'; +import { join } from 'path'; +import { publicS3Server } from '@/s3'; +import { mimeMap } from '@/s3/const'; +import { addLog } from '@/utils/log'; + +const UploadModelsS3Path = '/system/plugin/models'; + +/** + * Initialize and upload model provider logos to S3 + * This function should be called after S3 initialization + */ +export const initModelAvatars = async () => { + try { + addLog.info('Starting model avatars initialization...'); + + // Get all model provider logo files + const modelProviderLogos = glob('modules/model/provider/*/logo.*'); + + let uploadedCount = 0; + + for await (const logoPath of modelProviderLogos) { + try { + // Extract provider name from path: modules/model/provider/{ProviderName}/logo.svg + const pathParts = logoPath.split('/'); + const providerName = pathParts[3]; // provider directory name + + if (!providerName) { + addLog.warn(`Invalid logo path format: ${logoPath}`); + continue; + } + + // Get file extension for MIME type + const ext = logoPath.split('.').pop(); + if (!ext) { + addLog.warn(`No file extension found for: ${logoPath}`); + continue; + } + + const mimeType = mimeMap[`.${ext.toLowerCase()}`]; + if (!mimeType) { + addLog.warn(`Unsupported MIME type for extension: .${ext}`); + continue; + } + + // Read file and upload to S3 + const file = Bun.file(logoPath); + const s3Path = `${UploadModelsS3Path}/${providerName}/logo`; + + await publicS3Server.uploadFileAdvanced({ + path: logoPath, + prefix: UploadModelsS3Path.replace('/', '') + `/${providerName}`, + keepRawFilename: true, + contentType: mimeType + }); + + uploadedCount++; + addLog.info(`📦 Uploaded model avatar: ${providerName} -> ${s3Path}`); + } catch (error) { + addLog.error(`Failed to upload model avatar for ${logoPath}:`, error); + } + } + + addLog.info(`✅ Model avatars initialization completed. Uploaded ${uploadedCount} avatars.`); + } catch (error) { + addLog.error('❌ Model avatars initialization failed:', error); + throw error; + } +}; + +/** + * Get S3 URL for a model provider avatar + * @param providerName - The model provider name + * @returns Complete S3 URL for the avatar + */ +export const getModelAvatarUrl = async (providerName: string): Promise => { + const s3Path = `${UploadModelsS3Path}/${providerName}/logo`; + return await publicS3Server.generateExternalUrl(s3Path); +}; diff --git a/runtime/index.ts b/runtime/index.ts index d5b7667b..66c3c7db 100644 --- a/runtime/index.ts +++ b/runtime/index.ts @@ -10,6 +10,7 @@ import { addLog } from '@/utils/log'; import { setupProxy } from '@/utils/setupProxy'; import { connectSignoz } from '@/utils/signoz'; import { initModels } from '@model/init'; +import { initModelAvatars } from '@model/avatars'; import { basePath, tempDir, tempToolsDir } from '@tool/constants'; import { initWorkflowTemplates } from '@workflow/init'; import express from 'express'; @@ -44,6 +45,9 @@ try { await initializeS3(); +// Upload model provider avatars to S3 +await initModelAvatars(); + // Modules await refreshDir(tempDir); // upload pkg files, unpkg, temp dir await ensureDir(tempToolsDir); // ensure the unpkged tools temp dir From c1c39a60a982cb7475c1bbdd261cac44a16841c6 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 3 Nov 2025 21:23:48 +0800 Subject: [PATCH 2/6] fix: model avatar --- .claude/CLAUDE.md | 237 ++++++++++++++++++++++++++++++++++ .claude/skills/toolDev.zip | Bin 18422 -> 0 bytes modules/model/api/provider.ts | 42 ++---- modules/model/avatars.ts | 174 ++++++++++++++++++++----- modules/model/constants.ts | 6 +- modules/model/contract.ts | 2 +- runtime/build/index.ts | 39 ++++++ 7 files changed, 433 insertions(+), 67 deletions(-) create mode 100644 .claude/CLAUDE.md delete mode 100644 .claude/skills/toolDev.zip diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..cd7fcf3e --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,237 @@ +# FastGPT 插件开发指南 + +## 项目概述 + +FastGPT 插件系统是一个基于 monorepo 架构的插件开发平台,支持开发和部署各种 FastGPT 工具和工作流组件。 + +## 环境配置 + +### 开发环境 (Bun) +- **运行时**: Bun 1.2+ +- **包管理**: Bun +- **构建**: 原生 TypeScript 支持 +- **热重载**: 开发模式支持 + +### 生产环境 (Node.js v22) +- **运行时**: Node.js v22+ +- **包管理**: npm/yarn +- **构建**: 构建到 `dist` 目录 +- **部署**: 生产模式运行 + +## 项目结构 + +### Monorepo 工作空间 +```json +"workspaces": [ + "sdk", + "lib", + "modules/*", + "runtime" +] +``` + +### 目录结构 +- `packages/` - 核心包目录 + - `global/` - 全局配置和类型定义 + - `public/workflow-tool/modules/` - 工具集配置文件目录 + - `server/` - 服务器端代码 + - `src/` - 源代码 + - `api/` - API 路由和服务 + - `services/` - 服务层 + - `external/` - 外部工具适配服务 +- `modules/` - 模块目录 + - `api/` - API 相关模块 + - `services/` - API 服务 + - `external/` - 外部工具服务实现 + - `tool/` - 工具模块 + - `model/` - 数据模型模块 + - `workflow/` - 工作流模块 +- `runtime/` - 运行时模块 +- `sdk/` - SDK 模块 +- `lib/` - 共享库模块 +- `dist/` - 构建输出目录 +- `scripts/` - 构建和部署脚本 + +## 开发环境兼容性 + +### 关键差异 + +#### 1. 运行时环境 +- **开发**: Bun 运行时,支持原生 TypeScript 执行 +- **生产**: Node.js v22 运行时,构建后的 JavaScript 代码 + +#### 2. 构建输出 +- **开发**: 直接运行 TypeScript 代码 +- **生产**: 构建到 `dist/` 目录,包含: + - `index.js` - 主入口文件 + - `worker.js` - Web Worker 文件 + - `tools/` - 构建后的工具目录 + - `workflows/` - 构建后的工作流目录 + +### 代码兼容性要求 + +**⚠️ 重要**: 所有在生产环境中运行的代码必须同时兼容 Bun 和 Node.js v22 + +#### 1. 文件系统操作 +```typescript +// ✅ 正确:使用标准 Node.js API +import { readFile, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +// ❌ 避免:使用 Bun 特有 API +// const file = Bun.file('path'); +// const content = await file.text(); +``` + +#### 2. 网络请求 +```typescript +// ✅ 正确:使用标准 fetch API (Node.js 18+ 内置) +const response = await fetch(url); +const data = await response.json(); + +// ✅ 正确:使用 axios 等跨平台库 +import axios from 'axios'; +const response = await axios.get(url); + +// ❌ 避免:使用 Bun 特有的网络 API +// const response = await Bun.fetch(url); +``` + +#### 3. 模块导入 +```typescript +// ✅ 正确:标准 ES 模块语法 +import { config } from './config.js'; +import { tool } from '@tool/some-tool'; + +// ✅ 正确:使用路径别名 +import { helper } from '@/lib/helper'; +import { tool } from '@tool/some-tool'; + +// ❌ 避免:使用 Bun 特有的导入方式 +// const module = await Bun.import('./module.ts'); +``` + +#### 4. 环境变量 +```typescript +// ✅ 正确:标准方式 +const apiKey = process.env.API_KEY; + +// ✅ 正确:类型安全的访问 +const config = { + apiKey: process.env.API_KEY || 'default', + port: parseInt(process.env.PORT || '3000') +}; +``` + +## 代码克隆策略 + +### Sparse Checkout 配置 +对于只需要开发插件的场景,可以使用 sparse checkout 避免拉取所有代码: + +```bash +# 初始化仓库 +git clone --no-checkout +cd +git sparse-checkout init --no-cone + +# 配置 sparse checkout +git sparse-checkout set --no-cone \ + /modules/tool/packages/sparseTool \ + /modules/tool/packages/testTool \ + /lib \ + /scripts \ + /packages/global/public/workflow-tool/modules +``` + +### 当前 sparse-checkout 配置 +``` +--no-cone +/* +!/modules/tool/packages/* +/modules/tool/packages/testPR +/modules/tool/packages/testPR/* +/Volumes/Code/fastgpt-plugins/modules/tool/packages/sparseTool +/modules/tool/packages/testTool +``` + +## 开发工作流 + +### 1. 环境设置 +```bash +# 安装依赖 +bun install + +# 开发模式 +bun run dev + +# 构建项目 +bun run build:runtime +``` + +### 2. 工具开发 +```bash +# 创建新工具 +bun run new:tool + +# 安装插件 +bun run install:plugins + +# 构建工具包 +bun run build:pkg +``` + +### 3. 生产部署 +```bash +# 构建生产版本 +bun run build:runtime + +# 启动生产服务 +bun run start +``` + +## 快速适配指南 + +### 1. 工具适配步骤 +1. **分析工具结构**:查看工具的入口文件、依赖关系、API 接口 +2. **适配工具类型**:判断是 Simple Tool 还是 Complex Tool +3. **创建 FastGPT 配置**:根据工具类型创建对应的 manifest.json 和工具集配置 +4. **实现 API 适配层**:在 `modules/api/services/external/` 目录下创建适配服务 +5. **添加类型定义**:在对应的 types 目录下添加必要的类型定义 +6. **创建工具集配置**:在 `packages/global/public/workflow-tool/modules/` 目录下创建工具集配置文件 +7. **测试验证**:创建测试用例验证工具功能 + +### 2. 环境兼容性检查 +- 确保代码在 Bun 和 Node.js v22 中都能运行 +- 验证构建后的代码在 `dist` 目录中正确生成 +- 测试生产环境的启动和运行 + +## 最佳实践 + +### 1. 代码兼容性 +- 使用标准的 Node.js API +- 避免使用 Bun 特有功能 +- 定期在 Node.js 环境下测试构建结果 + +### 2. 性能优化 +- 利用 Bun 的快速构建和热重载 +- 生产环境使用优化的构建配置 +- 合理使用 monorepo 的依赖共享 + +### 3. 质量保证 +- 运行 linting 和格式化 +- 执行完整的测试套件 +- 验证跨环境兼容性 + +## 常见问题 + +### Q: 如何确保代码在两个环境都兼容? +A: 使用标准的 Node.js API,避免 Bun 特有功能,定期在 Node.js 环境下测试。 + +### Q: 如何高效地克隆只需要的代码? +A: 使用 git sparse checkout,配置只需要的目录和文件。 + +### Q: 生产环境构建失败怎么办? +A: 检查 `dist` 目录结构,确保所有依赖都正确安装,验证 Node.js 版本兼容性。 + +### Q: 如何调试生产环境问题? +A: 在 `dist` 目录中设置断点,使用 Node.js 调试工具,检查构建日志。 \ No newline at end of file diff --git a/.claude/skills/toolDev.zip b/.claude/skills/toolDev.zip deleted file mode 100644 index 5275c25c9500459dc67005f4cc1dd8775489856d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18422 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 NG-YC7SnUep0RSJ}zFYtR diff --git a/modules/model/api/provider.ts b/modules/model/api/provider.ts index a99801ac..4e513992 100644 --- a/modules/model/api/provider.ts +++ b/modules/model/api/provider.ts @@ -1,47 +1,23 @@ import { s } from '@/router/init'; import { contract } from '@/contract'; import { aiproxyIdMap, ModelProviders } from '../constants'; -import { publicS3Server } from '@/s3'; import { getModelAvatarUrl } from '../avatars'; export const getProvidersHandler = s.route(contract.model.getProviders, async () => { // Convert avatar paths to full URLs - const aiproxyIdMapWithUrls = Object.fromEntries( - await Promise.all( - Object.entries(aiproxyIdMap).map(async ([key, value]) => { - let avatarUrl = value.avatar; - - // If no avatar is set, try to generate one from the provider - if (!avatarUrl) { - try { - avatarUrl = await getModelAvatarUrl(value.provider); - } catch (error) { - // If avatar generation fails, leave as undefined - avatarUrl = undefined; - } - } else { - // Convert existing S3 paths to full URLs - if (value.avatar.startsWith('plugins/') || value.avatar.startsWith('model/')) { - avatarUrl = await publicS3Server.generateExternalUrl(value.avatar); - } - } - - return [ - key, - { - ...value, - avatar: avatarUrl - } - ]; - }) - ) + const modelProviders = await Promise.all( + ModelProviders.map(async (provider) => { + return { + ...provider, + avatar: await getModelAvatarUrl(provider.provider) + }; + }) ); - return { status: 200, body: { - modelProviders: ModelProviders, - aiproxyIdMap: aiproxyIdMapWithUrls + modelProviders, + aiproxyIdMap } }; }); diff --git a/modules/model/avatars.ts b/modules/model/avatars.ts index f80978fa..6f1b648d 100644 --- a/modules/model/avatars.ts +++ b/modules/model/avatars.ts @@ -1,11 +1,134 @@ -import { glob } from 'fs/promises'; -import { join } from 'path'; +import { existsSync } from 'node:fs'; +import { join, resolve, parse } from 'node:path'; import { publicS3Server } from '@/s3'; import { mimeMap } from '@/s3/const'; import { addLog } from '@/utils/log'; +import { isProd } from '@/constants'; const UploadModelsS3Path = '/system/plugin/models'; +// Supported image formats for logo files +const logoFormats = ['svg', 'png', 'jpeg', 'webp', 'jpg']; + +/** + * Find logo file with supported formats in the given directory + * @param directory Directory to search in + * @returns Logo file path if found, null otherwise + */ +function findLogoFile(directory: string): string | null { + for (const format of logoFormats) { + const logoPath = join(directory, `logo.${format}`); + if (existsSync(logoPath)) { + return logoPath; + } + } + return null; +} + +/** + * Get model provider logos from the source code (development only) + */ +const getDevelopmentModelLogos = async (): Promise< + Array<{ path: string; providerName: string }> +> => { + const providerDir = resolve('../modules/model/provider'); + const { readdir } = await import('node:fs/promises'); + const result: Array<{ path: string; providerName: string }> = []; + + try { + const providerNames = await readdir(providerDir); + + for (const providerName of providerNames) { + const providerPath = join(providerDir, providerName); + + // Skip if it's not a directory + if (!existsSync(providerPath) || !require('fs').statSync(providerPath).isDirectory()) { + continue; + } + + // Find logo file with any supported format + const logoPath = findLogoFile(providerPath); + if (logoPath) { + result.push({ path: logoPath, providerName }); + } + } + } catch (error) { + addLog.error('Failed to read development model provider directory:', error); + } + + return result; +}; + +/** + * Get model provider logos from the built distribution (production) + */ +const getProductionModelLogos = async (): Promise< + Array<{ path: string; providerName: string }> +> => { + const avatarsDir = resolve('dist/model/avatars'); + if (!existsSync(avatarsDir)) { + addLog.warn('Production avatars directory not found'); + return []; + } + + const { readdir } = await import('node:fs/promises'); + + try { + const files = await readdir(avatarsDir); + const result: Array<{ path: string; providerName: string }> = []; + + for (const file of files) { + if (file.startsWith('.')) continue; // Skip hidden files + + const filePath = join(avatarsDir, file); + const fileExt = parse(file).ext.toLowerCase(); + + // Check if it's a supported image format + if (logoFormats.some((format) => `.${format}` === fileExt)) { + // Provider name is filename without extension + const providerName = parse(file).name; + result.push({ path: filePath, providerName }); + } + } + + return result; + } catch (error) { + addLog.error('Failed to read production model avatars:', error); + return []; + } +}; + +/** + * Read and upload a single logo file to S3 + */ +const uploadLogoFile = async (logoPath: string, providerName: string): Promise => { + // Parse file information + const parsedPath = parse(logoPath); + const fileExt = parsedPath.ext.toLowerCase(); + + if (!fileExt) { + addLog.warn(`No file extension found for: ${logoPath}`); + return; + } + + const mimeType = mimeMap[fileExt]; + if (!mimeType) { + addLog.warn(`Unsupported MIME type for extension: ${fileExt}`); + return; + } + + await publicS3Server.uploadFileAdvanced({ + path: logoPath, + prefix: UploadModelsS3Path.replace('/', '') + `/${providerName}`, + keepRawFilename: true, + contentType: mimeType, + defaultFilename: 'logo' + }); + addLog.info( + `📦 Uploaded model avatar: ${providerName} -> ${`${UploadModelsS3Path}/${providerName}/logo`}` + ); +}; + /** * Initialize and upload model provider logos to S3 * This function should be called after S3 initialization @@ -14,50 +137,37 @@ export const initModelAvatars = async () => { try { addLog.info('Starting model avatars initialization...'); - // Get all model provider logo files - const modelProviderLogos = glob('modules/model/provider/*/logo.*'); + let logoItems: Array<{ path: string; providerName: string }>; + + if (!isProd) { + // Development: get actual files from source directory + logoItems = await getDevelopmentModelLogos(); + addLog.info('Running in development mode, reading from source files...'); + } else { + // Production: read from simplified avatars directory + logoItems = await getProductionModelLogos(); + addLog.info('Running in production mode, reading from dist/model/avatars...'); + } let uploadedCount = 0; - for await (const logoPath of modelProviderLogos) { + for (const { path: logoPath, providerName } of logoItems) { try { - // Extract provider name from path: modules/model/provider/{ProviderName}/logo.svg - const pathParts = logoPath.split('/'); - const providerName = pathParts[3]; // provider directory name - if (!providerName) { addLog.warn(`Invalid logo path format: ${logoPath}`); continue; } - // Get file extension for MIME type - const ext = logoPath.split('.').pop(); - if (!ext) { - addLog.warn(`No file extension found for: ${logoPath}`); - continue; - } - - const mimeType = mimeMap[`.${ext.toLowerCase()}`]; - if (!mimeType) { - addLog.warn(`Unsupported MIME type for extension: .${ext}`); + // Check if file exists before attempting to upload + if (!existsSync(logoPath)) { + addLog.warn(`Logo file not found: ${logoPath}, skipping ${providerName}`); continue; } - // Read file and upload to S3 - const file = Bun.file(logoPath); - const s3Path = `${UploadModelsS3Path}/${providerName}/logo`; - - await publicS3Server.uploadFileAdvanced({ - path: logoPath, - prefix: UploadModelsS3Path.replace('/', '') + `/${providerName}`, - keepRawFilename: true, - contentType: mimeType - }); - + await uploadLogoFile(logoPath, providerName); uploadedCount++; - addLog.info(`📦 Uploaded model avatar: ${providerName} -> ${s3Path}`); } catch (error) { - addLog.error(`Failed to upload model avatar for ${logoPath}:`, error); + addLog.error(`Failed to upload model avatar for ${providerName}:`, error); } } diff --git a/modules/model/constants.ts b/modules/model/constants.ts index 9ced4310..a6999808 100644 --- a/modules/model/constants.ts +++ b/modules/model/constants.ts @@ -7,7 +7,11 @@ export const modelsBuffer: { data: [] }; -export const ModelProviderMap = { +export type ModelProviderMap = { + [key: string]: I18nStringType; +}; + +export const ModelProviderMap: ModelProviderMap = { OpenAI: { en: 'OpenAI', 'zh-CN': 'OpenAI', diff --git a/modules/model/contract.ts b/modules/model/contract.ts index 20906887..97e6c214 100644 --- a/modules/model/contract.ts +++ b/modules/model/contract.ts @@ -20,7 +20,7 @@ export const modelContract = c.router( description: 'Get model provider list', responses: { 200: c.type<{ - modelProviders: { provider: string; value: I18nStringStrictType }[]; + modelProviders: { provider: string; value: I18nStringStrictType; avatar: string }[]; aiproxyIdMap: AiproxyMapProviderType; }>() } diff --git a/runtime/build/index.ts b/runtime/build/index.ts index c32b8b49..c7e804d3 100644 --- a/runtime/build/index.ts +++ b/runtime/build/index.ts @@ -1,8 +1,11 @@ import { $ } from 'bun'; import { cp } from 'node:fs/promises'; import { join } from 'node:path'; +import { existsSync, mkdirSync } from 'node:fs'; + // 1. build worker await $`bun run build:worker`; + // 2. copy templates await cp( join(__dirname, '..', '..', 'modules', 'workflow', 'templates'), @@ -12,4 +15,40 @@ await cp( } ); +// 3. copy model provider avatars for production use +const modelProviderDir = join(__dirname, '..', '..', 'modules', 'model', 'provider'); +if (existsSync(modelProviderDir)) { + const avatarsDir = join(__dirname, '..', '..', 'dist', 'model', 'avatars'); + + // Create avatars directory if it doesn't exist + mkdirSync(avatarsDir, { recursive: true }); + + // Copy only logo files and rename them to provider name + const { readdir } = await import('node:fs/promises'); + const providers = await readdir(modelProviderDir, { withFileTypes: true }); + + for (const provider of providers) { + if (provider.isDirectory()) { + const providerDir = join(modelProviderDir, provider.name); + const { readdir } = await import('node:fs/promises'); + const files = await readdir(providerDir); + + // Find logo file (could be logo.svg, logo.png, etc.) + const logoFile = files.find((file) => file.startsWith('logo.')); + if (logoFile) { + const srcPath = join(providerDir, logoFile); + const ext = logoFile.split('.').pop(); + const destPath = join(avatarsDir, `${provider.name}.${ext}`); + + await cp(srcPath, destPath); + console.log(`📦 Copied avatar: ${provider.name} -> ${provider.name}.${ext}`); + } + } + } + + console.log(`✅ Copied model provider avatars to dist/model/avatars`); +} else { + console.log('⚠️ Model provider directory not found, skipping avatar copy'); +} + await $`bun run build:main`; From 30f6945ca1d3f79d1da3daa2f45e239ed4778715 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 3 Nov 2025 22:09:00 +0800 Subject: [PATCH 3/6] fix: model avatar --- modules/model/api/provider.ts | 5 +++-- modules/model/constants.ts | 6 +++--- modules/tool/api/upload/confirmUpload.ts | 3 ++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/model/api/provider.ts b/modules/model/api/provider.ts index 4e513992..f1d9b355 100644 --- a/modules/model/api/provider.ts +++ b/modules/model/api/provider.ts @@ -2,17 +2,18 @@ import { s } from '@/router/init'; import { contract } from '@/contract'; import { aiproxyIdMap, ModelProviders } from '../constants'; import { getModelAvatarUrl } from '../avatars'; +import type { I18nStringStrictType } from '@/type/i18n'; export const getProvidersHandler = s.route(contract.model.getProviders, async () => { // Convert avatar paths to full URLs - const modelProviders = await Promise.all( + const modelProviders = (await Promise.all( ModelProviders.map(async (provider) => { return { ...provider, avatar: await getModelAvatarUrl(provider.provider) }; }) - ); + )) as { provider: string; value: I18nStringStrictType; avatar: string }[]; return { status: 200, body: { diff --git a/modules/model/constants.ts b/modules/model/constants.ts index a6999808..061ae19e 100644 --- a/modules/model/constants.ts +++ b/modules/model/constants.ts @@ -1,4 +1,4 @@ -import type { I18nStringType } from '@/type/i18n'; +import type { I18nStringStrictType } from '@/type/i18n'; import type { ListModelsType } from './api/type'; export const modelsBuffer: { @@ -8,7 +8,7 @@ export const modelsBuffer: { }; export type ModelProviderMap = { - [key: string]: I18nStringType; + [key: string]: I18nStringStrictType; }; export const ModelProviderMap: ModelProviderMap = { @@ -207,7 +207,7 @@ export type ModelProviderIdType = keyof typeof ModelProviderMap; export type AiproxyMapProviderType = Record< number, { - name: I18nStringType | string; + name: I18nStringStrictType | string; provider: ModelProviderIdType; // Use to sort,get avatar avatar?: string; } diff --git a/modules/tool/api/upload/confirmUpload.ts b/modules/tool/api/upload/confirmUpload.ts index 2c98cc14..96cfe6f4 100644 --- a/modules/tool/api/upload/confirmUpload.ts +++ b/modules/tool/api/upload/confirmUpload.ts @@ -9,7 +9,8 @@ import { addLog } from '@/utils/log'; import { privateS3Server, publicS3Server } from '@/s3'; export default s.route(contract.tool.upload.confirmUpload, async ({ body }) => { - const { toolIds } = body; + const { toolIds: _toolIds } = body; + const toolIds = [...new Set(_toolIds)]; addLog.debug(`Confirming uploaded tools: ${toolIds}`); const pendingTools = await privateS3Server.getFiles(`${UploadToolsS3Path}/temp`); const pendingToolIds = pendingTools From 4dd60a26022107a786d5440cf1b66833f54931a9 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 3 Nov 2025 22:28:27 +0800 Subject: [PATCH 4/6] chore: global error catch --- runtime/index.ts | 85 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/runtime/index.ts b/runtime/index.ts index 66c3c7db..7da6b38c 100644 --- a/runtime/index.ts +++ b/runtime/index.ts @@ -1,4 +1,4 @@ -import { getCachedData, refreshVersionKey } from '@/cache'; +import { getCachedData } from '@/cache'; import { SystemCacheKeyEnum } from '@/cache/type'; import { isProd } from '@/constants'; import { initOpenAPI } from '@/contract/openapi'; @@ -16,6 +16,9 @@ import { initWorkflowTemplates } from '@workflow/init'; import express from 'express'; import { join } from 'path'; +// 全局错误处理设置 +setupGlobalErrorHandling(); + const requestSizeLimit = `${Number(process.env.MAX_API_SIZE || 10)}mb`; const app = express().use( @@ -35,6 +38,41 @@ initOpenAPI(app); initRouter(app); setupProxy(); +// 添加全局错误处理中间件 +app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + addLog.error('Express 应用错误:', { + message: error.message, + stack: error.stack, + name: error.name, + url: req.url, + method: req.method, + headers: req.headers, + body: req.body + }); + + // 不调用 next(),防止错误继续传递导致进程崩溃 + if (!res.headersSent) { + res.status(500).json({ + error: 'Internal Server Error', + message: isProd ? '服务器内部错误' : error.message, + timestamp: new Date().toISOString() + }); + } +}); + +// 处理 404 错误 +app.use((req: express.Request, res: express.Response) => { + addLog.warn(`404: ${req.method} ${req.url}`); + + if (!res.headersSent) { + res.status(404).json({ + error: 'Not Found', + message: `FastGPT-Plugin Service is running: Router ${req.method} ${req.url} is not found`, + timestamp: new Date().toISOString() + }); + } +}); + // DB try { await connectMongo(connectionMongo, MONGO_URL); @@ -76,3 +114,48 @@ const server = app.listen(PORT, (error?: Error) => { }); }) ); + +/** + * 设置全局错误处理,防止未捕获的错误导致进程退出 + */ +function setupGlobalErrorHandling() { + // 处理未捕获的异常 + process.on('uncaughtException', (error: Error) => { + addLog.error('未捕获的异常 (uncaughtException):', { + message: error.message, + stack: error.stack, + name: error.name + }); + + // 记录错误但不退出进程,让开发服务器的重启机制处理 + addLog.warn('进程继续运行,依赖自动重启机制处理...'); + }); + + // 处理未处理的 Promise 拒绝 + process.on('unhandledRejection', (reason: any, promise: Promise) => { + addLog.error('未处理的 Promise 拒绝 (unhandledRejection):', { + reason: reason?.toString() || reason, + promise: promise.toString(), + stack: reason?.stack + }); + + // 记录错误但不退出进程 + addLog.warn('Promise 拒绝已记录,进程继续运行...'); + }); + + // 处理 warning 事件 + process.on('warning', (warning: Error) => { + addLog.warn('Node.js 警告:', { + name: warning.name, + message: warning.message, + stack: warning.stack + }); + }); + + // 添加多个 rejection 处理器以覆盖不同场景 + process.on('rejectionHandled', (promise: Promise) => { + addLog.debug('Promise 拒绝已被处理:', promise); + }); + + addLog.info('全局错误处理已启用'); +} From 22aa496b16c570a27815f5342f08c739be3eec76 Mon Sep 17 00:00:00 2001 From: FinleyGe Date: Mon, 3 Nov 2025 22:29:50 +0800 Subject: [PATCH 5/6] chore: sdk --- sdk/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/package.json b/sdk/package.json index b0740d11..27f32bdd 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@fastgpt-sdk/plugin", - "version": "0.2.13", + "version": "0.2.14", "description": "fastgpt-plugin sdk", "main": "dist/client.js", "types": "dist/client.d.ts", From 0c76ec32938457f6c8dec20d978339300ff431cd Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Tue, 4 Nov 2025 10:46:48 +0800 Subject: [PATCH 6/6] init tool --- modules/model/avatars.ts | 24 ++++------ modules/model/init.ts | 5 ++- modules/tool/init.ts | 3 ++ modules/tool/loadToolDev.ts | 7 ++- runtime/index.ts | 89 ++----------------------------------- runtime/utils/error.ts | 84 ++++++++++++++++++++++++++++++++++ 6 files changed, 105 insertions(+), 107 deletions(-) create mode 100644 runtime/utils/error.ts diff --git a/modules/model/avatars.ts b/modules/model/avatars.ts index 6f1b648d..156ea37c 100644 --- a/modules/model/avatars.ts +++ b/modules/model/avatars.ts @@ -124,7 +124,7 @@ const uploadLogoFile = async (logoPath: string, providerName: string): Promise ${`${UploadModelsS3Path}/${providerName}/logo`}` ); }; @@ -149,29 +149,21 @@ export const initModelAvatars = async () => { addLog.info('Running in production mode, reading from dist/model/avatars...'); } - let uploadedCount = 0; - - for (const { path: logoPath, providerName } of logoItems) { - try { + await Promise.allSettled( + logoItems.map(async ({ path: logoPath, providerName }) => { if (!providerName) { addLog.warn(`Invalid logo path format: ${logoPath}`); - continue; + return; } - - // Check if file exists before attempting to upload if (!existsSync(logoPath)) { addLog.warn(`Logo file not found: ${logoPath}, skipping ${providerName}`); - continue; + return; } - await uploadLogoFile(logoPath, providerName); - uploadedCount++; - } catch (error) { - addLog.error(`Failed to upload model avatar for ${providerName}:`, error); - } - } + }) + ); - addLog.info(`✅ Model avatars initialization completed. Uploaded ${uploadedCount} avatars.`); + addLog.info(`✅ Model avatars initialization completed.`); } catch (error) { addLog.error('❌ Model avatars initialization failed:', error); throw error; diff --git a/modules/model/init.ts b/modules/model/init.ts index eeb9f845..6d9c74c7 100644 --- a/modules/model/init.ts +++ b/modules/model/init.ts @@ -35,6 +35,7 @@ import openrouter from './provider/OpenRouter'; import { ModelItemSchema, ModelTypeEnum, type ProviderConfigType } from './type'; import { modelsBuffer } from './constants'; import { addLog } from '@/utils/log'; +import { initModelAvatars } from './avatars'; // All providers array in alphabetical order const allProviders: ProviderConfigType[] = [ @@ -73,7 +74,9 @@ const allProviders: ProviderConfigType[] = [ yi ]; -export const initModels = () => { +export const initModels = async () => { + await initModelAvatars(); + modelsBuffer.data = allProviders .map((item) => { return item.list.map((model) => { diff --git a/modules/tool/init.ts b/modules/tool/init.ts index dfee194a..56f07fbc 100644 --- a/modules/tool/init.ts +++ b/modules/tool/init.ts @@ -36,6 +36,8 @@ export async function initTools() { const toolsInMongo = await MongoPlugin.find({ type: 'tool' }).lean(); + + addLog.debug(`Tools in mongo: ${toolsInMongo.length}`); // 1.2 download it to temp dir await batch( 10, @@ -49,6 +51,7 @@ export async function initTools() { ); // 2. get all tool dirs + addLog.debug(`Load tool in local: ${toolsInMongo.length}`); const toolFiles = await readdir(toolsDir); const toolMap: ToolMapType = new Map(); diff --git a/modules/tool/loadToolDev.ts b/modules/tool/loadToolDev.ts index 0391c277..581abaf8 100644 --- a/modules/tool/loadToolDev.ts +++ b/modules/tool/loadToolDev.ts @@ -9,7 +9,6 @@ import type { ToolType, ToolSetType } from './type'; import { ToolTagEnum } from './type/tags'; import { publicS3Server } from '@/s3'; import { mimeMap } from '@/s3/const'; -import { file } from 'bun'; /** * Load Tools in dev mode. Only avaliable in dev mode @@ -44,7 +43,7 @@ export const LoadToolsDev = async (filename: string): Promise => { contentType: mimeMap[parse(logoPath).ext] }); addLog.debug( - `Uploaded logo file: ${logoPath} to ${UploadToolsS3Path}/${filename}/${logoNameWithoutExt}` + `📦 Uploaded tool logo file: ${filename} -> ${UploadToolsS3Path}/${filename}/${logoNameWithoutExt}` ); } catch (error) { addLog.warn(`Failed to upload logo file ${logoPath}: ${error}`); @@ -117,7 +116,7 @@ export const LoadToolsDev = async (filename: string): Promise => { contentType: mimeMap['.' + logoFilename.split('.').pop()!] }); addLog.debug( - `Uploaded child logo file: ${logoPath} to ${UploadToolsS3Path}/${toolsetId}/${file}/${logoNameWithoutExt}` + `📦 Uploaded child logo file: ${toolsetId} -> ${UploadToolsS3Path}/${toolsetId}/${file}/${logoNameWithoutExt}` ); } catch (error) { addLog.warn(`Failed to upload child logo file ${logoPath}: ${error}`); @@ -139,7 +138,7 @@ export const LoadToolsDev = async (filename: string): Promise => { contentType: mimeMap['.' + logoFilename.split('.').pop()!] }); addLog.debug( - `Uploaded parent logo to child: ${parentLogoPath} to ${UploadToolsS3Path}/${toolsetId}/${file}/${logoNameWithoutExt}` + `📦 Uploaded parent logo to child: ${toolsetId} -> ${UploadToolsS3Path}/${toolsetId}/${file}/${logoNameWithoutExt}` ); } catch (error) { addLog.warn(`Failed to upload parent logo for child tool ${file}: ${error}`); diff --git a/runtime/index.ts b/runtime/index.ts index 7da6b38c..6a171bc7 100644 --- a/runtime/index.ts +++ b/runtime/index.ts @@ -10,14 +10,11 @@ import { addLog } from '@/utils/log'; import { setupProxy } from '@/utils/setupProxy'; import { connectSignoz } from '@/utils/signoz'; import { initModels } from '@model/init'; -import { initModelAvatars } from '@model/avatars'; import { basePath, tempDir, tempToolsDir } from '@tool/constants'; import { initWorkflowTemplates } from '@workflow/init'; import express from 'express'; import { join } from 'path'; - -// 全局错误处理设置 -setupGlobalErrorHandling(); +import { setupGlobalErrorHandling } from './utils/error'; const requestSizeLimit = `${Number(process.env.MAX_API_SIZE || 10)}mb`; @@ -38,41 +35,6 @@ initOpenAPI(app); initRouter(app); setupProxy(); -// 添加全局错误处理中间件 -app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { - addLog.error('Express 应用错误:', { - message: error.message, - stack: error.stack, - name: error.name, - url: req.url, - method: req.method, - headers: req.headers, - body: req.body - }); - - // 不调用 next(),防止错误继续传递导致进程崩溃 - if (!res.headersSent) { - res.status(500).json({ - error: 'Internal Server Error', - message: isProd ? '服务器内部错误' : error.message, - timestamp: new Date().toISOString() - }); - } -}); - -// 处理 404 错误 -app.use((req: express.Request, res: express.Response) => { - addLog.warn(`404: ${req.method} ${req.url}`); - - if (!res.headersSent) { - res.status(404).json({ - error: 'Not Found', - message: `FastGPT-Plugin Service is running: Router ${req.method} ${req.url} is not found`, - timestamp: new Date().toISOString() - }); - } -}); - // DB try { await connectMongo(connectionMongo, MONGO_URL); @@ -83,9 +45,6 @@ try { await initializeS3(); -// Upload model provider avatars to S3 -await initModelAvatars(); - // Modules await refreshDir(tempDir); // upload pkg files, unpkg, temp dir await ensureDir(tempToolsDir); // ensure the unpkged tools temp dir @@ -115,47 +74,5 @@ const server = app.listen(PORT, (error?: Error) => { }) ); -/** - * 设置全局错误处理,防止未捕获的错误导致进程退出 - */ -function setupGlobalErrorHandling() { - // 处理未捕获的异常 - process.on('uncaughtException', (error: Error) => { - addLog.error('未捕获的异常 (uncaughtException):', { - message: error.message, - stack: error.stack, - name: error.name - }); - - // 记录错误但不退出进程,让开发服务器的重启机制处理 - addLog.warn('进程继续运行,依赖自动重启机制处理...'); - }); - - // 处理未处理的 Promise 拒绝 - process.on('unhandledRejection', (reason: any, promise: Promise) => { - addLog.error('未处理的 Promise 拒绝 (unhandledRejection):', { - reason: reason?.toString() || reason, - promise: promise.toString(), - stack: reason?.stack - }); - - // 记录错误但不退出进程 - addLog.warn('Promise 拒绝已记录,进程继续运行...'); - }); - - // 处理 warning 事件 - process.on('warning', (warning: Error) => { - addLog.warn('Node.js 警告:', { - name: warning.name, - message: warning.message, - stack: warning.stack - }); - }); - - // 添加多个 rejection 处理器以覆盖不同场景 - process.on('rejectionHandled', (promise: Promise) => { - addLog.debug('Promise 拒绝已被处理:', promise); - }); - - addLog.info('全局错误处理已启用'); -} +// 全局错误处理设置 +setupGlobalErrorHandling(app); diff --git a/runtime/utils/error.ts b/runtime/utils/error.ts new file mode 100644 index 00000000..8f06c9a5 --- /dev/null +++ b/runtime/utils/error.ts @@ -0,0 +1,84 @@ +import { isProd } from '@/constants'; +import { addLog } from '@/utils/log'; +import express, { type Express } from 'express'; + +/** + * 设置全局错误处理,防止未捕获的错误导致进程退出 + */ +export const setupGlobalErrorHandling = (app: Express) => { + // 添加全局错误处理中间件 + app.use( + (error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + addLog.error('Express error:', { + message: error.message, + stack: error.stack, + name: error.name, + url: req.url, + method: req.method, + headers: req.headers, + body: req.body + }); + + // 不调用 next(),防止错误继续传递导致进程崩溃 + if (!res.headersSent) { + res.status(500).json({ + error: 'Internal Server Error', + message: isProd ? 'Internal Server Error' : error.message, + timestamp: new Date().toISOString() + }); + } + } + ); + // 处理 404 错误 + app.use((req: express.Request, res: express.Response) => { + addLog.warn(`404: ${req.method} ${req.url}`); + + if (!res.headersSent) { + res.status(404).json({ + error: 'Not Found', + message: `FastGPT-Plugin Service is running: Router ${req.method} ${req.url} is not found`, + timestamp: new Date().toISOString() + }); + } + }); + + // 处理未捕获的异常 + process.on('uncaughtException', (error: Error) => { + addLog.error('未捕获的异常 (uncaughtException):', { + message: error.message, + stack: error.stack, + name: error.name + }); + + // 记录错误但不退出进程,让开发服务器的重启机制处理 + addLog.warn('进程继续运行,依赖自动重启机制处理...'); + }); + + // 处理未处理的 Promise 拒绝 + process.on('unhandledRejection', (reason: any, promise: Promise) => { + addLog.error('未处理的 Promise 拒绝 (unhandledRejection):', { + reason: reason?.toString() || reason, + promise: promise.toString(), + stack: reason?.stack + }); + + // 记录错误但不退出进程 + addLog.warn('Promise 拒绝已记录,进程继续运行...'); + }); + + // 处理 warning 事件 + process.on('warning', (warning: Error) => { + addLog.warn('Node.js 警告:', { + name: warning.name, + message: warning.message, + stack: warning.stack + }); + }); + + // 添加多个 rejection 处理器以覆盖不同场景 + process.on('rejectionHandled', (promise: Promise) => { + addLog.debug('Promise 拒绝已被处理:', promise); + }); + + addLog.info('全局错误处理已启用'); +};