Files
openclaw/skills/vk-gateway/vk-endpoint.mjs

167 lines
5.0 KiB
JavaScript
Raw Normal View History

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