#!/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}`); });