@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
@@ -3,7 +3,8 @@ import OpenAI from 'openai';
3
3
  import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  // 引入模块以便于对函数进行spy
6
- import { ChatStreamCallbacks, LobeOpenAICompatibleRuntime } from '@/libs/model-runtime';
6
+ import { ChatStreamCallbacks } from '@/libs/model-runtime';
7
+ import * as openai from '@/libs/model-runtime/openai';
7
8
 
8
9
  import * as debugStreamModule from '../utils/debugStream';
9
10
  import officalOpenAIModels from './fixtures/openai-models.json';
@@ -12,8 +13,21 @@ import { LobeOpenAI } from './index';
12
13
  // Mock the console.error to avoid polluting test output
13
14
  vi.spyOn(console, 'error').mockImplementation(() => {});
14
15
 
16
+ // Mock fetch for most tests, but will be restored for convertImageUrlToFile tests
17
+ const mockFetch = vi.fn();
18
+ global.fetch = mockFetch;
19
+
20
+ const convertImageUrlToFileSpy = vi.spyOn(openai, 'convertImageUrlToFile');
21
+
15
22
  describe('LobeOpenAI', () => {
16
- let instance: LobeOpenAICompatibleRuntime;
23
+ let instance: InstanceType<typeof LobeOpenAI>;
24
+
25
+ // Create mock params for createImage tests - only gpt-image-1 supported params
26
+ const mockParams = {
27
+ prompt: 'test prompt',
28
+ imageUrls: [] as string[],
29
+ size: '1024x1024' as const,
30
+ };
17
31
 
18
32
  beforeEach(() => {
19
33
  instance = new LobeOpenAI({ apiKey: 'test' });
@@ -23,10 +37,35 @@ describe('LobeOpenAI', () => {
23
37
  new ReadableStream() as any,
24
38
  );
25
39
  vi.spyOn(instance['client'].models, 'list').mockResolvedValue({ data: [] } as any);
40
+
41
+ // Mock responses.create for responses API tests
42
+ vi.spyOn(instance['client'].responses, 'create').mockResolvedValue(new ReadableStream() as any);
43
+
44
+ // Mock convertImageUrlToFile to return a mock File object
45
+ convertImageUrlToFileSpy.mockResolvedValue({
46
+ name: 'image.png',
47
+ type: 'image/png',
48
+ size: 1024,
49
+ } as any);
50
+
51
+ // Mock fetch response for most tests
52
+ mockFetch.mockResolvedValue({
53
+ ok: true,
54
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
55
+ headers: {
56
+ get: (header: string) => {
57
+ if (header === 'content-type') {
58
+ return 'image/png';
59
+ }
60
+ return null;
61
+ },
62
+ },
63
+ });
26
64
  });
27
65
 
28
66
  afterEach(() => {
29
67
  vi.clearAllMocks();
68
+ mockFetch.mockClear();
30
69
  });
31
70
 
32
71
  describe('chat', () => {
@@ -242,4 +281,359 @@ describe('LobeOpenAI', () => {
242
281
  expect(list).toMatchSnapshot();
243
282
  });
244
283
  });
284
+
285
+ describe('createImage', () => {
286
+ it('should generate an image with gpt-image-1', async () => {
287
+ // Arrange
288
+ const mockResponse = { data: [{ b64_json: 'test-base64-string' }] };
289
+ const generateSpy = vi
290
+ .spyOn(instance['client'].images, 'generate')
291
+ .mockResolvedValue(mockResponse as any);
292
+
293
+ // Act
294
+ const result = await instance.createImage({
295
+ model: 'gpt-image-1',
296
+ params: {
297
+ ...mockParams,
298
+ prompt: 'A cute cat',
299
+ size: '1024x1024',
300
+ },
301
+ });
302
+
303
+ // Assert
304
+ expect(generateSpy).toHaveBeenCalledWith(
305
+ expect.objectContaining({
306
+ model: 'gpt-image-1',
307
+ prompt: 'A cute cat',
308
+ n: 1,
309
+ size: '1024x1024',
310
+ }),
311
+ );
312
+ expect(result.imageUrl).toBe('data:image/png;base64,test-base64-string');
313
+ });
314
+
315
+ it('should edit an image from a URL', async () => {
316
+ // Arrange
317
+ const mockResponse = { data: [{ b64_json: 'edited-base64-string' }] };
318
+ const editSpy = vi
319
+ .spyOn(instance['client'].images, 'edit')
320
+ .mockResolvedValue(mockResponse as any);
321
+
322
+ // Temporarily restore the spy to use real implementation
323
+ convertImageUrlToFileSpy.mockRestore();
324
+
325
+ const imageUrl = 'https://lobehub.com/_next/static/media/logo.98482105.png';
326
+
327
+ // Act
328
+ const result = await instance.createImage({
329
+ model: 'gpt-image-1',
330
+ params: {
331
+ ...mockParams,
332
+ prompt: 'A cat in a hat',
333
+ imageUrls: [imageUrl],
334
+ },
335
+ });
336
+
337
+ // Assert
338
+ expect(editSpy).toHaveBeenCalled();
339
+ const callArg = editSpy.mock.calls[0][0];
340
+ expect(callArg.model).toBe('gpt-image-1');
341
+ expect(callArg.prompt).toBe('A cat in a hat');
342
+ expect(result.imageUrl).toBe('data:image/png;base64,edited-base64-string');
343
+
344
+ // Restore the spy for other tests
345
+ convertImageUrlToFileSpy.mockResolvedValue({
346
+ name: 'image.png',
347
+ type: 'image/png',
348
+ size: 1024,
349
+ } as any);
350
+ });
351
+
352
+ it('should handle `size` set to `auto`', async () => {
353
+ // Arrange
354
+ const mockResponse = { data: [{ b64_json: 'test-base64-string' }] };
355
+ const generateSpy = vi
356
+ .spyOn(instance['client'].images, 'generate')
357
+ .mockResolvedValue(mockResponse as any);
358
+
359
+ // Act
360
+ await instance.createImage({
361
+ model: 'gpt-image-1',
362
+ params: {
363
+ ...mockParams,
364
+ prompt: 'A cute cat',
365
+ size: 'auto',
366
+ },
367
+ });
368
+
369
+ // Assert
370
+ expect(generateSpy).toHaveBeenCalledWith(
371
+ expect.objectContaining({
372
+ model: 'gpt-image-1',
373
+ prompt: 'A cute cat',
374
+ n: 1,
375
+ }),
376
+ );
377
+ // Should not include size when it's 'auto'
378
+ expect(generateSpy.mock.calls[0][0]).not.toHaveProperty('size');
379
+ });
380
+
381
+ it('should throw an error if convertImageUrlToFile fails', async () => {
382
+ // Arrange
383
+ const imageUrl = 'https://example.com/test-image.png';
384
+
385
+ // Mock fetch to fail for the image URL, which will cause convertImageUrlToFile to fail
386
+ vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network error'));
387
+
388
+ // Mock the OpenAI API methods to ensure they don't get called
389
+ const generateSpy = vi.spyOn(instance['client'].images, 'generate');
390
+ const editSpy = vi.spyOn(instance['client'].images, 'edit');
391
+
392
+ // Act & Assert - Note: imageUrls must be non-empty array to trigger isImageEdit = true
393
+ await expect(
394
+ instance.createImage({
395
+ model: 'gpt-image-1',
396
+ params: {
397
+ prompt: 'A cat in a hat',
398
+ imageUrls: [imageUrl], // This is the key - non-empty array
399
+ },
400
+ }),
401
+ ).rejects.toThrow('Failed to convert image URLs to File objects: Error: Network error');
402
+
403
+ // Verify that OpenAI API methods were not called since conversion failed
404
+ expect(generateSpy).not.toHaveBeenCalled();
405
+ expect(editSpy).not.toHaveBeenCalled();
406
+ });
407
+
408
+ it('should throw an error when image response is missing data array', async () => {
409
+ // Arrange
410
+ const mockInvalidResponse = {}; // missing data property
411
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
412
+
413
+ // Act & Assert
414
+ await expect(
415
+ instance.createImage({
416
+ model: 'gpt-image-1',
417
+ params: { ...mockParams, prompt: 'A cute cat' },
418
+ }),
419
+ ).rejects.toThrow('Invalid image response: missing or empty data array');
420
+ });
421
+
422
+ it('should throw an error when image response data array is empty', async () => {
423
+ // Arrange
424
+ const mockInvalidResponse = { data: [] }; // empty data array
425
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
426
+
427
+ // Act & Assert
428
+ await expect(
429
+ instance.createImage({
430
+ model: 'gpt-image-1',
431
+ params: { ...mockParams, prompt: 'A cute cat' },
432
+ }),
433
+ ).rejects.toThrow('Invalid image response: missing or empty data array');
434
+ });
435
+
436
+ it('should throw an error when first data item is null', async () => {
437
+ // Arrange
438
+ const mockInvalidResponse = { data: [null] }; // first item is null
439
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
440
+
441
+ // Act & Assert
442
+ await expect(
443
+ instance.createImage({
444
+ model: 'gpt-image-1',
445
+ params: { ...mockParams, prompt: 'A cute cat' },
446
+ }),
447
+ ).rejects.toThrow('Invalid image response: first data item is null or undefined');
448
+ });
449
+
450
+ it('should throw an error when first data item is undefined', async () => {
451
+ // Arrange
452
+ const mockInvalidResponse = { data: [undefined] }; // first item is undefined
453
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
454
+
455
+ // Act & Assert
456
+ await expect(
457
+ instance.createImage({
458
+ model: 'gpt-image-1',
459
+ params: { ...mockParams, prompt: 'A cute cat' },
460
+ }),
461
+ ).rejects.toThrow('Invalid image response: first data item is null or undefined');
462
+ });
463
+
464
+ it('should re-throw OpenAI API errors during image generation', async () => {
465
+ // Arrange
466
+ const apiError = new OpenAI.APIError(
467
+ 400,
468
+ { error: { message: 'Bad Request' } },
469
+ 'Error message',
470
+ {},
471
+ );
472
+ vi.spyOn(instance['client'].images, 'generate').mockRejectedValue(apiError);
473
+
474
+ // Act & Assert
475
+ await expect(
476
+ instance.createImage({
477
+ model: 'gpt-image-1',
478
+ params: { ...mockParams, prompt: 'A cute cat' },
479
+ }),
480
+ ).rejects.toThrow(apiError);
481
+ });
482
+
483
+ it('should throw an error for invalid image response', async () => {
484
+ // Arrange
485
+ const mockInvalidResponse = { data: [{ url: 'some_url' }] }; // missing b64_json
486
+ vi.spyOn(instance['client'].images, 'generate').mockResolvedValue(mockInvalidResponse as any);
487
+
488
+ // Act & Assert
489
+ await expect(
490
+ instance.createImage({
491
+ model: 'gpt-image-1',
492
+ params: { ...mockParams, prompt: 'A cute cat' },
493
+ }),
494
+ ).rejects.toThrow('Invalid image response: missing b64_json field');
495
+ });
496
+ });
497
+
498
+ describe('convertImageUrlToFile', () => {
499
+ beforeEach(() => {
500
+ // Reset the spy to use the real implementation for these tests
501
+ convertImageUrlToFileSpy.mockRestore();
502
+ });
503
+
504
+ afterEach(() => {
505
+ // Restore the spy for other tests
506
+ convertImageUrlToFileSpy.mockResolvedValue({
507
+ name: 'image.png',
508
+ type: 'image/png',
509
+ size: 1024,
510
+ } as any);
511
+ });
512
+
513
+ it('should convert the real lobehub logo URL to a FileLike object', async () => {
514
+ const imageUrl = 'https://lobehub.com/_next/static/media/logo.98482105.png';
515
+ const file = await openai.convertImageUrlToFile(imageUrl);
516
+
517
+ expect(file).toBeDefined();
518
+ expect((file as any).name).toBe('image.png');
519
+ expect((file as any).type).toMatch(/^image\//);
520
+ expect((file as any).size).toBeGreaterThan(0);
521
+ });
522
+
523
+ it('should convert a base64 data URL to a FileLike object', async () => {
524
+ const dataUrl =
525
+ 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/wAALCAABAAEBAREA/8QAFAABAAAAAAAAAAAAAAAAAAAAA//EABQQAQAAAAAAAAAAAAAAAAAAAAD/2gAIAQEAAD8AN//Z';
526
+ const file = await openai.convertImageUrlToFile(dataUrl);
527
+
528
+ expect(file).toBeDefined();
529
+ expect((file as any).name).toBe('image.jpeg');
530
+ expect((file as any).type).toBe('image/jpeg');
531
+ });
532
+
533
+ it('should handle different image mime types from data URL', async () => {
534
+ const webpDataUrl = 'data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==';
535
+ const file = await openai.convertImageUrlToFile(webpDataUrl);
536
+
537
+ expect(file).toBeDefined();
538
+ expect((file as any).name).toBe('image.webp');
539
+ expect((file as any).type).toBe('image/webp');
540
+ });
541
+ });
542
+
543
+ // Separate describe block for mocked fetch scenarios
544
+ describe('convertImageUrlToFile - mocked scenarios', () => {
545
+ beforeEach(() => {
546
+ // Reset the spy to use the real implementation
547
+ convertImageUrlToFileSpy.mockRestore();
548
+ });
549
+
550
+ afterEach(() => {
551
+ // Restore the spy for other tests
552
+ convertImageUrlToFileSpy.mockResolvedValue({
553
+ name: 'image.png',
554
+ type: 'image/png',
555
+ size: 1024,
556
+ } as any);
557
+ });
558
+
559
+ it('should throw an error if fetching an image from a URL fails', async () => {
560
+ // Use vi.mocked for type-safe mocking
561
+ vi.mocked(global.fetch).mockResolvedValueOnce({
562
+ ok: false,
563
+ statusText: 'Not Found',
564
+ } as any);
565
+
566
+ const imageUrl = 'https://example.com/invalid-image.png';
567
+
568
+ await expect(openai.convertImageUrlToFile(imageUrl)).rejects.toThrow(
569
+ 'Failed to fetch image from https://example.com/invalid-image.png: Not Found',
570
+ );
571
+ });
572
+
573
+ it('should use a default mime type of image/png if content-type header is not available', async () => {
574
+ // Use vi.mocked for type-safe mocking
575
+ vi.mocked(global.fetch).mockResolvedValueOnce({
576
+ ok: true,
577
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)),
578
+ headers: {
579
+ get: () => null,
580
+ },
581
+ } as any);
582
+
583
+ const imageUrl = 'https://example.com/image-no-content-type';
584
+ const file = await openai.convertImageUrlToFile(imageUrl);
585
+
586
+ expect(file).toBeDefined();
587
+ expect((file as any).type).toBe('image/png');
588
+ });
589
+ });
590
+
591
+ describe('responses.handlePayload', () => {
592
+ it('should add web_search_preview tool when enabledSearch is true', async () => {
593
+ const payload = {
594
+ messages: [{ content: 'Hello', role: 'user' as const }],
595
+ model: 'gpt-4o', // 使用常规模型,通过 enabledSearch 触发 responses API
596
+ temperature: 0.7,
597
+ enabledSearch: true,
598
+ tools: [{ type: 'function' as const, function: { name: 'test', description: 'test' } }],
599
+ };
600
+
601
+ await instance.chat(payload);
602
+
603
+ const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
604
+ expect(createCall.tools).toEqual([
605
+ { type: 'function', name: 'test', description: 'test' },
606
+ { type: 'web_search_preview' },
607
+ ]);
608
+ });
609
+
610
+ it('should handle computer-use models with truncation and reasoning', async () => {
611
+ const payload = {
612
+ messages: [{ content: 'Hello', role: 'user' as const }],
613
+ model: 'computer-use-preview',
614
+ temperature: 0.7,
615
+ reasoning: { effort: 'medium' },
616
+ };
617
+
618
+ await instance.chat(payload);
619
+
620
+ const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
621
+ expect(createCall.truncation).toBe('auto');
622
+ expect(createCall.reasoning).toEqual({ effort: 'medium', summary: 'auto' });
623
+ });
624
+
625
+ it('should handle prunePrefixes models without computer-use truncation', async () => {
626
+ const payload = {
627
+ messages: [{ content: 'Hello', role: 'user' as const }],
628
+ model: 'o1-pro', // prunePrefixes 模型但非 computer-use
629
+ temperature: 0.7,
630
+ };
631
+
632
+ await instance.chat(payload);
633
+
634
+ const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
635
+ expect(createCall.reasoning).toEqual({ summary: 'auto' });
636
+ expect(createCall.truncation).toBeUndefined();
637
+ });
638
+ });
245
639
  });
