241 lines
8.1 KiB
JavaScript
241 lines
8.1 KiB
JavaScript
import express from 'express';
|
||
import cors from 'cors';
|
||
import dotenv from 'dotenv';
|
||
import path from 'path';
|
||
import fs from 'fs';
|
||
import { fileURLToPath } from 'url';
|
||
import { getLlama, LlamaChatSession } from 'node-llama-cpp';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
// Поднимаемся на уровень выше, чтобы попасть в корень проекта
|
||
const PROJECT_ROOT = path.join(__dirname, '..');
|
||
|
||
dotenv.config({ path: path.join(PROJECT_ROOT, '.env') });
|
||
|
||
const app = express();
|
||
const PORT = process.env.PORT || 3000;
|
||
|
||
app.use(cors());
|
||
app.use(express.json());
|
||
app.use(express.static(path.join(PROJECT_ROOT, 'public')));
|
||
|
||
let llama = null;
|
||
let model = null;
|
||
let context = null;
|
||
|
||
console.log("🌐 Запуск веб-сервера...");
|
||
|
||
async function initServer() {
|
||
try {
|
||
llama = await getLlama();
|
||
console.log("✅ Движок Llama инициализирован.");
|
||
console.log("⚠️ Модель НЕ загружена.");
|
||
} catch (err) {
|
||
console.error("❌ Критическая ошибка инициализации движка:", err.message);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
initServer();
|
||
|
||
// --- API Истории Чатов ---
|
||
|
||
// 1. Загрузка истории чата
|
||
app.get('/api/chat/load', async (req, res) => {
|
||
const { id } = req.query;
|
||
if (!id) return res.status(400).json({ error: 'ID чата не указан' });
|
||
|
||
const filePath = path.join(PROJECT_ROOT, 'userdata', `${id}.json`);
|
||
|
||
try {
|
||
// Читаем файл
|
||
const data = await fs.promises.readFile(filePath, 'utf8');
|
||
res.json(JSON.parse(data));
|
||
} catch (err) {
|
||
// Если файла нет (новый чат) - возвращаем пустой массив
|
||
res.json([]);
|
||
}
|
||
});
|
||
|
||
// 2. Сохранение истории чата
|
||
app.post('/api/chat/save', async (req, res) => {
|
||
const { id, history } = req.body;
|
||
if (!id || !history) return res.status(400).json({ error: 'Нет ID или истории' });
|
||
|
||
const userDataDir = path.join(PROJECT_ROOT, 'userdata');
|
||
|
||
// Если папки нет, создаем
|
||
if (!fs.existsSync(userDataDir)) {
|
||
await fs.promises.mkdir(userDataDir);
|
||
}
|
||
|
||
const filePath = path.join(userDataDir, `${id}.json`);
|
||
|
||
try {
|
||
await fs.promises.writeFile(filePath, JSON.stringify(history, null, 2));
|
||
res.json({ success: true });
|
||
} catch (err) {
|
||
console.error("Ошибка сохранения чата:", err);
|
||
res.status(500).json({ error: 'Не удалось сохранить файл' });
|
||
}
|
||
});
|
||
|
||
// --- API Управления Моделью ---
|
||
|
||
app.post('/api/model/load', async (req, res) => {
|
||
const { path: inputPath } = req.body;
|
||
|
||
if (!inputPath) {
|
||
return res.status(400).json({ success: false, message: 'Путь к модели не указан' });
|
||
}
|
||
|
||
if (model) {
|
||
return res.status(400).json({ success: false, message: 'Модель уже загружена.' });
|
||
}
|
||
|
||
let absolutePath;
|
||
if (path.isAbsolute(inputPath)) {
|
||
absolutePath = inputPath;
|
||
} else {
|
||
absolutePath = path.join(PROJECT_ROOT, inputPath);
|
||
}
|
||
|
||
try {
|
||
console.log(`🔄 [ЗАГРУЗКА] Попытка загрузки модели из: ${absolutePath}`);
|
||
|
||
model = await llama.loadModel({ modelPath: absolutePath });
|
||
context = await model.createContext({
|
||
contextSize: 2048
|
||
});
|
||
|
||
console.log("✅ [УСПЕХ] Модель успешно загружена.");
|
||
res.json({ success: true, message: `Модель загружена` });
|
||
|
||
} catch (err) {
|
||
console.error(`❌ [ОШИБКА] Не удалось загрузить модель: ${err.message}`);
|
||
model = null;
|
||
context = null;
|
||
res.status(500).json({ success: false, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/model/unload', async (req, res) => {
|
||
if (!model) {
|
||
return res.status(400).json({ success: false, message: 'Нет активной модели.' });
|
||
}
|
||
|
||
try {
|
||
console.log("🔄 [ВЫГРУЗКА] Освобождение ресурсов модели...");
|
||
if (context) context.dispose();
|
||
if (model) model.dispose();
|
||
model = null;
|
||
context = null;
|
||
console.log("✅ [УСПЕХ] Модель выгружена.");
|
||
res.json({ success: true, message: "Модель выгружена." });
|
||
} catch (err) {
|
||
res.status(500).json({ success: false, message: err.message });
|
||
}
|
||
});
|
||
|
||
app.post('/api/server/stop', (req, res) => {
|
||
console.log("🛑 [СЕРВЕР] Остановка...");
|
||
res.json({ success: true, message: "Остановка..." });
|
||
setTimeout(() => process.exit(0), 500);
|
||
});
|
||
|
||
app.get('/api/models/list', async (req, res) => {
|
||
const modelDir = path.join(PROJECT_ROOT, 'model');
|
||
|
||
try {
|
||
await fs.promises.access(modelDir);
|
||
const files = await fs.promises.readdir(modelDir);
|
||
const ggufModels = files.filter(file => file.endsWith('.gguf')).sort();
|
||
res.json(ggufModels);
|
||
} catch (err) {
|
||
console.log("⚠️ Папка 'model' не найдена.");
|
||
res.json([]);
|
||
}
|
||
});
|
||
|
||
// --- API Чата (С историей) ---
|
||
app.post('/api/chat', async (req, res) => {
|
||
if (!model || !context) {
|
||
return res.status(503).send("Модель не загружена.");
|
||
}
|
||
|
||
const { message, history } = req.body;
|
||
if (!message) return res.status(400).json({ error: 'Message is required' });
|
||
|
||
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||
res.setHeader('Transfer-Encoding', 'chunked');
|
||
|
||
let sequence;
|
||
|
||
try {
|
||
sequence = context.getSequence();
|
||
const session = new LlamaChatSession({ contextSequence: sequence });
|
||
|
||
// Формируем промпт из истории + новое сообщение
|
||
let fullPrompt = "";
|
||
if (Array.isArray(history)) {
|
||
history.forEach(msg => {
|
||
fullPrompt += `${msg.role === 'user' ? 'User' : 'Bot'}: ${msg.content}\n`;
|
||
});
|
||
}
|
||
fullPrompt += `User: ${message}\nBot:`;
|
||
|
||
await session.prompt(fullPrompt, {
|
||
onToken: (token) => {
|
||
try {
|
||
const chunk = model.detokenize(token);
|
||
res.write(chunk);
|
||
} catch (decodeErr) {
|
||
console.error("Ошибка декодирования:", decodeErr);
|
||
}
|
||
},
|
||
temperature: 0.7,
|
||
maxTokens: 2000,
|
||
});
|
||
|
||
res.end();
|
||
} catch (err) {
|
||
console.error("Ошибка генерации:", err.message);
|
||
res.write("\n\n[Ошибка генерации]");
|
||
res.end();
|
||
} finally {
|
||
if (sequence) sequence.dispose();
|
||
}
|
||
});
|
||
|
||
// --- API Переименования Чата ---
|
||
app.post('/api/chat/rename', async (req, res) => {
|
||
const { oldId, newName } = req.body;
|
||
if (!oldId || !newName) {
|
||
return res.status(400).json({ error: 'Не указан старый ID или новое имя' });
|
||
}
|
||
|
||
// Защита от некорректных имен файлов
|
||
// Убираем символы, запрещенные в Windows: \ / : * ? " < > |
|
||
const cleanName = newName.replace(/[\\/:*?"<>|]/g, '');
|
||
if (cleanName === '') {
|
||
return res.status(400).json({ error: 'Имя не может быть пустым' });
|
||
}
|
||
|
||
const oldPath = path.join(PROJECT_ROOT, 'userdata', `${oldId}.json`);
|
||
const newPath = path.join(PROJECT_ROOT, 'userdata', `${cleanName}.json`);
|
||
|
||
try {
|
||
// Переименовываем файл
|
||
await fs.promises.rename(oldPath, newPath);
|
||
res.json({ success: true, newId: cleanName });
|
||
} catch (err) {
|
||
console.error("Ошибка переименования:", err);
|
||
res.status(500).json({ error: err.message });
|
||
}
|
||
});
|
||
|
||
app.listen(PORT, () => {
|
||
console.log(`✅ [СЕРВЕР] Веб-сервер запущен на порту: ${PORT}`);
|
||
}); |