/**
* ПРОКСИ-СЕРВЕР ДЛЯ АССИСТЕНТА ПОДДЕРЖКИ
* ==========================================
* Хранит API-ключ и ID ассистента OpenAI.
* HTML-клиент подключается к этому серверу.
*
* Установка:
* npm install
*
* Запуск:
* node server.js
*
* Для продакшена рекомендуется pm2:
* npm install -g pm2
* pm2 start server.js --name support-proxy
*/
// ═══════════════════════════════════════════════
// НАСТРОЙКИ
// ═══════════════════════════════════════════════
const CONFIG = {
// ⚠️ Обязательно: вставьте ваш OpenAI API-ключ (начинается с sk-)
// Получить: https://platform.openai.com/api-keys
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "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",
// ✅ Ассистент создан и настроен — ID уже заполнен
ASSISTANT_ID: process.env.ASSISTANT_ID || "asst_gdxIf7pzdDoyius2IkBoxPIR",
PORT: process.env.PORT || 3001,
// Домены, с которых разрешены запросы к серверу.
// Замените на ваш домен в продакшене, например: "https://iniciolog.ru"
ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS || "*",
// ✅ Telegram-уведомления о заявках (уже настроены)
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN || "",
TELEGRAM_CHAT_ID: process.env.TELEGRAM_CHAT_ID || "8507111889",
};
// ═══════════════════════════════════════════════
const http = require("http");
const https = require("https");
const url = require("url");
// ── CORS headers ─────────────────────────────────────────────────────────────
function setCors(req, res) {
const origin = req.headers["origin"] || "*";
const allowed = CONFIG.ALLOWED_ORIGINS === "*" ? "*" : CONFIG.ALLOWED_ORIGINS;
res.setHeader("Access-Control-Allow-Origin", allowed);
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
res.setHeader("Access-Control-Allow-Credentials", "true");
}
// ── JSON body reader ──────────────────────────────────────────────────────────
function readBody(req) {
return new Promise((resolve, reject) => {
let body = "";
req.on("data", (chunk) => { body += chunk; });
req.on("end", () => {
try { resolve(JSON.parse(body || "{}")); }
catch { resolve({}); }
});
req.on("error", reject);
});
}
// ── OpenAI API call (returns response) ───────────────────────────────────────
function openaiRequest(path, method, body) {
return new Promise((resolve, reject) => {
const payload = body ? JSON.stringify(body) : null;
const options = {
hostname: "api.openai.com",
path,
method,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${CONFIG.OPENAI_API_KEY}`,
"OpenAI-Beta": "assistants=v2",
...(payload ? { "Content-Length": Buffer.byteLength(payload) } : {}),
},
};
const req = https.request(options, (res) => {
let data = "";
res.on("data", (c) => { data += c; });
res.on("end", () => {
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
catch { resolve({ status: res.statusCode, body: data }); }
});
});
req.on("error", reject);
if (payload) req.write(payload);
req.end();
});
}
// ── OpenAI streaming call (pipes SSE to response) ────────────────────────────
function openaiStream(path, method, body, res) {
return new Promise((resolve, reject) => {
const payload = JSON.stringify(body);
const options = {
hostname: "api.openai.com",
path,
method,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${CONFIG.OPENAI_API_KEY}`,
"OpenAI-Beta": "assistants=v2",
"Content-Length": Buffer.byteLength(payload),
},
};
const req = https.request(options, (upstream) => {
let buf = "";
upstream.on("data", (chunk) => {
buf += chunk.toString();
const parts = buf.split("\n");
buf = parts.pop() || "";
for (const line of parts) {
if (!line.startsWith("data: ")) continue;
const raw = line.slice(6).trim();
if (raw === "[DONE]") { resolve(); return; }
try {
const ev = JSON.parse(raw);
// Text delta
if (ev.object === "thread.message.delta") {
for (const block of ev.delta?.content || []) {
if (block.type === "text" && block.text?.value) {
res.write(`data: ${JSON.stringify({ content: block.text.value })}\n\n`);
}
}
}
// Run completed
if (ev.object === "thread.run" && (ev.status === "completed" || ev.status === "failed")) {
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
resolve();
}
} catch {}
}
});
upstream.on("end", () => { resolve(); });
upstream.on("error", reject);
});
req.on("error", reject);
req.write(payload);
req.end();
});
}
// ── Leads storage (in-memory + optional log file) ────────────────────────────
const leads = [];
function saveLead(lead) {
lead.id = leads.length + 1;
lead.createdAt = new Date().toISOString();
leads.push(lead);
// Print to console for easy monitoring
console.log("\n📋 НОВАЯ ЗАЯВКА:", JSON.stringify(lead, null, 2));
// Optional: write to leads.json
try {
const fs = require("fs");
fs.writeFileSync("leads.json", JSON.stringify(leads, null, 2));
} catch {}
}
// ── Telegram notification (optional) ─────────────────────────────────────────
async function notifyTelegram(lead) {
const token = CONFIG.TELEGRAM_BOT_TOKEN;
const chatId = CONFIG.TELEGRAM_CHAT_ID;
if (!token || !chatId) return;
const text = [
`📋 Новая заявка #${lead.id}`,
`👤 Имя: ${lead.name}`,
lead.phone ? `📞 Телефон: ${lead.phone}` : null,
lead.email ? `📧 Email: ${lead.email}` : null,
lead.service ? `🎓 Услуга: ${lead.service}` : null,
lead.preferredTime ? `🕐 Время: ${lead.preferredTime}` : null,
].filter(Boolean).join("\n");
try {
const payload = JSON.stringify({ chat_id: chatId, text, parse_mode: "HTML" });
await new Promise((resolve) => {
const req = https.request({
hostname: "api.telegram.org",
path: `/bot${token}/sendMessage`,
method: "POST",
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(payload) },
}, (r) => { r.resume(); r.on("end", resolve); });
req.on("error", resolve);
req.write(payload);
req.end();
});
} catch {}
}
// ── HTTP server ───────────────────────────────────────────────────────────────
const server = http.createServer(async (req, res) => {
setCors(req, res);
// Preflight
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
const { pathname } = url.parse(req.url);
// ── POST /thread — create a new conversation thread ──────────────────────
if (pathname === "/thread" && req.method === "POST") {
try {
const result = await openaiRequest("/v1/threads", "POST", {});
if (result.status !== 200) throw new Error(JSON.stringify(result.body));
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ threadId: result.body.id }));
} catch (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
}
return;
}
// ── POST /chat — add message and stream response ──────────────────────────
if (pathname === "/chat" && req.method === "POST") {
const body = await readBody(req);
const { threadId, message } = body;
if (!threadId || !message) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "threadId and message are required" }));
return;
}
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
});
try {
// Add message to thread
await openaiRequest(`/v1/threads/${threadId}/messages`, "POST", {
role: "user",
content: message,
});
// Stream the run
await openaiStream(
`/v1/threads/${threadId}/runs`,
"POST",
{ assistant_id: CONFIG.ASSISTANT_ID, stream: true },
res
);
} catch (err) {
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
}
res.end();
return;
}
// ── POST /leads — save a lead ─────────────────────────────────────────────
if (pathname === "/leads" && req.method === "POST") {
const body = await readBody(req);
const { name, phone, email, service, preferredTime } = body;
if (!name || (!phone && !email)) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Необходимо указать имя и телефон или email" }));
return;
}
const lead = { name, phone, email, service, preferredTime };
saveLead(lead);
await notifyTelegram(lead);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ success: true, id: lead.id }));
return;
}
// ── GET /leads — list all leads (protect this in production!) ────────────
if (pathname === "/leads" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(leads));
return;
}
// ── Health check ──────────────────────────────────────────────────────────
if (pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ ok: true, assistant: CONFIG.ASSISTANT_ID }));
return;
}
res.writeHead(404);
res.end("Not found");
});
server.listen(CONFIG.PORT, () => {
console.log(`✅ Прокси-сервер запущен на порту ${CONFIG.PORT}`);
console.log(` Ассистент: ${CONFIG.ASSISTANT_ID}`);
console.log(` API-ключ: ${CONFIG.OPENAI_API_KEY.slice(0,7)}...`);
console.log(`\n Эндпоинты:`);
console.log(` POST /thread — создать новую беседу`);
console.log(` POST /chat — отправить сообщение (SSE)`);
console.log(` POST /leads — сохранить заявку`);
console.log(` GET /health — проверка состояния\n`);
});