@lobehub/chat 1.98.2 → 1.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (458) hide show
  1. package/.cursor/rules/backend-architecture.mdc +93 -17
  2. package/.cursor/rules/cursor-ux.mdc +45 -35
  3. package/.cursor/rules/project-introduce.mdc +72 -6
  4. package/.cursor/rules/rules-attach.mdc +16 -7
  5. package/.eslintrc.js +10 -0
  6. package/CHANGELOG.md +44 -0
  7. package/apps/desktop/README.md +7 -0
  8. package/apps/desktop/electron-builder.js +5 -0
  9. package/apps/desktop/package.json +2 -1
  10. package/apps/desktop/src/main/appBrowsers.ts +1 -1
  11. package/apps/desktop/src/main/const/dir.ts +3 -0
  12. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +13 -8
  13. package/apps/desktop/src/main/core/App.ts +8 -0
  14. package/apps/desktop/src/main/core/BrowserManager.ts +5 -2
  15. package/apps/desktop/src/main/core/StaticFileServerManager.ts +221 -0
  16. package/apps/desktop/src/main/services/fileSrv.ts +231 -44
  17. package/apps/desktop/src/main/utils/next-electron-rsc.ts +36 -5
  18. package/changelog/v1.json +14 -0
  19. package/docs/development/database-schema.dbml +70 -0
  20. package/locales/ar/common.json +2 -0
  21. package/locales/ar/components.json +35 -0
  22. package/locales/ar/error.json +2 -0
  23. package/locales/ar/image.json +100 -0
  24. package/locales/ar/metadata.json +4 -0
  25. package/locales/ar/modelProvider.json +1 -0
  26. package/locales/ar/models.json +15 -0
  27. package/locales/ar/plugin.json +22 -0
  28. package/locales/ar/providers.json +3 -0
  29. package/locales/ar/setting.json +5 -0
  30. package/locales/bg-BG/common.json +2 -0
  31. package/locales/bg-BG/components.json +35 -0
  32. package/locales/bg-BG/error.json +2 -0
  33. package/locales/bg-BG/image.json +100 -0
  34. package/locales/bg-BG/metadata.json +4 -0
  35. package/locales/bg-BG/modelProvider.json +1 -0
  36. package/locales/bg-BG/models.json +15 -0
  37. package/locales/bg-BG/plugin.json +22 -0
  38. package/locales/bg-BG/providers.json +3 -0
  39. package/locales/bg-BG/setting.json +5 -0
  40. package/locales/de-DE/common.json +2 -0
  41. package/locales/de-DE/components.json +35 -0
  42. package/locales/de-DE/error.json +2 -0
  43. package/locales/de-DE/image.json +100 -0
  44. package/locales/de-DE/metadata.json +4 -0
  45. package/locales/de-DE/modelProvider.json +1 -0
  46. package/locales/de-DE/models.json +15 -0
  47. package/locales/de-DE/plugin.json +22 -0
  48. package/locales/de-DE/providers.json +3 -0
  49. package/locales/de-DE/setting.json +5 -0
  50. package/locales/en-US/common.json +2 -0
  51. package/locales/en-US/components.json +35 -0
  52. package/locales/en-US/error.json +2 -0
  53. package/locales/en-US/image.json +100 -0
  54. package/locales/en-US/metadata.json +4 -0
  55. package/locales/en-US/modelProvider.json +1 -0
  56. package/locales/en-US/models.json +15 -0
  57. package/locales/en-US/plugin.json +22 -0
  58. package/locales/en-US/providers.json +3 -0
  59. package/locales/en-US/setting.json +5 -0
  60. package/locales/es-ES/common.json +2 -0
  61. package/locales/es-ES/components.json +35 -0
  62. package/locales/es-ES/error.json +2 -0
  63. package/locales/es-ES/image.json +100 -0
  64. package/locales/es-ES/metadata.json +4 -0
  65. package/locales/es-ES/modelProvider.json +1 -0
  66. package/locales/es-ES/models.json +15 -0
  67. package/locales/es-ES/plugin.json +22 -0
  68. package/locales/es-ES/providers.json +3 -0
  69. package/locales/es-ES/setting.json +5 -0
  70. package/locales/fa-IR/common.json +2 -0
  71. package/locales/fa-IR/components.json +35 -0
  72. package/locales/fa-IR/error.json +2 -0
  73. package/locales/fa-IR/image.json +100 -0
  74. package/locales/fa-IR/metadata.json +4 -0
  75. package/locales/fa-IR/modelProvider.json +1 -0
  76. package/locales/fa-IR/models.json +15 -0
  77. package/locales/fa-IR/plugin.json +22 -0
  78. package/locales/fa-IR/providers.json +3 -0
  79. package/locales/fa-IR/setting.json +5 -0
  80. package/locales/fr-FR/common.json +2 -0
  81. package/locales/fr-FR/components.json +35 -0
  82. package/locales/fr-FR/error.json +2 -0
  83. package/locales/fr-FR/image.json +100 -0
  84. package/locales/fr-FR/metadata.json +4 -0
  85. package/locales/fr-FR/modelProvider.json +1 -0
  86. package/locales/fr-FR/models.json +15 -0
  87. package/locales/fr-FR/plugin.json +22 -0
  88. package/locales/fr-FR/providers.json +3 -0
  89. package/locales/fr-FR/setting.json +5 -0
  90. package/locales/it-IT/common.json +2 -0
  91. package/locales/it-IT/components.json +35 -0
  92. package/locales/it-IT/error.json +2 -0
  93. package/locales/it-IT/image.json +100 -0
  94. package/locales/it-IT/metadata.json +4 -0
  95. package/locales/it-IT/modelProvider.json +1 -0
  96. package/locales/it-IT/models.json +15 -0
  97. package/locales/it-IT/plugin.json +22 -0
  98. package/locales/it-IT/providers.json +3 -0
  99. package/locales/it-IT/setting.json +5 -0
  100. package/locales/ja-JP/common.json +2 -0
  101. package/locales/ja-JP/components.json +35 -0
  102. package/locales/ja-JP/error.json +2 -0
  103. package/locales/ja-JP/image.json +100 -0
  104. package/locales/ja-JP/metadata.json +4 -0
  105. package/locales/ja-JP/modelProvider.json +1 -0
  106. package/locales/ja-JP/models.json +15 -0
  107. package/locales/ja-JP/plugin.json +22 -0
  108. package/locales/ja-JP/providers.json +3 -0
  109. package/locales/ja-JP/setting.json +5 -0
  110. package/locales/ko-KR/common.json +2 -0
  111. package/locales/ko-KR/components.json +35 -0
  112. package/locales/ko-KR/error.json +2 -0
  113. package/locales/ko-KR/image.json +100 -0
  114. package/locales/ko-KR/metadata.json +4 -0
  115. package/locales/ko-KR/modelProvider.json +1 -0
  116. package/locales/ko-KR/models.json +15 -0
  117. package/locales/ko-KR/plugin.json +22 -0
  118. package/locales/ko-KR/providers.json +3 -0
  119. package/locales/ko-KR/setting.json +5 -0
  120. package/locales/nl-NL/common.json +2 -0
  121. package/locales/nl-NL/components.json +35 -0
  122. package/locales/nl-NL/error.json +2 -0
  123. package/locales/nl-NL/image.json +100 -0
  124. package/locales/nl-NL/metadata.json +4 -0
  125. package/locales/nl-NL/modelProvider.json +1 -0
  126. package/locales/nl-NL/models.json +15 -0
  127. package/locales/nl-NL/plugin.json +22 -0
  128. package/locales/nl-NL/providers.json +3 -0
  129. package/locales/nl-NL/setting.json +5 -0
  130. package/locales/pl-PL/common.json +2 -0
  131. package/locales/pl-PL/components.json +35 -0
  132. package/locales/pl-PL/error.json +2 -0
  133. package/locales/pl-PL/image.json +100 -0
  134. package/locales/pl-PL/metadata.json +4 -0
  135. package/locales/pl-PL/modelProvider.json +1 -0
  136. package/locales/pl-PL/models.json +15 -0
  137. package/locales/pl-PL/plugin.json +22 -0
  138. package/locales/pl-PL/providers.json +3 -0
  139. package/locales/pl-PL/setting.json +5 -0
  140. package/locales/pt-BR/common.json +2 -0
  141. package/locales/pt-BR/components.json +35 -0
  142. package/locales/pt-BR/error.json +2 -0
  143. package/locales/pt-BR/image.json +100 -0
  144. package/locales/pt-BR/metadata.json +4 -0
  145. package/locales/pt-BR/modelProvider.json +1 -0
  146. package/locales/pt-BR/models.json +15 -0
  147. package/locales/pt-BR/plugin.json +22 -0
  148. package/locales/pt-BR/providers.json +3 -0
  149. package/locales/pt-BR/setting.json +5 -0
  150. package/locales/ru-RU/common.json +2 -0
  151. package/locales/ru-RU/components.json +35 -0
  152. package/locales/ru-RU/error.json +2 -0
  153. package/locales/ru-RU/image.json +100 -0
  154. package/locales/ru-RU/metadata.json +4 -0
  155. package/locales/ru-RU/modelProvider.json +1 -0
  156. package/locales/ru-RU/models.json +15 -0
  157. package/locales/ru-RU/plugin.json +22 -0
  158. package/locales/ru-RU/providers.json +3 -0
  159. package/locales/ru-RU/setting.json +5 -0
  160. package/locales/tr-TR/common.json +2 -0
  161. package/locales/tr-TR/components.json +35 -0
  162. package/locales/tr-TR/error.json +2 -0
  163. package/locales/tr-TR/image.json +100 -0
  164. package/locales/tr-TR/metadata.json +4 -0
  165. package/locales/tr-TR/modelProvider.json +1 -0
  166. package/locales/tr-TR/models.json +15 -0
  167. package/locales/tr-TR/plugin.json +22 -0
  168. package/locales/tr-TR/providers.json +3 -0
  169. package/locales/tr-TR/setting.json +5 -0
  170. package/locales/vi-VN/common.json +2 -0
  171. package/locales/vi-VN/components.json +35 -0
  172. package/locales/vi-VN/error.json +2 -0
  173. package/locales/vi-VN/image.json +100 -0
  174. package/locales/vi-VN/metadata.json +4 -0
  175. package/locales/vi-VN/modelProvider.json +1 -0
  176. package/locales/vi-VN/models.json +15 -0
  177. package/locales/vi-VN/plugin.json +22 -0
  178. package/locales/vi-VN/providers.json +3 -0
  179. package/locales/vi-VN/setting.json +5 -0
  180. package/locales/zh-CN/common.json +2 -0
  181. package/locales/zh-CN/components.json +35 -0
  182. package/locales/zh-CN/error.json +2 -0
  183. package/locales/zh-CN/image.json +100 -0
  184. package/locales/zh-CN/metadata.json +4 -0
  185. package/locales/zh-CN/modelProvider.json +1 -0
  186. package/locales/zh-CN/models.json +15 -0
  187. package/locales/zh-CN/plugin.json +22 -0
  188. package/locales/zh-CN/providers.json +3 -0
  189. package/locales/zh-CN/setting.json +5 -0
  190. package/locales/zh-TW/common.json +2 -0
  191. package/locales/zh-TW/components.json +35 -0
  192. package/locales/zh-TW/error.json +2 -0
  193. package/locales/zh-TW/image.json +100 -0
  194. package/locales/zh-TW/metadata.json +4 -0
  195. package/locales/zh-TW/modelProvider.json +1 -0
  196. package/locales/zh-TW/models.json +15 -0
  197. package/locales/zh-TW/plugin.json +22 -0
  198. package/locales/zh-TW/providers.json +3 -0
  199. package/locales/zh-TW/setting.json +5 -0
  200. package/package.json +11 -4
  201. package/packages/electron-server-ipc/src/events/file.ts +3 -1
  202. package/packages/electron-server-ipc/src/types/file.ts +15 -0
  203. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -1
  204. package/src/app/[variants]/(main)/image/@menu/components/AspectRatioSelect/index.tsx +73 -0
  205. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +39 -0
  206. package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +89 -0
  207. package/src/app/[variants]/(main)/image/@menu/default.tsx +11 -0
  208. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +24 -0
  209. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +107 -0
  210. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageNum.tsx +290 -0
  211. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +504 -0
  212. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +18 -0
  213. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +19 -0
  214. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +155 -0
  215. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +415 -0
  216. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +732 -0
  217. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +24 -0
  218. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSelect.tsx +17 -0
  219. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +15 -0
  220. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +11 -0
  221. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/constants.ts +1 -0
  222. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +93 -0
  223. package/src/app/[variants]/(main)/image/@topic/default.tsx +17 -0
  224. package/src/app/[variants]/(main)/image/@topic/features/Topics/NewTopicButton.tsx +64 -0
  225. package/src/app/[variants]/(main)/image/@topic/features/Topics/SkeletonList.tsx +34 -0
  226. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +136 -0
  227. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +91 -0
  228. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +57 -0
  229. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicUrlSync.tsx +37 -0
  230. package/src/app/[variants]/(main)/image/@topic/features/Topics/index.tsx +19 -0
  231. package/src/app/[variants]/(main)/image/NotSupportClient.tsx +153 -0
  232. package/src/app/[variants]/(main)/image/_layout/Desktop/Container.tsx +35 -0
  233. package/src/app/[variants]/(main)/image/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  234. package/src/app/[variants]/(main)/image/_layout/Desktop/index.tsx +30 -0
  235. package/src/app/[variants]/(main)/image/_layout/Mobile/index.tsx +14 -0
  236. package/src/app/[variants]/(main)/image/_layout/type.ts +7 -0
  237. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +196 -0
  238. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx +60 -0
  239. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx +90 -0
  240. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +65 -0
  241. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +44 -0
  242. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +49 -0
  243. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +154 -0
  244. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/styles.ts +51 -0
  245. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +39 -0
  246. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +11 -0
  247. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +97 -0
  248. package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +48 -0
  249. package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +37 -0
  250. package/src/app/[variants]/(main)/image/features/ImageWorkspace/SkeletonList.tsx +50 -0
  251. package/src/app/[variants]/(main)/image/features/ImageWorkspace/index.tsx +23 -0
  252. package/src/app/[variants]/(main)/image/features/PromptInput/Title.tsx +38 -0
  253. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +114 -0
  254. package/src/app/[variants]/(main)/image/layout.tsx +19 -0
  255. package/src/app/[variants]/(main)/image/loading.tsx +3 -0
  256. package/src/app/[variants]/(main)/image/page.tsx +47 -0
  257. package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
  258. package/src/chains/summaryGenerationTitle.ts +25 -0
  259. package/src/components/ImageItem/index.tsx +9 -6
  260. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/Bedrock.tsx +3 -4
  261. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/ProviderApiKeyForm.tsx +5 -4
  262. package/src/components/InvalidAPIKey/APIKeyForm/index.tsx +108 -0
  263. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/useApiKey.ts +2 -1
  264. package/src/components/InvalidAPIKey/index.tsx +30 -0
  265. package/src/components/KeyValueEditor/index.tsx +203 -0
  266. package/src/components/KeyValueEditor/utils.ts +42 -0
  267. package/src/config/aiModels/fal.ts +52 -0
  268. package/src/config/aiModels/index.ts +3 -0
  269. package/src/config/aiModels/openai.ts +20 -6
  270. package/src/config/llm.ts +6 -0
  271. package/src/config/modelProviders/fal.ts +21 -0
  272. package/src/config/modelProviders/index.ts +3 -0
  273. package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +8 -0
  274. package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +11 -0
  275. package/src/config/paramsSchemas/fal/flux-schnell.ts +9 -0
  276. package/src/config/paramsSchemas/fal/imagen4.ts +10 -0
  277. package/src/config/paramsSchemas/openai/gpt-image-1.ts +10 -0
  278. package/src/const/hotkeys.ts +2 -2
  279. package/src/const/image.ts +6 -0
  280. package/src/const/settings/systemAgent.ts +1 -0
  281. package/src/database/client/migrations.json +27 -0
  282. package/src/database/migrations/0026_add_autovacuum_tuning.sql +2 -0
  283. package/src/database/migrations/0027_ai_image.sql +47 -0
  284. package/src/database/migrations/meta/0027_snapshot.json +6003 -0
  285. package/src/database/migrations/meta/_journal.json +7 -0
  286. package/src/database/models/__tests__/asyncTask.test.ts +7 -5
  287. package/src/database/models/__tests__/file.test.ts +287 -0
  288. package/src/database/models/__tests__/generation.test.ts +786 -0
  289. package/src/database/models/__tests__/generationBatch.test.ts +614 -0
  290. package/src/database/models/__tests__/generationTopic.test.ts +411 -0
  291. package/src/database/models/aiModel.ts +2 -0
  292. package/src/database/models/asyncTask.ts +1 -1
  293. package/src/database/models/file.ts +28 -20
  294. package/src/database/models/generation.ts +197 -0
  295. package/src/database/models/generationBatch.ts +212 -0
  296. package/src/database/models/generationTopic.ts +131 -0
  297. package/src/database/repositories/aiInfra/index.test.ts +157 -1
  298. package/src/database/repositories/aiInfra/index.ts +37 -19
  299. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  300. package/src/database/schemas/file.ts +8 -0
  301. package/src/database/schemas/generation.ts +127 -0
  302. package/src/database/schemas/index.ts +1 -0
  303. package/src/database/schemas/relations.ts +45 -1
  304. package/src/database/type.ts +2 -0
  305. package/src/database/utils/idGenerator.ts +3 -0
  306. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +39 -0
  307. package/src/features/Conversation/Error/InvalidAccessCode.tsx +2 -2
  308. package/src/features/Conversation/Error/index.tsx +3 -3
  309. package/src/features/ImageSidePanel/index.tsx +83 -0
  310. package/src/features/ImageTopicPanel/index.tsx +79 -0
  311. package/src/features/PluginDevModal/MCPManifestForm/CollapsibleSection.tsx +62 -0
  312. package/src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx +158 -0
  313. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +99 -155
  314. package/src/features/PluginStore/McpList/Detail/Settings/index.tsx +5 -2
  315. package/src/hooks/useDownloadImage.ts +31 -0
  316. package/src/hooks/useFetchGenerationTopics.ts +13 -0
  317. package/src/hooks/useHotkeys/imageScope.ts +48 -0
  318. package/src/libs/mcp/client.ts +55 -22
  319. package/src/libs/mcp/types.ts +42 -6
  320. package/src/libs/model-runtime/BaseAI.ts +3 -1
  321. package/src/libs/model-runtime/ModelRuntime.test.ts +80 -0
  322. package/src/libs/model-runtime/ModelRuntime.ts +15 -1
  323. package/src/libs/model-runtime/UniformRuntime/index.ts +4 -1
  324. package/src/libs/model-runtime/fal/index.test.ts +442 -0
  325. package/src/libs/model-runtime/fal/index.ts +88 -0
  326. package/src/libs/model-runtime/openai/index.test.ts +396 -2
  327. package/src/libs/model-runtime/openai/index.ts +129 -3
  328. package/src/libs/model-runtime/runtimeMap.ts +2 -0
  329. package/src/libs/model-runtime/types/image.ts +25 -0
  330. package/src/libs/model-runtime/types/type.ts +1 -0
  331. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +10 -0
  332. package/src/libs/standard-parameters/index.ts +1 -0
  333. package/src/libs/standard-parameters/meta-schema.test.ts +214 -0
  334. package/src/libs/standard-parameters/meta-schema.ts +147 -0
  335. package/src/libs/swr/index.ts +1 -0
  336. package/src/libs/trpc/async/asyncAuth.ts +29 -8
  337. package/src/libs/trpc/async/context.ts +42 -4
  338. package/src/libs/trpc/async/index.ts +17 -4
  339. package/src/libs/trpc/async/init.ts +8 -0
  340. package/src/libs/trpc/client/lambda.ts +19 -2
  341. package/src/locales/default/common.ts +2 -0
  342. package/src/locales/default/components.ts +35 -0
  343. package/src/locales/default/error.ts +2 -0
  344. package/src/locales/default/image.ts +100 -0
  345. package/src/locales/default/index.ts +2 -0
  346. package/src/locales/default/metadata.ts +4 -0
  347. package/src/locales/default/modelProvider.ts +2 -0
  348. package/src/locales/default/plugin.ts +22 -0
  349. package/src/locales/default/setting.ts +5 -0
  350. package/src/middleware.ts +1 -0
  351. package/src/server/modules/ElectronIPCClient/index.ts +9 -1
  352. package/src/server/modules/S3/index.ts +15 -0
  353. package/src/server/routers/async/caller.ts +9 -1
  354. package/src/server/routers/async/image.ts +253 -0
  355. package/src/server/routers/async/index.ts +2 -0
  356. package/src/server/routers/lambda/aiProvider.test.ts +2 -0
  357. package/src/server/routers/lambda/generation.test.ts +267 -0
  358. package/src/server/routers/lambda/generation.ts +86 -0
  359. package/src/server/routers/lambda/generationBatch.test.ts +376 -0
  360. package/src/server/routers/lambda/generationBatch.ts +56 -0
  361. package/src/server/routers/lambda/generationTopic.test.ts +508 -0
  362. package/src/server/routers/lambda/generationTopic.ts +93 -0
  363. package/src/server/routers/lambda/image.ts +248 -0
  364. package/src/server/routers/lambda/index.ts +8 -0
  365. package/src/server/routers/tools/mcp.ts +15 -0
  366. package/src/server/services/file/__tests__/index.test.ts +135 -0
  367. package/src/server/services/file/impls/local.test.ts +153 -52
  368. package/src/server/services/file/impls/local.ts +70 -46
  369. package/src/server/services/file/impls/s3.test.ts +114 -0
  370. package/src/server/services/file/impls/s3.ts +40 -0
  371. package/src/server/services/file/impls/type.ts +10 -0
  372. package/src/server/services/file/index.ts +14 -0
  373. package/src/server/services/generation/index.ts +239 -0
  374. package/src/server/services/mcp/index.ts +20 -2
  375. package/src/services/__tests__/generation.test.ts +40 -0
  376. package/src/services/__tests__/generationBatch.test.ts +36 -0
  377. package/src/services/__tests__/generationTopic.test.ts +72 -0
  378. package/src/services/electron/file.ts +3 -1
  379. package/src/services/generation.ts +16 -0
  380. package/src/services/generationBatch.ts +25 -0
  381. package/src/services/generationTopic.ts +28 -0
  382. package/src/services/image.ts +33 -0
  383. package/src/services/mcp.ts +12 -7
  384. package/src/services/upload.ts +43 -9
  385. package/src/store/aiInfra/slices/aiProvider/action.ts +31 -6
  386. package/src/store/aiInfra/slices/aiProvider/initialState.ts +1 -0
  387. package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
  388. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -5
  389. package/src/store/chat/slices/message/action.ts +2 -2
  390. package/src/store/chat/slices/translate/action.ts +1 -1
  391. package/src/store/global/initialState.ts +9 -0
  392. package/src/store/global/selectors/systemStatus.ts +8 -0
  393. package/src/store/image/index.ts +2 -0
  394. package/src/store/image/initialState.ts +25 -0
  395. package/src/store/image/selectors.ts +4 -0
  396. package/src/store/image/slices/createImage/action.test.ts +330 -0
  397. package/src/store/image/slices/createImage/action.ts +134 -0
  398. package/src/store/image/slices/createImage/initialState.ts +9 -0
  399. package/src/store/image/slices/createImage/selectors.test.ts +114 -0
  400. package/src/store/image/slices/createImage/selectors.ts +9 -0
  401. package/src/store/image/slices/generationBatch/action.test.ts +495 -0
  402. package/src/store/image/slices/generationBatch/action.ts +303 -0
  403. package/src/store/image/slices/generationBatch/initialState.ts +13 -0
  404. package/src/store/image/slices/generationBatch/reducer.test.ts +568 -0
  405. package/src/store/image/slices/generationBatch/reducer.ts +101 -0
  406. package/src/store/image/slices/generationBatch/selectors.test.ts +307 -0
  407. package/src/store/image/slices/generationBatch/selectors.ts +36 -0
  408. package/src/store/image/slices/generationConfig/action.test.ts +351 -0
  409. package/src/store/image/slices/generationConfig/action.ts +295 -0
  410. package/src/store/image/slices/generationConfig/hooks.test.ts +304 -0
  411. package/src/store/image/slices/generationConfig/hooks.ts +118 -0
  412. package/src/store/image/slices/generationConfig/index.ts +1 -0
  413. package/src/store/image/slices/generationConfig/initialState.ts +37 -0
  414. package/src/store/image/slices/generationConfig/selectors.test.ts +204 -0
  415. package/src/store/image/slices/generationConfig/selectors.ts +25 -0
  416. package/src/store/image/slices/generationTopic/action.test.ts +687 -0
  417. package/src/store/image/slices/generationTopic/action.ts +319 -0
  418. package/src/store/image/slices/generationTopic/index.ts +2 -0
  419. package/src/store/image/slices/generationTopic/initialState.ts +14 -0
  420. package/src/store/image/slices/generationTopic/reducer.test.ts +198 -0
  421. package/src/store/image/slices/generationTopic/reducer.ts +66 -0
  422. package/src/store/image/slices/generationTopic/selectors.test.ts +103 -0
  423. package/src/store/image/slices/generationTopic/selectors.ts +15 -0
  424. package/src/store/image/store.ts +42 -0
  425. package/src/store/image/utils/size.ts +51 -0
  426. package/src/store/tool/slices/customPlugin/action.ts +10 -1
  427. package/src/store/tool/slices/mcpStore/action.ts +6 -4
  428. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
  429. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  430. package/src/types/aiModel.ts +8 -3
  431. package/src/types/aiProvider.ts +2 -0
  432. package/src/types/asyncTask.ts +2 -0
  433. package/src/types/files/index.ts +5 -0
  434. package/src/types/generation/index.ts +80 -0
  435. package/src/types/hotkey.ts +2 -0
  436. package/src/types/plugins/mcp.ts +2 -6
  437. package/src/types/tool/plugin.ts +8 -0
  438. package/src/types/user/settings/keyVaults.ts +5 -0
  439. package/src/types/user/settings/systemAgent.ts +1 -0
  440. package/src/utils/client/downloadFile.ts +33 -4
  441. package/src/utils/number.test.ts +105 -0
  442. package/src/utils/number.ts +25 -0
  443. package/src/utils/server/__tests__/geo.test.ts +6 -3
  444. package/src/utils/storeDebug.test.ts +152 -0
  445. package/src/utils/storeDebug.ts +16 -7
  446. package/src/utils/time.test.ts +259 -0
  447. package/src/utils/time.ts +18 -0
  448. package/src/utils/units.ts +61 -0
  449. package/src/utils/url.test.ts +358 -9
  450. package/src/utils/url.ts +105 -3
  451. package/{vitest.server.config.ts → vitest.config.server.ts} +3 -0
  452. package/.cursor/rules/i18n/i18n-auto-attached.mdc +0 -6
  453. package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -105
  454. package/src/features/Conversation/Error/InvalidAPIKey.tsx +0 -16
  455. package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +0 -227
  456. /package/.cursor/rules/{i18n/i18n.mdc → i18n.mdc} +0 -0
  457. /package/src/app/[variants]/(main)/settings/system-agent/features/{createForm.tsx → SystemAgentForm.tsx} +0 -0
  458. /package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/LoadingContext.ts +0 -0
