@lobehub/chat 1.98.2 → 1.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +27 -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 +9 -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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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 +15 -0
  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,411 @@
1
+ // @vitest-environment node
2
+ import { eq } from 'drizzle-orm/expressions';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { LobeChatDatabase } from '@/database/type';
6
+ import { FileService } from '@/server/services/file';
7
+ import { ImageGenerationTopic } from '@/types/generation';
8
+
9
+ import { generationBatches, generationTopics, generations, users } from '../../schemas';
10
+ import { GenerationTopicItem } from '../../schemas/generation';
11
+ import { GenerationTopicModel } from '../generationTopic';
12
+ import { getTestDB } from './_util';
13
+
14
+ // Mock FileService
15
+ const mockGetFullFileUrl = vi.fn();
16
+ vi.mock('@/server/services/file', () => ({
17
+ FileService: vi.fn().mockImplementation(() => ({
18
+ getFullFileUrl: mockGetFullFileUrl,
19
+ })),
20
+ }));
21
+
22
+ const serverDB: LobeChatDatabase = await getTestDB();
23
+
24
+ const userId = 'generation-topic-test-user';
25
+ const otherUserId = 'other-user';
26
+ const generationTopicModel = new GenerationTopicModel(serverDB, userId);
27
+
28
+ beforeEach(async () => {
29
+ await serverDB.delete(users);
30
+ await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
31
+
32
+ // Reset mocks before each test
33
+ vi.clearAllMocks();
34
+ mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
35
+ });
36
+
37
+ afterEach(async () => {
38
+ await serverDB.delete(users);
39
+ });
40
+
41
+ describe('GenerationTopicModel', () => {
42
+ describe('create', () => {
43
+ it('should create a new generation topic', async () => {
44
+ const title = 'Test Generation Topic';
45
+
46
+ const result = await generationTopicModel.create(title);
47
+
48
+ expect(result.id).toBeDefined();
49
+ expect(result.title).toBe(title);
50
+ expect(result.userId).toBe(userId);
51
+ expect(result.coverUrl).toBeNull();
52
+ expect(result.createdAt).toBeInstanceOf(Date);
53
+ expect(result.updatedAt).toBeInstanceOf(Date);
54
+
55
+ // Verify it's saved in database
56
+ const topic = await serverDB.query.generationTopics.findFirst({
57
+ where: eq(generationTopics.id, result.id),
58
+ });
59
+ expect(topic).toMatchObject({ title, userId });
60
+ });
61
+
62
+ it('should create a topic with empty title', async () => {
63
+ const result = await generationTopicModel.create('');
64
+
65
+ expect(result.id).toBeDefined();
66
+ expect(result.title).toBe('');
67
+ expect(result.userId).toBe(userId);
68
+ });
69
+ });
70
+
71
+ describe('queryAll', () => {
72
+ it('should return all topics for the user ordered by updatedAt desc', async () => {
73
+ // Create test data with different timestamps
74
+ const now = new Date();
75
+ const earlier = new Date(now.getTime() - 60000); // 1 minute earlier
76
+ const earliest = new Date(now.getTime() - 120000); // 2 minutes earlier
77
+
78
+ await serverDB.insert(generationTopics).values([
79
+ {
80
+ id: 'topic1',
81
+ userId,
82
+ title: 'Topic 1',
83
+ updatedAt: earliest,
84
+ },
85
+ {
86
+ id: 'topic2',
87
+ userId,
88
+ title: 'Topic 2',
89
+ updatedAt: now,
90
+ },
91
+ {
92
+ id: 'topic3',
93
+ userId,
94
+ title: 'Topic 3',
95
+ updatedAt: earlier,
96
+ },
97
+ {
98
+ id: 'topic4',
99
+ userId: otherUserId,
100
+ title: 'Other User Topic',
101
+ updatedAt: now,
102
+ },
103
+ ]);
104
+
105
+ const result = await generationTopicModel.queryAll();
106
+
107
+ // Should return only topics for current user, ordered by updatedAt desc
108
+ expect(result).toHaveLength(3);
109
+ expect(result[0].id).toBe('topic2'); // most recent
110
+ expect(result[1].id).toBe('topic3'); // middle
111
+ expect(result[2].id).toBe('topic1'); // oldest
112
+ });
113
+
114
+ it('should process cover URLs through FileService', async () => {
115
+ await serverDB.insert(generationTopics).values([
116
+ {
117
+ id: 'topic1',
118
+ userId,
119
+ title: 'Topic with cover',
120
+ coverUrl: 'cover-image-key',
121
+ },
122
+ {
123
+ id: 'topic2',
124
+ userId,
125
+ title: 'Topic without cover',
126
+ coverUrl: null,
127
+ },
128
+ ]);
129
+
130
+ const result = await generationTopicModel.queryAll();
131
+
132
+ expect(result).toHaveLength(2);
133
+ expect(result[0].coverUrl).toBe('https://example.com/cover-image-key');
134
+ expect(result[1].coverUrl).toBeNull();
135
+
136
+ // Verify FileService was called for the topic with coverUrl
137
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('cover-image-key');
138
+ expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
139
+ });
140
+
141
+ it('should return empty array if no topics exist', async () => {
142
+ const result = await generationTopicModel.queryAll();
143
+ expect(result).toHaveLength(0);
144
+ });
145
+ });
146
+
147
+ describe('update', () => {
148
+ it('should update a generation topic', async () => {
149
+ // Create a test topic
150
+ const { id } = await generationTopicModel.create('Original Title');
151
+
152
+ const updateData: Partial<ImageGenerationTopic> = {
153
+ title: 'Updated Title',
154
+ coverUrl: 'new-cover-key',
155
+ };
156
+
157
+ const result = await generationTopicModel.update(id, updateData);
158
+
159
+ expect(result).toBeDefined();
160
+ expect(result!.id).toBe(id);
161
+ expect(result!.title).toBe('Updated Title');
162
+ expect(result!.coverUrl).toBe('new-cover-key');
163
+ expect(result!.updatedAt).toBeInstanceOf(Date);
164
+
165
+ // Verify in database
166
+ const updatedTopic = await serverDB.query.generationTopics.findFirst({
167
+ where: eq(generationTopics.id, id),
168
+ });
169
+ expect(updatedTopic).toMatchObject(updateData);
170
+ });
171
+
172
+ it('should not update topics of other users', async () => {
173
+ // Create a topic for another user
174
+ const [otherUserTopic] = await serverDB
175
+ .insert(generationTopics)
176
+ .values({ id: 'other-topic', userId: otherUserId, title: 'Other User Topic' })
177
+ .returning();
178
+
179
+ const updateData: Partial<ImageGenerationTopic> = {
180
+ title: 'Hacked Title',
181
+ };
182
+
183
+ // Attempt to update should not affect other user's topic
184
+ const result = await generationTopicModel.update(otherUserTopic.id, updateData);
185
+
186
+ // Should return undefined or empty result because of user permission check
187
+ expect(result).toBeUndefined();
188
+
189
+ // Verify the topic remains unchanged in the database
190
+ const unchangedTopic = await serverDB.query.generationTopics.findFirst({
191
+ where: eq(generationTopics.id, otherUserTopic.id),
192
+ });
193
+ expect(unchangedTopic?.title).toBe('Other User Topic');
194
+ });
195
+
196
+ it('should update only specified fields', async () => {
197
+ const { id } = await generationTopicModel.create('Original Title');
198
+
199
+ // Update only title
200
+ const result = await generationTopicModel.update(id, { title: 'Only Title Updated' });
201
+
202
+ expect(result).toBeDefined();
203
+ expect(result!.title).toBe('Only Title Updated');
204
+ expect(result!.coverUrl).toBeNull(); // Should remain unchanged
205
+ });
206
+ });
207
+
208
+ describe('delete', () => {
209
+ it('should delete a generation topic', async () => {
210
+ const { id } = await generationTopicModel.create('Topic to Delete');
211
+
212
+ const result = await generationTopicModel.delete(id);
213
+
214
+ expect(result).toBeDefined();
215
+ const deleteResult = result!;
216
+ expect(deleteResult.deletedTopic.id).toBe(id);
217
+ expect(deleteResult.deletedTopic.title).toBe('Topic to Delete');
218
+ expect(deleteResult.filesToDelete).toEqual([]);
219
+
220
+ // Verify it's deleted from database
221
+ const deletedTopic = await serverDB.query.generationTopics.findFirst({
222
+ where: eq(generationTopics.id, id),
223
+ });
224
+ expect(deletedTopic).toBeUndefined();
225
+ });
226
+
227
+ it('should not delete topics of other users', async () => {
228
+ // Create a topic for another user
229
+ const [otherUserTopic] = await serverDB
230
+ .insert(generationTopics)
231
+ .values({ id: 'other-topic', userId: otherUserId, title: 'Other User Topic' })
232
+ .returning();
233
+
234
+ // Attempt to delete should not affect other user's topic
235
+ const result = await generationTopicModel.delete(otherUserTopic.id);
236
+
237
+ // Should return undefined because of user permission check
238
+ expect(result).toBeUndefined();
239
+
240
+ // The topic should still exist in the database
241
+ const stillExistsTopic = await serverDB.query.generationTopics.findFirst({
242
+ where: eq(generationTopics.id, otherUserTopic.id),
243
+ });
244
+ expect(stillExistsTopic).toBeDefined();
245
+ expect(stillExistsTopic?.title).toBe('Other User Topic');
246
+ });
247
+
248
+ it('should return deleted topic data', async () => {
249
+ const { id } = await generationTopicModel.create('Topic with Data');
250
+
251
+ // Add some data to the topic
252
+ await generationTopicModel.update(id, {
253
+ title: 'Updated Topic',
254
+ coverUrl: 'cover-key',
255
+ });
256
+
257
+ const result = await generationTopicModel.delete(id);
258
+
259
+ expect(result).toBeDefined();
260
+ const deleteResult = result!;
261
+ expect(deleteResult.deletedTopic).toMatchObject({
262
+ id,
263
+ title: 'Updated Topic',
264
+ coverUrl: 'cover-key',
265
+ userId,
266
+ });
267
+ expect(deleteResult.filesToDelete).toContain('cover-key');
268
+ });
269
+
270
+ it('should return undefined when trying to delete non-existent topic', async () => {
271
+ const nonExistentId = 'non-existent-topic-id';
272
+
273
+ const result = await generationTopicModel.delete(nonExistentId);
274
+
275
+ // Should return undefined because topic doesn't exist
276
+ expect(result).toBeUndefined();
277
+ });
278
+
279
+ it('should return undefined when trying to delete topic with invalid format id', async () => {
280
+ const invalidId = 'invalid-format-id';
281
+
282
+ const result = await generationTopicModel.delete(invalidId);
283
+
284
+ // Should return undefined because topic doesn't exist with this invalid ID
285
+ expect(result).toBeUndefined();
286
+ });
287
+
288
+ it('should collect file URLs from batches and generations when deleting topic with data', async () => {
289
+ // Create a topic with cover image
290
+ const { id: topicId } = await generationTopicModel.create(
291
+ 'Topic with batches and generations',
292
+ );
293
+ await generationTopicModel.update(topicId, { coverUrl: 'topic-cover.jpg' });
294
+
295
+ // Create a generation batch associated with this topic
296
+ const [batch] = await serverDB
297
+ .insert(generationBatches)
298
+ .values({
299
+ userId,
300
+ generationTopicId: topicId,
301
+ provider: 'test-provider',
302
+ model: 'test-model',
303
+ prompt: 'Test generation prompt',
304
+ width: 1024,
305
+ height: 1024,
306
+ })
307
+ .returning();
308
+
309
+ // Create generations with asset data containing thumbnail URLs
310
+ await serverDB.insert(generations).values([
311
+ {
312
+ userId,
313
+ generationBatchId: batch.id,
314
+ asyncTaskId: null,
315
+ fileId: null,
316
+ seed: 12345,
317
+ asset: {
318
+ type: 'image',
319
+ thumbnailUrl: 'thumbnail1.jpg',
320
+ originalUrl: 'original1.jpg',
321
+ width: 1024,
322
+ height: 1024,
323
+ },
324
+ },
325
+ {
326
+ userId,
327
+ generationBatchId: batch.id,
328
+ asyncTaskId: null,
329
+ fileId: null,
330
+ seed: 12346,
331
+ asset: {
332
+ type: 'image',
333
+ thumbnailUrl: 'thumbnail2.jpg',
334
+ originalUrl: 'original2.jpg',
335
+ width: 1024,
336
+ height: 1024,
337
+ },
338
+ },
339
+ ]);
340
+
341
+ // Now delete the topic - this should collect all file URLs from cover + generations
342
+ const result = await generationTopicModel.delete(topicId);
343
+
344
+ expect(result).toBeDefined();
345
+ const deleteResult = result!;
346
+ expect(deleteResult.deletedTopic.id).toBe(topicId);
347
+
348
+ // Should collect cover URL and thumbnail URLs from generations (lines 111-117)
349
+ expect(deleteResult.filesToDelete).toContain('topic-cover.jpg');
350
+ expect(deleteResult.filesToDelete).toContain('thumbnail1.jpg');
351
+ expect(deleteResult.filesToDelete).toContain('thumbnail2.jpg');
352
+ expect(deleteResult.filesToDelete).toHaveLength(3);
353
+
354
+ // Verify topic is actually deleted from database
355
+ const deletedTopic = await serverDB.query.generationTopics.findFirst({
356
+ where: eq(generationTopics.id, topicId),
357
+ });
358
+ expect(deletedTopic).toBeUndefined();
359
+ });
360
+ });
361
+
362
+ describe('user isolation', () => {
363
+ it('should only operate on topics belonging to the user', async () => {
364
+ // Create topics for different users
365
+ await serverDB.insert(generationTopics).values([
366
+ { id: 'user1-topic1', userId, title: 'User 1 Topic 1' },
367
+ { id: 'user1-topic2', userId, title: 'User 1 Topic 2' },
368
+ { id: 'user2-topic1', userId: otherUserId, title: 'User 2 Topic 1' },
369
+ ]);
370
+
371
+ const result = await generationTopicModel.queryAll();
372
+
373
+ // Should only return topics for the current user
374
+ expect(result).toHaveLength(2);
375
+ expect(result.every((topic) => topic.userId === userId)).toBe(true);
376
+ expect(result.some((topic) => topic.title === 'User 2 Topic 1')).toBe(false);
377
+ });
378
+ });
379
+
380
+ describe('edge cases', () => {
381
+ it('should handle topics with null titles', async () => {
382
+ await serverDB.insert(generationTopics).values({
383
+ id: 'null-title-topic',
384
+ userId,
385
+ title: null,
386
+ });
387
+
388
+ const result = await generationTopicModel.queryAll();
389
+
390
+ expect(result).toHaveLength(1);
391
+ expect(result[0].title).toBeNull();
392
+ });
393
+
394
+ it('should handle topics with null coverUrl', async () => {
395
+ const { id } = await generationTopicModel.create('Topic');
396
+
397
+ const result = await generationTopicModel.update(id, { coverUrl: null });
398
+
399
+ expect(result).toBeDefined();
400
+ expect(result!.coverUrl).toBeNull();
401
+ });
402
+
403
+ it('should return undefined when updating non-existent topic', async () => {
404
+ const nonExistentId = 'non-existent-topic';
405
+
406
+ const updateResult = await generationTopicModel.update(nonExistentId, { title: 'New Title' });
407
+
408
+ expect(updateResult).toBeUndefined();
409
+ });
410
+ });
411
+ });
@@ -67,6 +67,7 @@ export class AiModelModel {
67
67
  displayName: aiModels.displayName,
68
68
  enabled: aiModels.enabled,
69
69
  id: aiModels.id,
70
+ parameters: aiModels.parameters,
70
71
  pricing: aiModels.pricing,
71
72
  releasedAt: aiModels.releasedAt,
72
73
  source: aiModels.source,
@@ -93,6 +94,7 @@ export class AiModelModel {
93
94
  displayName: aiModels.displayName,
94
95
  enabled: aiModels.enabled,
95
96
  id: aiModels.id,
97
+ parameters: aiModels.parameters,
96
98
  providerId: aiModels.providerId,
97
99
  sort: aiModels.sort,
98
100
  source: aiModels.source,
@@ -82,7 +82,7 @@ export class AsyncTaskModel {
82
82
  .set({
83
83
  error: new AsyncTaskError(
84
84
  AsyncTaskErrorType.Timeout,
85
- 'chunking task is timeout, please try again',
85
+ 'task is timeout, please try again',
86
86
  ),
87
87
  status: AsyncTaskStatus.Error,
88
88
  })
@@ -2,7 +2,7 @@ import { count, sum } from 'drizzle-orm';
2
2
  import { and, asc, desc, eq, ilike, inArray, like, notExists, or } from 'drizzle-orm/expressions';
3
3
  import type { PgTransaction } from 'drizzle-orm/pg-core';
4
4
 
5
- import { LobeChatDatabase } from '@/database/type';
5
+ import { LobeChatDatabase, Transaction } from '@/database/type';
6
6
  import { FilesTabs, QueryFileListParams, SortType } from '@/types/files';
7
7
 
8
8
  import {
@@ -29,10 +29,11 @@ export class FileModel {
29
29
  create = async (
30
30
  params: Omit<NewFile, 'id' | 'userId'> & { knowledgeBaseId?: string },
31
31
  insertToGlobalFiles?: boolean,
32
+ trx?: Transaction,
32
33
  ) => {
33
- const result = await this.db.transaction(async (trx) => {
34
+ const executeInTransaction = async (tx: Transaction) => {
34
35
  if (insertToGlobalFiles) {
35
- await trx.insert(globalFiles).values({
36
+ await tx.insert(globalFiles).values({
36
37
  creator: this.userId,
37
38
  fileType: params.fileType,
38
39
  hashId: params.fileHash!,
@@ -42,7 +43,7 @@ export class FileModel {
42
43
  });
43
44
  }
44
45
 
45
- const result = await trx
46
+ const result = await tx
46
47
  .insert(files)
47
48
  .values({ ...params, userId: this.userId })
48
49
  .returning();
@@ -50,7 +51,7 @@ export class FileModel {
50
51
  const item = result[0];
51
52
 
52
53
  if (params.knowledgeBaseId) {
53
- await trx.insert(knowledgeBaseFiles).values({
54
+ await tx.insert(knowledgeBaseFiles).values({
54
55
  fileId: item.id,
55
56
  knowledgeBaseId: params.knowledgeBaseId,
56
57
  userId: this.userId,
@@ -58,8 +59,11 @@ export class FileModel {
58
59
  }
59
60
 
60
61
  return item;
61
- });
62
+ };
62
63
 
64
+ const result = await (trx
65
+ ? executeInTransaction(trx)
66
+ : this.db.transaction(executeInTransaction));
63
67
  return { id: result.id };
64
68
  };
65
69
 
@@ -82,20 +86,21 @@ export class FileModel {
82
86
  };
83
87
  };
84
88
 
85
- delete = async (id: string, removeGlobalFile: boolean = true) => {
86
- const file = await this.findById(id);
87
- if (!file) return;
89
+ delete = async (id: string, removeGlobalFile: boolean = true, trx?: Transaction) => {
90
+ const executeInTransaction = async (tx: Transaction) => {
91
+ // pglite 环境下不能再 transaction 中使用非事务操作,会阻塞住
92
+ const file = await this.findById(id, tx);
93
+ if (!file) return;
88
94
 
89
- const fileHash = file.fileHash!;
95
+ const fileHash = file.fileHash!;
90
96
 
91
- return await this.db.transaction(async (trx) => {
92
- // 1. 删除相关的 chunks
93
- await this.deleteFileChunks(trx as any, [id]);
97
+ // 2. Delete related chunks
98
+ await this.deleteFileChunks(tx as any, [id]);
94
99
 
95
- // 2. 删除文件记录
96
- await trx.delete(files).where(and(eq(files.id, id), eq(files.userId, this.userId)));
100
+ // 3. Delete file record
101
+ await tx.delete(files).where(and(eq(files.id, id), eq(files.userId, this.userId)));
97
102
 
98
- const result = await trx
103
+ const result = await tx
99
104
  .select({ count: count() })
100
105
  .from(files)
101
106
  .where(and(eq(files.fileHash, fileHash)));
@@ -105,11 +110,13 @@ export class FileModel {
105
110
  // delete the file from global file if it is not used by other files
106
111
  // if `DISABLE_REMOVE_GLOBAL_FILE` is true, we will not remove the global file
107
112
  if (fileCount === 0 && removeGlobalFile) {
108
- await trx.delete(globalFiles).where(eq(globalFiles.hashId, fileHash));
113
+ await tx.delete(globalFiles).where(eq(globalFiles.hashId, fileHash));
109
114
 
110
115
  return file;
111
116
  }
112
- });
117
+ };
118
+
119
+ return await (trx ? executeInTransaction(trx) : this.db.transaction(executeInTransaction));
113
120
  };
114
121
 
115
122
  deleteGlobalFile = async (hashId: string) => {
@@ -261,8 +268,9 @@ export class FileModel {
261
268
  });
262
269
  };
263
270
 
264
- findById = async (id: string) => {
265
- return this.db.query.files.findFirst({
271
+ findById = async (id: string, trx?: Transaction) => {
272
+ const database = trx || this.db;
273
+ return database.query.files.findFirst({
266
274
  where: and(eq(files.id, id), eq(files.userId, this.userId)),
267
275
  });
268
276
  };