@lobehub/chat 1.98.1 → 1.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) 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 +52 -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/const/dir.ts +3 -0
  11. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +13 -8
  12. package/apps/desktop/src/main/core/App.ts +8 -0
  13. package/apps/desktop/src/main/core/StaticFileServerManager.ts +221 -0
  14. package/apps/desktop/src/main/services/fileSrv.ts +231 -44
  15. package/apps/desktop/src/main/utils/next-electron-rsc.ts +36 -5
  16. package/changelog/v1.json +18 -0
  17. package/docs/development/database-schema.dbml +70 -0
  18. package/locales/ar/common.json +2 -0
  19. package/locales/ar/components.json +35 -0
  20. package/locales/ar/error.json +2 -0
  21. package/locales/ar/image.json +100 -0
  22. package/locales/ar/metadata.json +4 -0
  23. package/locales/ar/modelProvider.json +1 -0
  24. package/locales/ar/models.json +51 -9
  25. package/locales/ar/plugin.json +22 -0
  26. package/locales/ar/providers.json +3 -0
  27. package/locales/ar/setting.json +5 -0
  28. package/locales/bg-BG/common.json +2 -0
  29. package/locales/bg-BG/components.json +35 -0
  30. package/locales/bg-BG/error.json +2 -0
  31. package/locales/bg-BG/image.json +100 -0
  32. package/locales/bg-BG/metadata.json +4 -0
  33. package/locales/bg-BG/modelProvider.json +1 -0
  34. package/locales/bg-BG/models.json +51 -9
  35. package/locales/bg-BG/plugin.json +22 -0
  36. package/locales/bg-BG/providers.json +3 -0
  37. package/locales/bg-BG/setting.json +5 -0
  38. package/locales/de-DE/common.json +2 -0
  39. package/locales/de-DE/components.json +35 -0
  40. package/locales/de-DE/error.json +2 -0
  41. package/locales/de-DE/image.json +100 -0
  42. package/locales/de-DE/metadata.json +4 -0
  43. package/locales/de-DE/modelProvider.json +1 -0
  44. package/locales/de-DE/models.json +51 -9
  45. package/locales/de-DE/plugin.json +22 -0
  46. package/locales/de-DE/providers.json +3 -0
  47. package/locales/de-DE/setting.json +5 -0
  48. package/locales/en-US/common.json +2 -0
  49. package/locales/en-US/components.json +35 -0
  50. package/locales/en-US/error.json +2 -0
  51. package/locales/en-US/image.json +100 -0
  52. package/locales/en-US/metadata.json +4 -0
  53. package/locales/en-US/modelProvider.json +1 -0
  54. package/locales/en-US/models.json +51 -9
  55. package/locales/en-US/plugin.json +22 -0
  56. package/locales/en-US/providers.json +3 -0
  57. package/locales/en-US/setting.json +5 -0
  58. package/locales/es-ES/common.json +2 -0
  59. package/locales/es-ES/components.json +35 -0
  60. package/locales/es-ES/error.json +2 -0
  61. package/locales/es-ES/image.json +100 -0
  62. package/locales/es-ES/metadata.json +4 -0
  63. package/locales/es-ES/modelProvider.json +1 -0
  64. package/locales/es-ES/models.json +51 -9
  65. package/locales/es-ES/plugin.json +22 -0
  66. package/locales/es-ES/providers.json +3 -0
  67. package/locales/es-ES/setting.json +5 -0
  68. package/locales/fa-IR/common.json +2 -0
  69. package/locales/fa-IR/components.json +35 -0
  70. package/locales/fa-IR/error.json +2 -0
  71. package/locales/fa-IR/image.json +100 -0
  72. package/locales/fa-IR/metadata.json +4 -0
  73. package/locales/fa-IR/modelProvider.json +1 -0
  74. package/locales/fa-IR/models.json +51 -9
  75. package/locales/fa-IR/plugin.json +22 -0
  76. package/locales/fa-IR/providers.json +3 -0
  77. package/locales/fa-IR/setting.json +5 -0
  78. package/locales/fr-FR/common.json +2 -0
  79. package/locales/fr-FR/components.json +35 -0
  80. package/locales/fr-FR/error.json +2 -0
  81. package/locales/fr-FR/image.json +100 -0
  82. package/locales/fr-FR/metadata.json +4 -0
  83. package/locales/fr-FR/modelProvider.json +1 -0
  84. package/locales/fr-FR/models.json +51 -9
  85. package/locales/fr-FR/plugin.json +22 -0
  86. package/locales/fr-FR/providers.json +3 -0
  87. package/locales/fr-FR/setting.json +5 -0
  88. package/locales/it-IT/common.json +2 -0
  89. package/locales/it-IT/components.json +35 -0
  90. package/locales/it-IT/error.json +2 -0
  91. package/locales/it-IT/image.json +100 -0
  92. package/locales/it-IT/metadata.json +4 -0
  93. package/locales/it-IT/modelProvider.json +1 -0
  94. package/locales/it-IT/models.json +51 -9
  95. package/locales/it-IT/plugin.json +22 -0
  96. package/locales/it-IT/providers.json +3 -0
  97. package/locales/it-IT/setting.json +5 -0
  98. package/locales/ja-JP/common.json +2 -0
  99. package/locales/ja-JP/components.json +35 -0
  100. package/locales/ja-JP/error.json +2 -0
  101. package/locales/ja-JP/image.json +100 -0
  102. package/locales/ja-JP/metadata.json +4 -0
  103. package/locales/ja-JP/modelProvider.json +1 -0
  104. package/locales/ja-JP/models.json +51 -9
  105. package/locales/ja-JP/plugin.json +22 -0
  106. package/locales/ja-JP/providers.json +3 -0
  107. package/locales/ja-JP/setting.json +5 -0
  108. package/locales/ko-KR/common.json +2 -0
  109. package/locales/ko-KR/components.json +35 -0
  110. package/locales/ko-KR/error.json +2 -0
  111. package/locales/ko-KR/image.json +100 -0
  112. package/locales/ko-KR/metadata.json +4 -0
  113. package/locales/ko-KR/modelProvider.json +1 -0
  114. package/locales/ko-KR/models.json +51 -9
  115. package/locales/ko-KR/plugin.json +22 -0
  116. package/locales/ko-KR/providers.json +3 -0
  117. package/locales/ko-KR/setting.json +5 -0
  118. package/locales/nl-NL/common.json +2 -0
  119. package/locales/nl-NL/components.json +35 -0
  120. package/locales/nl-NL/error.json +2 -0
  121. package/locales/nl-NL/image.json +100 -0
  122. package/locales/nl-NL/metadata.json +4 -0
  123. package/locales/nl-NL/modelProvider.json +1 -0
  124. package/locales/nl-NL/models.json +51 -9
  125. package/locales/nl-NL/plugin.json +22 -0
  126. package/locales/nl-NL/providers.json +3 -0
  127. package/locales/nl-NL/setting.json +5 -0
  128. package/locales/pl-PL/common.json +2 -0
  129. package/locales/pl-PL/components.json +35 -0
  130. package/locales/pl-PL/error.json +2 -0
  131. package/locales/pl-PL/image.json +100 -0
  132. package/locales/pl-PL/metadata.json +4 -0
  133. package/locales/pl-PL/modelProvider.json +1 -0
  134. package/locales/pl-PL/models.json +51 -9
  135. package/locales/pl-PL/plugin.json +22 -0
  136. package/locales/pl-PL/providers.json +3 -0
  137. package/locales/pl-PL/setting.json +5 -0
  138. package/locales/pt-BR/common.json +2 -0
  139. package/locales/pt-BR/components.json +35 -0
  140. package/locales/pt-BR/error.json +2 -0
  141. package/locales/pt-BR/image.json +100 -0
  142. package/locales/pt-BR/metadata.json +4 -0
  143. package/locales/pt-BR/modelProvider.json +1 -0
  144. package/locales/pt-BR/models.json +51 -9
  145. package/locales/pt-BR/plugin.json +22 -0
  146. package/locales/pt-BR/providers.json +3 -0
  147. package/locales/pt-BR/setting.json +5 -0
  148. package/locales/ru-RU/common.json +2 -0
  149. package/locales/ru-RU/components.json +35 -0
  150. package/locales/ru-RU/error.json +2 -0
  151. package/locales/ru-RU/image.json +100 -0
  152. package/locales/ru-RU/metadata.json +4 -0
  153. package/locales/ru-RU/modelProvider.json +1 -0
  154. package/locales/ru-RU/models.json +51 -9
  155. package/locales/ru-RU/plugin.json +22 -0
  156. package/locales/ru-RU/providers.json +3 -0
  157. package/locales/ru-RU/setting.json +5 -0
  158. package/locales/tr-TR/common.json +2 -0
  159. package/locales/tr-TR/components.json +35 -0
  160. package/locales/tr-TR/error.json +2 -0
  161. package/locales/tr-TR/image.json +100 -0
  162. package/locales/tr-TR/metadata.json +4 -0
  163. package/locales/tr-TR/modelProvider.json +1 -0
  164. package/locales/tr-TR/models.json +51 -9
  165. package/locales/tr-TR/plugin.json +22 -0
  166. package/locales/tr-TR/providers.json +3 -0
  167. package/locales/tr-TR/setting.json +5 -0
  168. package/locales/vi-VN/common.json +2 -0
  169. package/locales/vi-VN/components.json +35 -0
  170. package/locales/vi-VN/error.json +2 -0
  171. package/locales/vi-VN/image.json +100 -0
  172. package/locales/vi-VN/metadata.json +4 -0
  173. package/locales/vi-VN/modelProvider.json +1 -0
  174. package/locales/vi-VN/models.json +51 -9
  175. package/locales/vi-VN/plugin.json +22 -0
  176. package/locales/vi-VN/providers.json +3 -0
  177. package/locales/vi-VN/setting.json +5 -0
  178. package/locales/zh-CN/common.json +2 -0
  179. package/locales/zh-CN/components.json +35 -0
  180. package/locales/zh-CN/error.json +2 -0
  181. package/locales/zh-CN/image.json +100 -0
  182. package/locales/zh-CN/metadata.json +4 -0
  183. package/locales/zh-CN/modelProvider.json +1 -0
  184. package/locales/zh-CN/models.json +51 -9
  185. package/locales/zh-CN/plugin.json +22 -0
  186. package/locales/zh-CN/providers.json +3 -0
  187. package/locales/zh-CN/setting.json +5 -0
  188. package/locales/zh-TW/common.json +2 -0
  189. package/locales/zh-TW/components.json +35 -0
  190. package/locales/zh-TW/error.json +2 -0
  191. package/locales/zh-TW/image.json +100 -0
  192. package/locales/zh-TW/metadata.json +4 -0
  193. package/locales/zh-TW/modelProvider.json +1 -0
  194. package/locales/zh-TW/models.json +51 -9
  195. package/locales/zh-TW/plugin.json +22 -0
  196. package/locales/zh-TW/providers.json +3 -0
  197. package/locales/zh-TW/setting.json +5 -0
  198. package/package.json +11 -4
  199. package/packages/electron-server-ipc/src/events/file.ts +3 -1
  200. package/packages/electron-server-ipc/src/types/file.ts +15 -0
  201. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -1
  202. package/src/app/[variants]/(main)/image/@menu/components/AspectRatioSelect/index.tsx +73 -0
  203. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +39 -0
  204. package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +89 -0
  205. package/src/app/[variants]/(main)/image/@menu/default.tsx +11 -0
  206. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +24 -0
  207. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +107 -0
  208. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageNum.tsx +290 -0
  209. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +504 -0
  210. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +18 -0
  211. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +19 -0
  212. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +155 -0
  213. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +415 -0
  214. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +732 -0
  215. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +24 -0
  216. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSelect.tsx +17 -0
  217. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +15 -0
  218. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +11 -0
  219. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/constants.ts +1 -0
  220. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +93 -0
  221. package/src/app/[variants]/(main)/image/@topic/default.tsx +17 -0
  222. package/src/app/[variants]/(main)/image/@topic/features/Topics/NewTopicButton.tsx +64 -0
  223. package/src/app/[variants]/(main)/image/@topic/features/Topics/SkeletonList.tsx +34 -0
  224. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +136 -0
  225. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +91 -0
  226. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +57 -0
  227. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicUrlSync.tsx +37 -0
  228. package/src/app/[variants]/(main)/image/@topic/features/Topics/index.tsx +19 -0
  229. package/src/app/[variants]/(main)/image/NotSupportClient.tsx +153 -0
  230. package/src/app/[variants]/(main)/image/_layout/Desktop/Container.tsx +35 -0
  231. package/src/app/[variants]/(main)/image/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  232. package/src/app/[variants]/(main)/image/_layout/Desktop/index.tsx +30 -0
  233. package/src/app/[variants]/(main)/image/_layout/Mobile/index.tsx +14 -0
  234. package/src/app/[variants]/(main)/image/_layout/type.ts +7 -0
  235. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +196 -0
  236. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx +60 -0
  237. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx +90 -0
  238. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +65 -0
  239. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +43 -0
  240. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +49 -0
  241. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +156 -0
  242. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/styles.ts +51 -0
  243. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +39 -0
  244. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +11 -0
  245. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +97 -0
  246. package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +48 -0
  247. package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +37 -0
  248. package/src/app/[variants]/(main)/image/features/ImageWorkspace/SkeletonList.tsx +50 -0
  249. package/src/app/[variants]/(main)/image/features/ImageWorkspace/index.tsx +23 -0
  250. package/src/app/[variants]/(main)/image/features/PromptInput/Title.tsx +38 -0
  251. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +114 -0
  252. package/src/app/[variants]/(main)/image/layout.tsx +19 -0
  253. package/src/app/[variants]/(main)/image/loading.tsx +3 -0
  254. package/src/app/[variants]/(main)/image/page.tsx +47 -0
  255. package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
  256. package/src/chains/summaryGenerationTitle.ts +25 -0
  257. package/src/components/ImageItem/index.tsx +9 -6
  258. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/Bedrock.tsx +3 -4
  259. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/ProviderApiKeyForm.tsx +5 -4
  260. package/src/components/InvalidAPIKey/APIKeyForm/index.tsx +108 -0
  261. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/useApiKey.ts +2 -1
  262. package/src/components/InvalidAPIKey/index.tsx +30 -0
  263. package/src/components/KeyValueEditor/index.tsx +203 -0
  264. package/src/components/KeyValueEditor/utils.ts +42 -0
  265. package/src/config/aiModels/fal.ts +52 -0
  266. package/src/config/aiModels/index.ts +3 -0
  267. package/src/config/aiModels/openai.ts +20 -6
  268. package/src/config/llm.ts +6 -0
  269. package/src/config/modelProviders/fal.ts +21 -0
  270. package/src/config/modelProviders/index.ts +3 -0
  271. package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +8 -0
  272. package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +11 -0
  273. package/src/config/paramsSchemas/fal/flux-schnell.ts +9 -0
  274. package/src/config/paramsSchemas/fal/imagen4.ts +10 -0
  275. package/src/config/paramsSchemas/openai/gpt-image-1.ts +10 -0
  276. package/src/const/hotkeys.ts +2 -2
  277. package/src/const/image.ts +6 -0
  278. package/src/const/settings/systemAgent.ts +1 -0
  279. package/src/database/client/migrations.json +27 -0
  280. package/src/database/migrations/0026_add_autovacuum_tuning.sql +2 -0
  281. package/src/database/migrations/0027_ai_image.sql +47 -0
  282. package/src/database/migrations/meta/0027_snapshot.json +6003 -0
  283. package/src/database/migrations/meta/_journal.json +7 -0
  284. package/src/database/models/__tests__/asyncTask.test.ts +7 -5
  285. package/src/database/models/__tests__/file.test.ts +287 -0
  286. package/src/database/models/__tests__/generation.test.ts +786 -0
  287. package/src/database/models/__tests__/generationBatch.test.ts +614 -0
  288. package/src/database/models/__tests__/generationTopic.test.ts +411 -0
  289. package/src/database/models/aiModel.ts +2 -0
  290. package/src/database/models/asyncTask.ts +1 -1
  291. package/src/database/models/file.ts +28 -20
  292. package/src/database/models/generation.ts +197 -0
  293. package/src/database/models/generationBatch.ts +212 -0
  294. package/src/database/models/generationTopic.ts +131 -0
  295. package/src/database/repositories/aiInfra/index.test.ts +151 -1
  296. package/src/database/repositories/aiInfra/index.ts +28 -19
  297. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  298. package/src/database/schemas/file.ts +8 -0
  299. package/src/database/schemas/generation.ts +127 -0
  300. package/src/database/schemas/index.ts +1 -0
  301. package/src/database/schemas/relations.ts +45 -1
  302. package/src/database/type.ts +2 -0
  303. package/src/database/utils/idGenerator.ts +3 -0
  304. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +39 -0
  305. package/src/features/Conversation/Error/InvalidAccessCode.tsx +2 -2
  306. package/src/features/Conversation/Error/index.tsx +3 -3
  307. package/src/features/ImageSidePanel/index.tsx +83 -0
  308. package/src/features/ImageTopicPanel/index.tsx +79 -0
  309. package/src/features/PluginDevModal/MCPManifestForm/CollapsibleSection.tsx +62 -0
  310. package/src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx +158 -0
  311. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +99 -155
  312. package/src/features/PluginStore/McpList/Detail/Settings/index.tsx +5 -2
  313. package/src/hooks/useDownloadImage.ts +31 -0
  314. package/src/hooks/useFetchGenerationTopics.ts +13 -0
  315. package/src/hooks/useHotkeys/imageScope.ts +48 -0
  316. package/src/libs/mcp/client.ts +55 -22
  317. package/src/libs/mcp/types.ts +42 -6
  318. package/src/libs/model-runtime/BaseAI.ts +3 -1
  319. package/src/libs/model-runtime/ModelRuntime.test.ts +80 -0
  320. package/src/libs/model-runtime/ModelRuntime.ts +15 -1
  321. package/src/libs/model-runtime/UniformRuntime/index.ts +4 -1
  322. package/src/libs/model-runtime/fal/index.test.ts +442 -0
  323. package/src/libs/model-runtime/fal/index.ts +88 -0
  324. package/src/libs/model-runtime/openai/index.test.ts +396 -2
  325. package/src/libs/model-runtime/openai/index.ts +129 -3
  326. package/src/libs/model-runtime/runtimeMap.ts +2 -0
  327. package/src/libs/model-runtime/types/image.ts +25 -0
  328. package/src/libs/model-runtime/types/type.ts +1 -0
  329. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +10 -0
  330. package/src/libs/standard-parameters/index.ts +1 -0
  331. package/src/libs/standard-parameters/meta-schema.test.ts +214 -0
  332. package/src/libs/standard-parameters/meta-schema.ts +147 -0
  333. package/src/libs/swr/index.ts +1 -0
  334. package/src/libs/trpc/async/asyncAuth.ts +29 -8
  335. package/src/libs/trpc/async/context.ts +42 -4
  336. package/src/libs/trpc/async/index.ts +17 -4
  337. package/src/libs/trpc/async/init.ts +8 -0
  338. package/src/libs/trpc/client/lambda.ts +19 -2
  339. package/src/locales/default/common.ts +2 -0
  340. package/src/locales/default/components.ts +35 -0
  341. package/src/locales/default/error.ts +2 -0
  342. package/src/locales/default/image.ts +100 -0
  343. package/src/locales/default/index.ts +2 -0
  344. package/src/locales/default/metadata.ts +4 -0
  345. package/src/locales/default/modelProvider.ts +2 -0
  346. package/src/locales/default/plugin.ts +22 -0
  347. package/src/locales/default/setting.ts +5 -0
  348. package/src/middleware.ts +1 -0
  349. package/src/server/modules/ElectronIPCClient/index.ts +9 -1
  350. package/src/server/modules/S3/index.ts +15 -0
  351. package/src/server/routers/async/caller.ts +9 -1
  352. package/src/server/routers/async/image.ts +253 -0
  353. package/src/server/routers/async/index.ts +2 -0
  354. package/src/server/routers/lambda/aiProvider.test.ts +1 -0
  355. package/src/server/routers/lambda/generation.test.ts +267 -0
  356. package/src/server/routers/lambda/generation.ts +86 -0
  357. package/src/server/routers/lambda/generationBatch.test.ts +376 -0
  358. package/src/server/routers/lambda/generationBatch.ts +56 -0
  359. package/src/server/routers/lambda/generationTopic.test.ts +508 -0
  360. package/src/server/routers/lambda/generationTopic.ts +93 -0
  361. package/src/server/routers/lambda/image.ts +248 -0
  362. package/src/server/routers/lambda/index.ts +8 -0
  363. package/src/server/routers/tools/mcp.ts +15 -0
  364. package/src/server/services/file/__tests__/index.test.ts +135 -0
  365. package/src/server/services/file/impls/local.test.ts +153 -52
  366. package/src/server/services/file/impls/local.ts +70 -46
  367. package/src/server/services/file/impls/s3.test.ts +114 -0
  368. package/src/server/services/file/impls/s3.ts +40 -0
  369. package/src/server/services/file/impls/type.ts +10 -0
  370. package/src/server/services/file/index.ts +14 -0
  371. package/src/server/services/generation/index.ts +239 -0
  372. package/src/server/services/mcp/index.ts +20 -2
  373. package/src/services/__tests__/generation.test.ts +40 -0
  374. package/src/services/__tests__/generationBatch.test.ts +36 -0
  375. package/src/services/__tests__/generationTopic.test.ts +72 -0
  376. package/src/services/electron/file.ts +3 -1
  377. package/src/services/generation.ts +16 -0
  378. package/src/services/generationBatch.ts +25 -0
  379. package/src/services/generationTopic.ts +28 -0
  380. package/src/services/image.ts +33 -0
  381. package/src/services/mcp.ts +12 -7
  382. package/src/services/upload.ts +43 -9
  383. package/src/store/aiInfra/slices/aiProvider/action.ts +25 -5
  384. package/src/store/aiInfra/slices/aiProvider/initialState.ts +1 -0
  385. package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
  386. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -5
  387. package/src/store/chat/slices/message/action.ts +2 -2
  388. package/src/store/chat/slices/translate/action.ts +1 -1
  389. package/src/store/global/initialState.ts +9 -0
  390. package/src/store/global/selectors/systemStatus.ts +8 -0
  391. package/src/store/image/index.ts +2 -0
  392. package/src/store/image/initialState.ts +25 -0
  393. package/src/store/image/selectors.ts +4 -0
  394. package/src/store/image/slices/createImage/action.test.ts +330 -0
  395. package/src/store/image/slices/createImage/action.ts +134 -0
  396. package/src/store/image/slices/createImage/initialState.ts +9 -0
  397. package/src/store/image/slices/createImage/selectors.test.ts +114 -0
  398. package/src/store/image/slices/createImage/selectors.ts +9 -0
  399. package/src/store/image/slices/generationBatch/action.test.ts +495 -0
  400. package/src/store/image/slices/generationBatch/action.ts +303 -0
  401. package/src/store/image/slices/generationBatch/initialState.ts +13 -0
  402. package/src/store/image/slices/generationBatch/reducer.test.ts +568 -0
  403. package/src/store/image/slices/generationBatch/reducer.ts +101 -0
  404. package/src/store/image/slices/generationBatch/selectors.test.ts +307 -0
  405. package/src/store/image/slices/generationBatch/selectors.ts +36 -0
  406. package/src/store/image/slices/generationConfig/action.test.ts +351 -0
  407. package/src/store/image/slices/generationConfig/action.ts +295 -0
  408. package/src/store/image/slices/generationConfig/hooks.test.ts +304 -0
  409. package/src/store/image/slices/generationConfig/hooks.ts +118 -0
  410. package/src/store/image/slices/generationConfig/index.ts +1 -0
  411. package/src/store/image/slices/generationConfig/initialState.ts +37 -0
  412. package/src/store/image/slices/generationConfig/selectors.test.ts +204 -0
  413. package/src/store/image/slices/generationConfig/selectors.ts +25 -0
  414. package/src/store/image/slices/generationTopic/action.test.ts +687 -0
  415. package/src/store/image/slices/generationTopic/action.ts +319 -0
  416. package/src/store/image/slices/generationTopic/index.ts +2 -0
  417. package/src/store/image/slices/generationTopic/initialState.ts +14 -0
  418. package/src/store/image/slices/generationTopic/reducer.test.ts +198 -0
  419. package/src/store/image/slices/generationTopic/reducer.ts +66 -0
  420. package/src/store/image/slices/generationTopic/selectors.test.ts +103 -0
  421. package/src/store/image/slices/generationTopic/selectors.ts +15 -0
  422. package/src/store/image/store.ts +42 -0
  423. package/src/store/image/utils/size.ts +51 -0
  424. package/src/store/tool/slices/customPlugin/action.ts +10 -1
  425. package/src/store/tool/slices/mcpStore/action.ts +6 -4
  426. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
  427. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  428. package/src/types/aiModel.ts +8 -3
  429. package/src/types/aiProvider.ts +1 -0
  430. package/src/types/asyncTask.ts +2 -0
  431. package/src/types/files/index.ts +5 -0
  432. package/src/types/generation/index.ts +80 -0
  433. package/src/types/hotkey.ts +2 -0
  434. package/src/types/plugins/mcp.ts +2 -6
  435. package/src/types/tool/plugin.ts +8 -0
  436. package/src/types/user/settings/keyVaults.ts +5 -0
  437. package/src/types/user/settings/systemAgent.ts +1 -0
  438. package/src/utils/client/downloadFile.ts +33 -4
  439. package/src/utils/number.test.ts +105 -0
  440. package/src/utils/number.ts +25 -0
  441. package/src/utils/server/__tests__/geo.test.ts +6 -3
  442. package/src/utils/storeDebug.test.ts +152 -0
  443. package/src/utils/storeDebug.ts +16 -7
  444. package/src/utils/time.test.ts +259 -0
  445. package/src/utils/time.ts +18 -0
  446. package/src/utils/units.ts +61 -0
  447. package/src/utils/url.test.ts +358 -9
  448. package/src/utils/url.ts +105 -3
  449. package/{vitest.server.config.ts → vitest.config.server.ts} +3 -0
  450. package/.cursor/rules/i18n/i18n-auto-attached.mdc +0 -6
  451. package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -105
  452. package/src/features/Conversation/Error/InvalidAPIKey.tsx +0 -16
  453. package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +0 -227
  454. /package/.cursor/rules/{i18n/i18n.mdc → i18n.mdc} +0 -0
  455. /package/src/app/[variants]/(main)/settings/system-agent/features/{createForm.tsx → SystemAgentForm.tsx} +0 -0
  456. /package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/LoadingContext.ts +0 -0
