@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.
Files changed (458) hide show
  1. package/.cursor/rules/backend-architecture.mdc +93 -17
  2. package/.cursor/rules/cursor-ux.mdc +45 -35
  3. package/.cursor/rules/project-introduce.mdc +72 -6
  4. package/.cursor/rules/rules-attach.mdc +16 -7
  5. package/.eslintrc.js +10 -0
  6. package/CHANGELOG.md +44 -0
  7. package/apps/desktop/README.md +7 -0
  8. package/apps/desktop/electron-builder.js +5 -0
  9. package/apps/desktop/package.json +2 -1
  10. package/apps/desktop/src/main/appBrowsers.ts +1 -1
  11. package/apps/desktop/src/main/const/dir.ts +3 -0
  12. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +13 -8
  13. package/apps/desktop/src/main/core/App.ts +8 -0
  14. package/apps/desktop/src/main/core/BrowserManager.ts +5 -2
  15. package/apps/desktop/src/main/core/StaticFileServerManager.ts +221 -0
  16. package/apps/desktop/src/main/services/fileSrv.ts +231 -44
  17. package/apps/desktop/src/main/utils/next-electron-rsc.ts +36 -5
  18. package/changelog/v1.json +14 -0
  19. package/docs/development/database-schema.dbml +70 -0
  20. package/locales/ar/common.json +2 -0
  21. package/locales/ar/components.json +35 -0
  22. package/locales/ar/error.json +2 -0
  23. package/locales/ar/image.json +100 -0
  24. package/locales/ar/metadata.json +4 -0
  25. package/locales/ar/modelProvider.json +1 -0
  26. package/locales/ar/models.json +15 -0
  27. package/locales/ar/plugin.json +22 -0
  28. package/locales/ar/providers.json +3 -0
  29. package/locales/ar/setting.json +5 -0
  30. package/locales/bg-BG/common.json +2 -0
  31. package/locales/bg-BG/components.json +35 -0
  32. package/locales/bg-BG/error.json +2 -0
  33. package/locales/bg-BG/image.json +100 -0
  34. package/locales/bg-BG/metadata.json +4 -0
  35. package/locales/bg-BG/modelProvider.json +1 -0
  36. package/locales/bg-BG/models.json +15 -0
  37. package/locales/bg-BG/plugin.json +22 -0
  38. package/locales/bg-BG/providers.json +3 -0
  39. package/locales/bg-BG/setting.json +5 -0
  40. package/locales/de-DE/common.json +2 -0
  41. package/locales/de-DE/components.json +35 -0
  42. package/locales/de-DE/error.json +2 -0
  43. package/locales/de-DE/image.json +100 -0
  44. package/locales/de-DE/metadata.json +4 -0
  45. package/locales/de-DE/modelProvider.json +1 -0
  46. package/locales/de-DE/models.json +15 -0
  47. package/locales/de-DE/plugin.json +22 -0
  48. package/locales/de-DE/providers.json +3 -0
  49. package/locales/de-DE/setting.json +5 -0
  50. package/locales/en-US/common.json +2 -0
  51. package/locales/en-US/components.json +35 -0
  52. package/locales/en-US/error.json +2 -0
  53. package/locales/en-US/image.json +100 -0
  54. package/locales/en-US/metadata.json +4 -0
  55. package/locales/en-US/modelProvider.json +1 -0
  56. package/locales/en-US/models.json +15 -0
  57. package/locales/en-US/plugin.json +22 -0
  58. package/locales/en-US/providers.json +3 -0
  59. package/locales/en-US/setting.json +5 -0
  60. package/locales/es-ES/common.json +2 -0
  61. package/locales/es-ES/components.json +35 -0
  62. package/locales/es-ES/error.json +2 -0
  63. package/locales/es-ES/image.json +100 -0
  64. package/locales/es-ES/metadata.json +4 -0
  65. package/locales/es-ES/modelProvider.json +1 -0
  66. package/locales/es-ES/models.json +15 -0
  67. package/locales/es-ES/plugin.json +22 -0
  68. package/locales/es-ES/providers.json +3 -0
  69. package/locales/es-ES/setting.json +5 -0
  70. package/locales/fa-IR/common.json +2 -0
  71. package/locales/fa-IR/components.json +35 -0
  72. package/locales/fa-IR/error.json +2 -0
  73. package/locales/fa-IR/image.json +100 -0
  74. package/locales/fa-IR/metadata.json +4 -0
  75. package/locales/fa-IR/modelProvider.json +1 -0
  76. package/locales/fa-IR/models.json +15 -0
  77. package/locales/fa-IR/plugin.json +22 -0
  78. package/locales/fa-IR/providers.json +3 -0
  79. package/locales/fa-IR/setting.json +5 -0
  80. package/locales/fr-FR/common.json +2 -0
  81. package/locales/fr-FR/components.json +35 -0
  82. package/locales/fr-FR/error.json +2 -0
  83. package/locales/fr-FR/image.json +100 -0
  84. package/locales/fr-FR/metadata.json +4 -0
  85. package/locales/fr-FR/modelProvider.json +1 -0
  86. package/locales/fr-FR/models.json +15 -0
  87. package/locales/fr-FR/plugin.json +22 -0
  88. package/locales/fr-FR/providers.json +3 -0
  89. package/locales/fr-FR/setting.json +5 -0
  90. package/locales/it-IT/common.json +2 -0
  91. package/locales/it-IT/components.json +35 -0
  92. package/locales/it-IT/error.json +2 -0
  93. package/locales/it-IT/image.json +100 -0
  94. package/locales/it-IT/metadata.json +4 -0
  95. package/locales/it-IT/modelProvider.json +1 -0
  96. package/locales/it-IT/models.json +15 -0
  97. package/locales/it-IT/plugin.json +22 -0
  98. package/locales/it-IT/providers.json +3 -0
  99. package/locales/it-IT/setting.json +5 -0
  100. package/locales/ja-JP/common.json +2 -0
  101. package/locales/ja-JP/components.json +35 -0
  102. package/locales/ja-JP/error.json +2 -0
  103. package/locales/ja-JP/image.json +100 -0
  104. package/locales/ja-JP/metadata.json +4 -0
  105. package/locales/ja-JP/modelProvider.json +1 -0
  106. package/locales/ja-JP/models.json +15 -0
  107. package/locales/ja-JP/plugin.json +22 -0
  108. package/locales/ja-JP/providers.json +3 -0
  109. package/locales/ja-JP/setting.json +5 -0
  110. package/locales/ko-KR/common.json +2 -0
  111. package/locales/ko-KR/components.json +35 -0
  112. package/locales/ko-KR/error.json +2 -0
  113. package/locales/ko-KR/image.json +100 -0
  114. package/locales/ko-KR/metadata.json +4 -0
  115. package/locales/ko-KR/modelProvider.json +1 -0
  116. package/locales/ko-KR/models.json +15 -0
  117. package/locales/ko-KR/plugin.json +22 -0
  118. package/locales/ko-KR/providers.json +3 -0
  119. package/locales/ko-KR/setting.json +5 -0
  120. package/locales/nl-NL/common.json +2 -0
  121. package/locales/nl-NL/components.json +35 -0
  122. package/locales/nl-NL/error.json +2 -0
  123. package/locales/nl-NL/image.json +100 -0
  124. package/locales/nl-NL/metadata.json +4 -0
  125. package/locales/nl-NL/modelProvider.json +1 -0
  126. package/locales/nl-NL/models.json +15 -0
  127. package/locales/nl-NL/plugin.json +22 -0
  128. package/locales/nl-NL/providers.json +3 -0
  129. package/locales/nl-NL/setting.json +5 -0
  130. package/locales/pl-PL/common.json +2 -0
  131. package/locales/pl-PL/components.json +35 -0
  132. package/locales/pl-PL/error.json +2 -0
  133. package/locales/pl-PL/image.json +100 -0
  134. package/locales/pl-PL/metadata.json +4 -0
  135. package/locales/pl-PL/modelProvider.json +1 -0
  136. package/locales/pl-PL/models.json +15 -0
  137. package/locales/pl-PL/plugin.json +22 -0
  138. package/locales/pl-PL/providers.json +3 -0
  139. package/locales/pl-PL/setting.json +5 -0
  140. package/locales/pt-BR/common.json +2 -0
  141. package/locales/pt-BR/components.json +35 -0
  142. package/locales/pt-BR/error.json +2 -0
  143. package/locales/pt-BR/image.json +100 -0
  144. package/locales/pt-BR/metadata.json +4 -0
  145. package/locales/pt-BR/modelProvider.json +1 -0
  146. package/locales/pt-BR/models.json +15 -0
  147. package/locales/pt-BR/plugin.json +22 -0
  148. package/locales/pt-BR/providers.json +3 -0
  149. package/locales/pt-BR/setting.json +5 -0
  150. package/locales/ru-RU/common.json +2 -0
  151. package/locales/ru-RU/components.json +35 -0
  152. package/locales/ru-RU/error.json +2 -0
  153. package/locales/ru-RU/image.json +100 -0
  154. package/locales/ru-RU/metadata.json +4 -0
  155. package/locales/ru-RU/modelProvider.json +1 -0
  156. package/locales/ru-RU/models.json +15 -0
  157. package/locales/ru-RU/plugin.json +22 -0
  158. package/locales/ru-RU/providers.json +3 -0
  159. package/locales/ru-RU/setting.json +5 -0
  160. package/locales/tr-TR/common.json +2 -0
  161. package/locales/tr-TR/components.json +35 -0
  162. package/locales/tr-TR/error.json +2 -0
  163. package/locales/tr-TR/image.json +100 -0
  164. package/locales/tr-TR/metadata.json +4 -0
  165. package/locales/tr-TR/modelProvider.json +1 -0
  166. package/locales/tr-TR/models.json +15 -0
  167. package/locales/tr-TR/plugin.json +22 -0
  168. package/locales/tr-TR/providers.json +3 -0
  169. package/locales/tr-TR/setting.json +5 -0
  170. package/locales/vi-VN/common.json +2 -0
  171. package/locales/vi-VN/components.json +35 -0
  172. package/locales/vi-VN/error.json +2 -0
  173. package/locales/vi-VN/image.json +100 -0
  174. package/locales/vi-VN/metadata.json +4 -0
  175. package/locales/vi-VN/modelProvider.json +1 -0
  176. package/locales/vi-VN/models.json +15 -0
  177. package/locales/vi-VN/plugin.json +22 -0
  178. package/locales/vi-VN/providers.json +3 -0
  179. package/locales/vi-VN/setting.json +5 -0
  180. package/locales/zh-CN/common.json +2 -0
  181. package/locales/zh-CN/components.json +35 -0
  182. package/locales/zh-CN/error.json +2 -0
  183. package/locales/zh-CN/image.json +100 -0
  184. package/locales/zh-CN/metadata.json +4 -0
  185. package/locales/zh-CN/modelProvider.json +1 -0
  186. package/locales/zh-CN/models.json +15 -0
  187. package/locales/zh-CN/plugin.json +22 -0
  188. package/locales/zh-CN/providers.json +3 -0
  189. package/locales/zh-CN/setting.json +5 -0
  190. package/locales/zh-TW/common.json +2 -0
  191. package/locales/zh-TW/components.json +35 -0
  192. package/locales/zh-TW/error.json +2 -0
  193. package/locales/zh-TW/image.json +100 -0
  194. package/locales/zh-TW/metadata.json +4 -0
  195. package/locales/zh-TW/modelProvider.json +1 -0
  196. package/locales/zh-TW/models.json +15 -0
  197. package/locales/zh-TW/plugin.json +22 -0
  198. package/locales/zh-TW/providers.json +3 -0
  199. package/locales/zh-TW/setting.json +5 -0
  200. package/package.json +11 -4
  201. package/packages/electron-server-ipc/src/events/file.ts +3 -1
  202. package/packages/electron-server-ipc/src/types/file.ts +15 -0
  203. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -1
  204. package/src/app/[variants]/(main)/image/@menu/components/AspectRatioSelect/index.tsx +73 -0
  205. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +39 -0
  206. package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +89 -0
  207. package/src/app/[variants]/(main)/image/@menu/default.tsx +11 -0
  208. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +24 -0
  209. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +107 -0
  210. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageNum.tsx +290 -0
  211. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +504 -0
  212. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +18 -0
  213. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +19 -0
  214. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +155 -0
  215. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +415 -0
  216. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +732 -0
  217. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +24 -0
  218. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSelect.tsx +17 -0
  219. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +15 -0
  220. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +11 -0
  221. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/constants.ts +1 -0
  222. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +93 -0
  223. package/src/app/[variants]/(main)/image/@topic/default.tsx +17 -0
  224. package/src/app/[variants]/(main)/image/@topic/features/Topics/NewTopicButton.tsx +64 -0
  225. package/src/app/[variants]/(main)/image/@topic/features/Topics/SkeletonList.tsx +34 -0
  226. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +136 -0
  227. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +91 -0
  228. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +57 -0
  229. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicUrlSync.tsx +37 -0
  230. package/src/app/[variants]/(main)/image/@topic/features/Topics/index.tsx +19 -0
  231. package/src/app/[variants]/(main)/image/NotSupportClient.tsx +153 -0
  232. package/src/app/[variants]/(main)/image/_layout/Desktop/Container.tsx +35 -0
  233. package/src/app/[variants]/(main)/image/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  234. package/src/app/[variants]/(main)/image/_layout/Desktop/index.tsx +30 -0
  235. package/src/app/[variants]/(main)/image/_layout/Mobile/index.tsx +14 -0
  236. package/src/app/[variants]/(main)/image/_layout/type.ts +7 -0
  237. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +196 -0
  238. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx +60 -0
  239. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx +90 -0
  240. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +65 -0
  241. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +44 -0
  242. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +49 -0
  243. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +154 -0
  244. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/styles.ts +51 -0
  245. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +39 -0
  246. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +11 -0
  247. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +97 -0
  248. package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +48 -0
  249. package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +37 -0
  250. package/src/app/[variants]/(main)/image/features/ImageWorkspace/SkeletonList.tsx +50 -0
  251. package/src/app/[variants]/(main)/image/features/ImageWorkspace/index.tsx +23 -0
  252. package/src/app/[variants]/(main)/image/features/PromptInput/Title.tsx +38 -0
  253. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +114 -0
  254. package/src/app/[variants]/(main)/image/layout.tsx +19 -0
  255. package/src/app/[variants]/(main)/image/loading.tsx +3 -0
  256. package/src/app/[variants]/(main)/image/page.tsx +47 -0
  257. package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
  258. package/src/chains/summaryGenerationTitle.ts +25 -0
  259. package/src/components/ImageItem/index.tsx +9 -6
  260. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/Bedrock.tsx +3 -4
  261. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/ProviderApiKeyForm.tsx +5 -4
  262. package/src/components/InvalidAPIKey/APIKeyForm/index.tsx +108 -0
  263. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/useApiKey.ts +2 -1
  264. package/src/components/InvalidAPIKey/index.tsx +30 -0
  265. package/src/components/KeyValueEditor/index.tsx +203 -0
  266. package/src/components/KeyValueEditor/utils.ts +42 -0
  267. package/src/config/aiModels/fal.ts +52 -0
  268. package/src/config/aiModels/index.ts +3 -0
  269. package/src/config/aiModels/openai.ts +20 -6
  270. package/src/config/llm.ts +6 -0
  271. package/src/config/modelProviders/fal.ts +21 -0
  272. package/src/config/modelProviders/index.ts +3 -0
  273. package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +8 -0
  274. package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +11 -0
  275. package/src/config/paramsSchemas/fal/flux-schnell.ts +9 -0
  276. package/src/config/paramsSchemas/fal/imagen4.ts +10 -0
  277. package/src/config/paramsSchemas/openai/gpt-image-1.ts +10 -0
  278. package/src/const/hotkeys.ts +2 -2
  279. package/src/const/image.ts +6 -0
  280. package/src/const/settings/systemAgent.ts +1 -0
  281. package/src/database/client/migrations.json +27 -0
  282. package/src/database/migrations/0026_add_autovacuum_tuning.sql +2 -0
  283. package/src/database/migrations/0027_ai_image.sql +47 -0
  284. package/src/database/migrations/meta/0027_snapshot.json +6003 -0
  285. package/src/database/migrations/meta/_journal.json +7 -0
  286. package/src/database/models/__tests__/asyncTask.test.ts +7 -5
  287. package/src/database/models/__tests__/file.test.ts +287 -0
  288. package/src/database/models/__tests__/generation.test.ts +786 -0
  289. package/src/database/models/__tests__/generationBatch.test.ts +614 -0
  290. package/src/database/models/__tests__/generationTopic.test.ts +411 -0
  291. package/src/database/models/aiModel.ts +2 -0
  292. package/src/database/models/asyncTask.ts +1 -1
  293. package/src/database/models/file.ts +28 -20
  294. package/src/database/models/generation.ts +197 -0
  295. package/src/database/models/generationBatch.ts +212 -0
  296. package/src/database/models/generationTopic.ts +131 -0
  297. package/src/database/repositories/aiInfra/index.test.ts +157 -1
  298. package/src/database/repositories/aiInfra/index.ts +37 -19
  299. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  300. package/src/database/schemas/file.ts +8 -0
  301. package/src/database/schemas/generation.ts +127 -0
  302. package/src/database/schemas/index.ts +1 -0
  303. package/src/database/schemas/relations.ts +45 -1
  304. package/src/database/type.ts +2 -0
  305. package/src/database/utils/idGenerator.ts +3 -0
  306. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +39 -0
  307. package/src/features/Conversation/Error/InvalidAccessCode.tsx +2 -2
  308. package/src/features/Conversation/Error/index.tsx +3 -3
  309. package/src/features/ImageSidePanel/index.tsx +83 -0
  310. package/src/features/ImageTopicPanel/index.tsx +79 -0
  311. package/src/features/PluginDevModal/MCPManifestForm/CollapsibleSection.tsx +62 -0
  312. package/src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx +158 -0
  313. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +99 -155
  314. package/src/features/PluginStore/McpList/Detail/Settings/index.tsx +5 -2
  315. package/src/hooks/useDownloadImage.ts +31 -0
  316. package/src/hooks/useFetchGenerationTopics.ts +13 -0
  317. package/src/hooks/useHotkeys/imageScope.ts +48 -0
  318. package/src/libs/mcp/client.ts +55 -22
  319. package/src/libs/mcp/types.ts +42 -6
  320. package/src/libs/model-runtime/BaseAI.ts +3 -1
  321. package/src/libs/model-runtime/ModelRuntime.test.ts +80 -0
  322. package/src/libs/model-runtime/ModelRuntime.ts +15 -1
  323. package/src/libs/model-runtime/UniformRuntime/index.ts +4 -1
  324. package/src/libs/model-runtime/fal/index.test.ts +442 -0
  325. package/src/libs/model-runtime/fal/index.ts +88 -0
  326. package/src/libs/model-runtime/openai/index.test.ts +396 -2
  327. package/src/libs/model-runtime/openai/index.ts +129 -3
  328. package/src/libs/model-runtime/runtimeMap.ts +2 -0
  329. package/src/libs/model-runtime/types/image.ts +25 -0
  330. package/src/libs/model-runtime/types/type.ts +1 -0
  331. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +10 -0
  332. package/src/libs/standard-parameters/index.ts +1 -0
  333. package/src/libs/standard-parameters/meta-schema.test.ts +214 -0
  334. package/src/libs/standard-parameters/meta-schema.ts +147 -0
  335. package/src/libs/swr/index.ts +1 -0
  336. package/src/libs/trpc/async/asyncAuth.ts +29 -8
  337. package/src/libs/trpc/async/context.ts +42 -4
  338. package/src/libs/trpc/async/index.ts +17 -4
  339. package/src/libs/trpc/async/init.ts +8 -0
  340. package/src/libs/trpc/client/lambda.ts +19 -2
  341. package/src/locales/default/common.ts +2 -0
  342. package/src/locales/default/components.ts +35 -0
  343. package/src/locales/default/error.ts +2 -0
  344. package/src/locales/default/image.ts +100 -0
  345. package/src/locales/default/index.ts +2 -0
  346. package/src/locales/default/metadata.ts +4 -0
  347. package/src/locales/default/modelProvider.ts +2 -0
  348. package/src/locales/default/plugin.ts +22 -0
  349. package/src/locales/default/setting.ts +5 -0
  350. package/src/middleware.ts +1 -0
  351. package/src/server/modules/ElectronIPCClient/index.ts +9 -1
  352. package/src/server/modules/S3/index.ts +15 -0
  353. package/src/server/routers/async/caller.ts +9 -1
  354. package/src/server/routers/async/image.ts +253 -0
  355. package/src/server/routers/async/index.ts +2 -0
  356. package/src/server/routers/lambda/aiProvider.test.ts +2 -0
  357. package/src/server/routers/lambda/generation.test.ts +267 -0
  358. package/src/server/routers/lambda/generation.ts +86 -0
  359. package/src/server/routers/lambda/generationBatch.test.ts +376 -0
  360. package/src/server/routers/lambda/generationBatch.ts +56 -0
  361. package/src/server/routers/lambda/generationTopic.test.ts +508 -0
  362. package/src/server/routers/lambda/generationTopic.ts +93 -0
  363. package/src/server/routers/lambda/image.ts +248 -0
  364. package/src/server/routers/lambda/index.ts +8 -0
  365. package/src/server/routers/tools/mcp.ts +15 -0
  366. package/src/server/services/file/__tests__/index.test.ts +135 -0
  367. package/src/server/services/file/impls/local.test.ts +153 -52
  368. package/src/server/services/file/impls/local.ts +70 -46
  369. package/src/server/services/file/impls/s3.test.ts +114 -0
  370. package/src/server/services/file/impls/s3.ts +40 -0
  371. package/src/server/services/file/impls/type.ts +10 -0
  372. package/src/server/services/file/index.ts +14 -0
  373. package/src/server/services/generation/index.ts +239 -0
  374. package/src/server/services/mcp/index.ts +20 -2
  375. package/src/services/__tests__/generation.test.ts +40 -0
  376. package/src/services/__tests__/generationBatch.test.ts +36 -0
  377. package/src/services/__tests__/generationTopic.test.ts +72 -0
  378. package/src/services/electron/file.ts +3 -1
  379. package/src/services/generation.ts +16 -0
  380. package/src/services/generationBatch.ts +25 -0
  381. package/src/services/generationTopic.ts +28 -0
  382. package/src/services/image.ts +33 -0
  383. package/src/services/mcp.ts +12 -7
  384. package/src/services/upload.ts +43 -9
  385. package/src/store/aiInfra/slices/aiProvider/action.ts +31 -6
  386. package/src/store/aiInfra/slices/aiProvider/initialState.ts +1 -0
  387. package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
  388. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -5
  389. package/src/store/chat/slices/message/action.ts +2 -2
  390. package/src/store/chat/slices/translate/action.ts +1 -1
  391. package/src/store/global/initialState.ts +9 -0
  392. package/src/store/global/selectors/systemStatus.ts +8 -0
  393. package/src/store/image/index.ts +2 -0
  394. package/src/store/image/initialState.ts +25 -0
  395. package/src/store/image/selectors.ts +4 -0
  396. package/src/store/image/slices/createImage/action.test.ts +330 -0
  397. package/src/store/image/slices/createImage/action.ts +134 -0
  398. package/src/store/image/slices/createImage/initialState.ts +9 -0
  399. package/src/store/image/slices/createImage/selectors.test.ts +114 -0
  400. package/src/store/image/slices/createImage/selectors.ts +9 -0
  401. package/src/store/image/slices/generationBatch/action.test.ts +495 -0
  402. package/src/store/image/slices/generationBatch/action.ts +303 -0
  403. package/src/store/image/slices/generationBatch/initialState.ts +13 -0
  404. package/src/store/image/slices/generationBatch/reducer.test.ts +568 -0
  405. package/src/store/image/slices/generationBatch/reducer.ts +101 -0
  406. package/src/store/image/slices/generationBatch/selectors.test.ts +307 -0
  407. package/src/store/image/slices/generationBatch/selectors.ts +36 -0
  408. package/src/store/image/slices/generationConfig/action.test.ts +351 -0
  409. package/src/store/image/slices/generationConfig/action.ts +295 -0
  410. package/src/store/image/slices/generationConfig/hooks.test.ts +304 -0
  411. package/src/store/image/slices/generationConfig/hooks.ts +118 -0
  412. package/src/store/image/slices/generationConfig/index.ts +1 -0
  413. package/src/store/image/slices/generationConfig/initialState.ts +37 -0
  414. package/src/store/image/slices/generationConfig/selectors.test.ts +204 -0
  415. package/src/store/image/slices/generationConfig/selectors.ts +25 -0
  416. package/src/store/image/slices/generationTopic/action.test.ts +687 -0
  417. package/src/store/image/slices/generationTopic/action.ts +319 -0
  418. package/src/store/image/slices/generationTopic/index.ts +2 -0
  419. package/src/store/image/slices/generationTopic/initialState.ts +14 -0
  420. package/src/store/image/slices/generationTopic/reducer.test.ts +198 -0
  421. package/src/store/image/slices/generationTopic/reducer.ts +66 -0
  422. package/src/store/image/slices/generationTopic/selectors.test.ts +103 -0
  423. package/src/store/image/slices/generationTopic/selectors.ts +15 -0
  424. package/src/store/image/store.ts +42 -0
  425. package/src/store/image/utils/size.ts +51 -0
  426. package/src/store/tool/slices/customPlugin/action.ts +10 -1
  427. package/src/store/tool/slices/mcpStore/action.ts +6 -4
  428. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
  429. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  430. package/src/types/aiModel.ts +8 -3
  431. package/src/types/aiProvider.ts +2 -0
  432. package/src/types/asyncTask.ts +2 -0
  433. package/src/types/files/index.ts +5 -0
  434. package/src/types/generation/index.ts +80 -0
  435. package/src/types/hotkey.ts +2 -0
  436. package/src/types/plugins/mcp.ts +2 -6
  437. package/src/types/tool/plugin.ts +8 -0
  438. package/src/types/user/settings/keyVaults.ts +5 -0
  439. package/src/types/user/settings/systemAgent.ts +1 -0
  440. package/src/utils/client/downloadFile.ts +33 -4
  441. package/src/utils/number.test.ts +105 -0
  442. package/src/utils/number.ts +25 -0
  443. package/src/utils/server/__tests__/geo.test.ts +6 -3
  444. package/src/utils/storeDebug.test.ts +152 -0
  445. package/src/utils/storeDebug.ts +16 -7
  446. package/src/utils/time.test.ts +259 -0
  447. package/src/utils/time.ts +18 -0
  448. package/src/utils/units.ts +61 -0
  449. package/src/utils/url.test.ts +358 -9
  450. package/src/utils/url.ts +105 -3
  451. package/{vitest.server.config.ts → vitest.config.server.ts} +3 -0
  452. package/.cursor/rules/i18n/i18n-auto-attached.mdc +0 -6
  453. package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -105
  454. package/src/features/Conversation/Error/InvalidAPIKey.tsx +0 -16
  455. package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +0 -227
  456. /package/.cursor/rules/{i18n/i18n.mdc → i18n.mdc} +0 -0
  457. /package/src/app/[variants]/(main)/settings/system-agent/features/{createForm.tsx → SystemAgentForm.tsx} +0 -0
  458. /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
