Files
playwright-gitcode/utils/feishu.js
2026-07-06 09:34:39 +08:00

173 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 飞书 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,
};