@lobehub/chat 1.98.2 → 1.99.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (456) hide show
  1. package/.cursor/rules/backend-architecture.mdc +93 -17
  2. package/.cursor/rules/cursor-ux.mdc +45 -35
  3. package/.cursor/rules/project-introduce.mdc +72 -6
  4. package/.cursor/rules/rules-attach.mdc +16 -7
  5. package/.eslintrc.js +10 -0
  6. package/CHANGELOG.md +27 -0
  7. package/apps/desktop/README.md +7 -0
  8. package/apps/desktop/electron-builder.js +5 -0
  9. package/apps/desktop/package.json +2 -1
  10. package/apps/desktop/src/main/const/dir.ts +3 -0
  11. package/apps/desktop/src/main/controllers/UploadFileCtr.ts +13 -8
  12. package/apps/desktop/src/main/core/App.ts +8 -0
  13. package/apps/desktop/src/main/core/StaticFileServerManager.ts +221 -0
  14. package/apps/desktop/src/main/services/fileSrv.ts +231 -44
  15. package/apps/desktop/src/main/utils/next-electron-rsc.ts +36 -5
  16. package/changelog/v1.json +9 -0
  17. package/docs/development/database-schema.dbml +70 -0
  18. package/locales/ar/common.json +2 -0
  19. package/locales/ar/components.json +35 -0
  20. package/locales/ar/error.json +2 -0
  21. package/locales/ar/image.json +100 -0
  22. package/locales/ar/metadata.json +4 -0
  23. package/locales/ar/modelProvider.json +1 -0
  24. package/locales/ar/models.json +15 -0
  25. package/locales/ar/plugin.json +22 -0
  26. package/locales/ar/providers.json +3 -0
  27. package/locales/ar/setting.json +5 -0
  28. package/locales/bg-BG/common.json +2 -0
  29. package/locales/bg-BG/components.json +35 -0
  30. package/locales/bg-BG/error.json +2 -0
  31. package/locales/bg-BG/image.json +100 -0
  32. package/locales/bg-BG/metadata.json +4 -0
  33. package/locales/bg-BG/modelProvider.json +1 -0
  34. package/locales/bg-BG/models.json +15 -0
  35. package/locales/bg-BG/plugin.json +22 -0
  36. package/locales/bg-BG/providers.json +3 -0
  37. package/locales/bg-BG/setting.json +5 -0
  38. package/locales/de-DE/common.json +2 -0
  39. package/locales/de-DE/components.json +35 -0
  40. package/locales/de-DE/error.json +2 -0
  41. package/locales/de-DE/image.json +100 -0
  42. package/locales/de-DE/metadata.json +4 -0
  43. package/locales/de-DE/modelProvider.json +1 -0
  44. package/locales/de-DE/models.json +15 -0
  45. package/locales/de-DE/plugin.json +22 -0
  46. package/locales/de-DE/providers.json +3 -0
  47. package/locales/de-DE/setting.json +5 -0
  48. package/locales/en-US/common.json +2 -0
  49. package/locales/en-US/components.json +35 -0
  50. package/locales/en-US/error.json +2 -0
  51. package/locales/en-US/image.json +100 -0
  52. package/locales/en-US/metadata.json +4 -0
  53. package/locales/en-US/modelProvider.json +1 -0
  54. package/locales/en-US/models.json +15 -0
  55. package/locales/en-US/plugin.json +22 -0
  56. package/locales/en-US/providers.json +3 -0
  57. package/locales/en-US/setting.json +5 -0
  58. package/locales/es-ES/common.json +2 -0
  59. package/locales/es-ES/components.json +35 -0
  60. package/locales/es-ES/error.json +2 -0
  61. package/locales/es-ES/image.json +100 -0
  62. package/locales/es-ES/metadata.json +4 -0
  63. package/locales/es-ES/modelProvider.json +1 -0
  64. package/locales/es-ES/models.json +15 -0
  65. package/locales/es-ES/plugin.json +22 -0
  66. package/locales/es-ES/providers.json +3 -0
  67. package/locales/es-ES/setting.json +5 -0
  68. package/locales/fa-IR/common.json +2 -0
  69. package/locales/fa-IR/components.json +35 -0
  70. package/locales/fa-IR/error.json +2 -0
  71. package/locales/fa-IR/image.json +100 -0
  72. package/locales/fa-IR/metadata.json +4 -0
  73. package/locales/fa-IR/modelProvider.json +1 -0
  74. package/locales/fa-IR/models.json +15 -0
  75. package/locales/fa-IR/plugin.json +22 -0
  76. package/locales/fa-IR/providers.json +3 -0
  77. package/locales/fa-IR/setting.json +5 -0
  78. package/locales/fr-FR/common.json +2 -0
  79. package/locales/fr-FR/components.json +35 -0
  80. package/locales/fr-FR/error.json +2 -0
  81. package/locales/fr-FR/image.json +100 -0
  82. package/locales/fr-FR/metadata.json +4 -0
  83. package/locales/fr-FR/modelProvider.json +1 -0
  84. package/locales/fr-FR/models.json +15 -0
  85. package/locales/fr-FR/plugin.json +22 -0
  86. package/locales/fr-FR/providers.json +3 -0
  87. package/locales/fr-FR/setting.json +5 -0
  88. package/locales/it-IT/common.json +2 -0
  89. package/locales/it-IT/components.json +35 -0
  90. package/locales/it-IT/error.json +2 -0
  91. package/locales/it-IT/image.json +100 -0
  92. package/locales/it-IT/metadata.json +4 -0
  93. package/locales/it-IT/modelProvider.json +1 -0
  94. package/locales/it-IT/models.json +15 -0
  95. package/locales/it-IT/plugin.json +22 -0
  96. package/locales/it-IT/providers.json +3 -0
  97. package/locales/it-IT/setting.json +5 -0
  98. package/locales/ja-JP/common.json +2 -0
  99. package/locales/ja-JP/components.json +35 -0
  100. package/locales/ja-JP/error.json +2 -0
  101. package/locales/ja-JP/image.json +100 -0
  102. package/locales/ja-JP/metadata.json +4 -0
  103. package/locales/ja-JP/modelProvider.json +1 -0
  104. package/locales/ja-JP/models.json +15 -0
  105. package/locales/ja-JP/plugin.json +22 -0
  106. package/locales/ja-JP/providers.json +3 -0
  107. package/locales/ja-JP/setting.json +5 -0
  108. package/locales/ko-KR/common.json +2 -0
  109. package/locales/ko-KR/components.json +35 -0
  110. package/locales/ko-KR/error.json +2 -0
  111. package/locales/ko-KR/image.json +100 -0
  112. package/locales/ko-KR/metadata.json +4 -0
  113. package/locales/ko-KR/modelProvider.json +1 -0
  114. package/locales/ko-KR/models.json +15 -0
  115. package/locales/ko-KR/plugin.json +22 -0
  116. package/locales/ko-KR/providers.json +3 -0
  117. package/locales/ko-KR/setting.json +5 -0
  118. package/locales/nl-NL/common.json +2 -0
  119. package/locales/nl-NL/components.json +35 -0
  120. package/locales/nl-NL/error.json +2 -0
  121. package/locales/nl-NL/image.json +100 -0
  122. package/locales/nl-NL/metadata.json +4 -0
  123. package/locales/nl-NL/modelProvider.json +1 -0
  124. package/locales/nl-NL/models.json +15 -0
  125. package/locales/nl-NL/plugin.json +22 -0
  126. package/locales/nl-NL/providers.json +3 -0
  127. package/locales/nl-NL/setting.json +5 -0
  128. package/locales/pl-PL/common.json +2 -0
  129. package/locales/pl-PL/components.json +35 -0
  130. package/locales/pl-PL/error.json +2 -0
  131. package/locales/pl-PL/image.json +100 -0
  132. package/locales/pl-PL/metadata.json +4 -0
  133. package/locales/pl-PL/modelProvider.json +1 -0
  134. package/locales/pl-PL/models.json +15 -0
  135. package/locales/pl-PL/plugin.json +22 -0
  136. package/locales/pl-PL/providers.json +3 -0
  137. package/locales/pl-PL/setting.json +5 -0
  138. package/locales/pt-BR/common.json +2 -0
  139. package/locales/pt-BR/components.json +35 -0
  140. package/locales/pt-BR/error.json +2 -0
  141. package/locales/pt-BR/image.json +100 -0
  142. package/locales/pt-BR/metadata.json +4 -0
  143. package/locales/pt-BR/modelProvider.json +1 -0
  144. package/locales/pt-BR/models.json +15 -0
  145. package/locales/pt-BR/plugin.json +22 -0
  146. package/locales/pt-BR/providers.json +3 -0
  147. package/locales/pt-BR/setting.json +5 -0
  148. package/locales/ru-RU/common.json +2 -0
  149. package/locales/ru-RU/components.json +35 -0
  150. package/locales/ru-RU/error.json +2 -0
  151. package/locales/ru-RU/image.json +100 -0
  152. package/locales/ru-RU/metadata.json +4 -0
  153. package/locales/ru-RU/modelProvider.json +1 -0
  154. package/locales/ru-RU/models.json +15 -0
  155. package/locales/ru-RU/plugin.json +22 -0
  156. package/locales/ru-RU/providers.json +3 -0
  157. package/locales/ru-RU/setting.json +5 -0
  158. package/locales/tr-TR/common.json +2 -0
  159. package/locales/tr-TR/components.json +35 -0
  160. package/locales/tr-TR/error.json +2 -0
  161. package/locales/tr-TR/image.json +100 -0
  162. package/locales/tr-TR/metadata.json +4 -0
  163. package/locales/tr-TR/modelProvider.json +1 -0
  164. package/locales/tr-TR/models.json +15 -0
  165. package/locales/tr-TR/plugin.json +22 -0
  166. package/locales/tr-TR/providers.json +3 -0
  167. package/locales/tr-TR/setting.json +5 -0
  168. package/locales/vi-VN/common.json +2 -0
  169. package/locales/vi-VN/components.json +35 -0
  170. package/locales/vi-VN/error.json +2 -0
  171. package/locales/vi-VN/image.json +100 -0
  172. package/locales/vi-VN/metadata.json +4 -0
  173. package/locales/vi-VN/modelProvider.json +1 -0
  174. package/locales/vi-VN/models.json +15 -0
  175. package/locales/vi-VN/plugin.json +22 -0
  176. package/locales/vi-VN/providers.json +3 -0
  177. package/locales/vi-VN/setting.json +5 -0
  178. package/locales/zh-CN/common.json +2 -0
  179. package/locales/zh-CN/components.json +35 -0
  180. package/locales/zh-CN/error.json +2 -0
  181. package/locales/zh-CN/image.json +100 -0
  182. package/locales/zh-CN/metadata.json +4 -0
  183. package/locales/zh-CN/modelProvider.json +1 -0
  184. package/locales/zh-CN/models.json +15 -0
  185. package/locales/zh-CN/plugin.json +22 -0
  186. package/locales/zh-CN/providers.json +3 -0
  187. package/locales/zh-CN/setting.json +5 -0
  188. package/locales/zh-TW/common.json +2 -0
  189. package/locales/zh-TW/components.json +35 -0
  190. package/locales/zh-TW/error.json +2 -0
  191. package/locales/zh-TW/image.json +100 -0
  192. package/locales/zh-TW/metadata.json +4 -0
  193. package/locales/zh-TW/modelProvider.json +1 -0
  194. package/locales/zh-TW/models.json +15 -0
  195. package/locales/zh-TW/plugin.json +22 -0
  196. package/locales/zh-TW/providers.json +3 -0
  197. package/locales/zh-TW/setting.json +5 -0
  198. package/package.json +11 -4
  199. package/packages/electron-server-ipc/src/events/file.ts +3 -1
  200. package/packages/electron-server-ipc/src/types/file.ts +15 -0
  201. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/TopActions.tsx +11 -1
  202. package/src/app/[variants]/(main)/image/@menu/components/AspectRatioSelect/index.tsx +73 -0
  203. package/src/app/[variants]/(main)/image/@menu/components/SeedNumberInput/index.tsx +39 -0
  204. package/src/app/[variants]/(main)/image/@menu/components/SizeSelect/index.tsx +89 -0
  205. package/src/app/[variants]/(main)/image/@menu/default.tsx +11 -0
  206. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/AspectRatioSelect.tsx +24 -0
  207. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/DimensionControlGroup.tsx +107 -0
  208. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageNum.tsx +290 -0
  209. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUpload.tsx +504 -0
  210. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrl.tsx +18 -0
  211. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ImageUrlsUpload.tsx +19 -0
  212. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +155 -0
  213. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/ImageManageModal.tsx +415 -0
  214. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +732 -0
  215. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SeedNumberInput.tsx +24 -0
  216. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSelect.tsx +17 -0
  217. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/SizeSliderInput.tsx +15 -0
  218. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/StepsSliderInput.tsx +11 -0
  219. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/constants.ts +1 -0
  220. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +93 -0
  221. package/src/app/[variants]/(main)/image/@topic/default.tsx +17 -0
  222. package/src/app/[variants]/(main)/image/@topic/features/Topics/NewTopicButton.tsx +64 -0
  223. package/src/app/[variants]/(main)/image/@topic/features/Topics/SkeletonList.tsx +34 -0
  224. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItem.tsx +136 -0
  225. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicItemContainer.tsx +91 -0
  226. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicList.tsx +57 -0
  227. package/src/app/[variants]/(main)/image/@topic/features/Topics/TopicUrlSync.tsx +37 -0
  228. package/src/app/[variants]/(main)/image/@topic/features/Topics/index.tsx +19 -0
  229. package/src/app/[variants]/(main)/image/NotSupportClient.tsx +153 -0
  230. package/src/app/[variants]/(main)/image/_layout/Desktop/Container.tsx +35 -0
  231. package/src/app/[variants]/(main)/image/_layout/Desktop/RegisterHotkeys.tsx +10 -0
  232. package/src/app/[variants]/(main)/image/_layout/Desktop/index.tsx +30 -0
  233. package/src/app/[variants]/(main)/image/_layout/Mobile/index.tsx +14 -0
  234. package/src/app/[variants]/(main)/image/_layout/type.ts +7 -0
  235. package/src/app/[variants]/(main)/image/features/GenerationFeed/BatchItem.tsx +196 -0
  236. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ActionButtons.tsx +60 -0
  237. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ElapsedTime.tsx +90 -0
  238. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/ErrorState.tsx +65 -0
  239. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/LoadingState.tsx +43 -0
  240. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/SuccessState.tsx +49 -0
  241. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/index.tsx +156 -0
  242. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/styles.ts +51 -0
  243. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/types.ts +39 -0
  244. package/src/app/[variants]/(main)/image/features/GenerationFeed/GenerationItem/utils.ts +11 -0
  245. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +97 -0
  246. package/src/app/[variants]/(main)/image/features/ImageWorkspace/Content.tsx +48 -0
  247. package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +37 -0
  248. package/src/app/[variants]/(main)/image/features/ImageWorkspace/SkeletonList.tsx +50 -0
  249. package/src/app/[variants]/(main)/image/features/ImageWorkspace/index.tsx +23 -0
  250. package/src/app/[variants]/(main)/image/features/PromptInput/Title.tsx +38 -0
  251. package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +114 -0
  252. package/src/app/[variants]/(main)/image/layout.tsx +19 -0
  253. package/src/app/[variants]/(main)/image/loading.tsx +3 -0
  254. package/src/app/[variants]/(main)/image/page.tsx +47 -0
  255. package/src/app/[variants]/(main)/settings/system-agent/index.tsx +2 -1
  256. package/src/chains/summaryGenerationTitle.ts +25 -0
  257. package/src/components/ImageItem/index.tsx +9 -6
  258. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/Bedrock.tsx +3 -4
  259. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/ProviderApiKeyForm.tsx +5 -4
  260. package/src/components/InvalidAPIKey/APIKeyForm/index.tsx +108 -0
  261. package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/useApiKey.ts +2 -1
  262. package/src/components/InvalidAPIKey/index.tsx +30 -0
  263. package/src/components/KeyValueEditor/index.tsx +203 -0
  264. package/src/components/KeyValueEditor/utils.ts +42 -0
  265. package/src/config/aiModels/fal.ts +52 -0
  266. package/src/config/aiModels/index.ts +3 -0
  267. package/src/config/aiModels/openai.ts +20 -6
  268. package/src/config/llm.ts +6 -0
  269. package/src/config/modelProviders/fal.ts +21 -0
  270. package/src/config/modelProviders/index.ts +3 -0
  271. package/src/config/paramsSchemas/fal/flux-kontext-dev.ts +8 -0
  272. package/src/config/paramsSchemas/fal/flux-pro-kontext.ts +11 -0
  273. package/src/config/paramsSchemas/fal/flux-schnell.ts +9 -0
  274. package/src/config/paramsSchemas/fal/imagen4.ts +10 -0
  275. package/src/config/paramsSchemas/openai/gpt-image-1.ts +10 -0
  276. package/src/const/hotkeys.ts +2 -2
  277. package/src/const/image.ts +6 -0
  278. package/src/const/settings/systemAgent.ts +1 -0
  279. package/src/database/client/migrations.json +27 -0
  280. package/src/database/migrations/0026_add_autovacuum_tuning.sql +2 -0
  281. package/src/database/migrations/0027_ai_image.sql +47 -0
  282. package/src/database/migrations/meta/0027_snapshot.json +6003 -0
  283. package/src/database/migrations/meta/_journal.json +7 -0
  284. package/src/database/models/__tests__/asyncTask.test.ts +7 -5
  285. package/src/database/models/__tests__/file.test.ts +287 -0
  286. package/src/database/models/__tests__/generation.test.ts +786 -0
  287. package/src/database/models/__tests__/generationBatch.test.ts +614 -0
  288. package/src/database/models/__tests__/generationTopic.test.ts +411 -0
  289. package/src/database/models/aiModel.ts +2 -0
  290. package/src/database/models/asyncTask.ts +1 -1
  291. package/src/database/models/file.ts +28 -20
  292. package/src/database/models/generation.ts +197 -0
  293. package/src/database/models/generationBatch.ts +212 -0
  294. package/src/database/models/generationTopic.ts +131 -0
  295. package/src/database/repositories/aiInfra/index.test.ts +151 -1
  296. package/src/database/repositories/aiInfra/index.ts +28 -19
  297. package/src/database/repositories/tableViewer/index.test.ts +1 -1
  298. package/src/database/schemas/file.ts +8 -0
  299. package/src/database/schemas/generation.ts +127 -0
  300. package/src/database/schemas/index.ts +1 -0
  301. package/src/database/schemas/relations.ts +45 -1
  302. package/src/database/type.ts +2 -0
  303. package/src/database/utils/idGenerator.ts +3 -0
  304. package/src/features/Conversation/Error/ChatInvalidApiKey.tsx +39 -0
  305. package/src/features/Conversation/Error/InvalidAccessCode.tsx +2 -2
  306. package/src/features/Conversation/Error/index.tsx +3 -3
  307. package/src/features/ImageSidePanel/index.tsx +83 -0
  308. package/src/features/ImageTopicPanel/index.tsx +79 -0
  309. package/src/features/PluginDevModal/MCPManifestForm/CollapsibleSection.tsx +62 -0
  310. package/src/features/PluginDevModal/MCPManifestForm/QuickImportSection.tsx +158 -0
  311. package/src/features/PluginDevModal/MCPManifestForm/index.tsx +99 -155
  312. package/src/features/PluginStore/McpList/Detail/Settings/index.tsx +5 -2
  313. package/src/hooks/useDownloadImage.ts +31 -0
  314. package/src/hooks/useFetchGenerationTopics.ts +13 -0
  315. package/src/hooks/useHotkeys/imageScope.ts +48 -0
  316. package/src/libs/mcp/client.ts +55 -22
  317. package/src/libs/mcp/types.ts +42 -6
  318. package/src/libs/model-runtime/BaseAI.ts +3 -1
  319. package/src/libs/model-runtime/ModelRuntime.test.ts +80 -0
  320. package/src/libs/model-runtime/ModelRuntime.ts +15 -1
  321. package/src/libs/model-runtime/UniformRuntime/index.ts +4 -1
  322. package/src/libs/model-runtime/fal/index.test.ts +442 -0
  323. package/src/libs/model-runtime/fal/index.ts +88 -0
  324. package/src/libs/model-runtime/openai/index.test.ts +396 -2
  325. package/src/libs/model-runtime/openai/index.ts +129 -3
  326. package/src/libs/model-runtime/runtimeMap.ts +2 -0
  327. package/src/libs/model-runtime/types/image.ts +25 -0
  328. package/src/libs/model-runtime/types/type.ts +1 -0
  329. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +10 -0
  330. package/src/libs/standard-parameters/index.ts +1 -0
  331. package/src/libs/standard-parameters/meta-schema.test.ts +214 -0
  332. package/src/libs/standard-parameters/meta-schema.ts +147 -0
  333. package/src/libs/swr/index.ts +1 -0
  334. package/src/libs/trpc/async/asyncAuth.ts +29 -8
  335. package/src/libs/trpc/async/context.ts +42 -4
  336. package/src/libs/trpc/async/index.ts +17 -4
  337. package/src/libs/trpc/async/init.ts +8 -0
  338. package/src/libs/trpc/client/lambda.ts +19 -2
  339. package/src/locales/default/common.ts +2 -0
  340. package/src/locales/default/components.ts +35 -0
  341. package/src/locales/default/error.ts +2 -0
  342. package/src/locales/default/image.ts +100 -0
  343. package/src/locales/default/index.ts +2 -0
  344. package/src/locales/default/metadata.ts +4 -0
  345. package/src/locales/default/modelProvider.ts +2 -0
  346. package/src/locales/default/plugin.ts +22 -0
  347. package/src/locales/default/setting.ts +5 -0
  348. package/src/middleware.ts +1 -0
  349. package/src/server/modules/ElectronIPCClient/index.ts +9 -1
  350. package/src/server/modules/S3/index.ts +15 -0
  351. package/src/server/routers/async/caller.ts +9 -1
  352. package/src/server/routers/async/image.ts +253 -0
  353. package/src/server/routers/async/index.ts +2 -0
  354. package/src/server/routers/lambda/aiProvider.test.ts +1 -0
  355. package/src/server/routers/lambda/generation.test.ts +267 -0
  356. package/src/server/routers/lambda/generation.ts +86 -0
  357. package/src/server/routers/lambda/generationBatch.test.ts +376 -0
  358. package/src/server/routers/lambda/generationBatch.ts +56 -0
  359. package/src/server/routers/lambda/generationTopic.test.ts +508 -0
  360. package/src/server/routers/lambda/generationTopic.ts +93 -0
  361. package/src/server/routers/lambda/image.ts +248 -0
  362. package/src/server/routers/lambda/index.ts +8 -0
  363. package/src/server/routers/tools/mcp.ts +15 -0
  364. package/src/server/services/file/__tests__/index.test.ts +135 -0
  365. package/src/server/services/file/impls/local.test.ts +153 -52
  366. package/src/server/services/file/impls/local.ts +70 -46
  367. package/src/server/services/file/impls/s3.test.ts +114 -0
  368. package/src/server/services/file/impls/s3.ts +40 -0
  369. package/src/server/services/file/impls/type.ts +10 -0
  370. package/src/server/services/file/index.ts +14 -0
  371. package/src/server/services/generation/index.ts +239 -0
  372. package/src/server/services/mcp/index.ts +20 -2
  373. package/src/services/__tests__/generation.test.ts +40 -0
  374. package/src/services/__tests__/generationBatch.test.ts +36 -0
  375. package/src/services/__tests__/generationTopic.test.ts +72 -0
  376. package/src/services/electron/file.ts +3 -1
  377. package/src/services/generation.ts +16 -0
  378. package/src/services/generationBatch.ts +25 -0
  379. package/src/services/generationTopic.ts +28 -0
  380. package/src/services/image.ts +33 -0
  381. package/src/services/mcp.ts +12 -7
  382. package/src/services/upload.ts +43 -9
  383. package/src/store/aiInfra/slices/aiProvider/action.ts +25 -5
  384. package/src/store/aiInfra/slices/aiProvider/initialState.ts +1 -0
  385. package/src/store/aiInfra/slices/aiProvider/selectors.ts +3 -0
  386. package/src/store/chat/slices/aiChat/actions/generateAIChat.ts +5 -5
  387. package/src/store/chat/slices/message/action.ts +2 -2
  388. package/src/store/chat/slices/translate/action.ts +1 -1
  389. package/src/store/global/initialState.ts +9 -0
  390. package/src/store/global/selectors/systemStatus.ts +8 -0
  391. package/src/store/image/index.ts +2 -0
  392. package/src/store/image/initialState.ts +25 -0
  393. package/src/store/image/selectors.ts +4 -0
  394. package/src/store/image/slices/createImage/action.test.ts +330 -0
  395. package/src/store/image/slices/createImage/action.ts +134 -0
  396. package/src/store/image/slices/createImage/initialState.ts +9 -0
  397. package/src/store/image/slices/createImage/selectors.test.ts +114 -0
  398. package/src/store/image/slices/createImage/selectors.ts +9 -0
  399. package/src/store/image/slices/generationBatch/action.test.ts +495 -0
  400. package/src/store/image/slices/generationBatch/action.ts +303 -0
  401. package/src/store/image/slices/generationBatch/initialState.ts +13 -0
  402. package/src/store/image/slices/generationBatch/reducer.test.ts +568 -0
  403. package/src/store/image/slices/generationBatch/reducer.ts +101 -0
  404. package/src/store/image/slices/generationBatch/selectors.test.ts +307 -0
  405. package/src/store/image/slices/generationBatch/selectors.ts +36 -0
  406. package/src/store/image/slices/generationConfig/action.test.ts +351 -0
  407. package/src/store/image/slices/generationConfig/action.ts +295 -0
  408. package/src/store/image/slices/generationConfig/hooks.test.ts +304 -0
  409. package/src/store/image/slices/generationConfig/hooks.ts +118 -0
  410. package/src/store/image/slices/generationConfig/index.ts +1 -0
  411. package/src/store/image/slices/generationConfig/initialState.ts +37 -0
  412. package/src/store/image/slices/generationConfig/selectors.test.ts +204 -0
  413. package/src/store/image/slices/generationConfig/selectors.ts +25 -0
  414. package/src/store/image/slices/generationTopic/action.test.ts +687 -0
  415. package/src/store/image/slices/generationTopic/action.ts +319 -0
  416. package/src/store/image/slices/generationTopic/index.ts +2 -0
  417. package/src/store/image/slices/generationTopic/initialState.ts +14 -0
  418. package/src/store/image/slices/generationTopic/reducer.test.ts +198 -0
  419. package/src/store/image/slices/generationTopic/reducer.ts +66 -0
  420. package/src/store/image/slices/generationTopic/selectors.test.ts +103 -0
  421. package/src/store/image/slices/generationTopic/selectors.ts +15 -0
  422. package/src/store/image/store.ts +42 -0
  423. package/src/store/image/utils/size.ts +51 -0
  424. package/src/store/tool/slices/customPlugin/action.ts +10 -1
  425. package/src/store/tool/slices/mcpStore/action.ts +6 -4
  426. package/src/store/user/slices/settings/selectors/__snapshots__/settings.test.ts.snap +4 -0
  427. package/src/store/user/slices/settings/selectors/systemAgent.ts +2 -0
  428. package/src/types/aiModel.ts +8 -3
  429. package/src/types/aiProvider.ts +1 -0
  430. package/src/types/asyncTask.ts +2 -0
  431. package/src/types/files/index.ts +5 -0
  432. package/src/types/generation/index.ts +80 -0
  433. package/src/types/hotkey.ts +2 -0
  434. package/src/types/plugins/mcp.ts +2 -6
  435. package/src/types/tool/plugin.ts +8 -0
  436. package/src/types/user/settings/keyVaults.ts +5 -0
  437. package/src/types/user/settings/systemAgent.ts +1 -0
  438. package/src/utils/client/downloadFile.ts +33 -4
  439. package/src/utils/number.test.ts +105 -0
  440. package/src/utils/number.ts +25 -0
  441. package/src/utils/server/__tests__/geo.test.ts +6 -3
  442. package/src/utils/storeDebug.test.ts +152 -0
  443. package/src/utils/storeDebug.ts +16 -7
  444. package/src/utils/time.test.ts +259 -0
  445. package/src/utils/time.ts +18 -0
  446. package/src/utils/units.ts +61 -0
  447. package/src/utils/url.test.ts +358 -9
  448. package/src/utils/url.ts +105 -3
  449. package/{vitest.server.config.ts → vitest.config.server.ts} +3 -0
  450. package/.cursor/rules/i18n/i18n-auto-attached.mdc +0 -6
  451. package/src/features/Conversation/Error/APIKeyForm/index.tsx +0 -105
  452. package/src/features/Conversation/Error/InvalidAPIKey.tsx +0 -16
  453. package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +0 -227
  454. /package/.cursor/rules/{i18n/i18n.mdc → i18n.mdc} +0 -0
  455. /package/src/app/[variants]/(main)/settings/system-agent/features/{createForm.tsx → SystemAgentForm.tsx} +0 -0
  456. /package/src/{features/Conversation/Error → components/InvalidAPIKey}/APIKeyForm/LoadingContext.ts +0 -0
