@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,197 @@
1
+ import debug from 'debug';
2
+ import { and, eq } from 'drizzle-orm';
3
+
4
+ import { LobeChatDatabase, Transaction } from '@/database/type';
5
+ import { FileService } from '@/server/services/file';
6
+ import { AsyncTaskError, AsyncTaskStatus } from '@/types/asyncTask';
7
+ import { FileSource } from '@/types/files';
8
+ import { Generation, ImageGenerationAsset } from '@/types/generation';
9
+
10
+ import { NewFile } from '../schemas';
11
+ import {
12
+ GenerationItem,
13
+ GenerationWithAsyncTask,
14
+ NewGeneration,
15
+ generations,
16
+ } from '../schemas/generation';
17
+ import { FileModel } from './file';
18
+
19
+ // Create debug logger
20
+ const log = debug('lobe-image:generation-model');
21
+
22
+ export class GenerationModel {
23
+ private db: LobeChatDatabase;
24
+ private userId: string;
25
+ private fileModel: FileModel;
26
+ private fileService: FileService;
27
+
28
+ constructor(db: LobeChatDatabase, userId: string) {
29
+ this.db = db;
30
+ this.userId = userId;
31
+ this.fileModel = new FileModel(db, userId);
32
+ this.fileService = new FileService(db, userId);
33
+ }
34
+
35
+ async create(value: Omit<NewGeneration, 'userId'>): Promise<GenerationItem> {
36
+ log('Creating generation: %O', {
37
+ generationBatchId: value.generationBatchId,
38
+ userId: this.userId,
39
+ });
40
+
41
+ const [result] = await this.db
42
+ .insert(generations)
43
+ .values({ ...value, userId: this.userId })
44
+ .returning();
45
+
46
+ log('Generation created successfully: %s', result.id);
47
+ return result;
48
+ }
49
+
50
+ async findById(id: string): Promise<GenerationItem | undefined> {
51
+ log('Finding generation by ID: %s for user: %s', id, this.userId);
52
+
53
+ const result = await this.db.query.generations.findFirst({
54
+ where: and(eq(generations.id, id), eq(generations.userId, this.userId)),
55
+ });
56
+
57
+ log('Generation %s: %s', id, result ? 'found' : 'not found');
58
+ return result;
59
+ }
60
+
61
+ async findByIdWithAsyncTask(id: string): Promise<GenerationWithAsyncTask | undefined> {
62
+ log('Finding generation by ID: %s for user: %s', id, this.userId);
63
+
64
+ const result = await this.db.query.generations.findFirst({
65
+ where: and(eq(generations.id, id), eq(generations.userId, this.userId)),
66
+ with: {
67
+ asyncTask: true,
68
+ },
69
+ });
70
+
71
+ log('Generation %s: %s', id, result ? 'found' : 'not found');
72
+ return result as GenerationWithAsyncTask | undefined;
73
+ }
74
+
75
+ async update(id: string, value: Partial<NewGeneration>, trx?: Transaction) {
76
+ log('Updating generation: %s with values: %O', id, {
77
+ asyncTaskId: value.asyncTaskId,
78
+ hasAsset: !!value.asset,
79
+ });
80
+
81
+ const executeUpdate = async (tx: Transaction) => {
82
+ return await tx
83
+ .update(generations)
84
+ .set({ ...value, updatedAt: new Date() })
85
+ .where(and(eq(generations.id, id), eq(generations.userId, this.userId)));
86
+ };
87
+
88
+ const result = await (trx ? executeUpdate(trx) : this.db.transaction(executeUpdate));
89
+
90
+ log('Generation %s updated successfully', id);
91
+ return result;
92
+ }
93
+
94
+ async createAssetAndFile(
95
+ id: string,
96
+ asset: ImageGenerationAsset,
97
+ file: Omit<NewFile, 'id' | 'userId'>,
98
+ ) {
99
+ log('Creating generation asset and file with transaction: %s', id);
100
+
101
+ return await this.db.transaction(async (tx: Transaction) => {
102
+ // Create file first using transaction
103
+ // Since duplicates are very rare, we always create globalFile - checking existence first would be wasteful
104
+ const newFile = await this.fileModel.create(
105
+ {
106
+ ...file,
107
+ source: FileSource.ImageGeneration,
108
+ },
109
+ true,
110
+ tx,
111
+ );
112
+
113
+ // Update generation with asset and fileId using the transaction-aware update method
114
+ await this.update(
115
+ id,
116
+ {
117
+ asset,
118
+ fileId: newFile.id,
119
+ },
120
+ tx,
121
+ );
122
+
123
+ log('Generation %s updated with asset and file %s successfully', id, newFile.id);
124
+
125
+ return {
126
+ file: newFile,
127
+ };
128
+ });
129
+ }
130
+
131
+ async delete(id: string, trx?: Transaction) {
132
+ log('Deleting generation: %s for user: %s', id, this.userId);
133
+
134
+ const executeDelete = async (tx: Transaction) => {
135
+ return await tx
136
+ .delete(generations)
137
+ .where(and(eq(generations.id, id), eq(generations.userId, this.userId)))
138
+ .returning();
139
+ };
140
+
141
+ const result = await (trx ? executeDelete(trx) : this.db.transaction(executeDelete));
142
+ const deletedGeneration = result[0];
143
+
144
+ log('Generation %s deleted successfully', id);
145
+ return deletedGeneration;
146
+ }
147
+
148
+ /**
149
+ * Find generation by ID and transform it to frontend type
150
+ * This method uses findByIdWithAsyncTask and applies transformation
151
+ */
152
+ async findByIdAndTransform(id: string): Promise<Generation | null> {
153
+ log('Finding and transforming generation: %s', id);
154
+
155
+ const generation = await this.findByIdWithAsyncTask(id);
156
+ if (!generation) {
157
+ log('Generation %s not found', id);
158
+ return null;
159
+ }
160
+
161
+ return await this.transformGeneration(generation);
162
+ }
163
+
164
+ /**
165
+ * Transform a GenerationItem (database type) to Generation (frontend type)
166
+ * This method processes asset URLs and async task information
167
+ */
168
+ async transformGeneration(generation: GenerationWithAsyncTask): Promise<Generation> {
169
+ // Process asset URLs if they exist, following the same logic as in generationBatch.ts
170
+ const asset = generation.asset as ImageGenerationAsset | null;
171
+ if (asset && asset.url && asset.thumbnailUrl) {
172
+ const [url, thumbnailUrl] = await Promise.all([
173
+ this.fileService.getFullFileUrl(asset.url),
174
+ this.fileService.getFullFileUrl(asset.thumbnailUrl),
175
+ ]);
176
+ asset.url = url;
177
+ asset.thumbnailUrl = thumbnailUrl;
178
+ }
179
+
180
+ // Build the Generation object following the same structure as in generationBatch.ts
181
+ const result: Generation = {
182
+ asset,
183
+ asyncTaskId: generation.asyncTaskId || null,
184
+ createdAt: generation.createdAt,
185
+ id: generation.id,
186
+ seed: generation.seed,
187
+ task: {
188
+ error: generation.asyncTask?.error
189
+ ? (generation.asyncTask.error as AsyncTaskError)
190
+ : undefined,
191
+ id: generation.asyncTaskId || '',
192
+ status: (generation.asyncTask?.status as AsyncTaskStatus) || 'pending',
193
+ },
194
+ };
195
+ return result;
196
+ }
197
+ }
@@ -0,0 +1,212 @@
1
+ import debug from 'debug';
2
+ import { and, eq } from 'drizzle-orm';
3
+
4
+ import { LobeChatDatabase } from '@/database/type';
5
+ import { FileService } from '@/server/services/file';
6
+ import { Generation, GenerationAsset, GenerationBatch, GenerationConfig } from '@/types/generation';
7
+
8
+ import {
9
+ GenerationBatchItem,
10
+ GenerationBatchWithGenerations,
11
+ NewGenerationBatch,
12
+ generationBatches,
13
+ } from '../schemas/generation';
14
+ import { GenerationModel } from './generation';
15
+
16
+ const log = debug('lobe-image:generation-batch-model');
17
+
18
+ export class GenerationBatchModel {
19
+ private db: LobeChatDatabase;
20
+ private userId: string;
21
+ private fileService: FileService;
22
+ private generationModel: GenerationModel;
23
+
24
+ constructor(db: LobeChatDatabase, userId: string) {
25
+ this.db = db;
26
+ this.userId = userId;
27
+ this.fileService = new FileService(db, userId);
28
+ this.generationModel = new GenerationModel(db, userId);
29
+ }
30
+
31
+ async create(value: NewGenerationBatch): Promise<GenerationBatchItem> {
32
+ log('Creating generation batch: %O', {
33
+ topicId: value.generationTopicId,
34
+ userId: this.userId,
35
+ });
36
+
37
+ const [result] = await this.db
38
+ .insert(generationBatches)
39
+ .values({ ...value, userId: this.userId })
40
+ .returning();
41
+
42
+ log('Generation batch created successfully: %s', result.id);
43
+ return result;
44
+ }
45
+
46
+ async findById(id: string): Promise<GenerationBatchItem | undefined> {
47
+ log('Finding generation batch by ID: %s for user: %s', id, this.userId);
48
+
49
+ const result = await this.db.query.generationBatches.findFirst({
50
+ where: and(eq(generationBatches.id, id), eq(generationBatches.userId, this.userId)),
51
+ });
52
+
53
+ log('Generation batch %s: %s', id, result ? 'found' : 'not found');
54
+ return result;
55
+ }
56
+
57
+ async findByTopicId(topicId: string): Promise<GenerationBatchItem[]> {
58
+ log('Finding generation batches by topic ID: %s for user: %s', topicId, this.userId);
59
+
60
+ const results = await this.db.query.generationBatches.findMany({
61
+ orderBy: (table, { desc }) => [desc(table.createdAt)],
62
+ where: and(
63
+ eq(generationBatches.generationTopicId, topicId),
64
+ eq(generationBatches.userId, this.userId),
65
+ ),
66
+ });
67
+
68
+ log('Found %d generation batches for topic %s', results.length, topicId);
69
+ return results;
70
+ }
71
+
72
+ /**
73
+ * Find batches with their associated generations using relations
74
+ */
75
+ async findByTopicIdWithGenerations(topicId: string): Promise<GenerationBatchWithGenerations[]> {
76
+ log(
77
+ 'Finding generation batches with generations for topic ID: %s for user: %s',
78
+ topicId,
79
+ this.userId,
80
+ );
81
+
82
+ const results = await this.db.query.generationBatches.findMany({
83
+ orderBy: (table, { asc }) => [asc(table.createdAt)],
84
+ where: and(
85
+ eq(generationBatches.generationTopicId, topicId),
86
+ eq(generationBatches.userId, this.userId),
87
+ ),
88
+ with: {
89
+ generations: {
90
+ orderBy: (table, { asc }) => [asc(table.createdAt), asc(table.id)],
91
+ with: {
92
+ asyncTask: true,
93
+ },
94
+ },
95
+ },
96
+ });
97
+
98
+ log('Found %d generation batches with generations for topic %s', results.length, topicId);
99
+ return results as GenerationBatchWithGenerations[];
100
+ }
101
+
102
+ async queryGenerationBatchesByTopicIdWithGenerations(
103
+ topicId: string,
104
+ ): Promise<(GenerationBatch & { generations: Generation[] })[]> {
105
+ log('Fetching generation batches for topic ID: %s for user: %s', topicId, this.userId);
106
+
107
+ const batchesWithGenerations = await this.findByTopicIdWithGenerations(topicId);
108
+ if (batchesWithGenerations.length === 0) {
109
+ log('No batches found for topic: %s', topicId);
110
+ return [];
111
+ }
112
+
113
+ // Transform the database result to match our frontend types
114
+ const result: GenerationBatch[] = await Promise.all(
115
+ batchesWithGenerations.map(async (batch) => {
116
+ const [generations, config] = await Promise.all([
117
+ // Transform generations
118
+ Promise.all(
119
+ batch.generations.map((gen) => this.generationModel.transformGeneration(gen)),
120
+ ),
121
+ // Transform config
122
+ (async () => {
123
+ const config = batch.config as GenerationConfig;
124
+ if (Array.isArray(config.imageUrls)) {
125
+ config.imageUrls = await Promise.all(
126
+ config.imageUrls.map((url) => this.fileService.getFullFileUrl(url)),
127
+ );
128
+ }
129
+ return config;
130
+ })(),
131
+ ]);
132
+
133
+ return {
134
+ config,
135
+ createdAt: batch.createdAt,
136
+ generations,
137
+ height: batch.height,
138
+ id: batch.id,
139
+ model: batch.model,
140
+ prompt: batch.prompt,
141
+ provider: batch.provider,
142
+ width: batch.width,
143
+ };
144
+ }),
145
+ );
146
+
147
+ log('Feed construction complete for topic: %s, returning %d batches', topicId, result.length);
148
+ return result;
149
+ }
150
+
151
+ /**
152
+ * Delete a generation batch and return associated file URLs for cleanup
153
+ *
154
+ * This method follows the "database first, files second" deletion principle:
155
+ * 1. First queries the batch with its generations to collect thumbnail URLs
156
+ * 2. Then deletes the database record (cascade delete handles related generations)
157
+ * 3. Returns the deleted batch data and thumbnail URLs for file cleanup
158
+ *
159
+ * @param id - The batch ID to delete
160
+ * @returns Object containing deleted batch data and thumbnail URLs to clean, or undefined if batch not found or access denied
161
+ */
162
+ async delete(
163
+ id: string,
164
+ ): Promise<{ deletedBatch: GenerationBatchItem; thumbnailUrls: string[] } | undefined> {
165
+ log('Deleting generation batch: %s for user: %s', id, this.userId);
166
+
167
+ // 1. First, get generations with their assets to collect thumbnail URLs
168
+ const batchWithGenerations = await this.db.query.generationBatches.findFirst({
169
+ where: and(eq(generationBatches.id, id), eq(generationBatches.userId, this.userId)),
170
+ with: {
171
+ generations: {
172
+ columns: {
173
+ asset: true,
174
+ },
175
+ },
176
+ },
177
+ });
178
+
179
+ // If batch doesn't exist or doesn't belong to user, return undefined
180
+ if (!batchWithGenerations) {
181
+ return undefined;
182
+ }
183
+
184
+ // 2. Collect thumbnail URLs that need to be deleted
185
+ const thumbnailUrls: string[] = [];
186
+ if (batchWithGenerations.generations) {
187
+ for (const gen of batchWithGenerations.generations) {
188
+ const asset = gen.asset as GenerationAsset;
189
+ if (asset?.thumbnailUrl) {
190
+ thumbnailUrls.push(asset.thumbnailUrl);
191
+ }
192
+ }
193
+ }
194
+
195
+ // 3. Delete the batch record (this will cascade delete all associated generations)
196
+ const [deletedBatch] = await this.db
197
+ .delete(generationBatches)
198
+ .where(and(eq(generationBatches.id, id), eq(generationBatches.userId, this.userId)))
199
+ .returning();
200
+
201
+ log(
202
+ 'Generation batch %s deleted successfully with %d thumbnails to clean',
203
+ id,
204
+ thumbnailUrls.length,
205
+ );
206
+
207
+ return {
208
+ deletedBatch,
209
+ thumbnailUrls,
210
+ };
211
+ }
212
+ }
@@ -0,0 +1,131 @@
1
+ import { and, desc, eq } from 'drizzle-orm/expressions';
2
+
3
+ import { LobeChatDatabase } from '@/database/type';
4
+ import { FileService } from '@/server/services/file';
5
+ import { GenerationAsset, ImageGenerationTopic } from '@/types/generation';
6
+
7
+ import { GenerationTopicItem, generationTopics } from '../schemas/generation';
8
+
9
+ export class GenerationTopicModel {
10
+ private userId: string;
11
+ private db: LobeChatDatabase;
12
+ private fileService: FileService;
13
+
14
+ constructor(db: LobeChatDatabase, userId: string) {
15
+ this.userId = userId;
16
+ this.db = db;
17
+ this.fileService = new FileService(db, userId);
18
+ }
19
+
20
+ queryAll = async () => {
21
+ const topics = await this.db
22
+ .select()
23
+ .from(generationTopics)
24
+ .orderBy(desc(generationTopics.updatedAt))
25
+ .where(eq(generationTopics.userId, this.userId));
26
+
27
+ return Promise.all(
28
+ topics.map(async (topic) => {
29
+ if (topic.coverUrl) {
30
+ return {
31
+ ...topic,
32
+ coverUrl: await this.fileService.getFullFileUrl(topic.coverUrl),
33
+ };
34
+ }
35
+ return topic;
36
+ }),
37
+ );
38
+ };
39
+
40
+ create = async (title: string) => {
41
+ const [newGenerationTopic] = await this.db
42
+ .insert(generationTopics)
43
+ .values({
44
+ title,
45
+ userId: this.userId,
46
+ })
47
+ .returning();
48
+
49
+ return newGenerationTopic;
50
+ };
51
+
52
+ update = async (
53
+ id: string,
54
+ data: Partial<ImageGenerationTopic>,
55
+ ): Promise<GenerationTopicItem | undefined> => {
56
+ const [updatedTopic] = await this.db
57
+ .update(generationTopics)
58
+ .set({ ...data, updatedAt: new Date() })
59
+ .where(and(eq(generationTopics.id, id), eq(generationTopics.userId, this.userId)))
60
+ .returning();
61
+
62
+ return updatedTopic;
63
+ };
64
+
65
+ /**
66
+ * Delete a generation topic and return associated file URLs for cleanup
67
+ *
68
+ * This method follows the "database first, files second" deletion principle:
69
+ * 1. First queries the topic with all its batches and generations to collect file URLs
70
+ * 2. Then deletes the database record (cascade delete handles related batches and generations)
71
+ * 3. Returns the deleted topic data and file URLs for cleanup
72
+ *
73
+ * @param id - The topic ID to delete
74
+ * @returns Object containing deleted topic data and file URLs to clean, or undefined if topic not found or access denied
75
+ */
76
+ delete = async (
77
+ id: string,
78
+ ): Promise<{ deletedTopic: GenerationTopicItem; filesToDelete: string[] } | undefined> => {
79
+ // 1. First, get the topic with all its batches and generations to collect file URLs
80
+ const topicWithBatches = await this.db.query.generationTopics.findFirst({
81
+ where: and(eq(generationTopics.id, id), eq(generationTopics.userId, this.userId)),
82
+ with: {
83
+ batches: {
84
+ with: {
85
+ generations: {
86
+ columns: {
87
+ asset: true,
88
+ },
89
+ },
90
+ },
91
+ },
92
+ },
93
+ });
94
+
95
+ // If topic doesn't exist or doesn't belong to user, return undefined
96
+ if (!topicWithBatches) {
97
+ return undefined;
98
+ }
99
+
100
+ // 2. Collect all file URLs that need to be deleted
101
+ const filesToDelete: string[] = [];
102
+
103
+ // Add cover image URL if exists
104
+ if (topicWithBatches.coverUrl) {
105
+ filesToDelete.push(topicWithBatches.coverUrl);
106
+ }
107
+
108
+ // Add thumbnail URLs from all generations
109
+ if (topicWithBatches.batches) {
110
+ for (const batch of topicWithBatches.batches) {
111
+ for (const gen of batch.generations) {
112
+ const asset = gen.asset as GenerationAsset;
113
+ if (asset?.thumbnailUrl) {
114
+ filesToDelete.push(asset.thumbnailUrl);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ // 3. Delete the topic record (this will cascade delete all batches and generations)
121
+ const [deletedTopic] = await this.db
122
+ .delete(generationTopics)
123
+ .where(and(eq(generationTopics.id, id), eq(generationTopics.userId, this.userId)))
124
+ .returning();
125
+
126
+ return {
127
+ deletedTopic,
128
+ filesToDelete,
129
+ };
130
+ };
131
+ }