建立每日自动异机备份基线(07-27-automatic-offhost-backup)

这个提交包含在:
2026-07-27 10:50:01 +08:00
父节点 325c592aba
当前提交 b78f2a11ce
共修改 19 个文件,包含 2070 行新增和 10 行删除
+59 -10
查看文件
@@ -16,11 +16,11 @@ const SEND_CONFIRM_ENV = "WARCH3_OPS_ALERT_SEND";
function printHelp() {
process.stdout.write(`Usage: node src/scripts/send-wecom-ops-alert.js [options]
QA-only WARCH3 企微巡检脱敏告警。
QA/运维脱敏告警,支持 WARCH3 巡检和 AUTO_BACKUP 自动备份状态。
默认 dry-run:只生成预览和报告,不发送企微消息。
Options:
--summary-file=<path> WARCH3/WROAD2 summary.json。
--summary-file=<path|-> WARCH3/WROAD2 summary.json;- 表示从 stdin 读取。
--recipient-employee-no=<no> 收件员工号,可重复;默认 HYD000006。
--send 发送企微应用消息。
--force PASS 且无 finding 时也发送测试消息。
@@ -131,7 +131,12 @@ function assertAlertSendAllowed(options, env = process.env) {
}
}
function readSummary(summaryFile) {
function safeIssueId(value) {
const issueId = toSafeString(value, 40);
return /^[A-Z0-9_-]{1,40}$/.test(issueId) ? issueId : "WARCH3";
}
function readSummary(summaryFile, stdinText) {
if (!summaryFile) {
return {
status: "PARTIAL_ALERT_NEEDS_CONFIG",
@@ -141,12 +146,17 @@ function readSummary(summaryFile) {
notes: "summary_file_missing",
};
}
const resolved = path.resolve(summaryFile);
const raw = fs.readFileSync(resolved, "utf8");
const fromStdin = summaryFile === "-";
const resolved = fromStdin ? null : path.resolve(summaryFile);
const raw = fromStdin
? (stdinText ?? fs.readFileSync(0, "utf8"))
: fs.readFileSync(resolved, "utf8");
const parsed = JSON.parse(raw);
return {
...parsed,
reportDir: parsed.reportDir || path.dirname(resolved),
issueId: safeIssueId(parsed.issueId),
// stdin is an untrusted cross-host boundary. Never echo a supplied path.
reportDir: fromStdin ? "stdin-summary" : (parsed.reportDir || path.dirname(resolved)),
};
}
@@ -156,7 +166,20 @@ function shouldSendForSummary(summary, options) {
}
function nextStepForStatus(summary) {
const issueId = safeIssueId(summary?.issueId);
const status = toSafeString(summary?.status || summary?.conclusion, 80);
const findingText = Array.isArray(summary?.findings)
? summary.findings.map((finding) => toSafeString(finding?.detail, 160)).join(" ")
: "";
if (issueId === "AUTO_BACKUP" && /quarterly_shadow_restore_due/i.test(findingText)) {
return "按恢复卡在 ARM 隔离目标执行人工影子恢复,并核对就绪状态与关键计数。";
}
if (issueId === "AUTO_BACKUP" && (
/FAIL|STALE|MISSING|INVALID|MISMATCH|BLOCKED|ERROR/i.test(status)
|| Number(summary?.findingCount || 0) > 0
)) {
return "检查自动备份任务、异机传输链路和最近一个已校验成功的恢复点。";
}
if (status === "SKIP/BLOCKED_BY_EXTERNAL_CONFIG") {
return "检查 Secret、权限、可信 IP、可见范围或外部配置后复跑。";
}
@@ -167,13 +190,13 @@ function nextStepForStatus(summary) {
}
function buildAlertMessage(summary) {
const issueId = toSafeString(summary?.issueId || "WARCH3", 40) || "WARCH3";
const issueId = safeIssueId(summary?.issueId);
const status = toSafeString(summary?.status || summary?.conclusion || "UNKNOWN", 80) || "UNKNOWN";
const findingCount = Number(summary?.findingCount || 0);
const reportPath = toSafeString(summary?.reportDir || summary?.sourceReports?.wroad2 || "N/A", 500);
const nextStep = nextStepForStatus(summary);
return {
title: `OA 巡检 ${status}`,
title: issueId === "AUTO_BACKUP" ? `OA 自动备份 ${status}` : `OA 巡检 ${status}`,
body: [
`阶段:${issueId}`,
`状态:${status}`,
@@ -252,6 +275,7 @@ function writeAlertState(stateFile, state) {
function evaluateAlertDecision(summary, options = {}, priorState = null) {
const currentState = buildAlertState(summary);
const issueId = safeIssueId(summary?.issueId);
if (options.force) {
return {
shouldSend: true,
@@ -267,8 +291,31 @@ function evaluateAlertDecision(summary, options = {}, priorState = null) {
const previousRank = Number(priorState?.statusRank || statusRank(priorState?.status));
const previousKeys = new Set(Array.isArray(priorState?.findingKeys) ? priorState.findingKeys : []);
const newFindingKeys = currentState.findingKeys.filter((key) => !previousKeys.has(key));
const isAutomaticBackup = issueId === "AUTO_BACKUP";
const isQuarterlyReminder = currentState.findingKeys.some((key) => /quarterly_shadow_restore_due/i.test(key));
if (isAutomaticBackup && isQuarterlyReminder && (!priorState || newFindingKeys.length > 0)) {
return {
shouldSend: true,
reason: "quarterly_restore_reminder",
newFindingKeys,
previousStatus: priorState?.status || null,
currentStatus: currentState.status,
currentState,
};
}
if (currentRank >= 4) {
if (isAutomaticBackup && priorState && currentRank <= previousRank && newFindingKeys.length === 0) {
return {
shouldSend: false,
reason: "repeated_automatic_backup_failure",
newFindingKeys,
previousStatus: priorState.status || null,
currentStatus: currentState.status,
currentState,
};
}
return {
shouldSend: true,
reason: "blocked_or_failed",
@@ -412,6 +459,7 @@ async function executeOpsAlert(options, deps = {}) {
const generatedAt = deps.generatedAt || new Date().toISOString();
const config = deps.config || getConfig();
const summary = deps.summary || readSummary(options.summaryFile);
const issueId = safeIssueId(summary?.issueId);
const message = buildAlertMessage(summary);
const priorState = Object.prototype.hasOwnProperty.call(deps, "priorState")
? deps.priorState
@@ -497,7 +545,7 @@ async function executeOpsAlert(options, deps = {}) {
}
return {
issueId: "WARCH3",
issueId,
mode: "wecom-ops-alert",
generatedAt,
status,
@@ -513,7 +561,7 @@ async function executeOpsAlert(options, deps = {}) {
},
stateWrite,
summary: {
issueId: summary?.issueId || null,
issueId,
status: summary?.status || summary?.conclusion || "UNKNOWN",
findingCount: Number(summary?.findingCount || 0),
reportDir: summary?.reportDir || null,
@@ -572,6 +620,7 @@ module.exports = {
evaluateAlertDecision,
executeOpsAlert,
parseArgs,
readSummary,
readAlertState,
sanitizeDeliveryResponse,
shouldSendForSummary,