- this.retrieveOrInitialize(browser);
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 date = (Date.now() / 1000 / 60 / 60).toFixed(0);
61
- const dirname = join(this.UPLOADS_DIR, date);
62
- logger.debug(`Creating timestamp directory: ${dirname}`);
63
- makeSureDirExist(dirname);
64
-
65
- // 生成文件保存路径
66
- const fileExt = filename.split('.').pop() || '';
67
- const savedFilename = `${hash}${fileExt ? `.${fileExt}` : ''}`;
68
- const savedPath = join(dirname, savedFilename);
69
- logger.debug(`Generated file save path: ${savedPath}`);
70
-
71
- // 写入文件内容
72
- const buffer = Buffer.from(content);
73
- logger.debug(`Writing file content, size: ${buffer.length} bytes`);
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: Date.now(),
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://${date}/${savedFilename}`;
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: date,
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
- const content = await readFilePromise(filePath);
132
- logger.debug(`File content read complete, size: ${content.length} bytes`);
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(`Failed to read metadata file: ${(metaError as Error).message}, using default MIME type`);
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 = path.replace('desktop://', '');
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
- await unlinkPromise(filePath);
211
- logger.debug(`File deletion successful`);
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(`Batch deletion operation complete, success: ${success}, error count: ${errors.length}`);
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 = path.replace('desktop://', '');
290
- const fullPath = join(this.UPLOADS_DIR, relativePath);
291
- logger.debug(`Resolved filesystem path: ${fullPath}`);
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
  }