@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
@@ -9,6 +9,7 @@ import { buildDir, nextStandaloneDir } from '@/const/dir';
|
|
9
9
|
import { isDev } from '@/const/env';
|
10
10
|
import { IControlModule } from '@/controllers';
|
11
11
|
import { IServiceModule } from '@/services';
|
12
|
+
import FileService from '@/services/fileSrv';
|
12
13
|
import { IpcClientEventSender } from '@/types/ipcClientEvent';
|
13
14
|
import { createLogger } from '@/utils/logger';
|
14
15
|
import { CustomRequestHandler, createHandler } from '@/utils/next-electron-rsc';
|
@@ -18,6 +19,7 @@ import { I18nManager } from './I18nManager';
|
|
18
19
|
import { IoCContainer } from './IoCContainer';
|
19
20
|
import MenuManager from './MenuManager';
|
20
21
|
import { ShortcutManager } from './ShortcutManager';
|
22
|
+
import { StaticFileServerManager } from './StaticFileServerManager';
|
21
23
|
import { StoreManager } from './StoreManager';
|
22
24
|
import TrayManager from './TrayManager';
|
23
25
|
import { UpdaterManager } from './UpdaterManager';
|
@@ -41,6 +43,7 @@ export class App {
|
|
41
43
|
updaterManager: UpdaterManager;
|
42
44
|
shortcutManager: ShortcutManager;
|
43
45
|
trayManager: TrayManager;
|
46
|
+
staticFileServerManager: StaticFileServerManager;
|
44
47
|
chromeFlags: string[] = ['OverlayScrollbar', 'FluentOverlayScrollbar', 'FluentScrollbar'];
|
45
48
|
|
46
49
|
/**
|
@@ -97,6 +100,7 @@ export class App {
|
|
97
100
|
this.updaterManager = new UpdaterManager(this);
|
98
101
|
this.shortcutManager = new ShortcutManager(this);
|
99
102
|
this.trayManager = new TrayManager(this);
|
103
|
+
this.staticFileServerManager = new StaticFileServerManager(this);
|
100
104
|
|
101
105
|
// register the schema to interceptor url
|
102
106
|
// it should register before app ready
|
@@ -130,6 +134,9 @@ export class App {
|
|
130
134
|
await this.i18n.init();
|
131
135
|
this.menuManager.initialize();
|
132
136
|
|
137
|
+
// Initialize static file manager
|
138
|
+
await this.staticFileServerManager.initialize();
|
139
|
+
|
133
140
|
// Initialize global shortcuts: globalShortcut must be called after app.whenReady()
|
134
141
|
this.shortcutManager.initialize();
|
135
142
|
|
@@ -399,6 +406,7 @@ export class App {
|
|
399
406
|
}
|
400
407
|
|
401
408
|
// 执行清理操作
|
409
|
+
this.staticFileServerManager.destroy();
|
402
410
|
this.unregisterAllRequestHandlers();
|
403
411
|
};
|
404
412
|
}
|
@@ -128,9 +128,12 @@ export default class BrowserManager {
|
|
128
128
|
*/
|
129
129
|
initializeBrowsers() {
|
130
130
|
logger.info('Initializing all browsers');
|
131
|
-
Object.values(appBrowsers).forEach((browser) => {
|
131
|
+
Object.values(appBrowsers).forEach((browser: BrowserWindowOpts) => {
|
132
132
|
logger.debug(`Initializing browser: ${browser.identifier}`);
|
133
|
-
|
133
|
+
|
134
|
+
if (browser.keepAlive) {
|
135
|
+
this.retrieveOrInitialize(browser);
|
136
|
+
}
|
134
137
|
});
|
135
138
|
}
|
136
139
|
|
@@ -0,0 +1,221 @@
|
|
1
|
+
import { getPort } from 'get-port-please';
|
2
|
+
import { createServer } from 'node:http';
|
3
|
+
|
4
|
+
import { LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
|
5
|
+
import FileService from '@/services/fileSrv';
|
6
|
+
import { createLogger } from '@/utils/logger';
|
7
|
+
|
8
|
+
import type { App } from './App';
|
9
|
+
|
10
|
+
const logger = createLogger('core:StaticFileServerManager');
|
11
|
+
|
12
|
+
export class StaticFileServerManager {
|
13
|
+
private app: App;
|
14
|
+
private fileService: FileService;
|
15
|
+
private httpServer: any = null;
|
16
|
+
private serverPort: number = 0;
|
17
|
+
private isInitialized = false;
|
18
|
+
|
19
|
+
constructor(app: App) {
|
20
|
+
this.app = app;
|
21
|
+
this.fileService = app.getService(FileService);
|
22
|
+
logger.debug('StaticFileServerManager initialized');
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 初始化静态文件管理器
|
27
|
+
*/
|
28
|
+
async initialize(): Promise<void> {
|
29
|
+
if (this.isInitialized) {
|
30
|
+
logger.warn('StaticFileServerManager already initialized');
|
31
|
+
return;
|
32
|
+
}
|
33
|
+
|
34
|
+
logger.info('Initializing StaticFileServerManager');
|
35
|
+
|
36
|
+
try {
|
37
|
+
// 启动 HTTP 文件服务器
|
38
|
+
await this.startHttpServer();
|
39
|
+
|
40
|
+
this.isInitialized = true;
|
41
|
+
logger.info(
|
42
|
+
`StaticFileServerManager initialization completed, server running on port ${this.serverPort}`,
|
43
|
+
);
|
44
|
+
} catch (error) {
|
45
|
+
logger.error('Failed to initialize StaticFileServerManager:', error);
|
46
|
+
throw error;
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
/**
|
51
|
+
* 启动 HTTP 文件服务器
|
52
|
+
*/
|
53
|
+
private async startHttpServer(): Promise<void> {
|
54
|
+
try {
|
55
|
+
// 使用 get-port-please 获取可用端口
|
56
|
+
this.serverPort = await getPort({
|
57
|
+
port: 33250, // 首选端口
|
58
|
+
ports: [33251, 33252, 33253, 33254, 33255], // 备用端口
|
59
|
+
host: '127.0.0.1',
|
60
|
+
});
|
61
|
+
|
62
|
+
logger.debug(`Found available port: ${this.serverPort}`);
|
63
|
+
|
64
|
+
return new Promise((resolve, reject) => {
|
65
|
+
const server = createServer(async (req, res) => {
|
66
|
+
// 设置请求超时
|
67
|
+
req.setTimeout(30000, () => {
|
68
|
+
logger.warn('Request timeout, closing connection');
|
69
|
+
if (!res.destroyed && !res.headersSent) {
|
70
|
+
res.writeHead(408, { 'Content-Type': 'text/plain' });
|
71
|
+
res.end('Request Timeout');
|
72
|
+
}
|
73
|
+
});
|
74
|
+
|
75
|
+
// 监听客户端断开连接
|
76
|
+
req.on('close', () => {
|
77
|
+
logger.debug('Client disconnected during request processing');
|
78
|
+
});
|
79
|
+
|
80
|
+
try {
|
81
|
+
await this.handleHttpRequest(req, res);
|
82
|
+
} catch (error) {
|
83
|
+
logger.error('Unhandled error in HTTP request handler:', error);
|
84
|
+
|
85
|
+
// 尝试发送错误响应,但确保不会导致进一步错误
|
86
|
+
try {
|
87
|
+
if (!res.destroyed && !res.headersSent) {
|
88
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
89
|
+
res.end('Internal Server Error');
|
90
|
+
}
|
91
|
+
} catch (responseError) {
|
92
|
+
logger.error('Failed to send error response:', responseError);
|
93
|
+
}
|
94
|
+
}
|
95
|
+
});
|
96
|
+
|
97
|
+
// 监听指定端口
|
98
|
+
server.listen(this.serverPort, '127.0.0.1', () => {
|
99
|
+
this.httpServer = server;
|
100
|
+
logger.info(`HTTP file server started on port ${this.serverPort}`);
|
101
|
+
resolve();
|
102
|
+
});
|
103
|
+
|
104
|
+
server.on('error', (error) => {
|
105
|
+
logger.error('HTTP server error:', error);
|
106
|
+
reject(error);
|
107
|
+
});
|
108
|
+
});
|
109
|
+
} catch (error) {
|
110
|
+
logger.error('Failed to get available port:', error);
|
111
|
+
throw error;
|
112
|
+
}
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* 处理 HTTP 请求
|
117
|
+
*/
|
118
|
+
private async handleHttpRequest(req: any, res: any): Promise<void> {
|
119
|
+
try {
|
120
|
+
// 检查响应是否已经结束
|
121
|
+
if (res.destroyed || res.headersSent) {
|
122
|
+
logger.warn('Response already ended, skipping request processing');
|
123
|
+
return;
|
124
|
+
}
|
125
|
+
|
126
|
+
const url = new URL(req.url, `http://127.0.0.1:${this.serverPort}`);
|
127
|
+
logger.debug(`Processing HTTP file request: ${req.url}`);
|
128
|
+
|
129
|
+
// 提取文件路径:从 /desktop-file/path/to/file.png 中提取相对路径
|
130
|
+
let filePath = decodeURIComponent(url.pathname.slice(1)); // 移除开头的 /
|
131
|
+
|
132
|
+
// 如果路径以 desktop-file/ 开头,则移除该前缀
|
133
|
+
const prefixWithoutSlash = LOCAL_STORAGE_URL_PREFIX.slice(1) + '/'; // 移除开头的 / 并添加结尾的 /
|
134
|
+
if (filePath.startsWith(prefixWithoutSlash)) {
|
135
|
+
filePath = filePath.slice(prefixWithoutSlash.length);
|
136
|
+
}
|
137
|
+
|
138
|
+
if (!filePath) {
|
139
|
+
logger.warn(`Empty file path in HTTP request: ${req.url}`);
|
140
|
+
if (!res.headersSent) {
|
141
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
142
|
+
res.end('Bad Request: Empty file path');
|
143
|
+
}
|
144
|
+
return;
|
145
|
+
}
|
146
|
+
|
147
|
+
// 使用 FileService 获取文件
|
148
|
+
const fileResult = await this.fileService.getFile(`desktop://${filePath}`);
|
149
|
+
|
150
|
+
// 再次检查响应状态
|
151
|
+
if (res.destroyed || res.headersSent) {
|
152
|
+
logger.warn('Response ended during file processing');
|
153
|
+
return;
|
154
|
+
}
|
155
|
+
|
156
|
+
// 设置响应头
|
157
|
+
res.writeHead(200, {
|
158
|
+
'Content-Type': fileResult.mimeType,
|
159
|
+
'Cache-Control': 'public, max-age=31536000', // 缓存一年
|
160
|
+
'Access-Control-Allow-Origin': 'http://localhost:*', // 允许 localhost 的任意端口
|
161
|
+
'Content-Length': Buffer.byteLength(fileResult.content),
|
162
|
+
});
|
163
|
+
|
164
|
+
// 发送文件内容
|
165
|
+
res.end(Buffer.from(fileResult.content));
|
166
|
+
|
167
|
+
logger.debug(`HTTP file served successfully: desktop://${filePath}`);
|
168
|
+
} catch (error) {
|
169
|
+
logger.error(`Error serving HTTP file: ${error}`);
|
170
|
+
|
171
|
+
// 检查响应是否仍然可写
|
172
|
+
if (!res.destroyed && !res.headersSent) {
|
173
|
+
try {
|
174
|
+
// 判断是否是文件未找到错误
|
175
|
+
if (error.name === 'FileNotFoundError') {
|
176
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
177
|
+
res.end('File Not Found');
|
178
|
+
} else {
|
179
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
180
|
+
res.end('Internal Server Error');
|
181
|
+
}
|
182
|
+
} catch (writeError) {
|
183
|
+
logger.error('Failed to write error response:', writeError);
|
184
|
+
}
|
185
|
+
} else {
|
186
|
+
logger.warn('Cannot write error response: connection already closed');
|
187
|
+
}
|
188
|
+
}
|
189
|
+
}
|
190
|
+
|
191
|
+
/**
|
192
|
+
* 获取文件服务器域名
|
193
|
+
*/
|
194
|
+
getFileServerDomain(): string {
|
195
|
+
if (!this.isInitialized || !this.serverPort) {
|
196
|
+
throw new Error('StaticFileServerManager not initialized or server not started');
|
197
|
+
}
|
198
|
+
|
199
|
+
const serverDomain = `http://127.0.0.1:${this.serverPort}`;
|
200
|
+
return serverDomain;
|
201
|
+
}
|
202
|
+
|
203
|
+
/**
|
204
|
+
* 销毁静态文件管理器
|
205
|
+
*/
|
206
|
+
destroy() {
|
207
|
+
logger.info('Destroying StaticFileServerManager');
|
208
|
+
|
209
|
+
if (this.httpServer) {
|
210
|
+
logger.debug('Closing HTTP file server');
|
211
|
+
this.httpServer.close(() => {
|
212
|
+
logger.debug('HTTP file server closed');
|
213
|
+
});
|
214
|
+
this.httpServer = null;
|
215
|
+
this.serverPort = 0;
|
216
|
+
}
|
217
|
+
|
218
|
+
this.isInitialized = false;
|
219
|
+
logger.info('StaticFileServerManager destroyed');
|
220
|
+
}
|
221
|
+
}
|
@@ -1,15 +1,28 @@
|
|
1
1
|
import { DeleteFilesResponse } from '@lobechat/electron-server-ipc';
|
2
2
|
import * as fs from 'node:fs';
|
3
3
|
import { writeFile } from 'node:fs/promises';
|
4
|
-
import { join } from 'node:path';
|
4
|
+
import path, { join } from 'node:path';
|
5
5
|
import { promisify } from 'node:util';
|
6
6
|
|
7
|
-
import { FILE_STORAGE_DIR } from '@/const/dir';
|
7
|
+
import { FILE_STORAGE_DIR, LOCAL_STORAGE_URL_PREFIX } from '@/const/dir';
|
8
8
|
import { makeSureDirExist } from '@/utils/file-system';
|
9
9
|
import { createLogger } from '@/utils/logger';
|
10
10
|
|
11
11
|
import { ServiceModule } from './index';
|
12
12
|
|
13
|
+
/**
|
14
|
+
* 文件未找到错误类
|
15
|
+
*/
|
16
|
+
export class FileNotFoundError extends Error {
|
17
|
+
constructor(
|
18
|
+
message: string,
|
19
|
+
public path: string,
|
20
|
+
) {
|
21
|
+
super(message);
|
22
|
+
this.name = 'FileNotFoundError';
|
23
|
+
}
|
24
|
+
}
|
25
|
+
|
13
26
|
const readFilePromise = promisify(fs.readFile);
|
14
27
|
const unlinkPromise = promisify(fs.unlink);
|
15
28
|
|
@@ -17,7 +30,7 @@ const unlinkPromise = promisify(fs.unlink);
|
|
17
30
|
const logger = createLogger('services:FileService');
|
18
31
|
|
19
32
|
interface UploadFileParams {
|
20
|
-
content: ArrayBuffer;
|
33
|
+
content: ArrayBuffer | string; // ArrayBuffer from browser or Base64 string from server
|
21
34
|
filename: string;
|
22
35
|
hash: string;
|
23
36
|
path: string;
|
@@ -32,17 +45,16 @@ interface FileMetadata {
|
|
32
45
|
}
|
33
46
|
|
34
47
|
export default class FileService extends ServiceModule {
|
48
|
+
/**
|
49
|
+
* 获取旧版上传目录路径
|
50
|
+
* @deprecated 仅用于向后兼容旧版文件访问,新文件应存储在 FILE_STORAGE_DIR 的自定义路径下
|
51
|
+
*/
|
35
52
|
get UPLOADS_DIR() {
|
36
53
|
return join(this.app.appStoragePath, FILE_STORAGE_DIR, 'uploads');
|
37
54
|
}
|
38
55
|
|
39
56
|
constructor(app) {
|
40
57
|
super(app);
|
41
|
-
|
42
|
-
// Initialize file storage directory
|
43
|
-
logger.info('Initializing file storage directory');
|
44
|
-
makeSureDirExist(this.UPLOADS_DIR);
|
45
|
-
logger.debug(`Upload directory created: ${this.UPLOADS_DIR}`);
|
46
58
|
}
|
47
59
|
|
48
60
|
/**
|
@@ -52,31 +64,44 @@ export default class FileService extends ServiceModule {
|
|
52
64
|
content,
|
53
65
|
filename,
|
54
66
|
hash,
|
67
|
+
path: filePath,
|
55
68
|
type,
|
56
69
|
}: UploadFileParams): Promise<{ metadata: FileMetadata; success: boolean }> {
|
57
|
-
logger.info(`Starting to upload file: ${filename}, hash: ${hash}`);
|
70
|
+
logger.info(`Starting to upload file: ${filename}, hash: ${hash}, path: ${filePath}`);
|
58
71
|
try {
|
59
|
-
//
|
60
|
-
const
|
61
|
-
const
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
const
|
69
|
-
logger.debug(`
|
70
|
-
|
71
|
-
|
72
|
-
const
|
73
|
-
logger.debug(`
|
72
|
+
// 获取当前时间戳,避免重复调用 Date.now()
|
73
|
+
const now = Date.now();
|
74
|
+
const date = (now / 1000 / 60 / 60).toFixed(0);
|
75
|
+
|
76
|
+
// 使用传入的 filePath 作为文件的存储路径
|
77
|
+
const fullStoragePath = join(this.app.appStoragePath, FILE_STORAGE_DIR, filePath);
|
78
|
+
logger.debug(`Target file storage path: ${fullStoragePath}`);
|
79
|
+
|
80
|
+
// 确保目标目录存在
|
81
|
+
const targetDir = path.dirname(fullStoragePath);
|
82
|
+
logger.debug(`Ensuring target directory exists: ${targetDir}`);
|
83
|
+
makeSureDirExist(targetDir);
|
84
|
+
|
85
|
+
const savedPath = fullStoragePath;
|
86
|
+
logger.debug(`Final file save path: ${savedPath}`);
|
87
|
+
|
88
|
+
// 根据 content 类型创建 Buffer
|
89
|
+
let buffer: Buffer;
|
90
|
+
if (typeof content === 'string') {
|
91
|
+
// 来自服务端的 Base64 字符串
|
92
|
+
buffer = Buffer.from(content, 'base64');
|
93
|
+
logger.debug(`Creating buffer from Base64 string, size: ${buffer.length} bytes`);
|
94
|
+
} else {
|
95
|
+
// 来自浏览器端的 ArrayBuffer
|
96
|
+
buffer = Buffer.from(content);
|
97
|
+
logger.debug(`Creating buffer from ArrayBuffer, size: ${buffer.length} bytes`);
|
98
|
+
}
|
74
99
|
await writeFile(savedPath, buffer);
|
75
100
|
|
76
101
|
// 写入元数据文件
|
77
102
|
const metaFilePath = `${savedPath}.meta`;
|
78
103
|
const metadata = {
|
79
|
-
createdAt:
|
104
|
+
createdAt: now, // 使用统一的时间戳
|
80
105
|
filename,
|
81
106
|
hash,
|
82
107
|
size: buffer.length,
|
@@ -86,13 +111,18 @@ export default class FileService extends ServiceModule {
|
|
86
111
|
await writeFile(metaFilePath, JSON.stringify(metadata, null, 2));
|
87
112
|
|
88
113
|
// 返回与S3兼容的元数据格式
|
89
|
-
const desktopPath = `desktop://${
|
114
|
+
const desktopPath = `desktop://${filePath}`;
|
90
115
|
logger.info(`File upload successful: ${desktopPath}`);
|
91
116
|
|
117
|
+
// 从路径中提取文件名和目录信息
|
118
|
+
const parsedPath = path.parse(filePath);
|
119
|
+
const dirname = parsedPath.dir || '';
|
120
|
+
const savedFilename = parsedPath.base;
|
121
|
+
|
92
122
|
return {
|
93
123
|
metadata: {
|
94
|
-
date,
|
95
|
-
dirname
|
124
|
+
date, // 保持时间戳格式,用于兼容性和时间追踪
|
125
|
+
dirname,
|
96
126
|
filename: savedFilename,
|
97
127
|
path: desktopPath,
|
98
128
|
},
|
@@ -104,6 +134,24 @@ export default class FileService extends ServiceModule {
|
|
104
134
|
}
|
105
135
|
}
|
106
136
|
|
137
|
+
/**
|
138
|
+
* 判断路径是否为旧版格式(时间戳目录)
|
139
|
+
*
|
140
|
+
* 旧版路径格式: {timestamp}/{hash}.{ext} (例如: 1234567890/abc123.png)
|
141
|
+
* 新版路径格式: 任意自定义路径 (例如: user_uploads/images/photo.png, ai_generations/image.jpg)
|
142
|
+
*
|
143
|
+
* @param path - 相对路径,不包含 desktop:// 前缀
|
144
|
+
* @returns true 如果是旧版格式,false 如果是新版格式
|
145
|
+
*/
|
146
|
+
private isLegacyPath(path: string): boolean {
|
147
|
+
const parts = path.split('/');
|
148
|
+
if (parts.length < 2) return false;
|
149
|
+
|
150
|
+
// 如果第一部分是纯数字(时间戳),则认为是旧版格式
|
151
|
+
// 时间戳格式:精确到小时的 Unix 时间戳,通常是 10 位数字
|
152
|
+
return /^\d+$/.test(parts[0]);
|
153
|
+
}
|
154
|
+
|
107
155
|
/**
|
108
156
|
* 获取文件内容
|
109
157
|
*/
|
@@ -123,13 +171,49 @@ export default class FileService extends ServiceModule {
|
|
123
171
|
|
124
172
|
// 解析路径
|
125
173
|
const relativePath = normalizedPath.replace('desktop://', '');
|
126
|
-
const filePath = join(this.UPLOADS_DIR, relativePath);
|
127
|
-
logger.debug(`Reading file from path: ${filePath}`);
|
128
174
|
|
129
|
-
//
|
175
|
+
// 智能路由:根据路径格式决定从哪个目录读取文件
|
176
|
+
let filePath: string;
|
177
|
+
let isLegacyAttempt = false;
|
178
|
+
|
179
|
+
if (this.isLegacyPath(relativePath)) {
|
180
|
+
// 旧版路径:从 uploads 目录读取(向后兼容)
|
181
|
+
filePath = join(this.UPLOADS_DIR, relativePath);
|
182
|
+
isLegacyAttempt = true;
|
183
|
+
logger.debug(`Legacy path detected, reading from uploads directory: ${filePath}`);
|
184
|
+
} else {
|
185
|
+
// 新版路径:从 FILE_STORAGE_DIR 根目录读取
|
186
|
+
filePath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
|
187
|
+
logger.debug(`New path format, reading from storage root: ${filePath}`);
|
188
|
+
}
|
189
|
+
|
190
|
+
// 读取文件内容,如果第一次尝试失败且是 legacy 路径,则尝试新路径
|
130
191
|
logger.debug(`Starting to read file content`);
|
131
|
-
|
132
|
-
|
192
|
+
let content: Buffer;
|
193
|
+
try {
|
194
|
+
content = await readFilePromise(filePath);
|
195
|
+
logger.debug(`File content read complete, size: ${content.length} bytes`);
|
196
|
+
} catch (firstError) {
|
197
|
+
if (isLegacyAttempt) {
|
198
|
+
// 如果是 legacy 路径读取失败,尝试从新路径读取
|
199
|
+
const fallbackPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
|
200
|
+
logger.debug(
|
201
|
+
`Legacy path read failed, attempting fallback to storage root: ${fallbackPath}`,
|
202
|
+
);
|
203
|
+
try {
|
204
|
+
content = await readFilePromise(fallbackPath);
|
205
|
+
filePath = fallbackPath; // 更新 filePath 用于后续的元数据读取
|
206
|
+
logger.debug(`Fallback read successful, size: ${content.length} bytes`);
|
207
|
+
} catch (fallbackError) {
|
208
|
+
logger.error(
|
209
|
+
`Both legacy and fallback paths failed. Legacy error: ${(firstError as Error).message}, Fallback error: ${(fallbackError as Error).message}`,
|
210
|
+
);
|
211
|
+
throw firstError; // 抛出原始错误
|
212
|
+
}
|
213
|
+
} else {
|
214
|
+
throw firstError;
|
215
|
+
}
|
216
|
+
}
|
133
217
|
|
134
218
|
// 读取元数据获取MIME类型
|
135
219
|
const metaFilePath = `${filePath}.meta`;
|
@@ -142,7 +226,9 @@ export default class FileService extends ServiceModule {
|
|
142
226
|
mimeType = metadata.type || mimeType;
|
143
227
|
logger.debug(`Got MIME type from metadata: ${mimeType}`);
|
144
228
|
} catch (metaError) {
|
145
|
-
logger.warn(
|
229
|
+
logger.warn(
|
230
|
+
`Failed to read metadata file: ${(metaError as Error).message}, using default MIME type`,
|
231
|
+
);
|
146
232
|
// 如果元数据文件不存在,尝试从文件扩展名猜测MIME类型
|
147
233
|
const ext = path.split('.').pop()?.toLowerCase();
|
148
234
|
if (ext) {
|
@@ -184,6 +270,12 @@ export default class FileService extends ServiceModule {
|
|
184
270
|
};
|
185
271
|
} catch (error) {
|
186
272
|
logger.error(`File retrieval failed:`, error);
|
273
|
+
|
274
|
+
// 如果是文件不存在错误,抛出自定义的 FileNotFoundError
|
275
|
+
if (error instanceof Error && error.message.includes('ENOENT')) {
|
276
|
+
throw new FileNotFoundError(`File not found: ${path}`, path);
|
277
|
+
}
|
278
|
+
|
187
279
|
throw new Error(`File retrieval failed: ${(error as Error).message}`);
|
188
280
|
}
|
189
281
|
}
|
@@ -200,15 +292,53 @@ export default class FileService extends ServiceModule {
|
|
200
292
|
throw new Error(`Invalid desktop file path: ${path}`);
|
201
293
|
}
|
202
294
|
|
295
|
+
// 标准化路径格式
|
296
|
+
const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
|
297
|
+
|
203
298
|
// 解析路径
|
204
|
-
const relativePath =
|
205
|
-
const filePath = join(this.UPLOADS_DIR, relativePath);
|
206
|
-
logger.debug(`File deletion path: ${filePath}`);
|
299
|
+
const relativePath = normalizedPath.replace('desktop://', '');
|
207
300
|
|
208
|
-
//
|
301
|
+
// 智能路由:根据路径格式决定从哪个目录删除文件
|
302
|
+
let filePath: string;
|
303
|
+
let isLegacyAttempt = false;
|
304
|
+
|
305
|
+
if (this.isLegacyPath(relativePath)) {
|
306
|
+
// 旧版路径:从 uploads 目录删除(向后兼容)
|
307
|
+
filePath = join(this.UPLOADS_DIR, relativePath);
|
308
|
+
isLegacyAttempt = true;
|
309
|
+
logger.debug(`Legacy path detected, deleting from uploads directory: ${filePath}`);
|
310
|
+
} else {
|
311
|
+
// 新版路径:从 FILE_STORAGE_DIR 根目录删除
|
312
|
+
filePath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
|
313
|
+
logger.debug(`New path format, deleting from storage root: ${filePath}`);
|
314
|
+
}
|
315
|
+
|
316
|
+
// 删除文件及其元数据,如果第一次尝试失败且是 legacy 路径,则尝试新路径
|
209
317
|
logger.debug(`Starting file deletion`);
|
210
|
-
|
211
|
-
|
318
|
+
try {
|
319
|
+
await unlinkPromise(filePath);
|
320
|
+
logger.debug(`File deletion successful`);
|
321
|
+
} catch (firstError) {
|
322
|
+
if (isLegacyAttempt) {
|
323
|
+
// 如果是 legacy 路径删除失败,尝试从新路径删除
|
324
|
+
const fallbackPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
|
325
|
+
logger.debug(
|
326
|
+
`Legacy path deletion failed, attempting fallback to storage root: ${fallbackPath}`,
|
327
|
+
);
|
328
|
+
try {
|
329
|
+
await unlinkPromise(fallbackPath);
|
330
|
+
filePath = fallbackPath; // 更新 filePath 用于后续的元数据删除
|
331
|
+
logger.debug(`Fallback deletion successful`);
|
332
|
+
} catch (fallbackError) {
|
333
|
+
logger.error(
|
334
|
+
`Both legacy and fallback deletion failed. Legacy error: ${(firstError as Error).message}, Fallback error: ${(fallbackError as Error).message}`,
|
335
|
+
);
|
336
|
+
throw firstError; // 抛出原始错误
|
337
|
+
}
|
338
|
+
} else {
|
339
|
+
throw firstError;
|
340
|
+
}
|
341
|
+
}
|
212
342
|
|
213
343
|
// 尝试删除元数据文件,但不强制要求存在
|
214
344
|
try {
|
@@ -270,7 +400,9 @@ export default class FileService extends ServiceModule {
|
|
270
400
|
});
|
271
401
|
|
272
402
|
const success = errors.length === 0;
|
273
|
-
logger.info(
|
403
|
+
logger.info(
|
404
|
+
`Batch deletion operation complete, success: ${success}, error count: ${errors.length}`,
|
405
|
+
);
|
274
406
|
return {
|
275
407
|
success,
|
276
408
|
...(errors.length > 0 && { errors }),
|
@@ -285,10 +417,65 @@ export default class FileService extends ServiceModule {
|
|
285
417
|
throw new Error(`Invalid desktop file path: ${path}`);
|
286
418
|
}
|
287
419
|
|
420
|
+
// 标准化路径格式
|
421
|
+
const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
|
422
|
+
|
288
423
|
// 解析路径
|
289
|
-
const relativePath =
|
290
|
-
|
291
|
-
|
424
|
+
const relativePath = normalizedPath.replace('desktop://', '');
|
425
|
+
|
426
|
+
// 智能路由:根据路径格式决定从哪个目录获取文件路径
|
427
|
+
let fullPath: string;
|
428
|
+
if (this.isLegacyPath(relativePath)) {
|
429
|
+
// 旧版路径:从 uploads 目录获取(向后兼容)
|
430
|
+
fullPath = join(this.UPLOADS_DIR, relativePath);
|
431
|
+
logger.debug(`Legacy path detected, resolved to uploads directory: ${fullPath}`);
|
432
|
+
|
433
|
+
// 检查文件是否存在,如果不存在则尝试新路径
|
434
|
+
try {
|
435
|
+
await fs.promises.access(fullPath, fs.constants.F_OK);
|
436
|
+
logger.debug(`Legacy path file exists: ${fullPath}`);
|
437
|
+
} catch {
|
438
|
+
// 如果 legacy 路径文件不存在,尝试新路径
|
439
|
+
const fallbackPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
|
440
|
+
logger.debug(`Legacy path file not found, trying fallback path: ${fallbackPath}`);
|
441
|
+
try {
|
442
|
+
await fs.promises.access(fallbackPath, fs.constants.F_OK);
|
443
|
+
fullPath = fallbackPath;
|
444
|
+
logger.debug(`Fallback path file exists: ${fullPath}`);
|
445
|
+
} catch {
|
446
|
+
// 两个路径都不存在,返回原始的 legacy 路径(保持原有行为)
|
447
|
+
logger.debug(
|
448
|
+
`Neither legacy nor fallback path exists, returning legacy path: ${fullPath}`,
|
449
|
+
);
|
450
|
+
}
|
451
|
+
}
|
452
|
+
} else {
|
453
|
+
// 新版路径:从 FILE_STORAGE_DIR 根目录获取
|
454
|
+
fullPath = join(this.app.appStoragePath, FILE_STORAGE_DIR, relativePath);
|
455
|
+
logger.debug(`New path format, resolved to storage root: ${fullPath}`);
|
456
|
+
}
|
457
|
+
|
292
458
|
return fullPath;
|
293
459
|
}
|
460
|
+
|
461
|
+
async getFileHTTPURL(path: string): Promise<string> {
|
462
|
+
logger.debug(`Getting file HTTP URL: ${path}`);
|
463
|
+
// 处理desktop://路径
|
464
|
+
if (!path.startsWith('desktop://')) {
|
465
|
+
logger.error(`Invalid desktop file path: ${path}`);
|
466
|
+
throw new Error(`Invalid desktop file path: ${path}`);
|
467
|
+
}
|
468
|
+
|
469
|
+
// 标准化路径格式
|
470
|
+
const normalizedPath = path.replace(/^desktop:\/+/, 'desktop://');
|
471
|
+
|
472
|
+
// 解析路径:从 desktop://path/to/file.png 中提取 path/to/file.png
|
473
|
+
const relativePath = normalizedPath.replace('desktop://', '');
|
474
|
+
|
475
|
+
// 使用 StaticFileServerManager 获取文件服务器域名,然后构建完整 URL
|
476
|
+
const serverDomain = this.app.staticFileServerManager.getFileServerDomain();
|
477
|
+
const httpURL = `${serverDomain}${LOCAL_STORAGE_URL_PREFIX}/${relativePath}`;
|
478
|
+
logger.debug(`Generated HTTP URL: ${httpURL}`);
|
479
|
+
return httpURL;
|
480
|
+
}
|
294
481
|
}
|