Refactor GitCode daily bot
This commit is contained in:
172
utils/feishu.js
Normal file
172
utils/feishu.js
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 飞书 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,
|
||||
};
|
||||
Reference in New Issue
Block a user