From 0a9d8864358e9a99c77172a0d93939862c146d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=20=28Claude=29?= Date: Thu, 11 Jun 2026 19:45:13 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20VK=20photo=20upload=20=E2=80=94=202-step?= =?UTF-8?q?=20getWallUploadServer=20+=20saveWallPhoto?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit До: wall.post без attachments → картинка игнорировалась После: 1. photos.getWallUploadServer → upload_url 2. POST upload_url с файлом (local path или download) → server/photo/hash 3. photos.saveWallPhoto → owner_id + photo_id 4. wall.post с attachments=photo{owner_id}_{id} При ошибке загрузки фото — публикуем без картинки (graceful degradation) Поддерживает как локальные /uploads/ файлы так и внешние URL --- src/services/scheduledPostsRunner.js | 72 +++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/src/services/scheduledPostsRunner.js b/src/services/scheduledPostsRunner.js index 64a77c0..86a3c8d 100644 --- a/src/services/scheduledPostsRunner.js +++ b/src/services/scheduledPostsRunner.js @@ -127,7 +127,13 @@ async function publishToVK({ channel, text, photoUrl, article }) { if (!channel.vk_group_id || !channel.vk_access_token) { throw new Error('VK не настроен'); } - // VK не поддерживает кнопки в постах — добавляем ссылку в конец текста, если её там ещё нет + + const groupId = String(channel.vk_group_id).replace(/^-/, ''); + const ownerId = '-' + groupId; + const token = channel.vk_access_token; + const v = '5.199'; + + // Добавляем ссылку на статью в конец текста let finalText = text; if (article) { const url = articleUrl(article); @@ -136,15 +142,69 @@ async function publishToVK({ channel, text, photoUrl, article }) { finalText = `${finalText}\n\n${buttonText}\n${url}`; } } + + // 2-step upload картинки + let attachments = ''; + if (photoUrl) { + try { + // Шаг 1: получаем upload URL + const uploadServerRes = await axios.get('https://api.vk.com/method/photos.getWallUploadServer', { + params: { group_id: groupId, access_token: token, v }, + timeout: 10_000, + }); + if (uploadServerRes.data?.error) throw new Error(`VK getWallUploadServer: ${uploadServerRes.data.error.error_msg}`); + const uploadUrl = uploadServerRes.data.response.upload_url; + + // Шаг 2: загружаем файл + const localPath = resolveLocalPhoto(photoUrl); + let fileBuffer, fileName; + if (localPath) { + fileBuffer = fs.readFileSync(localPath); + fileName = path.basename(localPath); + } else { + // Внешний URL — скачиваем + const dl = await axios.get(photoUrl, { responseType: 'arraybuffer', timeout: 30_000 }); + fileBuffer = Buffer.from(dl.data); + fileName = 'photo.jpg'; + } + + const form = new FormData(); + form.append('photo', fileBuffer, { filename: fileName, contentType: 'image/jpeg' }); + const uploadRes = await axios.post(uploadUrl, form, { + headers: form.getHeaders(), + timeout: 60_000, + }); + const { server, photo: photoData, hash } = uploadRes.data; + if (!photoData) throw new Error('VK upload: пустой ответ'); + + // Шаг 3: сохраняем фото + const saveRes = await axios.get('https://api.vk.com/method/photos.saveWallPhoto', { + params: { group_id: groupId, server, photo: photoData, hash, access_token: token, v }, + timeout: 10_000, + }); + if (saveRes.data?.error) throw new Error(`VK saveWallPhoto: ${saveRes.data.error.error_msg}`); + const saved = saveRes.data.response?.[0]; + if (!saved) throw new Error('VK saveWallPhoto: нет данных'); + + attachments = `photo${saved.owner_id}_${saved.id}`; + } catch (photoErr) { + console.warn(`[VK] photo upload failed, posting without photo: ${photoErr.message}`); + // Публикуем без картинки если загрузка упала + } + } + + // Шаг 4: публикуем пост const params = new URLSearchParams({ - owner_id: '-' + String(channel.vk_group_id).replace(/^-/, ''), + owner_id: ownerId, from_group: '1', message: finalText, - access_token: channel.vk_access_token, - v: '5.199', + access_token: token, + v, }); - const res = await axios.post('https://api.vk.com/method/wall.post', params, { timeout: 15000 }); - if (res.data?.error) throw new Error(`VK: ${res.data.error.error_msg}`); + if (attachments) params.set('attachments', attachments); + + const res = await axios.post('https://api.vk.com/method/wall.post', params, { timeout: 15_000 }); + if (res.data?.error) throw new Error(`VK wall.post: ${res.data.error.error_msg}`); return res.data?.response?.post_id; }