From f424eeec00f096fe6040742a4907276a254b3f82 Mon Sep 17 00:00:00 2001 From: OpenClaw Assistant Date: Mon, 23 Feb 2026 13:35:24 +0000 Subject: [PATCH] Auto-send Telegram lead when VK guest is ready for prepayment --- skills/vk-gateway/.env.example | 1 + skills/vk-gateway/SKILL.md | 1 + skills/vk-gateway/vk-endpoint.mjs | 101 ++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/skills/vk-gateway/.env.example b/skills/vk-gateway/.env.example index 51ddf88..cb9f713 100644 --- a/skills/vk-gateway/.env.example +++ b/skills/vk-gateway/.env.example @@ -18,6 +18,7 @@ OPENCLAW_TIMEOUT_MS=60000 VK_ENDPOINT_HOST=127.0.0.1 VK_ENDPOINT_PORT=8787 VK_ENDPOINT_PATH=/vk/inbound +VK_LEAD_STORE_PATH=/home/openclaw/.openclaw/workspace/skills/paradiz/data/vk_prepay_leads.json # Optional VK_ALLOWED_EVENTS=message_new diff --git a/skills/vk-gateway/SKILL.md b/skills/vk-gateway/SKILL.md index c2c008e..5bf7fff 100644 --- a/skills/vk-gateway/SKILL.md +++ b/skills/vk-gateway/SKILL.md @@ -12,6 +12,7 @@ description: "Локальный VK gateway в одной папке: приём - `vk-endpoint.mjs` — локальный HTTP endpoint `/vk/inbound`, вызывает OpenClaw `/v1/responses`. - `vk-longpoll.mjs` — Long Poll worker, получает `message_new`, отправляет ответы в VK и при наличии пути к листу брони прикладывает `.doc/.docx` в чат клиента. - Правило: как только гость зафиксировал/подтвердил бронь, обязательно отправляй файл брони в VK-сообщении. +- Правило: когда гость пишет о готовности внести предоплату, автоматически отправляй Telegram-уведомление с данными гостя. - `.env.example` — все настройки (включая `VK_GROUP_ID` и `VK_TOKEN`) в одном месте. ## Быстрый старт diff --git a/skills/vk-gateway/vk-endpoint.mjs b/skills/vk-gateway/vk-endpoint.mjs index e5cb97c..1e39a87 100755 --- a/skills/vk-gateway/vk-endpoint.mjs +++ b/skills/vk-gateway/vk-endpoint.mjs @@ -37,6 +37,7 @@ const CFG = { 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), + leadStorePath: process.env.VK_LEAD_STORE_PATH || '/home/openclaw/.openclaw/workspace/skills/paradiz/data/vk_prepay_leads.json', }; if (!CFG.openclawToken) { @@ -57,6 +58,84 @@ const SYSTEM_INSTRUCTIONS = [ ].join(' '); const TECH_RE = /(сервер|файл|скрипт|ssh|linux|docker|git|github|api|токен|ключ|доступ|настрой|установ|деплой|хост|база|sql|конфиг|systemd|терминал|команд)/i; +const PREPAY_READY_RE = /(готов[а-я\s]*предоплат|готов[а-я\s]*оплат|хочу внести предоплат|могу внести предоплат|куда оплатить|как оплатить|готов[а-я\s]*внести)/i; +const EMAIL_RE = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i; +const PHONE_RE = /\+?\d[\d\s()\-]{8,}\d/; + +function readLeadStore() { + try { + if (!fs.existsSync(CFG.leadStorePath)) return {}; + return JSON.parse(fs.readFileSync(CFG.leadStorePath, 'utf8') || '{}'); + } catch { + return {}; + } +} + +function writeLeadStore(store) { + try { + fs.mkdirSync(CFG.leadStorePath.split('/').slice(0, -1).join('/'), { recursive: true }); + fs.writeFileSync(CFG.leadStorePath, JSON.stringify(store, null, 2), 'utf8'); + } catch { + // ignore + } +} + +function loadParadizTelegramConfig() { + let bot = process.env.PARADIZ_TG_BOT_TOKEN || ''; + let chat = process.env.PARADIZ_TG_CHAT_ID || ''; + if (bot && chat) return { bot, chat }; + + try { + const cfgRaw = fs.readFileSync('/home/openclaw/.openclaw/openclaw.json', 'utf8'); + const cfg = JSON.parse(cfgRaw); + const env = cfg?.skills?.entries?.paradiz?.env || {}; + bot = bot || String(env.PARADIZ_TG_BOT_TOKEN || '').trim(); + chat = chat || String(env.PARADIZ_TG_CHAT_ID || '').trim(); + } catch { + // ignore + } + return { bot, chat }; +} + +function extractLeadFields(text) { + const out = {}; + const t = String(text || ''); + const em = t.match(EMAIL_RE); + if (em) out.email = em[0]; + const ph = t.match(PHONE_RE); + if (ph) out.phone = ph[0].replace(/\s+/g, ' ').trim(); + return out; +} + +async function sendTelegramPrepayLead(lead) { + const { bot, chat } = loadParadizTelegramConfig(); + if (!bot || !chat) return false; + + const message = [ + '💳 Гость готов внести предоплату', + `VK user_id: ${lead.user_id || '-'}`, + `ФИО: ${lead.fio || '-'}`, + `Телефон: ${lead.phone || '-'}`, + `E-mail: ${lead.email || '-'}`, + `Даты: ${lead.dates || '-'}`, + `Номер: ${lead.room || '-'}`, + `Сумма: ${lead.amount || '-'}`, + `Текст гостя: ${lead.last_text || '-'}`, + ].join('\n'); + + const body = new URLSearchParams({ + chat_id: String(chat), + text: message, + disable_web_page_preview: 'true', + }).toString(); + + const res = await fetch(`https://api.telegram.org/bot${bot}/sendMessage`, { + method: 'POST', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + body, + }); + return res.ok; +} function buildTechRedirectReply() { const variants = [ @@ -126,6 +205,28 @@ async function askOpenClaw(payload) { const userText = String(payload?.text || '').trim(); if (!userText) return { silent: true }; + const store = readLeadStore(); + const userKey = String(payload?.user_id || 'unknown'); + const lead = store[userKey] || {}; + const extracted = extractLeadFields(userText); + store[userKey] = { + ...lead, + ...extracted, + user_id: payload?.user_id, + last_text: userText, + updated_at: new Date().toISOString(), + }; + + if (PREPAY_READY_RE.test(userText)) { + const nowMs = Date.now(); + const lastSentMs = new Date(store[userKey].last_sent_at || 0).getTime() || 0; + if (nowMs - lastSentMs > 10 * 60 * 1000) { + const ok = await sendTelegramPrepayLead(store[userKey]); + if (ok) store[userKey].last_sent_at = new Date().toISOString(); + } + } + writeLeadStore(store); + if (TECH_RE.test(userText)) { return { reply: buildTechRedirectReply() }; }