@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,197 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { and, eq } from 'drizzle-orm';
|
3
|
+
|
4
|
+
import { LobeChatDatabase, Transaction } from '@/database/type';
|
5
|
+
import { FileService } from '@/server/services/file';
|
6
|
+
import { AsyncTaskError, AsyncTaskStatus } from '@/types/asyncTask';
|
7
|
+
import { FileSource } from '@/types/files';
|
8
|
+
import { Generation, ImageGenerationAsset } from '@/types/generation';
|
9
|
+
|
10
|
+
import { NewFile } from '../schemas';
|
11
|
+
import {
|
12
|
+
GenerationItem,
|
13
|
+
GenerationWithAsyncTask,
|
14
|
+
NewGeneration,
|
15
|
+
generations,
|
16
|
+
} from '../schemas/generation';
|
17
|
+
import { FileModel } from './file';
|
18
|
+
|
19
|
+
// Create debug logger
|
20
|
+
const log = debug('lobe-image:generation-model');
|
21
|
+
|
22
|
+
export class GenerationModel {
|
23
|
+
private db: LobeChatDatabase;
|
24
|
+
private userId: string;
|
25
|
+
private fileModel: FileModel;
|
26
|
+
private fileService: FileService;
|
27
|
+
|
28
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
29
|
+
this.db = db;
|
30
|
+
this.userId = userId;
|
31
|
+
this.fileModel = new FileModel(db, userId);
|
32
|
+
this.fileService = new FileService(db, userId);
|
33
|
+
}
|
34
|
+
|
35
|
+
async create(value: Omit<NewGeneration, 'userId'>): Promise<GenerationItem> {
|
36
|
+
log('Creating generation: %O', {
|
37
|
+
generationBatchId: value.generationBatchId,
|
38
|
+
userId: this.userId,
|
39
|
+
});
|
40
|
+
|
41
|
+
const [result] = await this.db
|
42
|
+
.insert(generations)
|
43
|
+
.values({ ...value, userId: this.userId })
|
44
|
+
.returning();
|
45
|
+
|
46
|
+
log('Generation created successfully: %s', result.id);
|
47
|
+
return result;
|
48
|
+
}
|
49
|
+
|
50
|
+
async findById(id: string): Promise<GenerationItem | undefined> {
|
51
|
+
log('Finding generation by ID: %s for user: %s', id, this.userId);
|
52
|
+
|
53
|
+
const result = await this.db.query.generations.findFirst({
|
54
|
+
where: and(eq(generations.id, id), eq(generations.userId, this.userId)),
|
55
|
+
});
|
56
|
+
|
57
|
+
log('Generation %s: %s', id, result ? 'found' : 'not found');
|
58
|
+
return result;
|
59
|
+
}
|
60
|
+
|
61
|
+
async findByIdWithAsyncTask(id: string): Promise<GenerationWithAsyncTask | undefined> {
|
62
|
+
log('Finding generation by ID: %s for user: %s', id, this.userId);
|
63
|
+
|
64
|
+
const result = await this.db.query.generations.findFirst({
|
65
|
+
where: and(eq(generations.id, id), eq(generations.userId, this.userId)),
|
66
|
+
with: {
|
67
|
+
asyncTask: true,
|
68
|
+
},
|
69
|
+
});
|
70
|
+
|
71
|
+
log('Generation %s: %s', id, result ? 'found' : 'not found');
|
72
|
+
return result as GenerationWithAsyncTask | undefined;
|
73
|
+
}
|
74
|
+
|
75
|
+
async update(id: string, value: Partial<NewGeneration>, trx?: Transaction) {
|
76
|
+
log('Updating generation: %s with values: %O', id, {
|
77
|
+
asyncTaskId: value.asyncTaskId,
|
78
|
+
hasAsset: !!value.asset,
|
79
|
+
});
|
80
|
+
|
81
|
+
const executeUpdate = async (tx: Transaction) => {
|
82
|
+
return await tx
|
83
|
+
.update(generations)
|
84
|
+
.set({ ...value, updatedAt: new Date() })
|
85
|
+
.where(and(eq(generations.id, id), eq(generations.userId, this.userId)));
|
86
|
+
};
|
87
|
+
|
88
|
+
const result = await (trx ? executeUpdate(trx) : this.db.transaction(executeUpdate));
|
89
|
+
|
90
|
+
log('Generation %s updated successfully', id);
|
91
|
+
return result;
|
92
|
+
}
|
93
|
+
|
94
|
+
async createAssetAndFile(
|
95
|
+
id: string,
|
96
|
+
asset: ImageGenerationAsset,
|
97
|
+
file: Omit<NewFile, 'id' | 'userId'>,
|
98
|
+
) {
|
99
|
+
log('Creating generation asset and file with transaction: %s', id);
|
100
|
+
|
101
|
+
return await this.db.transaction(async (tx: Transaction) => {
|
102
|
+
// Create file first using transaction
|
103
|
+
// Since duplicates are very rare, we always create globalFile - checking existence first would be wasteful
|
104
|
+
const newFile = await this.fileModel.create(
|
105
|
+
{
|
106
|
+
...file,
|
107
|
+
source: FileSource.ImageGeneration,
|
108
|
+
},
|
109
|
+
true,
|
110
|
+
tx,
|
111
|
+
);
|
112
|
+
|
113
|
+
// Update generation with asset and fileId using the transaction-aware update method
|
114
|
+
await this.update(
|
115
|
+
id,
|
116
|
+
{
|
117
|
+
asset,
|
118
|
+
fileId: newFile.id,
|
119
|
+
},
|
120
|
+
tx,
|
121
|
+
);
|
122
|
+
|
123
|
+
log('Generation %s updated with asset and file %s successfully', id, newFile.id);
|
124
|
+
|
125
|
+
return {
|
126
|
+
file: newFile,
|
127
|
+
};
|
128
|
+
});
|
129
|
+
}
|
130
|
+
|
131
|
+
async delete(id: string, trx?: Transaction) {
|
132
|
+
log('Deleting generation: %s for user: %s', id, this.userId);
|
133
|
+
|
134
|
+
const executeDelete = async (tx: Transaction) => {
|
135
|
+
return await tx
|
136
|
+
.delete(generations)
|
137
|
+
.where(and(eq(generations.id, id), eq(generations.userId, this.userId)))
|
138
|
+
.returning();
|
139
|
+
};
|
140
|
+
|
141
|
+
const result = await (trx ? executeDelete(trx) : this.db.transaction(executeDelete));
|
142
|
+
const deletedGeneration = result[0];
|
143
|
+
|
144
|
+
log('Generation %s deleted successfully', id);
|
145
|
+
return deletedGeneration;
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* Find generation by ID and transform it to frontend type
|
150
|
+
* This method uses findByIdWithAsyncTask and applies transformation
|
151
|
+
*/
|
152
|
+
async findByIdAndTransform(id: string): Promise<Generation | null> {
|
153
|
+
log('Finding and transforming generation: %s', id);
|
154
|
+
|
155
|
+
const generation = await this.findByIdWithAsyncTask(id);
|
156
|
+
if (!generation) {
|
157
|
+
log('Generation %s not found', id);
|
158
|
+
return null;
|
159
|
+
}
|
160
|
+
|
161
|
+
return await this.transformGeneration(generation);
|
162
|
+
}
|
163
|
+
|
164
|
+
/**
|
165
|
+
* Transform a GenerationItem (database type) to Generation (frontend type)
|
166
|
+
* This method processes asset URLs and async task information
|
167
|
+
*/
|
168
|
+
async transformGeneration(generation: GenerationWithAsyncTask): Promise<Generation> {
|
169
|
+
// Process asset URLs if they exist, following the same logic as in generationBatch.ts
|
170
|
+
const asset = generation.asset as ImageGenerationAsset | null;
|
171
|
+
if (asset && asset.url && asset.thumbnailUrl) {
|
172
|
+
const [url, thumbnailUrl] = await Promise.all([
|
173
|
+
this.fileService.getFullFileUrl(asset.url),
|
174
|
+
this.fileService.getFullFileUrl(asset.thumbnailUrl),
|
175
|
+
]);
|
176
|
+
asset.url = url;
|
177
|
+
asset.thumbnailUrl = thumbnailUrl;
|
178
|
+
}
|
179
|
+
|
180
|
+
// Build the Generation object following the same structure as in generationBatch.ts
|
181
|
+
const result: Generation = {
|
182
|
+
asset,
|
183
|
+
asyncTaskId: generation.asyncTaskId || null,
|
184
|
+
createdAt: generation.createdAt,
|
185
|
+
id: generation.id,
|
186
|
+
seed: generation.seed,
|
187
|
+
task: {
|
188
|
+
error: generation.asyncTask?.error
|
189
|
+
? (generation.asyncTask.error as AsyncTaskError)
|
190
|
+
: undefined,
|
191
|
+
id: generation.asyncTaskId || '',
|
192
|
+
status: (generation.asyncTask?.status as AsyncTaskStatus) || 'pending',
|
193
|
+
},
|
194
|
+
};
|
195
|
+
return result;
|
196
|
+
}
|
197
|
+
}
|
@@ -0,0 +1,212 @@
|
|
1
|
+
import debug from 'debug';
|
2
|
+
import { and, eq } from 'drizzle-orm';
|
3
|
+
|
4
|
+
import { LobeChatDatabase } from '@/database/type';
|
5
|
+
import { FileService } from '@/server/services/file';
|
6
|
+
import { Generation, GenerationAsset, GenerationBatch, GenerationConfig } from '@/types/generation';
|
7
|
+
|
8
|
+
import {
|
9
|
+
GenerationBatchItem,
|
10
|
+
GenerationBatchWithGenerations,
|
11
|
+
NewGenerationBatch,
|
12
|
+
generationBatches,
|
13
|
+
} from '../schemas/generation';
|
14
|
+
import { GenerationModel } from './generation';
|
15
|
+
|
16
|
+
const log = debug('lobe-image:generation-batch-model');
|
17
|
+
|
18
|
+
export class GenerationBatchModel {
|
19
|
+
private db: LobeChatDatabase;
|
20
|
+
private userId: string;
|
21
|
+
private fileService: FileService;
|
22
|
+
private generationModel: GenerationModel;
|
23
|
+
|
24
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
25
|
+
this.db = db;
|
26
|
+
this.userId = userId;
|
27
|
+
this.fileService = new FileService(db, userId);
|
28
|
+
this.generationModel = new GenerationModel(db, userId);
|
29
|
+
}
|
30
|
+
|
31
|
+
async create(value: NewGenerationBatch): Promise<GenerationBatchItem> {
|
32
|
+
log('Creating generation batch: %O', {
|
33
|
+
topicId: value.generationTopicId,
|
34
|
+
userId: this.userId,
|
35
|
+
});
|
36
|
+
|
37
|
+
const [result] = await this.db
|
38
|
+
.insert(generationBatches)
|
39
|
+
.values({ ...value, userId: this.userId })
|
40
|
+
.returning();
|
41
|
+
|
42
|
+
log('Generation batch created successfully: %s', result.id);
|
43
|
+
return result;
|
44
|
+
}
|
45
|
+
|
46
|
+
async findById(id: string): Promise<GenerationBatchItem | undefined> {
|
47
|
+
log('Finding generation batch by ID: %s for user: %s', id, this.userId);
|
48
|
+
|
49
|
+
const result = await this.db.query.generationBatches.findFirst({
|
50
|
+
where: and(eq(generationBatches.id, id), eq(generationBatches.userId, this.userId)),
|
51
|
+
});
|
52
|
+
|
53
|
+
log('Generation batch %s: %s', id, result ? 'found' : 'not found');
|
54
|
+
return result;
|
55
|
+
}
|
56
|
+
|
57
|
+
async findByTopicId(topicId: string): Promise<GenerationBatchItem[]> {
|
58
|
+
log('Finding generation batches by topic ID: %s for user: %s', topicId, this.userId);
|
59
|
+
|
60
|
+
const results = await this.db.query.generationBatches.findMany({
|
61
|
+
orderBy: (table, { desc }) => [desc(table.createdAt)],
|
62
|
+
where: and(
|
63
|
+
eq(generationBatches.generationTopicId, topicId),
|
64
|
+
eq(generationBatches.userId, this.userId),
|
65
|
+
),
|
66
|
+
});
|
67
|
+
|
68
|
+
log('Found %d generation batches for topic %s', results.length, topicId);
|
69
|
+
return results;
|
70
|
+
}
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Find batches with their associated generations using relations
|
74
|
+
*/
|
75
|
+
async findByTopicIdWithGenerations(topicId: string): Promise<GenerationBatchWithGenerations[]> {
|
76
|
+
log(
|
77
|
+
'Finding generation batches with generations for topic ID: %s for user: %s',
|
78
|
+
topicId,
|
79
|
+
this.userId,
|
80
|
+
);
|
81
|
+
|
82
|
+
const results = await this.db.query.generationBatches.findMany({
|
83
|
+
orderBy: (table, { asc }) => [asc(table.createdAt)],
|
84
|
+
where: and(
|
85
|
+
eq(generationBatches.generationTopicId, topicId),
|
86
|
+
eq(generationBatches.userId, this.userId),
|
87
|
+
),
|
88
|
+
with: {
|
89
|
+
generations: {
|
90
|
+
orderBy: (table, { asc }) => [asc(table.createdAt), asc(table.id)],
|
91
|
+
with: {
|
92
|
+
asyncTask: true,
|
93
|
+
},
|
94
|
+
},
|
95
|
+
},
|
96
|
+
});
|
97
|
+
|
98
|
+
log('Found %d generation batches with generations for topic %s', results.length, topicId);
|
99
|
+
return results as GenerationBatchWithGenerations[];
|
100
|
+
}
|
101
|
+
|
102
|
+
async queryGenerationBatchesByTopicIdWithGenerations(
|
103
|
+
topicId: string,
|
104
|
+
): Promise<(GenerationBatch & { generations: Generation[] })[]> {
|
105
|
+
log('Fetching generation batches for topic ID: %s for user: %s', topicId, this.userId);
|
106
|
+
|
107
|
+
const batchesWithGenerations = await this.findByTopicIdWithGenerations(topicId);
|
108
|
+
if (batchesWithGenerations.length === 0) {
|
109
|
+
log('No batches found for topic: %s', topicId);
|
110
|
+
return [];
|
111
|
+
}
|
112
|
+
|
113
|
+
// Transform the database result to match our frontend types
|
114
|
+
const result: GenerationBatch[] = await Promise.all(
|
115
|
+
batchesWithGenerations.map(async (batch) => {
|
116
|
+
const [generations, config] = await Promise.all([
|
117
|
+
// Transform generations
|
118
|
+
Promise.all(
|
119
|
+
batch.generations.map((gen) => this.generationModel.transformGeneration(gen)),
|
120
|
+
),
|
121
|
+
// Transform config
|
122
|
+
(async () => {
|
123
|
+
const config = batch.config as GenerationConfig;
|
124
|
+
if (Array.isArray(config.imageUrls)) {
|
125
|
+
config.imageUrls = await Promise.all(
|
126
|
+
config.imageUrls.map((url) => this.fileService.getFullFileUrl(url)),
|
127
|
+
);
|
128
|
+
}
|
129
|
+
return config;
|
130
|
+
})(),
|
131
|
+
]);
|
132
|
+
|
133
|
+
return {
|
134
|
+
config,
|
135
|
+
createdAt: batch.createdAt,
|
136
|
+
generations,
|
137
|
+
height: batch.height,
|
138
|
+
id: batch.id,
|
139
|
+
model: batch.model,
|
140
|
+
prompt: batch.prompt,
|
141
|
+
provider: batch.provider,
|
142
|
+
width: batch.width,
|
143
|
+
};
|
144
|
+
}),
|
145
|
+
);
|
146
|
+
|
147
|
+
log('Feed construction complete for topic: %s, returning %d batches', topicId, result.length);
|
148
|
+
return result;
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Delete a generation batch and return associated file URLs for cleanup
|
153
|
+
*
|
154
|
+
* This method follows the "database first, files second" deletion principle:
|
155
|
+
* 1. First queries the batch with its generations to collect thumbnail URLs
|
156
|
+
* 2. Then deletes the database record (cascade delete handles related generations)
|
157
|
+
* 3. Returns the deleted batch data and thumbnail URLs for file cleanup
|
158
|
+
*
|
159
|
+
* @param id - The batch ID to delete
|
160
|
+
* @returns Object containing deleted batch data and thumbnail URLs to clean, or undefined if batch not found or access denied
|
161
|
+
*/
|
162
|
+
async delete(
|
163
|
+
id: string,
|
164
|
+
): Promise<{ deletedBatch: GenerationBatchItem; thumbnailUrls: string[] } | undefined> {
|
165
|
+
log('Deleting generation batch: %s for user: %s', id, this.userId);
|
166
|
+
|
167
|
+
// 1. First, get generations with their assets to collect thumbnail URLs
|
168
|
+
const batchWithGenerations = await this.db.query.generationBatches.findFirst({
|
169
|
+
where: and(eq(generationBatches.id, id), eq(generationBatches.userId, this.userId)),
|
170
|
+
with: {
|
171
|
+
generations: {
|
172
|
+
columns: {
|
173
|
+
asset: true,
|
174
|
+
},
|
175
|
+
},
|
176
|
+
},
|
177
|
+
});
|
178
|
+
|
179
|
+
// If batch doesn't exist or doesn't belong to user, return undefined
|
180
|
+
if (!batchWithGenerations) {
|
181
|
+
return undefined;
|
182
|
+
}
|
183
|
+
|
184
|
+
// 2. Collect thumbnail URLs that need to be deleted
|
185
|
+
const thumbnailUrls: string[] = [];
|
186
|
+
if (batchWithGenerations.generations) {
|
187
|
+
for (const gen of batchWithGenerations.generations) {
|
188
|
+
const asset = gen.asset as GenerationAsset;
|
189
|
+
if (asset?.thumbnailUrl) {
|
190
|
+
thumbnailUrls.push(asset.thumbnailUrl);
|
191
|
+
}
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
// 3. Delete the batch record (this will cascade delete all associated generations)
|
196
|
+
const [deletedBatch] = await this.db
|
197
|
+
.delete(generationBatches)
|
198
|
+
.where(and(eq(generationBatches.id, id), eq(generationBatches.userId, this.userId)))
|
199
|
+
.returning();
|
200
|
+
|
201
|
+
log(
|
202
|
+
'Generation batch %s deleted successfully with %d thumbnails to clean',
|
203
|
+
id,
|
204
|
+
thumbnailUrls.length,
|
205
|
+
);
|
206
|
+
|
207
|
+
return {
|
208
|
+
deletedBatch,
|
209
|
+
thumbnailUrls,
|
210
|
+
};
|
211
|
+
}
|
212
|
+
}
|
@@ -0,0 +1,131 @@
|
|
1
|
+
import { and, desc, eq } from 'drizzle-orm/expressions';
|
2
|
+
|
3
|
+
import { LobeChatDatabase } from '@/database/type';
|
4
|
+
import { FileService } from '@/server/services/file';
|
5
|
+
import { GenerationAsset, ImageGenerationTopic } from '@/types/generation';
|
6
|
+
|
7
|
+
import { GenerationTopicItem, generationTopics } from '../schemas/generation';
|
8
|
+
|
9
|
+
export class GenerationTopicModel {
|
10
|
+
private userId: string;
|
11
|
+
private db: LobeChatDatabase;
|
12
|
+
private fileService: FileService;
|
13
|
+
|
14
|
+
constructor(db: LobeChatDatabase, userId: string) {
|
15
|
+
this.userId = userId;
|
16
|
+
this.db = db;
|
17
|
+
this.fileService = new FileService(db, userId);
|
18
|
+
}
|
19
|
+
|
20
|
+
queryAll = async () => {
|
21
|
+
const topics = await this.db
|
22
|
+
.select()
|
23
|
+
.from(generationTopics)
|
24
|
+
.orderBy(desc(generationTopics.updatedAt))
|
25
|
+
.where(eq(generationTopics.userId, this.userId));
|
26
|
+
|
27
|
+
return Promise.all(
|
28
|
+
topics.map(async (topic) => {
|
29
|
+
if (topic.coverUrl) {
|
30
|
+
return {
|
31
|
+
...topic,
|
32
|
+
coverUrl: await this.fileService.getFullFileUrl(topic.coverUrl),
|
33
|
+
};
|
34
|
+
}
|
35
|
+
return topic;
|
36
|
+
}),
|
37
|
+
);
|
38
|
+
};
|
39
|
+
|
40
|
+
create = async (title: string) => {
|
41
|
+
const [newGenerationTopic] = await this.db
|
42
|
+
.insert(generationTopics)
|
43
|
+
.values({
|
44
|
+
title,
|
45
|
+
userId: this.userId,
|
46
|
+
})
|
47
|
+
.returning();
|
48
|
+
|
49
|
+
return newGenerationTopic;
|
50
|
+
};
|
51
|
+
|
52
|
+
update = async (
|
53
|
+
id: string,
|
54
|
+
data: Partial<ImageGenerationTopic>,
|
55
|
+
): Promise<GenerationTopicItem | undefined> => {
|
56
|
+
const [updatedTopic] = await this.db
|
57
|
+
.update(generationTopics)
|
58
|
+
.set({ ...data, updatedAt: new Date() })
|
59
|
+
.where(and(eq(generationTopics.id, id), eq(generationTopics.userId, this.userId)))
|
60
|
+
.returning();
|
61
|
+
|
62
|
+
return updatedTopic;
|
63
|
+
};
|
64
|
+
|
65
|
+
/**
|
66
|
+
* Delete a generation topic and return associated file URLs for cleanup
|
67
|
+
*
|
68
|
+
* This method follows the "database first, files second" deletion principle:
|
69
|
+
* 1. First queries the topic with all its batches and generations to collect file URLs
|
70
|
+
* 2. Then deletes the database record (cascade delete handles related batches and generations)
|
71
|
+
* 3. Returns the deleted topic data and file URLs for cleanup
|
72
|
+
*
|
73
|
+
* @param id - The topic ID to delete
|
74
|
+
* @returns Object containing deleted topic data and file URLs to clean, or undefined if topic not found or access denied
|
75
|
+
*/
|
76
|
+
delete = async (
|
77
|
+
id: string,
|
78
|
+
): Promise<{ deletedTopic: GenerationTopicItem; filesToDelete: string[] } | undefined> => {
|
79
|
+
// 1. First, get the topic with all its batches and generations to collect file URLs
|
80
|
+
const topicWithBatches = await this.db.query.generationTopics.findFirst({
|
81
|
+
where: and(eq(generationTopics.id, id), eq(generationTopics.userId, this.userId)),
|
82
|
+
with: {
|
83
|
+
batches: {
|
84
|
+
with: {
|
85
|
+
generations: {
|
86
|
+
columns: {
|
87
|
+
asset: true,
|
88
|
+
},
|
89
|
+
},
|
90
|
+
},
|
91
|
+
},
|
92
|
+
},
|
93
|
+
});
|
94
|
+
|
95
|
+
// If topic doesn't exist or doesn't belong to user, return undefined
|
96
|
+
if (!topicWithBatches) {
|
97
|
+
return undefined;
|
98
|
+
}
|
99
|
+
|
100
|
+
// 2. Collect all file URLs that need to be deleted
|
101
|
+
const filesToDelete: string[] = [];
|
102
|
+
|
103
|
+
// Add cover image URL if exists
|
104
|
+
if (topicWithBatches.coverUrl) {
|
105
|
+
filesToDelete.push(topicWithBatches.coverUrl);
|
106
|
+
}
|
107
|
+
|
108
|
+
// Add thumbnail URLs from all generations
|
109
|
+
if (topicWithBatches.batches) {
|
110
|
+
for (const batch of topicWithBatches.batches) {
|
111
|
+
for (const gen of batch.generations) {
|
112
|
+
const asset = gen.asset as GenerationAsset;
|
113
|
+
if (asset?.thumbnailUrl) {
|
114
|
+
filesToDelete.push(asset.thumbnailUrl);
|
115
|
+
}
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
// 3. Delete the topic record (this will cascade delete all batches and generations)
|
121
|
+
const [deletedTopic] = await this.db
|
122
|
+
.delete(generationTopics)
|
123
|
+
.where(and(eq(generationTopics.id, id), eq(generationTopics.userId, this.userId)))
|
124
|
+
.returning();
|
125
|
+
|
126
|
+
return {
|
127
|
+
deletedTopic,
|
128
|
+
filesToDelete,
|
129
|
+
};
|
130
|
+
};
|
131
|
+
}
|