libre-webui 0.10.0 → 0.11.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 (287) hide show
  1. package/README.md +64 -46
  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 +26 -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/ollama.d.ts.map +1 -1
  30. package/backend/dist/routes/ollama.js +7 -247
  31. package/backend/dist/routes/ollama.js.map +1 -1
  32. package/backend/dist/routes/plugins.d.ts.map +1 -1
  33. package/backend/dist/routes/plugins.js +7 -105
  34. package/backend/dist/routes/plugins.js.map +1 -1
  35. package/backend/dist/routes/tts.d.ts.map +1 -1
  36. package/backend/dist/routes/tts.js +7 -5
  37. package/backend/dist/routes/tts.js.map +1 -1
  38. package/backend/dist/routes/users.d.ts.map +1 -1
  39. package/backend/dist/routes/users.js +7 -5
  40. package/backend/dist/routes/users.js.map +1 -1
  41. package/backend/dist/services/assistantCompletionService.d.ts +26 -0
  42. package/backend/dist/services/assistantCompletionService.d.ts.map +1 -0
  43. package/backend/dist/services/assistantCompletionService.js +53 -0
  44. package/backend/dist/services/assistantCompletionService.js.map +1 -0
  45. package/backend/dist/services/authService.d.ts +2 -0
  46. package/backend/dist/services/authService.d.ts.map +1 -1
  47. package/backend/dist/services/authService.js +9 -5
  48. package/backend/dist/services/authService.js.map +1 -1
  49. package/backend/dist/services/chatGenerationService.d.ts +32 -0
  50. package/backend/dist/services/chatGenerationService.d.ts.map +1 -0
  51. package/backend/dist/services/chatGenerationService.js +118 -0
  52. package/backend/dist/services/chatGenerationService.js.map +1 -0
  53. package/backend/dist/services/chatRequestService.d.ts +55 -0
  54. package/backend/dist/services/chatRequestService.d.ts.map +1 -0
  55. package/backend/dist/services/chatRequestService.js +103 -0
  56. package/backend/dist/services/chatRequestService.js.map +1 -0
  57. package/backend/dist/services/chatService.d.ts +0 -1
  58. package/backend/dist/services/chatService.d.ts.map +1 -1
  59. package/backend/dist/services/chatService.js +13 -27
  60. package/backend/dist/services/chatService.js.map +1 -1
  61. package/backend/dist/services/documentService.d.ts.map +1 -1
  62. package/backend/dist/services/documentService.js +21 -19
  63. package/backend/dist/services/documentService.js.map +1 -1
  64. package/backend/dist/services/embeddingService.d.ts.map +1 -1
  65. package/backend/dist/services/embeddingService.js +3 -1
  66. package/backend/dist/services/embeddingService.js.map +1 -1
  67. package/backend/dist/services/encryptionService.d.ts.map +1 -1
  68. package/backend/dist/services/encryptionService.js +26 -24
  69. package/backend/dist/services/encryptionService.js.map +1 -1
  70. package/backend/dist/services/galleryService.d.ts.map +1 -1
  71. package/backend/dist/services/galleryService.js +8 -6
  72. package/backend/dist/services/galleryService.js.map +1 -1
  73. package/backend/dist/services/memoryService.d.ts +2 -13
  74. package/backend/dist/services/memoryService.d.ts.map +1 -1
  75. package/backend/dist/services/memoryService.js +37 -198
  76. package/backend/dist/services/memoryService.js.map +1 -1
  77. package/backend/dist/services/memoryUtils.d.ts +29 -0
  78. package/backend/dist/services/memoryUtils.d.ts.map +1 -0
  79. package/backend/dist/services/memoryUtils.js +165 -0
  80. package/backend/dist/services/memoryUtils.js.map +1 -0
  81. package/backend/dist/services/mutationEngineService.d.ts.map +1 -1
  82. package/backend/dist/services/mutationEngineService.js +3 -1
  83. package/backend/dist/services/mutationEngineService.js.map +1 -1
  84. package/backend/dist/services/ollamaService.d.ts.map +1 -1
  85. package/backend/dist/services/ollamaService.js +36 -34
  86. package/backend/dist/services/ollamaService.js.map +1 -1
  87. package/backend/dist/services/personaService.d.ts.map +1 -1
  88. package/backend/dist/services/personaService.js +3 -1
  89. package/backend/dist/services/personaService.js.map +1 -1
  90. package/backend/dist/services/pluginCapabilityRegistryService.d.ts +11 -0
  91. package/backend/dist/services/pluginCapabilityRegistryService.d.ts.map +1 -0
  92. package/backend/dist/services/pluginCapabilityRegistryService.js +70 -0
  93. package/backend/dist/services/pluginCapabilityRegistryService.js.map +1 -0
  94. package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -1
  95. package/backend/dist/services/pluginCredentialsService.js +13 -11
  96. package/backend/dist/services/pluginCredentialsService.js.map +1 -1
  97. package/backend/dist/services/pluginEmbeddingService.d.ts +26 -0
  98. package/backend/dist/services/pluginEmbeddingService.d.ts.map +1 -0
  99. package/backend/dist/services/pluginEmbeddingService.js +159 -0
  100. package/backend/dist/services/pluginEmbeddingService.js.map +1 -0
  101. package/backend/dist/services/pluginImageGenerationService.d.ts +29 -0
  102. package/backend/dist/services/pluginImageGenerationService.d.ts.map +1 -0
  103. package/backend/dist/services/pluginImageGenerationService.js +402 -0
  104. package/backend/dist/services/pluginImageGenerationService.js.map +1 -0
  105. package/backend/dist/services/pluginService.d.ts +9 -18
  106. package/backend/dist/services/pluginService.d.ts.map +1 -1
  107. package/backend/dist/services/pluginService.js +120 -1469
  108. package/backend/dist/services/pluginService.js.map +1 -1
  109. package/backend/dist/services/pluginTTSService.d.ts +28 -0
  110. package/backend/dist/services/pluginTTSService.d.ts.map +1 -0
  111. package/backend/dist/services/pluginTTSService.js +286 -0
  112. package/backend/dist/services/pluginTTSService.js.map +1 -0
  113. package/backend/dist/services/pluginVariablesService.d.ts.map +1 -1
  114. package/backend/dist/services/pluginVariablesService.js +7 -5
  115. package/backend/dist/services/pluginVariablesService.js.map +1 -1
  116. package/backend/dist/services/preferencesService.d.ts.map +1 -1
  117. package/backend/dist/services/preferencesService.js +9 -7
  118. package/backend/dist/services/preferencesService.js.map +1 -1
  119. package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -1
  120. package/backend/dist/services/simpleGitHubOAuth.js +10 -8
  121. package/backend/dist/services/simpleGitHubOAuth.js.map +1 -1
  122. package/backend/dist/services/simpleHuggingFaceOAuth.d.ts.map +1 -1
  123. package/backend/dist/services/simpleHuggingFaceOAuth.js +11 -9
  124. package/backend/dist/services/simpleHuggingFaceOAuth.js.map +1 -1
  125. package/backend/dist/services/systemSettingsService.d.ts.map +1 -1
  126. package/backend/dist/services/systemSettingsService.js +4 -2
  127. package/backend/dist/services/systemSettingsService.js.map +1 -1
  128. package/backend/dist/services/titleGenerationService.d.ts +55 -0
  129. package/backend/dist/services/titleGenerationService.d.ts.map +1 -0
  130. package/backend/dist/services/titleGenerationService.js +121 -0
  131. package/backend/dist/services/titleGenerationService.js.map +1 -0
  132. package/backend/dist/services/turnstileService.d.ts +13 -0
  133. package/backend/dist/services/turnstileService.d.ts.map +1 -0
  134. package/backend/dist/services/turnstileService.js +79 -0
  135. package/backend/dist/services/turnstileService.js.map +1 -0
  136. package/backend/dist/storage.d.ts +2 -22
  137. package/backend/dist/storage.d.ts.map +1 -1
  138. package/backend/dist/storage.js +22 -170
  139. package/backend/dist/storage.js.map +1 -1
  140. package/backend/dist/storageMappers.d.ts +82 -0
  141. package/backend/dist/storageMappers.d.ts.map +1 -0
  142. package/backend/dist/storageMappers.js +85 -0
  143. package/backend/dist/storageMappers.js.map +1 -0
  144. package/backend/dist/test-encryption.js +20 -18
  145. package/backend/dist/test-encryption.js.map +1 -1
  146. package/backend/dist/utils/assistantBranching.d.ts +8 -0
  147. package/backend/dist/utils/assistantBranching.d.ts.map +1 -0
  148. package/backend/dist/utils/assistantBranching.js +30 -0
  149. package/backend/dist/utils/assistantBranching.js.map +1 -0
  150. package/backend/dist/utils/chatContext.d.ts +17 -0
  151. package/backend/dist/utils/chatContext.d.ts.map +1 -0
  152. package/backend/dist/utils/chatContext.js +82 -0
  153. package/backend/dist/utils/chatContext.js.map +1 -0
  154. package/backend/dist/utils/chatDocumentContext.d.ts +7 -0
  155. package/backend/dist/utils/chatDocumentContext.d.ts.map +1 -0
  156. package/backend/dist/utils/chatDocumentContext.js +51 -0
  157. package/backend/dist/utils/chatDocumentContext.js.map +1 -0
  158. package/backend/dist/utils/logger.d.ts +12 -0
  159. package/backend/dist/utils/logger.d.ts.map +1 -0
  160. package/backend/dist/utils/logger.js +82 -0
  161. package/backend/dist/utils/logger.js.map +1 -0
  162. package/backend/dist/utils/ollamaLibrary.d.ts +17 -0
  163. package/backend/dist/utils/ollamaLibrary.d.ts.map +1 -0
  164. package/backend/dist/utils/ollamaLibrary.js +253 -0
  165. package/backend/dist/utils/ollamaLibrary.js.map +1 -0
  166. package/backend/dist/utils/ollamaStreaming.d.ts +19 -0
  167. package/backend/dist/utils/ollamaStreaming.d.ts.map +1 -0
  168. package/backend/dist/utils/ollamaStreaming.js +67 -0
  169. package/backend/dist/utils/ollamaStreaming.js.map +1 -0
  170. package/backend/dist/utils/pluginChatAdapter.d.ts +32 -0
  171. package/backend/dist/utils/pluginChatAdapter.d.ts.map +1 -0
  172. package/backend/dist/utils/pluginChatAdapter.js +308 -0
  173. package/backend/dist/utils/pluginChatAdapter.js.map +1 -0
  174. package/backend/dist/utils/pluginChatContext.d.ts +23 -0
  175. package/backend/dist/utils/pluginChatContext.d.ts.map +1 -0
  176. package/backend/dist/utils/pluginChatContext.js +77 -0
  177. package/backend/dist/utils/pluginChatContext.js.map +1 -0
  178. package/backend/dist/utils/pluginResponse.d.ts +6 -0
  179. package/backend/dist/utils/pluginResponse.d.ts.map +1 -0
  180. package/backend/dist/utils/pluginResponse.js +70 -0
  181. package/backend/dist/utils/pluginResponse.js.map +1 -0
  182. package/backend/dist/utils/pluginStreamAdapter.d.ts +16 -0
  183. package/backend/dist/utils/pluginStreamAdapter.d.ts.map +1 -0
  184. package/backend/dist/utils/pluginStreamAdapter.js +141 -0
  185. package/backend/dist/utils/pluginStreamAdapter.js.map +1 -0
  186. package/backend/dist/utils/pluginStreaming.d.ts +24 -0
  187. package/backend/dist/utils/pluginStreaming.d.ts.map +1 -0
  188. package/backend/dist/utils/pluginStreaming.js +106 -0
  189. package/backend/dist/utils/pluginStreaming.js.map +1 -0
  190. package/backend/dist/utils/pluginUpload.d.ts +8 -0
  191. package/backend/dist/utils/pluginUpload.d.ts.map +1 -0
  192. package/backend/dist/utils/pluginUpload.js +51 -0
  193. package/backend/dist/utils/pluginUpload.js.map +1 -0
  194. package/backend/dist/utils/pluginValidation.d.ts +10 -0
  195. package/backend/dist/utils/pluginValidation.d.ts.map +1 -0
  196. package/backend/dist/utils/pluginValidation.js +94 -0
  197. package/backend/dist/utils/pluginValidation.js.map +1 -0
  198. package/backend/dist/utils/pluginVariableValidation.d.ts +10 -0
  199. package/backend/dist/utils/pluginVariableValidation.d.ts.map +1 -0
  200. package/backend/dist/utils/pluginVariableValidation.js +80 -0
  201. package/backend/dist/utils/pluginVariableValidation.js.map +1 -0
  202. package/backend/dist/utils/websocketMessages.d.ts +26 -0
  203. package/backend/dist/utils/websocketMessages.d.ts.map +1 -0
  204. package/backend/dist/utils/websocketMessages.js +74 -0
  205. package/backend/dist/utils/websocketMessages.js.map +1 -0
  206. package/backend/dist/websocketServer.d.ts +3 -0
  207. package/backend/dist/websocketServer.d.ts.map +1 -0
  208. package/backend/dist/websocketServer.js +332 -0
  209. package/backend/dist/websocketServer.js.map +1 -0
  210. package/backend/package.json +2 -2
  211. package/frontend/dist/assets/index-BxHDVIti.css +1 -0
  212. package/frontend/dist/assets/index-CDA2f43l.js +5 -0
  213. package/frontend/dist/assets/markdown-math-CYlHEDRF.css +1 -0
  214. package/frontend/dist/index.html +11 -10
  215. package/frontend/dist/js/ArtifactContainer-CA523BOS.js +1 -0
  216. package/frontend/dist/js/ArtifactDemoPage-C0p2-ccP.js +200 -0
  217. package/frontend/dist/js/ArtifactSlideOutPanel-BBYuYyr_.js +1 -0
  218. package/frontend/dist/js/Button-BmZQkAPD.js +1 -0
  219. package/frontend/dist/js/ChatPage-QSOhIynr.js +16 -0
  220. package/frontend/dist/js/FirstTimeSetup-pi-S4VU-.js +1 -0
  221. package/frontend/dist/js/GalleryPage-BPXB-D4f.js +1 -0
  222. package/frontend/dist/js/ModelManagerModals-D3SZV7CU.js +1 -0
  223. package/frontend/dist/js/ModelsPage-Fmqc1Zxb.js +2 -0
  224. package/frontend/dist/js/OptimizedSyntaxHighlighter-DlI4omE2.js +1 -0
  225. package/frontend/dist/js/PersonasPage-BuLOCG1Y.js +13 -0
  226. package/frontend/dist/js/RichMessageContent-Darvuw8F.js +3 -0
  227. package/frontend/dist/js/SettingsModal-CQhAeaia.js +1 -0
  228. package/frontend/dist/js/UserManagementPage-yX9vOase.js +1 -0
  229. package/frontend/dist/js/api-d05r9G59.js +15 -0
  230. package/frontend/dist/js/ar-BIL8U_dv.js +2 -0
  231. package/frontend/dist/js/artifactHtml-CRdkOCnz.js +27 -0
  232. package/frontend/dist/js/bn-BWQoWPn6.js +2 -0
  233. package/frontend/dist/js/cs-DOD1vscE.js +2 -0
  234. package/frontend/dist/js/da-DYPzSzX4.js +2 -0
  235. package/frontend/dist/js/de-CHd4b2Xf.js +2 -0
  236. package/frontend/dist/js/demoMode-BhEpMVh5.js +1 -0
  237. package/frontend/dist/js/es-BmDO6uNe.js +2 -0
  238. package/frontend/dist/js/fr-DW24sUV7.js +2 -0
  239. package/frontend/dist/js/hi-BRDq_y1B.js +2 -0
  240. package/frontend/dist/js/id-I_F2nstV.js +2 -0
  241. package/frontend/dist/js/is-Boj7-J12.js +2 -0
  242. package/frontend/dist/js/it-CNeHl9Fh.js +2 -0
  243. package/frontend/dist/js/ja-DrVG6G8s.js +2 -0
  244. package/frontend/dist/js/ko-BtKRwlHW.js +2 -0
  245. package/frontend/dist/js/logger-1THq8-96.js +1 -0
  246. package/frontend/dist/js/markdown-core-CEpaJrKd.js +16 -0
  247. package/frontend/dist/js/markdown-math-DAmpvM9y.js +261 -0
  248. package/frontend/dist/js/ms-Dp9MigoU.js +2 -0
  249. package/frontend/dist/js/nl-Bk9pGoCF.js +2 -0
  250. package/frontend/dist/js/pl-seE8_lLS.js +2 -0
  251. package/frontend/dist/js/pt-BAeBtldI.js +2 -0
  252. package/frontend/dist/js/{react-vendor-Dh6nfvM2.js → react-vendor-DtQrJICq.js} +1 -1
  253. package/frontend/dist/js/ru-CkCpWFNb.js +2 -0
  254. package/frontend/dist/js/sv-BM7nqxmA.js +2 -0
  255. package/frontend/dist/js/syntax-highlight-B758xPeo.js +8 -0
  256. package/frontend/dist/js/th-vDIZbpa4.js +2 -0
  257. package/frontend/dist/js/tr-G2-XodhW.js +2 -0
  258. package/frontend/dist/js/ui-vendor-MCs1MDmN.js +177 -0
  259. package/frontend/dist/js/uk-DfVfZcBQ.js +2 -0
  260. package/frontend/dist/js/useBaseQuery-CrdrLcmR.js +1 -0
  261. package/frontend/dist/js/useQuery-DaEnYfWR.js +1 -0
  262. package/frontend/dist/js/utils-es0n-47H.js +1 -0
  263. package/frontend/dist/js/utils-vendor-B2ZBL57Y.js +9 -0
  264. package/frontend/dist/js/vi-Yp9yjb06.js +2 -0
  265. package/frontend/dist/js/zh-DtRb97we.js +2 -0
  266. package/package.json +12 -8
  267. package/plugins/openclaw-agent.json +1 -1
  268. package/backend/dist/services/openclawSessionService.d.ts +0 -63
  269. package/backend/dist/services/openclawSessionService.d.ts.map +0 -1
  270. package/backend/dist/services/openclawSessionService.js +0 -307
  271. package/backend/dist/services/openclawSessionService.js.map +0 -1
  272. package/frontend/dist/assets/index-cOuIFv6G.js +0 -314
  273. package/frontend/dist/assets/index-tuVjvNle.css +0 -1
  274. package/frontend/dist/js/ArtifactContainer-DXa2t8GS.js +0 -24
  275. package/frontend/dist/js/ArtifactDemoPage-Dms7P2gI.js +0 -98
  276. package/frontend/dist/js/ChatPage-C6hOVIAS.js +0 -15
  277. package/frontend/dist/js/GalleryPage-CGKPECJV.js +0 -1
  278. package/frontend/dist/js/ModelsPage-CNB-c-JM.js +0 -1
  279. package/frontend/dist/js/PersonasPage-hDiS0Rvl.js +0 -13
  280. package/frontend/dist/js/UserManagementPage-1H9kcHeR.js +0 -1
  281. package/frontend/dist/js/api-MQ7bONaU.js +0 -3
  282. package/frontend/dist/js/appStore-BpzQfKOd.js +0 -2
  283. package/frontend/dist/js/authStore-Cn_WS6Kb.js +0 -2
  284. package/frontend/dist/js/demoMode-PKHbba1m.js +0 -1
  285. package/frontend/dist/js/markdown-vendor-DLzJCMRQ.js +0 -22
  286. package/frontend/dist/js/ui-vendor-ClcVY3m-.js +0 -177
  287. package/frontend/dist/js/utils-vendor-BCQRX-0Q.js +0 -9
