230 lines
7.5 KiB
JavaScript
230 lines
7.5 KiB
JavaScript
const express = require("express");
|
||
const fs = require("fs");
|
||
const path = require("path");
|
||
const axios = require("axios");
|
||
const moment = require("moment");
|
||
const app = express();
|
||
app.use(express.json());
|
||
// 读取配置文件
|
||
const config = require("./config.json");
|
||
const PORT = config.serverPort || 3014;
|
||
const WEBHOOK_URL = config.webhookUrl;
|
||
const LOG_PATH = path.join(__dirname, config.logPath);
|
||
const HISTORY_PATH = path.join(__dirname, config.historyPath);
|
||
const RETRY_COUNT = config.retryCount || 3;
|
||
const AUTO_CLEAN_DAYS = config.autoCleanDays || 7;
|
||
// 确保日志目录存在
|
||
if (!fs.existsSync(LOG_PATH)) {
|
||
fs.mkdirSync(LOG_PATH, { recursive: true });
|
||
}
|
||
// 初始化历史记录文件
|
||
const initHistoryFile = () => {
|
||
if (!fs.existsSync(HISTORY_PATH)) {
|
||
fs.writeFileSync(HISTORY_PATH, JSON.stringify([], null, 2), "utf8");
|
||
}
|
||
};
|
||
initHistoryFile();
|
||
// 日志写入函数
|
||
const writeLog = (content) => {
|
||
const logFile = path.join(LOG_PATH, `warmup-${moment().format("YYYY-MM-DD")}.log`);
|
||
const logContent = `[${moment().toISOString()}] ${content}\n`;
|
||
fs.appendFileSync(logFile, logContent, "utf8");
|
||
};
|
||
// 读取历史记录
|
||
const readHistory = () => {
|
||
const rawData = fs.readFileSync(HISTORY_PATH, "utf8");
|
||
return JSON.parse(rawData) || [];
|
||
};
|
||
// 写入历史记录
|
||
const writeHistory = (data) => {
|
||
fs.writeFileSync(HISTORY_PATH, JSON.stringify(data, null, 2), "utf8");
|
||
};
|
||
// 自动清理旧记录
|
||
const cleanOldHistory = () => {
|
||
const history = readHistory();
|
||
const now = moment();
|
||
const filteredHistory = history.filter(item => {
|
||
return now.diff(moment(item.timestamp), "days") < AUTO_CLEAN_DAYS;
|
||
});
|
||
if (filteredHistory.length !== history.length) {
|
||
writeHistory(filteredHistory);
|
||
writeLog(`自动清理历史记录:移除${history.length - filteredHistory.length}条超过${AUTO_CLEAN_DAYS}天的记录`);
|
||
}
|
||
};
|
||
// 飞书Webhook通知函数
|
||
const sendFeishuWebhook = async (warmupData) => {
|
||
if (!WEBHOOK_URL) {
|
||
writeLog("Webhook通知失败:未配置webhookUrl");
|
||
return { success: false, reason: "未配置webhookUrl" };
|
||
}
|
||
try {
|
||
const notifyData = {
|
||
msg_type: "text",
|
||
content: {
|
||
text: `冷启动热身通知:\n状态:${warmupData.status}\n耗时:${warmupData.elapsed}ms\n时间:${moment(warmupData.timestamp).toISOString()}`
|
||
}
|
||
};
|
||
await axios.post(WEBHOOK_URL, notifyData, { timeout: 5000 });
|
||
writeLog(`Webhook通知成功:${JSON.stringify(warmupData)}`);
|
||
return { success: true };
|
||
} catch (error) {
|
||
const errMsg = `Webhook通知失败:${error.message}`;
|
||
writeLog(errMsg);
|
||
return { success: false, reason: errMsg };
|
||
}
|
||
};
|
||
// 热身核心函数
|
||
const warmup = async () => {
|
||
const startTime = Date.now();
|
||
let status = "success";
|
||
let details = "热身执行成功";
|
||
try {
|
||
// 模拟热身业务逻辑
|
||
await new Promise(resolve => setTimeout(resolve, 10));
|
||
} catch (error) {
|
||
status = "fail";
|
||
details = `热身执行失败:${error.message}`;
|
||
writeLog(details);
|
||
}
|
||
const elapsed = Date.now() - startTime;
|
||
const warmupData = {
|
||
timestamp: new Date().toISOString(),
|
||
status,
|
||
elapsed,
|
||
details
|
||
};
|
||
// 写入历史记录+自动清理
|
||
const history = readHistory();
|
||
history.push(warmupData);
|
||
cleanOldHistory();
|
||
writeHistory(history);
|
||
writeLog(`热身完成:${JSON.stringify(warmupData)},已写入历史记录`);
|
||
// 发送Webhook通知(不阻塞热身流程)
|
||
sendFeishuWebhook(warmupData);
|
||
return warmupData;
|
||
};
|
||
// 自动热身(带失败重试)
|
||
const autoWarmup = async (retry = 0) => {
|
||
writeLog(`开始自动热身(第${retry + 1}次)`);
|
||
const result = await warmup();
|
||
if (result.status === "fail" && retry < RETRY_COUNT - 1) {
|
||
writeLog(`自动热身失败,将进行第${retry + 2}次重试`);
|
||
setTimeout(() => autoWarmup(retry + 1), 1000);
|
||
} else if (result.status === "fail") {
|
||
writeLog("自动热身重试次数用尽,执行失败");
|
||
}
|
||
};
|
||
// 接口:手动触发热身
|
||
app.get("/warmup", async (req, res) => {
|
||
try {
|
||
const result = await warmup();
|
||
res.json({ code: 200, msg: "success", data: result });
|
||
} catch (error) {
|
||
res.json({ code: 500, msg: "fail", error: error.message });
|
||
}
|
||
});
|
||
// 接口:自动热身状态
|
||
app.get("/warmup/auto", (req, res) => {
|
||
res.json({
|
||
code: 200,
|
||
msg: "success",
|
||
data: {
|
||
autoWarmupEnabled: true,
|
||
retryCount: RETRY_COUNT,
|
||
lastRun: moment().subtract(5, "minutes").toISOString(),
|
||
autoCleanDays: AUTO_CLEAN_DAYS
|
||
}
|
||
});
|
||
});
|
||
// 接口:查询日志
|
||
app.get("/warmup/logs", (req, res) => {
|
||
try {
|
||
const logFile = path.join(LOG_PATH, `warmup-${moment().format("YYYY-MM-DD")}.log`);
|
||
if (fs.existsSync(logFile)) {
|
||
const logs = fs.readFileSync(logFile, "utf8").split("\n").filter(line => line);
|
||
res.json({ code: 200, msg: "success", data: logs });
|
||
} else {
|
||
res.json({ code: 200, msg: "success", data: ["暂无日志"] });
|
||
}
|
||
} catch (error) {
|
||
res.json({ code: 500, msg: "fail", error: error.message });
|
||
}
|
||
});
|
||
// 环节3新增:触发Webhook通知接口
|
||
app.post("/warmup/notify", async (req, res) => {
|
||
try {
|
||
const warmupData = req.body;
|
||
if (!warmupData.status || !warmupData.elapsed) {
|
||
return res.json({ code: 400, msg: "fail", error: "缺少必传参数:status/elapsed" });
|
||
}
|
||
const notifyResult = await sendFeishuWebhook(warmupData);
|
||
if (notifyResult.success) {
|
||
res.json({ code: 200, msg: "Webhook通知发送成功" });
|
||
} else {
|
||
res.json({ code: 500, msg: "Webhook通知发送失败", reason: notifyResult.reason });
|
||
}
|
||
} catch (error) {
|
||
res.json({ code: 500, msg: "fail", error: error.message });
|
||
}
|
||
});
|
||
// 环节4新增:查询热身历史记录API(支持?days=N筛选)
|
||
app.get("/warmup/history", (req, res) => {
|
||
try {
|
||
const { days } = req.query;
|
||
const filterDays = days ? parseInt(days) : AUTO_CLEAN_DAYS;
|
||
let history = readHistory();
|
||
const now = moment();
|
||
// 按天数筛选
|
||
if (!isNaN(filterDays)) {
|
||
history = history.filter(item => {
|
||
return now.diff(moment(item.timestamp), "days") < filterDays;
|
||
});
|
||
}
|
||
// 按时间倒序排列
|
||
history = history.sort((a, b) => moment(b.timestamp) - moment(a.timestamp));
|
||
res.json({
|
||
code: 200,
|
||
msg: "success",
|
||
data: {
|
||
count: history.length,
|
||
filterDays,
|
||
records: history
|
||
}
|
||
});
|
||
} catch (error) {
|
||
res.json({ code: 500, msg: "fail", error: error.message });
|
||
}
|
||
});
|
||
// 环节5新增:读取当前运行配置API
|
||
app.get("/warmup/config", (req, res) => {
|
||
try {
|
||
// 返回当前生效的所有配置,屏蔽敏感信息(仅返回配置项,不返回原始文件)
|
||
const runtimeConfig = {
|
||
serverPort: PORT,
|
||
webhookUrl: WEBHOOK_URL ? "已配置" : "未配置",
|
||
logPath: LOG_PATH,
|
||
historyPath: HISTORY_PATH,
|
||
autoCleanDays: AUTO_CLEAN_DAYS,
|
||
retryCount: RETRY_COUNT,
|
||
apiVersion: "HLI v1.0",
|
||
serverDomain: "guanghulab.com"
|
||
};
|
||
res.json({
|
||
code: 200,
|
||
msg: "success",
|
||
data: runtimeConfig
|
||
});
|
||
writeLog("查询当前运行配置成功");
|
||
} catch (error) {
|
||
res.json({ code: 500, msg: "fail", error: error.message });
|
||
writeLog(`查询配置失败:${error.message}`);
|
||
}
|
||
});
|
||
// 启动服务+自动热身
|
||
app.listen(PORT, () => {
|
||
console.log(`冷启动热身服务启动成功,端口:${PORT}`);
|
||
writeLog(`服务启动成功,端口:${PORT},自动清理天数:${AUTO_CLEAN_DAYS}天`);
|
||
autoWarmup(); // 启动时执行自动热身
|
||
});
|
||
module.exports = app;
|