mne-nuzhno-veb-prilozhenie-d/server.js

286 lines
10 KiB
JavaScript

const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = process.env.PORT || 3000;
const DATA = path.join(__dirname, "data.json");
const UPLOADS = path.join(__dirname, "uploads");
if (!fs.existsSync(UPLOADS)) fs.mkdirSync(UPLOADS);
function readData() {
try { return JSON.parse(fs.readFileSync(DATA, "utf8")); } catch (_) { return { presentations: [] }; }
}
function writeData(data) {
fs.writeFileSync(DATA, JSON.stringify(data, null, 2));
}
// Простой парсинг multipart
function parseMultipart(req) {
return new Promise((resolve, reject) => {
const contentType = req.headers["content-type"] || "";
const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/i);
if (!boundaryMatch) {
return reject(new Error("Invalid content-type: " + contentType));
}
const boundary = "--" + (boundaryMatch[1] || boundaryMatch[2]).trim();
const chunks = [];
req.on("data", chunk => chunks.push(chunk));
req.on("end", () => {
const buffer = Buffer.concat(chunks);
const parts = buffer.split(Buffer.from(boundary));
const result = { fields: {}, files: {} };
for (const part of parts) {
if (part.length < 10) continue;
const str = part.toString("binary");
const headerEnd = str.indexOf("\r\n\r\n");
if (headerEnd === -1) continue;
const headers = str.slice(0, headerEnd);
const content = part.slice(headerEnd + 4);
const nameMatch = headers.match(/name="([^"]+)"/);
const filenameMatch = headers.match(/filename="([^"]+)"/);
if (!nameMatch) continue;
const name = nameMatch[1];
// Файл
if (filenameMatch) {
const filename = filenameMatch[1];
const fileContent = content.slice(0, -2); // убираем \r\n в конце
result.files[name] = {
originalFilename: filename,
filepath: path.join(UPLOADS, Date.now() + "_" + filename),
size: fileContent.length
};
fs.writeFileSync(result.files[name].filepath, fileContent);
} else {
// Поле
result.fields[name] = content.toString("utf8").slice(0, -2).trim();
}
}
resolve(result);
});
req.on("error", reject);
});
}
function readExcel(buffer) {
const XLSX = require("xlsx");
const wb = XLSX.read(buffer, { type: "buffer" });
const sheet = wb.Sheets[wb.SheetNames[0]];
const json = XLSX.utils.sheet_to_json(sheet, { header: 1 });
return json.map(row => row.join(" ")).filter(t => t.trim());
}
async function readWord(buffer) {
const mammoth = require("mammoth");
const result = await mammoth.extractRawText({ buffer });
return result.value.split(/\n+/).filter(t => t.trim());
}
async function readPdf(buffer) {
const pdfParse = require("pdf-parse");
const result = await pdfParse(buffer);
return result.text.split(/\n+/).filter(t => t.trim());
}
function readText(buffer) {
return buffer.toString("utf8").split(/\n+/).filter(t => t.trim());
}
async function createPresentationFromText(text, title) {
const AI_BASE_URL = process.env.AI_BASE_URL;
const AI_API_KEY = process.env.AI_API_KEY;
const AI_MODEL = process.env.AI_MODEL;
if (!AI_BASE_URL || !AI_API_KEY) {
const chunks = text.filter(t => t.length > 20);
const slides = [];
for (let i = 0; i < Math.min(chunks.length, 10); i++) {
const chunk = chunks[i];
const lines = chunk.split(/[.!?]/).filter(l => l.trim());
slides.push({
title: lines[0]?.slice(0, 60) || "Слайд " + (i + 1),
bullets: lines.slice(1, 5).map(l => l.trim().slice(0, 100))
});
}
return slides.length > 0 ? slides : [{ title: "Презентация", bullets: text.slice(0, 5) }];
}
try {
const prompt = `Создай структуру презентации из этого текста. Верни ТОЛЬКО JSON массив слайдов в формате: [{"title": "Заголовок", "bullets": ["тезис 1", "тезис 2"]}, ...]. Максимум 8 слайдов. Текст: ${text.slice(0, 3000)}`;
const resp = await fetch(AI_BASE_URL + "/chat/completions", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + AI_API_KEY },
body: JSON.stringify({ model: AI_MODEL || "gpt-3.5-turbo", messages: [{ role: "user", content: prompt }], temperature: 0.7 })
});
const json = await resp.json();
const content = json.choices?.[0]?.message?.content || "";
const match = content.match(/\[[\s\S]*\]/);
if (match) {
return JSON.parse(match[0]);
}
throw new Error("No JSON in response");
} catch (e) {
console.error("AI error:", e);
const chunks = text.filter(t => t.length > 20);
const slides = [];
for (let i = 0; i < Math.min(chunks.length, 8); i++) {
const chunk = chunks[i];
const lines = chunk.split(/[.!?]/).filter(l => l.trim());
slides.push({
title: lines[0]?.slice(0, 60) || "Слайд " + (i + 1),
bullets: lines.slice(1, 5).map(l => l.trim().slice(0, 100))
});
}
return slides.length > 0 ? slides : [{ title: "Презентация", bullets: text.slice(0, 5) }];
}
}
http.createServer(async (req, res) => {
const url = req.url.split("?")[0];
if (req.method === "GET" && url === "/api/presentations") {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(readData().presentations));
}
if (req.method === "POST" && url === "/api/presentations") {
let s = "";
await new Promise(resolve => {
req.on("data", c => s += c);
req.on("end", resolve);
});
const presentation = JSON.parse(s || "{}");
const data = readData();
presentation.id = Date.now();
presentation.slides = presentation.slides || [{ title: "Новый слайд", bullets: ["Тезис 1", "Тезис 2"] }];
data.presentations.push(presentation);
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, id: presentation.id }));
}
if (req.method === "GET" && url.startsWith("/api/presentations/")) {
const id = parseInt(url.split("/").pop());
const data = readData();
const presentation = data.presentations.find(p => p.id === id);
if (presentation) {
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify(presentation));
}
res.writeHead(404);
return res.end("Not found");
}
if (req.method === "PUT" && url.startsWith("/api/presentations/")) {
let s = "";
await new Promise(resolve => {
req.on("data", c => s += c);
req.on("end", resolve);
});
const presentation = JSON.parse(s || "{}");
const id = parseInt(url.split("/").pop());
const data = readData();
const index = data.presentations.findIndex(p => p.id === id);
if (index !== -1) {
data.presentations[index] = { ...data.presentations[index], ...presentation };
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
res.writeHead(404);
return res.end("Not found");
}
if (req.method === "DELETE" && url.startsWith("/api/presentations/")) {
const id = parseInt(url.split("/").pop());
const data = readData();
data.presentations = data.presentations.filter(p => p.id !== id);
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true }));
}
if (req.method === "POST" && url === "/api/create-from-file") {
try {
console.log("Received file upload request");
const multipart = await parseMultipart(req);
console.log("Parsed multipart:", Object.keys(multipart.files));
const file = multipart.files.file;
if (!file || !file.filepath) {
console.error("No file found");
res.writeHead(400, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: false, error: "Нет файла" }));
}
console.log("File received:", file.originalFilename, file.size, "bytes");
const fileData = fs.readFileSync(file.filepath);
const filename = file.originalFilename || "file";
try { fs.unlinkSync(file.filepath); } catch (_) {}
let text = [];
const ext = filename.split(".").pop().toLowerCase();
console.log("File extension:", ext);
if (ext === "xlsx" || ext === "xls") {
text = readExcel(fileData);
} else if (ext === "docx") {
text = await readWord(fileData);
} else if (ext === "pdf") {
text = await readPdf(fileData);
} else {
text = readText(fileData);
}
console.log("Extracted", text.length, "lines");
if (text.length === 0) {
res.writeHead(400, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: false, error: "Пустой документ" }));
}
const title = filename.split(".")[0];
const slides = await createPresentationFromText(text.join(" "), title);
console.log("Created", slides.length, "slides");
const data = readData();
const presentation = {
id: Date.now(),
title: title,
author: "Авто-создано",
slides: slides
};
data.presentations.push(presentation);
writeData(data);
res.writeHead(200, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: true, slides: slides.length }));
} catch (e) {
console.error("Create from file error:", e.message, e.stack);
res.writeHead(500, { "Content-Type": "application/json" });
return res.end(JSON.stringify({ ok: false, error: e.message }));
}
}
let file = url === "/" ? "/index.html" : url.split("?")[0];
const full = path.join(__dirname, file);
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
const ext = path.extname(full);
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html";
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
return res.end(fs.readFileSync(full));
}
res.writeHead(404); res.end("Not found");
}).listen(PORT, () => console.log("Сервер презентаций на порту " + PORT));