@@ -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';
@@ -54,18 +53,14 @@ import imageGenRoutes from './routes/imageGen.js';
54
53
  import embeddingsRoutes from './routes/embeddings.js';
55
54
  import huggingfaceHubRoutes from './routes/huggingfaceHub.js';
56
55
  import ollamaService from './services/ollamaService.js';
57
- import chatService from './services/chatService.js';
58
56
  import { GitHubOAuthService } from './services/simpleGitHubOAuth.js';
59
57
  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
58
  import { loadAppPackage, resolveFrontendDist } from './utils/packagePaths.js';
59
+ import { registerWebSocketServer } from './websocketServer.js';
60
+ import { createLogger } from './utils/logger.js';
67
61
  const pkg = loadAppPackage(import.meta.url);
68
62
  const app = express();
63
+ const logger = createLogger('server');
69
64
  // Trust proxy setting for running behind reverse proxies (Nginx, Caddy, etc.)
70
65
  // Set TRUST_PROXY=1 or TRUST_PROXY=loopback or TRUST_PROXY=uniquelocal etc.
71
66
  const trustProxy = process.env.TRUST_PROXY;
@@ -83,6 +78,8 @@ if (trustProxy) {
83
78
  }
84
79
  const isProduction = process.env.NODE_ENV === 'production';