@@ -1,4 +1,9 @@
1
+ import debug from 'debug';
2
+ import { toFile } from 'openai';
3
+ import { FileLike } from 'openai/uploads';
4
+
1
5
  import { responsesAPIModels } from '@/const/models';
6
+ import { RuntimeImageGenParamsValue } from '@/libs/standard-parameters/meta-schema';
2
7
 
3
8
  import { ChatStreamPayload, ModelProvider } from '../types';
4
9
  import { processMultiProviderModelList } from '../utils/modelParse';
@@ -13,6 +18,42 @@ const prunePrefixes = ['o1', 'o3', 'o4', 'codex', 'computer-use'];
13
18
 
14
19
  const oaiSearchContextSize = process.env.OPENAI_SEARCH_CONTEXT_SIZE; // low, medium, high
15
20
 
21
+ const log = debug('lobe-image:openai');
22
+
23
+ /**
24
+ * 将图片 URL 转换为 File 对象
25
+ * @param imageUrl - 图片 URL(可以是 HTTP URL 或 base64 data URL)
26
+ * @returns FileLike 对象
27
+ */
28
+ export const convertImageUrlToFile = async (imageUrl: string): Promise<FileLike> => {
29
+ log('Converting image URL to File: %s', imageUrl.startsWith('data:') ? 'base64 data' : imageUrl);
30
+
31
+ let buffer: Buffer;
32
+ let mimeType: string;
33
+
34
+ if (imageUrl.startsWith('data:')) {
35
+ // 处理 base64 data URL
36
+ log('Processing base64 image data');
37
+ const [mimeTypePart, base64Data] = imageUrl.split(',');
38
+ mimeType = mimeTypePart.split(':')[1].split(';')[0];
39
+ buffer = Buffer.from(base64Data, 'base64');
40
+ } else {
41
+ // 处理 HTTP URL
42
+ log('Fetching image from URL: %s', imageUrl);
43
+ const response = await fetch(imageUrl);
44
+ if (!response.ok) {
45
+ throw new Error(`Failed to fetch image from ${imageUrl}: ${response.statusText}`);
46
+ }
47
+ buffer = Buffer.from(await response.arrayBuffer());
48
+ mimeType = response.headers.get('content-type') || 'image/png';
49
+ }
50
+
51
+ log('Successfully converted image to buffer, size: %s, mimeType: %s', buffer.length, mimeType);
52
+
53
+ // 使用 OpenAI 的 toFile 方法创建 File 对象
54
+ return toFile(buffer, `image.${mimeType.split('/')[1]}`, { type: mimeType });
55
+ };
56
+
16
57
  export const LobeOpenAI = createOpenAICompatibleRuntime({
17
58
  baseURL: 'https://api.openai.com/v1',
18
59
  chatCompletion: {
@@ -47,6 +88,91 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
47
88
  return { ...rest, model, stream: payload.stream ?? true };
48
89
  },
49
90
  },
91
+ createImage: async (payload) => {
92
+ const { model, params, client } = payload;
93
+ log('Creating image with model: %s and params: %O', model, params);
94
+
95
+ const defaultInput = {
96
+ n: 1,
97
+ };
98
+
99
+ // 映射参数名称,将 imageUrls 映射为 image
100
+ const paramsMap = new Map<RuntimeImageGenParamsValue, string>([['imageUrls', 'image']]);
101
+ const userInput: Record<string, any> = Object.fromEntries(
102
+ Object.entries(params).map(([key, value]) => [
103
+ paramsMap.get(key as RuntimeImageGenParamsValue) ?? key,
104
+ value,
105
+ ]),
106
+ );
107
+
108
+ const isImageEdit = Array.isArray(userInput.image) && userInput.image.length > 0;
109
+ // 如果有 imageUrls 参数,将其转换为 File 对象
110
+ if (isImageEdit) {
111
+ log('Converting imageUrls to File objects: %O', userInput.image);
112
+ try {
113
+ // 转换所有图片 URL 为 File 对象
114
+ const imageFiles = await Promise.all(
115
+ userInput.image.map((url: string) => convertImageUrlToFile(url)),
116
+ );
117
+
118
+ log('Successfully converted %d images to File objects', imageFiles.length);
119
+
120
+ // 根据官方文档,如果有多个图片,传递数组;如果只有一个,传递单个 File
121
+ userInput.image = imageFiles.length === 1 ? imageFiles[0] : imageFiles;
122
+ } catch (error) {
123
+ log('Error converting imageUrls to File objects: %O', error);
124
+ throw new Error(`Failed to convert image URLs to File objects: ${error}`);
125
+ }
126
+ } else {
127
+ delete userInput.image;
128
+ }
129
+
130
+ if (userInput.size === 'auto') {
131
+ delete userInput.size;
132
+ }
133
+
134
+ const options = {
135
+ model,
136
+ ...defaultInput,
137
+ ...(userInput as any),
138
+ };
139
+
140
+ log('options: %O', options);
141
+
142
+ // 判断是否为图片编辑操作
143
+ const img = isImageEdit
144
+ ? await client.images.edit(options)
145
+ : await client.images.generate(options);
146
+
147
+ // 检查响应数据的完整性
148
+ if (!img || !img.data || !Array.isArray(img.data) || img.data.length === 0) {
149
+ log('Invalid image response: missing data array');
150
+ throw new Error('Invalid image response: missing or empty data array');
151
+ }
152
+
153
+ const imageData = img.data[0];
154
+ if (!imageData) {
155
+ log('Invalid image response: first data item is null/undefined');
156
+ throw new Error('Invalid image response: first data item is null or undefined');
157
+ }
158
+
159
+ if (!imageData.b64_json) {
160
+ log('Invalid image response: missing b64_json field');
161
+ throw new Error('Invalid image response: missing b64_json field');
162
+ }
163
+
164
+ // 确定图片的 MIME 类型,默认为 PNG
165
+ const mimeType = 'image/png'; // OpenAI 图片生成默认返回 PNG 格式
166
+
167
+ // 将 base64 字符串转换为完整的 data URL
168
+ const dataUrl = `data:${mimeType};base64,${imageData.b64_json}`;
169
+
170
+ log('Successfully converted base64 to data URL, length: %d', dataUrl.length);
171
+
172
+ return {
173
+ imageUrl: dataUrl,
174
+ };
175
+ },
50
176
  debug: {
51
177
  chatCompletion: () => process.env.DEBUG_OPENAI_CHAT_COMPLETION === '1',
52
178
  responses: () => process.env.DEBUG_OPENAI_RESPONSES === '1',
@@ -79,9 +205,9 @@ export const LobeOpenAI = createOpenAICompatibleRuntime({
79
205
  return pruneReasoningPayload({
80
206
  ...rest,
81
207
  model,
82
- reasoning: payload.reasoning ?
83
- { ...payload.reasoning, summary: 'auto' } :
84
- { summary: 'auto' },
208
+ reasoning: payload.reasoning
209
+ ? { ...payload.reasoning, summary: 'auto' }
210
+ : { summary: 'auto' },
85
211
  stream: payload.stream ?? true,
86
212
  tools: openaiTools as any,
87
213
  // computer-use series must set truncation as auto
@@ -8,6 +8,7 @@ import { LobeBedrockAI } from './bedrock';
8
8
  import { LobeCloudflareAI } from './cloudflare';
9
9
  import { LobeCohereAI } from './cohere';
10
10
  import { LobeDeepSeekAI } from './deepseek';
11
+ import { LobeFalAI } from './fal';
11
12
  import { LobeFireworksAI } from './fireworksai';
12
13
  import { LobeGiteeAI } from './giteeai';
13
14
  import { LobeGithubAI } from './github';
@@ -63,6 +64,7 @@ export const providerRuntimeMap = {
63
64
  cloudflare: LobeCloudflareAI,
64
65
  cohere: LobeCohereAI,
65
66
  deepseek: LobeDeepSeekAI,
67
+ fal: LobeFalAI,
66
68
  fireworksai: LobeFireworksAI,
67
69
  giteeai: LobeGiteeAI,
68
70
  github: LobeGithubAI,
@@ -0,0 +1,25 @@
1
+ import { RuntimeImageGenParams } from '@/libs/standard-parameters/meta-schema';
2
+
3
+ export type CreateImagePayload = {
4
+ model: string;
5
+ params: RuntimeImageGenParams;
6
+ };
7
+
8
+ export type CreateImageResponse = {
9
+ /**
10
+ * 图片的高度
11
+ */
12
+ height?: number;
13
+
14
+ /**
15
+ * 一般是 provider 家的 cdn 地址,多数一段时间后就会失效,需要重新请求
16
+ */
17
+ imageUrl: string;
18
+ // 为什么返回宽高?
19
+ // 1. 你给的配置宽度和真正最后生成的宽度可能并不一致,这个需要存储到 generation 的 asset 中
20
+ // 2. 我需要图片宽高用于计算是否需要生成缩略图,很多 provider 一般都会返回宽高,这样也许我可以省去一些计算
21
+ /**
22
+ * 图片的宽度
23
+ */
24
+ width?: number;
25
+ };
@@ -32,6 +32,7 @@ export enum ModelProvider {
32
32
  Cloudflare = 'cloudflare',
33
33
  Cohere = 'cohere',
34
34
  DeepSeek = 'deepseek',
35
+ Fal = 'fal',
35
36
  FireworksAI = 'fireworksai',
36
37
  GiteeAI = 'giteeai',
37
38
  Github = 'github',
@@ -22,6 +22,7 @@ import {
22
22
  TextToSpeechOptions,
23
23
  TextToSpeechPayload,
24
24
  } from '../../types';
25
+ import { CreateImagePayload, CreateImageResponse } from '../../types/image';
25
26
  import { AgentRuntimeError } from '../createError';
26
27
  import { debugResponse, debugStream } from '../debugStream';
27
28
  import { desensitizeUrl } from '../desensitizeUrl';
@@ -81,6 +82,7 @@ interface OpenAICompatibleFactoryOptions<T extends Record<string, any> = any> {
81
82
  noUserId?: boolean;
82
83
  };
83
84
  constructorOptions?: ConstructorOptions<T>;
85
+ createImage?: (payload: CreateImagePayload & { client: OpenAI }) => Promise<CreateImageResponse>;
84
86
  customClient?: CustomClientOptions<T>;
85
87
  debug?: {
86
88
  chatCompletion: () => boolean;
@@ -166,6 +168,7 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
166
168
  debug,
167
169
  constructorOptions,
168
170
  chatCompletion,
171
+ createImage,
169
172
  models,
170
173
  customClient,
171
174
  responses,
@@ -307,6 +310,13 @@ export const createOpenAICompatibleRuntime = <T extends Record<string, any> = an
307
310
  }
308
311
  }
309
312
 
313
+ async createImage(payload: CreateImagePayload) {
314
+ return createImage!({
315
+ ...payload,
316
+ client: this.client,
317
+ });
318
+ }
319
+
310
320
  async models() {
311
321
  if (typeof models === 'function') return models({ client: this.client });
312
322
 
@@ -0,0 +1 @@
1
+ export * from './meta-schema';