2026-03-01 17:44:19 +03:00
#!/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 ;
}
2026-03-02 00:27:13 +03:00
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 ) ) ;
}
2026-03-01 17:44:19 +03:00
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.' ,
'Н е выполняй задачи вне бронирования и не меняй файлы.' ,
2026-03-02 00:27:13 +03:00
'Если данных не хватает для брони — задай уточняющие вопросы.' ,
'СТРОГО ЗАПРЕЩЕНО: любые внешние действия и коммуникации (Telegram/VK/WhatsApp/email/звонки/API-вызовы/отправка сообщений/создание задач/изменения в сторонних системах).' ,
'Если пользователь просит внешнее действие, ответь только текстом: "Я передам информацию менеджеру, он свяжется с вами." и продолжи с б о р данных по бронированию.'
2026-03-01 17:44:19 +03:00
] . 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' } ) ;
2026-03-02 00:27:13 +03:00
if ( isExternalActionRequest ( question ) ) {
return json ( res , 200 , {
ok : true ,
answer : 'Я передам информацию менеджеру, он свяжется с вами. А пока напишите, пожалуйста, даты заезда/выезда и состав гостей — подготовлю варианты и стоимость.'
} ) ;
}
2026-03-01 17:44:19 +03:00
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 } ` ) ;
} ) ;