@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
@@ -1,17 +1,20 @@
1
+ import { vi } from 'vitest';
2
+
1
3
  import { pathString } from './url';
4
+ import { inferContentTypeFromImageUrl, inferFileExtensionFromImageUrl } from './url';
2
5
 
3
6
  describe('pathString', () => {
4
- it('基本情况', () => {
7
+ it('should handle basic path', () => {
5
8
  const result = pathString('/home');
6
9
  expect(result).toBe('/home');
7
10
  });
8
11
 
9
- it('包含查询参数的情况', () => {
12
+ it('should handle path with search parameters', () => {
10
13
  const result = pathString('/home', { search: 'id=1&name=test' });
11
14
  expect(result).toBe('/home?id=1&name=test');
12
15
  });
13
16
 
14
- it('包含哈希值的情况', () => {
17
+ it('should handle path with hash', () => {
15
18
  const result = pathString('/home', { hash: 'top' });
16
19
  expect(result).toBe('/home#top');
17
20
 
@@ -19,33 +22,379 @@ describe('pathString', () => {
19
22
  expect(result2).toBe('/home#hash=abc');
20
23
  });
21
24
 
22
- it('path 参数包含相对路径的情况', () => {
25
+ it('should handle relative path', () => {
23
26
  const result = pathString('./home');
24
27
  expect(result).toBe('/home');
25
28
  });
26
29
 
27
- it('path 参数包含绝对路径的情况', () => {
30
+ it('should handle absolute path', () => {
28
31
  const result = pathString('/home');
29
32
  expect(result).toBe('/home');
30
33
  });
31
34
 
32
- it('path 参数包含协议的情况', () => {
35
+ it('should handle path with protocol', () => {
33
36
  const result = pathString('https://www.example.com/home');
34
37
  expect(result).toBe('https://www.example.com/home');
35
38
  });
36
39
 
37
- it('path 参数包含主机名的情况', () => {
40
+ it('should handle path with hostname', () => {
38
41
  const result = pathString('//www.example.com/home');
39
42
  expect(result).toBe('https://www.example.com/home');
40
43
  });
41
44
 
42
- it('path 参数包含端口号的情况', () => {
45
+ it('should handle path with port number', () => {
43
46
  const result = pathString('//www.example.com:8080/home');
44
47
  expect(result).toBe('https://www.example.com:8080/home');
45
48
  });
46
49
 
47
- it('path 参数包含特殊字符的情况', () => {
50
+ it('should handle path with special characters', () => {
48
51
  const result = pathString('/home/测试');
49
52
  expect(result).toBe('/home/%E6%B5%8B%E8%AF%95');
50
53
  });
51
54
  });
55
+
56
+ describe('inferContentTypeFromImageUrl', () => {
57
+ it('should return correct MIME type for jpg images', () => {
58
+ const result = inferContentTypeFromImageUrl('https://example.com/image.jpg');
59
+ expect(result).toBe('image/jpeg');
60
+ });
61
+
62
+ it('should return correct MIME type for png images', () => {
63
+ const result = inferContentTypeFromImageUrl('https://example.com/image.png');
64
+ expect(result).toBe('image/png');
65
+ });
66
+
67
+ it('should return correct MIME type for webp images', () => {
68
+ const result = inferContentTypeFromImageUrl('https://example.com/image.webp');
69
+ expect(result).toBe('image/webp');
70
+ });
71
+
72
+ it('should return correct MIME type for gif images', () => {
73
+ const result = inferContentTypeFromImageUrl('https://example.com/image.gif');
74
+ expect(result).toBe('image/gif');
75
+ });
76
+
77
+ it('should handle uppercase extensions', () => {
78
+ const result = inferContentTypeFromImageUrl('https://example.com/image.JPG');
79
+ expect(result).toBe('image/jpeg');
80
+ });
81
+
82
+ it('should handle URLs with query parameters', () => {
83
+ const result = inferContentTypeFromImageUrl('https://example.com/image.png?v=123&size=large');
84
+ expect(result).toBe('image/png');
85
+ });
86
+
87
+ it('should handle URLs with hash fragments', () => {
88
+ const result = inferContentTypeFromImageUrl('https://example.com/image.jpg#section');
89
+ expect(result).toBe('image/jpeg');
90
+ });
91
+
92
+ it('should throw error when no extension', () => {
93
+ expect(() => {
94
+ inferContentTypeFromImageUrl('https://example.com/image');
95
+ }).toThrow('Invalid image url: https://example.com/image');
96
+ });
97
+
98
+ it('should throw error when only dot without extension', () => {
99
+ expect(() => {
100
+ inferContentTypeFromImageUrl('https://example.com/image.');
101
+ }).toThrow('Invalid image url: https://example.com/image.');
102
+ });
103
+
104
+ it('should handle multiple dots in path', () => {
105
+ const result = inferContentTypeFromImageUrl('https://example.com/my.folder/image.test.png');
106
+ expect(result).toBe('image/png');
107
+ });
108
+
109
+ it('should handle complex query parameters and hash combination', () => {
110
+ const result = inferContentTypeFromImageUrl(
111
+ 'https://example.com/image.jpeg?width=800&height=600&format=jpeg#preview',
112
+ );
113
+ expect(result).toBe('image/jpeg');
114
+ });
115
+
116
+ it('should handle mixed case extensions', () => {
117
+ const result = inferContentTypeFromImageUrl('https://example.com/image.JpEg');
118
+ expect(result).toBe('image/jpeg');
119
+ });
120
+
121
+ it('should handle BMP format', () => {
122
+ const result = inferContentTypeFromImageUrl('https://example.com/image.bmp');
123
+ expect(result).toBe('image/bmp');
124
+ });
125
+
126
+ it('should handle TIFF format', () => {
127
+ const result = inferContentTypeFromImageUrl('https://example.com/image.tiff');
128
+ expect(result).toBe('image/tiff');
129
+ });
130
+
131
+ it('should handle TIF format', () => {
132
+ const result = inferContentTypeFromImageUrl('https://example.com/image.tif');
133
+ expect(result).toBe('image/tiff');
134
+ });
135
+
136
+ it('should handle SVG format', () => {
137
+ const result = inferContentTypeFromImageUrl('https://example.com/image.svg');
138
+ expect(result).toBe('image/svg+xml');
139
+ });
140
+
141
+ it('should throw error for invalid URLs', () => {
142
+ expect(() => {
143
+ inferContentTypeFromImageUrl('invalid-url');
144
+ }).toThrow('Invalid image url: invalid-url');
145
+ });
146
+
147
+ it('should throw error for empty string', () => {
148
+ expect(() => {
149
+ inferContentTypeFromImageUrl('');
150
+ }).toThrow('Invalid image url: ');
151
+ });
152
+
153
+ it('should throw error for non-image extensions', () => {
154
+ expect(() => {
155
+ inferContentTypeFromImageUrl('https://example.com/document.txt');
156
+ }).toThrow('Invalid image url: https://example.com/document.txt');
157
+ });
158
+
159
+ it('should throw error for unknown extensions', () => {
160
+ expect(() => {
161
+ inferContentTypeFromImageUrl('https://example.com/file.unknownext');
162
+ }).toThrow('Invalid image url: https://example.com/file.unknownext');
163
+ });
164
+
165
+ it('should handle URLs with port numbers', () => {
166
+ const result = inferContentTypeFromImageUrl('https://example.com:8080/image.png');
167
+ expect(result).toBe('image/png');
168
+ });
169
+
170
+ it('should handle deeply nested paths', () => {
171
+ const result = inferContentTypeFromImageUrl(
172
+ 'https://cdn.example.com/assets/images/gallery/2024/photo.jpg',
173
+ );
174
+ expect(result).toBe('image/jpeg');
175
+ });
176
+
177
+ it('should handle encoded filenames', () => {
178
+ const result = inferContentTypeFromImageUrl(
179
+ 'https://example.com/images/%E5%9B%BE%E7%89%87.png',
180
+ );
181
+ expect(result).toBe('image/png');
182
+ });
183
+
184
+ it('should handle protocol-relative URLs', () => {
185
+ const result = inferContentTypeFromImageUrl('//example.com/image.webp');
186
+ expect(result).toBe('image/webp');
187
+ });
188
+
189
+ it('should handle relative paths', () => {
190
+ const result = inferContentTypeFromImageUrl('generations/images/photo.jpg');
191
+ expect(result).toBe('image/jpeg');
192
+ });
193
+
194
+ it('should handle relative paths with complex filenames', () => {
195
+ const result = inferContentTypeFromImageUrl(
196
+ 'generations/images/2NPfAQAMNxXPi82mzOHog_1056x1136_20250702_110911_raw.png',
197
+ );
198
+ expect(result).toBe('image/png');
199
+ });
200
+
201
+ it('should handle relative paths with query parameters', () => {
202
+ const result = inferContentTypeFromImageUrl('images/photo.webp?v=123');
203
+ expect(result).toBe('image/webp');
204
+ });
205
+
206
+ it('should handle relative paths with hash fragments', () => {
207
+ const result = inferContentTypeFromImageUrl('assets/images/banner.gif#preview');
208
+ expect(result).toBe('image/gif');
209
+ });
210
+
211
+ it('should throw error for single character extensions (if no valid MIME type)', () => {
212
+ expect(() => {
213
+ inferContentTypeFromImageUrl('https://example.com/file.x');
214
+ }).toThrow('Invalid image url: https://example.com/file.x');
215
+ });
216
+
217
+ it('should throw error for dot at end of path', () => {
218
+ expect(() => {
219
+ inferContentTypeFromImageUrl('https://example.com/path/.');
220
+ }).toThrow('Invalid image url: https://example.com/path/.');
221
+ });
222
+
223
+ it('should handle malformed URLs that result in no valid extension', () => {
224
+ // These URLs will be processed by inferFileExtensionFromImageUrl and return empty string
225
+ const invalidUrls = [
226
+ 'data:image/jpeg;base64,invalid', // No file extension in path
227
+ 'javascript:alert("test")', // No file extension
228
+ 'https://example.com/file', // No extension
229
+ 'https://example.com/file.', // Dot without extension
230
+ 'ftp://example.com/document.pdf', // Valid URL but non-image extension
231
+ ];
232
+
233
+ invalidUrls.forEach((url) => {
234
+ expect(() => {
235
+ inferContentTypeFromImageUrl(url);
236
+ }).toThrow(/Invalid image url:/);
237
+ });
238
+ });
239
+
240
+ it('should handle all supported image formats consistently', () => {
241
+ const testCases = [
242
+ { extension: 'jpg', expected: 'image/jpeg' },
243
+ { extension: 'jpeg', expected: 'image/jpeg' },
244
+ { extension: 'png', expected: 'image/png' },
245
+ { extension: 'webp', expected: 'image/webp' },
246
+ { extension: 'gif', expected: 'image/gif' },
247
+ { extension: 'bmp', expected: 'image/bmp' },
248
+ { extension: 'svg', expected: 'image/svg+xml' },
249
+ { extension: 'tiff', expected: 'image/tiff' },
250
+ { extension: 'tif', expected: 'image/tiff' },
251
+ ];
252
+
253
+ testCases.forEach(({ extension, expected }) => {
254
+ const result = inferContentTypeFromImageUrl(`https://example.com/image.${extension}`);
255
+ expect(result).toBe(expected);
256
+ });
257
+ });
258
+ });
259
+
260
+ describe('inferFileExtensionFromImageUrl', () => {
261
+ it('should return jpg extension', () => {
262
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.jpg');
263
+ expect(result).toBe('jpg');
264
+ });
265
+
266
+ it('should return png extension', () => {
267
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.png');
268
+ expect(result).toBe('png');
269
+ });
270
+
271
+ it('should return webp extension', () => {
272
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.webp');
273
+ expect(result).toBe('webp');
274
+ });
275
+
276
+ it('should handle jpeg extension', () => {
277
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.jpeg');
278
+ expect(result).toBe('jpeg');
279
+ });
280
+
281
+ it('should handle gif extension', () => {
282
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.gif');
283
+ expect(result).toBe('gif');
284
+ });
285
+
286
+ it('should handle svg extension', () => {
287
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.svg');
288
+ expect(result).toBe('svg');
289
+ });
290
+
291
+ it('should handle uppercase extensions and convert to lowercase', () => {
292
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.PNG');
293
+ expect(result).toBe('png');
294
+ });
295
+
296
+ it('should handle mixed case extensions', () => {
297
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.JpEg');
298
+ expect(result).toBe('jpeg');
299
+ });
300
+
301
+ it('should handle URLs with query parameters', () => {
302
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.jpg?v=123&size=large');
303
+ expect(result).toBe('jpg');
304
+ });
305
+
306
+ it('should handle URLs with hash fragments', () => {
307
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.png#section');
308
+ expect(result).toBe('png');
309
+ });
310
+
311
+ it('should handle multiple dots in path', () => {
312
+ const result = inferFileExtensionFromImageUrl('https://example.com/my.folder/image.test.webp');
313
+ expect(result).toBe('webp');
314
+ });
315
+
316
+ it('should return empty string when no extension', () => {
317
+ const result = inferFileExtensionFromImageUrl('https://example.com/image');
318
+ expect(result).toBe('');
319
+ });
320
+
321
+ it('should return empty string when only dot without extension', () => {
322
+ const result = inferFileExtensionFromImageUrl('https://example.com/image.');
323
+ expect(result).toBe('');
324
+ });
325
+
326
+ it('should return empty string for non-image extensions', () => {
327
+ const result = inferFileExtensionFromImageUrl('https://example.com/document.txt');
328
+ expect(result).toBe('');
329
+ });
330
+
331
+ it('should return empty string for other format extensions', () => {
332
+ const result = inferFileExtensionFromImageUrl('https://example.com/video.mp4');
333
+ expect(result).toBe('');
334
+ });
335
+
336
+ it('should handle invalid URLs and return empty string', () => {
337
+ const result = inferFileExtensionFromImageUrl('invalid-url');
338
+ expect(result).toBe('');
339
+ });
340
+
341
+ it('should handle empty string URLs and return empty string', () => {
342
+ const result = inferFileExtensionFromImageUrl('');
343
+ expect(result).toBe('');
344
+ });
345
+
346
+ it('should handle all supported image extensions', () => {
347
+ const supportedExtensions = ['webp', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'tiff', 'tif'];
348
+
349
+ supportedExtensions.forEach((ext) => {
350
+ const result = inferFileExtensionFromImageUrl(`https://example.com/image.${ext}`);
351
+ expect(result).toBe(ext);
352
+ });
353
+ });
354
+
355
+ it('should handle URLs with port numbers', () => {
356
+ const result = inferFileExtensionFromImageUrl('https://example.com:8080/image.jpg');
357
+ expect(result).toBe('jpg');
358
+ });
359
+
360
+ it('should handle subdomain URLs', () => {
361
+ const result = inferFileExtensionFromImageUrl('https://cdn.example.com/images/photo.webp');
362
+ expect(result).toBe('webp');
363
+ });
364
+
365
+ it('should handle deep path URLs', () => {
366
+ const result = inferFileExtensionFromImageUrl(
367
+ 'https://example.com/assets/images/gallery/photo.png',
368
+ );
369
+ expect(result).toBe('png');
370
+ });
371
+
372
+ it('should handle encoded URLs', () => {
373
+ const result = inferFileExtensionFromImageUrl(
374
+ 'https://example.com/images/%E5%9B%BE%E7%89%87.jpg',
375
+ );
376
+ expect(result).toBe('jpg');
377
+ });
378
+
379
+ it('should handle relative paths', () => {
380
+ const result = inferFileExtensionFromImageUrl('generations/images/photo.jpg');
381
+ expect(result).toBe('jpg');
382
+ });
383
+
384
+ it('should handle relative paths with complex filenames', () => {
385
+ const result = inferFileExtensionFromImageUrl(
386
+ 'generations/images/2NPfAQAMNxXPi82mzOHog_1056x1136_20250702_110911_raw.png',
387
+ );
388
+ expect(result).toBe('png');
389
+ });
390
+
391
+ it('should handle relative paths with query parameters', () => {
392
+ const result = inferFileExtensionFromImageUrl('images/photo.webp?v=123');
393
+ expect(result).toBe('webp');
394
+ });
395
+
396
+ it('should handle relative paths with hash fragments', () => {
397
+ const result = inferFileExtensionFromImageUrl('assets/images/banner.gif#preview');
398
+ expect(result).toBe('gif');
399
+ });
400
+ });
package/src/utils/url.ts CHANGED
@@ -1,8 +1,26 @@
1
+ import mime from 'mime';
2
+
1
3
  /**
2
4
  * Build a path string from a path and a hash/search object
3
- * @param path
4
- * @param hash
5
- * @param search
5
+ *
6
+ * This function constructs a properly formatted URL path by combining a base path
7
+ * with optional hash and search parameters. It uses URL constructor for proper
8
+ * encoding and formatting while removing the temporary base domain.
9
+ *
10
+ * @param path - The base path (can be relative, absolute, or include protocol)
11
+ * @param options - Optional configuration object
12
+ * @param options.hash - Hash fragment to append (with or without leading #)
13
+ * @param options.search - Search/query parameters to append (with or without leading ?)
14
+ * @returns Formatted path string with hash and search parameters
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * pathString('/home') // '/home'
19
+ * pathString('/home', { search: 'id=1&name=test' }) // '/home?id=1&name=test'
20
+ * pathString('/home', { hash: 'top' }) // '/home#top'
21
+ * pathString('./home') // '/home'
22
+ * pathString('https://example.com/path') // 'https://example.com/path'
23
+ * ```
6
24
  */
7
25
  export const pathString = (
8
26
  path: string,
@@ -14,10 +32,94 @@ export const pathString = (
14
32
  search?: string;
15
33
  } = {},
16
34
  ) => {
35
+ // Use a temporary base URL for proper URL parsing and formatting
17
36
  const tempBase = 'https://a.com';
18
37
  const url = new URL(path, tempBase);
19
38
 
39
+ // Add hash fragment if provided
20
40
  if (hash) url.hash = hash;
41
+ // Add search parameters if provided
21
42
  if (search) url.search = search;
43
+
44
+ // Return the formatted URL without the temporary base
22
45
  return url.toString().replace(tempBase, '');
23
46
  };
47
+
48
+ /**
49
+ * Get file extension from URL
50
+ *
51
+ * This function extracts the file extension from a URL's pathname and validates it against
52
+ * common image formats. It properly handles URLs with query parameters, hash fragments,
53
+ * relative paths, and various edge cases. Returns empty string for invalid cases.
54
+ *
55
+ * @param url - The URL to extract extension from (can be relative, absolute, or include query parameters and hash fragments)
56
+ * @returns file extension without dot (e.g., 'jpg', 'png', 'webp'), or empty string for invalid cases
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * inferFileExtensionFromImageUrl('https://example.com/image.jpg') // 'jpg'
61
+ * inferFileExtensionFromImageUrl('https://example.com/image.png?v=123') // 'png'
62
+ * inferFileExtensionFromImageUrl('https://example.com/image.webp#section') // 'webp'
63
+ * inferFileExtensionFromImageUrl('generations/images/photo.png') // 'png'
64
+ * inferFileExtensionFromImageUrl('https://example.com/document.txt') // '' (empty string)
65
+ * inferFileExtensionFromImageUrl('invalid-url') // '' (empty string)
66
+ * ```
67
+ */
68
+ export const inferFileExtensionFromImageUrl = (url: string): string => {
69
+ // Use a temporary base URL for proper URL parsing and formatting (handles relative paths)
70
+ const tempBase = 'https://a.com';
71
+ const urlObj = new URL(url, tempBase);
72
+ const pathname = urlObj.pathname;
73
+
74
+ // Find the last dot in the pathname to get the file extension
75
+ const lastDotIndex = pathname.lastIndexOf('.');
76
+ if (lastDotIndex === -1) return ''; // No extension found, return empty string
77
+
78
+ // Extract extension after the last dot and convert to lowercase
79
+ const extension = pathname.slice(Math.max(0, lastDotIndex + 1)).toLowerCase();
80
+
81
+ // Validate against common image extensions
82
+ const validImageExtensions = ['webp', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'tiff', 'tif'];
83
+ if (validImageExtensions.includes(extension)) {
84
+ return extension;
85
+ }
86
+
87
+ // Default fallback for non-image extensions
88
+ return '';
89
+ };
90
+
91
+ /**
92
+ * Infer content type (MIME type) from an image URL
93
+ *
94
+ * This function extracts the file extension from a URL and returns the corresponding MIME type.
95
+ * It properly handles URLs with query parameters, hash fragments, relative paths, and various edge cases.
96
+ *
97
+ * @param url - The image URL to analyze (can be relative, absolute, or include query parameters and hash fragments)
98
+ * @returns MIME type string (e.g., 'image/jpeg', 'image/png')
99
+ * @throws {Error} When the URL doesn't contain a valid file extension
100
+ *
101
+ * @example
102
+ * ```typescript
103
+ * inferContentTypeFromImageUrl('https://example.com/image.jpg') // 'image/jpeg'
104
+ * inferContentTypeFromImageUrl('https://example.com/image.png?v=123') // 'image/png'
105
+ * inferContentTypeFromImageUrl('https://example.com/image.webp#section') // 'image/webp'
106
+ * inferContentTypeFromImageUrl('generations/images/photo.png') // 'image/png'
107
+ * ```
108
+ */
109
+ export function inferContentTypeFromImageUrl(url: string) {
110
+ // Get the file extension using the dedicated function
111
+ // inferFileExtensionFromImageUrl only returns valid image extensions or empty string
112
+ const extension = inferFileExtensionFromImageUrl(url);
113
+
114
+ // If no valid extension found, throw error
115
+ if (!extension) {
116
+ throw new Error(`Invalid image url: ${url}`);
117
+ }
118
+
119
+ // Get MIME type using the mime library
120
+ // Since extension is guaranteed to be a valid image extension from the whitelist,
121
+ // mime.getType() will always return a valid image MIME type
122
+ const mimeType = mime.getType(extension);
123
+
124
+ return mimeType!; // Non-null assertion is safe due to whitelist validation
125
+ }
@@ -26,5 +26,8 @@ export default defineConfig({
26
26
  },
27
27
  },
28
28
  setupFiles: './tests/setup-db.ts',
29
+ env: {
30
+ TEST_SERVER_DB: '1',
31
+ },
29
32
  },
30
33
  });
@@ -1,6 +0,0 @@
1
- ---
2
- description:
3
- globs: src/locales/**/*
4
- alwaysApply: false
5
- ---
6
- read [i18n.mdc](mdc:.cursor/rules/i18n/i18n.mdc)
@@ -1,105 +0,0 @@
1
- import { ProviderIcon } from '@lobehub/icons';
2
- import { Button } from '@lobehub/ui';
3
- import { memo, useMemo, useState } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
- import { Center, Flexbox } from 'react-layout-kit';
6
-
7
- import { ModelProvider } from '@/libs/model-runtime';
8
- import { useChatStore } from '@/store/chat';
9
- import { GlobalLLMProviderKey } from '@/types/user/settings';
10
-
11
- import BedrockForm from './Bedrock';
12
- import { LoadingContext } from './LoadingContext';
13
- import ProviderApiKeyForm from './ProviderApiKeyForm';
14
-
15
- interface APIKeyFormProps {
16
- id: string;
17
- provider?: string;
18
- }
19
-
20
- const APIKeyForm = memo<APIKeyFormProps>(({ id, provider }) => {
21
- const { t } = useTranslation('error');
22
- const [loading, setLoading] = useState(false);
23
-
24
- const [resend, deleteMessage] = useChatStore((s) => [s.regenerateMessage, s.deleteMessage]);
25
-
26
- const apiKeyPlaceholder = useMemo(() => {
27
- switch (provider) {
28
- case ModelProvider.Anthropic: {
29
- return 'sk-ant_*****************************';
30
- }
31
-
32
- case ModelProvider.OpenRouter: {
33
- return 'sk-or-********************************';
34
- }
35
-
36
- case ModelProvider.Perplexity: {
37
- return 'pplx-********************************';
38
- }
39
-
40
- case ModelProvider.ZhiPu: {
41
- return '*********************.*************';
42
- }
43
-
44
- case ModelProvider.Groq: {
45
- return 'gsk_*****************************';
46
- }
47
-
48
- case ModelProvider.DeepSeek: {
49
- return 'sk_******************************';
50
- }
51
-
52
- case ModelProvider.Qwen: {
53
- return 'sk-********************************';
54
- }
55
-
56
- case ModelProvider.Github: {
57
- return 'ghp_*****************************';
58
- }
59
-
60
- default: {
61
- return '*********************************';
62
- }
63
- }
64
- }, [provider]);
65
-
66
- return (
67
- <LoadingContext value={{ loading, setLoading }}>
68
- <Center gap={16} style={{ maxWidth: 300 }}>
69
- {provider === ModelProvider.Bedrock ? (
70
- <BedrockForm />
71
- ) : (
72
- <ProviderApiKeyForm
73
- apiKeyPlaceholder={apiKeyPlaceholder}
74
- avatar={<ProviderIcon provider={provider} size={80} type={'avatar'} />}
75
- provider={provider as GlobalLLMProviderKey}
76
- showEndpoint={provider === ModelProvider.OpenAI}
77
- />
78
- )}
79
- <Flexbox gap={12} width={'100%'}>
80
- <Button
81
- block
82
- disabled={loading}
83
- onClick={() => {
84
- resend(id);
85
- deleteMessage(id);
86
- }}
87
- style={{ marginTop: 8 }}
88
- type={'primary'}
89
- >
90
- {t('unlock.confirm')}
91
- </Button>
92
- <Button
93
- onClick={() => {
94
- deleteMessage(id);
95
- }}
96
- >
97
- {t('unlock.closeMessage')}
98
- </Button>
99
- </Flexbox>
100
- </Center>
101
- </LoadingContext>
102
- );
103
- });
104
-
105
- export default APIKeyForm;
@@ -1,16 +0,0 @@
1
- import { memo } from 'react';
2
-
3
- import APIKeyForm from './APIKeyForm';
4
- import { ErrorActionContainer } from './style';
5
-
6
- interface InvalidAPIKeyProps {
7
- id: string;
8
- provider?: string;
9
- }
10
- const InvalidAPIKey = memo<InvalidAPIKeyProps>(({ id, provider }) => (
11
- <ErrorActionContainer>
12
- <APIKeyForm id={id} provider={provider} />
13
- </ErrorActionContainer>
14
- ));
15
-
16
- export default InvalidAPIKey;