libre-webui 0.2.4
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.
- package/LICENSE +201 -0
- package/README.md +204 -0
- package/backend/dist/db.d.ts +19 -0
- package/backend/dist/db.d.ts.map +1 -0
- package/backend/dist/db.js +355 -0
- package/backend/dist/db.js.map +1 -0
- package/backend/dist/env.d.ts +2 -0
- package/backend/dist/env.d.ts.map +1 -0
- package/backend/dist/env.js +22 -0
- package/backend/dist/env.js.map +1 -0
- package/backend/dist/index.d.ts +4 -0
- package/backend/dist/index.d.ts.map +1 -0
- package/backend/dist/index.js +751 -0
- package/backend/dist/index.js.map +1 -0
- package/backend/dist/middleware/auth.d.ts +18 -0
- package/backend/dist/middleware/auth.d.ts.map +1 -0
- package/backend/dist/middleware/auth.js +98 -0
- package/backend/dist/middleware/auth.js.map +1 -0
- package/backend/dist/middleware/index.d.ts +5 -0
- package/backend/dist/middleware/index.d.ts.map +1 -0
- package/backend/dist/middleware/index.js +62 -0
- package/backend/dist/middleware/index.js.map +1 -0
- package/backend/dist/models/personaModel.d.ts +37 -0
- package/backend/dist/models/personaModel.d.ts.map +1 -0
- package/backend/dist/models/personaModel.js +269 -0
- package/backend/dist/models/personaModel.js.map +1 -0
- package/backend/dist/models/userModel.d.ts +86 -0
- package/backend/dist/models/userModel.d.ts.map +1 -0
- package/backend/dist/models/userModel.js +212 -0
- package/backend/dist/models/userModel.js.map +1 -0
- package/backend/dist/routes/auth.d.ts +3 -0
- package/backend/dist/routes/auth.d.ts.map +1 -0
- package/backend/dist/routes/auth.js +389 -0
- package/backend/dist/routes/auth.js.map +1 -0
- package/backend/dist/routes/chat.d.ts +3 -0
- package/backend/dist/routes/chat.d.ts.map +1 -0
- package/backend/dist/routes/chat.js +767 -0
- package/backend/dist/routes/chat.js.map +1 -0
- package/backend/dist/routes/documents.d.ts +3 -0
- package/backend/dist/routes/documents.d.ts.map +1 -0
- package/backend/dist/routes/documents.js +244 -0
- package/backend/dist/routes/documents.js.map +1 -0
- package/backend/dist/routes/ollama.d.ts +3 -0
- package/backend/dist/routes/ollama.d.ts.map +1 -0
- package/backend/dist/routes/ollama.js +549 -0
- package/backend/dist/routes/ollama.js.map +1 -0
- package/backend/dist/routes/personas.d.ts +3 -0
- package/backend/dist/routes/personas.d.ts.map +1 -0
- package/backend/dist/routes/personas.js +505 -0
- package/backend/dist/routes/personas.js.map +1 -0
- package/backend/dist/routes/plugins.d.ts +3 -0
- package/backend/dist/routes/plugins.d.ts.map +1 -0
- package/backend/dist/routes/plugins.js +417 -0
- package/backend/dist/routes/plugins.js.map +1 -0
- package/backend/dist/routes/preferences.d.ts +3 -0
- package/backend/dist/routes/preferences.d.ts.map +1 -0
- package/backend/dist/routes/preferences.js +303 -0
- package/backend/dist/routes/preferences.js.map +1 -0
- package/backend/dist/routes/tts.d.ts +3 -0
- package/backend/dist/routes/tts.d.ts.map +1 -0
- package/backend/dist/routes/tts.js +304 -0
- package/backend/dist/routes/tts.js.map +1 -0
- package/backend/dist/routes/users.d.ts +3 -0
- package/backend/dist/routes/users.d.ts.map +1 -0
- package/backend/dist/routes/users.js +246 -0
- package/backend/dist/routes/users.js.map +1 -0
- package/backend/dist/services/authService.d.ts +51 -0
- package/backend/dist/services/authService.d.ts.map +1 -0
- package/backend/dist/services/authService.js +153 -0
- package/backend/dist/services/authService.js.map +1 -0
- package/backend/dist/services/chatService.d.ts +52 -0
- package/backend/dist/services/chatService.d.ts.map +1 -0
- package/backend/dist/services/chatService.js +645 -0
- package/backend/dist/services/chatService.js.map +1 -0
- package/backend/dist/services/documentService.d.ts +34 -0
- package/backend/dist/services/documentService.d.ts.map +1 -0
- package/backend/dist/services/documentService.js +428 -0
- package/backend/dist/services/documentService.js.map +1 -0
- package/backend/dist/services/encryptionService.d.ts +62 -0
- package/backend/dist/services/encryptionService.d.ts.map +1 -0
- package/backend/dist/services/encryptionService.js +284 -0
- package/backend/dist/services/encryptionService.js.map +1 -0
- package/backend/dist/services/memoryService.d.ts +140 -0
- package/backend/dist/services/memoryService.d.ts.map +1 -0
- package/backend/dist/services/memoryService.js +867 -0
- package/backend/dist/services/memoryService.js.map +1 -0
- package/backend/dist/services/mutationEngineService.d.ts +49 -0
- package/backend/dist/services/mutationEngineService.d.ts.map +1 -0
- package/backend/dist/services/mutationEngineService.js +432 -0
- package/backend/dist/services/mutationEngineService.js.map +1 -0
- package/backend/dist/services/ollamaService.d.ts +55 -0
- package/backend/dist/services/ollamaService.d.ts.map +1 -0
- package/backend/dist/services/ollamaService.js +450 -0
- package/backend/dist/services/ollamaService.js.map +1 -0
- package/backend/dist/services/personaService.d.ts +67 -0
- package/backend/dist/services/personaService.d.ts.map +1 -0
- package/backend/dist/services/personaService.js +373 -0
- package/backend/dist/services/personaService.js.map +1 -0
- package/backend/dist/services/pluginService.d.ts +42 -0
- package/backend/dist/services/pluginService.d.ts.map +1 -0
- package/backend/dist/services/pluginService.js +961 -0
- package/backend/dist/services/pluginService.js.map +1 -0
- package/backend/dist/services/preferencesService.d.ts +35 -0
- package/backend/dist/services/preferencesService.d.ts.map +1 -0
- package/backend/dist/services/preferencesService.js +255 -0
- package/backend/dist/services/preferencesService.js.map +1 -0
- package/backend/dist/services/simpleGitHubOAuth.d.ts +48 -0
- package/backend/dist/services/simpleGitHubOAuth.d.ts.map +1 -0
- package/backend/dist/services/simpleGitHubOAuth.js +203 -0
- package/backend/dist/services/simpleGitHubOAuth.js.map +1 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.d.ts +43 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.d.ts.map +1 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.js +159 -0
- package/backend/dist/services/simpleHuggingFaceOAuth.js.map +1 -0
- package/backend/dist/services/userService.d.ts +1 -0
- package/backend/dist/services/userService.d.ts.map +1 -0
- package/backend/dist/services/userService.js +18 -0
- package/backend/dist/services/userService.js.map +1 -0
- package/backend/dist/storage.d.ts +55 -0
- package/backend/dist/storage.d.ts.map +1 -0
- package/backend/dist/storage.js +741 -0
- package/backend/dist/storage.js.map +1 -0
- package/backend/dist/test-encryption.d.ts +2 -0
- package/backend/dist/test-encryption.d.ts.map +1 -0
- package/backend/dist/test-encryption.js +64 -0
- package/backend/dist/test-encryption.js.map +1 -0
- package/backend/dist/types/index.d.ts +523 -0
- package/backend/dist/types/index.d.ts.map +1 -0
- package/backend/dist/types/index.js +31 -0
- package/backend/dist/types/index.js.map +1 -0
- package/backend/dist/utils/generationUtils.d.ts +10 -0
- package/backend/dist/utils/generationUtils.d.ts.map +1 -0
- package/backend/dist/utils/generationUtils.js +49 -0
- package/backend/dist/utils/generationUtils.js.map +1 -0
- package/backend/dist/utils/hash.d.ts +29 -0
- package/backend/dist/utils/hash.d.ts.map +1 -0
- package/backend/dist/utils/hash.js +73 -0
- package/backend/dist/utils/hash.js.map +1 -0
- package/backend/dist/utils/jwt.d.ts +37 -0
- package/backend/dist/utils/jwt.d.ts.map +1 -0
- package/backend/dist/utils/jwt.js +86 -0
- package/backend/dist/utils/jwt.js.map +1 -0
- package/bin/cli.js +150 -0
- package/electron/main.js +322 -0
- package/frontend/dist/_redirects +1 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-CRQkB7Wz.js +3 -0
- package/frontend/dist/css/index-B1OjddR-.css +1 -0
- package/frontend/dist/favicon-dark.png +0 -0
- package/frontend/dist/favicon-light.png +0 -0
- package/frontend/dist/index.html +23 -0
- package/frontend/dist/js/ArtifactContainer-c_oi7XMs.js +23 -0
- package/frontend/dist/js/ArtifactDemoPage-CdfwJVXu.js +98 -0
- package/frontend/dist/js/ChatPage-CyotkmS0.js +281 -0
- package/frontend/dist/js/ModelsPage-DNaziPHc.js +2 -0
- package/frontend/dist/js/PersonasPage-DcnbJf8Q.js +13 -0
- package/frontend/dist/js/UserManagementPage-DtTf92dS.js +1 -0
- package/frontend/dist/js/markdown-vendor-D-79K2xZ.js +22 -0
- package/frontend/dist/js/react-vendor-N--QU9DW.js +8 -0
- package/frontend/dist/js/router-vendor-B-t91v39.js +3 -0
- package/frontend/dist/js/ui-vendor-VxSCY_bv.js +177 -0
- package/frontend/dist/js/utils-vendor-DNzxLBGx.js +6 -0
- package/frontend/dist/logo-dark.png +0 -0
- package/frontend/dist/logo-light.png +0 -0
- package/frontend/dist/logo.svg +14 -0
- package/package.json +128 -0
- package/plugins/anthropic.json +25 -0
- package/plugins/elevenlabs.json +58 -0
- package/plugins/gemini.json +57 -0
- package/plugins/github.json +23 -0
- package/plugins/groq.json +25 -0
- package/plugins/mistral.json +73 -0
- package/plugins/openai-tts.json +38 -0
- package/plugins/openai.json +132 -0
- package/plugins/openrouter.json +353 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Libre WebUI
|
|
3
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
// Load environment variables FIRST before any other imports
|
|
18
|
+
import './env.js';
|
|
19
|
+
/*
|
|
20
|
+
* Libre WebUI
|
|
21
|
+
* Copyright (C) 2025 Kroonen AI, Inc.
|
|
22
|
+
*
|
|
23
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
24
|
+
* you may not use this file except in compliance with the License.
|
|
25
|
+
* You may obtain a copy of the License at:
|
|
26
|
+
*
|
|
27
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
28
|
+
*
|
|
29
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
30
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
31
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
32
|
+
* See the License for the specific language governing permissions and
|
|
33
|
+
* limitations under the License.
|
|
34
|
+
*/
|
|
35
|
+
import express from 'express';
|
|
36
|
+
import rateLimit from 'express-rate-limit';
|
|
37
|
+
import cors from 'cors';
|
|
38
|
+
import helmet from 'helmet';
|
|
39
|
+
import morgan from 'morgan';
|
|
40
|
+
import { createServer } from 'http';
|
|
41
|
+
import { WebSocketServer } from 'ws';
|
|
42
|
+
import { errorHandler, notFoundHandler, requestLogger, } from './middleware/index.js';
|
|
43
|
+
import { optionalAuth } from './middleware/auth.js';
|
|
44
|
+
import ollamaRoutes from './routes/ollama.js';
|
|
45
|
+
import chatRoutes from './routes/chat.js';
|
|
46
|
+
import preferencesRoutes from './routes/preferences.js';
|
|
47
|
+
import pluginRoutes from './routes/plugins.js';
|
|
48
|
+
import documentRoutes from './routes/documents.js';
|
|
49
|
+
import authRoutes from './routes/auth.js';
|
|
50
|
+
import usersRoutes from './routes/users.js';
|
|
51
|
+
import personaRoutes from './routes/personas.js';
|
|
52
|
+
import ttsRoutes from './routes/tts.js';
|
|
53
|
+
import ollamaService from './services/ollamaService.js';
|
|
54
|
+
import chatService from './services/chatService.js';
|
|
55
|
+
import { GitHubOAuthService } from './services/simpleGitHubOAuth.js';
|
|
56
|
+
import { HuggingFaceOAuthService } from './services/simpleHuggingFaceOAuth.js';
|
|
57
|
+
import pluginService from './services/pluginService.js';
|
|
58
|
+
import preferencesService from './services/preferencesService.js';
|
|
59
|
+
import documentService from './services/documentService.js';
|
|
60
|
+
import { mergeGenerationOptions } from './utils/generationUtils.js';
|
|
61
|
+
import { verifyToken } from './utils/jwt.js';
|
|
62
|
+
const app = express();
|
|
63
|
+
const port = process.env.PORT || 3001;
|
|
64
|
+
const corsOrigins = process.env.CORS_ORIGIN?.split(',') || [
|
|
65
|
+
'http://localhost:5173',
|
|
66
|
+
'http://localhost:3000',
|
|
67
|
+
'http://localhost:8080',
|
|
68
|
+
];
|
|
69
|
+
// Multi-user safe CORS configuration
|
|
70
|
+
const corsConfig = {
|
|
71
|
+
origin: (origin, callback) => {
|
|
72
|
+
// Allow requests with no origin (mobile apps, etc.)
|
|
73
|
+
if (!origin)
|
|
74
|
+
return callback(null, true);
|
|
75
|
+
// Check if the origin is in our allowed list
|
|
76
|
+
if (corsOrigins.indexOf(origin) !== -1) {
|
|
77
|
+
callback(null, true);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
// In development mode, allow network access (when --host is used)
|
|
81
|
+
// This allows access from network IPs like http://192.168.x.x:8080 or http://10.x.x.x:8080
|
|
82
|
+
const isDevelopment = process.env.NODE_ENV !== 'production';
|
|
83
|
+
const isNetworkOrigin = origin &&
|
|
84
|
+
/^https?:\/\/(?:192\.168\.|10\.|172\.(?:1[6-9]|2\d|3[01])\.|127\.|localhost)/.test(origin);
|
|
85
|
+
if (isDevelopment && isNetworkOrigin) {
|
|
86
|
+
callback(null, true);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
callback(new Error('Not allowed by CORS'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
// Security middleware
|
|
95
|
+
app.use(helmet({
|
|
96
|
+
// COEP - disable in Docker/development to avoid proxy issues
|
|
97
|
+
crossOriginEmbedderPolicy: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
|
|
98
|
+
? true
|
|
99
|
+
: false,
|
|
100
|
+
// Content Security Policy - Docker-aware configuration
|
|
101
|
+
contentSecurityPolicy: {
|
|
102
|
+
directives: {
|
|
103
|
+
defaultSrc: ["'self'"],
|
|
104
|
+
scriptSrc: [
|
|
105
|
+
"'self'",
|
|
106
|
+
...(process.env.NODE_ENV === 'production'
|
|
107
|
+
? [] // Strict in production
|
|
108
|
+
: ["'unsafe-inline'", "'unsafe-eval'"]), // Allow for dev tools
|
|
109
|
+
],
|
|
110
|
+
styleSrc: [
|
|
111
|
+
"'self'",
|
|
112
|
+
"'unsafe-inline'", // Required for styled-components and CSS-in-JS
|
|
113
|
+
'https://fonts.googleapis.com',
|
|
114
|
+
],
|
|
115
|
+
imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
|
|
116
|
+
connectSrc: [
|
|
117
|
+
"'self'",
|
|
118
|
+
'ws:',
|
|
119
|
+
'wss:',
|
|
120
|
+
'https:',
|
|
121
|
+
'http:',
|
|
122
|
+
// WebSocket connections - flexible for Docker networking
|
|
123
|
+
`ws://localhost:${port}`,
|
|
124
|
+
`wss://localhost:${port}`,
|
|
125
|
+
'ws://libre-webui:3001',
|
|
126
|
+
'wss://libre-webui:3001',
|
|
127
|
+
...(process.env.NODE_ENV !== 'production'
|
|
128
|
+
? [
|
|
129
|
+
'http://localhost:*',
|
|
130
|
+
'ws://localhost:*',
|
|
131
|
+
'http://libre-webui:*',
|
|
132
|
+
'ws://libre-webui:*',
|
|
133
|
+
]
|
|
134
|
+
: []),
|
|
135
|
+
],
|
|
136
|
+
fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
|
|
137
|
+
objectSrc: ["'none'"],
|
|
138
|
+
frameAncestors: ["'self'"],
|
|
139
|
+
formAction: ["'self'"],
|
|
140
|
+
upgradeInsecureRequests: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
|
|
141
|
+
? []
|
|
142
|
+
: null,
|
|
143
|
+
baseUri: ["'self'"],
|
|
144
|
+
manifestSrc: ["'self'"],
|
|
145
|
+
workerSrc: ["'self'", 'blob:'],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
// HSTS - disabled in Docker to avoid reverse proxy conflicts
|
|
149
|
+
hsts: process.env.NODE_ENV === 'production' && !process.env.DOCKER_ENV
|
|
150
|
+
? {
|
|
151
|
+
maxAge: 31536000, // 1 year
|
|
152
|
+
includeSubDomains: true,
|
|
153
|
+
preload: true,
|
|
154
|
+
}
|
|
155
|
+
: false, // Disabled in Docker/development
|
|
156
|
+
// Prevent clickjacking
|
|
157
|
+
frameguard: { action: 'deny' },
|
|
158
|
+
// Prevent MIME type sniffing
|
|
159
|
+
noSniff: true,
|
|
160
|
+
// Hide X-Powered-By header
|
|
161
|
+
hidePoweredBy: true,
|
|
162
|
+
// Prevent XSS attacks
|
|
163
|
+
xssFilter: true,
|
|
164
|
+
}));
|
|
165
|
+
// CORS configuration
|
|
166
|
+
app.use(cors({
|
|
167
|
+
...corsConfig,
|
|
168
|
+
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
|
|
169
|
+
allowedHeaders: ['Content-Type', 'Authorization'],
|
|
170
|
+
credentials: true,
|
|
171
|
+
}));
|
|
172
|
+
// Logging
|
|
173
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
174
|
+
app.use(morgan('combined'));
|
|
175
|
+
}
|
|
176
|
+
app.use(requestLogger);
|
|
177
|
+
// Body parsing
|
|
178
|
+
app.use(express.json({ limit: '10mb' }));
|
|
179
|
+
app.use(express.urlencoded({ extended: true }));
|
|
180
|
+
// Health check endpoint
|
|
181
|
+
app.get('/health', (req, res) => {
|
|
182
|
+
res.json({
|
|
183
|
+
success: true,
|
|
184
|
+
message: 'Libre WebUI Backend is running',
|
|
185
|
+
timestamp: new Date().toISOString(),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
// Static files are served by a separate frontend server on port 8080
|
|
189
|
+
// Backend only serves API endpoints
|
|
190
|
+
// Rate limiter for the /api/personas route
|
|
191
|
+
const personasRateLimiter = rateLimit({
|
|
192
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
193
|
+
max: 100, // limit each IP to 100 requests per windowMs
|
|
194
|
+
message: {
|
|
195
|
+
success: false,
|
|
196
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
197
|
+
},
|
|
198
|
+
standardHeaders: true,
|
|
199
|
+
legacyHeaders: false,
|
|
200
|
+
});
|
|
201
|
+
// Rate limiter for the /api/preferences route
|
|
202
|
+
const preferencesRateLimiter = rateLimit({
|
|
203
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
204
|
+
max: 100, // limit each IP to 100 requests per windowMs
|
|
205
|
+
message: {
|
|
206
|
+
success: false,
|
|
207
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
208
|
+
},
|
|
209
|
+
standardHeaders: true,
|
|
210
|
+
legacyHeaders: false,
|
|
211
|
+
});
|
|
212
|
+
// Rate limiter for the /api/ollama route
|
|
213
|
+
const ollamaRateLimiter = rateLimit({
|
|
214
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
215
|
+
max: 10000, // limit each IP to 10000 requests per windowMs (very high limit for streaming chunks)
|
|
216
|
+
message: {
|
|
217
|
+
success: false,
|
|
218
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
219
|
+
},
|
|
220
|
+
standardHeaders: true,
|
|
221
|
+
legacyHeaders: false,
|
|
222
|
+
});
|
|
223
|
+
// Rate limiter for the /api/documents route
|
|
224
|
+
const documentsRateLimiter = rateLimit({
|
|
225
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
226
|
+
max: 100, // limit each IP to 100 requests per windowMs
|
|
227
|
+
message: {
|
|
228
|
+
success: false,
|
|
229
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
230
|
+
},
|
|
231
|
+
standardHeaders: true,
|
|
232
|
+
legacyHeaders: false,
|
|
233
|
+
});
|
|
234
|
+
// Rate limiter for the /api/auth route (general limit, specific limits applied within route)
|
|
235
|
+
const authRateLimiter = rateLimit({
|
|
236
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
237
|
+
max: 100, // limit each IP to 100 requests per windowMs (higher level limit)
|
|
238
|
+
message: {
|
|
239
|
+
success: false,
|
|
240
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
241
|
+
},
|
|
242
|
+
standardHeaders: true,
|
|
243
|
+
legacyHeaders: false,
|
|
244
|
+
});
|
|
245
|
+
// Rate limiter for the /api/users route (general limit, specific limits applied within route)
|
|
246
|
+
const usersRateLimiter = rateLimit({
|
|
247
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
248
|
+
max: 50, // limit each IP to 50 requests per windowMs (moderate limit for user management)
|
|
249
|
+
message: {
|
|
250
|
+
success: false,
|
|
251
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
252
|
+
},
|
|
253
|
+
standardHeaders: true,
|
|
254
|
+
legacyHeaders: false,
|
|
255
|
+
});
|
|
256
|
+
// Rate limiter for the /api/chat route (general limit, specific limits applied within route)
|
|
257
|
+
const chatRateLimiter = rateLimit({
|
|
258
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
259
|
+
max: 1000, // limit each IP to 1000 requests per windowMs (high limit for chat interactions)
|
|
260
|
+
message: {
|
|
261
|
+
success: false,
|
|
262
|
+
error: 'Too many requests from this IP, please try again later.',
|
|
263
|
+
},
|
|
264
|
+
standardHeaders: true,
|
|
265
|
+
legacyHeaders: false,
|
|
266
|
+
});
|
|
267
|
+
// Rate limiter for TTS routes (higher limit for info endpoints, generation has stricter limits in route)
|
|
268
|
+
const ttsRateLimiter = rateLimit({
|
|
269
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
270
|
+
max: 500, // limit each IP to 500 requests per windowMs
|
|
271
|
+
message: {
|
|
272
|
+
success: false,
|
|
273
|
+
error: 'Too many TTS requests from this IP, please try again later.',
|
|
274
|
+
},
|
|
275
|
+
standardHeaders: true,
|
|
276
|
+
legacyHeaders: false,
|
|
277
|
+
});
|
|
278
|
+
// API routes
|
|
279
|
+
app.use('/api/auth', authRateLimiter, optionalAuth, authRoutes);
|
|
280
|
+
app.use('/api/users', usersRateLimiter, optionalAuth, usersRoutes);
|
|
281
|
+
app.use('/api/ollama', ollamaRateLimiter, ollamaRoutes);
|
|
282
|
+
app.use('/api/chat', chatRateLimiter, optionalAuth, chatRoutes);
|
|
283
|
+
app.use('/api/preferences', preferencesRateLimiter, optionalAuth, preferencesRoutes);
|
|
284
|
+
app.use('/api/plugins', pluginRoutes);
|
|
285
|
+
app.use('/api/documents', documentsRateLimiter, documentRoutes);
|
|
286
|
+
app.use('/api/personas', personasRateLimiter, optionalAuth, personaRoutes);
|
|
287
|
+
app.use('/api/tts', ttsRateLimiter, optionalAuth, ttsRoutes);
|
|
288
|
+
// Serve frontend static files in production (for npx libre-webui)
|
|
289
|
+
if (process.env.NODE_ENV === 'production' ||
|
|
290
|
+
process.env.SERVE_FRONTEND === 'true') {
|
|
291
|
+
const pathModule = await import('path');
|
|
292
|
+
const urlModule = await import('url');
|
|
293
|
+
const fsModule = await import('fs');
|
|
294
|
+
const __filename = urlModule.fileURLToPath(import.meta.url);
|
|
295
|
+
const __dirname = pathModule.dirname(__filename);
|
|
296
|
+
// Try multiple possible frontend locations
|
|
297
|
+
const possiblePaths = [
|
|
298
|
+
pathModule.join(__dirname, '../../frontend/dist'), // npm package structure
|
|
299
|
+
pathModule.join(__dirname, '../../../frontend/dist'), // development
|
|
300
|
+
pathModule.join(process.cwd(), 'frontend/dist'), // running from project root
|
|
301
|
+
];
|
|
302
|
+
let frontendPath = '';
|
|
303
|
+
for (const p of possiblePaths) {
|
|
304
|
+
if (fsModule.existsSync(pathModule.join(p, 'index.html'))) {
|
|
305
|
+
frontendPath = p;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (frontendPath) {
|
|
310
|
+
console.log(`Serving frontend from: ${frontendPath}`);
|
|
311
|
+
// Rate limiter for static files
|
|
312
|
+
const staticRateLimiter = rateLimit({
|
|
313
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
314
|
+
max: 1000, // limit each IP to 1000 requests per windowMs
|
|
315
|
+
message: 'Too many requests, please try again later.',
|
|
316
|
+
standardHeaders: true,
|
|
317
|
+
legacyHeaders: false,
|
|
318
|
+
});
|
|
319
|
+
app.use(staticRateLimiter, express.static(frontendPath));
|
|
320
|
+
// SPA fallback - serve index.html for all non-API routes
|
|
321
|
+
const indexPath = pathModule.join(frontendPath, 'index.html');
|
|
322
|
+
// Root route
|
|
323
|
+
app.get('/', staticRateLimiter, (_req, res) => {
|
|
324
|
+
res.sendFile(indexPath);
|
|
325
|
+
});
|
|
326
|
+
// All other non-API routes (Express 5 wildcard syntax)
|
|
327
|
+
app.get('/{*splat}', staticRateLimiter, (req, res, next) => {
|
|
328
|
+
if (req.path.startsWith('/api/') || req.path.startsWith('/ws')) {
|
|
329
|
+
return next();
|
|
330
|
+
}
|
|
331
|
+
res.sendFile(indexPath);
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.warn('Frontend build not found. Run `npm run build:frontend` first.');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Error handling
|
|
339
|
+
app.use(notFoundHandler);
|
|
340
|
+
app.use(errorHandler);
|
|
341
|
+
// Create HTTP server
|
|
342
|
+
const server = createServer(app);
|
|
343
|
+
// WebSocket server for real-time chat streaming
|
|
344
|
+
const wss = new WebSocketServer({
|
|
345
|
+
server,
|
|
346
|
+
path: '/ws',
|
|
347
|
+
});
|
|
348
|
+
wss.on('connection', (ws, req) => {
|
|
349
|
+
console.log('WebSocket client connected');
|
|
350
|
+
// Extract and verify auth token from query parameters
|
|
351
|
+
let userId = 'default';
|
|
352
|
+
try {
|
|
353
|
+
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
|
354
|
+
const token = url.searchParams.get('token');
|
|
355
|
+
if (token) {
|
|
356
|
+
// Verify JWT token using the same logic as the auth middleware
|
|
357
|
+
const decoded = verifyToken(token);
|
|
358
|
+
userId = decoded.userId;
|
|
359
|
+
console.log('WebSocket authenticated for user:', userId);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
console.log('WebSocket connection without auth token, using default user');
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch (error) {
|
|
366
|
+
console.error('WebSocket auth error:', error);
|
|
367
|
+
// Continue with default user for backward compatibility
|
|
368
|
+
}
|
|
369
|
+
ws.on('message', async (data) => {
|
|
370
|
+
try {
|
|
371
|
+
const message = JSON.parse(data.toString());
|
|
372
|
+
if (message.type === 'chat_stream') {
|
|
373
|
+
const { sessionId, content, images, format, options, assistantMessageId, regenerate, originalMessageId, } = message.data;
|
|
374
|
+
console.log('Backend: Received chat_stream for session:', sessionId, 'with images:', !!images, 'format:', !!format, 'regenerate:', !!regenerate, 'originalMessageId:', originalMessageId);
|
|
375
|
+
// Get session with user authentication
|
|
376
|
+
const session = chatService.getSession(sessionId, userId);
|
|
377
|
+
if (!session) {
|
|
378
|
+
console.log('Backend: Session not found:', sessionId, 'for user:', userId);
|
|
379
|
+
ws.send(JSON.stringify({
|
|
380
|
+
type: 'error',
|
|
381
|
+
data: {
|
|
382
|
+
error: 'Session not found',
|
|
383
|
+
code: 'SESSION_NOT_FOUND',
|
|
384
|
+
message: 'The requested session does not exist or does not belong to the current user. Please create a new session.',
|
|
385
|
+
sessionId: sessionId,
|
|
386
|
+
userId: userId,
|
|
387
|
+
},
|
|
388
|
+
}));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// Add user message with images if provided (skip for regenerations)
|
|
392
|
+
let userMessage;
|
|
393
|
+
if (!regenerate) {
|
|
394
|
+
userMessage = chatService.addMessage(sessionId, {
|
|
395
|
+
role: 'user',
|
|
396
|
+
content,
|
|
397
|
+
images: images || undefined,
|
|
398
|
+
}, userId);
|
|
399
|
+
if (!userMessage) {
|
|
400
|
+
ws.send(JSON.stringify({
|
|
401
|
+
type: 'error',
|
|
402
|
+
data: { error: 'Failed to add user message' },
|
|
403
|
+
}));
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// Send user message confirmation
|
|
407
|
+
ws.send(JSON.stringify({
|
|
408
|
+
type: 'user_message',
|
|
409
|
+
data: userMessage,
|
|
410
|
+
}));
|
|
411
|
+
}
|
|
412
|
+
// RAG: Get relevant document context for the user's query
|
|
413
|
+
const relevantContext = await documentService.getRelevantContext(content, sessionId);
|
|
414
|
+
let enhancedContent = content;
|
|
415
|
+
if (relevantContext.length > 0) {
|
|
416
|
+
console.log(`Found ${relevantContext.length} relevant document chunks for query`);
|
|
417
|
+
// Inject document context into the user message
|
|
418
|
+
const contextString = relevantContext.join('\n\n---\n\n');
|
|
419
|
+
enhancedContent = `Context from uploaded documents:\n\n${contextString}\n\n---\n\nUser question: ${content}`;
|
|
420
|
+
// Update the user message with enhanced content that includes document context
|
|
421
|
+
// We'll create a new message with the enhanced content for the AI model
|
|
422
|
+
console.log('Enhanced user message with document context');
|
|
423
|
+
}
|
|
424
|
+
// Use the modern chat completion API instead of legacy generate API
|
|
425
|
+
// This supports multimodal input and structured outputs
|
|
426
|
+
const contextMessages = chatService.getMessagesForContext(sessionId);
|
|
427
|
+
// Convert our messages to Ollama format
|
|
428
|
+
const ollamaMessages = contextMessages.map((msg, index) => {
|
|
429
|
+
const ollamaMessage = {
|
|
430
|
+
role: msg.role,
|
|
431
|
+
content: msg.content,
|
|
432
|
+
};
|
|
433
|
+
// Debug: Log what we're sending to Ollama
|
|
434
|
+
if (msg.role === 'system') {
|
|
435
|
+
console.log(`đ [DEBUG] Sending to Ollama - System message: "${msg.content.substring(0, 150)}${msg.content.length > 150 ? '...' : ''}"`);
|
|
436
|
+
}
|
|
437
|
+
// Use enhanced content for the last user message if we have document context
|
|
438
|
+
if (msg.role === 'user' &&
|
|
439
|
+
index === contextMessages.length - 1 &&
|
|
440
|
+
relevantContext.length > 0) {
|
|
441
|
+
ollamaMessage.content = enhancedContent;
|
|
442
|
+
}
|
|
443
|
+
// Process images: strip data URL prefix if present
|
|
444
|
+
if (msg.images && msg.images.length > 0) {
|
|
445
|
+
ollamaMessage.images = msg.images.map(img => {
|
|
446
|
+
// Strip data URL prefix if present (e.g., "data:image/png;base64,")
|
|
447
|
+
if (typeof img === 'string' && img.includes(',')) {
|
|
448
|
+
const base64Index = img.indexOf(',');
|
|
449
|
+
if (base64Index !== -1) {
|
|
450
|
+
return img.substring(base64Index + 1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return img;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return ollamaMessage;
|
|
457
|
+
});
|
|
458
|
+
let assistantContent = '';
|
|
459
|
+
let finalStatistics = undefined;
|
|
460
|
+
console.log('Backend: Using assistantMessageId:', assistantMessageId);
|
|
461
|
+
// Resolve the actual model name (handles persona IDs)
|
|
462
|
+
let actualModelName = session.model;
|
|
463
|
+
if (session.model.startsWith('persona:')) {
|
|
464
|
+
try {
|
|
465
|
+
const personaId = session.model.replace('persona:', '');
|
|
466
|
+
console.log(`[WebSocket] DEBUG: Resolving persona ${personaId} for user ${userId}`);
|
|
467
|
+
const { personaService } = await import('./services/personaService.js');
|
|
468
|
+
// Try to get persona for the current user first, then fallback to 'default'
|
|
469
|
+
let persona = await personaService.getPersonaById(personaId, userId);
|
|
470
|
+
if (!persona && userId !== 'default') {
|
|
471
|
+
console.log(`[WebSocket] DEBUG: Persona not found for user ${userId}, trying default user`);
|
|
472
|
+
persona = await personaService.getPersonaById(personaId, 'default');
|
|
473
|
+
}
|
|
474
|
+
console.log(`[WebSocket] DEBUG: Persona lookup result:`, persona
|
|
475
|
+
? `Found persona with model: ${persona.model}`
|
|
476
|
+
: 'Persona not found');
|
|
477
|
+
if (persona && persona.model) {
|
|
478
|
+
actualModelName = persona.model;
|
|
479
|
+
console.log(`[WebSocket] Resolved persona ${personaId} to model: ${actualModelName}`);
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
console.warn(`[WebSocket] Persona ${personaId} not found, using original model: ${session.model}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
console.error(`[WebSocket] Error resolving persona model:`, error);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Check if there's an active plugin for this model
|
|
490
|
+
console.log(`[WebSocket] Looking for plugin for model: ${actualModelName}`);
|
|
491
|
+
const activePlugin = pluginService.getActivePluginForModel(actualModelName);
|
|
492
|
+
console.log(`[WebSocket] Found plugin:`, activePlugin ? activePlugin.id : 'none');
|
|
493
|
+
if (activePlugin) {
|
|
494
|
+
console.log(`[WebSocket] Using plugin ${activePlugin.id} for model ${actualModelName}`);
|
|
495
|
+
try {
|
|
496
|
+
// Get user's preferred generation options
|
|
497
|
+
const userGenerationOptions = preferencesService.getGenerationOptions();
|
|
498
|
+
// Merge user preferences with request options
|
|
499
|
+
const mergedOptions = mergeGenerationOptions(userGenerationOptions, options);
|
|
500
|
+
// Get messages for context
|
|
501
|
+
const contextMessages = chatService.getMessagesForContext(sessionId);
|
|
502
|
+
// Use plugin for generation (non-streaming for now)
|
|
503
|
+
// For regenerations, the user message is already in context; for new messages, we need to add it
|
|
504
|
+
const messagesForPlugin = regenerate
|
|
505
|
+
? contextMessages
|
|
506
|
+
: contextMessages.concat([userMessage]);
|
|
507
|
+
const pluginResponse = await pluginService.executePluginRequest(actualModelName, messagesForPlugin, mergedOptions);
|
|
508
|
+
// Get the content from plugin response
|
|
509
|
+
assistantContent =
|
|
510
|
+
pluginResponse.choices[0]?.message?.content || '';
|
|
511
|
+
// Send the complete response as chunks to simulate streaming
|
|
512
|
+
const words = assistantContent.split(' ');
|
|
513
|
+
const BATCH_SIZE = 3; // Send 3 words at a time to reduce message frequency
|
|
514
|
+
for (let i = 0; i < words.length; i += BATCH_SIZE) {
|
|
515
|
+
const batch = words.slice(i, i + BATCH_SIZE);
|
|
516
|
+
const chunk = words.slice(0, i + batch.length).join(' ');
|
|
517
|
+
const isLast = i + BATCH_SIZE >= words.length;
|
|
518
|
+
ws.send(JSON.stringify({
|
|
519
|
+
type: 'assistant_chunk',
|
|
520
|
+
data: {
|
|
521
|
+
content: batch.join(' ') + (isLast ? '' : ' '),
|
|
522
|
+
total: chunk,
|
|
523
|
+
done: isLast,
|
|
524
|
+
messageId: assistantMessageId,
|
|
525
|
+
},
|
|
526
|
+
}));
|
|
527
|
+
// Small delay to simulate streaming but with better batching
|
|
528
|
+
if (!isLast) {
|
|
529
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
// Save the complete assistant message
|
|
533
|
+
if (assistantContent && assistantMessageId) {
|
|
534
|
+
console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
|
|
535
|
+
// Calculate branching fields if this is a regeneration
|
|
536
|
+
let branchingFields = {};
|
|
537
|
+
if (regenerate && originalMessageId) {
|
|
538
|
+
// Find the original message to get its parentId or use its ID as parent
|
|
539
|
+
const originalMsg = session.messages.find(m => m.id === originalMessageId);
|
|
540
|
+
const parentId = originalMsg?.parentId || originalMessageId;
|
|
541
|
+
// Count existing siblings to determine branch index
|
|
542
|
+
const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
|
|
543
|
+
branchingFields = {
|
|
544
|
+
parentId,
|
|
545
|
+
branchIndex: siblingCount, // New branch gets next index
|
|
546
|
+
isActive: true,
|
|
547
|
+
};
|
|
548
|
+
console.log('Backend: Setting branching fields:', branchingFields);
|
|
549
|
+
}
|
|
550
|
+
const assistantMessage = chatService.addMessage(sessionId, {
|
|
551
|
+
role: 'assistant',
|
|
552
|
+
content: assistantContent,
|
|
553
|
+
model: session.model,
|
|
554
|
+
id: assistantMessageId,
|
|
555
|
+
...branchingFields,
|
|
556
|
+
}, userId);
|
|
557
|
+
console.log('Backend: Assistant message saved:', !!assistantMessage);
|
|
558
|
+
// Send completion signal
|
|
559
|
+
ws.send(JSON.stringify({
|
|
560
|
+
type: 'assistant_complete',
|
|
561
|
+
data: assistantMessage,
|
|
562
|
+
}));
|
|
563
|
+
}
|
|
564
|
+
return; // Exit early since we handled the request via plugin
|
|
565
|
+
}
|
|
566
|
+
catch (pluginError) {
|
|
567
|
+
console.error('Plugin failed, falling back to Ollama:', pluginError);
|
|
568
|
+
// Continue to Ollama fallback below
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
console.log(`[WebSocket] No plugin found or plugin failed, using Ollama for model: ${actualModelName}`);
|
|
572
|
+
// Reuse the actualModelName variable that was already resolved above
|
|
573
|
+
// If we're here, it means either there was no plugin or plugin failed
|
|
574
|
+
// The actualModelName was already resolved in the earlier code block
|
|
575
|
+
// Get user's preferred generation options
|
|
576
|
+
const userGenerationOptions = preferencesService.getGenerationOptions();
|
|
577
|
+
// Merge user preferences with request options
|
|
578
|
+
const mergedOptions = mergeGenerationOptions(userGenerationOptions, options);
|
|
579
|
+
// Create chat request with advanced features
|
|
580
|
+
const chatRequest = {
|
|
581
|
+
model: actualModelName,
|
|
582
|
+
messages: ollamaMessages,
|
|
583
|
+
stream: true,
|
|
584
|
+
options: mergedOptions,
|
|
585
|
+
};
|
|
586
|
+
// Add structured output format if specified
|
|
587
|
+
if (format) {
|
|
588
|
+
chatRequest.format = format;
|
|
589
|
+
}
|
|
590
|
+
// Stream response from Ollama using chat completion
|
|
591
|
+
await ollamaService.generateChatStreamResponse(chatRequest, chunk => {
|
|
592
|
+
if (chunk.message?.content) {
|
|
593
|
+
assistantContent += chunk.message.content;
|
|
594
|
+
// Send streaming chunk with the provided message ID
|
|
595
|
+
ws.send(JSON.stringify({
|
|
596
|
+
type: 'assistant_chunk',
|
|
597
|
+
data: {
|
|
598
|
+
content: chunk.message.content,
|
|
599
|
+
total: assistantContent,
|
|
600
|
+
done: chunk.done,
|
|
601
|
+
messageId: assistantMessageId,
|
|
602
|
+
},
|
|
603
|
+
}));
|
|
604
|
+
}
|
|
605
|
+
// Capture final statistics when streaming is done
|
|
606
|
+
if (chunk.done) {
|
|
607
|
+
finalStatistics = {
|
|
608
|
+
total_duration: chunk.total_duration,
|
|
609
|
+
load_duration: chunk.load_duration,
|
|
610
|
+
prompt_eval_count: chunk.prompt_eval_count,
|
|
611
|
+
prompt_eval_duration: chunk.prompt_eval_duration,
|
|
612
|
+
eval_count: chunk.eval_count,
|
|
613
|
+
eval_duration: chunk.eval_duration,
|
|
614
|
+
created_at: chunk.created_at,
|
|
615
|
+
model: chunk.model,
|
|
616
|
+
};
|
|
617
|
+
// Calculate tokens per second if we have the necessary data
|
|
618
|
+
if (chunk.eval_count && chunk.eval_duration) {
|
|
619
|
+
finalStatistics.tokens_per_second =
|
|
620
|
+
Math.round((chunk.eval_count / (chunk.eval_duration / 1e9)) * 100) / 100;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}, error => {
|
|
624
|
+
ws.send(JSON.stringify({
|
|
625
|
+
type: 'error',
|
|
626
|
+
data: { error: error.message },
|
|
627
|
+
}));
|
|
628
|
+
}, () => {
|
|
629
|
+
// Save the complete assistant message with the provided ID
|
|
630
|
+
if (assistantContent && assistantMessageId) {
|
|
631
|
+
console.log('Backend: Saving complete assistant message with ID:', assistantMessageId, 'regenerate:', !!regenerate);
|
|
632
|
+
// Calculate branching fields if this is a regeneration
|
|
633
|
+
let branchingFields = {};
|
|
634
|
+
if (regenerate && originalMessageId) {
|
|
635
|
+
// Find the original message to get its parentId or use its ID as parent
|
|
636
|
+
const originalMsg = session.messages.find(m => m.id === originalMessageId);
|
|
637
|
+
const parentId = originalMsg?.parentId || originalMessageId;
|
|
638
|
+
// Count existing siblings to determine branch index
|
|
639
|
+
const siblingCount = session.messages.filter(m => m.id === parentId || m.parentId === parentId).length;
|
|
640
|
+
branchingFields = {
|
|
641
|
+
parentId,
|
|
642
|
+
branchIndex: siblingCount, // New branch gets next index
|
|
643
|
+
isActive: true,
|
|
644
|
+
};
|
|
645
|
+
console.log('Backend: Setting branching fields:', branchingFields);
|
|
646
|
+
}
|
|
647
|
+
console.log('Backend: About to save assistant message:', {
|
|
648
|
+
sessionId,
|
|
649
|
+
messageId: assistantMessageId,
|
|
650
|
+
contentLength: assistantContent.length,
|
|
651
|
+
hasBranchingFields: Object.keys(branchingFields).length > 0,
|
|
652
|
+
branchingFields,
|
|
653
|
+
});
|
|
654
|
+
const assistantMessage = chatService.addMessage(sessionId, {
|
|
655
|
+
role: 'assistant',
|
|
656
|
+
content: assistantContent,
|
|
657
|
+
model: session.model,
|
|
658
|
+
id: assistantMessageId,
|
|
659
|
+
statistics: finalStatistics,
|
|
660
|
+
...branchingFields,
|
|
661
|
+
}, userId);
|
|
662
|
+
console.log('Backend: Assistant message saved:', !!assistantMessage, assistantMessage
|
|
663
|
+
? {
|
|
664
|
+
id: assistantMessage.id,
|
|
665
|
+
contentLength: assistantMessage.content.length,
|
|
666
|
+
}
|
|
667
|
+
: 'FAILED TO SAVE');
|
|
668
|
+
// Send completion signal with statistics
|
|
669
|
+
ws.send(JSON.stringify({
|
|
670
|
+
type: 'assistant_complete',
|
|
671
|
+
data: {
|
|
672
|
+
content: assistantContent,
|
|
673
|
+
role: 'assistant',
|
|
674
|
+
timestamp: Date.now(),
|
|
675
|
+
messageId: assistantMessageId,
|
|
676
|
+
statistics: finalStatistics,
|
|
677
|
+
...branchingFields,
|
|
678
|
+
},
|
|
679
|
+
}));
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
catch (error) {
|
|
685
|
+
console.error('WebSocket error:', error);
|
|
686
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
687
|
+
ws.send(JSON.stringify({
|
|
688
|
+
type: 'error',
|
|
689
|
+
data: { error: errorMessage },
|
|
690
|
+
}));
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
ws.on('close', () => {
|
|
694
|
+
console.log('WebSocket client disconnected');
|
|
695
|
+
});
|
|
696
|
+
ws.on('error', error => {
|
|
697
|
+
console.error('WebSocket error:', error);
|
|
698
|
+
});
|
|
699
|
+
// Send initial connection confirmation
|
|
700
|
+
ws.send(JSON.stringify({
|
|
701
|
+
type: 'connected',
|
|
702
|
+
data: { message: 'Connected to Libre WebUI' },
|
|
703
|
+
}));
|
|
704
|
+
});
|
|
705
|
+
// Start server
|
|
706
|
+
server.listen({ port, host: '0.0.0.0' }, () => {
|
|
707
|
+
console.log(`đ Libre WebUI Backend running on port ${port}`);
|
|
708
|
+
console.log(`đĄ WebSocket server running on ws://localhost:${port}/ws`);
|
|
709
|
+
console.log(`đ CORS enabled for: ${corsOrigins.join(', ')}`);
|
|
710
|
+
// Check OAuth providers configuration on startup
|
|
711
|
+
const githubOAuth = new GitHubOAuthService();
|
|
712
|
+
const hfOAuth = new HuggingFaceOAuthService();
|
|
713
|
+
const githubConfigured = githubOAuth.isConfigured();
|
|
714
|
+
const hfConfigured = hfOAuth.isConfigured();
|
|
715
|
+
if (githubConfigured || hfConfigured) {
|
|
716
|
+
console.log('đ SSO Configuration:');
|
|
717
|
+
if (githubConfigured) {
|
|
718
|
+
console.log(' â
GitHub OAuth configured and ready');
|
|
719
|
+
}
|
|
720
|
+
if (hfConfigured) {
|
|
721
|
+
console.log(' â
Hugging Face OAuth configured and ready');
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log('âšī¸ No SSO providers configured (optional)');
|
|
726
|
+
}
|
|
727
|
+
// Check Ollama connection on startup
|
|
728
|
+
ollamaService.isHealthy().then(isHealthy => {
|
|
729
|
+
if (isHealthy) {
|
|
730
|
+
console.log('â
Ollama service is connected and ready');
|
|
731
|
+
}
|
|
732
|
+
else {
|
|
733
|
+
console.log("â ī¸ Ollama service is not available - make sure it's running on http://localhost:11434");
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
// Graceful shutdown
|
|
738
|
+
process.on('SIGTERM', () => {
|
|
739
|
+
console.log('SIGTERM signal received: closing HTTP server');
|
|
740
|
+
server.close(() => {
|
|
741
|
+
console.log('HTTP server closed');
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
process.on('SIGINT', () => {
|
|
745
|
+
console.log('SIGINT signal received: closing HTTP server');
|
|
746
|
+
server.close(() => {
|
|
747
|
+
console.log('HTTP server closed');
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
export default app;
|
|
751
|
+
//# sourceMappingURL=index.js.map
|