85
80
  const port = process.env.PORT || (isProduction ? 8080 : 3001);
81
+ const hasTurnstile = Boolean(process.env.TURNSTILE_SITE_KEY?.trim()) &&
82
+ Boolean(process.env.TURNSTILE_SECRET_KEY?.trim());
86
83
  const corsOrigins = process.env.CORS_ORIGIN?.split(',') || [
87
84
  'http://localhost:5173',
88
85
  'http://localhost:3000',
@@ -109,7 +106,7 @@ const corsConfig = {
109
106
  const allowNetworkAccess = process.env.NODE_ENV !== 'production' ||
110
107
  process.env.DOCKER_ENV === 'true';
111
108
  const isNetworkOrigin = origin &&
112
- /^https?:\/\/(?:192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|127\.|localhost)/.test(origin);
109
+ /^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
110
  if (allowNetworkAccess && isNetworkOrigin) {
114
111
  callback(null, true);
115
112
  }
@@ -122,7 +119,9 @@ const corsConfig = {
122
119
  // Security middleware
123
120
  app.use(helmet({
124
121
  // COEP - disable in Docker/development to avoid proxy issues
125
- crossOriginEmbedderPolicy: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
122
+ crossOriginEmbedderPolicy: process.env.NODE_ENV === 'production' &&
123
+ !process.env.DOCKER_ENV &&
124
+ !hasTurnstile
126
125
  ? true
127
126
  : false,
128
127
  // Content Security Policy - Docker-aware configuration
@@ -131,6 +130,7 @@ app.use(helmet({
131
130
  defaultSrc: ["'self'"],
132
131
  scriptSrc: [
133
132
  "'self'",
133
+ 'https://challenges.cloudflare.com',
134
134
  ...(process.env.NODE_ENV === 'production'
135
135
  ? [] // Strict in production
136
136
  : ["'unsafe-inline'", "'unsafe-eval'"]), // Allow for dev tools
@@ -164,6 +164,7 @@ app.use(helmet({
164
164
  fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
165
165
  mediaSrc: ["'self'", 'data:', 'blob:'],
166
166
  objectSrc: ["'none'"],
167
+ frameSrc: ["'self'", 'https://challenges.cloudflare.com'],
167
168
  frameAncestors: ["'self'"],
168
169
  formAction: ["'self'"],
169
170
  upgradeInsecureRequests: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
@@ -334,7 +335,7 @@ if (process.env.NODE_ENV === 'production' ||
334
335
  const pathModule = await import('path');
335
336
  const frontendPath = resolveFrontendDist(import.meta.url);
336
337
  if (frontendPath) {
337
- console.log(`Serving frontend from: ${frontendPath}`);
338
+ logger.info(`Serving frontend from: ${frontendPath}`);
338
339
  // Rate limiter for static files
339
340
  const staticRateLimiter = rateLimit({
340
341
  windowMs: 15 * 60 * 1000, // 15 minutes
@@ -359,7 +360,7 @@ if (process.env.NODE_ENV === 'production' ||
359
360
  });
360
361
  }
361
362
  else {
362
- console.warn('Frontend build not found. Run `npm run build:frontend` first.');
363
+ logger.warn('Frontend build not found. Run `npm run build:frontend` first.');
363
364
  }
364
365
  }
365
366
  // Error handling
@@ -367,895 +368,12 @@ app.use(notFoundHandler);
367
368
  app.use(errorHandler);
368
369
  // Create HTTP server
369
370
  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
- });
371
+ registerWebSocketServer(server);
1254
372
  // Start server
1255
373
  server.listen({ port, host: '0.0.0.0' }, () => {
1256
374
  const url = `http://localhost:${port}`;
1257
- console.log(`\nšŸš€ Libre WebUI v${pkg.version}`);
1258
- console.log(` ${url}\n`);
375
+ logger.info(`Libre WebUI v${pkg.version}`);
376
+ logger.info(url);
1259
377
  // Open browser in production mode
1260
378
  if (process.env.SERVE_FRONTEND === 'true') {
1261
379
  import('child_process').then(({ exec }) => {
@@ -1273,38 +391,38 @@ server.listen({ port, host: '0.0.0.0' }, () => {
1273
391
  const githubConfigured = githubOAuth.isConfigured();
1274
392
  const hfConfigured = hfOAuth.isConfigured();
1275
393
  if (githubConfigured || hfConfigured) {
1276
- console.log('šŸ” SSO Configuration:');
394
+ logger.info('SSO configuration:');
1277
395
  if (githubConfigured) {
1278
- console.log(' āœ… GitHub OAuth configured and ready');
396
+ logger.info('GitHub OAuth configured and ready');
1279
397
  }
1280
398
  if (hfConfigured) {
1281
- console.log(' āœ… Hugging Face OAuth configured and ready');
399
+ logger.info('Hugging Face OAuth configured and ready');
1282
400
  }
1283
401
  }
1284
402
  else {
1285
- console.log('ā„¹ļø No SSO providers configured (optional)');
403
+ logger.info('No SSO providers configured (optional)');
1286
404
  }
1287
405
  // Check Ollama connection on startup
1288
406
  ollamaService.isHealthy().then(isHealthy => {
1289
407
  if (isHealthy) {
1290
- console.log('āœ… Ollama service is connected and ready');
408
+ logger.info('Ollama service is connected and ready');
1291
409
  }
1292
410
  else {
1293
- console.log("āš ļø Ollama service is not available - make sure it's running on http://localhost:11434");
411
+ logger.warn("Ollama service is not available - make sure it's running on http://localhost:11434");
1294
412
  }
1295
413
  });
1296
414
  });
1297
415
  // Graceful shutdown
1298
416
  process.on('SIGTERM', () => {
1299
- console.log('SIGTERM signal received: closing HTTP server');
417
+ logger.info('SIGTERM signal received: closing HTTP server');
1300
418
  server.close(() => {
1301
- console.log('HTTP server closed');
419
+ logger.info('HTTP server closed');
1302
420
  });
1303
421
  });
1304
422
  process.on('SIGINT', () => {
1305
- console.log('SIGINT signal received: closing HTTP server');
423
+ logger.info('SIGINT signal received: closing HTTP server');
1306
424
  server.close(() => {
1307
- console.log('HTTP server closed');
425
+ logger.info('HTTP server closed');
1308
426
  });
1309
427
  });
1310
428
  export default app;