WOPS1 调整巡检告警降噪

Issue: WOPS1, WROAD2C, WREAL1P
这个提交包含在:
2026-05-20 15:17:07 +08:00
父节点 56b720b8ab
当前提交 3ce183ddcf
共修改 13 个文件,包含 329 行新增和 87 行删除
+170 -5
查看文件
@@ -9,7 +9,8 @@ const { getConfig } = require("../config");
const { sendWecomMessage } = require("../services/integrations/im/wecom");
const DEFAULT_REPORT_ROOT = path.resolve(__dirname, "../../../artifacts/qa-flow");
const DEFAULT_RECIPIENTS = Object.freeze(["wangshunli", "HYD000006"]);
const DEFAULT_ALERT_STATE_FILE = path.join(DEFAULT_REPORT_ROOT, "warch3-ops-alert-state.json");
const DEFAULT_RECIPIENTS = Object.freeze(["HYD000006"]);
const SEND_CONFIRM_ENV = "WARCH3_OPS_ALERT_SEND";
function printHelp() {
@@ -20,9 +21,10 @@ QA-only WARCH3 企微巡检脱敏告警。
Options:
--summary-file=<path> WARCH3/WROAD2 summary.json。
--recipient-employee-no=<no> 收件员工号,可重复;默认 wangshunli + HYD000006。
--recipient-employee-no=<no> 收件员工号,可重复;默认 HYD000006。
--send 发送企微应用消息。
--force PASS 且无 finding 时也发送测试消息。
--state-file=<path> 告警状态文件,默认 artifacts/qa-flow/warch3-ops-alert-state.json。
--format=text|json 输出格式,默认 text。
--report-dir=<path> 报告目录,默认 artifacts/qa-flow/wecom-ops-alert-<timestamp>。
--help 显示帮助。
@@ -66,6 +68,7 @@ function parseArgs(argv) {
recipientEmployeeNos: [],
send: false,
force: false,
stateFile: process.env.WARCH3_OPS_ALERT_STATE_FILE || DEFAULT_ALERT_STATE_FILE,
format: "text",
reportDir: path.join(DEFAULT_REPORT_ROOT, `wecom-ops-alert-${timestamp()}`),
help: false,
@@ -89,6 +92,11 @@ function parseArgs(argv) {
options.send = true;
} else if (arg === "--force") {
options.force = true;
} else if (arg === "--state-file") {
options.stateFile = toSafeString(argv[index + 1], 1000);
index += 1;
} else if (arg.startsWith("--state-file=")) {
options.stateFile = toSafeString(arg.slice("--state-file=".length), 1000);
} else if (arg === "--format") {
options.format = argv[index + 1] || options.format;
index += 1;
@@ -144,8 +152,7 @@ function readSummary(summaryFile) {
function shouldSendForSummary(summary, options) {
if (options.force) return true;
const status = toSafeString(summary?.status || summary?.conclusion, 80);
return status !== "PASS_ARCHIVE_OPS_READY" || Number(summary?.findingCount || 0) > 0;
return evaluateAlertDecision(summary, options).shouldSend;
}
function nextStepForStatus(summary) {
@@ -178,6 +185,132 @@ function buildAlertMessage(summary) {
};
}
function statusRank(statusValue) {
const status = toSafeString(statusValue, 120).toUpperCase();
if (!status || status === "UNKNOWN") return 2;
if (status.includes("SKIP") || status.includes("BLOCKED") || status.includes("FAIL") || status.includes("ERROR")) {
return 4;
}
if (status.includes("PARTIAL") || status.includes("NEEDS_CONFIG")) {
return 3;
}
if (status.includes("WITH_FINDINGS")) {
return 2;
}
if (status.includes("PASS")) {
return 1;
}
return 2;
}
function collectFindingKeys(summary) {
const findings = Array.isArray(summary?.findings) ? summary.findings : [];
const keys = findings
.map((finding) => [
toSafeString(finding?.source, 80),
toSafeString(finding?.status, 80),
toSafeString(finding?.severity, 40),
toSafeString(finding?.evidenceDigest || finding?.detail, 160),
].filter(Boolean).join(":"))
.filter(Boolean);
if (!keys.length && Number(summary?.findingCount || 0) > 0) {
keys.push(`findingCount:${Number(summary.findingCount || 0)}`);
}
return Array.from(new Set(keys)).sort();
}
function buildAlertState(summary, generatedAt = new Date().toISOString()) {
const status = toSafeString(summary?.status || summary?.conclusion || "UNKNOWN", 120);
return {
status,
statusRank: statusRank(status),
findingKeys: collectFindingKeys(summary),
findingCount: Number(summary?.findingCount || 0),
reportDir: summary?.reportDir || null,
updatedAt: generatedAt,
};
}
function readAlertState(stateFile) {
if (!stateFile) return null;
try {
if (!fs.existsSync(stateFile)) return null;
const parsed = JSON.parse(fs.readFileSync(stateFile, "utf8"));
return parsed && typeof parsed === "object" ? parsed : null;
} catch {
return null;
}
}
function writeAlertState(stateFile, state) {
if (!stateFile) return null;
const resolved = path.resolve(stateFile);
fs.mkdirSync(path.dirname(resolved), { recursive: true });
fs.writeFileSync(resolved, `${JSON.stringify(state, null, 2)}\n`, "utf8");
return resolved;
}
function evaluateAlertDecision(summary, options = {}, priorState = null) {
const currentState = buildAlertState(summary);
if (options.force) {
return {
shouldSend: true,
reason: "force_requested",
newFindingKeys: [],
previousStatus: priorState?.status || null,
currentStatus: currentState.status,
currentState,
};
}
const currentRank = currentState.statusRank;
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));
if (currentRank >= 4) {
return {
shouldSend: true,
reason: "blocked_or_failed",
newFindingKeys,
previousStatus: priorState?.status || null,
currentStatus: currentState.status,
currentState,
};
}
if (priorState && currentRank > previousRank) {
return {
shouldSend: true,
reason: "status_worsened",
newFindingKeys,
previousStatus: priorState.status || null,
currentStatus: currentState.status,
currentState,
};
}
if (priorState && newFindingKeys.length > 0) {
return {
shouldSend: true,
reason: "new_findings",
newFindingKeys,
previousStatus: priorState.status || null,
currentStatus: currentState.status,
currentState,
};
}
return {
shouldSend: false,
reason: priorState ? "no_new_findings_or_status_worsening" : "baseline_recorded",
newFindingKeys,
previousStatus: priorState?.status || null,
currentStatus: currentState.status,
currentState,
};
}
async function queryRecipientRows(employeeNos) {
if (!process.env.DATABASE_URL) {
throw new Error("Missing DATABASE_URL");
@@ -280,7 +413,11 @@ async function executeOpsAlert(options, deps = {}) {
const config = deps.config || getConfig();
const summary = deps.summary || readSummary(options.summaryFile);
const message = buildAlertMessage(summary);
const shouldSend = shouldSendForSummary(summary, options);
const priorState = Object.prototype.hasOwnProperty.call(deps, "priorState")
? deps.priorState
: readAlertState(options.stateFile);
const alertDecision = evaluateAlertDecision(summary, options, priorState);
const shouldSend = alertDecision.shouldSend;
let recipientRows = deps.recipientRows;
let recipientReadError = null;
@@ -344,6 +481,21 @@ async function executeOpsAlert(options, deps = {}) {
conclusion = "PASS_ALERT_SKIPPED_NO_FINDINGS";
}
let stateWrite = {
status: "SKIPPED",
stateFile: options.stateFile ? path.resolve(options.stateFile) : null,
};
if (options.send && (delivery.status === "OK" || delivery.status === "SKIPPED_NO_FINDINGS")) {
const writeStateImpl = deps.writeAlertState || writeAlertState;
stateWrite = {
status: "OK",
stateFile: writeStateImpl(options.stateFile, {
...alertDecision.currentState,
updatedAt: generatedAt,
}),
};
}
return {
issueId: "WARCH3",
mode: "wecom-ops-alert",
@@ -353,6 +505,13 @@ async function executeOpsAlert(options, deps = {}) {
sendRequested: Boolean(options.send),
sendAllowed: Boolean(options.send && env[SEND_CONFIRM_ENV] === "1"),
shouldSend,
alertDecision: {
reason: alertDecision.reason,
previousStatus: alertDecision.previousStatus,
currentStatus: alertDecision.currentStatus,
newFindingCount: alertDecision.newFindingKeys.length,
},
stateWrite,
summary: {
issueId: summary?.issueId || null,
status: summary?.status || summary?.conclusion || "UNKNOWN",
@@ -381,6 +540,7 @@ function printText(report, reportPath) {
if (report.recipientGaps.length) {
process.stdout.write(`[wecom-ops-alert] recipientGaps=${JSON.stringify(report.recipientGaps)}\n`);
}
process.stdout.write(`[wecom-ops-alert] decision=${report.alertDecision.reason} newFindings=${report.alertDecision.newFindingCount}\n`);
}
async function main() {
@@ -407,9 +567,14 @@ if (require.main === module) {
module.exports = {
assertAlertSendAllowed,
buildAlertState,
buildAlertMessage,
evaluateAlertDecision,
executeOpsAlert,
parseArgs,
readAlertState,
sanitizeDeliveryResponse,
shouldSendForSummary,
statusRank,
writeAlertState,
};