@lobehub/chat 1.98.2 → 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 +27 -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 +9 -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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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 +15 -0
- 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
package/src/utils/url.test.ts
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
+
import { vi } from 'vitest';
|
2
|
+
|
1
3
|
import { pathString } from './url';
|
4
|
+
import { inferContentTypeFromImageUrl, inferFileExtensionFromImageUrl } from './url';
|
2
5
|
|
3
6
|
describe('pathString', () => {
|
4
|
-
it('
|
7
|
+
it('should handle basic path', () => {
|
5
8
|
const result = pathString('/home');
|
6
9
|
expect(result).toBe('/home');
|
7
10
|
});
|
8
11
|
|
9
|
-
it('
|
12
|
+
it('should handle path with search parameters', () => {
|
10
13
|
const result = pathString('/home', { search: 'id=1&name=test' });
|
11
14
|
expect(result).toBe('/home?id=1&name=test');
|
12
15
|
});
|
13
16
|
|
14
|
-
it('
|
17
|
+
it('should handle path with hash', () => {
|
15
18
|
const result = pathString('/home', { hash: 'top' });
|
16
19
|
expect(result).toBe('/home#top');
|
17
20
|
|
@@ -19,33 +22,379 @@ describe('pathString', () => {
|
|
19
22
|
expect(result2).toBe('/home#hash=abc');
|
20
23
|
});
|
21
24
|
|
22
|
-
it('path
|
25
|
+
it('should handle relative path', () => {
|
23
26
|
const result = pathString('./home');
|
24
27
|
expect(result).toBe('/home');
|
25
28
|
});
|
26
29
|
|
27
|
-
it('path
|
30
|
+
it('should handle absolute path', () => {
|
28
31
|
const result = pathString('/home');
|
29
32
|
expect(result).toBe('/home');
|
30
33
|
});
|
31
34
|
|
32
|
-
it('path
|
35
|
+
it('should handle path with protocol', () => {
|
33
36
|
const result = pathString('https://www.example.com/home');
|
34
37
|
expect(result).toBe('https://www.example.com/home');
|
35
38
|
});
|
36
39
|
|
37
|
-
it('path
|
40
|
+
it('should handle path with hostname', () => {
|
38
41
|
const result = pathString('//www.example.com/home');
|
39
42
|
expect(result).toBe('https://www.example.com/home');
|
40
43
|
});
|
41
44
|
|
42
|
-
it('path
|
45
|
+
it('should handle path with port number', () => {
|
43
46
|
const result = pathString('//www.example.com:8080/home');
|
44
47
|
expect(result).toBe('https://www.example.com:8080/home');
|
45
48
|
});
|
46
49
|
|
47
|
-
it('path
|
50
|
+
it('should handle path with special characters', () => {
|
48
51
|
const result = pathString('/home/测试');
|
49
52
|
expect(result).toBe('/home/%E6%B5%8B%E8%AF%95');
|
50
53
|
});
|
51
54
|
});
|
55
|
+
|
56
|
+
describe('inferContentTypeFromImageUrl', () => {
|
57
|
+
it('should return correct MIME type for jpg images', () => {
|
58
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.jpg');
|
59
|
+
expect(result).toBe('image/jpeg');
|
60
|
+
});
|
61
|
+
|
62
|
+
it('should return correct MIME type for png images', () => {
|
63
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.png');
|
64
|
+
expect(result).toBe('image/png');
|
65
|
+
});
|
66
|
+
|
67
|
+
it('should return correct MIME type for webp images', () => {
|
68
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.webp');
|
69
|
+
expect(result).toBe('image/webp');
|
70
|
+
});
|
71
|
+
|
72
|
+
it('should return correct MIME type for gif images', () => {
|
73
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.gif');
|
74
|
+
expect(result).toBe('image/gif');
|
75
|
+
});
|
76
|
+
|
77
|
+
it('should handle uppercase extensions', () => {
|
78
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.JPG');
|
79
|
+
expect(result).toBe('image/jpeg');
|
80
|
+
});
|
81
|
+
|
82
|
+
it('should handle URLs with query parameters', () => {
|
83
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.png?v=123&size=large');
|
84
|
+
expect(result).toBe('image/png');
|
85
|
+
});
|
86
|
+
|
87
|
+
it('should handle URLs with hash fragments', () => {
|
88
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.jpg#section');
|
89
|
+
expect(result).toBe('image/jpeg');
|
90
|
+
});
|
91
|
+
|
92
|
+
it('should throw error when no extension', () => {
|
93
|
+
expect(() => {
|
94
|
+
inferContentTypeFromImageUrl('https://example.com/image');
|
95
|
+
}).toThrow('Invalid image url: https://example.com/image');
|
96
|
+
});
|
97
|
+
|
98
|
+
it('should throw error when only dot without extension', () => {
|
99
|
+
expect(() => {
|
100
|
+
inferContentTypeFromImageUrl('https://example.com/image.');
|
101
|
+
}).toThrow('Invalid image url: https://example.com/image.');
|
102
|
+
});
|
103
|
+
|
104
|
+
it('should handle multiple dots in path', () => {
|
105
|
+
const result = inferContentTypeFromImageUrl('https://example.com/my.folder/image.test.png');
|
106
|
+
expect(result).toBe('image/png');
|
107
|
+
});
|
108
|
+
|
109
|
+
it('should handle complex query parameters and hash combination', () => {
|
110
|
+
const result = inferContentTypeFromImageUrl(
|
111
|
+
'https://example.com/image.jpeg?width=800&height=600&format=jpeg#preview',
|
112
|
+
);
|
113
|
+
expect(result).toBe('image/jpeg');
|
114
|
+
});
|
115
|
+
|
116
|
+
it('should handle mixed case extensions', () => {
|
117
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.JpEg');
|
118
|
+
expect(result).toBe('image/jpeg');
|
119
|
+
});
|
120
|
+
|
121
|
+
it('should handle BMP format', () => {
|
122
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.bmp');
|
123
|
+
expect(result).toBe('image/bmp');
|
124
|
+
});
|
125
|
+
|
126
|
+
it('should handle TIFF format', () => {
|
127
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.tiff');
|
128
|
+
expect(result).toBe('image/tiff');
|
129
|
+
});
|
130
|
+
|
131
|
+
it('should handle TIF format', () => {
|
132
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.tif');
|
133
|
+
expect(result).toBe('image/tiff');
|
134
|
+
});
|
135
|
+
|
136
|
+
it('should handle SVG format', () => {
|
137
|
+
const result = inferContentTypeFromImageUrl('https://example.com/image.svg');
|
138
|
+
expect(result).toBe('image/svg+xml');
|
139
|
+
});
|
140
|
+
|
141
|
+
it('should throw error for invalid URLs', () => {
|
142
|
+
expect(() => {
|
143
|
+
inferContentTypeFromImageUrl('invalid-url');
|
144
|
+
}).toThrow('Invalid image url: invalid-url');
|
145
|
+
});
|
146
|
+
|
147
|
+
it('should throw error for empty string', () => {
|
148
|
+
expect(() => {
|
149
|
+
inferContentTypeFromImageUrl('');
|
150
|
+
}).toThrow('Invalid image url: ');
|
151
|
+
});
|
152
|
+
|
153
|
+
it('should throw error for non-image extensions', () => {
|
154
|
+
expect(() => {
|
155
|
+
inferContentTypeFromImageUrl('https://example.com/document.txt');
|
156
|
+
}).toThrow('Invalid image url: https://example.com/document.txt');
|
157
|
+
});
|
158
|
+
|
159
|
+
it('should throw error for unknown extensions', () => {
|
160
|
+
expect(() => {
|
161
|
+
inferContentTypeFromImageUrl('https://example.com/file.unknownext');
|
162
|
+
}).toThrow('Invalid image url: https://example.com/file.unknownext');
|
163
|
+
});
|
164
|
+
|
165
|
+
it('should handle URLs with port numbers', () => {
|
166
|
+
const result = inferContentTypeFromImageUrl('https://example.com:8080/image.png');
|
167
|
+
expect(result).toBe('image/png');
|
168
|
+
});
|
169
|
+
|
170
|
+
it('should handle deeply nested paths', () => {
|
171
|
+
const result = inferContentTypeFromImageUrl(
|
172
|
+
'https://cdn.example.com/assets/images/gallery/2024/photo.jpg',
|
173
|
+
);
|
174
|
+
expect(result).toBe('image/jpeg');
|
175
|
+
});
|
176
|
+
|
177
|
+
it('should handle encoded filenames', () => {
|
178
|
+
const result = inferContentTypeFromImageUrl(
|
179
|
+
'https://example.com/images/%E5%9B%BE%E7%89%87.png',
|
180
|
+
);
|
181
|
+
expect(result).toBe('image/png');
|
182
|
+
});
|
183
|
+
|
184
|
+
it('should handle protocol-relative URLs', () => {
|
185
|
+
const result = inferContentTypeFromImageUrl('//example.com/image.webp');
|
186
|
+
expect(result).toBe('image/webp');
|
187
|
+
});
|
188
|
+
|
189
|
+
it('should handle relative paths', () => {
|
190
|
+
const result = inferContentTypeFromImageUrl('generations/images/photo.jpg');
|
191
|
+
expect(result).toBe('image/jpeg');
|
192
|
+
});
|
193
|
+
|
194
|
+
it('should handle relative paths with complex filenames', () => {
|
195
|
+
const result = inferContentTypeFromImageUrl(
|
196
|
+
'generations/images/2NPfAQAMNxXPi82mzOHog_1056x1136_20250702_110911_raw.png',
|
197
|
+
);
|
198
|
+
expect(result).toBe('image/png');
|
199
|
+
});
|
200
|
+
|
201
|
+
it('should handle relative paths with query parameters', () => {
|
202
|
+
const result = inferContentTypeFromImageUrl('images/photo.webp?v=123');
|
203
|
+
expect(result).toBe('image/webp');
|
204
|
+
});
|
205
|
+
|
206
|
+
it('should handle relative paths with hash fragments', () => {
|
207
|
+
const result = inferContentTypeFromImageUrl('assets/images/banner.gif#preview');
|
208
|
+
expect(result).toBe('image/gif');
|
209
|
+
});
|
210
|
+
|
211
|
+
it('should throw error for single character extensions (if no valid MIME type)', () => {
|
212
|
+
expect(() => {
|
213
|
+
inferContentTypeFromImageUrl('https://example.com/file.x');
|
214
|
+
}).toThrow('Invalid image url: https://example.com/file.x');
|
215
|
+
});
|
216
|
+
|
217
|
+
it('should throw error for dot at end of path', () => {
|
218
|
+
expect(() => {
|
219
|
+
inferContentTypeFromImageUrl('https://example.com/path/.');
|
220
|
+
}).toThrow('Invalid image url: https://example.com/path/.');
|
221
|
+
});
|
222
|
+
|
223
|
+
it('should handle malformed URLs that result in no valid extension', () => {
|
224
|
+
// These URLs will be processed by inferFileExtensionFromImageUrl and return empty string
|
225
|
+
const invalidUrls = [
|
226
|
+
'data:image/jpeg;base64,invalid', // No file extension in path
|
227
|
+
'javascript:alert("test")', // No file extension
|
228
|
+
'https://example.com/file', // No extension
|
229
|
+
'https://example.com/file.', // Dot without extension
|
230
|
+
'ftp://example.com/document.pdf', // Valid URL but non-image extension
|
231
|
+
];
|
232
|
+
|
233
|
+
invalidUrls.forEach((url) => {
|
234
|
+
expect(() => {
|
235
|
+
inferContentTypeFromImageUrl(url);
|
236
|
+
}).toThrow(/Invalid image url:/);
|
237
|
+
});
|
238
|
+
});
|
239
|
+
|
240
|
+
it('should handle all supported image formats consistently', () => {
|
241
|
+
const testCases = [
|
242
|
+
{ extension: 'jpg', expected: 'image/jpeg' },
|
243
|
+
{ extension: 'jpeg', expected: 'image/jpeg' },
|
244
|
+
{ extension: 'png', expected: 'image/png' },
|
245
|
+
{ extension: 'webp', expected: 'image/webp' },
|
246
|
+
{ extension: 'gif', expected: 'image/gif' },
|
247
|
+
{ extension: 'bmp', expected: 'image/bmp' },
|
248
|
+
{ extension: 'svg', expected: 'image/svg+xml' },
|
249
|
+
{ extension: 'tiff', expected: 'image/tiff' },
|
250
|
+
{ extension: 'tif', expected: 'image/tiff' },
|
251
|
+
];
|
252
|
+
|
253
|
+
testCases.forEach(({ extension, expected }) => {
|
254
|
+
const result = inferContentTypeFromImageUrl(`https://example.com/image.${extension}`);
|
255
|
+
expect(result).toBe(expected);
|
256
|
+
});
|
257
|
+
});
|
258
|
+
});
|
259
|
+
|
260
|
+
describe('inferFileExtensionFromImageUrl', () => {
|
261
|
+
it('should return jpg extension', () => {
|
262
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.jpg');
|
263
|
+
expect(result).toBe('jpg');
|
264
|
+
});
|
265
|
+
|
266
|
+
it('should return png extension', () => {
|
267
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.png');
|
268
|
+
expect(result).toBe('png');
|
269
|
+
});
|
270
|
+
|
271
|
+
it('should return webp extension', () => {
|
272
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.webp');
|
273
|
+
expect(result).toBe('webp');
|
274
|
+
});
|
275
|
+
|
276
|
+
it('should handle jpeg extension', () => {
|
277
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.jpeg');
|
278
|
+
expect(result).toBe('jpeg');
|
279
|
+
});
|
280
|
+
|
281
|
+
it('should handle gif extension', () => {
|
282
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.gif');
|
283
|
+
expect(result).toBe('gif');
|
284
|
+
});
|
285
|
+
|
286
|
+
it('should handle svg extension', () => {
|
287
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.svg');
|
288
|
+
expect(result).toBe('svg');
|
289
|
+
});
|
290
|
+
|
291
|
+
it('should handle uppercase extensions and convert to lowercase', () => {
|
292
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.PNG');
|
293
|
+
expect(result).toBe('png');
|
294
|
+
});
|
295
|
+
|
296
|
+
it('should handle mixed case extensions', () => {
|
297
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.JpEg');
|
298
|
+
expect(result).toBe('jpeg');
|
299
|
+
});
|
300
|
+
|
301
|
+
it('should handle URLs with query parameters', () => {
|
302
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.jpg?v=123&size=large');
|
303
|
+
expect(result).toBe('jpg');
|
304
|
+
});
|
305
|
+
|
306
|
+
it('should handle URLs with hash fragments', () => {
|
307
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.png#section');
|
308
|
+
expect(result).toBe('png');
|
309
|
+
});
|
310
|
+
|
311
|
+
it('should handle multiple dots in path', () => {
|
312
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/my.folder/image.test.webp');
|
313
|
+
expect(result).toBe('webp');
|
314
|
+
});
|
315
|
+
|
316
|
+
it('should return empty string when no extension', () => {
|
317
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image');
|
318
|
+
expect(result).toBe('');
|
319
|
+
});
|
320
|
+
|
321
|
+
it('should return empty string when only dot without extension', () => {
|
322
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/image.');
|
323
|
+
expect(result).toBe('');
|
324
|
+
});
|
325
|
+
|
326
|
+
it('should return empty string for non-image extensions', () => {
|
327
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/document.txt');
|
328
|
+
expect(result).toBe('');
|
329
|
+
});
|
330
|
+
|
331
|
+
it('should return empty string for other format extensions', () => {
|
332
|
+
const result = inferFileExtensionFromImageUrl('https://example.com/video.mp4');
|
333
|
+
expect(result).toBe('');
|
334
|
+
});
|
335
|
+
|
336
|
+
it('should handle invalid URLs and return empty string', () => {
|
337
|
+
const result = inferFileExtensionFromImageUrl('invalid-url');
|
338
|
+
expect(result).toBe('');
|
339
|
+
});
|
340
|
+
|
341
|
+
it('should handle empty string URLs and return empty string', () => {
|
342
|
+
const result = inferFileExtensionFromImageUrl('');
|
343
|
+
expect(result).toBe('');
|
344
|
+
});
|
345
|
+
|
346
|
+
it('should handle all supported image extensions', () => {
|
347
|
+
const supportedExtensions = ['webp', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'tiff', 'tif'];
|
348
|
+
|
349
|
+
supportedExtensions.forEach((ext) => {
|
350
|
+
const result = inferFileExtensionFromImageUrl(`https://example.com/image.${ext}`);
|
351
|
+
expect(result).toBe(ext);
|
352
|
+
});
|
353
|
+
});
|
354
|
+
|
355
|
+
it('should handle URLs with port numbers', () => {
|
356
|
+
const result = inferFileExtensionFromImageUrl('https://example.com:8080/image.jpg');
|
357
|
+
expect(result).toBe('jpg');
|
358
|
+
});
|
359
|
+
|
360
|
+
it('should handle subdomain URLs', () => {
|
361
|
+
const result = inferFileExtensionFromImageUrl('https://cdn.example.com/images/photo.webp');
|
362
|
+
expect(result).toBe('webp');
|
363
|
+
});
|
364
|
+
|
365
|
+
it('should handle deep path URLs', () => {
|
366
|
+
const result = inferFileExtensionFromImageUrl(
|
367
|
+
'https://example.com/assets/images/gallery/photo.png',
|
368
|
+
);
|
369
|
+
expect(result).toBe('png');
|
370
|
+
});
|
371
|
+
|
372
|
+
it('should handle encoded URLs', () => {
|
373
|
+
const result = inferFileExtensionFromImageUrl(
|
374
|
+
'https://example.com/images/%E5%9B%BE%E7%89%87.jpg',
|
375
|
+
);
|
376
|
+
expect(result).toBe('jpg');
|
377
|
+
});
|
378
|
+
|
379
|
+
it('should handle relative paths', () => {
|
380
|
+
const result = inferFileExtensionFromImageUrl('generations/images/photo.jpg');
|
381
|
+
expect(result).toBe('jpg');
|
382
|
+
});
|
383
|
+
|
384
|
+
it('should handle relative paths with complex filenames', () => {
|
385
|
+
const result = inferFileExtensionFromImageUrl(
|
386
|
+
'generations/images/2NPfAQAMNxXPi82mzOHog_1056x1136_20250702_110911_raw.png',
|
387
|
+
);
|
388
|
+
expect(result).toBe('png');
|
389
|
+
});
|
390
|
+
|
391
|
+
it('should handle relative paths with query parameters', () => {
|
392
|
+
const result = inferFileExtensionFromImageUrl('images/photo.webp?v=123');
|
393
|
+
expect(result).toBe('webp');
|
394
|
+
});
|
395
|
+
|
396
|
+
it('should handle relative paths with hash fragments', () => {
|
397
|
+
const result = inferFileExtensionFromImageUrl('assets/images/banner.gif#preview');
|
398
|
+
expect(result).toBe('gif');
|
399
|
+
});
|
400
|
+
});
|
package/src/utils/url.ts
CHANGED
@@ -1,8 +1,26 @@
|
|
1
|
+
import mime from 'mime';
|
2
|
+
|
1
3
|
/**
|
2
4
|
* Build a path string from a path and a hash/search object
|
3
|
-
*
|
4
|
-
*
|
5
|
-
*
|
5
|
+
*
|
6
|
+
* This function constructs a properly formatted URL path by combining a base path
|
7
|
+
* with optional hash and search parameters. It uses URL constructor for proper
|
8
|
+
* encoding and formatting while removing the temporary base domain.
|
9
|
+
*
|
10
|
+
* @param path - The base path (can be relative, absolute, or include protocol)
|
11
|
+
* @param options - Optional configuration object
|
12
|
+
* @param options.hash - Hash fragment to append (with or without leading #)
|
13
|
+
* @param options.search - Search/query parameters to append (with or without leading ?)
|
14
|
+
* @returns Formatted path string with hash and search parameters
|
15
|
+
*
|
16
|
+
* @example
|
17
|
+
* ```typescript
|
18
|
+
* pathString('/home') // '/home'
|
19
|
+
* pathString('/home', { search: 'id=1&name=test' }) // '/home?id=1&name=test'
|
20
|
+
* pathString('/home', { hash: 'top' }) // '/home#top'
|
21
|
+
* pathString('./home') // '/home'
|
22
|
+
* pathString('https://example.com/path') // 'https://example.com/path'
|
23
|
+
* ```
|
6
24
|
*/
|
7
25
|
export const pathString = (
|
8
26
|
path: string,
|
@@ -14,10 +32,94 @@ export const pathString = (
|
|
14
32
|
search?: string;
|
15
33
|
} = {},
|
16
34
|
) => {
|
35
|
+
// Use a temporary base URL for proper URL parsing and formatting
|
17
36
|
const tempBase = 'https://a.com';
|
18
37
|
const url = new URL(path, tempBase);
|
19
38
|
|
39
|
+
// Add hash fragment if provided
|
20
40
|
if (hash) url.hash = hash;
|
41
|
+
// Add search parameters if provided
|
21
42
|
if (search) url.search = search;
|
43
|
+
|
44
|
+
// Return the formatted URL without the temporary base
|
22
45
|
return url.toString().replace(tempBase, '');
|
23
46
|
};
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Get file extension from URL
|
50
|
+
*
|
51
|
+
* This function extracts the file extension from a URL's pathname and validates it against
|
52
|
+
* common image formats. It properly handles URLs with query parameters, hash fragments,
|
53
|
+
* relative paths, and various edge cases. Returns empty string for invalid cases.
|
54
|
+
*
|
55
|
+
* @param url - The URL to extract extension from (can be relative, absolute, or include query parameters and hash fragments)
|
56
|
+
* @returns file extension without dot (e.g., 'jpg', 'png', 'webp'), or empty string for invalid cases
|
57
|
+
*
|
58
|
+
* @example
|
59
|
+
* ```typescript
|
60
|
+
* inferFileExtensionFromImageUrl('https://example.com/image.jpg') // 'jpg'
|
61
|
+
* inferFileExtensionFromImageUrl('https://example.com/image.png?v=123') // 'png'
|
62
|
+
* inferFileExtensionFromImageUrl('https://example.com/image.webp#section') // 'webp'
|
63
|
+
* inferFileExtensionFromImageUrl('generations/images/photo.png') // 'png'
|
64
|
+
* inferFileExtensionFromImageUrl('https://example.com/document.txt') // '' (empty string)
|
65
|
+
* inferFileExtensionFromImageUrl('invalid-url') // '' (empty string)
|
66
|
+
* ```
|
67
|
+
*/
|
68
|
+
export const inferFileExtensionFromImageUrl = (url: string): string => {
|
69
|
+
// Use a temporary base URL for proper URL parsing and formatting (handles relative paths)
|
70
|
+
const tempBase = 'https://a.com';
|
71
|
+
const urlObj = new URL(url, tempBase);
|
72
|
+
const pathname = urlObj.pathname;
|
73
|
+
|
74
|
+
// Find the last dot in the pathname to get the file extension
|
75
|
+
const lastDotIndex = pathname.lastIndexOf('.');
|
76
|
+
if (lastDotIndex === -1) return ''; // No extension found, return empty string
|
77
|
+
|
78
|
+
// Extract extension after the last dot and convert to lowercase
|
79
|
+
const extension = pathname.slice(Math.max(0, lastDotIndex + 1)).toLowerCase();
|
80
|
+
|
81
|
+
// Validate against common image extensions
|
82
|
+
const validImageExtensions = ['webp', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'tiff', 'tif'];
|
83
|
+
if (validImageExtensions.includes(extension)) {
|
84
|
+
return extension;
|
85
|
+
}
|
86
|
+
|
87
|
+
// Default fallback for non-image extensions
|
88
|
+
return '';
|
89
|
+
};
|
90
|
+
|
91
|
+
/**
|
92
|
+
* Infer content type (MIME type) from an image URL
|
93
|
+
*
|
94
|
+
* This function extracts the file extension from a URL and returns the corresponding MIME type.
|
95
|
+
* It properly handles URLs with query parameters, hash fragments, relative paths, and various edge cases.
|
96
|
+
*
|
97
|
+
* @param url - The image URL to analyze (can be relative, absolute, or include query parameters and hash fragments)
|
98
|
+
* @returns MIME type string (e.g., 'image/jpeg', 'image/png')
|
99
|
+
* @throws {Error} When the URL doesn't contain a valid file extension
|
100
|
+
*
|
101
|
+
* @example
|
102
|
+
* ```typescript
|
103
|
+
* inferContentTypeFromImageUrl('https://example.com/image.jpg') // 'image/jpeg'
|
104
|
+
* inferContentTypeFromImageUrl('https://example.com/image.png?v=123') // 'image/png'
|
105
|
+
* inferContentTypeFromImageUrl('https://example.com/image.webp#section') // 'image/webp'
|
106
|
+
* inferContentTypeFromImageUrl('generations/images/photo.png') // 'image/png'
|
107
|
+
* ```
|
108
|
+
*/
|
109
|
+
export function inferContentTypeFromImageUrl(url: string) {
|
110
|
+
// Get the file extension using the dedicated function
|
111
|
+
// inferFileExtensionFromImageUrl only returns valid image extensions or empty string
|
112
|
+
const extension = inferFileExtensionFromImageUrl(url);
|
113
|
+
|
114
|
+
// If no valid extension found, throw error
|
115
|
+
if (!extension) {
|
116
|
+
throw new Error(`Invalid image url: ${url}`);
|
117
|
+
}
|
118
|
+
|
119
|
+
// Get MIME type using the mime library
|
120
|
+
// Since extension is guaranteed to be a valid image extension from the whitelist,
|
121
|
+
// mime.getType() will always return a valid image MIME type
|
122
|
+
const mimeType = mime.getType(extension);
|
123
|
+
|
124
|
+
return mimeType!; // Non-null assertion is safe due to whitelist validation
|
125
|
+
}
|
@@ -1,105 +0,0 @@
|
|
1
|
-
import { ProviderIcon } from '@lobehub/icons';
|
2
|
-
import { Button } from '@lobehub/ui';
|
3
|
-
import { memo, useMemo, useState } from 'react';
|
4
|
-
import { useTranslation } from 'react-i18next';
|
5
|
-
import { Center, Flexbox } from 'react-layout-kit';
|
6
|
-
|
7
|
-
import { ModelProvider } from '@/libs/model-runtime';
|
8
|
-
import { useChatStore } from '@/store/chat';
|
9
|
-
import { GlobalLLMProviderKey } from '@/types/user/settings';
|
10
|
-
|
11
|
-
import BedrockForm from './Bedrock';
|
12
|
-
import { LoadingContext } from './LoadingContext';
|
13
|
-
import ProviderApiKeyForm from './ProviderApiKeyForm';
|
14
|
-
|
15
|
-
interface APIKeyFormProps {
|
16
|
-
id: string;
|
17
|
-
provider?: string;
|
18
|
-
}
|
19
|
-
|
20
|
-
const APIKeyForm = memo<APIKeyFormProps>(({ id, provider }) => {
|
21
|
-
const { t } = useTranslation('error');
|
22
|
-
const [loading, setLoading] = useState(false);
|
23
|
-
|
24
|
-
const [resend, deleteMessage] = useChatStore((s) => [s.regenerateMessage, s.deleteMessage]);
|
25
|
-
|
26
|
-
const apiKeyPlaceholder = useMemo(() => {
|
27
|
-
switch (provider) {
|
28
|
-
case ModelProvider.Anthropic: {
|
29
|
-
return 'sk-ant_*****************************';
|
30
|
-
}
|
31
|
-
|
32
|
-
case ModelProvider.OpenRouter: {
|
33
|
-
return 'sk-or-********************************';
|
34
|
-
}
|
35
|
-
|
36
|
-
case ModelProvider.Perplexity: {
|
37
|
-
return 'pplx-********************************';
|
38
|
-
}
|
39
|
-
|
40
|
-
case ModelProvider.ZhiPu: {
|
41
|
-
return '*********************.*************';
|
42
|
-
}
|
43
|
-
|
44
|
-
case ModelProvider.Groq: {
|
45
|
-
return 'gsk_*****************************';
|
46
|
-
}
|
47
|
-
|
48
|
-
case ModelProvider.DeepSeek: {
|
49
|
-
return 'sk_******************************';
|
50
|
-
}
|
51
|
-
|
52
|
-
case ModelProvider.Qwen: {
|
53
|
-
return 'sk-********************************';
|
54
|
-
}
|
55
|
-
|
56
|
-
case ModelProvider.Github: {
|
57
|
-
return 'ghp_*****************************';
|
58
|
-
}
|
59
|
-
|
60
|
-
default: {
|
61
|
-
return '*********************************';
|
62
|
-
}
|
63
|
-
}
|
64
|
-
}, [provider]);
|
65
|
-
|
66
|
-
return (
|
67
|
-
<LoadingContext value={{ loading, setLoading }}>
|
68
|
-
<Center gap={16} style={{ maxWidth: 300 }}>
|
69
|
-
{provider === ModelProvider.Bedrock ? (
|
70
|
-
<BedrockForm />
|
71
|
-
) : (
|
72
|
-
<ProviderApiKeyForm
|
73
|
-
apiKeyPlaceholder={apiKeyPlaceholder}
|
74
|
-
avatar={<ProviderIcon provider={provider} size={80} type={'avatar'} />}
|
75
|
-
provider={provider as GlobalLLMProviderKey}
|
76
|
-
showEndpoint={provider === ModelProvider.OpenAI}
|
77
|
-
/>
|
78
|
-
)}
|
79
|
-
<Flexbox gap={12} width={'100%'}>
|
80
|
-
<Button
|
81
|
-
block
|
82
|
-
disabled={loading}
|
83
|
-
onClick={() => {
|
84
|
-
resend(id);
|
85
|
-
deleteMessage(id);
|
86
|
-
}}
|
87
|
-
style={{ marginTop: 8 }}
|
88
|
-
type={'primary'}
|
89
|
-
>
|
90
|
-
{t('unlock.confirm')}
|
91
|
-
</Button>
|
92
|
-
<Button
|
93
|
-
onClick={() => {
|
94
|
-
deleteMessage(id);
|
95
|
-
}}
|
96
|
-
>
|
97
|
-
{t('unlock.closeMessage')}
|
98
|
-
</Button>
|
99
|
-
</Flexbox>
|
100
|
-
</Center>
|
101
|
-
</LoadingContext>
|
102
|
-
);
|
103
|
-
});
|
104
|
-
|
105
|
-
export default APIKeyForm;
|
@@ -1,16 +0,0 @@
|
|
1
|
-
import { memo } from 'react';
|
2
|
-
|
3
|
-
import APIKeyForm from './APIKeyForm';
|
4
|
-
import { ErrorActionContainer } from './style';
|
5
|
-
|
6
|
-
interface InvalidAPIKeyProps {
|
7
|
-
id: string;
|
8
|
-
provider?: string;
|
9
|
-
}
|
10
|
-
const InvalidAPIKey = memo<InvalidAPIKeyProps>(({ id, provider }) => (
|
11
|
-
<ErrorActionContainer>
|
12
|
-
<APIKeyForm id={id} provider={provider} />
|
13
|
-
</ErrorActionContainer>
|
14
|
-
));
|
15
|
-
|
16
|
-
export default InvalidAPIKey;
|