Refactor GitCode daily bot

This commit is contained in:
2026-07-06 09:34:39 +08:00
parent 120ae7d3f9
commit 77a07aae0a
19 changed files with 2251 additions and 533 deletions

172
utils/feishu.js Normal file
View 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,
};