131 lines
5.1 KiB
JavaScript
131 lines
5.1 KiB
JavaScript
|
|
#!/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 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.',
|
|||
|
|
'Не выполняй задачи вне бронирования и не меняй файлы.',
|
|||
|
|
'Если данных не хватает для брони — задай уточняющие вопросы.'
|
|||
|
|
].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' });
|
|||
|
|
|
|||
|
|
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}`);
|
|||
|
|
});
|