@@ -0,0 +1,786 @@
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 { FileSource } from '@/types/files';
8
+ import { ImageGenerationAsset } from '@/types/generation';
9
+
10
+ import {
11
+ NewGeneration,
12
+ asyncTasks,
13
+ files,
14
+ generationBatches,
15
+ generationTopics,
16
+ generations,
17
+ users,
18
+ } from '../../schemas';
19
+ import { GenerationModel } from '../generation';
20
+ import { getTestDB } from './_util';
21
+
22
+ const serverDB: LobeChatDatabase = await getTestDB();
23
+
24
+ // Mock FileService
25
+ const mockGetFullFileUrl = vi.fn();
26
+ vi.mock('@/server/services/file', () => ({
27
+ FileService: vi.fn().mockImplementation(() => ({
28
+ getFullFileUrl: mockGetFullFileUrl,
29
+ })),
30
+ }));
31
+
32
+ // Mock FileModel
33
+ const mockFileModelCreate = vi.fn();
34
+ vi.mock('../file', () => ({
35
+ FileModel: vi.fn().mockImplementation(() => ({
36
+ create: mockFileModelCreate,
37
+ })),
38
+ }));
39
+
40
+ const userId = 'generation-test-user-id';
41
+ const otherUserId = 'other-user-id';
42
+ const generationModel = new GenerationModel(serverDB, userId);
43
+
44
+ // Test data
45
+ const testTopic = {
46
+ id: 'test-topic-id',
47
+ userId,
48
+ title: 'Test Generation Topic',
49
+ coverUrl: null,
50
+ };
51
+
52
+ const testBatch = {
53
+ id: 'test-batch-id',
54
+ generationTopicId: 'test-topic-id',
55
+ provider: 'test-provider',
56
+ model: 'test-model',
57
+ prompt: 'Test prompt for image generation',
58
+ width: 1024,
59
+ height: 1024,
60
+ config: {},
61
+ userId,
62
+ };
63
+
64
+ const testAsyncTask = {
65
+ id: '550e8400-e29b-41d4-a716-446655440000',
66
+ status: AsyncTaskStatus.Success,
67
+ type: 'imageGeneration',
68
+ params: {},
69
+ error: null,
70
+ userId,
71
+ };
72
+
73
+ const testGeneration: Omit<NewGeneration, 'userId'> = {
74
+ generationBatchId: 'test-batch-id',
75
+ asyncTaskId: '550e8400-e29b-41d4-a716-446655440000', // 使用有效的 asyncTaskId
76
+ fileId: null, // 使用 null 避免外键约束
77
+ seed: 12345,
78
+ asset: {
79
+ url: 'asset-url.jpg',
80
+ thumbnailUrl: 'thumbnail-url.jpg',
81
+ width: 1024,
82
+ height: 1024,
83
+ } as ImageGenerationAsset,
84
+ };
85
+
86
+ const testFile = {
87
+ id: 'test-file-id',
88
+ name: 'generated-image.jpg',
89
+ url: 'https://example.com/generated-image.jpg',
90
+ size: 1048576,
91
+ fileType: 'image/jpeg',
92
+ source: FileSource.ImageGeneration,
93
+ userId,
94
+ };
95
+
96
+ beforeEach(async () => {
97
+ // Clear all mocks
98
+ vi.clearAllMocks();
99
+
100
+ // Setup mock return values
101
+ mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
102
+
103
+ // Clear database and create test users
104
+ await serverDB.delete(users);
105
+ await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
106
+
107
+ // Create test topic
108
+ await serverDB.insert(generationTopics).values(testTopic);
109
+
110
+ // Create test batch
111
+ await serverDB.insert(generationBatches).values(testBatch);
112
+
113
+ // Create test async task
114
+ await serverDB.insert(asyncTasks).values(testAsyncTask);
115
+
116
+ // Create test file
117
+ await serverDB.insert(files).values(testFile);
118
+
119
+ // Create a file that will be returned by the mock for createAssetAndFile tests
120
+ const mockFileForUpdateTest = {
121
+ id: 'new-file-id',
122
+ name: 'mock-generated-image.jpg',
123
+ url: 'https://example.com/mock-generated-image.jpg',
124
+ size: 1048576,
125
+ fileType: 'image/jpeg',
126
+ source: FileSource.ImageGeneration,
127
+ userId,
128
+ };
129
+ await serverDB.insert(files).values(mockFileForUpdateTest);
130
+
131
+ // Setup FileModel mock to return the actual file ID that exists in database
132
+ mockFileModelCreate.mockResolvedValue({ id: 'new-file-id' });
133
+ });
134
+
135
+ afterEach(async () => {
136
+ // Clean up database
137
+ await serverDB.delete(users);
138
+ });
139
+
140
+ describe('GenerationModel', () => {
141
+ describe('create', () => {
142
+ it('should create a new generation', async () => {
143
+ const result = await generationModel.create(testGeneration);
144
+
145
+ expect(result.id).toBeDefined();
146
+ expect(result).toMatchObject({
147
+ ...testGeneration,
148
+ userId,
149
+ });
150
+
151
+ // Verify in database
152
+ const dbGeneration = await serverDB.query.generations.findFirst({
153
+ where: eq(generations.id, result.id),
154
+ });
155
+ expect(dbGeneration).toMatchObject({
156
+ ...testGeneration,
157
+ userId,
158
+ });
159
+ });
160
+
161
+ it('should automatically set userId when creating generation', async () => {
162
+ const result = await generationModel.create(testGeneration);
163
+
164
+ expect(result.userId).toBe(userId);
165
+ });
166
+
167
+ it('should create generation with minimal data', async () => {
168
+ const minimalGeneration: Omit<NewGeneration, 'userId'> = {
169
+ generationBatchId: 'test-batch-id',
170
+ };
171
+
172
+ const result = await generationModel.create(minimalGeneration);
173
+
174
+ expect(result.id).toBeDefined();
175
+ expect(result.userId).toBe(userId);
176
+ expect(result.generationBatchId).toBe('test-batch-id');
177
+ });
178
+ });
179
+
180
+ describe('findById', () => {
181
+ it('should find a generation by id', async () => {
182
+ const [createdGeneration] = await serverDB
183
+ .insert(generations)
184
+ .values({ ...testGeneration, userId })
185
+ .returning();
186
+
187
+ const result = await generationModel.findById(createdGeneration.id);
188
+
189
+ expect(result).toMatchObject({
190
+ id: createdGeneration.id,
191
+ ...testGeneration,
192
+ userId,
193
+ });
194
+ });
195
+
196
+ it('should return undefined for non-existent generation', async () => {
197
+ const result = await generationModel.findById('non-existent-id');
198
+ expect(result).toBeUndefined();
199
+ });
200
+
201
+ it('should NOT find generations from other users', async () => {
202
+ // Create generation for other user
203
+ const [otherUserGeneration] = await serverDB
204
+ .insert(generations)
205
+ .values({ ...testGeneration, userId: otherUserId })
206
+ .returning();
207
+
208
+ const result = await generationModel.findById(otherUserGeneration.id);
209
+ expect(result).toBeUndefined();
210
+ });
211
+ });
212
+
213
+ describe('findByIdWithAsyncTask', () => {
214
+ it('should find generation with async task data', async () => {
215
+ const [createdGeneration] = await serverDB
216
+ .insert(generations)
217
+ .values({ ...testGeneration, userId })
218
+ .returning();
219
+
220
+ const result = await generationModel.findByIdWithAsyncTask(createdGeneration.id);
221
+
222
+ expect(result).toMatchObject({
223
+ id: createdGeneration.id,
224
+ ...testGeneration,
225
+ userId,
226
+ });
227
+ expect(result?.asyncTask).toMatchObject({
228
+ id: testAsyncTask.id,
229
+ status: AsyncTaskStatus.Success,
230
+ });
231
+ });
232
+
233
+ it('should return undefined for non-existent generation', async () => {
234
+ const result = await generationModel.findByIdWithAsyncTask('non-existent-id');
235
+ expect(result).toBeUndefined();
236
+ });
237
+
238
+ it('should NOT find generations from other users', async () => {
239
+ const [otherUserGeneration] = await serverDB
240
+ .insert(generations)
241
+ .values({ ...testGeneration, userId: otherUserId })
242
+ .returning();
243
+
244
+ const result = await generationModel.findByIdWithAsyncTask(otherUserGeneration.id);
245
+ expect(result).toBeUndefined();
246
+ });
247
+ });
248
+
249
+ describe('update', () => {
250
+ it('should update a generation', async () => {
251
+ const [createdGeneration] = await serverDB
252
+ .insert(generations)
253
+ .values({ ...testGeneration, userId })
254
+ .returning();
255
+
256
+ const updateData = {
257
+ seed: 54321,
258
+ asset: {
259
+ url: 'updated-asset.jpg',
260
+ thumbnailUrl: 'updated-thumbnail.jpg',
261
+ width: 512,
262
+ height: 512,
263
+ } as ImageGenerationAsset,
264
+ };
265
+
266
+ await generationModel.update(createdGeneration.id, updateData);
267
+
268
+ const updatedGeneration = await serverDB.query.generations.findFirst({
269
+ where: eq(generations.id, createdGeneration.id),
270
+ });
271
+
272
+ expect(updatedGeneration).toMatchObject({
273
+ ...testGeneration,
274
+ ...updateData,
275
+ userId,
276
+ });
277
+ expect(updatedGeneration?.updatedAt).toBeDefined();
278
+ });
279
+
280
+ it('should NOT update generations from other users', async () => {
281
+ // Create generation for other user
282
+ const [otherUserGeneration] = await serverDB
283
+ .insert(generations)
284
+ .values({ ...testGeneration, userId: otherUserId })
285
+ .returning();
286
+
287
+ const updateData = { seed: 99999 };
288
+
289
+ await generationModel.update(otherUserGeneration.id, updateData);
290
+
291
+ // Verify original data unchanged
292
+ const unchanged = await serverDB.query.generations.findFirst({
293
+ where: eq(generations.id, otherUserGeneration.id),
294
+ });
295
+ expect(unchanged?.seed).toBe(testGeneration.seed);
296
+ });
297
+
298
+ it('should handle partial updates', async () => {
299
+ const [createdGeneration] = await serverDB
300
+ .insert(generations)
301
+ .values({ ...testGeneration, userId })
302
+ .returning();
303
+
304
+ await generationModel.update(createdGeneration.id, { seed: 11111 });
305
+
306
+ const updatedGeneration = await serverDB.query.generations.findFirst({
307
+ where: eq(generations.id, createdGeneration.id),
308
+ });
309
+
310
+ expect(updatedGeneration?.seed).toBe(11111);
311
+ expect(updatedGeneration?.generationBatchId).toBe(testGeneration.generationBatchId);
312
+ });
313
+ });
314
+
315
+ describe('createAssetAndFile', () => {
316
+ it('should update generation asset and create file in transaction', async () => {
317
+ const [createdGeneration] = await serverDB
318
+ .insert(generations)
319
+ .values({ ...testGeneration, userId, asset: null, fileId: null })
320
+ .returning();
321
+
322
+ const newAsset = {
323
+ url: 'new-asset.jpg',
324
+ thumbnailUrl: 'new-thumbnail.jpg',
325
+ width: 2048,
326
+ height: 2048,
327
+ } as ImageGenerationAsset;
328
+
329
+ const newFileData = {
330
+ name: 'new-generated-image.jpg',
331
+ url: 'https://example.com/new-generated-image.jpg',
332
+ size: 2097152,
333
+ fileType: 'image/jpeg',
334
+ };
335
+
336
+ const result = await generationModel.createAssetAndFile(
337
+ createdGeneration.id,
338
+ newAsset,
339
+ newFileData,
340
+ );
341
+
342
+ expect(result.file.id).toBe('new-file-id');
343
+ expect(mockFileModelCreate).toHaveBeenCalledWith(
344
+ {
345
+ ...newFileData,
346
+ source: FileSource.ImageGeneration,
347
+ },
348
+ true,
349
+ expect.any(Object), // transaction object
350
+ );
351
+
352
+ // Verify generation was updated
353
+ const updatedGeneration = await serverDB.query.generations.findFirst({
354
+ where: eq(generations.id, createdGeneration.id),
355
+ });
356
+ expect(updatedGeneration?.asset).toEqual(newAsset);
357
+ expect(updatedGeneration?.fileId).toBe('new-file-id');
358
+ });
359
+
360
+ it('should NOT update assets for other users generations', async () => {
361
+ // Create generation for other user
362
+ const [otherUserGeneration] = await serverDB
363
+ .insert(generations)
364
+ .values({ ...testGeneration, userId: otherUserId, asset: null, fileId: null })
365
+ .returning();
366
+
367
+ const newAsset = {
368
+ url: 'hacked-asset.jpg',
369
+ thumbnailUrl: 'hacked-thumbnail.jpg',
370
+ width: 1,
371
+ height: 1,
372
+ } as ImageGenerationAsset;
373
+
374
+ const newFileData = {
375
+ name: 'hacked-file.jpg',
376
+ url: 'https://example.com/hacked-file.jpg',
377
+ size: 1,
378
+ fileType: 'image/jpeg',
379
+ };
380
+
381
+ await generationModel.createAssetAndFile(otherUserGeneration.id, newAsset, newFileData);
382
+
383
+ // Verify no changes to other user's generation
384
+ const unchanged = await serverDB.query.generations.findFirst({
385
+ where: eq(generations.id, otherUserGeneration.id),
386
+ });
387
+ expect(unchanged?.asset).toBeNull();
388
+ expect(unchanged?.fileId).toBeNull();
389
+ });
390
+
391
+ it('should handle FileModel errors in transaction', async () => {
392
+ const [createdGeneration] = await serverDB
393
+ .insert(generations)
394
+ .values({ ...testGeneration, userId, asset: null, fileId: null })
395
+ .returning();
396
+
397
+ mockFileModelCreate.mockRejectedValue(new Error('File creation failed'));
398
+
399
+ const newAsset = {
400
+ url: 'asset.jpg',
401
+ thumbnailUrl: 'thumbnail.jpg',
402
+ width: 1024,
403
+ height: 1024,
404
+ } as ImageGenerationAsset;
405
+
406
+ const newFileData = {
407
+ name: 'image.jpg',
408
+ url: 'https://example.com/image.jpg',
409
+ size: 1024,
410
+ fileType: 'image/jpeg',
411
+ };
412
+
413
+ await expect(
414
+ generationModel.createAssetAndFile(createdGeneration.id, newAsset, newFileData),
415
+ ).rejects.toThrow('File creation failed');
416
+
417
+ // Verify generation was not updated due to transaction rollback
418
+ const unchanged = await serverDB.query.generations.findFirst({
419
+ where: eq(generations.id, createdGeneration.id),
420
+ });
421
+ expect(unchanged?.asset).toBeNull();
422
+ expect(unchanged?.fileId).toBeNull();
423
+ });
424
+ });
425
+
426
+ describe('delete', () => {
427
+ it('should delete a generation', async () => {
428
+ const [createdGeneration] = await serverDB
429
+ .insert(generations)
430
+ .values({ ...testGeneration, userId })
431
+ .returning();
432
+
433
+ const deletedGeneration = await generationModel.delete(createdGeneration.id);
434
+
435
+ expect(deletedGeneration.id).toBe(createdGeneration.id);
436
+
437
+ const deletedRecord = await serverDB.query.generations.findFirst({
438
+ where: eq(generations.id, createdGeneration.id),
439
+ });
440
+ expect(deletedRecord).toBeUndefined();
441
+ });
442
+
443
+ it('should NOT delete generations from other users', async () => {
444
+ // Create generation for other user
445
+ const [otherUserGeneration] = await serverDB
446
+ .insert(generations)
447
+ .values({ ...testGeneration, userId: otherUserId })
448
+ .returning();
449
+
450
+ const result = await generationModel.delete(otherUserGeneration.id);
451
+
452
+ expect(result).toBeUndefined();
453
+
454
+ // Verify generation still exists
455
+ const stillExists = await serverDB.query.generations.findFirst({
456
+ where: eq(generations.id, otherUserGeneration.id),
457
+ });
458
+ expect(stillExists).toBeDefined();
459
+ });
460
+
461
+ it('should return undefined when trying to delete non-existent generation', async () => {
462
+ const result = await generationModel.delete('non-existent-id');
463
+ expect(result).toBeUndefined();
464
+ });
465
+ });
466
+
467
+ describe('findByIdAndTransform', () => {
468
+ it('should find and transform generation to frontend type', async () => {
469
+ const [createdGeneration] = await serverDB
470
+ .insert(generations)
471
+ .values({ ...testGeneration, userId })
472
+ .returning();
473
+
474
+ const result = await generationModel.findByIdAndTransform(createdGeneration.id);
475
+
476
+ expect(result).toMatchObject({
477
+ id: createdGeneration.id,
478
+ asset: {
479
+ url: 'https://example.com/asset-url.jpg',
480
+ thumbnailUrl: 'https://example.com/thumbnail-url.jpg',
481
+ width: 1024,
482
+ height: 1024,
483
+ },
484
+ seed: testGeneration.seed,
485
+ asyncTaskId: testGeneration.asyncTaskId,
486
+ task: {
487
+ id: testGeneration.asyncTaskId,
488
+ status: AsyncTaskStatus.Success,
489
+ },
490
+ });
491
+
492
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('asset-url.jpg');
493
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('thumbnail-url.jpg');
494
+ });
495
+
496
+ it('should return null for non-existent generation', async () => {
497
+ const result = await generationModel.findByIdAndTransform('non-existent-id');
498
+ expect(result).toBeNull();
499
+ });
500
+
501
+ it('should NOT transform generations from other users', async () => {
502
+ const [otherUserGeneration] = await serverDB
503
+ .insert(generations)
504
+ .values({ ...testGeneration, userId: otherUserId })
505
+ .returning();
506
+
507
+ const result = await generationModel.findByIdAndTransform(otherUserGeneration.id);
508
+ expect(result).toBeNull();
509
+ });
510
+ });
511
+
512
+ describe('transformGeneration', () => {
513
+ it('should transform generation with asset URLs', async () => {
514
+ const generationWithTask = {
515
+ id: 'test-gen-id',
516
+ userId,
517
+ generationBatchId: 'batch-id',
518
+ asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
519
+ fileId: 'file-id',
520
+ seed: 12345,
521
+ asset: {
522
+ url: 'original-asset.jpg',
523
+ thumbnailUrl: 'original-thumbnail.jpg',
524
+ width: 1024,
525
+ height: 1024,
526
+ } as ImageGenerationAsset,
527
+ accessedAt: new Date(),
528
+ createdAt: new Date(),
529
+ updatedAt: new Date(),
530
+ asyncTask: {
531
+ id: '550e8400-e29b-41d4-a716-446655440000',
532
+ status: AsyncTaskStatus.Success,
533
+ type: 'imageGeneration',
534
+ params: {},
535
+ error: null,
536
+ duration: null,
537
+ accessedAt: new Date(),
538
+ createdAt: new Date(),
539
+ updatedAt: new Date(),
540
+ userId,
541
+ },
542
+ };
543
+
544
+ const result = await generationModel.transformGeneration(generationWithTask);
545
+
546
+ expect(result).toMatchObject({
547
+ id: 'test-gen-id',
548
+ asset: {
549
+ url: 'https://example.com/original-asset.jpg',
550
+ thumbnailUrl: 'https://example.com/original-thumbnail.jpg',
551
+ width: 1024,
552
+ height: 1024,
553
+ },
554
+ seed: 12345,
555
+ asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
556
+ task: {
557
+ id: '550e8400-e29b-41d4-a716-446655440000',
558
+ status: AsyncTaskStatus.Success,
559
+ },
560
+ });
561
+
562
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('original-asset.jpg');
563
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('original-thumbnail.jpg');
564
+ });
565
+
566
+ it('should handle generation without asset', async () => {
567
+ const generationWithoutAsset = {
568
+ id: 'test-gen-id',
569
+ userId,
570
+ generationBatchId: 'batch-id',
571
+ asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
572
+ fileId: null,
573
+ seed: 12345,
574
+ asset: null,
575
+ accessedAt: new Date(),
576
+ createdAt: new Date(),
577
+ updatedAt: new Date(),
578
+ asyncTask: {
579
+ id: '550e8400-e29b-41d4-a716-446655440000',
580
+ status: AsyncTaskStatus.Pending,
581
+ type: 'imageGeneration',
582
+ params: {},
583
+ error: null,
584
+ duration: null,
585
+ accessedAt: new Date(),
586
+ createdAt: new Date(),
587
+ updatedAt: new Date(),
588
+ userId,
589
+ },
590
+ };
591
+
592
+ const result = await generationModel.transformGeneration(generationWithoutAsset as any);
593
+
594
+ expect(result).toMatchObject({
595
+ id: 'test-gen-id',
596
+ asset: null,
597
+ seed: 12345,
598
+ asyncTaskId: '550e8400-e29b-41d4-a716-446655440000',
599
+ task: {
600
+ id: '550e8400-e29b-41d4-a716-446655440000',
601
+ status: AsyncTaskStatus.Pending,
602
+ },
603
+ });
604
+
605
+ expect(mockGetFullFileUrl).not.toHaveBeenCalled();
606
+ });
607
+
608
+ it('should handle generation without async task', async () => {
609
+ const generationWithoutTask = {
610
+ id: 'test-gen-id',
611
+ userId,
612
+ generationBatchId: 'batch-id',
613
+ asyncTaskId: null,
614
+ fileId: null,
615
+ seed: 12345,
616
+ asset: null,
617
+ accessedAt: new Date(),
618
+ createdAt: new Date(),
619
+ updatedAt: new Date(),
620
+ asyncTask: undefined,
621
+ };
622
+
623
+ const result = await generationModel.transformGeneration(generationWithoutTask as any);
624
+
625
+ expect(result).toMatchObject({
626
+ id: 'test-gen-id',
627
+ asset: null,
628
+ seed: 12345,
629
+ asyncTaskId: null,
630
+ task: {
631
+ id: '',
632
+ status: 'pending',
633
+ },
634
+ });
635
+ });
636
+
637
+ it('should handle FileService errors during transformation', async () => {
638
+ mockGetFullFileUrl.mockRejectedValue(new Error('FileService error'));
639
+
640
+ const generationWithAsset = {
641
+ id: 'test-gen-id',
642
+ userId,
643
+ generationBatchId: 'batch-id',
644
+ asyncTaskId: null,
645
+ fileId: null,
646
+ seed: 12345,
647
+ asset: {
648
+ url: 'failing-asset.jpg',
649
+ thumbnailUrl: 'failing-thumbnail.jpg',
650
+ width: 1024,
651
+ height: 1024,
652
+ } as ImageGenerationAsset,
653
+ accessedAt: new Date(),
654
+ createdAt: new Date(),
655
+ updatedAt: new Date(),
656
+ asyncTask: undefined,
657
+ };
658
+
659
+ await expect(generationModel.transformGeneration(generationWithAsset as any)).rejects.toThrow(
660
+ 'FileService error',
661
+ );
662
+ });
663
+ });
664
+
665
+ describe('user isolation security tests', () => {
666
+ it('should enforce user data isolation across all methods', async () => {
667
+ // Create generations for both users
668
+ const userGeneration = { ...testGeneration, userId };
669
+ const otherUserGeneration = { ...testGeneration, userId: otherUserId };
670
+
671
+ const [userGenerationCreated] = await serverDB
672
+ .insert(generations)
673
+ .values(userGeneration)
674
+ .returning();
675
+
676
+ const [otherUserGenerationCreated] = await serverDB
677
+ .insert(generations)
678
+ .values(otherUserGeneration)
679
+ .returning();
680
+
681
+ // Test findById isolation
682
+ const foundUserGeneration = await generationModel.findById(userGenerationCreated.id);
683
+ const foundOtherGeneration = await generationModel.findById(otherUserGenerationCreated.id);
684
+
685
+ expect(foundUserGeneration).toBeDefined();
686
+ expect(foundOtherGeneration).toBeUndefined(); // Should not find other user's generation
687
+
688
+ // Test findByIdWithAsyncTask isolation
689
+ const foundUserGenerationWithTask = await generationModel.findByIdWithAsyncTask(
690
+ userGenerationCreated.id,
691
+ );
692
+ const foundOtherGenerationWithTask = await generationModel.findByIdWithAsyncTask(
693
+ otherUserGenerationCreated.id,
694
+ );
695
+
696
+ expect(foundUserGenerationWithTask).toBeDefined();
697
+ expect(foundOtherGenerationWithTask).toBeUndefined();
698
+
699
+ // Test findByIdAndTransform isolation
700
+ const transformedUserGeneration = await generationModel.findByIdAndTransform(
701
+ userGenerationCreated.id,
702
+ );
703
+ const transformedOtherGeneration = await generationModel.findByIdAndTransform(
704
+ otherUserGenerationCreated.id,
705
+ );
706
+
707
+ expect(transformedUserGeneration).toBeDefined();
708
+ expect(transformedOtherGeneration).toBeNull();
709
+
710
+ // Test update isolation - should not affect other user's data
711
+ await generationModel.update(otherUserGenerationCreated.id, { seed: 99999 });
712
+ const otherUserGenerationUnchanged = await serverDB.query.generations.findFirst({
713
+ where: eq(generations.id, otherUserGenerationCreated.id),
714
+ });
715
+ expect(otherUserGenerationUnchanged?.seed).toBe(testGeneration.seed); // Should not be updated
716
+
717
+ // Test delete isolation - should not affect other user's data
718
+ await generationModel.delete(otherUserGenerationCreated.id);
719
+ const otherUserGenerationStillExists = await serverDB.query.generations.findFirst({
720
+ where: eq(generations.id, otherUserGenerationCreated.id),
721
+ });
722
+ expect(otherUserGenerationStillExists).toBeDefined(); // Should not be deleted
723
+ });
724
+ });
725
+
726
+ describe('External service integration', () => {
727
+ it('should call FileService with correct parameters', async () => {
728
+ const [createdGeneration] = await serverDB
729
+ .insert(generations)
730
+ .values({ ...testGeneration, userId })
731
+ .returning();
732
+
733
+ await generationModel.findByIdAndTransform(createdGeneration.id);
734
+
735
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('asset-url.jpg');
736
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('thumbnail-url.jpg');
737
+ expect(mockGetFullFileUrl).toHaveBeenCalledTimes(2);
738
+ });
739
+
740
+ it('should call FileModel.create with correct parameters during createAssetAndFile', async () => {
741
+ const [createdGeneration] = await serverDB
742
+ .insert(generations)
743
+ .values({ ...testGeneration, userId, asset: null, fileId: null })
744
+ .returning();
745
+
746
+ const asset = {
747
+ url: 'new-asset.jpg',
748
+ thumbnailUrl: 'new-thumbnail.jpg',
749
+ width: 1024,
750
+ height: 1024,
751
+ } as ImageGenerationAsset;
752
+
753
+ const fileData = {
754
+ name: 'test-image.jpg',
755
+ url: 'https://example.com/test-image.jpg',
756
+ size: 1024,
757
+ fileType: 'image/jpeg',
758
+ };
759
+
760
+ await generationModel.createAssetAndFile(createdGeneration.id, asset, fileData);
761
+
762
+ expect(mockFileModelCreate).toHaveBeenCalledWith(
763
+ {
764
+ ...fileData,
765
+ source: FileSource.ImageGeneration,
766
+ },
767
+ true,
768
+ expect.any(Object),
769
+ );
770
+ expect(mockFileModelCreate).toHaveBeenCalledTimes(1);
771
+ });
772
+
773
+ it('should handle FileService errors gracefully', async () => {
774
+ mockGetFullFileUrl.mockRejectedValue(new Error('FileService error'));
775
+
776
+ const [createdGeneration] = await serverDB
777
+ .insert(generations)
778
+ .values({ ...testGeneration, userId })
779
+ .returning();
780
+
781
+ await expect(generationModel.findByIdAndTransform(createdGeneration.id)).rejects.toThrow(
782
+ 'FileService error',
783
+ );
784
+ });
785
+ });
786
+ });