From 8482296ed8359150c2dc01ca16c9be06dd62f8a0 Mon Sep 17 00:00:00 2001 From: OpenClaw Assistant Date: Mon, 23 Feb 2026 10:30:45 +0000 Subject: [PATCH] Add VK booking-only policy and anti-bot protection --- .gitignore | 1 + skills/vk-gateway/.env.example | 32 ++++++++++++++++++++++ skills/vk-gateway/SKILL.md | 15 +++++++++++ skills/vk-gateway/vk-endpoint.mjs | 29 ++++++++++++++++---- skills/vk-gateway/vk-longpoll.mjs | 44 +++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 skills/vk-gateway/.env.example diff --git a/.gitignore b/.gitignore index ea3c144..d5a75ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .env .env.* +!**/.env.example # runtime data skills/paradiz/data/ diff --git a/skills/vk-gateway/.env.example b/skills/vk-gateway/.env.example new file mode 100644 index 0000000..51ddf88 --- /dev/null +++ b/skills/vk-gateway/.env.example @@ -0,0 +1,32 @@ +# VK community settings +VK_GROUP_ID=24068391 +VK_TOKEN=vk1.a.your_community_token +VK_API_VERSION=5.199 +VK_WAIT=25 + +# Local endpoint used by longpoll worker +OPENCLAW_BRIDGE_URL=http://127.0.0.1:8787/vk/inbound +OPENCLAW_BRIDGE_TIMEOUT_MS=45000 + +# OpenClaw Responses API +OPENCLAW_RESPONSES_URL=http://127.0.0.1:18789/v1/responses +OPENCLAW_GATEWAY_TOKEN=replace_with_gateway_token +OPENCLAW_AGENT_MODEL=openclaw:main +OPENCLAW_TIMEOUT_MS=60000 + +# Endpoint bind +VK_ENDPOINT_HOST=127.0.0.1 +VK_ENDPOINT_PORT=8787 +VK_ENDPOINT_PATH=/vk/inbound + +# Optional +VK_ALLOWED_EVENTS=message_new +VK_SYNC_LONGPOLL_SETTINGS=1 + +# Anti-bot guard (rate + spam patterns) +VK_ANTIBOT_ENABLED=1 +VK_ANTIBOT_WINDOW_SEC=20 +VK_ANTIBOT_MAX_MSGS=4 +VK_ANTIBOT_BLOCK_SEC=300 + +LOG_LEVEL=info diff --git a/skills/vk-gateway/SKILL.md b/skills/vk-gateway/SKILL.md index 1ff95a8..cee6975 100644 --- a/skills/vk-gateway/SKILL.md +++ b/skills/vk-gateway/SKILL.md @@ -33,6 +33,21 @@ description: "Локальный VK gateway в одной папке: приём - `OPENCLAW_RESPONSES_URL` — URL OpenClaw endpoint `/v1/responses`. - `OPENCLAW_GATEWAY_TOKEN` — токен gateway. +## Правила общения в VK + +- Фокусируйся только на теме подбора и бронирования номеров (логика paradiz). +- Если пользователь уходит в технические вопросы (файлы, сервер, доступы, скрипты, API) — не обсуждай технические детали. +- На техвопросы отвечай коротко, с лёгкой шуткой, и сразу возвращай разговор к бронированию. + +## Антибот + +- Включён фильтр спама/ботов по частоте и шаблонам сообщений. +- Настройки через env: + - `VK_ANTIBOT_ENABLED` + - `VK_ANTIBOT_WINDOW_SEC` + - `VK_ANTIBOT_MAX_MSGS` + - `VK_ANTIBOT_BLOCK_SEC` + ## Надёжность - `failed=1` → обновляй `ts`. diff --git a/skills/vk-gateway/vk-endpoint.mjs b/skills/vk-gateway/vk-endpoint.mjs index 96af407..8ad8ec4 100755 --- a/skills/vk-gateway/vk-endpoint.mjs +++ b/skills/vk-gateway/vk-endpoint.mjs @@ -32,7 +32,7 @@ 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', + openclawUrl: process.env.OPENCLAW_RESPONSES_URL || 'http://127.0.0.1:18789/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), @@ -45,12 +45,27 @@ if (!CFG.openclawToken) { } const SYSTEM_INSTRUCTIONS = [ - 'Ты отвечаешь клиентам во ВКонтакте от имени дружелюбного менеджера.', - 'Пиши на русском, коротко и по делу, без воды.', - 'Не выдумывай цены и наличие. Если данных не хватает — задай минимально нужные уточнения.', - 'Всегда завершай мягким следующим шагом.', + 'Ты отвечаешь клиентам во ВКонтакте как менеджер по бронированию размещения.', + 'Всегда общайся на русском, коротко, дружелюбно и с лёгким юмором.', + 'Твоя основная задача: переводить диалог к бронированию номеров и подбору вариантов.', + 'Для расчётов и логики бронирования используй подход paradiz: даты, состав гостей, питание, подходящие номера/связки номеров.', + 'Если вопрос технический (серверы, файлы, скрипты, доступы, установка, админка, API), не обсуждай детали и не давай инструкций.', + 'На технические вопросы мягко отшучивайся и сразу возвращай разговор к бронированию.', + 'Не выдумывай цены и наличие. Если данных не хватает — запрашивай только минимально нужные поля.', + 'Всегда заканчивай понятным следующим шагом по бронированию.', ].join(' '); +const TECH_RE = /(сервер|файл|скрипт|ssh|linux|docker|git|github|api|токен|ключ|доступ|настрой|установ|деплой|хост|база|sql|конфиг|systemd|терминал|команд)/i; + +function buildTechRedirectReply() { + const variants = [ + 'С техникой у меня режим «тишина в серверной» 😄 Зато с радостью подберу бронь: напишите даты и сколько гостей?', + 'Про файлы и серверы сегодня не болтаю — берегу магию отдыха ✨ Давайте лучше подберём номер: какие даты и состав гостей?', + 'Техвопросы пропускаю с улыбкой 😉 А вот с бронью помогу сразу: когда планируете заезд и сколько будет взрослых/детей?', + ]; + return variants[Math.floor(Math.random() * variants.length)]; +} + function readBody(req, limit) { return new Promise((resolve, reject) => { let size = 0; @@ -90,6 +105,10 @@ async function askOpenClaw(payload) { const userText = String(payload?.text || '').trim(); if (!userText) return { silent: true }; + if (TECH_RE.test(userText)) { + return { reply: buildTechRedirectReply() }; + } + const reqBody = { model: CFG.agentModel, user: `vk:${payload.group_id}:${payload.user_id}`, diff --git a/skills/vk-gateway/vk-longpoll.mjs b/skills/vk-gateway/vk-longpoll.mjs index b6b2805..0419011 100755 --- a/skills/vk-gateway/vk-longpoll.mjs +++ b/skills/vk-gateway/vk-longpoll.mjs @@ -45,6 +45,10 @@ const CFG = { bridgeTimeout: Number(process.env.OPENCLAW_BRIDGE_TIMEOUT_MS || 45000), allowedEvents: new Set(parseCsv(process.env.VK_ALLOWED_EVENTS, ['message_new'])), syncSettings: process.env.VK_SYNC_LONGPOLL_SETTINGS === '1', + antiBotEnabled: process.env.VK_ANTIBOT_ENABLED !== '0', + antiBotWindowSec: Number(process.env.VK_ANTIBOT_WINDOW_SEC || 20), + antiBotMaxMsgsPerWindow: Number(process.env.VK_ANTIBOT_MAX_MSGS || 4), + antiBotBlockSec: Number(process.env.VK_ANTIBOT_BLOCK_SEC || 300), }; if (!CFG.groupId || !CFG.token || !CFG.bridgeUrl) { @@ -60,6 +64,12 @@ if (CFG.wait > 90 || CFG.wait < 1) { } const seen = new Set(); +const userTraffic = new Map(); +const blockedUsers = new Map(); + +function nowSec() { + return Math.floor(Date.now() / 1000); +} function vkApi(method, params = {}) { const qs = new URLSearchParams({ @@ -110,6 +120,39 @@ async function ensureLongPollSettings() { console.log('[vk-bridge] long poll settings synced'); } +function isLikelyBot(msg) { + if (!CFG.antiBotEnabled) return false; + + const uid = msg?.from_id; + if (!uid) return false; + + const now = nowSec(); + const blockedUntil = blockedUsers.get(uid) || 0; + if (blockedUntil > now) return true; + + const text = String(msg?.text || '').trim(); + + const spamByPattern = + text.length > 1200 || + (text.match(/https?:\/\//gi)?.length || 0) >= 3 || + /(крипт|ставк|казино|эрот|18\+|viagra|быстр[ыо]й\s*заработок)/i.test(text); + + const userState = userTraffic.get(uid) || { times: [] }; + userState.times = userState.times.filter((t) => now - t <= CFG.antiBotWindowSec); + userState.times.push(now); + userTraffic.set(uid, userState); + + const spamByRate = userState.times.length > CFG.antiBotMaxMsgsPerWindow; + + if (spamByPattern || spamByRate) { + blockedUsers.set(uid, now + CFG.antiBotBlockSec); + console.log(`[vk-bridge] antibot block user=${uid} for ${CFG.antiBotBlockSec}s`); + return true; + } + + return false; +} + async function callBridge(evt) { const msg = evt?.object?.message; if (!msg) return { silent: true }; @@ -161,6 +204,7 @@ function isInboundUserMessage(evt) { if (!msg) return false; if ((msg.out ?? 0) === 1) return false; if (!msg.from_id || !msg.peer_id) return false; + if (isLikelyBot(msg)) return false; return true; }