#!/usr/bin/env node import http from 'node:http'; import { execFile } from 'node:child_process'; import { readFileSync, existsSync } from 'node:fs'; 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'; 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'; 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 (isExternalActionRequest(question)) { return json(res, 200, { ok: true, answer: 'Я передам информацию менеджеру, он свяжется с вами. А пока напишите, пожалуйста, даты заезда/выезда и состав гостей — подготовлю варианты и стоимость.' }); } const answer = await runAgent(question); 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}`); });