Files
openclaw/integrations/paradiz-web/paradiz-web-agent-server.mjs
2026-03-02 22:09:24 +03:00

191 lines
8.9 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.
#!/usr/bin/env node
import http from 'node:http';
import { execFile } from 'node:child_process';
import { readFileSync, existsSync } from 'node:fs';
const HUMAN_RE = /(жив[а-яёa-z]*\s*(человек|менеджер|оператор)|менеджер|оператор|свяж(ите|ись)|перезвон|позвон)/i;
const PHONE_RE = /\+?\d[\d\s()\-]{8,}\d/;
const EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i;
const ENV_PATH = process.env.PARADIZWEB_ENV_PATH || '/home/openclaw/.openclaw/agents/paradizweb/agent/.env';
function loadEnv(path) {
const env = {};
if (!existsSync(path)) return env;
const raw = readFileSync(path, 'utf8');
for (const line of raw.split(/\r?\n/)) {
if (!line || line.trim().startsWith('#') || !line.includes('=')) continue;
const idx = line.indexOf('=');
const key = line.slice(0, idx).trim();
const val = line.slice(idx + 1).trim();
env[key] = val;
}
return env;
}
const env = { ...loadEnv(ENV_PATH), ...process.env };
const PORT = Number(env.PARADIZWEB_UPSTREAM_PORT || 8787);
const HOST = env.PARADIZWEB_UPSTREAM_HOST || '127.0.0.1';
const TOKEN = env.PARADIZWEB_API_TOKEN || '';
const ALLOWED_ORIGIN = env.PARADIZWEB_ALLOWED_ORIGIN || 'https://vparadize.ru';
const AGENT_ID = env.PARADIZWEB_AGENT_ID || 'paradizweb';
const TG_BOT = env.PARADIZ_TG_BOT_TOKEN || '';
const TG_CHAT = env.PARADIZ_TG_CHAT_ID || '217610143';
if (!TOKEN) {
console.error('PARADIZWEB_API_TOKEN is empty. Set it in .env');
process.exit(1);
}
function sanitizeForWebsite(text) {
const lines = String(text || '').split(/\r?\n/);
const cleaned = lines.filter((line) => {
const t = line.trim();
if (!t) return true;
if (t.startsWith('🔵')) return false;
if (t.startsWith('Лимиты:')) return false;
if (/^Аккаунт:/i.test(t)) return false;
if (/^Модель:/i.test(t)) return false;
if (/^Codex-аккаунт:/i.test(t)) return false;
return true;
}).join('\n').trim();
return cleaned;
}
function isExternalActionRequest(text) {
const t = String(text || '').toLowerCase();
const markers = [
'отправ', 'напиши в telegram', 'напиши в телеграм', 'telegram', 'телеграм',
'whatsapp', 'ватсап', 'vk', 'вк', 'email', 'e-mail', 'почт', 'позвон',
'создай задачу', 'api', 'webhook', 'уведомлен'
];
return markers.some((m) => t.includes(m));
}
function json(res, code, payload) {
res.writeHead(code, {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Allow-Methods': 'POST, OPTIONS'
});
res.end(JSON.stringify(payload));
}
const OPENCLAW_BIN = env.OPENCLAW_BIN || '/home/openclaw/.npm-global/bin/openclaw';
async function sendTelegramHumanLead(userText) {
if (!TG_BOT || !TG_CHAT) return false;
const phone = (String(userText).match(PHONE_RE) || [])[0] || '-';
const email = (String(userText).match(EMAIL_RE) || [])[0] || '-';
const text = [
'🧑‍💼 Запрос на живого менеджера (сайт)',
`Телефон: ${phone}`,
`E-mail: ${email}`,
`Сообщение: ${String(userText).slice(0, 700)}`
].join('\n');
const body = new URLSearchParams({ chat_id: String(TG_CHAT), text, disable_web_page_preview: 'true' }).toString();
const res = await fetch(`https://api.telegram.org/bot${TG_BOT}/sendMessage`, {
method: 'POST',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
body
});
return res.ok;
}
function runAgent(userText) {
return new Promise((resolve, reject) => {
const guardrail = [
'Ты агент paradizweb.',
'Работай только по теме бронирования/подбора проживания для Парадиз.',
'Используй только логику скилла paradiz.',
'Не выполняй задачи вне бронирования и не меняй файлы.',
'Если данных не хватает для брони — задай уточняющие вопросы.',
'СТРОГО ЗАПРЕЩЕНО: любые внешние действия и коммуникации (Telegram/VK/WhatsApp/email/звонки/API-вызовы/отправка сообщений/создание задач/изменения в сторонних системах).',
'Если пользователь просит внешнее действие, ответь только текстом: "Я передам информацию менеджеру, он свяжется с вами." и продолжи сбор данных по бронированию.'
].join(' ');
const message = `${guardrail}\n\nОтвечай клиенту полезно и конкретно. Не используй NO_REPLY для веб-чата. Никогда не выводи служебные техстроки (аккаунт, модель, лимиты, codex-аккаунт).\n\nВопрос клиента: ${userText}`;
execFile(
OPENCLAW_BIN,
['agent', '--agent', AGENT_ID, '--message', message, '--json'],
{ timeout: 120000, maxBuffer: 1024 * 1024 },
(err, stdout, stderr) => {
if (err) {
return reject(new Error(stderr || err.message));
}
try {
const parsed = JSON.parse(stdout);
const text = (parsed?.result?.payloads?.[0]?.text || '').trim();
if (!text || text === 'NO_REPLY') {
return resolve('Здравствуйте! Я ИИ-агент по бронированию «Парадиз» 😊 Напишите, пожалуйста, даты заезда/выезда и состав гостей — сразу подберу варианты и посчитаю стоимость.');
}
const sanitized = sanitizeForWebsite(text);
if (!sanitized) {
return resolve('Здравствуйте! Я ИИ-агент по бронированию «Парадиз». Напишите, пожалуйста, даты и количество гостей — сразу подготовлю варианты.');
}
resolve(sanitized);
} catch (e) {
reject(new Error(`Bad JSON from openclaw agent: ${e.message}`));
}
}
);
});
}
const server = http.createServer(async (req, res) => {
if (req.method === 'OPTIONS') return json(res, 200, { ok: true });
if (req.method !== 'POST' || req.url !== '/chat') {
return json(res, 404, { ok: false, error: 'not_found' });
}
const auth = req.headers.authorization || '';
if (auth !== `Bearer ${TOKEN}`) {
return json(res, 401, { ok: false, error: 'unauthorized' });
}
let body = '';
req.on('data', (chunk) => (body += chunk));
req.on('end', async () => {
try {
const parsed = JSON.parse(body || '{}');
const question = String(parsed.question || '').trim();
if (!question) return json(res, 400, { ok: false, error: 'question_required' });
if (HUMAN_RE.test(question)) {
const hasContact = PHONE_RE.test(question) || EMAIL_RE.test(question) || /телеграм|whatsapp|вотсап|вк|vk/i.test(question);
if (!hasContact) {
return json(res, 200, {
ok: true,
answer: 'Я ИИ-агент по бронированию «Парадиз». Подключу живого менеджера. Напишите, пожалуйста, как с вами связаться: телефон или e-mail, и удобное время.'
});
}
await sendTelegramHumanLead(question).catch(() => {});
return json(res, 200, {
ok: true,
answer: 'Я ИИ-агент по бронированию «Парадиз». Передала ваш запрос менеджеру, он свяжется с вами по указанному контакту.'
});
}
if (isExternalActionRequest(question)) {
return json(res, 200, {
ok: true,
answer: 'Я ИИ-агент по бронированию «Парадиз». Я передам информацию менеджеру, он свяжется с вами. А пока напишите, пожалуйста, даты заезда/выезда и состав гостей — подготовлю варианты и стоимость.'
});
}
const answerRaw = await runAgent(question);
const answer = /^я\s*ии-агент/i.test(answerRaw.trim()) ? answerRaw : `Я ИИ-агент по бронированию «Парадиз». ${answerRaw}`;
return json(res, 200, { ok: true, answer });
} catch (e) {
return json(res, 500, { ok: false, error: e.message });
}
});
});
server.listen(PORT, HOST, () => {
console.log(`paradiz-web-agent listening on http://${HOST}:${PORT}`);
});