@@ -0,0 +1,732 @@
1
+ 'use client';
2
+
3
+ // Removed Image import - using img tags instead
4
+ import { createStyles, useTheme } from 'antd-style';
5
+ import { Image as ImageIcon, X } from 'lucide-react';
6
+ import React, { type FC, memo, useEffect, useRef, useState } from 'react';
7
+ import { useTranslation } from 'react-i18next';
8
+ import { Center } from 'react-layout-kit';
9
+
10
+ import { useFileStore } from '@/store/file';
11
+ import { FileUploadStatus } from '@/types/files/upload';
12
+
13
+ import { CONFIG_PANEL_WIDTH } from '../../constants';
14
+ import ImageManageModal, { type ImageItem } from './ImageManageModal';
15
+
16
+ // ======== Business Types ======== //
17
+
18
+ /**
19
+ * Internal type for managing upload state and display
20
+ */
21
+ interface DisplayItem {
22
+ // Upload status - using the correct type from file upload system
23
+ error?: string;
24
+ // URL for display, can be a blob: URL for local previews or a remote URL
25
+ file?: File;
26
+ id: string;
27
+ // Error message if upload failed
28
+ progress?: number;
29
+ // The raw File object, present only for new, not-yet-uploaded images
30
+ status?: FileUploadStatus;
31
+ // Unique identifier for the item
32
+ url: string; // Upload progress (0-100)
33
+ }
34
+
35
+ export interface MultiImagesUploadProps {
36
+ // Callback when URLs change
37
+ className?: string; // Array of image URLs
38
+ onChange?: (urls: string[]) => void;
39
+ style?: React.CSSProperties;
40
+ value?: string[];
41
+ }
42
+
43
+ // ======== Styles ======== //
44
+
45
+ const useStyles = createStyles(({ css, token }) => {
46
+ // Calculate available width for thumbnails
47
+ // Panel width - outer padding (16px * 2) - gaps (8px * 3 for 4 items)
48
+ const availableWidth = CONFIG_PANEL_WIDTH - 32 - 24;
49
+ const thumbnailSize = availableWidth / 4;
50
+
51
+ return {
52
+ deleteIcon: css`
53
+ cursor: pointer;
54
+
55
+ position: absolute;
56
+ z-index: 10;
57
+ inset-block-start: 4px;
58
+ inset-inline-end: 4px;
59
+
60
+ display: flex;
61
+ align-items: center;
62
+ justify-content: center;
63
+
64
+ width: 20px;
65
+ height: 20px;
66
+ border-radius: 50%;
67
+
68
+ color: ${token.colorTextLightSolid};
69
+
70
+ opacity: 0;
71
+ background: ${token.colorBgMask};
72
+
73
+ transition: opacity 0.2s ease;
74
+
75
+ &:hover {
76
+ color: ${token.colorError};
77
+ background: ${token.colorErrorBg};
78
+ }
79
+ `,
80
+
81
+ imageItem: css`
82
+ position: relative;
83
+
84
+ overflow: hidden;
85
+
86
+ width: ${thumbnailSize}px;
87
+ height: ${thumbnailSize}px;
88
+ border-radius: ${token.borderRadius}px;
89
+
90
+ background: ${token.colorBgContainer};
91
+
92
+ &:hover .delete-icon {
93
+ opacity: 1;
94
+ }
95
+ `,
96
+
97
+ // Image thumbnails styles
98
+ imageThumbnails: css`
99
+ cursor: pointer;
100
+
101
+ display: flex;
102
+ gap: 8px;
103
+
104
+ width: 100%;
105
+ height: ${thumbnailSize}px;
106
+ padding: 0;
107
+ `,
108
+
109
+ moreOverlay: css`
110
+ position: absolute;
111
+ inset-block: 0 0;
112
+ inset-inline: 0 0;
113
+
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+
118
+ font-size: 16px;
119
+ font-weight: 600;
120
+ color: ${token.colorTextLightSolid};
121
+
122
+ background: ${token.colorBgMask};
123
+ `,
124
+
125
+ placeholder: css`
126
+ cursor: pointer;
127
+
128
+ width: 100%;
129
+ height: 120px;
130
+ border: 2px dashed ${token.colorBorder};
131
+ border-radius: ${token.borderRadiusLG}px;
132
+
133
+ background: ${token.colorFillAlter};
134
+
135
+ transition: all 0.2s ease;
136
+
137
+ &:hover {
138
+ border-color: ${token.colorPrimary};
139
+ background: ${token.colorFillSecondary};
140
+ }
141
+ `,
142
+
143
+ placeholderIcon: css`
144
+ color: ${token.colorTextTertiary};
145
+ `,
146
+
147
+ placeholderText: css`
148
+ font-size: 12px; /* Made smaller than default token.fontSize (14px) */
149
+ line-height: 1.4;
150
+ color: ${token.colorTextSecondary};
151
+ text-align: center;
152
+ `,
153
+
154
+ progress: css`
155
+ cursor: pointer;
156
+
157
+ position: relative;
158
+
159
+ overflow: hidden;
160
+
161
+ width: 100%;
162
+ height: 120px;
163
+ border: 2px solid ${token.colorPrimary};
164
+ border-radius: ${token.borderRadiusLG}px;
165
+
166
+ background: ${token.colorFillSecondary};
167
+
168
+ transition: all 0.2s ease;
169
+ `,
170
+
171
+ progressPrimary: css`
172
+ margin-block-end: 4px;
173
+ font-size: 14px;
174
+ font-weight: 600;
175
+ color: ${token.colorPrimary};
176
+ `,
177
+
178
+ progressText: css`
179
+ font-size: 12px;
180
+ font-weight: 500;
181
+ line-height: 1.4;
182
+ color: ${token.colorText};
183
+ text-align: center;
184
+ `,
185
+ // Single image display styles
186
+ singleImageDisplay: css`
187
+ cursor: pointer;
188
+
189
+ position: relative;
190
+
191
+ overflow: hidden;
192
+
193
+ width: 100%;
194
+ height: 160px;
195
+ border-radius: ${token.borderRadiusLG}px;
196
+
197
+ background: ${token.colorBgContainer};
198
+
199
+ &:hover .upload-more-overlay {
200
+ opacity: 1;
201
+ }
202
+
203
+ &:hover .delete-icon {
204
+ opacity: 1;
205
+ }
206
+ `,
207
+ uploadMoreButton: css`
208
+ cursor: pointer;
209
+
210
+ padding-block: 8px;
211
+ padding-inline: 16px;
212
+ border: 1px solid ${token.colorBorder};
213
+ border-radius: ${token.borderRadius}px;
214
+
215
+ font-size: 12px;
216
+ font-weight: 500;
217
+ color: ${token.colorText};
218
+
219
+ background: ${token.colorBgContainer};
220
+ box-shadow: ${token.boxShadowSecondary};
221
+
222
+ &:hover {
223
+ border-color: ${token.colorPrimary};
224
+ color: ${token.colorPrimary};
225
+ background: ${token.colorBgElevated};
226
+ }
227
+ `,
228
+ uploadMoreOverlay: css`
229
+ position: absolute;
230
+ z-index: 5;
231
+ inset-block: 0 0;
232
+ inset-inline: 0 0;
233
+
234
+ display: flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+
238
+ opacity: 0;
239
+ background: ${token.colorBgMask};
240
+
241
+ transition: opacity 0.2s ease;
242
+ `,
243
+ };
244
+ });
245
+
246
+ // ======== Utils ======== //
247
+
248
+ /**
249
+ * Check if a URL is a local blob URL
250
+ * @param url - The URL to check
251
+ * @returns true if the URL is a blob URL
252
+ */
253
+ const isLocalBlobUrl = (url: string): boolean => url.startsWith('blob:');
254
+
255
+ // ======== Sub-Components ======== //
256
+
257
+ interface ImageUploadPlaceholderProps {
258
+ onClick?: () => void;
259
+ }
260
+
261
+ const ImageUploadPlaceholder: FC<ImageUploadPlaceholderProps> = memo(({ onClick }) => {
262
+ const { styles } = useStyles();
263
+ const { t } = useTranslation('components');
264
+
265
+ return (
266
+ <Center className={styles.placeholder} gap={16} horizontal={false} onClick={onClick}>
267
+ <ImageIcon className={styles.placeholderIcon} size={48} strokeWidth={1.5} />
268
+ <div className={styles.placeholderText}>
269
+ {t('MultiImagesUpload.placeholder.primary')}
270
+ <br />
271
+ {t('MultiImagesUpload.placeholder.secondary')}
272
+ </div>
273
+ </Center>
274
+ );
275
+ });
276
+
277
+ ImageUploadPlaceholder.displayName = 'ImageUploadPlaceholder';
278
+
279
+ // ======== 圆形进度组件 ======== //
280
+
281
+ interface CircularProgressProps {
282
+ className?: string;
283
+ showText?: boolean;
284
+ // 0-100
285
+ size?: number;
286
+ strokeWidth?: number;
287
+ value: number;
288
+ }
289
+
290
+ const CircularProgress: FC<CircularProgressProps> = memo(
291
+ ({ value, size = 48, strokeWidth = 4, className, showText = true }) => {
292
+ const theme = useTheme();
293
+
294
+ // Ensure value is between 0 and 100
295
+ const progress = Math.min(100, Math.max(0, value));
296
+
297
+ // Calculate circle properties
298
+ const radius = (size - strokeWidth) / 2;
299
+ const circumference = radius * 2 * Math.PI;
300
+ const offset = circumference - (progress / 100) * circumference;
301
+
302
+ return (
303
+ <div
304
+ className={className}
305
+ style={{
306
+ alignItems: 'center',
307
+ display: 'flex',
308
+ height: size,
309
+ justifyContent: 'center',
310
+ position: 'relative',
311
+ width: size,
312
+ }}
313
+ >
314
+ {/* Background circle */}
315
+ <svg
316
+ height={size}
317
+ style={{ position: 'absolute', transform: 'rotate(-90deg)' }}
318
+ width={size}
319
+ >
320
+ <circle
321
+ cx={size / 2}
322
+ cy={size / 2}
323
+ fill="none"
324
+ r={radius}
325
+ stroke={theme.colorBorder}
326
+ strokeWidth={strokeWidth}
327
+ />
328
+ </svg>
329
+
330
+ {/* Progress circle */}
331
+ <svg
332
+ height={size}
333
+ style={{ position: 'absolute', transform: 'rotate(-90deg)' }}
334
+ width={size}
335
+ >
336
+ <circle
337
+ cx={size / 2}
338
+ cy={size / 2}
339
+ fill="none"
340
+ r={radius}
341
+ stroke={theme.colorPrimary}
342
+ strokeDasharray={circumference}
343
+ strokeDashoffset={offset}
344
+ strokeLinecap="round"
345
+ strokeWidth={strokeWidth}
346
+ style={{
347
+ transition: 'stroke-dashoffset 0.2s ease-in-out',
348
+ }}
349
+ />
350
+ </svg>
351
+
352
+ {/* Progress text */}
353
+ {showText && (
354
+ <span
355
+ style={{
356
+ color: theme.colorPrimary,
357
+ fontSize: '12px',
358
+ fontWeight: 600,
359
+ position: 'relative',
360
+ zIndex: 1,
361
+ }}
362
+ >
363
+ {Math.round(progress)}%
364
+ </span>
365
+ )}
366
+ </div>
367
+ );
368
+ },
369
+ );
370
+
371
+ CircularProgress.displayName = 'CircularProgress';
372
+
373
+ interface ImageUploadProgressProps {
374
+ completedCount: number;
375
+ currentProgress: number;
376
+ showCount?: boolean;
377
+ totalCount: number;
378
+ }
379
+
380
+ const ImageUploadProgress: FC<ImageUploadProgressProps> = memo(
381
+ ({ completedCount, totalCount, currentProgress, showCount = true }) => {
382
+ const { styles } = useStyles();
383
+ const { t } = useTranslation('components');
384
+
385
+ return (
386
+ <Center className={styles.progress} gap={16} horizontal={false}>
387
+ {/* 圆形进度条 */}
388
+ <CircularProgress size={60} strokeWidth={6} value={currentProgress} />
389
+
390
+ {/* 进度文本 */}
391
+ <div className={styles.progressText}>
392
+ {showCount ? (
393
+ <div className={styles.progressPrimary}>
394
+ {t('MultiImagesUpload.progress.uploadingWithCount', {
395
+ completed: completedCount,
396
+ total: totalCount,
397
+ })}
398
+ </div>
399
+ ) : null}
400
+ </div>
401
+ </Center>
402
+ );
403
+ },
404
+ );
405
+
406
+ ImageUploadProgress.displayName = 'ImageUploadProgress';
407
+
408
+ interface ImageThumbnailsProps {
409
+ images: string[];
410
+ onClick?: () => void;
411
+ onDelete?: (index: number) => void;
412
+ }
413
+
414
+ const ImageThumbnails: FC<ImageThumbnailsProps> = memo(({ images, onClick, onDelete }) => {
415
+ const { styles } = useStyles();
416
+
417
+ // Display max 4 images, with overflow indication
418
+ const displayImages = images.slice(0, 4);
419
+ const remainingCount = Math.max(0, images.length - 3); // Show +x for images beyond first 3
420
+
421
+ const handleDelete = (index: number, event: React.MouseEvent) => {
422
+ event.stopPropagation(); // Prevent triggering container click
423
+ onDelete?.(index);
424
+ };
425
+
426
+ const renderImageItem = (imageUrl: string, index: number) => {
427
+ const isLastItem = index === 3;
428
+ const showOverlay = isLastItem && remainingCount > 1;
429
+
430
+ return (
431
+ <div className={styles.imageItem} key={imageUrl}>
432
+ <img
433
+ alt={`Uploaded image ${index + 1}`}
434
+ src={imageUrl}
435
+ style={{ height: '100%', objectFit: 'cover', width: '100%' }}
436
+ />
437
+ {!showOverlay && (
438
+ <div
439
+ className={`${styles.deleteIcon} delete-icon`}
440
+ onClick={(e) => handleDelete(index, e)}
441
+ >
442
+ <X size={12} />
443
+ </div>
444
+ )}
445
+ {showOverlay && <div className={styles.moreOverlay}>+{remainingCount}</div>}
446
+ </div>
447
+ );
448
+ };
449
+
450
+ return (
451
+ <div className={styles.imageThumbnails} onClick={onClick}>
452
+ {displayImages.map(renderImageItem)}
453
+ </div>
454
+ );
455
+ });
456
+
457
+ ImageThumbnails.displayName = 'ImageThumbnails';
458
+
459
+ interface SingleImageDisplayProps {
460
+ imageUrl: string;
461
+ onClick?: () => void;
462
+ onDelete?: () => void;
463
+ }
464
+
465
+ const SingleImageDisplay: FC<SingleImageDisplayProps> = memo(({ imageUrl, onClick, onDelete }) => {
466
+ const { styles } = useStyles();
467
+ const { t } = useTranslation('components');
468
+
469
+ const handleDelete = (event: React.MouseEvent) => {
470
+ event.stopPropagation(); // Prevent triggering container click
471
+ onDelete?.();
472
+ };
473
+
474
+ const handleOverlayClick = (event: React.MouseEvent) => {
475
+ event.stopPropagation(); // Prevent triggering container click
476
+ onClick?.();
477
+ };
478
+
479
+ return (
480
+ <div className={styles.singleImageDisplay}>
481
+ <img
482
+ alt="Uploaded image"
483
+ src={imageUrl}
484
+ style={{ height: '100%', objectFit: 'cover', width: '100%' }}
485
+ />
486
+
487
+ {/* Delete button */}
488
+ <div className={`${styles.deleteIcon} delete-icon`} onClick={handleDelete}>
489
+ <X size={12} />
490
+ </div>
491
+
492
+ {/* Upload more overlay */}
493
+ <div
494
+ className={`${styles.uploadMoreOverlay} upload-more-overlay`}
495
+ onClick={handleOverlayClick}
496
+ >
497
+ <button className={styles.uploadMoreButton} type="button">
498
+ {t('MultiImagesUpload.actions.uploadMore')}
499
+ </button>
500
+ </div>
501
+ </div>
502
+ );
503
+ });
504
+
505
+ SingleImageDisplay.displayName = 'SingleImageDisplay';
506
+
507
+ // ======== Main Component ======== //
508
+
509
+ const MultiImagesUpload: FC<MultiImagesUploadProps> = memo(
510
+ ({ value, onChange, style, className }) => {
511
+ const inputRef = useRef<HTMLInputElement>(null);
512
+ const uploadWithProgress = useFileStore((s) => s.uploadWithProgress);
513
+ const [displayItems, setDisplayItems] = useState<DisplayItem[]>([]);
514
+ const [modalOpen, setModalOpen] = useState(false);
515
+
516
+ // Cleanup blob URLs to prevent memory leaks
517
+ useEffect(() => {
518
+ return () => {
519
+ // Cleanup function: revoke all blob URLs when component unmounts or displayItems change
520
+ displayItems.forEach((item) => {
521
+ if (item.file && isLocalBlobUrl(item.url)) {
522
+ URL.revokeObjectURL(item.url);
523
+ }
524
+ });
525
+ };
526
+ }, [displayItems]);
527
+
528
+ const handlePlaceholderClick = () => {
529
+ inputRef.current?.click();
530
+ };
531
+
532
+ const handleOpenModal = () => {
533
+ setModalOpen(true);
534
+ };
535
+
536
+ const handleCloseModal = () => {
537
+ setModalOpen(false);
538
+ };
539
+
540
+ const handleDelete = (index: number) => {
541
+ if (!value) return;
542
+ const newUrls = value.filter((_, i) => i !== index);
543
+ onChange?.(newUrls);
544
+ };
545
+
546
+ const handleFilesSelected = async (files: File[], baseUrls?: string[]) => {
547
+ if (files.length === 0) return;
548
+
549
+ const currentUrls = baseUrls !== undefined ? baseUrls : value || [];
550
+
551
+ // Create initial display items with blob URLs for immediate preview
552
+ const newDisplayItems: DisplayItem[] = files.map((file) => ({
553
+ file,
554
+ id: `${Date.now()}-${file.name}`,
555
+ progress: 0,
556
+ status: 'pending' as FileUploadStatus,
557
+ url: URL.createObjectURL(file),
558
+ }));
559
+
560
+ setDisplayItems((prev) => [...prev, ...newDisplayItems]);
561
+
562
+ // Start uploading files
563
+ const uploadPromises = newDisplayItems.map((item) =>
564
+ uploadWithProgress({
565
+ file: item.file!,
566
+ // Skip file type check for images
567
+ onStatusUpdate: (updateData) => {
568
+ if (updateData.type === 'updateFile') {
569
+ setDisplayItems((prev) =>
570
+ prev.map((displayItem) =>
571
+ displayItem.id === item.id
572
+ ? {
573
+ ...displayItem,
574
+ error: updateData.value.status === 'error' ? 'Upload failed' : undefined,
575
+ progress: updateData.value.uploadState?.progress || 0,
576
+ status: updateData.value.status,
577
+ }
578
+ : displayItem,
579
+ ),
580
+ );
581
+ } else if (updateData.type === 'removeFile') {
582
+ setDisplayItems((prev) => prev.filter((displayItem) => displayItem.id !== item.id));
583
+ }
584
+ },
585
+ skipCheckFileType: true,
586
+ }),
587
+ );
588
+
589
+ // Wait for all uploads to complete and collect successful URLs
590
+ const uploadResults = await Promise.allSettled(uploadPromises);
591
+ const successfulUrls: string[] = [];
592
+
593
+ uploadResults.forEach((result, index) => {
594
+ const displayItem = newDisplayItems[index];
595
+
596
+ if (result.status === 'fulfilled' && result.value) {
597
+ successfulUrls.push(result.value.url);
598
+
599
+ // Update display item with final URL and success status
600
+ setDisplayItems((prev) =>
601
+ prev.map((item) =>
602
+ item.id === displayItem.id
603
+ ? {
604
+ ...item,
605
+ file: undefined,
606
+ progress: 100,
607
+ status: 'success',
608
+ url: result.value!.url, // Clear file reference after successful upload
609
+ }
610
+ : item,
611
+ ),
612
+ );
613
+ } else {
614
+ // Handle upload failure
615
+ setDisplayItems((prev) =>
616
+ prev.map((item) =>
617
+ item.id === displayItem.id
618
+ ? {
619
+ ...item,
620
+ error: 'Upload failed',
621
+ progress: 0,
622
+ status: 'error',
623
+ }
624
+ : item,
625
+ ),
626
+ );
627
+ }
628
+
629
+ // Clean up blob URL regardless of success or failure
630
+ if (isLocalBlobUrl(displayItem.url)) {
631
+ URL.revokeObjectURL(displayItem.url);
632
+ }
633
+ });
634
+
635
+ // Update parent component with new URLs
636
+ if (successfulUrls.length > 0) {
637
+ const updatedUrls = [...currentUrls, ...successfulUrls];
638
+ onChange?.(updatedUrls);
639
+ }
640
+
641
+ // Clear display items after all uploads complete
642
+ setTimeout(() => {
643
+ setDisplayItems([]);
644
+ }, 1000); // Show success state for 1 second before clearing
645
+ };
646
+
647
+ const handleFilesChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
648
+ const files = event.target.files;
649
+ if (!files || files.length === 0) return;
650
+
651
+ await handleFilesSelected(Array.from(files));
652
+ };
653
+
654
+ // 处理 Modal 完成回调
655
+ const handleModalComplete = async (imageItems: ImageItem[]) => {
656
+ // 分离现有URL和新文件
657
+ const existingUrls = imageItems.filter((item) => item.url).map((item) => item.url!);
658
+
659
+ const newFiles = imageItems.filter((item) => item.file).map((item) => item.file!);
660
+
661
+ // 立即更新现有URL(删除的图片会被过滤掉)
662
+ onChange?.(existingUrls);
663
+
664
+ // 如果有新文件需要上传,基于 existingUrls 启动上传流程
665
+ if (newFiles.length > 0) {
666
+ await handleFilesSelected(newFiles, existingUrls);
667
+ }
668
+ };
669
+
670
+ // Calculate progress data
671
+ const totalFiles = displayItems.length;
672
+ const completedFiles = displayItems.filter((item) => (item.progress || 0) >= 100).length;
673
+ const overallProgress =
674
+ totalFiles > 0
675
+ ? displayItems.reduce((sum, item) => sum + (item.progress || 0), 0) / totalFiles
676
+ : 0;
677
+
678
+ const hasImages = value && value.length > 0;
679
+ const isUploading = displayItems.length > 0;
680
+ const isSingleImage = value && value.length === 1;
681
+
682
+ return (
683
+ <div className={className} style={style}>
684
+ {/* Hidden file input */}
685
+ <input
686
+ accept="image/*"
687
+ multiple
688
+ onChange={handleFilesChange}
689
+ onClick={(e) => {
690
+ // Reset value to allow re-selecting the same file
691
+ e.currentTarget.value = '';
692
+ }}
693
+ ref={inputRef}
694
+ style={{ display: 'none' }}
695
+ type="file"
696
+ />
697
+
698
+ {/* Conditional rendering based on state */}
699
+ {isUploading ? (
700
+ <ImageUploadProgress
701
+ completedCount={completedFiles}
702
+ currentProgress={overallProgress}
703
+ showCount={totalFiles > 1}
704
+ totalCount={totalFiles}
705
+ />
706
+ ) : isSingleImage ? (
707
+ <SingleImageDisplay
708
+ imageUrl={value[0]}
709
+ onClick={handleOpenModal}
710
+ onDelete={() => handleDelete(0)}
711
+ />
712
+ ) : hasImages ? (
713
+ <ImageThumbnails images={value || []} onClick={handleOpenModal} onDelete={handleDelete} />
714
+ ) : (
715
+ <ImageUploadPlaceholder onClick={handlePlaceholderClick} />
716
+ )}
717
+
718
+ {/* Image Management Modal */}
719
+ <ImageManageModal
720
+ images={value || []}
721
+ onClose={handleCloseModal}
722
+ onComplete={handleModalComplete}
723
+ open={modalOpen}
724
+ />
725
+ </div>
726
+ );
727
+ },
728
+ );
729
+
730
+ MultiImagesUpload.displayName = 'MultiImagesUpload';
731
+
732
+ export default MultiImagesUpload;