libre-webui 0.10.0 → 0.12.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 (298) hide show
  1. package/README.md +75 -61
  2. package/backend/dist/db.d.ts.map +1 -1
  3. package/backend/dist/db.js +21 -19
  4. package/backend/dist/db.js.map +1 -1
  5. package/backend/dist/index.d.ts.map +1 -1
  6. package/backend/dist/index.js +39 -908
  7. package/backend/dist/index.js.map +1 -1
  8. package/backend/dist/middleware/auth.d.ts.map +1 -1
  9. package/backend/dist/middleware/auth.js +4 -2
  10. package/backend/dist/middleware/auth.js.map +1 -1
  11. package/backend/dist/middleware/index.d.ts.map +1 -1
  12. package/backend/dist/middleware/index.js +4 -2
  13. package/backend/dist/middleware/index.js.map +1 -1
  14. package/backend/dist/models/personaModel.d.ts.map +1 -1
  15. package/backend/dist/models/personaModel.js +11 -9
  16. package/backend/dist/models/personaModel.js.map +1 -1
  17. package/backend/dist/routes/auth.d.ts.map +1 -1
  18. package/backend/dist/routes/auth.js +36 -14
  19. package/backend/dist/routes/auth.js.map +1 -1
  20. package/backend/dist/routes/chat.d.ts.map +1 -1
  21. package/backend/dist/routes/chat.js +61 -249
  22. package/backend/dist/routes/chat.js.map +1 -1
  23. package/backend/dist/routes/documents.d.ts.map +1 -1
  24. package/backend/dist/routes/documents.js +10 -8
  25. package/backend/dist/routes/documents.js.map +1 -1
  26. package/backend/dist/routes/imageGen.d.ts.map +1 -1
  27. package/backend/dist/routes/imageGen.js +9 -7
  28. package/backend/dist/routes/imageGen.js.map +1 -1
  29. package/backend/dist/routes/libreClaw.d.ts +3 -0
  30. package/backend/dist/routes/libreClaw.d.ts.map +1 -0
  31. package/backend/dist/routes/libreClaw.js +156 -0
  32. package/backend/dist/routes/libreClaw.js.map +1 -0
  33. package/backend/dist/routes/ollama.d.ts.map +1 -1
  34. package/backend/dist/routes/ollama.js +7 -247
  35. package/backend/dist/routes/ollama.js.map +1 -1
  36. package/backend/dist/routes/plugins.d.ts.map +1 -1
  37. package/backend/dist/routes/plugins.js +7 -105
  38. package/backend/dist/routes/plugins.js.map +1 -1
  39. package/backend/dist/routes/tts.d.ts.map +1 -1
  40. package/backend/dist/routes/tts.js +7 -5
  41. package/backend/dist/routes/tts.js.map +1 -1
  42. package/backend/dist/routes/users.d.ts.map +1 -1
  43. package/backend/dist/routes/users.js +7 -5
  44. package/backend/dist/routes/users.js.map +1 -1
  45. package/backend/dist/services/assistantCompletionService.d.ts +26 -0
  46. package/backend/dist/services/assistantCompletionService.d.ts.map +1 -0
  47. package/backend/dist/services/assistantCompletionService.js +53 -0
  48. package/backend/dist/services/assistantCompletionService.js.map +1 -0
  49. package/backend/dist/services/authService.d.ts +2 -0
  50. package/backend/dist/services/authService.d.ts.map +1 -1
  51. package/backend/dist/services/authService.js +9 -5
  52. package/backend/dist/services/authService.js.map +1 -1
  53. package/backend/dist/services/chatGenerationService.d.ts +32 -0
  54. package/backend/dist/services/chatGenerationService.d.ts.map +1 -0
  55. package/backend/dist/services/chatGenerationService.js +118 -0
  56. package/backend/dist/services/chatGenerationService.js.map +1 -0
  57. package/backend/dist/services/chatRequestService.d.ts +55 -0
  58. package/backend/dist/services/chatRequestService.d.ts.map +1 -0
  59. package/backend/dist/services/chatRequestService.js +103 -0
  60. package/backend/dist/services/chatRequestService.js.map +1 -0
  61. package/backend/dist/services/chatService.d.ts +0 -1
  62. package/backend/dist/services/chatService.d.ts.map +1 -1
  63. package/backend/dist/services/chatService.js +13 -27
  64. package/backend/dist/services/chatService.js.map +1 -1
  65. package/backend/dist/services/documentService.d.ts.map +1 -1
  66. package/backend/dist/services/documentService.js +21 -19
  67. package/backend/dist/services/documentService.js.map +1 -1
  68. package/backend/dist/services/embeddingService.d.ts.map +1 -1
  69. package/backend/dist/services/embeddingService.js +3 -1
  70. package/backend/dist/services/embeddingService.js.map +1 -1
  71. package/backend/dist/services/encryptionService.d.ts.map +1 -1
  72. package/backend/dist/services/encryptionService.js +26 -24
  73. package/backend/dist/services/encryptionService.js.map +1 -1
  74. package/backend/dist/services/galleryService.d.ts.map +1 -1
  75. package/backend/dist/services/galleryService.js +8 -6
  76. package/backend/dist/services/galleryService.js.map +1 -1
  77. package/backend/dist/services/libreClawService.d.ts +105 -0
  78. package/backend/dist/services/libreClawService.d.ts.map +1 -0
  79. package/backend/dist/services/libreClawService.js +169 -0
  80. package/backend/dist/services/libreClawService.js.map +1 -0
  81. package/backend/dist/services/memoryService.d.ts +2 -13
  82. package/backend/dist/services/memoryService.d.ts.map +1 -1
  83. package/backend/dist/services/memoryService.js +37 -198
  84. package/backend/dist/services/memoryService.js.map +1 -1
  85. package/backend/dist/services/memoryUtils.d.ts +29 -0
  86. package/backend/dist/services/memoryUtils.d.ts.map +1 -0
  87. package/backend/dist/services/memoryUtils.js +165 -0
  88. package/backend/dist/services/memoryUtils.js.map +1 -0
  89. package/backend/dist/services/mutationEngineService.d.ts.map +1 -1
  90. package/backend/dist/services/mutationEngineService.js +3 -1
  91. package/backend/dist/services/mutationEngineService.js.map +1 -1
  92. package/backend/dist/services/ollamaService.d.ts.map +1 -1
  93. package/backend/dist/services/ollamaService.js +36 -34
  94. package/backend/dist/services/ollamaService.js.map +1 -1
  95. package/backend/dist/services/personaService.d.ts.map +1 -1
  96. package/backend/dist/services/personaService.js +3 -1
  97. package/backend/dist/services/personaService.js.map +1 -1
  98. package/backend/dist/services/pluginCapabilityRegistryService.d.ts +11 -0
  99. package/backend/dist/services/pluginCapabilityRegistryService.d.ts.map +1 -0
  100. package/backend/dist/services/pluginCapabilityRegistryService.js +70 -0
  101. package/backend/dist/services/pluginCapabilityRegistryService.js.map +1 -0
  102. package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -1
  103. package/backend/dist/services/pluginCredentialsService.js +13 -11
  104. package/backend/dist/services/pluginCredentialsService.js.map +1 -1
  105. package/backend/dist/services/pluginEmbeddingService.d.ts +26 -0
  106. package/backend/dist/services/pluginEmbeddingService.d.ts.map +1 -0
  107. package/backend/dist/services/pluginEmbeddingService.js +159 -0
  108. package/backend/dist/services/pluginEmbeddingService.js.map +1 -0
  109. package/backend/dist/services/pluginImageGenerationService.d.ts +29 -0
  110. package/backend/dist/services/pluginImageGenerationService.d.ts.map +1 -0
  111. package/backend/dist/services/pluginImageGenerationService.js +402 -0
  112. package/backend/dist/services/pluginImageGenerationService.js.map +1 -0
  113. package/backend/dist/services/pluginService.d.ts +9 -18
  114. package/backend/dist/services/pluginService.d.ts.map +1 -1
  115. package/backend/dist/services/pluginService.js +118 -1469
  116. package/backend/dist/services/pluginService.js.map +1 -1
  117. package/backend/dist/services/pluginTTSService.d.ts +28 -0
  118. package/backend/dist/services/pluginTTSService.d.ts.map +1 -0
  119. package/backend/dist/services/pluginTTSService.js +286 -0
  120. package/backend/dist/services/pluginTTSService.js.map +1 -0
  121. package/backend/dist/services/pluginVariablesService.d.ts.map +1 -1
  122. package/backend/dist/services/pluginVariablesService.js +7 -5
  123. package/backend/dist/services/pluginVariablesService.js.map +1 -1
  124. package/backend/dist/services/preferencesService.d.ts.map +1 -1
  125. package/backend/dist/services/preferencesService.js +9 -7
  126. package/backend/dist/services/preferencesService.js.map +1 -1
  127. package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -1
  128. package/backend/dist/services/simpleGitHubOAuth.js +10 -8
  129. package/backend/dist/services/simpleGitHubOAuth.js.map +1 -1
  130. package/backend/dist/services/simpleHuggingFaceOAuth.d.ts.map +1 -1
  131. package/backend/dist/services/simpleHuggingFaceOAuth.js +11 -9
  132. package/backend/dist/services/simpleHuggingFaceOAuth.js.map +1 -1
  133. package/backend/dist/services/systemSettingsService.d.ts.map +1 -1
  134. package/backend/dist/services/systemSettingsService.js +4 -2
  135. package/backend/dist/services/systemSettingsService.js.map +1 -1
  136. package/backend/dist/services/titleGenerationService.d.ts +55 -0
  137. package/backend/dist/services/titleGenerationService.d.ts.map +1 -0
  138. package/backend/dist/services/titleGenerationService.js +121 -0
  139. package/backend/dist/services/titleGenerationService.js.map +1 -0
  140. package/backend/dist/services/turnstileService.d.ts +13 -0
  141. package/backend/dist/services/turnstileService.d.ts.map +1 -0
  142. package/backend/dist/services/turnstileService.js +79 -0
  143. package/backend/dist/services/turnstileService.js.map +1 -0
  144. package/backend/dist/storage.d.ts +2 -22
  145. package/backend/dist/storage.d.ts.map +1 -1
  146. package/backend/dist/storage.js +22 -170
  147. package/backend/dist/storage.js.map +1 -1
  148. package/backend/dist/storageMappers.d.ts +82 -0
  149. package/backend/dist/storageMappers.d.ts.map +1 -0
  150. package/backend/dist/storageMappers.js +85 -0
  151. package/backend/dist/storageMappers.js.map +1 -0
  152. package/backend/dist/test-encryption.js +20 -18
  153. package/backend/dist/test-encryption.js.map +1 -1
  154. package/backend/dist/types/index.d.ts.map +1 -1
  155. package/backend/dist/types/index.js.map +1 -1
  156. package/backend/dist/utils/assistantBranching.d.ts +8 -0
  157. package/backend/dist/utils/assistantBranching.d.ts.map +1 -0
  158. package/backend/dist/utils/assistantBranching.js +30 -0
  159. package/backend/dist/utils/assistantBranching.js.map +1 -0
  160. package/backend/dist/utils/chatContext.d.ts +17 -0
  161. package/backend/dist/utils/chatContext.d.ts.map +1 -0
  162. package/backend/dist/utils/chatContext.js +82 -0
  163. package/backend/dist/utils/chatContext.js.map +1 -0
  164. package/backend/dist/utils/chatDocumentContext.d.ts +7 -0
  165. package/backend/dist/utils/chatDocumentContext.d.ts.map +1 -0
  166. package/backend/dist/utils/chatDocumentContext.js +51 -0
  167. package/backend/dist/utils/chatDocumentContext.js.map +1 -0
  168. package/backend/dist/utils/logger.d.ts +12 -0
  169. package/backend/dist/utils/logger.d.ts.map +1 -0
  170. package/backend/dist/utils/logger.js +82 -0
  171. package/backend/dist/utils/logger.js.map +1 -0
  172. package/backend/dist/utils/ollamaLibrary.d.ts +17 -0
  173. package/backend/dist/utils/ollamaLibrary.d.ts.map +1 -0
  174. package/backend/dist/utils/ollamaLibrary.js +253 -0
  175. package/backend/dist/utils/ollamaLibrary.js.map +1 -0
  176. package/backend/dist/utils/ollamaStreaming.d.ts +19 -0
  177. package/backend/dist/utils/ollamaStreaming.d.ts.map +1 -0
  178. package/backend/dist/utils/ollamaStreaming.js +67 -0
  179. package/backend/dist/utils/ollamaStreaming.js.map +1 -0
  180. package/backend/dist/utils/pluginChatAdapter.d.ts +32 -0
  181. package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -0
  182. package/backend/dist/utils/pluginChatAdapter.js +308 -0
  183. package/backend/dist/utils/pluginChatAdapter.js.map +1 -0
  184. package/backend/dist/utils/pluginChatContext.d.ts +23 -0
  185. package/backend/dist/utils/pluginChatContext.d.ts.map +1 -0
  186. package/backend/dist/utils/pluginChatContext.js +77 -0
  187. package/backend/dist/utils/pluginChatContext.js.map +1 -0
  188. package/backend/dist/utils/pluginResponse.d.ts +6 -0
  189. package/backend/dist/utils/pluginResponse.d.ts.map +1 -0
  190. package/backend/dist/utils/pluginResponse.js +70 -0
  191. package/backend/dist/utils/pluginResponse.js.map +1 -0
  192. package/backend/dist/utils/pluginStreamAdapter.d.ts +16 -0
  193. package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -0
  194. package/backend/dist/utils/pluginStreamAdapter.js +141 -0
  195. package/backend/dist/utils/pluginStreamAdapter.js.map +1 -0
  196. package/backend/dist/utils/pluginStreaming.d.ts +24 -0
  197. package/backend/dist/utils/pluginStreaming.d.ts.map +1 -0
  198. package/backend/dist/utils/pluginStreaming.js +106 -0
  199. package/backend/dist/utils/pluginStreaming.js.map +1 -0
  200. package/backend/dist/utils/pluginUpload.d.ts +8 -0
  201. package/backend/dist/utils/pluginUpload.d.ts.map +1 -0
  202. package/backend/dist/utils/pluginUpload.js +51 -0
  203. package/backend/dist/utils/pluginUpload.js.map +1 -0
  204. package/backend/dist/utils/pluginValidation.d.ts +9 -0
  205. package/backend/dist/utils/pluginValidation.d.ts.map +1 -0
  206. package/backend/dist/utils/pluginValidation.js +84 -0
  207. package/backend/dist/utils/pluginValidation.js.map +1 -0
  208. package/backend/dist/utils/pluginVariableValidation.d.ts +10 -0
  209. package/backend/dist/utils/pluginVariableValidation.d.ts.map +1 -0
  210. package/backend/dist/utils/pluginVariableValidation.js +80 -0
  211. package/backend/dist/utils/pluginVariableValidation.js.map +1 -0
  212. package/backend/dist/utils/websocketMessages.d.ts +26 -0
  213. package/backend/dist/utils/websocketMessages.d.ts.map +1 -0
  214. package/backend/dist/utils/websocketMessages.js +74 -0
  215. package/backend/dist/utils/websocketMessages.js.map +1 -0
  216. package/backend/dist/websocketServer.d.ts +3 -0
  217. package/backend/dist/websocketServer.d.ts.map +1 -0
  218. package/backend/dist/websocketServer.js +332 -0
  219. package/backend/dist/websocketServer.js.map +1 -0
  220. package/backend/package.json +2 -2
  221. package/frontend/dist/assets/index-Dc8Q4dJZ.css +1 -0
  222. package/frontend/dist/assets/index-ONn9VIq7.js +5 -0
  223. package/frontend/dist/assets/markdown-math-CYlHEDRF.css +1 -0
  224. package/frontend/dist/index.html +11 -10
  225. package/frontend/dist/js/ArtifactContainer-CqKsiKz0.js +1 -0
  226. package/frontend/dist/js/ArtifactDemoPage-Dd5BjIu2.js +200 -0
  227. package/frontend/dist/js/ArtifactSlideOutPanel-Yc7ttIaY.js +1 -0
  228. package/frontend/dist/js/Button-BUjLpHmR.js +1 -0
  229. package/frontend/dist/js/ChatPage-Cpy7lQ0D.js +16 -0
  230. package/frontend/dist/js/FirstTimeSetup-DcGhm0H0.js +1 -0
  231. package/frontend/dist/js/GalleryPage-DNI2Evvf.js +1 -0
  232. package/frontend/dist/js/LibreClawPage-CmTiROsJ.js +1 -0
  233. package/frontend/dist/js/ModelManagerModals-CfvSeNsT.js +1 -0
  234. package/frontend/dist/js/ModelsPage-CeTGqx4q.js +2 -0
  235. package/frontend/dist/js/OptimizedSyntaxHighlighter-CNjLqj1Z.js +1 -0
  236. package/frontend/dist/js/PersonasPage-BUqu7gJ3.js +13 -0
  237. package/frontend/dist/js/RichMessageContent-B0JO8vkR.js +3 -0
  238. package/frontend/dist/js/SettingsModal-B5aJ5vfV.js +1 -0
  239. package/frontend/dist/js/UserManagementPage-BqIIWX2M.js +1 -0
  240. package/frontend/dist/js/api-CbmZzKBu.js +15 -0
  241. package/frontend/dist/js/ar-BAOdgn6O.js +2 -0
  242. package/frontend/dist/js/artifactHtml-B7Oy2e__.js +27 -0
  243. package/frontend/dist/js/bn-Bp2KS5mx.js +2 -0
  244. package/frontend/dist/js/cs-DOgJ0c5p.js +2 -0
  245. package/frontend/dist/js/da-BCKTkC35.js +2 -0
  246. package/frontend/dist/js/de-BXyECph5.js +2 -0
  247. package/frontend/dist/js/demoMode-BhEpMVh5.js +1 -0
  248. package/frontend/dist/js/es-Db4bDYIF.js +2 -0
  249. package/frontend/dist/js/fr-BPIkf6zH.js +2 -0
  250. package/frontend/dist/js/hi-Bm8D_Aux.js +2 -0
  251. package/frontend/dist/js/id-DiWaH74_.js +2 -0
  252. package/frontend/dist/js/is-DLgJywtZ.js +2 -0
  253. package/frontend/dist/js/it-lldrM8pE.js +2 -0
  254. package/frontend/dist/js/ja-Cfe9uZmQ.js +2 -0
  255. package/frontend/dist/js/ko-BhChO3MQ.js +2 -0
  256. package/frontend/dist/js/logger-BmpKNkls.js +1 -0
  257. package/frontend/dist/js/markdown-core-fkbHPd-g.js +16 -0
  258. package/frontend/dist/js/markdown-math-Ci4QOsf-.js +261 -0
  259. package/frontend/dist/js/ms-SWtmetXo.js +2 -0
  260. package/frontend/dist/js/nl-CUO6I3nO.js +2 -0
  261. package/frontend/dist/js/pl-BUAu96P6.js +2 -0
  262. package/frontend/dist/js/pt-DCHwoWiE.js +2 -0
  263. package/frontend/dist/js/{react-vendor-Dh6nfvM2.js → react-vendor-Co1u1rWb.js} +2 -2
  264. package/frontend/dist/js/ru-Ciz053N7.js +2 -0
  265. package/frontend/dist/js/sv-D38ZHjv-.js +2 -0
  266. package/frontend/dist/js/syntax-highlight-D5wpTVgr.js +8 -0
  267. package/frontend/dist/js/th-D2cAucvn.js +2 -0
  268. package/frontend/dist/js/tr-Go355oDu.js +2 -0
  269. package/frontend/dist/js/ui-vendor-BJAeWL11.js +177 -0
  270. package/frontend/dist/js/uk-cdzjhgya.js +2 -0
  271. package/frontend/dist/js/useBaseQuery-6OJiOiyu.js +1 -0
  272. package/frontend/dist/js/useQuery-DfkI6VAv.js +1 -0
  273. package/frontend/dist/js/utils-BaPYjs_z.js +1 -0
  274. package/frontend/dist/js/utils-vendor-BX3ZwoIK.js +9 -0
  275. package/frontend/dist/js/vi-BSOpE423.js +2 -0
  276. package/frontend/dist/js/zh-BFh6sHZG.js +2 -0
  277. package/package.json +12 -8
  278. package/backend/dist/services/openclawSessionService.d.ts +0 -63
  279. package/backend/dist/services/openclawSessionService.d.ts.map +0 -1
  280. package/backend/dist/services/openclawSessionService.js +0 -307
  281. package/backend/dist/services/openclawSessionService.js.map +0 -1
  282. package/frontend/dist/assets/index-cOuIFv6G.js +0 -314
  283. package/frontend/dist/assets/index-tuVjvNle.css +0 -1
  284. package/frontend/dist/js/ArtifactContainer-DXa2t8GS.js +0 -24
  285. package/frontend/dist/js/ArtifactDemoPage-Dms7P2gI.js +0 -98
  286. package/frontend/dist/js/ChatPage-C6hOVIAS.js +0 -15
  287. package/frontend/dist/js/GalleryPage-CGKPECJV.js +0 -1
  288. package/frontend/dist/js/ModelsPage-CNB-c-JM.js +0 -1
  289. package/frontend/dist/js/PersonasPage-hDiS0Rvl.js +0 -13
  290. package/frontend/dist/js/UserManagementPage-1H9kcHeR.js +0 -1
  291. package/frontend/dist/js/api-MQ7bONaU.js +0 -3
  292. package/frontend/dist/js/appStore-BpzQfKOd.js +0 -2
  293. package/frontend/dist/js/authStore-Cn_WS6Kb.js +0 -2
  294. package/frontend/dist/js/demoMode-PKHbba1m.js +0 -1
  295. package/frontend/dist/js/markdown-vendor-DLzJCMRQ.js +0 -22
  296. package/frontend/dist/js/ui-vendor-ClcVY3m-.js +0 -177
  297. package/frontend/dist/js/utils-vendor-BCQRX-0Q.js +0 -9
  298. package/plugins/openclaw-agent.json +0 -69
