#!/usr/bin/env node import fs from 'node:fs'; import http from 'node:http'; function parseArgs() { const args = process.argv.slice(2); const out = { env: null }; for (let i = 0; i < args.length; i++) { if (args[i] === '--env') out.env = args[++i]; } return out; } function loadEnv(path) { if (!path) return; const txt = fs.readFileSync(path, 'utf8'); for (const line of txt.split(/\r?\n/)) { const t = line.trim(); if (!t || t.startsWith('#')) continue; const idx = t.indexOf('='); if (idx === -1) continue; const k = t.slice(0, idx).trim(); const v = t.slice(idx + 1).trim(); if (!(k in process.env)) process.env[k] = v; } } const { env } = parseArgs(); loadEnv(env); const CFG = { bindHost: process.env.VK_ENDPOINT_HOST || '127.0.0.1', bindPort: Number(process.env.VK_ENDPOINT_PORT || 8787), path: process.env.VK_ENDPOINT_PATH || '/vk/inbound', openclawUrl: process.env.OPENCLAW_RESPONSES_URL || 'http://127.0.0.1:3100/v1/responses', openclawToken: process.env.OPENCLAW_GATEWAY_TOKEN || '', agentModel: process.env.OPENCLAW_AGENT_MODEL || 'openclaw:main', maxBodyBytes: Number(process.env.VK_ENDPOINT_MAX_BODY_BYTES || 1_000_000), timeoutMs: Number(process.env.OPENCLAW_TIMEOUT_MS || 60000), }; if (!CFG.openclawToken) { console.error('Missing OPENCLAW_GATEWAY_TOKEN'); process.exit(1); } const SYSTEM_INSTRUCTIONS = [ 'Ты отвечаешь клиентам во ВКонтакте от имени дружелюбного менеджера.', 'Пиши на русском, коротко и по делу, без воды.', 'Не выдумывай цены и наличие. Если данных не хватает — задай минимально нужные уточнения.', 'Всегда завершай мягким следующим шагом.', ].join(' '); function readBody(req, limit) { return new Promise((resolve, reject) => { let size = 0; const chunks = []; req.on('data', (c) => { size += c.length; if (size > limit) { reject(new Error('Payload too large')); req.destroy(); return; } chunks.push(c); }); req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))); req.on('error', reject); }); } function extractText(responseJson) { if (typeof responseJson?.output_text === 'string' && responseJson.output_text.trim()) { return responseJson.output_text.trim(); } const out = responseJson?.output; if (!Array.isArray(out)) return ''; for (const item of out) { if (item?.type !== 'message' || !Array.isArray(item?.content)) continue; for (const c of item.content) { if (c?.type === 'output_text' && typeof c.text === 'string' && c.text.trim()) { return c.text.trim(); } } } return ''; } async function askOpenClaw(payload) { const userText = String(payload?.text || '').trim(); if (!userText) return { silent: true }; const reqBody = { model: CFG.agentModel, user: `vk:${payload.group_id}:${payload.user_id}`, max_output_tokens: 350, instructions: SYSTEM_INSTRUCTIONS, input: [ { type: 'message', role: 'user', content: [ { type: 'input_text', text: `VK message from user ${payload.user_id}: ${userText}`, }, ], }, ], }; const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), CFG.timeoutMs); try { const res = await fetch(CFG.openclawUrl, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${CFG.openclawToken}`, }, body: JSON.stringify(reqBody), signal: ctrl.signal, }); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`OpenClaw HTTP ${res.status} ${text}`.trim()); } const json = await res.json(); const reply = extractText(json); if (!reply) return { silent: true }; return { reply }; } finally { clearTimeout(t); } } const server = http.createServer(async (req, res) => { if (req.method === 'GET' && req.url === '/healthz') { res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify({ ok: true })); return; } if (req.method !== 'POST' || req.url !== CFG.path) { res.writeHead(404, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'not_found' })); return; } try { const raw = await readBody(req, CFG.maxBodyBytes); const body = JSON.parse(raw || '{}'); const out = await askOpenClaw(body); res.writeHead(200, { 'content-type': 'application/json' }); res.end(JSON.stringify(out)); } catch (e) { res.writeHead(500, { 'content-type': 'application/json' }); res.end(JSON.stringify({ error: 'bridge_failed', detail: e.message })); } }); server.listen(CFG.bindPort, CFG.bindHost, () => { console.log(`[vk-endpoint] listening on http://${CFG.bindHost}:${CFG.bindPort}${CFG.path}`); });