Files
airllm-fork-nodejs/server/server.js

241 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`);
});