@@ -38,7 +38,6 @@ import cors from 'cors';
38
38
  import helmet from 'helmet';
39
39
  import morgan from 'morgan';
40
40
  import { createServer } from 'http';
41
- import { WebSocketServer } from 'ws';
42
41
  import { errorHandler, notFoundHandler, requestLogger, } from './middleware/index.js';
43
42
  import { optionalAuth } from './middleware/auth.js';
44
43
  import ollamaRoutes from './routes/ollama.js';
@@ -53,19 +52,16 @@ import ttsRoutes from './routes/tts.js';
53
52
  import imageGenRoutes from './routes/imageGen.js';
54
53
  import embeddingsRoutes from './routes/embeddings.js';
55
54
  import huggingfaceHubRoutes from './routes/huggingfaceHub.js';
55
+ import libreClawRoutes from './routes/libreClaw.js';
56
56
  import ollamaService from './services/ollamaService.js';
57
- import chatService from './services/chatService.js';
58
57
  import { GitHubOAuthService } from './services/simpleGitHubOAuth.js';
59
58
  import { HuggingFaceOAuthService } from './services/simpleHuggingFaceOAuth.js';
60
- import pluginService from './services/pluginService.js';
61
- import preferencesService from './services/preferencesService.js';
62
- import documentService from './services/documentService.js';
63
- import openclawSessionService, { extractTextFromMessage, } from './services/openclawSessionService.js';
64
- import { mergeGenerationOptions } from './utils/generationUtils.js';
65
- import { verifyToken } from './utils/jwt.js';
66
59
  import { loadAppPackage, resolveFrontendDist } from './utils/packagePaths.js';
