@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,614 @@
1
+ // @vitest-environment node
2
+ import { eq } from 'drizzle-orm';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { LobeChatDatabase } from '@/database/type';
6
+ import { AsyncTaskStatus } from '@/types/asyncTask';
7
+ import { GenerationConfig } from '@/types/generation';
8
+
9
+ import {
10
+ NewGenerationBatch,
11
+ generationBatches,
12
+ generationTopics,
13
+ generations,
14
+ users,
15
+ } from '../../schemas';
16
+ import { GenerationBatchModel } from '../generationBatch';
17
+ import { getTestDB } from './_util';
18
+
19
+ const serverDB: LobeChatDatabase = await getTestDB();
20
+
21
+ // Mock FileService
22
+ const mockGetFullFileUrl = vi.fn();
23
+ vi.mock('@/server/services/file', () => ({
24
+ FileService: vi.fn().mockImplementation(() => ({
25
+ getFullFileUrl: mockGetFullFileUrl,
26
+ })),
27
+ }));
28
+
29
+ // Mock GenerationModel
30
+ const mockTransformGeneration = vi.fn();
31
+ vi.mock('../generation', () => ({
32
+ GenerationModel: vi.fn().mockImplementation(() => ({
33
+ transformGeneration: mockTransformGeneration,
34
+ })),
35
+ }));
36
+
37
+ const userId = 'generation-batch-test-user-id';
38
+ const otherUserId = 'other-user-id';
39
+ const generationBatchModel = new GenerationBatchModel(serverDB, userId);
40
+
41
+ // Test data
42
+ const testTopic = {
43
+ id: 'test-topic-id',
44
+ userId,
45
+ title: 'Test Generation Topic',
46
+ coverUrl: null,
47
+ };
48
+
49
+ const testBatch: NewGenerationBatch = {
50
+ userId, // Required by schema, but will be overridden by model
51
+ generationTopicId: 'test-topic-id',
52
+ provider: 'test-provider',
53
+ model: 'test-model',
54
+ prompt: 'Test prompt for image generation',
55
+ width: 1024,
56
+ height: 1024,
57
+ config: {
58
+ prompt: 'Test prompt for image generation',
59
+ imageUrls: ['image1.jpg', 'image2.jpg'],
60
+ width: 1024,
61
+ height: 1024,
62
+ } as GenerationConfig,
63
+ };
64
+
65
+ const testGeneration = {
66
+ id: 'test-gen-id',
67
+ generationBatchId: 'test-batch-id',
68
+ asyncTaskId: null, // Use null instead of invalid foreign key
69
+ fileId: null, // Use null instead of invalid foreign key
70
+ seed: 12345,
71
+ asset: {
72
+ type: 'image',
73
+ url: 'asset-url.jpg',
74
+ thumbnailUrl: 'thumbnail-url.jpg',
75
+ width: 1024,
76
+ height: 1024,
77
+ },
78
+ userId,
79
+ };
80
+
81
+ beforeEach(async () => {
82
+ // Clear all mocks
83
+ vi.clearAllMocks();
84
+
85
+ // Setup mock return values
86
+ mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
87
+ mockTransformGeneration.mockResolvedValue({
88
+ id: testGeneration.id,
89
+ asset: {
90
+ url: 'https://example.com/asset-url.jpg',
91
+ thumbnailUrl: 'https://example.com/thumbnail-url.jpg',
92
+ width: 1024,
93
+ height: 1024,
94
+ },
95
+ seed: testGeneration.seed,
96
+ createdAt: new Date(),
97
+ asyncTaskId: testGeneration.asyncTaskId,
98
+ task: {
99
+ id: testGeneration.asyncTaskId,
100
+ status: AsyncTaskStatus.Success,
101
+ },
102
+ });
103
+
104
+ // Clear database and create test users
105
+ await serverDB.delete(users);
106
+ await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
107
+
108
+ // Create test topic
109
+ await serverDB.insert(generationTopics).values(testTopic);
110
+ });
111
+
112
+ afterEach(async () => {
113
+ // Clean up database
114
+ await serverDB.delete(users);
115
+ });
116
+
117
+ describe('GenerationBatchModel', () => {
118
+ describe('create', () => {
119
+ it('should create a new generation batch', async () => {
120
+ const result = await generationBatchModel.create(testBatch);
121
+
122
+ expect(result.id).toBeDefined();
123
+ expect(result).toMatchObject({
124
+ ...testBatch,
125
+ userId,
126
+ });
127
+
128
+ // Verify in database
129
+ const dbBatch = await serverDB.query.generationBatches.findFirst({
130
+ where: eq(generationBatches.id, result.id),
131
+ });
132
+ expect(dbBatch).toMatchObject({
133
+ ...testBatch,
134
+ userId,
135
+ });
136
+ });
137
+
138
+ it('should automatically set userId when creating batch', async () => {
139
+ // Create batch data without userId to test auto-assignment
140
+ const batchWithoutUserId = {
141
+ generationTopicId: 'test-topic-id',
142
+ provider: 'test-provider',
143
+ model: 'test-model',
144
+ prompt: 'Test prompt for image generation',
145
+ width: 1024,
146
+ height: 1024,
147
+ config: {
148
+ prompt: 'Test prompt for image generation',
149
+ imageUrls: ['image1.jpg', 'image2.jpg'],
150
+ width: 1024,
151
+ height: 1024,
152
+ } as GenerationConfig,
153
+ };
154
+
155
+ const result = await generationBatchModel.create(batchWithoutUserId as NewGenerationBatch);
156
+
157
+ expect(result.userId).toBe(userId);
158
+ });
159
+ });
160
+
161
+ describe('findById', () => {
162
+ it('should find a generation batch by id', async () => {
163
+ const [createdBatch] = await serverDB
164
+ .insert(generationBatches)
165
+ .values({ ...testBatch, userId })
166
+ .returning();
167
+
168
+ const result = await generationBatchModel.findById(createdBatch.id);
169
+
170
+ expect(result).toMatchObject({
171
+ id: createdBatch.id,
172
+ ...testBatch,
173
+ userId,
174
+ });
175
+ });
176
+
177
+ it('should return undefined for non-existent batch', async () => {
178
+ const result = await generationBatchModel.findById('non-existent-id');
179
+ expect(result).toBeUndefined();
180
+ });
181
+
182
+ it('should NOT find batches from other users', async () => {
183
+ // Create batch for other user
184
+ const [otherUserBatch] = await serverDB
185
+ .insert(generationBatches)
186
+ .values({ ...testBatch, userId: otherUserId })
187
+ .returning();
188
+
189
+ const result = await generationBatchModel.findById(otherUserBatch.id);
190
+ expect(result).toBeUndefined();
191
+ });
192
+ });
193
+
194
+ describe('findByTopicId', () => {
195
+ it('should find batches by topic id', async () => {
196
+ // Create multiple batches for the topic with explicit timestamps
197
+ const oldDate = new Date('2024-01-01T10:00:00Z');
198
+ const newDate = new Date('2024-01-02T10:00:00Z');
199
+
200
+ const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
201
+ const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };
202
+
203
+ await serverDB.insert(generationBatches).values([batch1, batch2]);
204
+
205
+ const results = await generationBatchModel.findByTopicId(testTopic.id);
206
+
207
+ expect(results).toHaveLength(2);
208
+ expect(results[0].prompt).toBe('Second batch'); // Latest first (desc order)
209
+ expect(results[1].prompt).toBe('First batch');
210
+ });
211
+
212
+ it('should only return batches for current user', async () => {
213
+ // Create batches for both users
214
+ await serverDB.insert(generationBatches).values([
215
+ { ...testBatch, userId, prompt: 'User batch' },
216
+ { ...testBatch, userId: otherUserId, prompt: 'Other user batch' },
217
+ ]);
218
+
219
+ const results = await generationBatchModel.findByTopicId(testTopic.id);
220
+
221
+ expect(results).toHaveLength(1);
222
+ expect(results[0].prompt).toBe('User batch');
223
+ });
224
+
225
+ it('should return empty array when no batches exist for topic', async () => {
226
+ const results = await generationBatchModel.findByTopicId('non-existent-topic');
227
+ expect(results).toHaveLength(0);
228
+ });
229
+ });
230
+
231
+ describe('findByTopicIdWithGenerations', () => {
232
+ it('should find batches with their generations', async () => {
233
+ // Create batch
234
+ const [createdBatch] = await serverDB
235
+ .insert(generationBatches)
236
+ .values({ ...testBatch, userId })
237
+ .returning();
238
+
239
+ // Create generations for the batch
240
+ await serverDB.insert(generations).values([
241
+ { ...testGeneration, generationBatchId: createdBatch.id },
242
+ { ...testGeneration, id: 'gen2', generationBatchId: createdBatch.id },
243
+ ]);
244
+
245
+ const results = await generationBatchModel.findByTopicIdWithGenerations(testTopic.id);
246
+
247
+ expect(results).toHaveLength(1);
248
+ expect(results[0].generations).toHaveLength(2);
249
+ // Generations are ordered by createdAt ASC, then by id ASC
250
+ // Since both have same createdAt, 'gen2' comes before 'test-gen-id' alphabetically
251
+ expect(results[0].generations[0].id).toBe('gen2');
252
+ });
253
+
254
+ it('should order generations by createdAt and id', async () => {
255
+ const [createdBatch] = await serverDB
256
+ .insert(generationBatches)
257
+ .values({ ...testBatch, userId })
258
+ .returning();
259
+
260
+ // Create generations with different timestamps
261
+ const oldDate = new Date('2024-01-01');
262
+ const newDate = new Date('2024-01-02');
263
+
264
+ await serverDB.insert(generations).values([
265
+ {
266
+ ...testGeneration,
267
+ id: 'gen-new',
268
+ generationBatchId: createdBatch.id,
269
+ createdAt: newDate,
270
+ },
271
+ {
272
+ ...testGeneration,
273
+ id: 'gen-old',
274
+ generationBatchId: createdBatch.id,
275
+ createdAt: oldDate,
276
+ },
277
+ ]);
278
+
279
+ const results = await generationBatchModel.findByTopicIdWithGenerations(testTopic.id);
280
+
281
+ expect(results[0].generations[0].id).toBe('gen-old');
282
+ expect(results[0].generations[1].id).toBe('gen-new');
283
+ });
284
+
285
+ it('should NOT include generations from other users', async () => {
286
+ // Create batch for current user
287
+ const [userBatch] = await serverDB
288
+ .insert(generationBatches)
289
+ .values({ ...testBatch, userId })
290
+ .returning();
291
+
292
+ // Create batch for other user
293
+ const [otherBatch] = await serverDB
294
+ .insert(generationBatches)
295
+ .values({ ...testBatch, userId: otherUserId })
296
+ .returning();
297
+
298
+ // Create generations for both batches
299
+ await serverDB.insert(generations).values([
300
+ { ...testGeneration, generationBatchId: userBatch.id, userId },
301
+ {
302
+ ...testGeneration,
303
+ id: 'other-gen',
304
+ generationBatchId: otherBatch.id,
305
+ userId: otherUserId,
306
+ },
307
+ ]);
308
+
309
+ const results = await generationBatchModel.findByTopicIdWithGenerations(testTopic.id);
310
+
311
+ expect(results).toHaveLength(1);
312
+ expect(results[0].id).toBe(userBatch.id);
313
+ });
314
+ });
315
+
316
+ describe('queryGenerationBatchesByTopicIdWithGenerations', () => {
317
+ it('should return transformed batches with generations', async () => {
318
+ // Create batch
319
+ const [createdBatch] = await serverDB
320
+ .insert(generationBatches)
321
+ .values({ ...testBatch, userId })
322
+ .returning();
323
+
324
+ // Create generation
325
+ await serverDB
326
+ .insert(generations)
327
+ .values([{ ...testGeneration, generationBatchId: createdBatch.id }]);
328
+
329
+ const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
330
+ testTopic.id,
331
+ );
332
+
333
+ expect(results).toHaveLength(1);
334
+ expect(results[0]).toMatchObject({
335
+ id: createdBatch.id,
336
+ provider: testBatch.provider,
337
+ model: testBatch.model,
338
+ prompt: testBatch.prompt,
339
+ width: testBatch.width,
340
+ height: testBatch.height,
341
+ generations: expect.any(Array),
342
+ });
343
+
344
+ // Verify FileService was called for config imageUrls
345
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('image1.jpg');
346
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('image2.jpg');
347
+
348
+ // Verify GenerationModel.transformGeneration was called
349
+ expect(mockTransformGeneration).toHaveBeenCalledTimes(1);
350
+ });
351
+
352
+ it('should transform config imageUrls through FileService', async () => {
353
+ const [createdBatch] = await serverDB
354
+ .insert(generationBatches)
355
+ .values({
356
+ ...testBatch,
357
+ userId,
358
+ config: { imageUrls: ['url1.jpg', 'url2.jpg'] },
359
+ })
360
+ .returning();
361
+
362
+ const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
363
+ testTopic.id,
364
+ );
365
+
366
+ expect(results[0].config).toEqual({
367
+ imageUrls: ['https://example.com/url1.jpg', 'https://example.com/url2.jpg'],
368
+ });
369
+ });
370
+
371
+ it('should handle config without imageUrls', async () => {
372
+ const [createdBatch] = await serverDB
373
+ .insert(generationBatches)
374
+ .values({
375
+ ...testBatch,
376
+ userId,
377
+ config: { otherField: 'value' },
378
+ })
379
+ .returning();
380
+
381
+ const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
382
+ testTopic.id,
383
+ );
384
+
385
+ expect(results[0].config).toEqual({ otherField: 'value' });
386
+ expect(mockGetFullFileUrl).not.toHaveBeenCalled();
387
+ });
388
+
389
+ it('should return empty array when no batches exist', async () => {
390
+ const results =
391
+ await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
392
+ 'non-existent-topic',
393
+ );
394
+ expect(results).toHaveLength(0);
395
+ });
396
+
397
+ it('should handle batches without generations', async () => {
398
+ await serverDB.insert(generationBatches).values({ ...testBatch, userId });
399
+
400
+ const results = await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(
401
+ testTopic.id,
402
+ );
403
+
404
+ expect(results).toHaveLength(1);
405
+ expect(results[0].generations).toHaveLength(0);
406
+ });
407
+ });
408
+
409
+ describe('delete', () => {
410
+ it('should delete a generation batch and return batch data with thumbnail URLs', async () => {
411
+ const [createdBatch] = await serverDB
412
+ .insert(generationBatches)
413
+ .values({ ...testBatch, userId })
414
+ .returning();
415
+
416
+ // Create generation with thumbnail URL
417
+ await serverDB.insert(generations).values({
418
+ ...testGeneration,
419
+ generationBatchId: createdBatch.id,
420
+ asset: {
421
+ type: 'image',
422
+ url: 'asset-url.jpg',
423
+ thumbnailUrl: 'thumbnail-url.jpg',
424
+ width: 1024,
425
+ height: 1024,
426
+ },
427
+ });
428
+
429
+ const result = await generationBatchModel.delete(createdBatch.id);
430
+
431
+ // Verify return value structure
432
+ expect(result).toBeDefined();
433
+ expect(result!.deletedBatch).toMatchObject({
434
+ id: createdBatch.id,
435
+ ...testBatch,
436
+ userId,
437
+ });
438
+ expect(result!.thumbnailUrls).toEqual(['thumbnail-url.jpg']);
439
+
440
+ // Verify batch was actually deleted from database
441
+ const deletedBatch = await serverDB.query.generationBatches.findFirst({
442
+ where: eq(generationBatches.id, createdBatch.id),
443
+ });
444
+ expect(deletedBatch).toBeUndefined();
445
+ });
446
+
447
+ it('should collect multiple thumbnail URLs from multiple generations', async () => {
448
+ const [createdBatch] = await serverDB
449
+ .insert(generationBatches)
450
+ .values({ ...testBatch, userId })
451
+ .returning();
452
+
453
+ // Create multiple generations with different thumbnail URLs
454
+ await serverDB.insert(generations).values([
455
+ {
456
+ ...testGeneration,
457
+ id: 'gen1',
458
+ generationBatchId: createdBatch.id,
459
+ asset: {
460
+ type: 'image',
461
+ url: 'asset1.jpg',
462
+ thumbnailUrl: 'thumbnail1.jpg',
463
+ width: 1024,
464
+ height: 1024,
465
+ },
466
+ },
467
+ {
468
+ ...testGeneration,
469
+ id: 'gen2',
470
+ generationBatchId: createdBatch.id,
471
+ asset: {
472
+ type: 'image',
473
+ url: 'asset2.jpg',
474
+ thumbnailUrl: 'thumbnail2.jpg',
475
+ width: 1024,
476
+ height: 1024,
477
+ },
478
+ },
479
+ ]);
480
+
481
+ const result = await generationBatchModel.delete(createdBatch.id);
482
+
483
+ expect(result).toBeDefined();
484
+ expect(result!.thumbnailUrls).toHaveLength(2);
485
+ expect(result!.thumbnailUrls).toContain('thumbnail1.jpg');
486
+ expect(result!.thumbnailUrls).toContain('thumbnail2.jpg');
487
+ });
488
+
489
+ it('should return empty thumbnail URLs when no generations have thumbnails', async () => {
490
+ const [createdBatch] = await serverDB
491
+ .insert(generationBatches)
492
+ .values({ ...testBatch, userId })
493
+ .returning();
494
+
495
+ const result = await generationBatchModel.delete(createdBatch.id);
496
+
497
+ expect(result).toBeDefined();
498
+ expect(result!.deletedBatch.id).toBe(createdBatch.id);
499
+ expect(result!.thumbnailUrls).toEqual([]);
500
+ });
501
+
502
+ it('should return undefined when trying to delete non-existent batch', async () => {
503
+ const result = await generationBatchModel.delete('non-existent-id');
504
+ expect(result).toBeUndefined();
505
+ });
506
+
507
+ it('should return undefined when trying to delete batch from other user', async () => {
508
+ // Create batch for other user
509
+ const [otherUserBatch] = await serverDB
510
+ .insert(generationBatches)
511
+ .values({ ...testBatch, userId: otherUserId })
512
+ .returning();
513
+
514
+ // Try to delete using current user's model
515
+ const result = await generationBatchModel.delete(otherUserBatch.id);
516
+ expect(result).toBeUndefined();
517
+
518
+ // Verify batch still exists
519
+ const stillExists = await serverDB.query.generationBatches.findFirst({
520
+ where: eq(generationBatches.id, otherUserBatch.id),
521
+ });
522
+ expect(stillExists).toBeDefined();
523
+ });
524
+ });
525
+
526
+ describe('user isolation security tests', () => {
527
+ it('should enforce user data isolation across all methods', async () => {
528
+ // Create batches for both users with same topic
529
+ const userBatch = { ...testBatch, userId };
530
+ const otherUserBatch = { ...testBatch, userId: otherUserId };
531
+
532
+ const [userBatchCreated] = await serverDB
533
+ .insert(generationBatches)
534
+ .values(userBatch)
535
+ .returning();
536
+
537
+ const [otherUserBatchCreated] = await serverDB
538
+ .insert(generationBatches)
539
+ .values(otherUserBatch)
540
+ .returning();
541
+
542
+ // Test findById isolation
543
+ const foundUserBatch = await generationBatchModel.findById(userBatchCreated.id);
544
+ const foundOtherBatch = await generationBatchModel.findById(otherUserBatchCreated.id);
545
+
546
+ expect(foundUserBatch).toBeDefined();
547
+ expect(foundOtherBatch).toBeUndefined(); // Should not find other user's batch
548
+
549
+ // Test findByTopicId isolation
550
+ const topicBatches = await generationBatchModel.findByTopicId(testTopic.id);
551
+ expect(topicBatches).toHaveLength(1);
552
+ expect(topicBatches[0].id).toBe(userBatchCreated.id);
553
+
554
+ // Test delete isolation - should not affect other user's data
555
+ await generationBatchModel.delete(otherUserBatchCreated.id);
556
+ const otherUserBatchStillExists = await serverDB.query.generationBatches.findFirst({
557
+ where: eq(generationBatches.id, otherUserBatchCreated.id),
558
+ });
559
+ expect(otherUserBatchStillExists).toBeDefined(); // Should not be deleted
560
+ });
561
+ });
562
+
563
+ describe('External service integration', () => {
564
+ it('should call FileService with correct parameters', async () => {
565
+ const [createdBatch] = await serverDB
566
+ .insert(generationBatches)
567
+ .values({
568
+ ...testBatch,
569
+ userId,
570
+ config: { imageUrls: ['test-image.jpg'] },
571
+ })
572
+ .returning();
573
+
574
+ await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(testTopic.id);
575
+
576
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('test-image.jpg');
577
+ expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
578
+ });
579
+
580
+ it('should call GenerationModel.transformGeneration for each generation', async () => {
581
+ const [createdBatch] = await serverDB
582
+ .insert(generationBatches)
583
+ .values({ ...testBatch, userId })
584
+ .returning();
585
+
586
+ // Create multiple generations
587
+ await serverDB.insert(generations).values([
588
+ { ...testGeneration, id: 'gen1', generationBatchId: createdBatch.id },
589
+ { ...testGeneration, id: 'gen2', generationBatchId: createdBatch.id },
590
+ ]);
591
+
592
+ await generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(testTopic.id);
593
+
594
+ expect(mockTransformGeneration).toHaveBeenCalledTimes(2);
595
+ });
596
+
597
+ it('should handle FileService errors gracefully', async () => {
598
+ mockGetFullFileUrl.mockRejectedValue(new Error('FileService error'));
599
+
600
+ const [createdBatch] = await serverDB
601
+ .insert(generationBatches)
602
+ .values({
603
+ ...testBatch,
604
+ userId,
605
+ config: { imageUrls: ['failing-image.jpg'] },
606
+ })
607
+ .returning();
608
+
609
+ await expect(
610
+ generationBatchModel.queryGenerationBatchesByTopicIdWithGenerations(testTopic.id),
611
+ ).rejects.toThrow('FileService error');
612
+ });
613
+ });
614
+ });