fix: VK photo upload — 2-step getWallUploadServer + saveWallPhoto

До: 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
This commit is contained in:
Ник (Claude)
2026-06-11 19:45:13 +03:00
parent 4580264de9
commit 0a9d886435
+66 -6
View File
@@ -127,7 +127,13 @@ async function publishToVK({ channel, text, photoUrl, article }) {
if (!channel.vk_group_id || !channel.vk_access_token) { if (!channel.vk_group_id || !channel.vk_access_token) {
throw new Error('VK не настроен'); 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; let finalText = text;
if (article) { if (article) {
const url = articleUrl(article); const url = articleUrl(article);
@@ -136,15 +142,69 @@ async function publishToVK({ channel, text, photoUrl, article }) {
finalText = `${finalText}\n\n${buttonText}\n${url}`; 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({ const params = new URLSearchParams({
owner_id: '-' + String(channel.vk_group_id).replace(/^-/, ''), owner_id: ownerId,
from_group: '1', from_group: '1',
message: finalText, message: finalText,
access_token: channel.vk_access_token, access_token: token,
v: '5.199', v,
}); });
const res = await axios.post('https://api.vk.com/method/wall.post', params, { timeout: 15000 }); if (attachments) params.set('attachments', attachments);
if (res.data?.error) throw new Error(`VK: ${res.data.error.error_msg}`);
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; return res.data?.response?.post_id;
} }