@lobehub/chat 1.98.1 → 1.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cursor/rules/backend-architecture.mdc +93 -17
- package/.cursor/rules/cursor-ux.mdc +45 -35
- package/.cursor/rules/project-introduce.mdc +72 -6
- package/.cursor/rules/rules-attach.mdc +16 -7
- package/.eslintrc.js +10 -0
- package/CHANGELOG.md +52 -0
- package/apps/desktop/README.md +7 -0
- package/apps/desktop/electron-builder.js +5 -0
- package/apps/desktop/package.json +2 -1
- package/apps/desktop/src/main/const/dir.ts +3 -0
- package/apps/desktop/src/main/controllers/UploadFileCtr.ts +13 -8
- package/apps/desktop/src/main/core/App.ts +8 -0
- package/apps/desktop/src/main/core/StaticFileServerManager.ts +221 -0
- package/apps/desktop/src/main/services/fileSrv.ts +231 -44
- package/apps/desktop/src/main/utils/next-electron-rsc.ts +36 -5
- package/changelog/v1.json +18 -0
- package/docs/development/database-schema.dbml +70 -0
- package/locales/ar/common.json +2 -0
- package/locales/ar/components.json +35 -0
- package/locales/ar/error.json +2 -0
- package/locales/ar/image.json +100 -0
- package/locales/ar/metadata.json +4 -0
- package/locales/ar/modelProvider.json +1 -0
- package/locales/ar/models.json +51 -9
- package/locales/ar/plugin.json +22 -0
- package/locales/ar/providers.json +3 -0
- package/locales/ar/setting.json +5 -0
- package/locales/bg-BG/common.json +2 -0
- package/locales/bg-BG/components.json +35 -0
- package/locales/bg-BG/error.json +2 -0
- package/locales/bg-BG/image.json +100 -0
- package/locales/bg-BG/metadata.json +4 -0
- package/locales/bg-BG/modelProvider.json +1 -0
- package/locales/bg-BG/models.json +51 -9
- package/locales/bg-BG/plugin.json +22 -0
- package/locales/bg-BG/providers.json +3 -0
- package/locales/bg-BG/setting.json +5 -0
- package/locales/de-DE/common.json +2 -0
- package/locales/de-DE/components.json +35 -0
- package/locales/de-DE/error.json +2 -0
- package/locales/de-DE/image.json +100 -0
- package/locales/de-DE/metadata.json +4 -0
- package/locales/de-DE/modelProvider.json +1 -0
- package/locales/de-DE/models.json +51 -9
- package/locales/de-DE/plugin.json +22 -0
- package/locales/de-DE/providers.json +3 -0
- package/locales/de-DE/setting.json +5 -0
- package/locales/en-US/common.json +2 -0
- package/locales/en-US/components.json +35 -0
- package/locales/en-US/error.json +2 -0
- package/locales/en-US/image.json +100 -0
- package/locales/en-US/metadata.json +4 -0
- package/locales/en-US/modelProvider.json +1 -0
- package/locales/en-US/models.json +51 -9
- package/locales/en-US/plugin.json +22 -0
- package/locales/en-US/providers.json +3 -0
- package/locales/en-US/setting.json +5 -0
- package/locales/es-ES/common.json +2 -0
- package/locales/es-ES/components.json +35 -0
- package/locales/es-ES/error.json +2 -0
- package/locales/es-ES/image.json +100 -0
- package/locales/es-ES/metadata.json +4 -0
- package/locales/es-ES/modelProvider.json +1 -0
- package/locales/es-ES/models.json +51 -9
- package/locales/es-ES/plugin.json +22 -0
- package/locales/es-ES/providers.json +3 -0
- package/locales/es-ES/setting.json +5 -0
- package/locales/fa-IR/common.json +2 -0
- package/locales/fa-IR/components.json +35 -0
- package/locales/fa-IR/error.json +2 -0
- package/locales/fa-IR/image.json +100 -0
- package/locales/fa-IR/metadata.json +4 -0
- package/locales/fa-IR/modelProvider.json +1 -0
- package/locales/fa-IR/models.json +51 -9
- package/locales/fa-IR/plugin.json +22 -0
- package/locales/fa-IR/providers.json +3 -0
- package/locales/fa-IR/setting.json +5 -0
- package/locales/fr-FR/common.json +2 -0
- package/locales/fr-FR/components.json +35 -0
- package/locales/fr-FR/error.json +2 -0
- package/locales/fr-FR/image.json +100 -0
- package/locales/fr-FR/metadata.json +4 -0
- package/locales/fr-FR/modelProvider.json +1 -0
- package/locales/fr-FR/models.json +51 -9
- package/locales/fr-FR/plugin.json +22 -0
- package/locales/fr-FR/providers.json +3 -0
- package/locales/fr-FR/setting.json +5 -0
- package/locales/it-IT/common.json +2 -0
- package/locales/it-IT/components.json +35 -0
- package/locales/it-IT/error.json +2 -0
- package/locales/it-IT/image.json +100 -0
- package/locales/it-IT/metadata.json +4 -0
- package/locales/it-IT/modelProvider.json +1 -0
- package/locales/it-IT/models.json +51 -9
- package/locales/it-IT/plugin.json +22 -0
- package/locales/it-IT/providers.json +3 -0
- package/locales/it-IT/setting.json +5 -0
- package/locales/ja-JP/common.json +2 -0
- package/locales/ja-JP/components.json +35 -0
- package/locales/ja-JP/error.json +2 -0
- package/locales/ja-JP/image.json +100 -0
- package/locales/ja-JP/metadata.json +4 -0
- package/locales/ja-JP/modelProvider.json +1 -0
- package/locales/ja-JP/models.json +51 -9
- package/locales/ja-JP/plugin.json +22 -0
- package/locales/ja-JP/providers.json +3 -0
- package/locales/ja-JP/setting.json +5 -0
- package/locales/ko-KR/common.json +2 -0
- package/locales/ko-KR/components.json +35 -0
- package/locales/ko-KR/error.json +2 -0
- package/locales/ko-KR/image.json +100 -0
- package/locales/ko-KR/metadata.json +4 -0
- package/locales/ko-KR/modelProvider.json +1 -0
- package/locales/ko-KR/models.json +51 -9
- package/locales/ko-KR/plugin.json +22 -0
- package/locales/ko-KR/providers.json +3 -0
- package/locales/ko-KR/setting.json +5 -0
- package/locales/nl-NL/common.json +2 -0
- package/locales/nl-NL/components.json +35 -0
- package/locales/nl-NL/error.json +2 -0
- package/locales/nl-NL/image.json +100 -0
- package/locales/nl-NL/metadata.json +4 -0
- package/locales/nl-NL/modelProvider.json +1 -0
- package/locales/nl-NL/models.json +51 -9
- package/locales/nl-NL/plugin.json +22 -0
- package/locales/nl-NL/providers.json +3 -0
- package/locales/nl-NL/setting.json +5 -0
- package/locales/pl-PL/common.json +2 -0
- package/locales/pl-PL/components.json +35 -0
- package/locales/pl-PL/error.json +2 -0
- package/locales/pl-PL/image.json +100 -0
- package/locales/pl-PL/metadata.json +4 -0
- package/locales/pl-PL/modelProvider.json +1 -0
- package/locales/pl-PL/models.json +51 -9
- package/locales/pl-PL/plugin.json +22 -0
- package/locales/pl-PL/providers.json +3 -0
- package/locales/pl-PL/setting.json +5 -0
- package/locales/pt-BR/common.json +2 -0
- package/locales/pt-BR/components.json +35 -0
- package/locales/pt-BR/error.json +2 -0
- package/locales/pt-BR/image.json +100 -0
- package/locales/pt-BR/metadata.json +4 -0
- package/locales/pt-BR/modelProvider.json +1 -0
- package/locales/pt-BR/models.json +51 -9
- package/locales/pt-BR/plugin.json +22 -0
- package/locales/pt-BR/providers.json +3 -0
- package/locales/pt-BR/setting.json +5 -0
- package/locales/ru-RU/common.json +2 -0
- package/locales/ru-RU/components.json +35 -0
- package/locales/ru-RU/error.json +2 -0
- package/locales/ru-RU/image.json +100 -0
- package/locales/ru-RU/metadata.json +4 -0
- package/locales/ru-RU/modelProvider.json +1 -0
- package/locales/ru-RU/models.json +51 -9
- package/locales/ru-RU/plugin.json +22 -0
- package/locales/ru-RU/providers.json +3 -0
- package/locales/ru-RU/setting.json +5 -0
- package/locales/tr-TR/common.json +2 -0
- package/locales/tr-TR/components.json +35 -0
- package/locales/tr-TR/error.json +2 -0
- package/locales/tr-TR/image.json +100 -0
- package/locales/tr-TR/metadata.json +4 -0
- package/locales/tr-TR/modelProvider.json +1 -0
- package/locales/tr-TR/models.json +51 -9
- package/locales/tr-TR/plugin.json +22 -0
- package/locales/tr-TR/providers.json +3 -0
- package/locales/tr-TR/setting.json +5 -0
- package/locales/vi-VN/common.json +2 -0
- package/locales/vi-VN/components.json +35 -0
- package/locales/vi-VN/error.json +2 -0
- package/locales/vi-VN/image.json +100 -0
- package/locales/vi-VN/metadata.json +4 -0
- package/locales/vi-VN/modelProvider.json +1 -0
- package/locales/vi-VN/models.json +51 -9
- package/locales/vi-VN/plugin.json +22 -0
- package/locales/vi-VN/providers.json +3 -0
- package/locales/vi-VN/setting.json +5 -0
- package/locales/zh-CN/common.json +2 -0
- package/locales/zh-CN/components.json +35 -0
- package/locales/zh-CN/error.json +2 -0
- package/locales/zh-CN/image.json +100 -0
- package/locales/zh-CN/metadata.json +4 -0
- package/locales/zh-CN/modelProvider.json +1 -0
- package/locales/zh-CN/models.json +51 -9
- package/locales/zh-CN/plugin.json +22 -0
- package/locales/zh-CN/providers.json +3 -0
- package/locales/zh-CN/setting.json +5 -0
- package/locales/zh-TW/common.json +2 -0
- package/locales/zh-TW/components.json +35 -0
- package/locales/zh-TW/error.json +2 -0
- package/locales/zh-TW/image.json +100 -0
- package/locales/zh-TW/metadata.json +4 -0
- package/locales/zh-TW/modelProvider.json +1 -0
- package/locales/zh-TW/models.json +51 -9
- package/locales/zh-TW/plugin.json +22 -0
- package/locales/zh-TW/providers.json +3 -0
- package/locales/zh-TW/setting.json +5 -0
- package/package.json +11 -4
- package/packages/electron-server-ipc/src/events/file.ts +3 -1
- package/packages/electron-server-ipc/src/types/file.ts +15 -0
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -1
- package/src/app/[variants]/(main)/image/@menu/components/AspectRatioSelect/index.tsx +73 -0
- package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +39 -0
- package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +89 -0
- package/src/app/[variants]/(main)/image/@menu/default.tsx +11 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +24 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +107 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageNum.tsx +290 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +504 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +18 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +19 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +155 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +415 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +732 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +24 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSelect.tsx +17 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +15 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +11 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/constants.ts +1 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +93 -0
- package/src/app/[variants]/(main)/image/@topic/default.tsx +17 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/NewTopicButton.tsx +64 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/SkeletonList.tsx +34 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +136 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +91 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +57 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicUrlSync.tsx +37 -0
- package/src/app/[variants]/(main)/image/@topic/features/Topics/index.tsx +19 -0
- package/src/app/[variants]/(main)/image/NotSupportClient.tsx +153 -0
- package/src/app/[variants]/(main)/image/_layout/Desktop/Container.tsx +35 -0
- package/src/app/[variants]/(main)/image/_layout/Desktop/RegisterHotkeys.tsx +10 -0
- package/src/app/[variants]/(main)/image/_layout/Desktop/index.tsx +30 -0
- package/src/app/[variants]/(main)/image/_layout/Mobile/index.tsx +14 -0
- package/src/app/[variants]/(main)/image/_layout/type.ts +7 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +196 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx +60 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx +90 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +65 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +43 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +49 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +156 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/styles.ts +51 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +39 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +11 -0
- package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +97 -0
- package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +48 -0
- package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +37 -0
- package/src/app/[variants]/(main)/image/features/ImageWorkspace/SkeletonList.tsx +50 -0
- package/src/app/[variants]/(main)/image/features/ImageWorkspace/index.tsx +23 -0
- package/src/app/[variants]/(main)/image/features/PromptInput/Title.tsx +38 -0
- package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +114 -0
- package/src/app/[variants]/(main)/image/layout.tsx +19 -0
- package/src/app/[variants]/(main)/image/loading.tsx +3 -0
- package/src/app/[variants]/(main)/image/page.tsx +47 -0
- package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
- package/src/chains/summaryGenerationTitle.ts +25 -0
- package/src/components/ImageItem/index.tsx +9 -6
- package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/Bedrock.tsx +3 -4
- package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/ProviderApiKeyForm.tsx +5 -4
- package/src/components/InvalidAPIKey/APIKeyForm/index.tsx +108 -0
- package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/useApiKey.ts +2 -1
- package/src/components/InvalidAPIKey/index.tsx +30 -0
- package/src/components/KeyValueEditor/index.tsx +203 -0
- package/src/components/KeyValueEditor/utils.ts +42 -0
- package/src/config/aiModels/fal.ts +52 -0
- package/src/config/aiModels/index.ts +3 -0
- package/src/config/aiModels/openai.ts +20 -6
- package/src/config/llm.ts +6 -0
- package/src/config/modelProviders/fal.ts +21 -0
- package/src/config/modelProviders/index.ts +3 -0
- package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +8 -0
- package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +11 -0
- package/src/config/paramsSchemas/fal/flux-schnell.ts +9 -0
- package/src/config/paramsSchemas/fal/imagen4.ts +10 -0
- package/src/config/paramsSchemas/openai/gpt-image-1.ts +10 -0
- package/src/const/hotkeys.ts +2 -2
- package/src/const/image.ts +6 -0
- package/src/const/settings/systemAgent.ts +1 -0
- package/src/database/client/migrations.json +27 -0
- package/src/database/migrations/0026_add_autovacuum_tuning.sql +2 -0
- package/src/database/migrations/0027_ai_image.sql +47 -0
- package/src/database/migrations/meta/0027_snapshot.json +6003 -0
- package/src/database/migrations/meta/_journal.json +7 -0
- package/src/database/models/__tests__/asyncTask.test.ts +7 -5
- package/src/database/models/__tests__/file.test.ts +287 -0
- package/src/database/models/__tests__/generation.test.ts +786 -0
- package/src/database/models/__tests__/generationBatch.test.ts +614 -0
- package/src/database/models/__tests__/generationTopic.test.ts +411 -0
- package/src/database/models/aiModel.ts +2 -0
- package/src/database/models/asyncTask.ts +1 -1
- package/src/database/models/file.ts +28 -20
- package/src/database/models/generation.ts +197 -0
- package/src/database/models/generationBatch.ts +212 -0
- package/src/database/models/generationTopic.ts +131 -0
- package/src/database/repositories/aiInfra/index.test.ts +151 -1
- package/src/database/repositories/aiInfra/index.ts +28 -19
- package/src/database/repositories/tableViewer/index.test.ts +1 -1
- package/src/database/schemas/file.ts +8 -0
- package/src/database/schemas/generation.ts +127 -0
- package/src/database/schemas/index.ts +1 -0
- package/src/database/schemas/relations.ts +45 -1
- package/src/database/type.ts +2 -0
- package/src/database/utils/idGenerator.ts +3 -0
- package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +39 -0
- package/src/features/Conversation/Error/InvalidAccessCode.tsx +2 -2
- package/src/features/Conversation/Error/index.tsx +3 -3
- package/src/features/ImageSidePanel/index.tsx +83 -0
- package/src/features/ImageTopicPanel/index.tsx +79 -0
- package/src/features/PluginDevModal/MCPManifestForm/CollapsibleSection.tsx +62 -0
- package/src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx +158 -0
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +99 -155
- package/src/features/PluginStore/McpList/Detail/Settings/index.tsx +5 -2
- package/src/hooks/useDownloadImage.ts +31 -0
- package/src/hooks/useFetchGenerationTopics.ts +13 -0
- package/src/hooks/useHotkeys/imageScope.ts +48 -0
- package/src/libs/mcp/client.ts +55 -22
- package/src/libs/mcp/types.ts +42 -6
- package/src/libs/model-runtime/BaseAI.ts +3 -1
- package/src/libs/model-runtime/ModelRuntime.test.ts +80 -0
- package/src/libs/model-runtime/ModelRuntime.ts +15 -1
- package/src/libs/model-runtime/UniformRuntime/index.ts +4 -1
- package/src/libs/model-runtime/fal/index.test.ts +442 -0
- package/src/libs/model-runtime/fal/index.ts +88 -0
- package/src/libs/model-runtime/openai/index.test.ts +396 -2
- package/src/libs/model-runtime/openai/index.ts +129 -3
- package/src/libs/model-runtime/runtimeMap.ts +2 -0
- package/src/libs/model-runtime/types/image.ts +25 -0
- package/src/libs/model-runtime/types/type.ts +1 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +10 -0
- package/src/libs/standard-parameters/index.ts +1 -0
- package/src/libs/standard-parameters/meta-schema.test.ts +214 -0
- package/src/libs/standard-parameters/meta-schema.ts +147 -0
- package/src/libs/swr/index.ts +1 -0
- package/src/libs/trpc/async/asyncAuth.ts +29 -8
- package/src/libs/trpc/async/context.ts +42 -4
- package/src/libs/trpc/async/index.ts +17 -4
- package/src/libs/trpc/async/init.ts +8 -0
- package/src/libs/trpc/client/lambda.ts +19 -2
- package/src/locales/default/common.ts +2 -0
- package/src/locales/default/components.ts +35 -0
- package/src/locales/default/error.ts +2 -0
- package/src/locales/default/image.ts +100 -0
- package/src/locales/default/index.ts +2 -0
- package/src/locales/default/metadata.ts +4 -0
- package/src/locales/default/modelProvider.ts +2 -0
- package/src/locales/default/plugin.ts +22 -0
- package/src/locales/default/setting.ts +5 -0
- package/src/middleware.ts +1 -0
- package/src/server/modules/ElectronIPCClient/index.ts +9 -1
- package/src/server/modules/S3/index.ts +15 -0
- package/src/server/routers/async/caller.ts +9 -1
- package/src/server/routers/async/image.ts +253 -0
- package/src/server/routers/async/index.ts +2 -0
- package/src/server/routers/lambda/aiProvider.test.ts +1 -0
- package/src/server/routers/lambda/generation.test.ts +267 -0
- package/src/server/routers/lambda/generation.ts +86 -0
- package/src/server/routers/lambda/generationBatch.test.ts +376 -0
- package/src/server/routers/lambda/generationBatch.ts +56 -0
- package/src/server/routers/lambda/generationTopic.test.ts +508 -0
- package/src/server/routers/lambda/generationTopic.ts +93 -0
- package/src/server/routers/lambda/image.ts +248 -0
- package/src/server/routers/lambda/index.ts +8 -0
- package/src/server/routers/tools/mcp.ts +15 -0
- package/src/server/services/file/__tests__/index.test.ts +135 -0
- package/src/server/services/file/impls/local.test.ts +153 -52
- package/src/server/services/file/impls/local.ts +70 -46
- package/src/server/services/file/impls/s3.test.ts +114 -0
- package/src/server/services/file/impls/s3.ts +40 -0
- package/src/server/services/file/impls/type.ts +10 -0
- package/src/server/services/file/index.ts +14 -0
- package/src/server/services/generation/index.ts +239 -0
- package/src/server/services/mcp/index.ts +20 -2
- package/src/services/__tests__/generation.test.ts +40 -0
- package/src/services/__tests__/generationBatch.test.ts +36 -0
- package/src/services/__tests__/generationTopic.test.ts +72 -0
- package/src/services/electron/file.ts +3 -1
- package/src/services/generation.ts +16 -0
- package/src/services/generationBatch.ts +25 -0
- package/src/services/generationTopic.ts +28 -0
- package/src/services/image.ts +33 -0
- package/src/services/mcp.ts +12 -7
- package/src/services/upload.ts +43 -9
- package/src/store/aiInfra/slices/aiProvider/action.ts +25 -5
- package/src/store/aiInfra/slices/aiProvider/initialState.ts +1 -0
- package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
- package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -5
- package/src/store/chat/slices/message/action.ts +2 -2
- package/src/store/chat/slices/translate/action.ts +1 -1
- package/src/store/global/initialState.ts +9 -0
- package/src/store/global/selectors/systemStatus.ts +8 -0
- package/src/store/image/index.ts +2 -0
- package/src/store/image/initialState.ts +25 -0
- package/src/store/image/selectors.ts +4 -0
- package/src/store/image/slices/createImage/action.test.ts +330 -0
- package/src/store/image/slices/createImage/action.ts +134 -0
- package/src/store/image/slices/createImage/initialState.ts +9 -0
- package/src/store/image/slices/createImage/selectors.test.ts +114 -0
- package/src/store/image/slices/createImage/selectors.ts +9 -0
- package/src/store/image/slices/generationBatch/action.test.ts +495 -0
- package/src/store/image/slices/generationBatch/action.ts +303 -0
- package/src/store/image/slices/generationBatch/initialState.ts +13 -0
- package/src/store/image/slices/generationBatch/reducer.test.ts +568 -0
- package/src/store/image/slices/generationBatch/reducer.ts +101 -0
- package/src/store/image/slices/generationBatch/selectors.test.ts +307 -0
- package/src/store/image/slices/generationBatch/selectors.ts +36 -0
- package/src/store/image/slices/generationConfig/action.test.ts +351 -0
- package/src/store/image/slices/generationConfig/action.ts +295 -0
- package/src/store/image/slices/generationConfig/hooks.test.ts +304 -0
- package/src/store/image/slices/generationConfig/hooks.ts +118 -0
- package/src/store/image/slices/generationConfig/index.ts +1 -0
- package/src/store/image/slices/generationConfig/initialState.ts +37 -0
- package/src/store/image/slices/generationConfig/selectors.test.ts +204 -0
- package/src/store/image/slices/generationConfig/selectors.ts +25 -0
- package/src/store/image/slices/generationTopic/action.test.ts +687 -0
- package/src/store/image/slices/generationTopic/action.ts +319 -0
- package/src/store/image/slices/generationTopic/index.ts +2 -0
- package/src/store/image/slices/generationTopic/initialState.ts +14 -0
- package/src/store/image/slices/generationTopic/reducer.test.ts +198 -0
- package/src/store/image/slices/generationTopic/reducer.ts +66 -0
- package/src/store/image/slices/generationTopic/selectors.test.ts +103 -0
- package/src/store/image/slices/generationTopic/selectors.ts +15 -0
- package/src/store/image/store.ts +42 -0
- package/src/store/image/utils/size.ts +51 -0
- package/src/store/tool/slices/customPlugin/action.ts +10 -1
- package/src/store/tool/slices/mcpStore/action.ts +6 -4
- package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
- package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
- package/src/types/aiModel.ts +8 -3
- package/src/types/aiProvider.ts +1 -0
- package/src/types/asyncTask.ts +2 -0
- package/src/types/files/index.ts +5 -0
- package/src/types/generation/index.ts +80 -0
- package/src/types/hotkey.ts +2 -0
- package/src/types/plugins/mcp.ts +2 -6
- package/src/types/tool/plugin.ts +8 -0
- package/src/types/user/settings/keyVaults.ts +5 -0
- package/src/types/user/settings/systemAgent.ts +1 -0
- package/src/utils/client/downloadFile.ts +33 -4
- package/src/utils/number.test.ts +105 -0
- package/src/utils/number.ts +25 -0
- package/src/utils/server/__tests__/geo.test.ts +6 -3
- package/src/utils/storeDebug.test.ts +152 -0
- package/src/utils/storeDebug.ts +16 -7
- package/src/utils/time.test.ts +259 -0
- package/src/utils/time.ts +18 -0
- package/src/utils/units.ts +61 -0
- package/src/utils/url.test.ts +358 -9
- package/src/utils/url.ts +105 -3
- package/{vitest.server.config.ts → vitest.config.server.ts} +3 -0
- package/.cursor/rules/i18n/i18n-auto-attached.mdc +0 -6
- package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -105
- package/src/features/Conversation/Error/InvalidAPIKey.tsx +0 -16
- package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +0 -227
- /package/.cursor/rules/{i18n/i18n.mdc → i18n.mdc} +0 -0
- /package/src/app/[variants]/(main)/settings/system-agent/features/{createForm.tsx → SystemAgentForm.tsx} +0 -0
- /package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/LoadingContext.ts +0 -0
@@ -3,7 +3,8 @@ import OpenAI from 'openai';
|
|
3
3
|
import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
4
4
|
|
5
5
|
// 引入模块以便于对函数进行spy
|
6
|
-
import { ChatStreamCallbacks
|
6
|
+
import { ChatStreamCallbacks } from '@/libs/model-runtime';
|
7
|
+
import * as openai from '@/libs/model-runtime/openai';
|
7
8
|
|
8
9
|
import * as debugStreamModule from '../utils/debugStream';
|
9
10
|
import officalOpenAIModels from './fixtures/openai-models.json';
|
@@ -12,8 +13,21 @@ import { LobeOpenAI } from './index';
|
|
12
13
|
// Mock the console.error to avoid polluting test output
|
13
14
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
14
15
|
|
16
|
+
// Mock fetch for most tests, but will be restored for convertImageUrlToFile tests
|
17
|
+
const mockFetch = vi.fn();
|
18
|
+
global.fetch = mockFetch;
|
19
|
+
|
20
|
+
const convertImageUrlToFileSpy = vi.spyOn(openai, 'convertImageUrlToFile');
|
21
|
+
|
15
22
|
describe('LobeOpenAI', () => {
|
16
|
-
let instance:
|
23
|
+
let instance: InstanceType<typeof LobeOpenAI>;
|
24
|
+
|
25
|
+
// Create mock params for createImage tests - only gpt-image-1 supported params
|
26
|
+
const mockParams = {
|
27
|
+
prompt: 'test prompt',
|
28
|
+
imageUrls: [] as string[],
|
29
|
+
size: '1024x1024' as const,
|
30
|
+
};
|
17
31
|
|
18
32
|
beforeEach(() => {
|
19
33
|
instance = new LobeOpenAI({ apiKey: 'test' });
|
@@ -23,10 +37,35 @@ describe('LobeOpenAI', () => {
|
|
23
37
|
new ReadableStream() as any,
|
24
38
|
);
|
25
39
|
vi.spyOn(instance['client'].models, 'list').mockResolvedValue({ data: [] } as any);
|
40
|
+
|
41
|
+
// Mock responses.create for responses API tests
|
42
|
+
vi.spyOn(instance['client'].responses, 'create').mockResolvedValue(new ReadableStream() as any);
|
43
|
+
|
44
|
+
// Mock convertImageUrlToFile to return a mock File object
|
45
|
+
convertImageUrlToFileSpy.mockResolvedValue({
|
46
|
+
name: 'image.png',
|
47
|
+
type: 'image/png',
|
48
|
+
size: 1024,
|
49
|
+
} as any);
|
50
|
+
|
51
|
+
// Mock fetch response for most tests
|
52
|
+
mockFetch.mockResolvedValue({
|
53
|
+
ok: true,
|
54
|
+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
55
|
+
headers: {
|
56
|
+
get: (header: string) => {
|
57
|
+
if (header === 'content-type') {
|
58
|
+
return 'image/png';
|
59
|
+
}
|
60
|
+
return null;
|
61
|
+
},
|
62
|
+
},
|
63
|
+
});
|
26
64
|
});
|
27
65
|
|
28
66
|
afterEach(() => {
|
29
67
|
vi.clearAllMocks();
|
68
|
+
mockFetch.mockClear();
|
30
69
|
});
|
31
70
|
|
32
71
|
describe('chat', () => {
|
@@ -242,4 +281,359 @@ describe('LobeOpenAI', () => {
|
|
242
281
|
expect(list).toMatchSnapshot();
|
243
282
|
});
|
244
283
|
});
|
284
|
+
|
285
|
+
describe('createImage', () => {
|
286
|
+
it('should generate an image with gpt-image-1', async () => {
|
287
|
+
// Arrange
|
288
|
+
const mockResponse = { data: [{ b64_json: 'test-base64-string' }] };
|
289
|
+
const generateSpy = vi
|
290
|
+
.spyOn(instance['client'].images, 'generate')
|
291
|
+
.mockResolvedValue(mockResponse as any);
|
292
|
+
|
293
|
+
// Act
|
294
|
+
const result = await instance.createImage({
|
295
|
+
model: 'gpt-image-1',
|
296
|
+
params: {
|
297
|
+
...mockParams,
|
298
|
+
prompt: 'A cute cat',
|
299
|
+
size: '1024x1024',
|
300
|
+
},
|
301
|
+
});
|
302
|
+
|
303
|
+
// Assert
|
304
|
+
expect(generateSpy).toHaveBeenCalledWith(
|
305
|
+
expect.objectContaining({
|
306
|
+
model: 'gpt-image-1',
|
307
|
+
prompt: 'A cute cat',
|
308
|
+
n: 1,
|
309
|
+
size: '1024x1024',
|
310
|
+
}),
|
311
|
+
);
|
312
|
+
expect(result.imageUrl).toBe('data:image/png;base64,test-base64-string');
|
313
|
+
});
|
314
|
+
|
315
|
+
it('should edit an image from a URL', async () => {
|
316
|
+
// Arrange
|
317
|
+
const mockResponse = { data: [{ b64_json: 'edited-base64-string' }] };
|
318
|
+
const editSpy = vi
|
319
|
+
.spyOn(instance['client'].images, 'edit')
|
320
|
+
.mockResolvedValue(mockResponse as any);
|
321
|
+
|
322
|
+
// Temporarily restore the spy to use real implementation
|
323
|
+
convertImageUrlToFileSpy.mockRestore();
|
324
|
+
|
325
|
+
const imageUrl = 'https://lobehub.com/_next/static/media/logo.98482105.png';
|
326
|
+
|
327
|
+
// Act
|
328
|
+
const result = await instance.createImage({
|
329
|
+
model: 'gpt-image-1',
|
330
|
+
params: {
|
331
|
+
...mockParams,
|
332
|
+
prompt: 'A cat in a hat',
|
333
|
+
imageUrls: [imageUrl],
|
334
|
+
},
|
335
|
+
});
|
336
|
+
|
337
|
+
// Assert
|
338
|
+
expect(editSpy).toHaveBeenCalled();
|
339
|
+
const callArg = editSpy.mock.calls[0][0];
|
340
|
+
expect(callArg.model).toBe('gpt-image-1');
|
341
|
+
expect(callArg.prompt).toBe('A cat in a hat');
|
342
|
+
expect(result.imageUrl).toBe('data:image/png;base64,edited-base64-string');
|
343
|
+
|
344
|
+
// Restore the spy for other tests
|
345
|
+
convertImageUrlToFileSpy.mockResolvedValue({
|
346
|
+
name: 'image.png',
|
347
|
+
type: 'image/png',
|
348
|
+
size: 1024,
|
349
|
+
} as any);
|
350
|
+
});
|
351
|
+
|
352
|
+
it('should handle `size` set to `auto`', async () => {
|
353
|
+
// Arrange
|
354
|
+
const mockResponse = { data: [{ b64_json: 'test-base64-string' }] };
|
355
|
+
const generateSpy = vi
|
356
|
+
.spyOn(instance['client'].images, 'generate')
|
357
|
+
.mockResolvedValue(mockResponse as any);
|
358
|
+
|
359
|
+
// Act
|
360
|
+
await instance.createImage({
|
361
|
+
model: 'gpt-image-1',
|
362
|
+
params: {
|
363
|
+
...mockParams,
|
364
|
+
prompt: 'A cute cat',
|
365
|
+
size: 'auto',
|
366
|
+
},
|
367
|
+
});
|
368
|
+
|
369
|
+
// Assert
|
370
|
+
expect(generateSpy).toHaveBeenCalledWith(
|
371
|
+
expect.objectContaining({
|
372
|
+
model: 'gpt-image-1',
|
373
|
+
prompt: 'A cute cat',
|
374
|
+
n: 1,
|
375
|
+
}),
|
376
|
+
);
|
377
|
+
// Should not include size when it's 'auto'
|
378
|
+
expect(generateSpy.mock.calls[0][0]).not.toHaveProperty('size');
|
379
|
+
});
|
380
|
+
|
381
|
+
it('should throw an error if convertImageUrlToFile fails', async () => {
|
382
|
+
// Arrange
|
383
|
+
const imageUrl = 'https://example.com/test-image.png';
|
384
|
+
|
385
|
+
// Mock fetch to fail for the image URL, which will cause convertImageUrlToFile to fail
|
386
|
+
vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network error'));
|
387
|
+
|
388
|
+
// Mock the OpenAI API methods to ensure they don't get called
|
389
|
+
const generateSpy = vi.spyOn(instance['client'].images, 'generate');
|
390
|
+
const editSpy = vi.spyOn(instance['client'].images, 'edit');
|
391
|
+
|
392
|
+
// Act & Assert - Note: imageUrls must be non-empty array to trigger isImageEdit = true
|
393
|
+
await expect(
|
394
|
+
instance.createImage({
|
395
|
+
model: 'gpt-image-1',
|
396
|
+
params: {
|
397
|
+
prompt: 'A cat in a hat',
|
398
|
+
imageUrls: [imageUrl], // This is the key - non-empty array
|
399
|
+
},
|
400
|
+
}),
|
401
|
+
).rejects.toThrow('Failed to convert image URLs to File objects: Error: Network error');
|
402
|
+
|
403
|
+
// Verify that OpenAI API methods were not called since conversion failed
|
404
|
+
expect(generateSpy).not.toHaveBeenCalled();
|
405
|
+
expect(editSpy).not.toHaveBeenCalled();
|
406
|
+
});
|
407
|
+
|
408
|
+
it('should throw an error when image response is missing data array', async () => {
|
409
|
+
// Arrange
|
410
|
+
const mockInvalidResponse = {}; // missing data property
|
411
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
412
|
+
|
413
|
+
// Act & Assert
|
414
|
+
await expect(
|
415
|
+
instance.createImage({
|
416
|
+
model: 'gpt-image-1',
|
417
|
+
params: { ...mockParams, prompt: 'A cute cat' },
|
418
|
+
}),
|
419
|
+
).rejects.toThrow('Invalid image response: missing or empty data array');
|
420
|
+
});
|
421
|
+
|
422
|
+
it('should throw an error when image response data array is empty', async () => {
|
423
|
+
// Arrange
|
424
|
+
const mockInvalidResponse = { data: [] }; // empty data array
|
425
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
426
|
+
|
427
|
+
// Act & Assert
|
428
|
+
await expect(
|
429
|
+
instance.createImage({
|
430
|
+
model: 'gpt-image-1',
|
431
|
+
params: { ...mockParams, prompt: 'A cute cat' },
|
432
|
+
}),
|
433
|
+
).rejects.toThrow('Invalid image response: missing or empty data array');
|
434
|
+
});
|
435
|
+
|
436
|
+
it('should throw an error when first data item is null', async () => {
|
437
|
+
// Arrange
|
438
|
+
const mockInvalidResponse = { data: [null] }; // first item is null
|
439
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
440
|
+
|
441
|
+
// Act & Assert
|
442
|
+
await expect(
|
443
|
+
instance.createImage({
|
444
|
+
model: 'gpt-image-1',
|
445
|
+
params: { ...mockParams, prompt: 'A cute cat' },
|
446
|
+
}),
|
447
|
+
).rejects.toThrow('Invalid image response: first data item is null or undefined');
|
448
|
+
});
|
449
|
+
|
450
|
+
it('should throw an error when first data item is undefined', async () => {
|
451
|
+
// Arrange
|
452
|
+
const mockInvalidResponse = { data: [undefined] }; // first item is undefined
|
453
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
454
|
+
|
455
|
+
// Act & Assert
|
456
|
+
await expect(
|
457
|
+
instance.createImage({
|
458
|
+
model: 'gpt-image-1',
|
459
|
+
params: { ...mockParams, prompt: 'A cute cat' },
|
460
|
+
}),
|
461
|
+
).rejects.toThrow('Invalid image response: first data item is null or undefined');
|
462
|
+
});
|
463
|
+
|
464
|
+
it('should re-throw OpenAI API errors during image generation', async () => {
|
465
|
+
// Arrange
|
466
|
+
const apiError = new OpenAI.APIError(
|
467
|
+
400,
|
468
|
+
{ error: { message: 'Bad Request' } },
|
469
|
+
'Error message',
|
470
|
+
{},
|
471
|
+
);
|
472
|
+
vi.spyOn(instance['client'].images, 'generate').mockRejectedValue(apiError);
|
473
|
+
|
474
|
+
// Act & Assert
|
475
|
+
await expect(
|
476
|
+
instance.createImage({
|
477
|
+
model: 'gpt-image-1',
|
478
|
+
params: { ...mockParams, prompt: 'A cute cat' },
|
479
|
+
}),
|
480
|
+
).rejects.toThrow(apiError);
|
481
|
+
});
|
482
|
+
|
483
|
+
it('should throw an error for invalid image response', async () => {
|
484
|
+
// Arrange
|
485
|
+
const mockInvalidResponse = { data: [{ url: 'some_url' }] }; // missing b64_json
|
486
|
+
vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
|
487
|
+
|
488
|
+
// Act & Assert
|
489
|
+
await expect(
|
490
|
+
instance.createImage({
|
491
|
+
model: 'gpt-image-1',
|
492
|
+
params: { ...mockParams, prompt: 'A cute cat' },
|
493
|
+
}),
|
494
|
+
).rejects.toThrow('Invalid image response: missing b64_json field');
|
495
|
+
});
|
496
|
+
});
|
497
|
+
|
498
|
+
describe('convertImageUrlToFile', () => {
|
499
|
+
beforeEach(() => {
|
500
|
+
// Reset the spy to use the real implementation for these tests
|
501
|
+
convertImageUrlToFileSpy.mockRestore();
|
502
|
+
});
|
503
|
+
|
504
|
+
afterEach(() => {
|
505
|
+
// Restore the spy for other tests
|
506
|
+
convertImageUrlToFileSpy.mockResolvedValue({
|
507
|
+
name: 'image.png',
|
508
|
+
type: 'image/png',
|
509
|
+
size: 1024,
|
510
|
+
} as any);
|
511
|
+
});
|
512
|
+
|
513
|
+
it('should convert the real lobehub logo URL to a FileLike object', async () => {
|
514
|
+
const imageUrl = 'https://lobehub.com/_next/static/media/logo.98482105.png';
|
515
|
+
const file = await openai.convertImageUrlToFile(imageUrl);
|
516
|
+
|
517
|
+
expect(file).toBeDefined();
|
518
|
+
expect((file as any).name).toBe('image.png');
|
519
|
+
expect((file as any).type).toMatch(/^image\//);
|
520
|
+
expect((file as any).size).toBeGreaterThan(0);
|
521
|
+
});
|
522
|
+
|
523
|
+
it('should convert a base64 data URL to a FileLike object', async () => {
|
524
|
+
const dataUrl =
|
525
|
+
'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AN//Z';
|
526
|
+
const file = await openai.convertImageUrlToFile(dataUrl);
|
527
|
+
|
528
|
+
expect(file).toBeDefined();
|
529
|
+
expect((file as any).name).toBe('image.jpeg');
|
530
|
+
expect((file as any).type).toBe('image/jpeg');
|
531
|
+
});
|
532
|
+
|
533
|
+
it('should handle different image mime types from data URL', async () => {
|
534
|
+
const webpDataUrl = 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==';
|
535
|
+
const file = await openai.convertImageUrlToFile(webpDataUrl);
|
536
|
+
|
537
|
+
expect(file).toBeDefined();
|
538
|
+
expect((file as any).name).toBe('image.webp');
|
539
|
+
expect((file as any).type).toBe('image/webp');
|
540
|
+
});
|
541
|
+
});
|
542
|
+
|
543
|
+
// Separate describe block for mocked fetch scenarios
|
544
|
+
describe('convertImageUrlToFile - mocked scenarios', () => {
|
545
|
+
beforeEach(() => {
|
546
|
+
// Reset the spy to use the real implementation
|
547
|
+
convertImageUrlToFileSpy.mockRestore();
|
548
|
+
});
|
549
|
+
|
550
|
+
afterEach(() => {
|
551
|
+
// Restore the spy for other tests
|
552
|
+
convertImageUrlToFileSpy.mockResolvedValue({
|
553
|
+
name: 'image.png',
|
554
|
+
type: 'image/png',
|
555
|
+
size: 1024,
|
556
|
+
} as any);
|
557
|
+
});
|
558
|
+
|
559
|
+
it('should throw an error if fetching an image from a URL fails', async () => {
|
560
|
+
// Use vi.mocked for type-safe mocking
|
561
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
562
|
+
ok: false,
|
563
|
+
statusText: 'Not Found',
|
564
|
+
} as any);
|
565
|
+
|
566
|
+
const imageUrl = 'https://example.com/invalid-image.png';
|
567
|
+
|
568
|
+
await expect(openai.convertImageUrlToFile(imageUrl)).rejects.toThrow(
|
569
|
+
'Failed to fetch image from https://example.com/invalid-image.png: Not Found',
|
570
|
+
);
|
571
|
+
});
|
572
|
+
|
573
|
+
it('should use a default mime type of image/png if content-type header is not available', async () => {
|
574
|
+
// Use vi.mocked for type-safe mocking
|
575
|
+
vi.mocked(global.fetch).mockResolvedValueOnce({
|
576
|
+
ok: true,
|
577
|
+
arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
|
578
|
+
headers: {
|
579
|
+
get: () => null,
|
580
|
+
},
|
581
|
+
} as any);
|
582
|
+
|
583
|
+
const imageUrl = 'https://example.com/image-no-content-type';
|
584
|
+
const file = await openai.convertImageUrlToFile(imageUrl);
|
585
|
+
|
586
|
+
expect(file).toBeDefined();
|
587
|
+
expect((file as any).type).toBe('image/png');
|
588
|
+
});
|
589
|
+
});
|
590
|
+
|
591
|
+
describe('responses.handlePayload', () => {
|
592
|
+
it('should add web_search_preview tool when enabledSearch is true', async () => {
|
593
|
+
const payload = {
|
594
|
+
messages: [{ content: 'Hello', role: 'user' as const }],
|
595
|
+
model: 'gpt-4o', // 使用常规模型,通过 enabledSearch 触发 responses API
|
596
|
+
temperature: 0.7,
|
597
|
+
enabledSearch: true,
|
598
|
+
tools: [{ type: 'function' as const, function: { name: 'test', description: 'test' } }],
|
599
|
+
};
|
600
|
+
|
601
|
+
await instance.chat(payload);
|
602
|
+
|
603
|
+
const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
|
604
|
+
expect(createCall.tools).toEqual([
|
605
|
+
{ type: 'function', name: 'test', description: 'test' },
|
606
|
+
{ type: 'web_search_preview' },
|
607
|
+
]);
|
608
|
+
});
|
609
|
+
|
610
|
+
it('should handle computer-use models with truncation and reasoning', async () => {
|
611
|
+
const payload = {
|
612
|
+
messages: [{ content: 'Hello', role: 'user' as const }],
|
613
|
+
model: 'computer-use-preview',
|
614
|
+
temperature: 0.7,
|
615
|
+
reasoning: { effort: 'medium' },
|
616
|
+
};
|
617
|
+
|
618
|
+
await instance.chat(payload);
|
619
|
+
|
620
|
+
const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
|
621
|
+
expect(createCall.truncation).toBe('auto');
|
622
|
+
expect(createCall.reasoning).toEqual({ effort: 'medium', summary: 'auto' });
|
623
|
+
});
|
624
|
+
|
625
|
+
it('should handle prunePrefixes models without computer-use truncation', async () => {
|
626
|
+
const payload = {
|
627
|
+
messages: [{ content: 'Hello', role: 'user' as const }],
|
628
|
+
model: 'o1-pro', // prunePrefixes 模型但非 computer-use
|
629
|
+
temperature: 0.7,
|
630
|
+
};
|
631
|
+
|
632
|
+
await instance.chat(payload);
|
633
|
+
|
634
|
+
const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
|
635
|
+
expect(createCall.reasoning).toEqual({ summary: 'auto' });
|
636
|
+
expect(createCall.truncation).toBeUndefined();
|
637
|
+
});
|
638
|
+
});
|
245
639
|
});
|
@@ -1,4 +1,9 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { toFile } from 'openai';
|
3
|
+
import { FileLike } from 'openai/uploads';
|
4
|
+
|
1
5
|
import { responsesAPIModels } from '@/const/models';
|
6
|
+
import { RuntimeImageGenParamsValue } from '@/libs/standard-parameters/meta-schema';
|
2
7
|
|
3
8
|
import { ChatStreamPayload, ModelProvider } from '../types';
|
4
9
|
import { processMultiProviderModelList } from '../utils/modelParse';
|
@@ -13,6 +18,42 @@ const prunePrefixes = ['o1', 'o3', 'o4', 'codex', 'computer-use'];
|
|
13
18
|
|
14
19
|
const oaiSearchContextSize = process.env.OPENAI_SEARCH_CONTEXT_SIZE; // low, medium, high
|
15
20
|
|
21
|
+
const log = debug('lobe-image:openai');
|
22
|
+
|
23
|
+
/**
|
24
|
+
* 将图片 URL 转换为 File 对象
|
25
|
+
* @param imageUrl - 图片 URL(可以是 HTTP URL 或 base64 data URL)
|
26
|
+
* @returns FileLike 对象
|
27
|
+
*/
|
28
|
+
export const convertImageUrlToFile = async (imageUrl: string): Promise<FileLike> => {
|
29
|
+
log('Converting image URL to File: %s', imageUrl.startsWith('data:') ? 'base64 data' : imageUrl);
|
30
|
+
|
31
|
+
let buffer: Buffer;
|
32
|
+
let mimeType: string;
|
33
|
+
|
34
|
+
if (imageUrl.startsWith('data:')) {
|
35
|
+
// 处理 base64 data URL
|
36
|
+
log('Processing base64 image data');
|
37
|
+
const [mimeTypePart, base64Data] = imageUrl.split(',');
|
38
|
+
mimeType = mimeTypePart.split(':')[1].split(';')[0];
|
39
|
+
buffer = Buffer.from(base64Data, 'base64');
|
40
|
+
} else {
|
41
|
+
// 处理 HTTP URL
|
42
|
+
log('Fetching image from URL: %s', imageUrl);
|
43
|
+
const response = await fetch(imageUrl);
|
44
|
+
if (!response.ok) {
|
45
|
+
throw new Error(`Failed to fetch image from ${imageUrl}: ${response.statusText}`);
|
46
|
+
}
|
47
|
+
buffer = Buffer.from(await response.arrayBuffer());
|
48
|
+
mimeType = response.headers.get('content-type') || 'image/png';
|
49
|
+
}
|
50
|
+
|
51
|
+
log('Successfully converted image to buffer, size: %s, mimeType: %s', buffer.length, mimeType);
|
52
|
+
|
53
|
+
// 使用 OpenAI 的 toFile 方法创建 File 对象
|
54
|
+
return toFile(buffer, `image.${mimeType.split('/')[1]}`, { type: mimeType });
|
55
|
+
};
|
56
|
+
|
16
57
|
export const LobeOpenAI = createOpenAICompatibleRuntime({
|
17
58
|
baseURL: 'https://api.openai.com/v1',
|
18
59
|
chatCompletion: {
|
@@ -47,6 +88,91 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
|
|
47
88
|
return { ...rest, model, stream: payload.stream ?? true };
|
48
89
|
},
|
49
90
|
},
|
91
|
+
createImage: async (payload) => {
|
92
|
+
const { model, params, client } = payload;
|
93
|
+
log('Creating image with model: %s and params: %O', model, params);
|
94
|
+
|
95
|
+
const defaultInput = {
|
96
|
+
n: 1,
|
97
|
+
};
|
98
|
+
|
99
|
+
// 映射参数名称,将 imageUrls 映射为 image
|
100
|
+
const paramsMap = new Map<RuntimeImageGenParamsValue, string>([['imageUrls', 'image']]);
|
101
|
+
const userInput: Record<string, any> = Object.fromEntries(
|
102
|
+
Object.entries(params).map(([key, value]) => [
|
103
|
+
paramsMap.get(key as RuntimeImageGenParamsValue) ?? key,
|
104
|
+
value,
|
105
|
+
]),
|
106
|
+
);
|
107
|
+
|
108
|
+
const isImageEdit = Array.isArray(userInput.image) && userInput.image.length > 0;
|
109
|
+
// 如果有 imageUrls 参数,将其转换为 File 对象
|
110
|
+
if (isImageEdit) {
|
111
|
+
log('Converting imageUrls to File objects: %O', userInput.image);
|
112
|
+
try {
|
113
|
+
// 转换所有图片 URL 为 File 对象
|
114
|
+
const imageFiles = await Promise.all(
|
115
|
+
userInput.image.map((url: string) => convertImageUrlToFile(url)),
|
116
|
+
);
|
117
|
+
|
118
|
+
log('Successfully converted %d images to File objects', imageFiles.length);
|
119
|
+
|
120
|
+
// 根据官方文档,如果有多个图片,传递数组;如果只有一个,传递单个 File
|
121
|
+
userInput.image = imageFiles.length === 1 ? imageFiles[0] : imageFiles;
|
122
|
+
} catch (error) {
|
123
|
+
log('Error converting imageUrls to File objects: %O', error);
|
124
|
+
throw new Error(`Failed to convert image URLs to File objects: ${error}`);
|
125
|
+
}
|
126
|
+
} else {
|
127
|
+
delete userInput.image;
|
128
|
+
}
|
129
|
+
|
130
|
+
if (userInput.size === 'auto') {
|
131
|
+
delete userInput.size;
|
132
|
+
}
|
133
|
+
|
134
|
+
const options = {
|
135
|
+
model,
|
136
|
+
...defaultInput,
|
137
|
+
...(userInput as any),
|
138
|
+
};
|
139
|
+
|
140
|
+
log('options: %O', options);
|
141
|
+
|
142
|
+
// 判断是否为图片编辑操作
|
143
|
+
const img = isImageEdit
|
144
|
+
? await client.images.edit(options)
|
145
|
+
: await client.images.generate(options);
|
146
|
+
|
147
|
+
// 检查响应数据的完整性
|
148
|
+
if (!img || !img.data || !Array.isArray(img.data) || img.data.length === 0) {
|
149
|
+
log('Invalid image response: missing data array');
|
150
|
+
throw new Error('Invalid image response: missing or empty data array');
|
151
|
+
}
|
152
|
+
|
153
|
+
const imageData = img.data[0];
|
154
|
+
if (!imageData) {
|
155
|
+
log('Invalid image response: first data item is null/undefined');
|
156
|
+
throw new Error('Invalid image response: first data item is null or undefined');
|
157
|
+
}
|
158
|
+
|
159
|
+
if (!imageData.b64_json) {
|
160
|
+
log('Invalid image response: missing b64_json field');
|
161
|
+
throw new Error('Invalid image response: missing b64_json field');
|
162
|
+
}
|
163
|
+
|
164
|
+
// 确定图片的 MIME 类型,默认为 PNG
|
165
|
+
const mimeType = 'image/png'; // OpenAI 图片生成默认返回 PNG 格式
|
166
|
+
|
167
|
+
// 将 base64 字符串转换为完整的 data URL
|
168
|
+
const dataUrl = `data:${mimeType};base64,${imageData.b64_json}`;
|
169
|
+
|
170
|
+
log('Successfully converted base64 to data URL, length: %d', dataUrl.length);
|
171
|
+
|
172
|
+
return {
|
173
|
+
imageUrl: dataUrl,
|
174
|
+
};
|
175
|
+
},
|
50
176
|
debug: {
|
51
177
|
chatCompletion: () => process.env.DEBUG_OPENAI_CHAT_COMPLETION === '1',
|
52
178
|
responses: () => process.env.DEBUG_OPENAI_RESPONSES === '1',
|
@@ -79,9 +205,9 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
|
|
79
205
|
return pruneReasoningPayload({
|
80
206
|
...rest,
|
81
207
|
model,
|
82
|
-
reasoning: payload.reasoning
|
83
|
-
{ ...payload.reasoning, summary: 'auto' }
|
84
|
-
{ summary: 'auto' },
|
208
|
+
reasoning: payload.reasoning
|
209
|
+
? { ...payload.reasoning, summary: 'auto' }
|
210
|
+
: { summary: 'auto' },
|
85
211
|
stream: payload.stream ?? true,
|
86
212
|
tools: openaiTools as any,
|
87
213
|
// computer-use series must set truncation as auto
|
@@ -8,6 +8,7 @@ import { LobeBedrockAI } from './bedrock';
|
|
8
8
|
import { LobeCloudflareAI } from './cloudflare';
|
9
9
|
import { LobeCohereAI } from './cohere';
|
10
10
|
import { LobeDeepSeekAI } from './deepseek';
|
11
|
+
import { LobeFalAI } from './fal';
|
11
12
|
import { LobeFireworksAI } from './fireworksai';
|
12
13
|
import { LobeGiteeAI } from './giteeai';
|
13
14
|
import { LobeGithubAI } from './github';
|
@@ -63,6 +64,7 @@ export const providerRuntimeMap = {
|
|
63
64
|
cloudflare: LobeCloudflareAI,
|
64
65
|
cohere: LobeCohereAI,
|
65
66
|
deepseek: LobeDeepSeekAI,
|
67
|
+
fal: LobeFalAI,
|
66
68
|
fireworksai: LobeFireworksAI,
|
67
69
|
giteeai: LobeGiteeAI,
|
68
70
|
github: LobeGithubAI,
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { RuntimeImageGenParams } from '@/libs/standard-parameters/meta-schema';
|
2
|
+
|
3
|
+
export type CreateImagePayload = {
|
4
|
+
model: string;
|
5
|
+
params: RuntimeImageGenParams;
|
6
|
+
};
|
7
|
+
|
8
|
+
export type CreateImageResponse = {
|
9
|
+
/**
|
10
|
+
* 图片的高度
|
11
|
+
*/
|
12
|
+
height?: number;
|
13
|
+
|
14
|
+
/**
|
15
|
+
* 一般是 provider 家的 cdn 地址,多数一段时间后就会失效,需要重新请求
|
16
|
+
*/
|
17
|
+
imageUrl: string;
|
18
|
+
// 为什么返回宽高?
|
19
|
+
// 1. 你给的配置宽度和真正最后生成的宽度可能并不一致,这个需要存储到 generation 的 asset 中
|
20
|
+
// 2. 我需要图片宽高用于计算是否需要生成缩略图,很多 provider 一般都会返回宽高,这样也许我可以省去一些计算
|
21
|
+
/**
|
22
|
+
* 图片的宽度
|
23
|
+
*/
|
24
|
+
width?: number;
|
25
|
+
};
|
@@ -22,6 +22,7 @@ import {
|
|
22
22
|
TextToSpeechOptions,
|
23
23
|
TextToSpeechPayload,
|
24
24
|
} from '../../types';
|
25
|
+
import { CreateImagePayload, CreateImageResponse } from '../../types/image';
|
25
26
|
import { AgentRuntimeError } from '../createError';
|
26
27
|
import { debugResponse, debugStream } from '../debugStream';
|
27
28
|
import { desensitizeUrl } from '../desensitizeUrl';
|
@@ -81,6 +82,7 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
|
|
81
82
|
noUserId?: boolean;
|
82
83
|
};
|
83
84
|
constructorOptions?: ConstructorOptions<T>;
|
85
|
+
createImage?: (payload: CreateImagePayload & { client: OpenAI }) => Promise<CreateImageResponse>;
|
84
86
|
customClient?: CustomClientOptions<T>;
|
85
87
|
debug?: {
|
86
88
|
chatCompletion: () => boolean;
|
@@ -166,6 +168,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
166
168
|
debug,
|
167
169
|
constructorOptions,
|
168
170
|
chatCompletion,
|
171
|
+
createImage,
|
169
172
|
models,
|
170
173
|
customClient,
|
171
174
|
responses,
|
@@ -307,6 +310,13 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
|
|
307
310
|
}
|
308
311
|
}
|
309
312
|
|
313
|
+
async createImage(payload: CreateImagePayload) {
|
314
|
+
return createImage!({
|
315
|
+
...payload,
|
316
|
+
client: this.client,
|
317
|
+
});
|
318
|
+
}
|
319
|
+
|
310
320
|
async models() {
|
311
321
|
if (typeof models === 'function') return models({ client: this.client });
|
312
322
|
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './meta-schema';
|