@@ -0,0 +1,239 @@
1
+ import debug from 'debug';
2
+ import { sha256 } from 'js-sha256';
3
+ import mime from 'mime';
4
+ import { nanoid } from 'nanoid';
5
+ import sharp from 'sharp';
6
+
7
+ import { LobeChatDatabase } from '@/database/type';
8
+ import { FileService } from '@/server/services/file';
9
+ import { getYYYYmmddHHMMss } from '@/utils/time';
10
+
11
+ const log = debug('lobe-image:generation-service');
12
+
13
+ interface ImageForGeneration {
14
+ buffer: Buffer;
15
+ extension: string;
16
+ hash: string;
17
+ height: number;
18
+ mime: string;
19
+ size: number;
20
+ width: number;
21
+ }
22
+
23
+ /**
24
+ * 图片生成服务
25
+ * 负责处理AI生成图片的转换、上传和封面创建
26
+ */
27
+ export class GenerationService {
28
+ private fileService: FileService;
29
+
30
+ constructor(db: LobeChatDatabase, userId: string) {
31
+ this.fileService = new FileService(db, userId);
32
+ }
33
+
34
+ /**
35
+ * Generate width 512px image as thumbnail when width > 512, end with _512.webp
36
+ */
37
+ async transformImageForGeneration(url: string): Promise<{
38
+ image: ImageForGeneration;
39
+ thumbnailImage: ImageForGeneration;
40
+ }> {
41
+ log('Starting image transformation for:', url.startsWith('data:') ? 'base64 data' : url);
42
+
43
+ // If the url is in base64 format, extract the Buffer directly; otherwise, use fetch to get the Buffer
44
+ let originalImageBuffer: Buffer;
45
+ let originalMimeType: string;
46
+
47
+ if (url.startsWith('data:')) {
48
+ log('Processing base64 image data');
49
+ // Extract the MIME type and base64 data part
50
+ const [mimeTypePart, base64Data] = url.split(',');
51
+ originalMimeType = mimeTypePart.split(':')[1].split(';')[0];
52
+ originalImageBuffer = Buffer.from(base64Data, 'base64');
53
+ } else {
54
+ log('Fetching image from URL:', url);
55
+ const response = await fetch(url);
56
+ if (!response.ok) {
57
+ throw new Error(
58
+ `Failed to fetch image from ${url}: ${response.status} ${response.statusText}`,
59
+ );
60
+ }
61
+ const arrayBuffer = await response.arrayBuffer();
62
+ originalImageBuffer = Buffer.from(arrayBuffer);
63
+ originalMimeType = response.headers.get('content-type') || 'application/octet-stream';
64
+ log('Successfully fetched image, buffer size:', originalImageBuffer.length);
65
+ }
66
+
67
+ // Calculate hash for original image
68
+ const originalHash = sha256(originalImageBuffer);
69
+ log('Original image hash calculated:', originalHash);
70
+
71
+ const sharpInstance = sharp(originalImageBuffer);
72
+ const { format, width, height } = await sharpInstance.metadata();
73
+ log('Image metadata:', { format, height, width });
74
+
75
+ if (!width || !height) {
76
+ throw new Error(`Invalid image format: ${format}, url: ${url}`);
77
+ }
78
+
79
+ const shouldResize = format !== 'webp' || width > 512 || height > 512;
80
+ const thumbnailWidth = shouldResize
81
+ ? width > height
82
+ ? 512
83
+ : Math.round((width * 512) / height)
84
+ : width;
85
+ const thumbnailHeight = shouldResize
86
+ ? height > width
87
+ ? 512
88
+ : Math.round((height * 512) / width)
89
+ : height;
90
+
91
+ log('Thumbnail dimensions calculated:', { shouldResize, thumbnailHeight, thumbnailWidth });
92
+
93
+ const thumbnailBuffer = shouldResize
94
+ ? await sharpInstance.resize(thumbnailWidth, thumbnailHeight).webp().toBuffer()
95
+ : originalImageBuffer;
96
+
97
+ // Calculate hash for thumbnail
98
+ const thumbnailHash = sha256(thumbnailBuffer);
99
+ log('Thumbnail image hash calculated:', thumbnailHash);
100
+
101
+ log('Image transformation completed successfully');
102
+
103
+ // Determine extension without fallback
104
+ let extension: string;
105
+ if (url.startsWith('data:')) {
106
+ const mimeExtension = mime.getExtension(originalMimeType);
107
+ if (!mimeExtension) {
108
+ throw new Error(`Unable to determine file extension for MIME type: ${originalMimeType}`);
109
+ }
110
+ extension = mimeExtension;
111
+ } else {
112
+ const urlExtension = url.split('.').pop();
113
+ if (!urlExtension) {
114
+ throw new Error(`Unable to determine file extension from URL: ${url}`);
115
+ }
116
+ extension = urlExtension;
117
+ }
118
+
119
+ return {
120
+ image: {
121
+ buffer: originalImageBuffer,
122
+ extension,
123
+ hash: originalHash,
124
+ height,
125
+ mime: originalMimeType,
126
+ size: originalImageBuffer.length,
127
+ width,
128
+ },
129
+ thumbnailImage: {
130
+ buffer: thumbnailBuffer,
131
+ extension: 'webp',
132
+ hash: thumbnailHash,
133
+ height: thumbnailHeight,
134
+ mime: 'image/webp',
135
+ size: thumbnailBuffer.length,
136
+ width: thumbnailWidth,
137
+ },
138
+ };
139
+ }
140
+
141
+ async uploadImageForGeneration(image: ImageForGeneration, thumbnail: ImageForGeneration) {
142
+ log('Starting image upload for generation');
143
+
144
+ const generationImagesFolder = 'generations/images';
145
+ const uuid = nanoid();
146
+ const dateTime = getYYYYmmddHHMMss(new Date());
147
+ const pathPrefix = `${generationImagesFolder}/${uuid}_${image.width}x${image.height}_${dateTime}`;
148
+ const imageKey = `${pathPrefix}_raw.${image.extension}`;
149
+ const thumbnailKey = `${pathPrefix}_thumb.${thumbnail.extension}`;
150
+
151
+ log('Generated paths:', { imagePath: imageKey, thumbnailPath: thumbnailKey });
152
+
153
+ // Check if image and thumbnail buffers are identical
154
+ const isIdenticalBuffer = image.buffer.equals(thumbnail.buffer);
155
+ log('Buffer comparison:', {
156
+ imageSize: image.buffer.length,
157
+ isIdenticalBuffer,
158
+ thumbnailSize: thumbnail.buffer.length,
159
+ });
160
+
161
+ if (isIdenticalBuffer) {
162
+ log('Buffers are identical, uploading single image');
163
+ // If buffers are identical, only upload once
164
+ const result = await this.fileService.uploadMedia(imageKey, image.buffer);
165
+ log('Single image uploaded successfully:', result.key);
166
+ // Use the same key for both image and thumbnail
167
+ return {
168
+ imageUrl: result.key,
169
+ thumbnailImageUrl: result.key,
170
+ };
171
+ } else {
172
+ log('Buffers are different, uploading both images');
173
+ // If buffers are different, upload both
174
+ const [imageResult, thumbnailResult] = await Promise.all([
175
+ this.fileService.uploadMedia(imageKey, image.buffer),
176
+ this.fileService.uploadMedia(thumbnailKey, thumbnail.buffer),
177
+ ]);
178
+
179
+ log('Both images uploaded successfully:', {
180
+ imageUrl: imageResult.key,
181
+ thumbnailImageUrl: thumbnailResult.key,
182
+ });
183
+
184
+ return {
185
+ imageUrl: imageResult.key,
186
+ thumbnailImageUrl: thumbnailResult.key,
187
+ };
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Create a 256x256 cover image from a given URL and upload
193
+ * @param coverUrl - The source image URL (can be base64 or HTTP URL)
194
+ * @returns The key of the uploaded cover image
195
+ */
196
+ async createCoverFromUrl(coverUrl: string): Promise<string> {
197
+ log('Creating cover image from URL:', coverUrl.startsWith('data:') ? 'base64 data' : coverUrl);
198
+
199
+ // Download the original image
200
+ let originalImageBuffer: Buffer;
201
+ if (coverUrl.startsWith('data:')) {
202
+ log('Processing base64 cover image data');
203
+ // Extract base64 data part
204
+ const [, base64Data] = coverUrl.split(',');
205
+ originalImageBuffer = Buffer.from(base64Data, 'base64');
206
+ } else {
207
+ log('Fetching cover image from URL:', coverUrl);
208
+ const response = await fetch(coverUrl);
209
+ if (!response.ok) {
210
+ throw new Error(
211
+ `Failed to fetch cover image from ${coverUrl}: ${response.status} ${response.statusText}`,
212
+ );
213
+ }
214
+ const arrayBuffer = await response.arrayBuffer();
215
+ originalImageBuffer = Buffer.from(arrayBuffer);
216
+ log('Successfully fetched cover image, buffer size:', originalImageBuffer.length);
217
+ }
218
+
219
+ // Process image to 256x256 cover with webp format
220
+ log('Processing cover image to 256x256 webp format');
221
+ const coverBuffer = await sharp(originalImageBuffer)
222
+ .resize(256, 256, { fit: 'cover', position: 'center' })
223
+ .toBuffer();
224
+
225
+ log('Cover image processed, final size:', coverBuffer.length);
226
+
227
+ // Upload using FileService
228
+ const coverFolder = 'generations/covers';
229
+ const uuid = nanoid();
230
+ const dateTime = getYYYYmmddHHMMss(new Date());
231
+ const coverKey = `${coverFolder}/${uuid}_cover_${dateTime}.webp`;
232
+
233
+ log('Uploading cover image:', coverKey);
234
+ const result = await this.fileService.uploadMedia(coverKey, coverBuffer);
235
+
236
+ log('Cover image uploaded successfully:', result.key);
237
+ return result.key;
238
+ }
239
+ }
@@ -216,7 +216,7 @@ class MCPService {
216
216
  log(`New client initialized and cached for key: ${key.slice(0, 20)}`);
217
217
  return client;
218
218
  } catch (error) {
219
- console.error(`Failed to initialize MCP client for key ${key}:`, error);
219
+ console.error(`Failed to initialize MCP client:`, error);
220
220
 
221
221
  // 保留完整的错误信息,特别是详细的 stderr 输出
222
222
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -266,8 +266,26 @@ class MCPService {
266
266
  identifier: string,
267
267
  url: string,
268
268
  metadata?: CustomPluginMetadata,
269
+ auth?: {
270
+ accessToken?: string;
271
+ token?: string;
272
+ type: 'none' | 'bearer' | 'oauth2';
273
+ },
274
+ headers?: Record<string, string>,
269
275
  ): Promise<LobeChatPluginManifest> {
270
- const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
276
+ const mcpParams = { name: identifier, type: 'http' as const, url };
277
+
278
+ // 如果有认证信息,添加到参数中
279
+ if (auth) {
280
+ (mcpParams as any).auth = auth;
281
+ }
282
+
283
+ // 如果有 headers 信息,添加到参数中
284
+ if (headers) {
285
+ (mcpParams as any).headers = headers;
286
+ }
287
+
288
+ const tools = await this.listTools(mcpParams);
271
289
 
272
290
  return {
273
291
  api: tools,
@@ -0,0 +1,40 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { lambdaClient } from '@/libs/trpc/client';
4
+
5
+ import { generationService } from '../generation';
6
+
7
+ vi.mock('@/libs/trpc/client', () => ({
8
+ lambdaClient: {
9
+ generation: {
10
+ getGenerationStatus: { query: vi.fn() },
11
+ deleteGeneration: { mutate: vi.fn() },
12
+ },
13
+ },
14
+ }));
15
+
16
+ describe('GenerationService', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ it('getGenerationStatus should call lambdaClient with correct params', async () => {
22
+ const generationId = 'test-generation-id';
23
+ const asyncTaskId = 'test-async-task-id';
24
+
25
+ await generationService.getGenerationStatus(generationId, asyncTaskId);
26
+
27
+ expect(lambdaClient.generation.getGenerationStatus.query).toBeCalledWith({
28
+ generationId,
29
+ asyncTaskId,
30
+ });
31
+ });
32
+
33
+ it('deleteGeneration should call lambdaClient with correct params', async () => {
34
+ const generationId = 'test-generation-id';
35
+
36
+ await generationService.deleteGeneration(generationId);
37
+
38
+ expect(lambdaClient.generation.deleteGeneration.mutate).toBeCalledWith({ generationId });
39
+ });
40
+ });
@@ -0,0 +1,36 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { lambdaClient } from '@/libs/trpc/client';
4
+
5
+ import { generationBatchService } from '../generationBatch';
6
+
7
+ vi.mock('@/libs/trpc/client', () => ({
8
+ lambdaClient: {
9
+ generationBatch: {
10
+ getGenerationBatches: { query: vi.fn() },
11
+ deleteGenerationBatch: { mutate: vi.fn() },
12
+ },
13
+ },
14
+ }));
15
+
16
+ describe('GenerationBatchService', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks();
19
+ });
20
+
21
+ it('getGenerationBatches should call lambdaClient with correct params', async () => {
22
+ const topicId = 'test-topic-id';
23
+
24
+ await generationBatchService.getGenerationBatches(topicId);
25
+
26
+ expect(lambdaClient.generationBatch.getGenerationBatches.query).toBeCalledWith({ topicId });
27
+ });
28
+
29
+ it('deleteGenerationBatch should call lambdaClient with correct params', async () => {
30
+ const batchId = 'test-batch-id';
31
+
32
+ await generationBatchService.deleteGenerationBatch(batchId);
33
+
34
+ expect(lambdaClient.generationBatch.deleteGenerationBatch.mutate).toBeCalledWith({ batchId });
35
+ });
36
+ });
@@ -0,0 +1,72 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { lambdaClient } from '@/libs/trpc/client';
4
+ import { UpdateTopicValue } from '@/server/routers/lambda/generationTopic';
5
+
6
+ import { ServerService } from '../generationTopic';
7
+
8
+ vi.mock('@/libs/trpc/client', () => ({
9
+ lambdaClient: {
10
+ generationTopic: {
11
+ getAllGenerationTopics: { query: vi.fn() },
12
+ createTopic: { mutate: vi.fn() },
13
+ updateTopic: { mutate: vi.fn() },
14
+ updateTopicCover: { mutate: vi.fn() },
15
+ deleteTopic: { mutate: vi.fn() },
16
+ },
17
+ },
18
+ }));
19
+
20
+ describe('GenerationTopic ServerService', () => {
21
+ let service: ServerService;
22
+
23
+ beforeEach(() => {
24
+ vi.clearAllMocks();
25
+ service = new ServerService();
26
+ });
27
+
28
+ it('getAllGenerationTopics should call lambdaClient', async () => {
29
+ await service.getAllGenerationTopics();
30
+ expect(lambdaClient.generationTopic.getAllGenerationTopics.query).toBeCalled();
31
+ });
32
+
33
+ it('createTopic should call lambdaClient with undefined', async () => {
34
+ await service.createTopic();
35
+ expect(lambdaClient.generationTopic.createTopic.mutate).toBeCalledWith(undefined);
36
+ });
37
+
38
+ it('updateTopic should call lambdaClient with correct params', async () => {
39
+ const id = 'test-topic-id';
40
+ const data: UpdateTopicValue = {
41
+ title: 'Updated Topic',
42
+ coverUrl: 'https://example.com/cover.jpg',
43
+ };
44
+
45
+ await service.updateTopic(id, data);
46
+
47
+ expect(lambdaClient.generationTopic.updateTopic.mutate).toBeCalledWith({
48
+ id,
49
+ value: data,
50
+ });
51
+ });
52
+
53
+ it('updateTopicCover should call lambdaClient with correct params', async () => {
54
+ const id = 'test-topic-id';
55
+ const coverUrl = 'https://example.com/cover.jpg';
56
+
57
+ await service.updateTopicCover(id, coverUrl);
58
+
59
+ expect(lambdaClient.generationTopic.updateTopicCover.mutate).toBeCalledWith({
60
+ id,
61
+ coverUrl,
62
+ });
63
+ });
64
+
65
+ it('deleteTopic should call lambdaClient with correct params', async () => {
66
+ const id = 'test-topic-id';
67
+
68
+ await service.deleteTopic(id);
69
+
70
+ expect(lambdaClient.generationTopic.deleteTopic.mutate).toBeCalledWith({ id });
71
+ });
72
+ });
@@ -10,11 +10,13 @@ class DesktopFileAPI {
10
10
  * 上传文件到桌面应用
11
11
  * @param file 文件对象
12
12
  * @param hash 文件哈希
13
+ * @param path 文件存储路径
13
14
  * @returns 上传结果
14
15
  */
15
16
  async uploadFile(
16
17
  file: File,
17
18
  hash: string,
19
+ path: string,
18
20
  ): Promise<{ metadata: FileMetadata; success: boolean }> {
19
21
  const arrayBuffer = await file.arrayBuffer();
20
22
 
@@ -22,7 +24,7 @@ class DesktopFileAPI {
22
24
  content: arrayBuffer,
23
25
  filename: file.name,
24
26
  hash,
25
- path: file.name,
27
+ path,
26
28
  type: file.type,
27
29
  });
28
30
  }
@@ -0,0 +1,16 @@
1
+ import { lambdaClient } from '@/libs/trpc/client';
2
+
3
+ class GenerationService {
4
+ async getGenerationStatus(generationId: string, asyncTaskId: string) {
5
+ return lambdaClient.generation.getGenerationStatus.query({ asyncTaskId, generationId });
6
+ }
7
+
8
+ /**
9
+ * Delete a single generation
10
+ */
11
+ async deleteGeneration(generationId: string) {
12
+ return lambdaClient.generation.deleteGeneration.mutate({ generationId });
13
+ }
14
+ }
15
+
16
+ export const generationService = new GenerationService();
@@ -0,0 +1,25 @@
1
+ import { GenerationBatchItem } from '@/database/schemas';
2
+ import { lambdaClient } from '@/libs/trpc/client';
3
+ import { Generation, GenerationBatch } from '@/types/generation';
4
+
5
+ type GenerationBatchWithAsyncTaskId = GenerationBatch & {
6
+ generations: (Generation & { asyncTaskId?: string | null })[];
7
+ };
8
+
9
+ class GenerationBatchService {
10
+ /**
11
+ * Get generation batches for a specific topic
12
+ */
13
+ async getGenerationBatches(topicId: string): Promise<GenerationBatchWithAsyncTaskId[]> {
14
+ return lambdaClient.generationBatch.getGenerationBatches.query({ topicId });
15
+ }
16
+
17
+ /**
18
+ * Delete a generation batch
19
+ */
20
+ async deleteGenerationBatch(batchId: string): Promise<GenerationBatchItem | undefined> {
21
+ return lambdaClient.generationBatch.deleteGenerationBatch.mutate({ batchId });
22
+ }
23
+ }
24
+
25
+ export const generationBatchService = new GenerationBatchService();
@@ -0,0 +1,28 @@
1
+ import { GenerationTopicItem } from '@/database/schemas';
2
+ import { lambdaClient } from '@/libs/trpc/client';
3
+ import { UpdateTopicValue } from '@/server/routers/lambda/generationTopic';
4
+ import { ImageGenerationTopic } from '@/types/generation';
5
+
6
+ export class ServerService {
7
+ async getAllGenerationTopics(): Promise<ImageGenerationTopic[]> {
8
+ return lambdaClient.generationTopic.getAllGenerationTopics.query();
9
+ }
10
+
11
+ async createTopic(): Promise<string> {
12
+ return lambdaClient.generationTopic.createTopic.mutate(undefined);
13
+ }
14
+
15
+ async updateTopic(id: string, data: UpdateTopicValue): Promise<GenerationTopicItem | undefined> {
16
+ return lambdaClient.generationTopic.updateTopic.mutate({ id, value: data });
17
+ }
18
+
19
+ async updateTopicCover(id: string, coverUrl: string): Promise<GenerationTopicItem | undefined> {
20
+ return lambdaClient.generationTopic.updateTopicCover.mutate({ coverUrl, id });
21
+ }
22
+
23
+ async deleteTopic(id: string): Promise<GenerationTopicItem | undefined> {
24
+ return lambdaClient.generationTopic.deleteTopic.mutate({ id });
25
+ }
26
+ }
27
+
28
+ export const generationTopicService = new ServerService();
@@ -0,0 +1,33 @@
1
+ import debug from 'debug';
2
+
3
+ import { lambdaClient } from '@/libs/trpc/client';
4
+ import { CreateImageServicePayload } from '@/server/routers/lambda/image';
5
+
6
+ // Create debug logger
7
+ const log = debug('lobe-image:service');
8
+
9
+ export class AiImageService {
10
+ async createImage(payload: CreateImageServicePayload) {
11
+ log('Creating image with payload: %O', payload);
12
+
13
+ try {
14
+ const result = await lambdaClient.image.createImage.mutate(payload);
15
+ log('Image creation service call completed successfully: %O', {
16
+ batchId: result.data?.batch?.id,
17
+ generationCount: result.data?.generations?.length,
18
+ success: result.success,
19
+ });
20
+
21
+ return result;
22
+ } catch (error) {
23
+ log('Image creation service call failed: %O', {
24
+ error: (error as Error).message,
25
+ payload,
26
+ });
27
+
28
+ throw error;
29
+ }
30
+ }
31
+ }
32
+
33
+ export const imageService = new AiImageService();
@@ -128,15 +128,20 @@ class MCPService {
128
128
  }
129
129
 
130
130
  async getStreamableMcpServerManifest(
131
- identifier: string,
132
- url: string,
133
- metadata?: CustomPluginMetadata,
131
+ params: {
132
+ auth?: {
133
+ accessToken?: string;
134
+ token?: string;
135
+ type: 'none' | 'bearer' | 'oauth2';
136
+ };
137
+ headers?: Record<string, string>;
138
+ identifier: string;
139
+ metadata?: CustomPluginMetadata;
140
+ url: string;
141
+ },
134
142
  signal?: AbortSignal,
135
143
  ) {
136
- return toolsClient.mcp.getStreamableMcpServerManifest.query(
137
- { identifier, metadata, url },
138
- { signal },
139
- );
144
+ return toolsClient.mcp.getStreamableMcpServerManifest.query(params, { signal });
140
145
  }
141
146
 
142
147
  async getStdioMcpServerManifest(
@@ -13,6 +13,38 @@ import { uuid } from '@/utils/uuid';
13
13
 
14
14
  export const UPLOAD_NETWORK_ERROR = 'NetWorkError';
15
15
 
16
+ /**
17
+ * Generate file storage path metadata for S3-compatible storage
18
+ * @param originalFilename - Original filename
19
+ * @param options - Path generation options
20
+ * @returns Path metadata including date, dirname, filename, and pathname
21
+ */
22
+ const generateFilePathMetadata = (
23
+ originalFilename: string,
24
+ options: { directory?: string; pathname?: string } = {},
25
+ ): {
26
+ date: string;
27
+ dirname: string;
28
+ filename: string;
29
+ pathname: string;
30
+ } => {
31
+ // Generate unique filename with UUID prefix and original extension
32
+ const extension = originalFilename.split('.').at(-1);
33
+ const filename = `${uuid()}.${extension}`;
34
+
35
+ // Generate timestamp-based directory path
36
+ const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
37
+ const dirname = `${options.directory || fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/${date}`;
38
+ const pathname = options.pathname ?? `${dirname}/${filename}`;
39
+
40
+ return {
41
+ date,
42
+ dirname,
43
+ filename,
44
+ pathname,
45
+ };
46
+ };
47
+
16
48
  interface UploadFileToS3Options {
17
49
  directory?: string;
18
50
  filename?: string;
@@ -38,7 +70,7 @@ class UploadService {
38
70
 
39
71
  // 桌面端上传逻辑(并且没开启 sync 同步)
40
72
  if (isDesktop && !isSyncActive) {
41
- const data = await this.uploadToDesktopS3(file);
73
+ const data = await this.uploadToDesktopS3(file, { directory, pathname });
42
74
  return { data, success: true };
43
75
  }
44
76
 
@@ -182,12 +214,18 @@ class UploadService {
182
214
  return result;
183
215
  };
184
216
 
185
- private uploadToDesktopS3 = async (file: File) => {
217
+ private uploadToDesktopS3 = async (
218
+ file: File,
219
+ options: { directory?: string; pathname?: string } = {},
220
+ ) => {
186
221
  const fileArrayBuffer = await file.arrayBuffer();
187
222
  const hash = sha256(fileArrayBuffer);
188
223
 
224
+ // 生成文件路径元数据
225
+ const { pathname } = generateFilePathMetadata(file.name, options);
226
+
189
227
  const { desktopFileAPI } = await import('@/services/electron/file');
190
- const { metadata } = await desktopFileAPI.uploadFile(file, hash);
228
+ const { metadata } = await desktopFileAPI.uploadFile(file, hash, pathname);
191
229
  return metadata;
192
230
  };
193
231
 
@@ -223,12 +261,8 @@ class UploadService {
223
261
  preSignUrl: string;
224
262
  }
225
263
  > => {
226
- const filename = `${uuid()}.${file.name.split('.').at(-1)}`;
227
-
228
- // 精确到以 h 为单位的 path
229
- const date = (Date.now() / 1000 / 60 / 60).toFixed(0);
230
- const dirname = `${options.directory || fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/${date}`;
231
- const pathname = options.pathname ?? `${dirname}/${filename}`;
264
+ // 生成文件路径元数据
265
+ const { date, dirname, filename, pathname } = generateFilePathMetadata(file.name, options);
232
266
 
233
267
  const preSignUrl = await edgeClient.upload.createS3PreSignedUrl.mutate({ pathname });
234
268