@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
@@ -0,0 +1,614 @@
|
|
1
|
+
// @vitest-environment node
|
2
|
+
import { eq } from 'drizzle-orm';
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
4
|
+
|
5
|
+
import { LobeChatDatabase } from '@/database/type';
|
6
|
+
import { AsyncTaskStatus } from '@/types/asyncTask';
|
7
|
+
import { GenerationConfig } from '@/types/generation';
|
8
|
+
|
9
|
+
import {
|
10
|
+
NewGenerationBatch,
|
11
|
+
generationBatches,
|
12
|
+
generationTopics,
|
13
|
+
generations,
|
14
|
+
users,
|
15
|
+
} from '../../schemas';
|
16
|
+
import { GenerationBatchModel } from '../generationBatch';
|
17
|
+
import { getTestDB } from './_util';
|
18
|
+
|
19
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
20
|
+
|
21
|
+
// Mock FileService
|
22
|
+
const mockGetFullFileUrl = vi.fn();
|
23
|
+
vi.mock('@/server/services/file', () => ({
|
24
|
+
FileService: vi.fn().mockImplementation(() => ({
|
25
|
+
getFullFileUrl: mockGetFullFileUrl,
|
26
|
+
})),
|
27
|
+
}));
|
28
|
+
|
29
|
+
// Mock GenerationModel
|
30
|
+
const mockTransformGeneration = vi.fn();
|
31
|
+
vi.mock('../generation', () => ({
|
32
|
+
GenerationModel: vi.fn().mockImplementation(() => ({
|
33
|
+
transformGeneration: mockTransformGeneration,
|
34
|
+
})),
|
35
|
+
}));
|
36
|
+
|
37
|
+
const userId = 'generation-batch-test-user-id';
|
38
|
+
const otherUserId = 'other-user-id';
|
39
|
+
const generationBatchModel = new GenerationBatchModel(serverDB, userId);
|
40
|
+
|
41
|
+
// Test data
|
42
|
+
const testTopic = {
|
43
|
+
id: 'test-topic-id',
|
44
|
+
userId,
|
45
|
+
title: 'Test Generation Topic',
|
46
|
+
coverUrl: null,
|
47
|
+
};
|
48
|
+
|
49
|
+
const testBatch: NewGenerationBatch = {
|
50
|
+
userId, // Required by schema, but will be overridden by model
|
51
|
+
generationTopicId: 'test-topic-id',
|
52
|
+
provider: 'test-provider',
|
53
|
+
model: 'test-model',
|
54
|
+
prompt: 'Test prompt for image generation',
|
55
|
+
width: 1024,
|
56
|
+
height: 1024,
|
57
|
+
config: {
|
58
|
+
prompt: 'Test prompt for image generation',
|
59
|
+
imageUrls: ['image1.jpg', 'image2.jpg'],
|
60
|
+
width: 1024,
|
61
|
+
height: 1024,
|
62
|
+
} as GenerationConfig,
|
63
|
+
};
|
64
|
+
|
65
|
+
const testGeneration = {
|
66
|
+
id: 'test-gen-id',
|
67
|
+
generationBatchId: 'test-batch-id',
|
68
|
+
asyncTaskId: null, // Use null instead of invalid foreign key
|
69
|
+
fileId: null, // Use null instead of invalid foreign key
|
70
|
+
seed: 12345,
|
71
|
+
asset: {
|
72
|
+
type: 'image',
|
73
|
+
url: 'asset-url.jpg',
|
74
|
+
thumbnailUrl: 'thumbnail-url.jpg',
|
75
|
+
width: 1024,
|
76
|
+
height: 1024,
|
77
|
+
},
|
78
|
+
userId,
|
79
|
+
};
|
80
|
+
|
81
|
+
beforeEach(async () => {
|
82
|
+
// Clear all mocks
|
83
|
+
vi.clearAllMocks();
|
84
|
+
|
85
|
+
// Setup mock return values
|
86
|
+
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
|
87
|
+
mockTransformGeneration.mockResolvedValue({
|
88
|
+
id: testGeneration.id,
|
89
|
+
asset: {
|
90
|
+
url: 'https://example.com/asset-url.jpg',
|
91
|
+
thumbnailUrl: 'https://example.com/thumbnail-url.jpg',
|
92
|
+
width: 1024,
|
93
|
+
height: 1024,
|
94
|
+
},
|
95
|
+
seed: testGeneration.seed,
|
96
|
+
createdAt: new Date(),
|
97
|
+
asyncTaskId: testGeneration.asyncTaskId,
|
98
|
+
task: {
|
99
|
+
id: testGeneration.asyncTaskId,
|
100
|
+
status: AsyncTaskStatus.Success,
|
101
|
+
},
|
102
|
+
});
|
103
|
+
|
104
|
+
// Clear database and create test users
|
105
|
+
await serverDB.delete(users);
|
106
|
+
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
107
|
+
|
108
|
+
// Create test topic
|
109
|
+
await serverDB.insert(generationTopics).values(testTopic);
|
110
|
+
});
|
111
|
+
|
112
|
+
afterEach(async () => {
|
113
|
+
// Clean up database
|
114
|
+
await serverDB.delete(users);
|
115
|
+
});
|
116
|
+
|
117
|
+
describe('GenerationBatchModel', () => {
|
118
|
+
describe('create', () => {
|
119
|
+
it('should create a new generation batch', async () => {
|
120
|
+
const result = await generationBatchModel.create(testBatch);
|
121
|
+
|
122
|
+
expect(result.id).toBeDefined();
|
123
|
+
expect(result).toMatchObject({
|
124
|
+
...testBatch,
|
125
|
+
userId,
|
126
|
+
});
|
127
|
+
|
128
|
+
// Verify in database
|
129
|
+
const dbBatch = await serverDB.query.generationBatches.findFirst({
|
130
|
+
where: eq(generationBatches.id, result.id),
|
131
|
+
});
|
132
|
+
expect(dbBatch).toMatchObject({
|
133
|
+
...testBatch,
|
134
|
+
userId,
|
135
|
+
});
|
136
|
+
});
|
137
|
+
|
138
|
+
it('should automatically set userId when creating batch', async () => {
|
139
|
+
// Create batch data without userId to test auto-assignment
|
140
|
+
const batchWithoutUserId = {
|
141
|
+
generationTopicId: 'test-topic-id',
|
142
|
+
provider: 'test-provider',
|
143
|
+
model: 'test-model',
|
144
|
+
prompt: 'Test prompt for image generation',
|
145
|
+
width: 1024,
|
146
|
+
height: 1024,
|
147
|
+
config: {
|
148
|
+
prompt: 'Test prompt for image generation',
|
149
|
+
imageUrls: ['image1.jpg', 'image2.jpg'],
|
150
|
+
width: 1024,
|
151
|
+
height: 1024,
|
152
|
+
} as GenerationConfig,
|
153
|
+
};
|
154
|
+
|
155
|
+
const result = await generationBatchModel.create(batchWithoutUserId as NewGenerationBatch);
|
156
|
+
|
157
|
+
expect(result.userId).toBe(userId);
|
158
|
+
});
|
159
|
+
});
|
160
|
+
|
161
|
+
describe('findById', () => {
|
162
|
+
it('should find a generation batch by id', async () => {
|
163
|
+
const [createdBatch] = await serverDB
|
164
|
+
.insert(generationBatches)
|
165
|
+
.values({ ...testBatch, userId })
|
166
|
+
.returning();
|
167
|
+
|
168
|
+
const result = await generationBatchModel.findById(createdBatch.id);
|
169
|
+
|
170
|
+
expect(result).toMatchObject({
|
171
|
+
id: createdBatch.id,
|
172
|
+
...testBatch,
|
173
|
+
userId,
|
174
|
+
});
|
175
|
+
});
|
176
|
+
|
177
|
+
it('should return undefined for non-existent batch', async () => {
|
178
|
+
const result = await generationBatchModel.findById('non-existent-id');
|
179
|
+
expect(result).toBeUndefined();
|
180
|
+
});
|
181
|
+
|
182
|
+
it('should NOT find batches from other users', async () => {
|
183
|
+
// Create batch for other user
|
184
|
+
const [otherUserBatch] = await serverDB
|
185
|
+
.insert(generationBatches)
|
186
|
+
.values({ ...testBatch, userId: otherUserId })
|
187
|
+
.returning();
|
188
|
+
|
189
|
+
const result = await generationBatchModel.findById(otherUserBatch.id);
|
190
|
+
expect(result).toBeUndefined();
|
191
|
+
});
|
192
|
+
});
|
193
|
+
|
194
|
+
describe('findByTopicId', () => {
|
195
|
+
it('should find batches by topic id', async () => {
|
196
|
+
// Create multiple batches for the topic with explicit timestamps
|
197
|
+
const oldDate = new Date('2024-01-01T10:00:00Z');
|
198
|
+
const newDate = new Date('2024-01-02T10:00:00Z');
|
199
|
+
|
200
|
+
const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
|
201
|
+
const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };
|
202
|
+
|
203
|
+
await serverDB.insert(generationBatches).values([batch1, batch2]);
|
204
|
+
|
205
|
+
const results = await generationBatchModel.findByTopicId(testTopic.id);
|
206
|
+
|
207
|
+
expect(results).toHaveLength(2);
|
208
|
+
expect(results[0].prompt).toBe('Second batch'); // Latest first (desc order)
|
209
|
+
expect(results[1].prompt).toBe('First batch');
|
210
|
+
});
|
211
|
+
|
212
|
+
it('should only return batches for current user', async () => {
|
213
|
+
// Create batches for both users
|
214
|
+
await serverDB.insert(generationBatches).values([
|
215
|
+
{ ...testBatch, userId, prompt: 'User batch' },
|
216
|
+
{ ...testBatch, userId: otherUserId, prompt: 'Other user batch' },
|
217
|
+
]);
|
218
|
+
|
219
|
+
const results = await generationBatchModel.findByTopicId(testTopic.id);
|
220
|
+
|
221
|
+
expect(results).toHaveLength(1);
|
222
|
+
expect(results[0].prompt).toBe('User batch');
|
223
|
+
});
|
224
|
+
|
225
|
+
it('should return empty array when no batches exist for topic', async () => {
|
226
|
+
const results = await generationBatchModel.findByTopicId('non-existent-topic');
|
227
|
+
expect(results).toHaveLength(0);
|
228
|
+
});
|
229
|
+
});
|
230
|
+
|
231
|
+
describe('findByTopicIdWithGenerations', () => {
|
232
|
+
it('should find batches with their generations', async () => {
|
233
|
+
// Create batch
|
234
|
+
const [createdBatch] = await serverDB
|
235
|
+
.insert(generationBatches)
|
236
|
+
.values({ ...testBatch, userId })
|
237
|
+
.returning();
|
238
|
+
|
239
|
+
// Create generations for the batch
|
240
|
+
await serverDB.insert(generations).values([
|
241
|
+
{ ...testGeneration, generationBatchId: createdBatch.id },
|
242
|
+
{ ...testGeneration, id: 'gen2', generationBatchId: createdBatch.id },
|
243
|
+
]);
|
244
|
+
|
245
|
+
const results = await generationBatchModel.findByTopicIdWithGenerations(testTopic.id);
|
246
|
+
|
247
|
+
expect(results).toHaveLength(1);
|
248
|
+
expect(results[0].generations).toHaveLength(2);
|
249
|
+
// Generations are ordered by createdAt ASC, then by id ASC
|
250
|
+
// Since both have same createdAt, 'gen2' comes before 'test-gen-id' alphabetically
|
251
|
+
expect(results[0].generations[0].id).toBe('gen2');
|
252
|
+
});
|
253
|
+
|
254
|
+
it('should order generations by createdAt and id', async () => {
|
255
|
+
const [createdBatch] = await serverDB
|
256
|
+
.insert(generationBatches)
|
257
|
+
.values({ ...testBatch, userId })
|
258
|
+
.returning();
|
259
|
+
|
260
|
+
// Create generations with different timestamps
|
261
|
+
const oldDate = new Date('2024-01-01');
|
262
|
+
const newDate = new Date('2024-01-02');
|
263
|
+
|
264
|
+
await serverDB.insert(generations).values([
|
265
|
+
{
|
266
|
+
...testGeneration,
|
267
|
+
id: 'gen-new',
|
268
|
+
generationBatchId: createdBatch.id,
|
269
|
+
createdAt: newDate,
|
270
|
+
},
|
271
|
+
{
|
272
|
+
...testGeneration,
|
273
|
+
id: 'gen-old',
|
274
|
+
generationBatchId: createdBatch.id,
|
275
|
+
createdAt: oldDate,
|
276
|
+
},
|
277
|
+
]);
|
278
|
+
|
279
|
+
const results = await generationBatchModel.findByTopicIdWithGenerations(testTopic.id);
|
280
|
+
|
281
|
+
expect(results[0].generations[0].id).toBe('gen-old');
|
282
|
+
expect(results[0].generations[1].id).toBe('gen-new');
|
283
|
+
});
|
284
|
+
|
285
|
+
it('should NOT include generations from other users', async () => {
|
286
|
+
// Create batch for current user
|
287
|
+
const [userBatch] = await serverDB
|
288
|
+
.insert(generationBatches)
|
289
|
+
.values({ ...testBatch, userId })
|
290
|
+
.returning();
|
291
|
+
|
292
|
+
// Create batch for other user
|
293
|
+
const [otherBatch] = await serverDB
|
294
|
+
.insert(generationBatches)
|
295
|
+
.values({ ...testBatch, userId: otherUserId })
|
296
|
+
.returning();
|
297
|
+
|
298
|
+
// Create generations for both batches
|
299
|
+
await serverDB.insert(generations).values([
|
300
|
+
{ ...testGeneration, generationBatchId: userBatch.id, userId },
|
301
|
+
{
|
302
|
+
...testGeneration,
|
303
|
+
id: 'other-gen',
|
304
|
+
generationBatchId: otherBatch.id,
|
305
|
+
userId: otherUserId,
|
306
|
+
},
|
307
|
+
]);
|
308
|
+
|
309
|
+
const results = await generationBatchModel.findByTopicIdWithGenerations(testTopic.id);
|
310
|
+
|
311
|
+
expect(results).toHaveLength(1);
|
312
|
+
expect(results[0].id).toBe(userBatch.id);
|
313
|
+
});
|
314
|
+
});
|
315
|
+
|
316
|
+
describe('queryGenerationBatchesByTopicIdWithGenerations', () => {
|
317
|
+
it('should return transformed batches with generations', async () => {
|
318
|
+
// Create batch
|
319
|
+
const [createdBatch] = await serverDB
|
320
|
+
.insert(generationBatches)
|
321
|
+
.values({ ...testBatch, userId })
|
322
|
+
.returning();
|
323
|
+
|
324
|
+
// Create generation
|
325
|
+
await serverDB
|
326
|
+
.insert(generations)
|
327
|
+
.values([{ ...testGeneration, generationBatchId: createdBatch.id }]);
|
328
|
+
|
329
|
+
const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
|
330
|
+
testTopic.id,
|
331
|
+
);
|
332
|
+
|
333
|
+
expect(results).toHaveLength(1);
|
334
|
+
expect(results[0]).toMatchObject({
|
335
|
+
id: createdBatch.id,
|
336
|
+
provider: testBatch.provider,
|
337
|
+
model: testBatch.model,
|
338
|
+
prompt: testBatch.prompt,
|
339
|
+
width: testBatch.width,
|
340
|
+
height: testBatch.height,
|
341
|
+
generations: expect.any(Array),
|
342
|
+
});
|
343
|
+
|
344
|
+
// Verify FileService was called for config imageUrls
|
345
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('image1.jpg');
|
346
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('image2.jpg');
|
347
|
+
|
348
|
+
// Verify GenerationModel.transformGeneration was called
|
349
|
+
expect(mockTransformGeneration).toHaveBeenCalledTimes(1);
|
350
|
+
});
|
351
|
+
|
352
|
+
it('should transform config imageUrls through FileService', async () => {
|
353
|
+
const [createdBatch] = await serverDB
|
354
|
+
.insert(generationBatches)
|
355
|
+
.values({
|
356
|
+
...testBatch,
|
357
|
+
userId,
|
358
|
+
config: { imageUrls: ['url1.jpg', 'url2.jpg'] },
|
359
|
+
})
|
360
|
+
.returning();
|
361
|
+
|
362
|
+
const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
|
363
|
+
testTopic.id,
|
364
|
+
);
|
365
|
+
|
366
|
+
expect(results[0].config).toEqual({
|
367
|
+
imageUrls: ['https://example.com/url1.jpg', 'https://example.com/url2.jpg'],
|
368
|
+
});
|
369
|
+
});
|
370
|
+
|
371
|
+
it('should handle config without imageUrls', async () => {
|
372
|
+
const [createdBatch] = await serverDB
|
373
|
+
.insert(generationBatches)
|
374
|
+
.values({
|
375
|
+
...testBatch,
|
376
|
+
userId,
|
377
|
+
config: { otherField: 'value' },
|
378
|
+
})
|
379
|
+
.returning();
|
380
|
+
|
381
|
+
const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
|
382
|
+
testTopic.id,
|
383
|
+
);
|
384
|
+
|
385
|
+
expect(results[0].config).toEqual({ otherField: 'value' });
|
386
|
+
expect(mockGetFullFileUrl).not.toHaveBeenCalled();
|
387
|
+
});
|
388
|
+
|
389
|
+
it('should return empty array when no batches exist', async () => {
|
390
|
+
const results =
|
391
|
+
await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
|
392
|
+
'non-existent-topic',
|
393
|
+
);
|
394
|
+
expect(results).toHaveLength(0);
|
395
|
+
});
|
396
|
+
|
397
|
+
it('should handle batches without generations', async () => {
|
398
|
+
await serverDB.insert(generationBatches).values({ ...testBatch, userId });
|
399
|
+
|
400
|
+
const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
|
401
|
+
testTopic.id,
|
402
|
+
);
|
403
|
+
|
404
|
+
expect(results).toHaveLength(1);
|
405
|
+
expect(results[0].generations).toHaveLength(0);
|
406
|
+
});
|
407
|
+
});
|
408
|
+
|
409
|
+
describe('delete', () => {
|
410
|
+
it('should delete a generation batch and return batch data with thumbnail URLs', async () => {
|
411
|
+
const [createdBatch] = await serverDB
|
412
|
+
.insert(generationBatches)
|
413
|
+
.values({ ...testBatch, userId })
|
414
|
+
.returning();
|
415
|
+
|
416
|
+
// Create generation with thumbnail URL
|
417
|
+
await serverDB.insert(generations).values({
|
418
|
+
...testGeneration,
|
419
|
+
generationBatchId: createdBatch.id,
|
420
|
+
asset: {
|
421
|
+
type: 'image',
|
422
|
+
url: 'asset-url.jpg',
|
423
|
+
thumbnailUrl: 'thumbnail-url.jpg',
|
424
|
+
width: 1024,
|
425
|
+
height: 1024,
|
426
|
+
},
|
427
|
+
});
|
428
|
+
|
429
|
+
const result = await generationBatchModel.delete(createdBatch.id);
|
430
|
+
|
431
|
+
// Verify return value structure
|
432
|
+
expect(result).toBeDefined();
|
433
|
+
expect(result!.deletedBatch).toMatchObject({
|
434
|
+
id: createdBatch.id,
|
435
|
+
...testBatch,
|
436
|
+
userId,
|
437
|
+
});
|
438
|
+
expect(result!.thumbnailUrls).toEqual(['thumbnail-url.jpg']);
|
439
|
+
|
440
|
+
// Verify batch was actually deleted from database
|
441
|
+
const deletedBatch = await serverDB.query.generationBatches.findFirst({
|
442
|
+
where: eq(generationBatches.id, createdBatch.id),
|
443
|
+
});
|
444
|
+
expect(deletedBatch).toBeUndefined();
|
445
|
+
});
|
446
|
+
|
447
|
+
it('should collect multiple thumbnail URLs from multiple generations', async () => {
|
448
|
+
const [createdBatch] = await serverDB
|
449
|
+
.insert(generationBatches)
|
450
|
+
.values({ ...testBatch, userId })
|
451
|
+
.returning();
|
452
|
+
|
453
|
+
// Create multiple generations with different thumbnail URLs
|
454
|
+
await serverDB.insert(generations).values([
|
455
|
+
{
|
456
|
+
...testGeneration,
|
457
|
+
id: 'gen1',
|
458
|
+
generationBatchId: createdBatch.id,
|
459
|
+
asset: {
|
460
|
+
type: 'image',
|
461
|
+
url: 'asset1.jpg',
|
462
|
+
thumbnailUrl: 'thumbnail1.jpg',
|
463
|
+
width: 1024,
|
464
|
+
height: 1024,
|
465
|
+
},
|
466
|
+
},
|
467
|
+
{
|
468
|
+
...testGeneration,
|
469
|
+
id: 'gen2',
|
470
|
+
generationBatchId: createdBatch.id,
|
471
|
+
asset: {
|
472
|
+
type: 'image',
|
473
|
+
url: 'asset2.jpg',
|
474
|
+
thumbnailUrl: 'thumbnail2.jpg',
|
475
|
+
width: 1024,
|
476
|
+
height: 1024,
|
477
|
+
},
|
478
|
+
},
|
479
|
+
]);
|
480
|
+
|
481
|
+
const result = await generationBatchModel.delete(createdBatch.id);
|
482
|
+
|
483
|
+
expect(result).toBeDefined();
|
484
|
+
expect(result!.thumbnailUrls).toHaveLength(2);
|
485
|
+
expect(result!.thumbnailUrls).toContain('thumbnail1.jpg');
|
486
|
+
expect(result!.thumbnailUrls).toContain('thumbnail2.jpg');
|
487
|
+
});
|
488
|
+
|
489
|
+
it('should return empty thumbnail URLs when no generations have thumbnails', async () => {
|
490
|
+
const [createdBatch] = await serverDB
|
491
|
+
.insert(generationBatches)
|
492
|
+
.values({ ...testBatch, userId })
|
493
|
+
.returning();
|
494
|
+
|
495
|
+
const result = await generationBatchModel.delete(createdBatch.id);
|
496
|
+
|
497
|
+
expect(result).toBeDefined();
|
498
|
+
expect(result!.deletedBatch.id).toBe(createdBatch.id);
|
499
|
+
expect(result!.thumbnailUrls).toEqual([]);
|
500
|
+
});
|
501
|
+
|
502
|
+
it('should return undefined when trying to delete non-existent batch', async () => {
|
503
|
+
const result = await generationBatchModel.delete('non-existent-id');
|
504
|
+
expect(result).toBeUndefined();
|
505
|
+
});
|
506
|
+
|
507
|
+
it('should return undefined when trying to delete batch from other user', async () => {
|
508
|
+
// Create batch for other user
|
509
|
+
const [otherUserBatch] = await serverDB
|
510
|
+
.insert(generationBatches)
|
511
|
+
.values({ ...testBatch, userId: otherUserId })
|
512
|
+
.returning();
|
513
|
+
|
514
|
+
// Try to delete using current user's model
|
515
|
+
const result = await generationBatchModel.delete(otherUserBatch.id);
|
516
|
+
expect(result).toBeUndefined();
|
517
|
+
|
518
|
+
// Verify batch still exists
|
519
|
+
const stillExists = await serverDB.query.generationBatches.findFirst({
|
520
|
+
where: eq(generationBatches.id, otherUserBatch.id),
|
521
|
+
});
|
522
|
+
expect(stillExists).toBeDefined();
|
523
|
+
});
|
524
|
+
});
|
525
|
+
|
526
|
+
describe('user isolation security tests', () => {
|
527
|
+
it('should enforce user data isolation across all methods', async () => {
|
528
|
+
// Create batches for both users with same topic
|
529
|
+
const userBatch = { ...testBatch, userId };
|
530
|
+
const otherUserBatch = { ...testBatch, userId: otherUserId };
|
531
|
+
|
532
|
+
const [userBatchCreated] = await serverDB
|
533
|
+
.insert(generationBatches)
|
534
|
+
.values(userBatch)
|
535
|
+
.returning();
|
536
|
+
|
537
|
+
const [otherUserBatchCreated] = await serverDB
|
538
|
+
.insert(generationBatches)
|
539
|
+
.values(otherUserBatch)
|
540
|
+
.returning();
|
541
|
+
|
542
|
+
// Test findById isolation
|
543
|
+
const foundUserBatch = await generationBatchModel.findById(userBatchCreated.id);
|
544
|
+
const foundOtherBatch = await generationBatchModel.findById(otherUserBatchCreated.id);
|
545
|
+
|
546
|
+
expect(foundUserBatch).toBeDefined();
|
547
|
+
expect(foundOtherBatch).toBeUndefined(); // Should not find other user's batch
|
548
|
+
|
549
|
+
// Test findByTopicId isolation
|
550
|
+
const topicBatches = await generationBatchModel.findByTopicId(testTopic.id);
|
551
|
+
expect(topicBatches).toHaveLength(1);
|
552
|
+
expect(topicBatches[0].id).toBe(userBatchCreated.id);
|
553
|
+
|
554
|
+
// Test delete isolation - should not affect other user's data
|
555
|
+
await generationBatchModel.delete(otherUserBatchCreated.id);
|
556
|
+
const otherUserBatchStillExists = await serverDB.query.generationBatches.findFirst({
|
557
|
+
where: eq(generationBatches.id, otherUserBatchCreated.id),
|
558
|
+
});
|
559
|
+
expect(otherUserBatchStillExists).toBeDefined(); // Should not be deleted
|
560
|
+
});
|
561
|
+
});
|
562
|
+
|
563
|
+
describe('External service integration', () => {
|
564
|
+
it('should call FileService with correct parameters', async () => {
|
565
|
+
const [createdBatch] = await serverDB
|
566
|
+
.insert(generationBatches)
|
567
|
+
.values({
|
568
|
+
...testBatch,
|
569
|
+
userId,
|
570
|
+
config: { imageUrls: ['test-image.jpg'] },
|
571
|
+
})
|
572
|
+
.returning();
|
573
|
+
|
574
|
+
await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(testTopic.id);
|
575
|
+
|
576
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('test-image.jpg');
|
577
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
|
578
|
+
});
|
579
|
+
|
580
|
+
it('should call GenerationModel.transformGeneration for each generation', async () => {
|
581
|
+
const [createdBatch] = await serverDB
|
582
|
+
.insert(generationBatches)
|
583
|
+
.values({ ...testBatch, userId })
|
584
|
+
.returning();
|
585
|
+
|
586
|
+
// Create multiple generations
|
587
|
+
await serverDB.insert(generations).values([
|
588
|
+
{ ...testGeneration, id: 'gen1', generationBatchId: createdBatch.id },
|
589
|
+
{ ...testGeneration, id: 'gen2', generationBatchId: createdBatch.id },
|
590
|
+
]);
|
591
|
+
|
592
|
+
await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(testTopic.id);
|
593
|
+
|
594
|
+
expect(mockTransformGeneration).toHaveBeenCalledTimes(2);
|
595
|
+
});
|
596
|
+
|
597
|
+
it('should handle FileService errors gracefully', async () => {
|
598
|
+
mockGetFullFileUrl.mockRejectedValue(new Error('FileService error'));
|
599
|
+
|
600
|
+
const [createdBatch] = await serverDB
|
601
|
+
.insert(generationBatches)
|
602
|
+
.values({
|
603
|
+
...testBatch,
|
604
|
+
userId,
|
605
|
+
config: { imageUrls: ['failing-image.jpg'] },
|
606
|
+
})
|
607
|
+
.returning();
|
608
|
+
|
609
|
+
await expect(
|
610
|
+
generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(testTopic.id),
|
611
|
+
).rejects.toThrow('FileService error');
|
612
|
+
});
|
613
|
+
});
|
614
|
+
});
|