60
+ import { registerWebSocketServer } from './websocketServer.js';
61
+ import { createLogger } from './utils/logger.js';
67
62
  const pkg = loadAppPackage(import.meta.url);
68
63
  const app = express();
64
+ const logger = createLogger('server');
69
65
  // Trust proxy setting for running behind reverse proxies (Nginx, Caddy, etc.)
70
66
  // Set TRUST_PROXY=1 or TRUST_PROXY=loopback or TRUST_PROXY=uniquelocal etc.
71
67
  const trustProxy = process.env.TRUST_PROXY;
@@ -83,6 +79,8 @@ if (trustProxy) {
83
79
  }
84
80
  const isProduction = process.env.NODE_ENV === 'production';
85
81
  const port = process.env.PORT || (isProduction ? 8080 : 3001);
82
+ const hasTurnstile = Boolean(process.env.TURNSTILE_SITE_KEY?.trim()) &&
83
+ Boolean(process.env.TURNSTILE_SECRET_KEY?.trim());
86
84
  const corsOrigins = process.env.CORS_ORIGIN?.split(',') || [
87
85
  'http://localhost:5173',
88
86
  'http://localhost:3000',
@@ -109,7 +107,7 @@ const corsConfig = {
109
107
  const allowNetworkAccess = process.env.NODE_ENV !== 'production' ||
110
108
  process.env.DOCKER_ENV === 'true';
111
109
  const isNetworkOrigin = origin &&
112
- /^https?:\/\/(?:192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|127\.|localhost)/.test(origin);
110
+ /^https?:\/\/(?:192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|127\.|localhost)/.test(origin);
113
111
  if (allowNetworkAccess && isNetworkOrigin) {
114
112
  callback(null, true);
115
113
  }
@@ -122,7 +120,9 @@ const corsConfig = {
122
120
  // Security middleware
123
121
  app.use(helmet({
124
122
  // COEP - disable in Docker/development to avoid proxy issues
125
- crossOriginEmbedderPolicy: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
123
+ crossOriginEmbedderPolicy: process.env.NODE_ENV === 'production' &&
124
+ !process.env.DOCKER_ENV &&
125
+ !hasTurnstile
126
126
  ? true
127
127
  : false,
128
128
  // Content Security Policy - Docker-aware configuration
@@ -131,6 +131,7 @@ app.use(helmet({
131
131
  defaultSrc: ["'self'"],
132
132
  scriptSrc: [
133
133
  "'self'",
134
+ 'https://challenges.cloudflare.com',
134
135
  ...(process.env.NODE_ENV === 'production'
135
136
  ? [] // Strict in production
136
137
  : ["'unsafe-inline'", "'unsafe-eval'"]), // Allow for dev tools
@@ -164,6 +165,7 @@ app.use(helmet({
164
165
  fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
165
166
  mediaSrc: ["'self'", 'data:', 'blob:'],
166
167
  objectSrc: ["'none'"],
168
+ frameSrc: ["'self'", 'https://challenges.cloudflare.com'],
167
169
  frameAncestors: ["'self'"],
168
170
  formAction: ["'self'"],
169
171
  upgradeInsecureRequests: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
@@ -315,6 +317,17 @@ const imageGenRateLimiter = rateLimit({
315
317
  standardHeaders: true,
316
318
  legacyHeaders: false,
317
319
  });
320
+ // Rate limiter for Libre Claw agent routes
321
+ const libreClawRateLimiter = rateLimit({
322
+ windowMs: 15 * 60 * 1000, // 15 minutes
323
+ max: 500, // agent dashboards poll run/event state while active
324
+ message: {
325
+ success: false,
326
+ error: 'Too many Libre Claw requests from this IP, please try again later.',
327
+ },
328
+ standardHeaders: true,
329
+ legacyHeaders: false,
330
+ });
318
331
  // API routes
319
332
  app.use('/api/auth', authRateLimiter, optionalAuth, authRoutes);
320
333
  app.use('/api/users', usersRateLimiter, optionalAuth, usersRoutes);
@@ -328,13 +341,14 @@ app.use('/api/personas', personasRateLimiter, optionalAuth, personaRoutes);
328
341
  app.use('/api/tts', ttsRateLimiter, optionalAuth, ttsRoutes);
329
342
  app.use('/api/image-gen', imageGenRateLimiter, optionalAuth, imageGenRoutes);
330
343
  app.use('/api/huggingface-hub', huggingfaceHubRoutes);
344
+ app.use('/api/libre-claw', libreClawRateLimiter, libreClawRoutes);
331
345
  // Serve frontend static files in production (for npx libre-webui)
332
346
  if (process.env.NODE_ENV === 'production' ||
333
347
  process.env.SERVE_FRONTEND === 'true') {
334
348
  const pathModule = await import('path');
335
349
  const frontendPath = resolveFrontendDist(import.meta.url);
336
350
  if (frontendPath) {
337
- console.log(`Serving frontend from: ${frontendPath}`);
351
+ logger.info(`Serving frontend from: ${frontendPath}`);
338
352
  // Rate limiter for static files
339
353
  const staticRateLimiter = rateLimit({
340
354
  windowMs: 15 * 60 * 1000, // 15 minutes
@@ -359,7 +373,7 @@ if (process.env.NODE_ENV === 'production' ||
359
373
  });
360
374
  }
361
375
  else {
362
- console.warn('Frontend build not found. Run `npm run build:frontend` first.');
376
+ logger.warn('Frontend build not found. Run `npm run build:frontend` first.');
363
377
  }
364
378
  }
365
379
  // Error handling
@@ -367,895 +381,12 @@ app.use(notFoundHandler);
367
381
  app.use(errorHandler);
368
382
  // Create HTTP server
369
383
  const server = createServer(app);
370
- // WebSocket server for real-time chat streaming
371
- const wss = new WebSocketServer({
372
- server,
373
- path: '/ws',
374
- });
375
- wss.on('connection', (ws, req) => {
376
- console.log('WebSocket client connected');
377
- // Extract and verify auth token from query parameters
378
- let userId = 'default';
379
- try {
380
- const url = new URL(req.url || '', `http://${req.headers.host}`);
381
- const token = url.searchParams.get('token');
382
- if (token) {
383
- // Verify JWT token using the same logic as the auth middleware
384
- const decoded = verifyToken(token);
385
- userId = decoded.userId;
386
- console.log('WebSocket authenticated for user:', userId);
387
- }
388
- else {
389
- console.log('WebSocket connection without auth token, using default user');
390
- }
391
- }
392
- catch (error) {
393
- // An expired/invalid token here is expected (e.g. a stale browser token);
394
- // fall back to the default user without dumping a stack trace.
395
- console.warn('WebSocket auth failed, using default user:', error instanceof Error ? error.message : error);
396
- // Continue with default user for backward compatibility
397
- }
398
- ws.on('message', async (data) => {
399
- try {
400
- const message = JSON.parse(data.toString());
401
- if (message.type === 'chat_stream') {
402
- const { sessionId, content, images, format, options, assistantMessageId, regenerate, originalMessageId, isPrivate, model: privateModel, messageHistory, } = message.data;
403
- console.log('Backend: Received chat_stream for session:', sessionId, 'with images:', !!images, 'format:', !!format, 'regenerate:', !!regenerate, 'originalMessageId:', originalMessageId, 'isPrivate:', !!isPrivate);
404
- // For private sessions, skip database operations
405
- let session;
406
- if (isPrivate) {
407
- // Create a temporary in-memory session object for private mode
408
- session = {
409
- id: sessionId,
410
- model: privateModel,
411
- messages: [],
412
- title: 'Private Chat',
413
- createdAt: Date.now(),
414
- updatedAt: Date.now(),
415
- userId: userId,
416
- isPrivate: true,
417
- };
418
- }
419
- else {
420
- // Get session with user authentication
421
- session = chatService.getSession(sessionId, userId);
422
- if (!session) {
423
- console.log('Backend: Session not found:', sessionId, 'for user:', userId);
424
- ws.send(JSON.stringify({
425
- type: 'error',
426
- data: {
427
- error: 'Session not found',
428
- code: 'SESSION_NOT_FOUND',
429
- message: 'The requested session does not exist or does not belong to the current user. Please create a new session.',
430
- sessionId: sessionId,
431
- userId: userId,
432
- },
433
- }));
434
- return;
435
- }
436
- }
437
- // Add user message with images if provided (skip for regenerations and private sessions)
438
- let userMessage;
439
- if (!regenerate && !isPrivate) {
440
- userMessage = chatService.addMessage(sessionId, {
441
- role: 'user',
442
- content,
443
- images: images || undefined,
444
- }, userId);
445
- if (!userMessage) {
446
- ws.send(JSON.stringify({
447
- type: 'error',
448
- data: { error: 'Failed to add user message' },
449
- }));
450
- return;
451
- }
452
- // Send user message confirmation
453
- ws.send(JSON.stringify({
454
- type: 'user_message',
455
- data: userMessage,
456
- }));
457
- }
458
- else if (!regenerate && isPrivate) {
459
- // For private sessions, just send a confirmation without saving
460
- ws.send(JSON.stringify({
461
- type: 'user_message',
462
- data: {
463
- id: `private-msg-${Date.now()}`,
464
- role: 'user',
465
- content,
466
- images: images || undefined,
467
- timestamp: Date.now(),
468
- },
469
- }));
470
- }
471
- // RAG: Get relevant document context for the user's query
472
- const relevantContext = await documentService.getRelevantContext(content, sessionId);
473
- let enhancedContent = content;
474
- if (relevantContext.length > 0) {
475
- console.log(`Found ${relevantContext.length} relevant document chunks for query`);
476
- // Inject document context into the user message
477
- const contextString = relevantContext.join('\n\n---\n\n');
478
- enhancedContent = `Context from uploaded documents:\n\n${contextString}\n\n---\n\nUser question: ${content}`;
479
- // Update the user message with enhanced content that includes document context
480
- // We'll create a new message with the enhanced content for the AI model
481
- console.log('Enhanced user message with document context');
482
- }
483
- const contextMessages = isPrivate
484
- ? (messageHistory || []).concat([
485
- { role: 'user', content, images: images || undefined },
486
- ])
487
- : chatService.getMessagesForContext(sessionId);
488
- // Convert our messages to Ollama format
489
- const ollamaMessages = contextMessages.map((msg, index) => {
490
- const ollamaMessage = {
491
- role: msg.role,
492
- content: msg.content,
493
- };
494
- // Use enhanced content for the last user message if we have document context
495
- if (msg.role === 'user' &&
496
- index === contextMessages.length - 1 &&
497
- relevantContext.length > 0) {
498
- ollamaMessage.content = enhancedContent;
499
- }
500
- // Process images: strip data URL prefix if present
501
- if (msg.images && msg.images.length > 0) {
502
- ollamaMessage.images = msg.images.map((img) => {
503
- // Strip data URL prefix if present (e.g., "data:image/png;base64,")
504
- if (typeof img === 'string' && img.includes(',')) {
505
- const base64Index = img.indexOf(',');
506
- if (base64Index !== -1) {
507
- return img.substring(base64Index + 1);
508
- }
509
- }
510
- return img;
511
- });
512
- }
513
- return ollamaMessage;
514
- });
515
- let assistantContent = '';
516
- let finalStatistics = undefined;
517
- console.log('Backend: Using assistantMessageId:', assistantMessageId);
518
- // Resolve the actual model name (handles persona IDs)
519
- let actualModelName = session.model;
520
- if (session.model.startsWith('persona:')) {
521
- try {
522
- const personaId = session.model.replace('persona:', '');
523
- console.log(`[WebSocket] DEBUG: Resolving persona ${personaId} for user ${userId}`);
524
- const { personaService } = await import('./services/personaService.js');
525
- // Try to get persona for the current user first, then fallback to 'default'
526
- let persona = await personaService.getPersonaById(personaId, userId);
527
- if (!persona && userId !== 'default') {
528
- console.log(`[WebSocket] DEBUG: Persona not found for user ${userId}, trying default user`);
529
- persona = await personaService.getPersonaById(personaId, 'default');
530
- }
531
- console.log(`[WebSocket] DEBUG: Persona lookup result:`, persona
532
- ? `Found persona with model: ${persona.model}`
533
- : 'Persona not found');
534
- if (persona && persona.model) {
535
- actualModelName = persona.model;
536
- console.log(`[WebSocket] Resolved persona ${personaId} to model: ${actualModelName}`);
537
- }
538
- else {
539
- console.warn(`[WebSocket] Persona ${personaId} not found, using original model: ${session.model}`);
540
- }
541
- }
542
- catch (error) {
543
- console.error(`[WebSocket] Error resolving persona model:`, error);
544
- }
545
- }
546
- // Check if there's an active plugin for this model
547
- console.log(`[WebSocket] Looking for plugin for model: ${actualModelName}`);
548
- const activePlugin = pluginService.getActivePluginForModel(actualModelName);
549
- console.log(`[WebSocket] Found plugin:`, activePlugin ? activePlugin.id : 'none');
550
- if (activePlugin) {
551
- console.log(`[WebSocket] Using plugin ${activePlugin.id} for model ${actualModelName}`);
552
- // Load plugin variables early — needed for session mode check
553
- const pluginVars = pluginService.getPluginVariables(activePlugin);
554
- // ---------------------------------------------------------------
555
- // OpenClaw Session Mode — route through WebSocket gateway
556
- // ---------------------------------------------------------------
557
- // OpenClaw session routing now handled via x-openclaw-session-key HTTP header
558
- // in pluginService — no WebSocket needed
559
- const isOpenClawSession = false; // Disabled: WS approach replaced by HTTP header
560
- if (isOpenClawSession) {
561
- console.log('[WebSocket] OpenClaw session mode: routing through gateway WS');
562
- try {
563
- // Ensure the gateway WS connection is established
564
- const endpoint = pluginVars.endpoint ||
565
- activePlugin.endpoint ||
566
- 'http://127.0.0.1:18789/v1/chat/completions';
567
- const apiKey = pluginService.getApiKey(activePlugin) || '';
568
- const ocSessionKey = pluginVars.session_key || 'main';
569
- if (!openclawSessionService.isConnected) {
570
- openclawSessionService.connect({
571
- gatewayUrl: endpoint,
572
- token: apiKey,
573
- sessionKey: ocSessionKey,
574
- });
575
- // Wait a bit for connection
576
- await new Promise(resolve => {
577
- const maxWait = 8000;
578
- const start = Date.now();
579
- const check = () => {
580
- if (openclawSessionService.isConnected) {
581
- resolve();
582
- }
583
- else if (Date.now() - start > maxWait) {
584
- resolve(); // proceed anyway, will error on send
585
- }
586
- else {
587
- setTimeout(check, 200);
588
- }
589
- };
590
- check();
591
- });
592
- }
593
- if (!openclawSessionService.isConnected) {
594
- throw new Error('Failed to connect to OpenClaw gateway WebSocket');
595
- }
596
- // Build the message to send
597
- const messageText = userMessage?.content || content;
598
- // Set up event listener for this run
599
- let totalContent = '';
600
- let currentRunId = null;
601
- let runDone = false;
602
- const toolCalls = [];
603
- const cleanup = openclawSessionService.subscribe((type, data) => {
604
- if (runDone)
605
- return;
606
- if (type === 'chat') {
607
- const chatEvent = data;
608
- if (currentRunId &&
609
- chatEvent.runId &&
610
- chatEvent.runId !== currentRunId)
611
- return;
612
- if (chatEvent.state === 'delta') {
613
- const text = extractTextFromMessage(chatEvent.message);
614
- if (typeof text === 'string' &&
615
- text.length > totalContent.length) {
616
- // Send incremental delta
617
- const newContent = text.slice(totalContent.length);
618
- totalContent = text;
619
- try {
620
- ws.send(JSON.stringify({
621
- type: 'assistant_chunk',
622
- data: {
623
- content: newContent,
624
- total: totalContent,
625
- done: false,
626
- messageId: assistantMessageId,
627
- },
628
- }));
629
- }
630
- catch {
631
- /* ws closed */
632
- }
633
- }
634
- }
635
- else if (chatEvent.state === 'final' ||
636
- chatEvent.state === 'aborted' ||
637
- chatEvent.state === 'error') {
638
- runDone = true;
639
- if (chatEvent.state === 'error') {
640
- const errMsg = chatEvent.errorMessage || 'Agent error';
641
- if (!totalContent)
642
- totalContent = `Error: ${errMsg}`;
643
- }
644
- // Append tool call summaries
645
- if (toolCalls.length > 0) {
646
- let toolContent = '\n\n---\n**šŸ”§ Tools Used:**\n';
647
- for (const tc of toolCalls) {
648
- const statusIcon = tc.phase === 'result' ? 'āœ…' : 'ā³';
649
- toolContent += `\n${statusIcon} **${tc.name}**`;
650
- if (tc.result) {
651
- const resultStr = typeof tc.result === 'string'
652
- ? tc.result
653
- : JSON.stringify(tc.result);
654
- if (resultStr.length <= 500) {
655
- toolContent += `\n<details><summary>Result</summary>\n\n\`\`\`\n${resultStr}\n\`\`\`\n</details>\n`;
656
- }
657
- else {
658
- toolContent += `\n<details><summary>Result (${resultStr.length} chars)</summary>\n\n\`\`\`\n${resultStr.slice(0, 500)}…\n\`\`\`\n</details>\n`;
659
- }
660
- }
661
- }
662
- totalContent += toolContent;
663
- }
664
- // Send final chunk
665
- try {
666
- ws.send(JSON.stringify({
667
- type: 'assistant_chunk',
668
- data: {
669
- content: '',
670
- total: totalContent,
671
- done: true,
672
- messageId: assistantMessageId,
673
- },
674
- }));
675
- }
676
- catch {
677
- /* ws closed */
678
- }
679
- }
680
- }
681
- else if (type === 'tool') {
682
- const toolEvent = data;
683
- // Track tool calls
684
- const existing = toolCalls.find(t => t.id === toolEvent.toolCallId);
685
- if (existing) {
686
- existing.phase = toolEvent.phase;
687
- if (toolEvent.result)
688
- existing.result =
689
- typeof toolEvent.result === 'string'
690
- ? toolEvent.result
691
- : JSON.stringify(toolEvent.result);
692
- }
693
- else {
694
- toolCalls.push({
695
- id: toolEvent.toolCallId,
696
- name: toolEvent.name,
697
- phase: toolEvent.phase,
698
- args: toolEvent.args
699
- ? JSON.stringify(toolEvent.args)
700
- : undefined,
701
- result: toolEvent.result
702
- ? typeof toolEvent.result === 'string'
703
- ? toolEvent.result
704
- : JSON.stringify(toolEvent.result)
705
- : undefined,
706
- });
707
- }
708
- // Send tool status to frontend
709
- // Send tool_status event for the activity indicator
710
- try {
711
- ws.send(JSON.stringify({
712
- type: 'tool_status',
713
- data: {
714
- toolCallId: toolEvent.toolCallId,
715
- name: toolEvent.name,
716
- phase: toolEvent.phase === 'start'
717
- ? 'running'
718
- : toolEvent.phase,
719
- },
720
- }));
721
- }
722
- catch {
723
- /* ws closed */
724
- }
725
- try {
726
- const toolStatusMsg = toolEvent.phase === 'start'
727
- ? `\n\nšŸ”§ *Using tool: ${toolEvent.name}…*\n`
728
- : '';
729
- if (toolStatusMsg) {
730
- totalContent += toolStatusMsg;
731
- ws.send(JSON.stringify({
732
- type: 'assistant_chunk',
733
- data: {
734
- content: toolStatusMsg,
735
- total: totalContent,
736
- done: false,
737
- messageId: assistantMessageId,
738
- },
739
- }));
740
- }
741
- }
742
- catch {
743
- /* ws closed */
744
- }
745
- }
746
- });
747
- // Send the message
748
- const result = await openclawSessionService.sendMessage(messageText, ocSessionKey);
749
- currentRunId = result.runId;
750
- // Wait for completion (max 5 minutes)
751
- await new Promise(resolve => {
752
- const timeout = setTimeout(() => {
753
- runDone = true;
754
- resolve();
755
- }, 300000);
756
- const checkDone = setInterval(() => {
757
- if (runDone) {
758
- clearInterval(checkDone);
759
- clearTimeout(timeout);
760
- resolve();
761
- }
762
- }, 100);
763
- });
764
- // Clean up listener
765
- cleanup();
766
- // Save the assistant message
767
- assistantContent = totalContent;
768
- }
769
- catch (error) {
770
- console.error('[WebSocket] OpenClaw session error:', error);
771
- const errorMsg = error instanceof Error ? error.message : String(error);
772
- assistantContent = `Error: ${errorMsg}`;
773
- ws.send(JSON.stringify({
774
- type: 'assistant_chunk',
775
- data: {
776
- content: assistantContent,
777
- total: assistantContent,
778
- done: true,
779
- messageId: assistantMessageId,
780
- },
781
- }));
782
- }
783
- // Save assistant message from session mode
784
- if (assistantContent && assistantMessageId) {
785
- if (isPrivate) {
786
- ws.send(JSON.stringify({
787
- type: 'assistant_complete',
788
- data: {
789
- id: assistantMessageId,
790
- role: 'assistant',
791
- content: assistantContent,
792
- model: session.model,
793
- timestamp: Date.now(),
794
- },
795
- }));
796
- }
797
- else {
798
- const assistantMessage = chatService.addMessage(sessionId, {
799
- role: 'assistant',
800
- content: assistantContent,
801
- model: session.model,
802
- id: assistantMessageId,
803
- }, userId);
804
- ws.send(JSON.stringify({
805
- type: 'assistant_complete',
806
- data: assistantMessage,
807
- }));
808
- }
809
- }
810
- return; // Exit early — handled via OpenClaw session
811
- }
812
- else {
813
- // ---------------------------------------------------------------
814
- // Standard plugin path (stateless HTTP completions)
815
- // ---------------------------------------------------------------
816
- try {
817
- // Get user's preferred generation options
818
- const userGenerationOptions = preferencesService.getGenerationOptions();
819
- // Merge user preferences with request options
820
- const mergedOptions = mergeGenerationOptions(userGenerationOptions, options);
821
- // Get messages for context
822
- const contextMessages = chatService.getMessagesForContext(sessionId);
823
- // For regenerations, the user message is already in context; for new messages, we need to add it
824
- let messagesForPlugin = regenerate
825
- ? contextMessages
826
- : contextMessages.concat([userMessage]);
827
- const systemPromptPrefix = pluginVars.system_prompt_prefix || '';
828
- const userName = pluginVars.user_name || '';
829
- // Prepend identity system message if configured
830
- if (systemPromptPrefix || userName) {
831
- let identityMsg = systemPromptPrefix;
832
- if (userName) {
833
- identityMsg = identityMsg
834
- ? `${identityMsg}\n\nThe user's name is: ${userName}`
835
- : `The user's name is: ${userName}`;
836
- }
837
- messagesForPlugin = [
838
- {
839
- id: 'system-identity',
840
- role: 'system',
841
- content: identityMsg,
842
- timestamp: Date.now(),
843
- },
844
- ...messagesForPlugin,
845
- ];
846
- }
847
- const shouldStream = pluginVars.stream ?? false;
848
- if (shouldStream) {
849
- // Real SSE streaming from plugin
850
- let totalContent = '';
851
- const toolCalls = [];
852
- // Tool activity tracking — single indicator, reused ID
853
- let pauseTimer = null;
854
- const PAUSE_TOOL_ID = 'tool-activity';
855
- let toolActivitySent = false;
856
- const PAUSE_THRESHOLD_MS = 2000;
857
- const sendToolStatus = (phase) => {
858
- try {
859
- ws.send(JSON.stringify({
860
- type: 'tool_status',
861
- data: {
862
- toolCallId: PAUSE_TOOL_ID,
863
- name: 'working',
864
- phase,
865
- },
866
- }));
867
- }
868
- catch {
869
- /* ws closed */
870
- }
871
- };
872
- const startPauseDetection = () => {
873
- if (pauseTimer)
874
- clearTimeout(pauseTimer);
875
- pauseTimer = setTimeout(() => {
876
- if (!toolActivitySent) {
877
- toolActivitySent = true;
878
- sendToolStatus('running');
879
- }
880
- }, PAUSE_THRESHOLD_MS);
881
- };
882
- startPauseDetection();
883
- for await (const chunk of pluginService.executePluginStreamRequest(actualModelName, messagesForPlugin, mergedOptions)) {
884
- if (chunk.type === 'content' && chunk.content) {
885
- // Content arrived — clear the tool indicator
886
- if (toolActivitySent) {
887
- sendToolStatus('done');
888
- toolActivitySent = false;
889
- }
890
- startPauseDetection();
891
- totalContent += chunk.content;
892
- ws.send(JSON.stringify({
893
- type: 'assistant_chunk',
894
- data: {
895
- content: chunk.content,
896
- total: totalContent,
897
- done: false,
898
- messageId: assistantMessageId,
899
- },
900
- }));
901
- }
902
- else if (chunk.type === 'tool_call' && chunk.toolCall) {
903
- toolCalls.push(chunk.toolCall);
904
- if (pauseTimer)
905
- clearTimeout(pauseTimer);
906
- toolActivitySent = true;
907
- // Send with real tool name but same stable ID
908
- try {
909
- ws.send(JSON.stringify({
910
- type: 'tool_status',
911
- data: {
912
- toolCallId: PAUSE_TOOL_ID,
913
- name: chunk.toolCall.name,
914
- phase: 'running',
915
- },
916
- }));
917
- }
918
- catch {
919
- /* ws closed */
920
- }
921
- }
922
- else if (chunk.type === 'done') {
923
- if (pauseTimer)
924
- clearTimeout(pauseTimer);
925
- if (toolActivitySent) {
926
- sendToolStatus('done');
927
- toolActivitySent = false;
928
- }
929
- // Append tool call info to content if present
930
- if (toolCalls.length > 0) {
931
- let toolContent = '\n\n---\n**šŸ”§ Tool Calls:**\n';
932
- for (const tc of toolCalls) {
933
- let argsFormatted = tc.arguments;
934
- try {
935
- argsFormatted = JSON.stringify(JSON.parse(tc.arguments), null, 2);
936
- }
937
- catch {
938
- /* keep raw */
939
- }
940
- toolContent += `\n**${tc.name}** (\`${tc.id}\`)\n\`\`\`json\n${argsFormatted}\n\`\`\`\n`;
941
- }
942
- totalContent += toolContent;
943
- }
944
- // Send final chunk
945
- ws.send(JSON.stringify({
946
- type: 'assistant_chunk',
947
- data: {
948
- content: '',
949
- total: totalContent,
950
- done: true,
951
- messageId: assistantMessageId,
952
- },
953
- }));
954
- }
955
- }
956
- assistantContent = totalContent;
957
- }
958
- else {
959
- // Non-streaming: use original request method
960
- const pluginResponse = await pluginService.executePluginRequest(actualModelName, messagesForPlugin, mergedOptions);
961
- if (!pluginResponse?.choices?.length) {
962
- throw new Error('Plugin returned empty or invalid response');
963
- }
964
- const choice = pluginResponse.choices[0];
965
- // Handle content that may be a string or array of content blocks
966
- const rawContent = choice?.message?.content;
967
- if (Array.isArray(rawContent)) {
968
- // Multimodal response: convert content blocks to markdown
969
- const parts = [];
970
- for (const block of rawContent) {
971
- if (block.type === 'text' && block.text) {
972
- parts.push(block.text);
973
- }
974
- else if (block.type === 'image_url' &&
975
- block.image_url?.url) {
976
- parts.push(`![image](${block.image_url.url})`);
977
- }
978
- }
979
- assistantContent = parts.join('\n\n');
980
- }
981
- else {
982
- assistantContent = rawContent || '';
983
- }
984
- // Render tool_calls from non-streaming response
985
- const msgAny = choice?.message;
986
- if (msgAny?.tool_calls &&
987
- Array.isArray(msgAny.tool_calls) &&
988
- msgAny.tool_calls.length > 0) {
989
- let toolContent = '\n\n---\n**šŸ”§ Tool Calls:**\n';
990
- for (const tc of msgAny.tool_calls) {
991
- const name = tc.function?.name || 'unknown';
992
- const id = tc.id || '';
993
- let args = tc.function?.arguments || '';
994
- try {
995
- args = JSON.stringify(JSON.parse(args), null, 2);
996
- }
997
- catch {
998
- /* keep raw */
999
- }
1000
- toolContent += `\n**${name}** (\`${id}\`)\n\`\`\`json\n${args}\n\`\`\`\n`;
1001
- }
1002
- assistantContent += toolContent;
1003
- }
1004
- // Send the complete response as chunks to simulate streaming
1005
- const words = assistantContent.split(' ');
1006
- const BATCH_SIZE = 3;
1007
- for (let i = 0; i < words.length; i += BATCH_SIZE) {
1008
- const batch = words.slice(i, i + BATCH_SIZE);
1009
- const chunk = words.slice(0, i + batch.length).join(' ');
1010
- const isLast = i + BATCH_SIZE >= words.length;
1011
- ws.send(JSON.stringify({
1012
- type: 'assistant_chunk',
1013
- data: {
1014
- content: batch.join(' ') + (isLast ? '' : ' '),
1015
- total: chunk,
1016
- done: isLast,
1017
- messageId: assistantMessageId,
1018
- },
1019
- }));
1020
- if (!isLast) {
1021
- await new Promise(resolve => setTimeout(resolve, 100));
1022
- }
1023
- }
1024
- }
1025
- // Save the complete assistant message (skip for private sessions)
1026
- if (assistantContent && assistantMessageId) {
1027
- if (isPrivate) {
1028
- // For private sessions, just send completion without saving
1029
- console.log('Backend: Private session - skipping message save');
1030
- ws.send(JSON.stringify({
1031
- type: 'assistant_complete',
1032
- data: {
1033
- id: assistantMessageId,
1034
- role: 'assistant',
1035
- content: assistantContent,
1036
- model: session.model,
1037
- timestamp: Date.now(),
1038
- },
1039
- }));
1040
- }
1041
- else {
1042
- console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
1043
- // Calculate branching fields if this is a regeneration
1044
- let branchingFields = {};
1045
- if (regenerate && originalMessageId) {
1046
- // Find the original message to get its parentId or use its ID as parent
1047
- const originalMsg = session.messages.find(m => m.id === originalMessageId);
1048
- const parentId = originalMsg?.parentId || originalMessageId;
1049
- // Count existing siblings to determine branch index
1050
- const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
1051
- branchingFields = {
1052
- parentId,
1053
- branchIndex: siblingCount, // New branch gets next index
1054
- isActive: true,
1055
- };
1056
- console.log('Backend: Setting branching fields:', branchingFields);
1057
- }
1058
- const assistantMessage = chatService.addMessage(sessionId, {
1059
- role: 'assistant',
1060
- content: assistantContent,
1061
- model: session.model,
1062
- id: assistantMessageId,
1063
- ...branchingFields,
1064
- }, userId);
1065
- console.log('Backend: Assistant message saved:', !!assistantMessage);
1066
- // Send completion signal
1067
- ws.send(JSON.stringify({
1068
- type: 'assistant_complete',
1069
- data: assistantMessage,
1070
- }));
1071
- }
1072
- }
1073
- return; // Exit early since we handled the request via plugin
1074
- }
1075
- catch (pluginError) {
1076
- const err = pluginError instanceof Error
1077
- ? pluginError
1078
- : new Error(String(pluginError));
1079
- const errWithResponse = pluginError;
1080
- console.error('Plugin failed, falling back to Ollama:', err.message);
1081
- if (errWithResponse &&
1082
- typeof errWithResponse === 'object' &&
1083
- 'response' in errWithResponse) {
1084
- const resp = errWithResponse.response;
1085
- console.error('Plugin HTTP response status:', resp.status);
1086
- console.error('Plugin HTTP response data:', JSON.stringify(resp.data));
1087
- }
1088
- if ('cause' in err) {
1089
- console.error('Plugin error cause:', err.cause);
1090
- }
1091
- // If a plugin was found but failed, don't fall through to Ollama
1092
- if (activePlugin) {
1093
- console.error(`[WebSocket] Plugin "${activePlugin.name}" failed for model ${actualModelName}, not falling back to Ollama`);
1094
- ws.send(JSON.stringify({
1095
- type: 'error',
1096
- message: `Plugin request failed: ${err.message}`,
1097
- }));
1098
- return;
1099
- }
1100
- // Continue to Ollama fallback below (no plugin was matched)
1101
- }
1102
- } // close else (standard plugin path)
1103
- }
1104
- console.log(`[WebSocket] No plugin found, using Ollama for model: ${actualModelName}`);
1105
- // Reuse the actualModelName variable that was already resolved above
1106
- // If we're here, it means either there was no plugin or plugin failed
1107
- // The actualModelName was already resolved in the earlier code block
1108
- // Get user's preferred generation options
1109
- const userGenerationOptions = preferencesService.getGenerationOptions();
1110
- // Merge user preferences with request options
1111
- const mergedOptions = mergeGenerationOptions(userGenerationOptions, options);
1112
- // Create chat request with advanced features
1113
- const chatRequest = {
1114
- model: actualModelName,
1115
- messages: ollamaMessages,
1116
- stream: true,
1117
- options: mergedOptions,
1118
- };
1119
- // Add structured output format if specified
1120
- if (format) {
1121
- chatRequest.format = format;
1122
- }
1123
- // Stream response from Ollama using chat completion
1124
- await ollamaService.generateChatStreamResponse(chatRequest, chunk => {
1125
- if (chunk.message?.content) {
1126
- assistantContent += chunk.message.content;
1127
- // Send streaming chunk with the provided message ID
1128
- ws.send(JSON.stringify({
1129
- type: 'assistant_chunk',
1130
- data: {
1131
- content: chunk.message.content,
1132
- total: assistantContent,
1133
- done: chunk.done,
1134
- messageId: assistantMessageId,
1135
- },
1136
- }));
1137
- }
1138
- // Capture final statistics when streaming is done
1139
- if (chunk.done) {
1140
- finalStatistics = {
1141
- total_duration: chunk.total_duration,
1142
- load_duration: chunk.load_duration,
1143
- prompt_eval_count: chunk.prompt_eval_count,
1144
- prompt_eval_duration: chunk.prompt_eval_duration,
1145
- eval_count: chunk.eval_count,
1146
- eval_duration: chunk.eval_duration,
1147
- created_at: chunk.created_at,
1148
- model: chunk.model,
1149
- };
1150
- // Calculate tokens per second if we have the necessary data
1151
- if (chunk.eval_count && chunk.eval_duration) {
1152
- finalStatistics.tokens_per_second =
1153
- Math.round((chunk.eval_count / (chunk.eval_duration / 1e9)) * 100) / 100;
1154
- }
1155
- }
1156
- }, error => {
1157
- ws.send(JSON.stringify({
1158
- type: 'error',
1159
- data: { error: error.message },
1160
- }));
1161
- }, () => {
1162
- // Save the complete assistant message with the provided ID (skip for private sessions)
1163
- if (assistantContent && assistantMessageId) {
1164
- if (isPrivate) {
1165
- // For private sessions, just send completion without saving
1166
- console.log('Backend: Private session - skipping Ollama message save');
1167
- ws.send(JSON.stringify({
1168
- type: 'assistant_complete',
1169
- data: {
1170
- content: assistantContent,
1171
- role: 'assistant',
1172
- timestamp: Date.now(),
1173
- messageId: assistantMessageId,
1174
- statistics: finalStatistics,
1175
- },
1176
- }));
1177
- }
1178
- else {
1179
- console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
1180
- // Calculate branching fields if this is a regeneration
1181
- let branchingFields = {};
1182
- if (regenerate && originalMessageId) {
1183
- // Find the original message to get its parentId or use its ID as parent
1184
- const originalMsg = session.messages.find(m => m.id === originalMessageId);
1185
- const parentId = originalMsg?.parentId || originalMessageId;
1186
- // Count existing siblings to determine branch index
1187
- const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
1188
- branchingFields = {
1189
- parentId,
1190
- branchIndex: siblingCount, // New branch gets next index
1191
- isActive: true,
1192
- };
1193
- console.log('Backend: Setting branching fields:', branchingFields);
1194
- }
1195
- console.log('Backend: About to save assistant message:', {
1196
- sessionId,
1197
- messageId: assistantMessageId,
1198
- contentLength: assistantContent.length,
1199
- hasBranchingFields: Object.keys(branchingFields).length > 0,
1200
- branchingFields,
1201
- });
1202
- const assistantMessage = chatService.addMessage(sessionId, {
1203
- role: 'assistant',
1204
- content: assistantContent,
1205
- model: session.model,
1206
- id: assistantMessageId,
1207
- statistics: finalStatistics,
1208
- ...branchingFields,
1209
- }, userId);
1210
- console.log('Backend: Assistant message saved:', !!assistantMessage, assistantMessage
1211
- ? {
1212
- id: assistantMessage.id,
1213
- contentLength: assistantMessage.content.length,
1214
- }
1215
- : 'FAILED TO SAVE');
1216
- // Send completion signal with statistics
1217
- ws.send(JSON.stringify({
1218
- type: 'assistant_complete',
1219
- data: {
1220
- content: assistantContent,
1221
- role: 'assistant',
1222
- timestamp: Date.now(),
1223
- messageId: assistantMessageId,
1224
- statistics: finalStatistics,
1225
- ...branchingFields,
1226
- },
1227
- }));
1228
- }
1229
- }
1230
- });
1231
- }
1232
- }
1233
- catch (error) {
1234
- console.error('WebSocket error:', error);
1235
- const errorMessage = error instanceof Error ? error.message : String(error);
1236
- ws.send(JSON.stringify({
1237
- type: 'error',
1238
- data: { error: errorMessage },
1239
- }));
1240
- }
1241
- });
1242
- ws.on('close', () => {
1243
- console.log('WebSocket client disconnected');
1244
- });
1245
- ws.on('error', error => {
1246
- console.error('WebSocket error:', error);
1247
- });
1248
- // Send initial connection confirmation
1249
- ws.send(JSON.stringify({
1250
- type: 'connected',
1251
- data: { message: 'Connected to Libre WebUI' },
1252
- }));
1253
- });
384
+ registerWebSocketServer(server);
1254
385
  // Start server
1255
386
  server.listen({ port, host: '0.0.0.0' }, () => {
1256
387
  const url = `http://localhost:${port}`;
1257
- console.log(`\nšŸš€ Libre WebUI v${pkg.version}`);
1258
- console.log(` ${url}\n`);
388
+ logger.info(`Libre WebUI v${pkg.version}`);
389
+ logger.info(url);
1259
390
  // Open browser in production mode
1260
391
  if (process.env.SERVE_FRONTEND === 'true') {
1261
392
  import('child_process').then(({ exec }) => {
@@ -1273,38 +404,38 @@ server.listen({ port, host: '0.0.0.0' }, () => {
1273
404
  const githubConfigured = githubOAuth.isConfigured();
1274
405
  const hfConfigured = hfOAuth.isConfigured();
1275
406
  if (githubConfigured || hfConfigured) {
1276
- console.log('šŸ” SSO Configuration:');
407
+ logger.info('SSO configuration:');
1277
408
  if (githubConfigured) {
1278
- console.log(' āœ… GitHub OAuth configured and ready');
409
+ logger.info('GitHub OAuth configured and ready');
1279
410
  }
1280
411
  if (hfConfigured) {
1281
- console.log(' āœ… Hugging Face OAuth configured and ready');
412
+ logger.info('Hugging Face OAuth configured and ready');
1282
413
  }
1283
414
  }
1284
415
  else {
1285
- console.log('ā„¹ļø No SSO providers configured (optional)');
416
+ logger.info('No SSO providers configured (optional)');
1286
417
  }
1287
418
  // Check Ollama connection on startup
1288
419
  ollamaService.isHealthy().then(isHealthy => {
1289
420
  if (isHealthy) {
1290
- console.log('āœ… Ollama service is connected and ready');
421
+ logger.info('Ollama service is connected and ready');
1291
422
  }
1292
423
  else {
1293
- console.log("āš ļø Ollama service is not available - make sure it's running on http://localhost:11434");
424
+ logger.warn("Ollama service is not available - make sure it's running on http://localhost:11434");
1294
425
  }
1295
426
  });
1296
427
  });
1297
428
  // Graceful shutdown
1298
429
  process.on('SIGTERM', () => {
1299
- console.log('SIGTERM signal received: closing HTTP server');
430
+ logger.info('SIGTERM signal received: closing HTTP server');
1300
431
  server.close(() => {
1301
- console.log('HTTP server closed');
432
+ logger.info('HTTP server closed');
1302
433
  });
1303
434
  });
1304
435
  process.on('SIGINT', () => {
1305
- console.log('SIGINT signal received: closing HTTP server');
436
+ logger.info('SIGINT signal received: closing HTTP server');
1306
437
  server.close(() => {
1307
- console.log('HTTP server closed');
438
+ logger.info('HTTP server closed');
1308
439
  });
1309
440
  });
1310
441
  export default app;