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

167 lines
5.0 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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}`);
});