@lobehub/chat 1.98.2 → 1.99.1
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 +44 -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/appBrowsers.ts +1 -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/BrowserManager.ts +5 -2
- 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 +14 -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 +44 -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 +154 -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 +157 -1
- package/src/database/repositories/aiInfra/index.ts +37 -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 +2 -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 +31 -6
- 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 +2 -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,786 @@
|
|
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 { FileSource } from '@/types/files';
|
8
|
+
import { ImageGenerationAsset } from '@/types/generation';
|
9
|
+
|
10
|
+
import {
|
11
|
+
NewGeneration,
|
12
|
+
asyncTasks,
|
13
|
+
files,
|
14
|
+
generationBatches,
|
15
|
+
generationTopics,
|
16
|
+
generations,
|
17
|
+
users,
|
18
|
+
} from '../../schemas';
|
19
|
+
import { GenerationModel } from '../generation';
|
20
|
+
import { getTestDB } from './_util';
|
21
|
+
|
22
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
23
|
+
|
24
|
+
// Mock FileService
|
25
|
+
const mockGetFullFileUrl = vi.fn();
|
26
|
+
vi.mock('@/server/services/file', () => ({
|
27
|
+
FileService: vi.fn().mockImplementation(() => ({
|
28
|
+
getFullFileUrl: mockGetFullFileUrl,
|
29
|
+
})),
|
30
|
+
}));
|
31
|
+
|
32
|
+
// Mock FileModel
|
33
|
+
const mockFileModelCreate = vi.fn();
|
34
|
+
vi.mock('../file', () => ({
|
35
|
+
FileModel: vi.fn().mockImplementation(() => ({
|
36
|
+
create: mockFileModelCreate,
|
37
|
+
})),
|
38
|
+
}));
|
39
|
+
|
40
|
+
const userId = 'generation-test-user-id';
|
41
|
+
const otherUserId = 'other-user-id';
|
42
|
+
const generationModel = new GenerationModel(serverDB, userId);
|
43
|
+
|
44
|
+
// Test data
|
45
|
+
const testTopic = {
|
46
|
+
id: 'test-topic-id',
|
47
|
+
userId,
|
48
|
+
title: 'Test Generation Topic',
|
49
|
+
coverUrl: null,
|
50
|
+
};
|
51
|
+
|
52
|
+
const testBatch = {
|
53
|
+
id: 'test-batch-id',
|
54
|
+
generationTopicId: 'test-topic-id',
|
55
|
+
provider: 'test-provider',
|
56
|
+
model: 'test-model',
|
57
|
+
prompt: 'Test prompt for image generation',
|
58
|
+
width: 1024,
|
59
|
+
height: 1024,
|
60
|
+
config: {},
|
61
|
+
userId,
|
62
|
+
};
|
63
|
+
|
64
|
+
const testAsyncTask = {
|
65
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
66
|
+
status: AsyncTaskStatus.Success,
|
67
|
+
type: 'imageGeneration',
|
68
|
+
params: {},
|
69
|
+
error: null,
|
70
|
+
userId,
|
71
|
+
};
|
72
|
+
|
73
|
+
const testGeneration: Omit<NewGeneration, 'userId'> = {
|
74
|
+
generationBatchId: 'test-batch-id',
|
75
|
+
asyncTaskId: '550e8400-e29b-41d4-a716-446655440000', // 使用有效的 asyncTaskId
|
76
|
+
fileId: null, // 使用 null 避免外键约束
|
77
|
+
seed: 12345,
|
78
|
+
asset: {
|
79
|
+
url: 'asset-url.jpg',
|
80
|
+
thumbnailUrl: 'thumbnail-url.jpg',
|
81
|
+
width: 1024,
|
82
|
+
height: 1024,
|
83
|
+
} as ImageGenerationAsset,
|
84
|
+
};
|
85
|
+
|
86
|
+
const testFile = {
|
87
|
+
id: 'test-file-id',
|
88
|
+
name: 'generated-image.jpg',
|
89
|
+
url: 'https://example.com/generated-image.jpg',
|
90
|
+
size: 1048576,
|
91
|
+
fileType: 'image/jpeg',
|
92
|
+
source: FileSource.ImageGeneration,
|
93
|
+
userId,
|
94
|
+
};
|
95
|
+
|
96
|
+
beforeEach(async () => {
|
97
|
+
// Clear all mocks
|
98
|
+
vi.clearAllMocks();
|
99
|
+
|
100
|
+
// Setup mock return values
|
101
|
+
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
|
102
|
+
|
103
|
+
// Clear database and create test users
|
104
|
+
await serverDB.delete(users);
|
105
|
+
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
106
|
+
|
107
|
+
// Create test topic
|
108
|
+
await serverDB.insert(generationTopics).values(testTopic);
|
109
|
+
|
110
|
+
// Create test batch
|
111
|
+
await serverDB.insert(generationBatches).values(testBatch);
|
112
|
+
|
113
|
+
// Create test async task
|
114
|
+
await serverDB.insert(asyncTasks).values(testAsyncTask);
|
115
|
+
|
116
|
+
// Create test file
|
117
|
+
await serverDB.insert(files).values(testFile);
|
118
|
+
|
119
|
+
// Create a file that will be returned by the mock for createAssetAndFile tests
|
120
|
+
const mockFileForUpdateTest = {
|
121
|
+
id: 'new-file-id',
|
122
|
+
name: 'mock-generated-image.jpg',
|
123
|
+
url: 'https://example.com/mock-generated-image.jpg',
|
124
|
+
size: 1048576,
|
125
|
+
fileType: 'image/jpeg',
|
126
|
+
source: FileSource.ImageGeneration,
|
127
|
+
userId,
|
128
|
+
};
|
129
|
+
await serverDB.insert(files).values(mockFileForUpdateTest);
|
130
|
+
|
131
|
+
// Setup FileModel mock to return the actual file ID that exists in database
|
132
|
+
mockFileModelCreate.mockResolvedValue({ id: 'new-file-id' });
|
133
|
+
});
|
134
|
+
|
135
|
+
afterEach(async () => {
|
136
|
+
// Clean up database
|
137
|
+
await serverDB.delete(users);
|
138
|
+
});
|
139
|
+
|
140
|
+
describe('GenerationModel', () => {
|
141
|
+
describe('create', () => {
|
142
|
+
it('should create a new generation', async () => {
|
143
|
+
const result = await generationModel.create(testGeneration);
|
144
|
+
|
145
|
+
expect(result.id).toBeDefined();
|
146
|
+
expect(result).toMatchObject({
|
147
|
+
...testGeneration,
|
148
|
+
userId,
|
149
|
+
});
|
150
|
+
|
151
|
+
// Verify in database
|
152
|
+
const dbGeneration = await serverDB.query.generations.findFirst({
|
153
|
+
where: eq(generations.id, result.id),
|
154
|
+
});
|
155
|
+
expect(dbGeneration).toMatchObject({
|
156
|
+
...testGeneration,
|
157
|
+
userId,
|
158
|
+
});
|
159
|
+
});
|
160
|
+
|
161
|
+
it('should automatically set userId when creating generation', async () => {
|
162
|
+
const result = await generationModel.create(testGeneration);
|
163
|
+
|
164
|
+
expect(result.userId).toBe(userId);
|
165
|
+
});
|
166
|
+
|
167
|
+
it('should create generation with minimal data', async () => {
|
168
|
+
const minimalGeneration: Omit<NewGeneration, 'userId'> = {
|
169
|
+
generationBatchId: 'test-batch-id',
|
170
|
+
};
|
171
|
+
|
172
|
+
const result = await generationModel.create(minimalGeneration);
|
173
|
+
|
174
|
+
expect(result.id).toBeDefined();
|
175
|
+
expect(result.userId).toBe(userId);
|
176
|
+
expect(result.generationBatchId).toBe('test-batch-id');
|
177
|
+
});
|
178
|
+
});
|
179
|
+
|
180
|
+
describe('findById', () => {
|
181
|
+
it('should find a generation by id', async () => {
|
182
|
+
const [createdGeneration] = await serverDB
|
183
|
+
.insert(generations)
|
184
|
+
.values({ ...testGeneration, userId })
|
185
|
+
.returning();
|
186
|
+
|
187
|
+
const result = await generationModel.findById(createdGeneration.id);
|
188
|
+
|
189
|
+
expect(result).toMatchObject({
|
190
|
+
id: createdGeneration.id,
|
191
|
+
...testGeneration,
|
192
|
+
userId,
|
193
|
+
});
|
194
|
+
});
|
195
|
+
|
196
|
+
it('should return undefined for non-existent generation', async () => {
|
197
|
+
const result = await generationModel.findById('non-existent-id');
|
198
|
+
expect(result).toBeUndefined();
|
199
|
+
});
|
200
|
+
|
201
|
+
it('should NOT find generations from other users', async () => {
|
202
|
+
// Create generation for other user
|
203
|
+
const [otherUserGeneration] = await serverDB
|
204
|
+
.insert(generations)
|
205
|
+
.values({ ...testGeneration, userId: otherUserId })
|
206
|
+
.returning();
|
207
|
+
|
208
|
+
const result = await generationModel.findById(otherUserGeneration.id);
|
209
|
+
expect(result).toBeUndefined();
|
210
|
+
});
|
211
|
+
});
|
212
|
+
|
213
|
+
describe('findByIdWithAsyncTask', () => {
|
214
|
+
it('should find generation with async task data', async () => {
|
215
|
+
const [createdGeneration] = await serverDB
|
216
|
+
.insert(generations)
|
217
|
+
.values({ ...testGeneration, userId })
|
218
|
+
.returning();
|
219
|
+
|
220
|
+
const result = await generationModel.findByIdWithAsyncTask(createdGeneration.id);
|
221
|
+
|
222
|
+
expect(result).toMatchObject({
|
223
|
+
id: createdGeneration.id,
|
224
|
+
...testGeneration,
|
225
|
+
userId,
|
226
|
+
});
|
227
|
+
expect(result?.asyncTask).toMatchObject({
|
228
|
+
id: testAsyncTask.id,
|
229
|
+
status: AsyncTaskStatus.Success,
|
230
|
+
});
|
231
|
+
});
|
232
|
+
|
233
|
+
it('should return undefined for non-existent generation', async () => {
|
234
|
+
const result = await generationModel.findByIdWithAsyncTask('non-existent-id');
|
235
|
+
expect(result).toBeUndefined();
|
236
|
+
});
|
237
|
+
|
238
|
+
it('should NOT find generations from other users', async () => {
|
239
|
+
const [otherUserGeneration] = await serverDB
|
240
|
+
.insert(generations)
|
241
|
+
.values({ ...testGeneration, userId: otherUserId })
|
242
|
+
.returning();
|
243
|
+
|
244
|
+
const result = await generationModel.findByIdWithAsyncTask(otherUserGeneration.id);
|
245
|
+
expect(result).toBeUndefined();
|
246
|
+
});
|
247
|
+
});
|
248
|
+
|
249
|
+
describe('update', () => {
|
250
|
+
it('should update a generation', async () => {
|
251
|
+
const [createdGeneration] = await serverDB
|
252
|
+
.insert(generations)
|
253
|
+
.values({ ...testGeneration, userId })
|
254
|
+
.returning();
|
255
|
+
|
256
|
+
const updateData = {
|
257
|
+
seed: 54321,
|
258
|
+
asset: {
|
259
|
+
url: 'updated-asset.jpg',
|
260
|
+
thumbnailUrl: 'updated-thumbnail.jpg',
|
261
|
+
width: 512,
|
262
|
+
height: 512,
|
263
|
+
} as ImageGenerationAsset,
|
264
|
+
};
|
265
|
+
|
266
|
+
await generationModel.update(createdGeneration.id, updateData);
|
267
|
+
|
268
|
+
const updatedGeneration = await serverDB.query.generations.findFirst({
|
269
|
+
where: eq(generations.id, createdGeneration.id),
|
270
|
+
});
|
271
|
+
|
272
|
+
expect(updatedGeneration).toMatchObject({
|
273
|
+
...testGeneration,
|
274
|
+
...updateData,
|
275
|
+
userId,
|
276
|
+
});
|
277
|
+
expect(updatedGeneration?.updatedAt).toBeDefined();
|
278
|
+
});
|
279
|
+
|
280
|
+
it('should NOT update generations from other users', async () => {
|
281
|
+
// Create generation for other user
|
282
|
+
const [otherUserGeneration] = await serverDB
|
283
|
+
.insert(generations)
|
284
|
+
.values({ ...testGeneration, userId: otherUserId })
|
285
|
+
.returning();
|
286
|
+
|
287
|
+
const updateData = { seed: 99999 };
|
288
|
+
|
289
|
+
await generationModel.update(otherUserGeneration.id, updateData);
|
290
|
+
|
291
|
+
// Verify original data unchanged
|
292
|
+
const unchanged = await serverDB.query.generations.findFirst({
|
293
|
+
where: eq(generations.id, otherUserGeneration.id),
|
294
|
+
});
|
295
|
+
expect(unchanged?.seed).toBe(testGeneration.seed);
|
296
|
+
});
|
297
|
+
|
298
|
+
it('should handle partial updates', async () => {
|
299
|
+
const [createdGeneration] = await serverDB
|
300
|
+
.insert(generations)
|
301
|
+
.values({ ...testGeneration, userId })
|
302
|
+
.returning();
|
303
|
+
|
304
|
+
await generationModel.update(createdGeneration.id, { seed: 11111 });
|
305
|
+
|
306
|
+
const updatedGeneration = await serverDB.query.generations.findFirst({
|
307
|
+
where: eq(generations.id, createdGeneration.id),
|
308
|
+
});
|
309
|
+
|
310
|
+
expect(updatedGeneration?.seed).toBe(11111);
|
311
|
+
expect(updatedGeneration?.generationBatchId).toBe(testGeneration.generationBatchId);
|
312
|
+
});
|
313
|
+
});
|
314
|
+
|
315
|
+
describe('createAssetAndFile', () => {
|
316
|
+
it('should update generation asset and create file in transaction', async () => {
|
317
|
+
const [createdGeneration] = await serverDB
|
318
|
+
.insert(generations)
|
319
|
+
.values({ ...testGeneration, userId, asset: null, fileId: null })
|
320
|
+
.returning();
|
321
|
+
|
322
|
+
const newAsset = {
|
323
|
+
url: 'new-asset.jpg',
|
324
|
+
thumbnailUrl: 'new-thumbnail.jpg',
|
325
|
+
width: 2048,
|
326
|
+
height: 2048,
|
327
|
+
} as ImageGenerationAsset;
|
328
|
+
|
329
|
+
const newFileData = {
|
330
|
+
name: 'new-generated-image.jpg',
|
331
|
+
url: 'https://example.com/new-generated-image.jpg',
|
332
|
+
size: 2097152,
|
333
|
+
fileType: 'image/jpeg',
|
334
|
+
};
|
335
|
+
|
336
|
+
const result = await generationModel.createAssetAndFile(
|
337
|
+
createdGeneration.id,
|
338
|
+
newAsset,
|
339
|
+
newFileData,
|
340
|
+
);
|
341
|
+
|
342
|
+
expect(result.file.id).toBe('new-file-id');
|
343
|
+
expect(mockFileModelCreate).toHaveBeenCalledWith(
|
344
|
+
{
|
345
|
+
...newFileData,
|
346
|
+
source: FileSource.ImageGeneration,
|
347
|
+
},
|
348
|
+
true,
|
349
|
+
expect.any(Object), // transaction object
|
350
|
+
);
|
351
|
+
|
352
|
+
// Verify generation was updated
|
353
|
+
const updatedGeneration = await serverDB.query.generations.findFirst({
|
354
|
+
where: eq(generations.id, createdGeneration.id),
|
355
|
+
});
|
356
|
+
expect(updatedGeneration?.asset).toEqual(newAsset);
|
357
|
+
expect(updatedGeneration?.fileId).toBe('new-file-id');
|
358
|
+
});
|
359
|
+
|
360
|
+
it('should NOT update assets for other users generations', async () => {
|
361
|
+
// Create generation for other user
|
362
|
+
const [otherUserGeneration] = await serverDB
|
363
|
+
.insert(generations)
|
364
|
+
.values({ ...testGeneration, userId: otherUserId, asset: null, fileId: null })
|
365
|
+
.returning();
|
366
|
+
|
367
|
+
const newAsset = {
|
368
|
+
url: 'hacked-asset.jpg',
|
369
|
+
thumbnailUrl: 'hacked-thumbnail.jpg',
|
370
|
+
width: 1,
|
371
|
+
height: 1,
|
372
|
+
} as ImageGenerationAsset;
|
373
|
+
|
374
|
+
const newFileData = {
|
375
|
+
name: 'hacked-file.jpg',
|
376
|
+
url: 'https://example.com/hacked-file.jpg',
|
377
|
+
size: 1,
|
378
|
+
fileType: 'image/jpeg',
|
379
|
+
};
|
380
|
+
|
381
|
+
await generationModel.createAssetAndFile(otherUserGeneration.id, newAsset, newFileData);
|
382
|
+
|
383
|
+
// Verify no changes to other user's generation
|
384
|
+
const unchanged = await serverDB.query.generations.findFirst({
|
385
|
+
where: eq(generations.id, otherUserGeneration.id),
|
386
|
+
});
|
387
|
+
expect(unchanged?.asset).toBeNull();
|
388
|
+
expect(unchanged?.fileId).toBeNull();
|
389
|
+
});
|
390
|
+
|
391
|
+
it('should handle FileModel errors in transaction', async () => {
|
392
|
+
const [createdGeneration] = await serverDB
|
393
|
+
.insert(generations)
|
394
|
+
.values({ ...testGeneration, userId, asset: null, fileId: null })
|
395
|
+
.returning();
|
396
|
+
|
397
|
+
mockFileModelCreate.mockRejectedValue(new Error('File creation failed'));
|
398
|
+
|
399
|
+
const newAsset = {
|
400
|
+
url: 'asset.jpg',
|
401
|
+
thumbnailUrl: 'thumbnail.jpg',
|
402
|
+
width: 1024,
|
403
|
+
height: 1024,
|
404
|
+
} as ImageGenerationAsset;
|
405
|
+
|
406
|
+
const newFileData = {
|
407
|
+
name: 'image.jpg',
|
408
|
+
url: 'https://example.com/image.jpg',
|
409
|
+
size: 1024,
|
410
|
+
fileType: 'image/jpeg',
|
411
|
+
};
|
412
|
+
|
413
|
+
await expect(
|
414
|
+
generationModel.createAssetAndFile(createdGeneration.id, newAsset, newFileData),
|
415
|
+
).rejects.toThrow('File creation failed');
|
416
|
+
|
417
|
+
// Verify generation was not updated due to transaction rollback
|
418
|
+
const unchanged = await serverDB.query.generations.findFirst({
|
419
|
+
where: eq(generations.id, createdGeneration.id),
|
420
|
+
});
|
421
|
+
expect(unchanged?.asset).toBeNull();
|
422
|
+
expect(unchanged?.fileId).toBeNull();
|
423
|
+
});
|
424
|
+
});
|
425
|
+
|
426
|
+
describe('delete', () => {
|
427
|
+
it('should delete a generation', async () => {
|
428
|
+
const [createdGeneration] = await serverDB
|
429
|
+
.insert(generations)
|
430
|
+
.values({ ...testGeneration, userId })
|
431
|
+
.returning();
|
432
|
+
|
433
|
+
const deletedGeneration = await generationModel.delete(createdGeneration.id);
|
434
|
+
|
435
|
+
expect(deletedGeneration.id).toBe(createdGeneration.id);
|
436
|
+
|
437
|
+
const deletedRecord = await serverDB.query.generations.findFirst({
|
438
|
+
where: eq(generations.id, createdGeneration.id),
|
439
|
+
});
|
440
|
+
expect(deletedRecord).toBeUndefined();
|
441
|
+
});
|
442
|
+
|
443
|
+
it('should NOT delete generations from other users', async () => {
|
444
|
+
// Create generation for other user
|
445
|
+
const [otherUserGeneration] = await serverDB
|
446
|
+
.insert(generations)
|
447
|
+
.values({ ...testGeneration, userId: otherUserId })
|
448
|
+
.returning();
|
449
|
+
|
450
|
+
const result = await generationModel.delete(otherUserGeneration.id);
|
451
|
+
|
452
|
+
expect(result).toBeUndefined();
|
453
|
+
|
454
|
+
// Verify generation still exists
|
455
|
+
const stillExists = await serverDB.query.generations.findFirst({
|
456
|
+
where: eq(generations.id, otherUserGeneration.id),
|
457
|
+
});
|
458
|
+
expect(stillExists).toBeDefined();
|
459
|
+
});
|
460
|
+
|
461
|
+
it('should return undefined when trying to delete non-existent generation', async () => {
|
462
|
+
const result = await generationModel.delete('non-existent-id');
|
463
|
+
expect(result).toBeUndefined();
|
464
|
+
});
|
465
|
+
});
|
466
|
+
|
467
|
+
describe('findByIdAndTransform', () => {
|
468
|
+
it('should find and transform generation to frontend type', async () => {
|
469
|
+
const [createdGeneration] = await serverDB
|
470
|
+
.insert(generations)
|
471
|
+
.values({ ...testGeneration, userId })
|
472
|
+
.returning();
|
473
|
+
|
474
|
+
const result = await generationModel.findByIdAndTransform(createdGeneration.id);
|
475
|
+
|
476
|
+
expect(result).toMatchObject({
|
477
|
+
id: createdGeneration.id,
|
478
|
+
asset: {
|
479
|
+
url: 'https://example.com/asset-url.jpg',
|
480
|
+
thumbnailUrl: 'https://example.com/thumbnail-url.jpg',
|
481
|
+
width: 1024,
|
482
|
+
height: 1024,
|
483
|
+
},
|
484
|
+
seed: testGeneration.seed,
|
485
|
+
asyncTaskId: testGeneration.asyncTaskId,
|
486
|
+
task: {
|
487
|
+
id: testGeneration.asyncTaskId,
|
488
|
+
status: AsyncTaskStatus.Success,
|
489
|
+
},
|
490
|
+
});
|
491
|
+
|
492
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('asset-url.jpg');
|
493
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('thumbnail-url.jpg');
|
494
|
+
});
|
495
|
+
|
496
|
+
it('should return null for non-existent generation', async () => {
|
497
|
+
const result = await generationModel.findByIdAndTransform('non-existent-id');
|
498
|
+
expect(result).toBeNull();
|
499
|
+
});
|
500
|
+
|
501
|
+
it('should NOT transform generations from other users', async () => {
|
502
|
+
const [otherUserGeneration] = await serverDB
|
503
|
+
.insert(generations)
|
504
|
+
.values({ ...testGeneration, userId: otherUserId })
|
505
|
+
.returning();
|
506
|
+
|
507
|
+
const result = await generationModel.findByIdAndTransform(otherUserGeneration.id);
|
508
|
+
expect(result).toBeNull();
|
509
|
+
});
|
510
|
+
});
|
511
|
+
|
512
|
+
describe('transformGeneration', () => {
|
513
|
+
it('should transform generation with asset URLs', async () => {
|
514
|
+
const generationWithTask = {
|
515
|
+
id: 'test-gen-id',
|
516
|
+
userId,
|
517
|
+
generationBatchId: 'batch-id',
|
518
|
+
asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
|
519
|
+
fileId: 'file-id',
|
520
|
+
seed: 12345,
|
521
|
+
asset: {
|
522
|
+
url: 'original-asset.jpg',
|
523
|
+
thumbnailUrl: 'original-thumbnail.jpg',
|
524
|
+
width: 1024,
|
525
|
+
height: 1024,
|
526
|
+
} as ImageGenerationAsset,
|
527
|
+
accessedAt: new Date(),
|
528
|
+
createdAt: new Date(),
|
529
|
+
updatedAt: new Date(),
|
530
|
+
asyncTask: {
|
531
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
532
|
+
status: AsyncTaskStatus.Success,
|
533
|
+
type: 'imageGeneration',
|
534
|
+
params: {},
|
535
|
+
error: null,
|
536
|
+
duration: null,
|
537
|
+
accessedAt: new Date(),
|
538
|
+
createdAt: new Date(),
|
539
|
+
updatedAt: new Date(),
|
540
|
+
userId,
|
541
|
+
},
|
542
|
+
};
|
543
|
+
|
544
|
+
const result = await generationModel.transformGeneration(generationWithTask);
|
545
|
+
|
546
|
+
expect(result).toMatchObject({
|
547
|
+
id: 'test-gen-id',
|
548
|
+
asset: {
|
549
|
+
url: 'https://example.com/original-asset.jpg',
|
550
|
+
thumbnailUrl: 'https://example.com/original-thumbnail.jpg',
|
551
|
+
width: 1024,
|
552
|
+
height: 1024,
|
553
|
+
},
|
554
|
+
seed: 12345,
|
555
|
+
asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
|
556
|
+
task: {
|
557
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
558
|
+
status: AsyncTaskStatus.Success,
|
559
|
+
},
|
560
|
+
});
|
561
|
+
|
562
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('original-asset.jpg');
|
563
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('original-thumbnail.jpg');
|
564
|
+
});
|
565
|
+
|
566
|
+
it('should handle generation without asset', async () => {
|
567
|
+
const generationWithoutAsset = {
|
568
|
+
id: 'test-gen-id',
|
569
|
+
userId,
|
570
|
+
generationBatchId: 'batch-id',
|
571
|
+
asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
|
572
|
+
fileId: null,
|
573
|
+
seed: 12345,
|
574
|
+
asset: null,
|
575
|
+
accessedAt: new Date(),
|
576
|
+
createdAt: new Date(),
|
577
|
+
updatedAt: new Date(),
|
578
|
+
asyncTask: {
|
579
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
580
|
+
status: AsyncTaskStatus.Pending,
|
581
|
+
type: 'imageGeneration',
|
582
|
+
params: {},
|
583
|
+
error: null,
|
584
|
+
duration: null,
|
585
|
+
accessedAt: new Date(),
|
586
|
+
createdAt: new Date(),
|
587
|
+
updatedAt: new Date(),
|
588
|
+
userId,
|
589
|
+
},
|
590
|
+
};
|
591
|
+
|
592
|
+
const result = await generationModel.transformGeneration(generationWithoutAsset as any);
|
593
|
+
|
594
|
+
expect(result).toMatchObject({
|
595
|
+
id: 'test-gen-id',
|
596
|
+
asset: null,
|
597
|
+
seed: 12345,
|
598
|
+
asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
|
599
|
+
task: {
|
600
|
+
id: '550e8400-e29b-41d4-a716-446655440000',
|
601
|
+
status: AsyncTaskStatus.Pending,
|
602
|
+
},
|
603
|
+
});
|
604
|
+
|
605
|
+
expect(mockGetFullFileUrl).not.toHaveBeenCalled();
|
606
|
+
});
|
607
|
+
|
608
|
+
it('should handle generation without async task', async () => {
|
609
|
+
const generationWithoutTask = {
|
610
|
+
id: 'test-gen-id',
|
611
|
+
userId,
|
612
|
+
generationBatchId: 'batch-id',
|
613
|
+
asyncTaskId: null,
|
614
|
+
fileId: null,
|
615
|
+
seed: 12345,
|
616
|
+
asset: null,
|
617
|
+
accessedAt: new Date(),
|
618
|
+
createdAt: new Date(),
|
619
|
+
updatedAt: new Date(),
|
620
|
+
asyncTask: undefined,
|
621
|
+
};
|
622
|
+
|
623
|
+
const result = await generationModel.transformGeneration(generationWithoutTask as any);
|
624
|
+
|
625
|
+
expect(result).toMatchObject({
|
626
|
+
id: 'test-gen-id',
|
627
|
+
asset: null,
|
628
|
+
seed: 12345,
|
629
|
+
asyncTaskId: null,
|
630
|
+
task: {
|
631
|
+
id: '',
|
632
|
+
status: 'pending',
|
633
|
+
},
|
634
|
+
});
|
635
|
+
});
|
636
|
+
|
637
|
+
it('should handle FileService errors during transformation', async () => {
|
638
|
+
mockGetFullFileUrl.mockRejectedValue(new Error('FileService error'));
|
639
|
+
|
640
|
+
const generationWithAsset = {
|
641
|
+
id: 'test-gen-id',
|
642
|
+
userId,
|
643
|
+
generationBatchId: 'batch-id',
|
644
|
+
asyncTaskId: null,
|
645
|
+
fileId: null,
|
646
|
+
seed: 12345,
|
647
|
+
asset: {
|
648
|
+
url: 'failing-asset.jpg',
|
649
|
+
thumbnailUrl: 'failing-thumbnail.jpg',
|
650
|
+
width: 1024,
|
651
|
+
height: 1024,
|
652
|
+
} as ImageGenerationAsset,
|
653
|
+
accessedAt: new Date(),
|
654
|
+
createdAt: new Date(),
|
655
|
+
updatedAt: new Date(),
|
656
|
+
asyncTask: undefined,
|
657
|
+
};
|
658
|
+
|
659
|
+
await expect(generationModel.transformGeneration(generationWithAsset as any)).rejects.toThrow(
|
660
|
+
'FileService error',
|
661
|
+
);
|
662
|
+
});
|
663
|
+
});
|
664
|
+
|
665
|
+
describe('user isolation security tests', () => {
|
666
|
+
it('should enforce user data isolation across all methods', async () => {
|
667
|
+
// Create generations for both users
|
668
|
+
const userGeneration = { ...testGeneration, userId };
|
669
|
+
const otherUserGeneration = { ...testGeneration, userId: otherUserId };
|
670
|
+
|
671
|
+
const [userGenerationCreated] = await serverDB
|
672
|
+
.insert(generations)
|
673
|
+
.values(userGeneration)
|
674
|
+
.returning();
|
675
|
+
|
676
|
+
const [otherUserGenerationCreated] = await serverDB
|
677
|
+
.insert(generations)
|
678
|
+
.values(otherUserGeneration)
|
679
|
+
.returning();
|
680
|
+
|
681
|
+
// Test findById isolation
|
682
|
+
const foundUserGeneration = await generationModel.findById(userGenerationCreated.id);
|
683
|
+
const foundOtherGeneration = await generationModel.findById(otherUserGenerationCreated.id);
|
684
|
+
|
685
|
+
expect(foundUserGeneration).toBeDefined();
|
686
|
+
expect(foundOtherGeneration).toBeUndefined(); // Should not find other user's generation
|
687
|
+
|
688
|
+
// Test findByIdWithAsyncTask isolation
|
689
|
+
const foundUserGenerationWithTask = await generationModel.findByIdWithAsyncTask(
|
690
|
+
userGenerationCreated.id,
|
691
|
+
);
|
692
|
+
const foundOtherGenerationWithTask = await generationModel.findByIdWithAsyncTask(
|
693
|
+
otherUserGenerationCreated.id,
|
694
|
+
);
|
695
|
+
|
696
|
+
expect(foundUserGenerationWithTask).toBeDefined();
|
697
|
+
expect(foundOtherGenerationWithTask).toBeUndefined();
|
698
|
+
|
699
|
+
// Test findByIdAndTransform isolation
|
700
|
+
const transformedUserGeneration = await generationModel.findByIdAndTransform(
|
701
|
+
userGenerationCreated.id,
|
702
|
+
);
|
703
|
+
const transformedOtherGeneration = await generationModel.findByIdAndTransform(
|
704
|
+
otherUserGenerationCreated.id,
|
705
|
+
);
|
706
|
+
|
707
|
+
expect(transformedUserGeneration).toBeDefined();
|
708
|
+
expect(transformedOtherGeneration).toBeNull();
|
709
|
+
|
710
|
+
// Test update isolation - should not affect other user's data
|
711
|
+
await generationModel.update(otherUserGenerationCreated.id, { seed: 99999 });
|
712
|
+
const otherUserGenerationUnchanged = await serverDB.query.generations.findFirst({
|
713
|
+
where: eq(generations.id, otherUserGenerationCreated.id),
|
714
|
+
});
|
715
|
+
expect(otherUserGenerationUnchanged?.seed).toBe(testGeneration.seed); // Should not be updated
|
716
|
+
|
717
|
+
// Test delete isolation - should not affect other user's data
|
718
|
+
await generationModel.delete(otherUserGenerationCreated.id);
|
719
|
+
const otherUserGenerationStillExists = await serverDB.query.generations.findFirst({
|
720
|
+
where: eq(generations.id, otherUserGenerationCreated.id),
|
721
|
+
});
|
722
|
+
expect(otherUserGenerationStillExists).toBeDefined(); // Should not be deleted
|
723
|
+
});
|
724
|
+
});
|
725
|
+
|
726
|
+
describe('External service integration', () => {
|
727
|
+
it('should call FileService with correct parameters', async () => {
|
728
|
+
const [createdGeneration] = await serverDB
|
729
|
+
.insert(generations)
|
730
|
+
.values({ ...testGeneration, userId })
|
731
|
+
.returning();
|
732
|
+
|
733
|
+
await generationModel.findByIdAndTransform(createdGeneration.id);
|
734
|
+
|
735
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('asset-url.jpg');
|
736
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('thumbnail-url.jpg');
|
737
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(2);
|
738
|
+
});
|
739
|
+
|
740
|
+
it('should call FileModel.create with correct parameters during createAssetAndFile', async () => {
|
741
|
+
const [createdGeneration] = await serverDB
|
742
|
+
.insert(generations)
|
743
|
+
.values({ ...testGeneration, userId, asset: null, fileId: null })
|
744
|
+
.returning();
|
745
|
+
|
746
|
+
const asset = {
|
747
|
+
url: 'new-asset.jpg',
|
748
|
+
thumbnailUrl: 'new-thumbnail.jpg',
|
749
|
+
width: 1024,
|
750
|
+
height: 1024,
|
751
|
+
} as ImageGenerationAsset;
|
752
|
+
|
753
|
+
const fileData = {
|
754
|
+
name: 'test-image.jpg',
|
755
|
+
url: 'https://example.com/test-image.jpg',
|
756
|
+
size: 1024,
|
757
|
+
fileType: 'image/jpeg',
|
758
|
+
};
|
759
|
+
|
760
|
+
await generationModel.createAssetAndFile(createdGeneration.id, asset, fileData);
|
761
|
+
|
762
|
+
expect(mockFileModelCreate).toHaveBeenCalledWith(
|
763
|
+
{
|
764
|
+
...fileData,
|
765
|
+
source: FileSource.ImageGeneration,
|
766
|
+
},
|
767
|
+
true,
|
768
|
+
expect.any(Object),
|
769
|
+
);
|
770
|
+
expect(mockFileModelCreate).toHaveBeenCalledTimes(1);
|
771
|
+
});
|
772
|
+
|
773
|
+
it('should handle FileService errors gracefully', async () => {
|
774
|
+
mockGetFullFileUrl.mockRejectedValue(new Error('FileService error'));
|
775
|
+
|
776
|
+
const [createdGeneration] = await serverDB
|
777
|
+
.insert(generations)
|
778
|
+
.values({ ...testGeneration, userId })
|
779
|
+
.returning();
|
780
|
+
|
781
|
+
await expect(generationModel.findByIdAndTransform(createdGeneration.id)).rejects.toThrow(
|
782
|
+
'FileService error',
|
783
|
+
);
|
784
|
+
});
|
785
|
+
});
|
786
|
+
});
|