173 lines
4.6 KiB
JavaScript
173 lines
4.6 KiB
JavaScript
/**
|
||
* 飞书 Webhook 通知模块
|
||
*
|
||
* 功能:
|
||
* - 发送文本消息
|
||
* - 发送截图:截图 → 上传到图床(telegra.ph) → 飞书消息附带图片URL
|
||
* - 如果图片上传失败,自动降级为文本+文件名
|
||
*/
|
||
|
||
const config = require('../config');
|
||
const logger = require('./logger');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
|
||
/**
|
||
* 发送飞书文本消息(带重试)
|
||
* 调试模式下跳过发送
|
||
* @param {string} text 消息内容
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function sendFeishuText(text) {
|
||
if (process.env.DEBUG === 'true') {
|
||
logger.info('[调试模式] 跳过飞书通知');
|
||
return true;
|
||
}
|
||
|
||
if (!config.feishu.webhook) {
|
||
logger.info('未配置 FEISHU_WEBHOOK,跳过飞书通知');
|
||
return true;
|
||
}
|
||
|
||
const payload = {
|
||
msg_type: 'text',
|
||
content: { text },
|
||
};
|
||
|
||
return sendPayload(payload);
|
||
}
|
||
|
||
/**
|
||
* 统一发送请求(带重试)
|
||
* @param {object} payload
|
||
* @returns {Promise<boolean>}
|
||
*/
|
||
async function sendPayload(payload) {
|
||
const maxRetries = config.retryCount;
|
||
|
||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||
try {
|
||
logger.info(`发送飞书通知 (第${attempt}次尝试)...`);
|
||
|
||
const controller = new AbortController();
|
||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||
|
||
const res = await fetch(config.feishu.webhook, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
signal: controller.signal,
|
||
});
|
||
|
||
clearTimeout(timeout);
|
||
|
||
const data = await res.text();
|
||
logger.info(`飞书响应: ${data}`);
|
||
return true;
|
||
} catch (err) {
|
||
logger.warn(`飞书发送失败 (第${attempt}次): ${err.message}`);
|
||
if (attempt < maxRetries) {
|
||
logger.info(`等待 3 秒后重试...`);
|
||
await new Promise((r) => setTimeout(r, 3000));
|
||
}
|
||
}
|
||
}
|
||
|
||
logger.error(`飞书通知发送失败,已重试 ${maxRetries} 次`);
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* 截图并发送到飞书(成功/失败通用)
|
||
*
|
||
* 流程:
|
||
* 1. 截图保存到本地 screenshots/
|
||
* 2. 发送飞书文本消息附带截图文件名(纯本地,不上传任何外部服务)
|
||
*
|
||
* @param {import('playwright').Page} page
|
||
* @param {string} taskName 任务名称
|
||
* @param {boolean} isSuccess 是否成功
|
||
* @param {string} [errorMessage] 失败时的错误信息
|
||
*/
|
||
async function sendScreenshotNotify(page, taskName, isSuccess, errorMessage) {
|
||
if (process.env.DEBUG === 'true') {
|
||
logger.info('[调试模式] 跳过飞书截图通知');
|
||
return;
|
||
}
|
||
|
||
// 1. 截图保存本地
|
||
const screenshotPath = await takeScreenshot(page, taskName);
|
||
|
||
// 2. 构建文本消息(附带本地文件名)
|
||
const status = isSuccess ? '✅' : '❌';
|
||
const lines = [`${status} ${taskName} ${isSuccess ? '执行成功' : '执行失败'}`];
|
||
if (!isSuccess && errorMessage) {
|
||
lines.push(`错误: ${errorMessage}`);
|
||
}
|
||
lines.push(`截图已保存: ${path.basename(screenshotPath)}`);
|
||
|
||
await sendFeishuText(lines.join('\n'));
|
||
}
|
||
|
||
/**
|
||
* 截图辅助(本地保存)
|
||
* @param {import('playwright').Page} page
|
||
* @param {string} label
|
||
* @returns {Promise<string>}
|
||
*/
|
||
async function takeScreenshot(page, label) {
|
||
const dir = path.resolve(config.dirs.screenshots);
|
||
if (!fs.existsSync(dir)) {
|
||
fs.mkdirSync(dir, { recursive: true });
|
||
}
|
||
|
||
const safeLabel = label.replace(/[\\/:*?"<>|]/g, '_');
|
||
const timestamp = Date.now();
|
||
const filename = `screenshot-${safeLabel}-${timestamp}.png`;
|
||
const filepath = path.join(dir, filename);
|
||
|
||
try {
|
||
await page.screenshot({ path: filepath, fullPage: true });
|
||
logger.info(`截图已保存: ${filepath}`);
|
||
} catch (err) {
|
||
logger.warn(`截图失败: ${err.message}`);
|
||
}
|
||
|
||
return filepath;
|
||
}
|
||
|
||
/**
|
||
* 发送任务完成通知(文本汇总)
|
||
* @param {object} results 任务执行结果 { taskName: true/false, ... }
|
||
*/
|
||
async function notifySuccess(results) {
|
||
const successCount = Object.values(results).filter(Boolean).length;
|
||
const totalCount = Object.keys(results).length;
|
||
|
||
const lines = ['✅ 今日任务完成', ''];
|
||
for (const [name, ok] of Object.entries(results)) {
|
||
lines.push(`${ok ? '✅' : '❌'} ${name}`);
|
||
}
|
||
lines.push('');
|
||
lines.push(`完成: ${successCount}/${totalCount}`);
|
||
|
||
await sendFeishuText(lines.join('\n'));
|
||
}
|
||
|
||
/**
|
||
* 发送任务失败通知(文本)
|
||
* @param {string} taskName
|
||
* @param {string} reason
|
||
*/
|
||
async function notifyError(taskName, reason) {
|
||
const text = `❌ ${taskName} 执行失败\n\n错误原因: ${reason}`;
|
||
await sendFeishuText(text);
|
||
}
|
||
|
||
module.exports = {
|
||
sendFeishuText,
|
||
sendScreenshotNotify,
|
||
notifySuccess,
|
||
notifyError,
|
||
};
|