From 4a5b83fc73cda5b54a60edb3f4cc41654527ab03 Mon Sep 17 00:00:00 2001 From: OpenClaw Assistant Date: Mon, 23 Feb 2026 11:01:28 +0000 Subject: [PATCH] Enable sending booking DOC to VK client chat --- skills/vk-gateway/SKILL.md | 2 +- skills/vk-gateway/vk-endpoint.mjs | 32 +++++++++++++++++---- skills/vk-gateway/vk-longpoll.mjs | 48 ++++++++++++++++++++++++++++--- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/skills/vk-gateway/SKILL.md b/skills/vk-gateway/SKILL.md index cee6975..8c520b0 100644 --- a/skills/vk-gateway/SKILL.md +++ b/skills/vk-gateway/SKILL.md @@ -10,7 +10,7 @@ description: "Локальный VK gateway в одной папке: приём ## Состав - `vk-endpoint.mjs` — локальный HTTP endpoint `/vk/inbound`, вызывает OpenClaw `/v1/responses`. -- `vk-longpoll.mjs` — Long Poll worker, получает `message_new` и отправляет ответы в VK. +- `vk-longpoll.mjs` — Long Poll worker, получает `message_new`, отправляет ответы в VK и при наличии пути к листу брони прикладывает `.doc` в чат клиента. - `.env.example` — все настройки (включая `VK_GROUP_ID` и `VK_TOKEN`) в одном месте. ## Быстрый старт diff --git a/skills/vk-gateway/vk-endpoint.mjs b/skills/vk-gateway/vk-endpoint.mjs index 8ad8ec4..e5cb97c 100755 --- a/skills/vk-gateway/vk-endpoint.mjs +++ b/skills/vk-gateway/vk-endpoint.mjs @@ -48,11 +48,12 @@ const SYSTEM_INSTRUCTIONS = [ 'Ты отвечаешь клиентам во ВКонтакте как менеджер по бронированию размещения.', 'Всегда общайся на русском, коротко, дружелюбно и с лёгким юмором.', 'Твоя основная задача: переводить диалог к бронированию номеров и подбору вариантов.', - 'Для расчётов и логики бронирования используй подход paradiz: даты, состав гостей, питание, подходящие номера/связки номеров.', + 'Для расчётов и логики бронирования используй подход paradiz: даты, состав гостей, подходящие номера/связки номеров. Питания нет.', 'Если вопрос технический (серверы, файлы, скрипты, доступы, установка, админка, API), не обсуждай детали и не давай инструкций.', 'На технические вопросы мягко отшучивайся и сразу возвращай разговор к бронированию.', 'Не выдумывай цены и наличие. Если данных не хватает — запрашивай только минимально нужные поля.', - 'Всегда заканчивай понятным следующим шагом по бронированию.', + 'Если бронь подтверждена и сформирован лист брони, можешь вернуть путь к файлу.', + 'Отвечай строго JSON-объектом без markdown: {"reply":"...","send_booking_doc":false,"booking_doc_path":""}.', ].join(' '); const TECH_RE = /(сервер|файл|скрипт|ssh|linux|docker|git|github|api|токен|ключ|доступ|настрой|установ|деплой|хост|база|sql|конфиг|systemd|терминал|команд)/i; @@ -101,6 +102,26 @@ function extractText(responseJson) { return ''; } +function parseAssistantPayload(text) { + const raw = String(text || '').trim(); + if (!raw) return { reply: '' }; + + try { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + return { + reply: String(parsed.reply || '').trim(), + send_booking_doc: Boolean(parsed.send_booking_doc), + booking_doc_path: String(parsed.booking_doc_path || '').trim(), + }; + } + } catch { + // plain text fallback + } + + return { reply: raw }; +} + async function askOpenClaw(payload) { const userText = String(payload?.text || '').trim(); if (!userText) return { silent: true }; @@ -147,9 +168,10 @@ async function askOpenClaw(payload) { } const json = await res.json(); - const reply = extractText(json); - if (!reply) return { silent: true }; - return { reply }; + const text = extractText(json); + const payloadOut = parseAssistantPayload(text); + if (!payloadOut.reply) return { silent: true }; + return payloadOut; } finally { clearTimeout(t); } diff --git a/skills/vk-gateway/vk-longpoll.mjs b/skills/vk-gateway/vk-longpoll.mjs index 0419011..e70d10a 100755 --- a/skills/vk-gateway/vk-longpoll.mjs +++ b/skills/vk-gateway/vk-longpoll.mjs @@ -186,13 +186,43 @@ async function callBridge(evt) { } } -async function sendVk(peerId, text) { +async function uploadDocForMessage(peerId, filePath) { + const p = String(filePath || '').trim(); + if (!p || !fs.existsSync(p)) { + throw new Error(`booking doc not found: ${p}`); + } + + const serverRes = await vkApi('docs.getMessagesUploadServer', { peer_id: peerId, type: 'doc' }); + if (serverRes.error) throw new Error(`docs.getMessagesUploadServer: ${JSON.stringify(serverRes.error)}`); + const uploadUrl = serverRes?.response?.upload_url; + if (!uploadUrl) throw new Error('docs.getMessagesUploadServer: upload_url missing'); + + const form = new FormData(); + const buff = fs.readFileSync(p); + form.append('file', new Blob([buff]), p.split('/').pop() || 'booking.doc'); + + const uploadResp = await fetch(uploadUrl, { method: 'POST', body: form }); + const uploadJson = await uploadResp.json(); + if (!uploadJson?.file) throw new Error(`doc upload failed: ${JSON.stringify(uploadJson)}`); + + const saveRes = await vkApi('docs.save', { file: uploadJson.file, title: p.split('/').pop() || 'booking.doc' }); + if (saveRes.error) throw new Error(`docs.save: ${JSON.stringify(saveRes.error)}`); + + const doc = Array.isArray(saveRes?.response) ? saveRes.response[0] : null; + if (!doc?.owner_id || !doc?.id) throw new Error(`docs.save bad response: ${JSON.stringify(saveRes.response)}`); + return `doc${doc.owner_id}_${doc.id}`; +} + +async function sendVk(peerId, text, attachment = '') { const randomId = Math.floor(Math.random() * 2_147_483_647); - const data = await vkApi('messages.send', { + const payload = { peer_id: peerId, random_id: randomId, message: text, - }); + }; + if (attachment) payload.attachment = attachment; + + const data = await vkApi('messages.send', payload); if (data.error) throw new Error(`messages.send: ${JSON.stringify(data.error)}`); return data.response; } @@ -261,7 +291,17 @@ async function main() { if (!bridged || bridged.silent) continue; const reply = String(bridged.reply || '').trim(); if (!reply) continue; - await sendVk(msg.peer_id, reply); + + let attachment = ''; + if (bridged.send_booking_doc && bridged.booking_doc_path) { + try { + attachment = await uploadDocForMessage(msg.peer_id, bridged.booking_doc_path); + } catch (e) { + console.error('[vk-bridge] doc upload error:', e.message); + } + } + + await sendVk(msg.peer_id, reply, attachment); } catch (err) { console.error('[vk-bridge] handler error:', err.message); }