@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,508 @@
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
2
|
+
|
3
|
+
import { GenerationTopicModel } from '@/database/models/generationTopic';
|
4
|
+
import { GenerationTopicItem } from '@/database/schemas/generation';
|
5
|
+
import { FileService } from '@/server/services/file';
|
6
|
+
import { GenerationService } from '@/server/services/generation';
|
7
|
+
|
8
|
+
import { generationTopicRouter } from './generationTopic';
|
9
|
+
|
10
|
+
vi.mock('@/database/models/generationTopic');
|
11
|
+
vi.mock('@/server/services/file');
|
12
|
+
vi.mock('@/server/services/generation');
|
13
|
+
|
14
|
+
describe('generationTopicRouter', () => {
|
15
|
+
const mockCtx = {
|
16
|
+
userId: 'test-user',
|
17
|
+
serverDB: {} as any,
|
18
|
+
};
|
19
|
+
|
20
|
+
beforeEach(() => {
|
21
|
+
vi.clearAllMocks();
|
22
|
+
});
|
23
|
+
|
24
|
+
it('should create a new topic', async () => {
|
25
|
+
const mockTopicId = 'topic-123';
|
26
|
+
const mockCreatedTopic = {
|
27
|
+
id: mockTopicId,
|
28
|
+
title: '',
|
29
|
+
userId: 'test-user',
|
30
|
+
coverUrl: null,
|
31
|
+
accessedAt: new Date(),
|
32
|
+
createdAt: new Date(),
|
33
|
+
updatedAt: new Date(),
|
34
|
+
};
|
35
|
+
|
36
|
+
const mockCreate = vi.fn().mockResolvedValue(mockCreatedTopic);
|
37
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
38
|
+
() =>
|
39
|
+
({
|
40
|
+
create: mockCreate,
|
41
|
+
}) as any,
|
42
|
+
);
|
43
|
+
|
44
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
45
|
+
const result = await caller.createTopic();
|
46
|
+
|
47
|
+
expect(result).toBe(mockTopicId);
|
48
|
+
expect(mockCreate).toHaveBeenCalledWith('');
|
49
|
+
});
|
50
|
+
|
51
|
+
it('should get all generation topics', async () => {
|
52
|
+
const mockTopics: GenerationTopicItem[] = [
|
53
|
+
{
|
54
|
+
id: 'topic-1',
|
55
|
+
title: 'Test Topic 1',
|
56
|
+
userId: 'test-user',
|
57
|
+
coverUrl: 'cover-url-1',
|
58
|
+
accessedAt: new Date(),
|
59
|
+
createdAt: new Date(),
|
60
|
+
updatedAt: new Date(),
|
61
|
+
},
|
62
|
+
{
|
63
|
+
id: 'topic-2',
|
64
|
+
title: 'Test Topic 2',
|
65
|
+
userId: 'test-user',
|
66
|
+
coverUrl: null,
|
67
|
+
accessedAt: new Date(),
|
68
|
+
createdAt: new Date(),
|
69
|
+
updatedAt: new Date(),
|
70
|
+
},
|
71
|
+
];
|
72
|
+
|
73
|
+
const mockQueryAll = vi.fn().mockResolvedValue(mockTopics);
|
74
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
75
|
+
() =>
|
76
|
+
({
|
77
|
+
queryAll: mockQueryAll,
|
78
|
+
}) as any,
|
79
|
+
);
|
80
|
+
|
81
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
82
|
+
const result = await caller.getAllGenerationTopics();
|
83
|
+
|
84
|
+
expect(result).toEqual(mockTopics);
|
85
|
+
expect(mockQueryAll).toHaveBeenCalled();
|
86
|
+
});
|
87
|
+
|
88
|
+
it('should update a topic', async () => {
|
89
|
+
const mockTopicId = 'topic-123';
|
90
|
+
const mockUpdateValue = {
|
91
|
+
title: 'Updated Title',
|
92
|
+
coverUrl: 'updated-cover-url',
|
93
|
+
};
|
94
|
+
const mockUpdatedTopic = {
|
95
|
+
id: mockTopicId,
|
96
|
+
...mockUpdateValue,
|
97
|
+
userId: 'test-user',
|
98
|
+
accessedAt: new Date(),
|
99
|
+
createdAt: new Date(),
|
100
|
+
updatedAt: new Date(),
|
101
|
+
};
|
102
|
+
|
103
|
+
const mockUpdate = vi.fn().mockResolvedValue(mockUpdatedTopic);
|
104
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
105
|
+
() =>
|
106
|
+
({
|
107
|
+
update: mockUpdate,
|
108
|
+
}) as any,
|
109
|
+
);
|
110
|
+
|
111
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
112
|
+
const result = await caller.updateTopic({
|
113
|
+
id: mockTopicId,
|
114
|
+
value: mockUpdateValue,
|
115
|
+
});
|
116
|
+
|
117
|
+
expect(result).toEqual(mockUpdatedTopic);
|
118
|
+
expect(mockUpdate).toHaveBeenCalledWith(mockTopicId, mockUpdateValue);
|
119
|
+
});
|
120
|
+
|
121
|
+
it('should update topic cover', async () => {
|
122
|
+
const mockTopicId = 'topic-123';
|
123
|
+
const mockCoverUrl = 'https://example.com/cover.jpg';
|
124
|
+
const mockNewCoverKey = 'generations/covers/new-cover-key.webp';
|
125
|
+
const mockUpdatedTopic = {
|
126
|
+
id: mockTopicId,
|
127
|
+
title: 'Test Topic',
|
128
|
+
userId: 'test-user',
|
129
|
+
coverUrl: mockNewCoverKey,
|
130
|
+
accessedAt: new Date(),
|
131
|
+
createdAt: new Date(),
|
132
|
+
updatedAt: new Date(),
|
133
|
+
};
|
134
|
+
|
135
|
+
const mockCreateCoverFromUrl = vi.fn().mockResolvedValue(mockNewCoverKey);
|
136
|
+
const mockUpdate = vi.fn().mockResolvedValue(mockUpdatedTopic);
|
137
|
+
|
138
|
+
vi.mocked(GenerationService).mockImplementation(
|
139
|
+
() =>
|
140
|
+
({
|
141
|
+
createCoverFromUrl: mockCreateCoverFromUrl,
|
142
|
+
}) as any,
|
143
|
+
);
|
144
|
+
|
145
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
146
|
+
() =>
|
147
|
+
({
|
148
|
+
update: mockUpdate,
|
149
|
+
}) as any,
|
150
|
+
);
|
151
|
+
|
152
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
153
|
+
const result = await caller.updateTopicCover({
|
154
|
+
id: mockTopicId,
|
155
|
+
coverUrl: mockCoverUrl,
|
156
|
+
});
|
157
|
+
|
158
|
+
expect(result).toEqual(mockUpdatedTopic);
|
159
|
+
expect(mockCreateCoverFromUrl).toHaveBeenCalledWith(mockCoverUrl);
|
160
|
+
expect(mockUpdate).toHaveBeenCalledWith(mockTopicId, { coverUrl: mockNewCoverKey });
|
161
|
+
});
|
162
|
+
|
163
|
+
it('should delete a topic without cover', async () => {
|
164
|
+
const mockTopicId = 'topic-123';
|
165
|
+
const mockDeletedTopic = {
|
166
|
+
id: mockTopicId,
|
167
|
+
title: 'Deleted Topic',
|
168
|
+
userId: 'test-user',
|
169
|
+
coverUrl: null,
|
170
|
+
accessedAt: new Date(),
|
171
|
+
createdAt: new Date(),
|
172
|
+
updatedAt: new Date(),
|
173
|
+
};
|
174
|
+
|
175
|
+
// 修复 Mock 返回结构以匹配新的实现
|
176
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
177
|
+
deletedTopic: mockDeletedTopic,
|
178
|
+
filesToDelete: [], // 没有封面时,文件列表为空
|
179
|
+
});
|
180
|
+
const mockDeleteFiles = vi.fn();
|
181
|
+
|
182
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
183
|
+
() =>
|
184
|
+
({
|
185
|
+
delete: mockDelete,
|
186
|
+
}) as any,
|
187
|
+
);
|
188
|
+
|
189
|
+
vi.mocked(FileService).mockImplementation(
|
190
|
+
() =>
|
191
|
+
({
|
192
|
+
deleteFiles: mockDeleteFiles,
|
193
|
+
}) as any,
|
194
|
+
);
|
195
|
+
|
196
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
197
|
+
const result = await caller.deleteTopic({ id: mockTopicId });
|
198
|
+
|
199
|
+
expect(result).toEqual(mockDeletedTopic);
|
200
|
+
expect(mockDelete).toHaveBeenCalledWith(mockTopicId);
|
201
|
+
expect(mockDeleteFiles).not.toHaveBeenCalled(); // 没有文件要删除
|
202
|
+
});
|
203
|
+
|
204
|
+
it('should delete a topic with cover and remove the cover file', async () => {
|
205
|
+
const mockTopicId = 'topic-123';
|
206
|
+
const mockCoverUrl = 'generations/covers/cover-key.webp';
|
207
|
+
const mockDeletedTopic = {
|
208
|
+
id: mockTopicId,
|
209
|
+
title: 'Deleted Topic',
|
210
|
+
userId: 'test-user',
|
211
|
+
coverUrl: mockCoverUrl,
|
212
|
+
accessedAt: new Date(),
|
213
|
+
createdAt: new Date(),
|
214
|
+
updatedAt: new Date(),
|
215
|
+
};
|
216
|
+
|
217
|
+
// 修复 Mock 返回结构以匹配新的实现
|
218
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
219
|
+
deletedTopic: mockDeletedTopic,
|
220
|
+
filesToDelete: [mockCoverUrl], // 包含需要删除的封面文件
|
221
|
+
});
|
222
|
+
const mockDeleteFiles = vi.fn().mockResolvedValue(true);
|
223
|
+
|
224
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
225
|
+
() =>
|
226
|
+
({
|
227
|
+
delete: mockDelete,
|
228
|
+
}) as any,
|
229
|
+
);
|
230
|
+
|
231
|
+
vi.mocked(FileService).mockImplementation(
|
232
|
+
() =>
|
233
|
+
({
|
234
|
+
deleteFiles: mockDeleteFiles,
|
235
|
+
}) as any,
|
236
|
+
);
|
237
|
+
|
238
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
239
|
+
const result = await caller.deleteTopic({ id: mockTopicId });
|
240
|
+
|
241
|
+
expect(result).toEqual(mockDeletedTopic);
|
242
|
+
expect(mockDelete).toHaveBeenCalledWith(mockTopicId);
|
243
|
+
expect(mockDeleteFiles).toHaveBeenCalledWith([mockCoverUrl]); // 验证文件删除调用
|
244
|
+
});
|
245
|
+
|
246
|
+
it('should still return deleted topic when file deletion fails', async () => {
|
247
|
+
const mockTopicId = 'topic-123';
|
248
|
+
const mockCoverUrl = 'generations/covers/cover-key.webp';
|
249
|
+
const mockDeletedTopic = {
|
250
|
+
id: mockTopicId,
|
251
|
+
title: 'Deleted Topic',
|
252
|
+
userId: 'test-user',
|
253
|
+
coverUrl: mockCoverUrl,
|
254
|
+
accessedAt: new Date(),
|
255
|
+
createdAt: new Date(),
|
256
|
+
updatedAt: new Date(),
|
257
|
+
};
|
258
|
+
|
259
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
260
|
+
deletedTopic: mockDeletedTopic,
|
261
|
+
filesToDelete: [mockCoverUrl],
|
262
|
+
});
|
263
|
+
|
264
|
+
// Mock file deletion to fail
|
265
|
+
const mockDeleteFiles = vi.fn().mockRejectedValue(new Error('S3 deletion failed'));
|
266
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
267
|
+
|
268
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
269
|
+
() =>
|
270
|
+
({
|
271
|
+
delete: mockDelete,
|
272
|
+
}) as any,
|
273
|
+
);
|
274
|
+
|
275
|
+
vi.mocked(FileService).mockImplementation(
|
276
|
+
() =>
|
277
|
+
({
|
278
|
+
deleteFiles: mockDeleteFiles,
|
279
|
+
}) as any,
|
280
|
+
);
|
281
|
+
|
282
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
283
|
+
const result = await caller.deleteTopic({ id: mockTopicId });
|
284
|
+
|
285
|
+
// Database deletion should succeed even if file deletion fails
|
286
|
+
expect(result).toEqual(mockDeletedTopic);
|
287
|
+
expect(mockDelete).toHaveBeenCalledWith(mockTopicId);
|
288
|
+
expect(mockDeleteFiles).toHaveBeenCalledWith([mockCoverUrl]);
|
289
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to delete files from S3:', expect.any(Error));
|
290
|
+
|
291
|
+
consoleSpy.mockRestore();
|
292
|
+
});
|
293
|
+
|
294
|
+
it('should handle topic with multiple files (cover + thumbnails)', async () => {
|
295
|
+
const mockTopicId = 'topic-123';
|
296
|
+
const mockCoverUrl = 'generations/covers/cover-key.webp';
|
297
|
+
const mockThumbnailUrls = ['thumb1.jpg', 'thumb2.jpg', 'thumb3.jpg'];
|
298
|
+
const mockDeletedTopic = {
|
299
|
+
id: mockTopicId,
|
300
|
+
title: 'Deleted Topic with Multiple Files',
|
301
|
+
userId: 'test-user',
|
302
|
+
coverUrl: mockCoverUrl,
|
303
|
+
accessedAt: new Date(),
|
304
|
+
createdAt: new Date(),
|
305
|
+
updatedAt: new Date(),
|
306
|
+
};
|
307
|
+
|
308
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
309
|
+
deletedTopic: mockDeletedTopic,
|
310
|
+
filesToDelete: [mockCoverUrl, ...mockThumbnailUrls], // 包含封面和缩略图
|
311
|
+
});
|
312
|
+
const mockDeleteFiles = vi.fn().mockResolvedValue(true);
|
313
|
+
|
314
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
315
|
+
() =>
|
316
|
+
({
|
317
|
+
delete: mockDelete,
|
318
|
+
}) as any,
|
319
|
+
);
|
320
|
+
|
321
|
+
vi.mocked(FileService).mockImplementation(
|
322
|
+
() =>
|
323
|
+
({
|
324
|
+
deleteFiles: mockDeleteFiles,
|
325
|
+
}) as any,
|
326
|
+
);
|
327
|
+
|
328
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
329
|
+
const result = await caller.deleteTopic({ id: mockTopicId });
|
330
|
+
|
331
|
+
expect(result).toEqual(mockDeletedTopic);
|
332
|
+
expect(mockDelete).toHaveBeenCalledWith(mockTopicId);
|
333
|
+
expect(mockDeleteFiles).toHaveBeenCalledWith([mockCoverUrl, ...mockThumbnailUrls]);
|
334
|
+
});
|
335
|
+
|
336
|
+
it('should handle partial file deletion failure gracefully', async () => {
|
337
|
+
const mockTopicId = 'topic-123';
|
338
|
+
const mockCoverUrl = 'generations/covers/cover-key.webp';
|
339
|
+
const mockThumbnailUrls = ['thumb1.jpg', 'thumb2.jpg'];
|
340
|
+
const mockDeletedTopic = {
|
341
|
+
id: mockTopicId,
|
342
|
+
title: 'Deleted Topic',
|
343
|
+
userId: 'test-user',
|
344
|
+
coverUrl: mockCoverUrl,
|
345
|
+
accessedAt: new Date(),
|
346
|
+
createdAt: new Date(),
|
347
|
+
updatedAt: new Date(),
|
348
|
+
};
|
349
|
+
|
350
|
+
const mockDelete = vi.fn().mockResolvedValue({
|
351
|
+
deletedTopic: mockDeletedTopic,
|
352
|
+
filesToDelete: [mockCoverUrl, ...mockThumbnailUrls],
|
353
|
+
});
|
354
|
+
|
355
|
+
// Mock partial failure - some files deleted, others failed
|
356
|
+
const mockDeleteFiles = vi
|
357
|
+
.fn()
|
358
|
+
.mockRejectedValue(new Error('Some files could not be deleted from S3'));
|
359
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
360
|
+
|
361
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
362
|
+
() =>
|
363
|
+
({
|
364
|
+
delete: mockDelete,
|
365
|
+
}) as any,
|
366
|
+
);
|
367
|
+
|
368
|
+
vi.mocked(FileService).mockImplementation(
|
369
|
+
() =>
|
370
|
+
({
|
371
|
+
deleteFiles: mockDeleteFiles,
|
372
|
+
}) as any,
|
373
|
+
);
|
374
|
+
|
375
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
376
|
+
const result = await caller.deleteTopic({ id: mockTopicId });
|
377
|
+
|
378
|
+
// Even with partial file deletion failure, topic deletion should succeed
|
379
|
+
expect(result).toEqual(mockDeletedTopic);
|
380
|
+
expect(mockDelete).toHaveBeenCalledWith(mockTopicId);
|
381
|
+
expect(mockDeleteFiles).toHaveBeenCalledWith([mockCoverUrl, ...mockThumbnailUrls]);
|
382
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to delete files from S3:', expect.any(Error));
|
383
|
+
|
384
|
+
consoleSpy.mockRestore();
|
385
|
+
});
|
386
|
+
|
387
|
+
it('should return undefined when deleting non-existent topic', async () => {
|
388
|
+
const mockTopicId = 'non-existent-topic';
|
389
|
+
|
390
|
+
// Mock delete method to return undefined for non-existent topic
|
391
|
+
const mockDelete = vi.fn().mockResolvedValue(undefined);
|
392
|
+
const mockDeleteFiles = vi.fn();
|
393
|
+
|
394
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
395
|
+
() =>
|
396
|
+
({
|
397
|
+
delete: mockDelete,
|
398
|
+
}) as any,
|
399
|
+
);
|
400
|
+
|
401
|
+
vi.mocked(FileService).mockImplementation(
|
402
|
+
() =>
|
403
|
+
({
|
404
|
+
deleteFiles: mockDeleteFiles,
|
405
|
+
}) as any,
|
406
|
+
);
|
407
|
+
|
408
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
409
|
+
|
410
|
+
// Expect the function to return undefined for non-existent topic
|
411
|
+
const result = await caller.deleteTopic({ id: mockTopicId });
|
412
|
+
expect(result).toBeUndefined();
|
413
|
+
|
414
|
+
expect(mockDelete).toHaveBeenCalledWith(mockTopicId);
|
415
|
+
expect(mockDeleteFiles).not.toHaveBeenCalled(); // 没有文件要删除
|
416
|
+
});
|
417
|
+
|
418
|
+
it('should handle edge cases for update with partial data', async () => {
|
419
|
+
const mockTopicId = 'topic-123';
|
420
|
+
const mockUpdateValue = {
|
421
|
+
title: 'Only Title Updated',
|
422
|
+
};
|
423
|
+
const mockUpdatedTopic = {
|
424
|
+
id: mockTopicId,
|
425
|
+
title: 'Only Title Updated',
|
426
|
+
userId: 'test-user',
|
427
|
+
coverUrl: null,
|
428
|
+
accessedAt: new Date(),
|
429
|
+
createdAt: new Date(),
|
430
|
+
updatedAt: new Date(),
|
431
|
+
};
|
432
|
+
|
433
|
+
const mockUpdate = vi.fn().mockResolvedValue(mockUpdatedTopic);
|
434
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
435
|
+
() =>
|
436
|
+
({
|
437
|
+
update: mockUpdate,
|
438
|
+
}) as any,
|
439
|
+
);
|
440
|
+
|
441
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
442
|
+
const result = await caller.updateTopic({
|
443
|
+
id: mockTopicId,
|
444
|
+
value: mockUpdateValue,
|
445
|
+
});
|
446
|
+
|
447
|
+
expect(result).toEqual(mockUpdatedTopic);
|
448
|
+
expect(mockUpdate).toHaveBeenCalledWith(mockTopicId, mockUpdateValue);
|
449
|
+
});
|
450
|
+
|
451
|
+
it('should handle null values in update', async () => {
|
452
|
+
const mockTopicId = 'topic-123';
|
453
|
+
const mockUpdateValue = {
|
454
|
+
title: null,
|
455
|
+
coverUrl: null,
|
456
|
+
};
|
457
|
+
const mockUpdatedTopic = {
|
458
|
+
id: mockTopicId,
|
459
|
+
title: null,
|
460
|
+
userId: 'test-user',
|
461
|
+
coverUrl: null,
|
462
|
+
accessedAt: new Date(),
|
463
|
+
createdAt: new Date(),
|
464
|
+
updatedAt: new Date(),
|
465
|
+
};
|
466
|
+
|
467
|
+
const mockUpdate = vi.fn().mockResolvedValue(mockUpdatedTopic);
|
468
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
469
|
+
() =>
|
470
|
+
({
|
471
|
+
update: mockUpdate,
|
472
|
+
}) as any,
|
473
|
+
);
|
474
|
+
|
475
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
476
|
+
const result = await caller.updateTopic({
|
477
|
+
id: mockTopicId,
|
478
|
+
value: mockUpdateValue,
|
479
|
+
});
|
480
|
+
|
481
|
+
expect(result).toEqual(mockUpdatedTopic);
|
482
|
+
expect(mockUpdate).toHaveBeenCalledWith(mockTopicId, mockUpdateValue);
|
483
|
+
});
|
484
|
+
|
485
|
+
it('should return undefined when updating non-existent topic', async () => {
|
486
|
+
const mockTopicId = 'non-existent-topic';
|
487
|
+
const mockUpdateValue = {
|
488
|
+
title: 'New Title',
|
489
|
+
};
|
490
|
+
|
491
|
+
const mockUpdate = vi.fn().mockResolvedValue(undefined);
|
492
|
+
vi.mocked(GenerationTopicModel).mockImplementation(
|
493
|
+
() =>
|
494
|
+
({
|
495
|
+
update: mockUpdate,
|
496
|
+
}) as any,
|
497
|
+
);
|
498
|
+
|
499
|
+
const caller = generationTopicRouter.createCaller(mockCtx);
|
500
|
+
const result = await caller.updateTopic({
|
501
|
+
id: mockTopicId,
|
502
|
+
value: mockUpdateValue,
|
503
|
+
});
|
504
|
+
|
505
|
+
expect(result).toBeUndefined();
|
506
|
+
expect(mockUpdate).toHaveBeenCalledWith(mockTopicId, mockUpdateValue);
|
507
|
+
});
|
508
|
+
});
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import { z } from 'zod';
|
2
|
+
|
3
|
+
import { GenerationTopicModel } from '@/database/models/generationTopic';
|
4
|
+
import { GenerationTopicItem } from '@/database/schemas/generation';
|
5
|
+
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
6
|
+
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
7
|
+
import { FileService } from '@/server/services/file';
|
8
|
+
import { GenerationService } from '@/server/services/generation';
|
9
|
+
|
10
|
+
const generationTopicProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
|
11
|
+
const { ctx } = opts;
|
12
|
+
|
13
|
+
return opts.next({
|
14
|
+
ctx: {
|
15
|
+
fileService: new FileService(ctx.serverDB, ctx.userId),
|
16
|
+
generationService: new GenerationService(ctx.serverDB, ctx.userId),
|
17
|
+
generationTopicModel: new GenerationTopicModel(ctx.serverDB, ctx.userId),
|
18
|
+
},
|
19
|
+
});
|
20
|
+
});
|
21
|
+
|
22
|
+
// Define input schemas
|
23
|
+
const updateTopicSchema = z.object({
|
24
|
+
id: z.string(),
|
25
|
+
value: z.object({
|
26
|
+
coverUrl: z.string().nullable().optional(),
|
27
|
+
title: z.string().nullable().optional(),
|
28
|
+
}),
|
29
|
+
});
|
30
|
+
|
31
|
+
const updateTopicCoverSchema = z.object({
|
32
|
+
coverUrl: z.string(),
|
33
|
+
id: z.string(),
|
34
|
+
});
|
35
|
+
|
36
|
+
export const generationTopicRouter = router({
|
37
|
+
createTopic: generationTopicProcedure.input(z.void()).mutation(async ({ ctx }) => {
|
38
|
+
const data = await ctx.generationTopicModel.create('');
|
39
|
+
return data.id;
|
40
|
+
}),
|
41
|
+
deleteTopic: generationTopicProcedure
|
42
|
+
.input(z.object({ id: z.string() }))
|
43
|
+
.mutation(async ({ ctx, input }) => {
|
44
|
+
// 1. Delete database records and get file URLs to clean
|
45
|
+
const result = await ctx.generationTopicModel.delete(input.id);
|
46
|
+
|
47
|
+
// If topic not found, throw an error instead of returning undefined
|
48
|
+
if (!result) {
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
|
52
|
+
const { deletedTopic, filesToDelete } = result;
|
53
|
+
|
54
|
+
// 2. Clean up all files from S3 (cover image and thumbnails)
|
55
|
+
// Note: Even if file deletion fails, we consider the topic deletion successful
|
56
|
+
// since the database record has been removed and users won't see the topic anymore
|
57
|
+
if (filesToDelete.length > 0) {
|
58
|
+
try {
|
59
|
+
await ctx.fileService.deleteFiles(filesToDelete);
|
60
|
+
} catch (error) {
|
61
|
+
// Log the error but don't throw - file cleanup failure shouldn't affect
|
62
|
+
// the user experience since the database operation succeeded
|
63
|
+
console.error('Failed to delete files from S3:', error);
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
return deletedTopic;
|
68
|
+
}),
|
69
|
+
getAllGenerationTopics: generationTopicProcedure.query(async ({ ctx }) => {
|
70
|
+
return ctx.generationTopicModel.queryAll();
|
71
|
+
}),
|
72
|
+
updateTopic: generationTopicProcedure
|
73
|
+
.input(updateTopicSchema)
|
74
|
+
.mutation(async ({ ctx, input }) => {
|
75
|
+
return ctx.generationTopicModel.update(input.id, input.value as Partial<GenerationTopicItem>);
|
76
|
+
}),
|
77
|
+
updateTopicCover: generationTopicProcedure
|
78
|
+
.input(updateTopicCoverSchema)
|
79
|
+
.mutation(async ({ ctx, input }) => {
|
80
|
+
// Process the cover image and get key
|
81
|
+
const newCoverKey = await ctx.generationService.createCoverFromUrl(input.coverUrl);
|
82
|
+
|
83
|
+
// Update the topic with the new cover key
|
84
|
+
return ctx.generationTopicModel.update(input.id, { coverUrl: newCoverKey });
|
85
|
+
}),
|
86
|
+
});
|
87
|
+
|
88
|
+
export type GenerationTopicRouter = typeof generationTopicRouter;
|
89
|
+
|
90
|
+
// Export input types for client/server service consistency
|
91
|
+
export type UpdateTopicInput = z.infer<typeof updateTopicSchema>;
|
92
|
+
export type UpdateTopicValue = UpdateTopicInput['value'];
|
93
|
+
export type UpdateTopicCoverInput = z.infer<typeof updateTopicCoverSchema>;
|