WARCH1 实现企微审批最小历史留存
这个提交包含在:
@@ -6,6 +6,7 @@ const path = require("node:path");
|
||||
const { Pool } = require("pg");
|
||||
|
||||
const { getConfig } = require("../config");
|
||||
const { createPrismaClient } = require("../prismaClient");
|
||||
const {
|
||||
APPROVAL_GET_INFO_ENDPOINT,
|
||||
APPROVAL_POC_TARGETS,
|
||||
@@ -13,6 +14,13 @@ const {
|
||||
fetchWecomApprovalInfoList,
|
||||
maskApprovalIdentifier,
|
||||
} = require("../services/integrations/wecomApproval");
|
||||
const {
|
||||
corpHash,
|
||||
createCompensationRun,
|
||||
finishCompensationRun,
|
||||
persistApprovalDetail,
|
||||
recordReconcileFinding,
|
||||
} = require("../services/integrations/wecomApprovalArchive");
|
||||
const { readCallbackEvidence } = require("../services/integrations/wecomCallbackCrypto");
|
||||
|
||||
const DEFAULT_REPORT_ROOT = path.resolve(__dirname, "../../../artifacts/qa-flow");
|
||||
@@ -47,11 +55,13 @@ Options:
|
||||
--discover-applicant-employee-no=<no>
|
||||
自动发现使用的 OA 申请人员工号,默认 SMOKE002。
|
||||
--size=<1..100> 单页审批单号数量,默认 100。
|
||||
--persist-to-db 写入 OA 自建留存表;还必须设置 WECOM_ARCHIVE_WRITE=1。
|
||||
--help 显示帮助。
|
||||
|
||||
Secret 固定规则:
|
||||
审批单号批量查询和审批详情读取:WECOM_AGENT_SECRET。
|
||||
不使用 WECOM_CONTACT_SECRET、WECOM_CHECKIN_SECRET 或 WECOM_HR_ASSISTANT_SECRET 兜底。
|
||||
OA DB 留存写入必须显式 --persist-to-db + WECOM_ARCHIVE_WRITE=1;不写企微数据。
|
||||
|
||||
隐私边界:
|
||||
报告只输出审批类型、计数、状态摘要、脱敏 sp_no/template/userid 前缀和错误分类。
|
||||
@@ -108,6 +118,7 @@ function parseArgs(argv) {
|
||||
discoverApplicantEmployeeNo: DEFAULT_DISCOVERY_APPLICANT_EMPLOYEE_NO,
|
||||
size: DEFAULT_PAGE_SIZE,
|
||||
maxPages: DEFAULT_MAX_PAGES,
|
||||
persistToDb: false,
|
||||
help: false,
|
||||
};
|
||||
|
||||
@@ -158,6 +169,10 @@ function parseArgs(argv) {
|
||||
index += 1;
|
||||
} else if (arg.startsWith("--size=")) {
|
||||
options.size = Number(arg.slice("--size=".length));
|
||||
} else if (arg === "--persist-to-db") {
|
||||
options.persistToDb = true;
|
||||
} else if (arg === "--no-persist-to-db") {
|
||||
options.persistToDb = false;
|
||||
} else {
|
||||
throw new Error(`Unsupported argument: ${arg}`);
|
||||
}
|
||||
@@ -535,10 +550,21 @@ function statusLabelFromSpStatus(value) {
|
||||
}[key] || (key ? `状态${key}` : null);
|
||||
}
|
||||
|
||||
function attachHiddenValue(target, key, value) {
|
||||
if (value != null && value !== "") {
|
||||
Object.defineProperty(target, key, {
|
||||
value,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function summarizeDetailResult(target, spNo, result) {
|
||||
const summary = result?.summary || {};
|
||||
const spStatus = summary.spStatus == null ? null : toSafeString(summary.spStatus, 40);
|
||||
return {
|
||||
return attachHiddenValue({
|
||||
target: target.key,
|
||||
label: target.label,
|
||||
status: result?.ok ? "OK" : "BLOCKED",
|
||||
@@ -554,7 +580,7 @@ function summarizeDetailResult(target, spNo, result) {
|
||||
notifyerCount: summary.notifyerCount,
|
||||
commentCount: summary.commentCount,
|
||||
response: summarizeApiResponse(result?.response),
|
||||
};
|
||||
}, "spNo", spNo);
|
||||
}
|
||||
|
||||
function buildMissingTemplateCheck(input) {
|
||||
@@ -596,6 +622,7 @@ async function runLiveTarget({
|
||||
options,
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
archivePersistence,
|
||||
}) {
|
||||
const pages = [];
|
||||
const spNos = [];
|
||||
@@ -655,8 +682,38 @@ async function runLiveTarget({
|
||||
const uniqueSpNos = Array.from(new Set(spNos));
|
||||
const details = [];
|
||||
for (const spNo of uniqueSpNos) {
|
||||
const detailResult = await fetchApprovalDetailImpl({ config, spNo });
|
||||
details.push(summarizeDetailResult(input, spNo, detailResult));
|
||||
const detailResult = await fetchApprovalDetailImpl({
|
||||
config,
|
||||
spNo,
|
||||
includeRawPayload: Boolean(archivePersistence?.enabled),
|
||||
});
|
||||
const detailSummary = summarizeDetailResult(input, spNo, detailResult);
|
||||
if (detailResult.ok && archivePersistence?.enabled) {
|
||||
try {
|
||||
const stored = await persistApprovalDetail({
|
||||
prisma: archivePersistence.prisma,
|
||||
config,
|
||||
payload: detailResult.rawPayload,
|
||||
spNo,
|
||||
target: input.key,
|
||||
source: "compensation_pull",
|
||||
compensationRunId: archivePersistence.runId,
|
||||
});
|
||||
detailSummary.archivePersistence = {
|
||||
status: "STORED",
|
||||
archiveId: stored.archiveId,
|
||||
spNoPrefix: stored.spNoPrefix,
|
||||
templateIdPrefix: stored.templateIdPrefix,
|
||||
};
|
||||
} catch (error) {
|
||||
detailSummary.archivePersistence = {
|
||||
status: "FAILED",
|
||||
reason: "archive_persist_failed",
|
||||
message: safeMessage(error?.message || error),
|
||||
};
|
||||
}
|
||||
}
|
||||
details.push(detailSummary);
|
||||
}
|
||||
|
||||
const detailBlocked = details.find((item) => item.status === "BLOCKED");
|
||||
@@ -685,6 +742,7 @@ async function runTargetChecks({
|
||||
options,
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
archivePersistence,
|
||||
}) {
|
||||
const targetChecks = [];
|
||||
for (const input of inputs) {
|
||||
@@ -700,6 +758,7 @@ async function runTargetChecks({
|
||||
options,
|
||||
fetchApprovalInfoListImpl,
|
||||
fetchApprovalDetailImpl,
|
||||
archivePersistence,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -707,7 +766,7 @@ async function runTargetChecks({
|
||||
}
|
||||
|
||||
function sanitizeEvidenceEvent(event) {
|
||||
return {
|
||||
return attachHiddenValue({
|
||||
callbackKind: toSafeString(event.callbackKind, 80),
|
||||
approvalType: toSafeString(event.approvalType, 80),
|
||||
spStatus: event.spStatus == null ? null : toSafeString(event.spStatus, 40),
|
||||
@@ -715,27 +774,85 @@ function sanitizeEvidenceEvent(event) {
|
||||
spNoPrefix: toSafeString(event.spNoPrefix, 80) || null,
|
||||
templateIdPrefix: toSafeString(event.templateIdPrefix, 80) || null,
|
||||
recordedAt: toSafeString(event.recordedAt, 80) || null,
|
||||
};
|
||||
}, "spNo", event.spNo);
|
||||
}
|
||||
|
||||
async function buildEvidenceCheck({ config, readCallbackEvidenceImpl = readCallbackEvidence }) {
|
||||
const events = await readCallbackEvidenceImpl({
|
||||
evidenceFile: config.wecomApprovalCallbackEvidenceFile,
|
||||
function sanitizeDbCallbackEvent(event) {
|
||||
return attachHiddenValue({
|
||||
callbackKind: toSafeString(event.callback_kind, 80),
|
||||
approvalType: toSafeString(event.approval_type, 80),
|
||||
spStatus: event.wecom_status == null ? null : toSafeString(event.wecom_status, 40),
|
||||
statusLabel: null,
|
||||
spNoPrefix: maskApprovalIdentifier(event.sp_no),
|
||||
templateIdPrefix: maskApprovalIdentifier(event.template_id),
|
||||
recordedAt: event.received_at ? new Date(event.received_at).toISOString() : null,
|
||||
}, "spNo", event.sp_no);
|
||||
}
|
||||
|
||||
async function readDbCallbackEvidence({ prisma, config }) {
|
||||
if (!prisma?.wecomApprovalCallbackEvent?.findMany) return [];
|
||||
const where = {
|
||||
callback_kind: "approval_status_change",
|
||||
};
|
||||
if (config?.wecomCorpId) {
|
||||
where.corp_id_hash = corpHash(config);
|
||||
}
|
||||
const events = await prisma.wecomApprovalCallbackEvent.findMany({
|
||||
where,
|
||||
orderBy: [{ received_at: "asc" }],
|
||||
take: 200,
|
||||
});
|
||||
return events.map(sanitizeDbCallbackEvent);
|
||||
}
|
||||
|
||||
async function buildEvidenceCheck({
|
||||
config,
|
||||
prisma = null,
|
||||
preferDb = false,
|
||||
readDbCallbackEvidenceImpl = readDbCallbackEvidence,
|
||||
readCallbackEvidenceImpl = readCallbackEvidence,
|
||||
}) {
|
||||
let evidenceSource = "file";
|
||||
let events = [];
|
||||
let dbReadError = null;
|
||||
if (preferDb && prisma) {
|
||||
try {
|
||||
events = await readDbCallbackEvidenceImpl({ prisma, config });
|
||||
if (events.length) {
|
||||
evidenceSource = "db";
|
||||
}
|
||||
} catch (error) {
|
||||
dbReadError = safeMessage(error?.message || error);
|
||||
events = [];
|
||||
}
|
||||
}
|
||||
if (!events.length) {
|
||||
events = await readCallbackEvidenceImpl({
|
||||
evidenceFile: config.wecomApprovalCallbackEvidenceFile,
|
||||
});
|
||||
}
|
||||
const approvalEvents = (events || [])
|
||||
.filter((item) => item?.callbackKind === "approval_status_change")
|
||||
.map(sanitizeEvidenceEvent);
|
||||
const bySpNoPrefix = {};
|
||||
const byFullSpNo = {};
|
||||
for (const event of approvalEvents) {
|
||||
if (!event.spNoPrefix) continue;
|
||||
bySpNoPrefix[event.spNoPrefix] = bySpNoPrefix[event.spNoPrefix] || [];
|
||||
bySpNoPrefix[event.spNoPrefix].push(event);
|
||||
if (event.spNo) {
|
||||
byFullSpNo[event.spNo] = byFullSpNo[event.spNo] || [];
|
||||
byFullSpNo[event.spNo].push(event);
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: "callback_event_evidence",
|
||||
status: approvalEvents.length ? "OK" : "PENDING",
|
||||
source: evidenceSource,
|
||||
dbReadError,
|
||||
eventCount: approvalEvents.length,
|
||||
latestEvent: approvalEvents[approvalEvents.length - 1] || null,
|
||||
byFullSpNo,
|
||||
bySpNoPrefix,
|
||||
};
|
||||
}
|
||||
@@ -752,43 +869,46 @@ function compareCallbackEvidence(targetChecks, evidenceCheck) {
|
||||
const findings = [];
|
||||
const pulledDetails = collectPulledDetails(targetChecks);
|
||||
const evidenceBySpNo = evidenceCheck.bySpNoPrefix || {};
|
||||
const evidenceByFullSpNo = evidenceCheck.byFullSpNo || {};
|
||||
const pulledPrefixes = new Set();
|
||||
|
||||
for (const detail of pulledDetails) {
|
||||
if (!detail.spNoPrefix) continue;
|
||||
pulledPrefixes.add(detail.spNoPrefix);
|
||||
const events = evidenceBySpNo[detail.spNoPrefix] || [];
|
||||
const events = detail.spNo
|
||||
? evidenceByFullSpNo[detail.spNo] || evidenceBySpNo[detail.spNoPrefix] || []
|
||||
: evidenceBySpNo[detail.spNoPrefix] || [];
|
||||
if (!events.length) {
|
||||
findings.push({
|
||||
findings.push(attachHiddenValue({
|
||||
id: "callback_missing_for_pulled_detail",
|
||||
target: detail.target,
|
||||
spNoPrefix: detail.spNoPrefix,
|
||||
status: "FINDING",
|
||||
});
|
||||
}, "spNo", detail.spNo));
|
||||
continue;
|
||||
}
|
||||
if (events.length > 1) {
|
||||
findings.push({
|
||||
findings.push(attachHiddenValue({
|
||||
id: "callback_duplicate_for_pulled_detail",
|
||||
target: detail.target,
|
||||
spNoPrefix: detail.spNoPrefix,
|
||||
count: events.length,
|
||||
status: "FINDING",
|
||||
});
|
||||
}, "spNo", detail.spNo));
|
||||
}
|
||||
const hasStatusMatch = events.some((event) => (
|
||||
(event.spStatus && detail.spStatus && event.spStatus === detail.spStatus)
|
||||
|| (event.statusLabel && detail.statusLabel && event.statusLabel === detail.statusLabel)
|
||||
));
|
||||
if (!hasStatusMatch) {
|
||||
findings.push({
|
||||
findings.push(attachHiddenValue({
|
||||
id: "callback_status_mismatch",
|
||||
target: detail.target,
|
||||
spNoPrefix: detail.spNoPrefix,
|
||||
pulledStatus: detail.statusLabel || detail.spStatus,
|
||||
callbackStatus: events[0]?.statusLabel || events[0]?.spStatus || null,
|
||||
status: "FINDING",
|
||||
});
|
||||
}, "spNo", detail.spNo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -863,6 +983,132 @@ function collectMainBlockers(secretCheck, targetChecks) {
|
||||
return blockers;
|
||||
}
|
||||
|
||||
function buildArchivePersistencePlan({ options, env }) {
|
||||
if (!options.persistToDb) {
|
||||
return {
|
||||
id: "archive_persistence",
|
||||
status: "SKIPPED",
|
||||
enabled: false,
|
||||
reason: "default_no_oa_db_writes",
|
||||
writeBehavior: "disabled unless --persist-to-db and WECOM_ARCHIVE_WRITE=1 are both present",
|
||||
};
|
||||
}
|
||||
if (!options.live) {
|
||||
return {
|
||||
id: "archive_persistence",
|
||||
status: "NEEDS_CONFIG",
|
||||
enabled: false,
|
||||
reason: "live_read_required_for_archive_persistence",
|
||||
missing: ["--live"],
|
||||
};
|
||||
}
|
||||
if (env.WECOM_ARCHIVE_WRITE !== "1") {
|
||||
return {
|
||||
id: "archive_persistence",
|
||||
status: "NEEDS_CONFIG",
|
||||
enabled: false,
|
||||
reason: "missing_archive_write_switch",
|
||||
missing: ["WECOM_ARCHIVE_WRITE=1"],
|
||||
};
|
||||
}
|
||||
if (!env.DATABASE_URL) {
|
||||
return {
|
||||
id: "archive_persistence",
|
||||
status: "BLOCKED",
|
||||
enabled: false,
|
||||
reason: "database_url_missing",
|
||||
missing: ["DATABASE_URL"],
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: "archive_persistence",
|
||||
status: "READY",
|
||||
enabled: false,
|
||||
reason: null,
|
||||
writeBehavior: "OA_DB_ONLY; WeCom remains read-only",
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeArchivePersistence(targetChecks, archivePersistenceCheck) {
|
||||
if (!archivePersistenceCheck?.enabled) return archivePersistenceCheck;
|
||||
const details = collectPulledDetails(targetChecks);
|
||||
const stored = details.filter((item) => item.archivePersistence?.status === "STORED");
|
||||
const failed = details.filter((item) => item.archivePersistence?.status === "FAILED");
|
||||
return {
|
||||
...archivePersistenceCheck,
|
||||
status: failed.length ? "BLOCKED" : "OK",
|
||||
storedCount: stored.length,
|
||||
failedCount: failed.length,
|
||||
failures: failed.map((item) => ({
|
||||
target: item.target,
|
||||
spNoPrefix: item.spNoPrefix,
|
||||
reason: item.archivePersistence?.reason || "archive_persist_failed",
|
||||
message: item.archivePersistence?.message || null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async function recordCompareFindings({ prisma, runId, compareCheck, targetChecks }) {
|
||||
if (!prisma || !runId || !compareCheck?.findings?.length) return 0;
|
||||
const archiveBySpNo = new Map();
|
||||
const archiveByTargetPrefix = new Map();
|
||||
for (const detail of collectPulledDetails(targetChecks)) {
|
||||
if (detail.spNo && detail.archivePersistence?.archiveId) {
|
||||
archiveBySpNo.set(detail.spNo, detail.archivePersistence.archiveId);
|
||||
}
|
||||
if (detail.spNoPrefix && detail.archivePersistence?.archiveId) {
|
||||
archiveByTargetPrefix.set(`${detail.target}:${detail.spNoPrefix}`, detail.archivePersistence.archiveId);
|
||||
}
|
||||
}
|
||||
let recorded = 0;
|
||||
for (const finding of compareCheck.findings) {
|
||||
const archiveId = (finding.spNo ? archiveBySpNo.get(finding.spNo) : null)
|
||||
|| archiveByTargetPrefix.get(`${finding.target}:${finding.spNoPrefix}`)
|
||||
|| null;
|
||||
await recordReconcileFinding({
|
||||
prisma,
|
||||
runId,
|
||||
archiveId,
|
||||
findingType: finding.id,
|
||||
severity: finding.id === "callback_status_mismatch" ? "warning" : "info",
|
||||
source: "compensation_compare",
|
||||
description: finding.id,
|
||||
evidence: {
|
||||
...finding,
|
||||
spNoPrefix: finding.spNoPrefix || null,
|
||||
},
|
||||
});
|
||||
recorded += 1;
|
||||
}
|
||||
return recorded;
|
||||
}
|
||||
|
||||
function appendArchivePersistenceGate({ mainBlockers, configGaps, archivePersistenceCheck }) {
|
||||
if (!archivePersistenceCheck) return;
|
||||
if (archivePersistenceCheck.status === "NEEDS_CONFIG") {
|
||||
configGaps.push({
|
||||
id: archivePersistenceCheck.id,
|
||||
status: "NEEDS_CONFIG",
|
||||
reason: archivePersistenceCheck.reason,
|
||||
missing: archivePersistenceCheck.missing || [],
|
||||
});
|
||||
} else if (archivePersistenceCheck.status === "BLOCKED") {
|
||||
mainBlockers.push({
|
||||
id: archivePersistenceCheck.id,
|
||||
reason: archivePersistenceCheck.reason || "archive_persistence_blocked",
|
||||
missing: archivePersistenceCheck.missing || [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function countTargetPages(targetChecks) {
|
||||
return targetChecks.reduce((total, item) => total + (item.pageCount || 0), 0);
|
||||
}
|
||||
|
||||
function countPulledItems(targetChecks) {
|
||||
return targetChecks.reduce((total, item) => total + (item.spNoCount || 0), 0);
|
||||
}
|
||||
|
||||
function evaluateConclusion({ mainBlockers, configGaps, compareCheck }) {
|
||||
if (mainBlockers.length) {
|
||||
return { status: "SKIP", conclusion: "BLOCKED_BY_EXTERNAL_CONFIG" };
|
||||
@@ -884,6 +1130,13 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
const generatedAt = deps.generatedAt || new Date().toISOString();
|
||||
const config = deps.config || getConfig();
|
||||
const timeWindow = buildTimeWindow(options, generatedAt);
|
||||
let ownedPrisma = null;
|
||||
let ownedPool = null;
|
||||
let archivePersistence = { enabled: false, prisma: null, runId: null };
|
||||
let archivePersistenceCheck = buildArchivePersistencePlan({ options, env });
|
||||
let finishRunInput = null;
|
||||
|
||||
try {
|
||||
const inputs = buildInputs(options, env);
|
||||
const secretCheck = buildSecretCheck(config);
|
||||
const discoveryCheck = await discoverTemplateIds({
|
||||
@@ -896,6 +1149,35 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
queryApplicantActorImpl: deps.queryApplicantActorImpl || queryApplicantActorRowReadOnly,
|
||||
});
|
||||
const resolvedInputs = applyDiscoveredTemplateIds(inputs, discoveryCheck);
|
||||
if (archivePersistenceCheck.status === "READY") {
|
||||
try {
|
||||
let prisma = deps.prisma;
|
||||
if (!prisma) {
|
||||
const created = deps.createPrismaClientImpl ? deps.createPrismaClientImpl() : createPrismaClient();
|
||||
ownedPrisma = created.prisma;
|
||||
ownedPool = created.pool;
|
||||
prisma = ownedPrisma;
|
||||
}
|
||||
const run = await createCompensationRun({ prisma, timeWindow, inputs: resolvedInputs, options });
|
||||
archivePersistence = { enabled: true, prisma, runId: run.id };
|
||||
archivePersistenceCheck = {
|
||||
...archivePersistenceCheck,
|
||||
status: "OK",
|
||||
enabled: true,
|
||||
runId: run.id,
|
||||
startedAt: run.started_at ? new Date(run.started_at).toISOString() : null,
|
||||
};
|
||||
} catch (error) {
|
||||
archivePersistenceCheck = {
|
||||
...archivePersistenceCheck,
|
||||
status: "BLOCKED",
|
||||
enabled: false,
|
||||
reason: "archive_persistence_unavailable",
|
||||
message: safeMessage(error?.message || error),
|
||||
};
|
||||
archivePersistence = { enabled: false, prisma: null, runId: null };
|
||||
}
|
||||
}
|
||||
const targetChecks = await runTargetChecks({
|
||||
config,
|
||||
inputs: resolvedInputs,
|
||||
@@ -903,12 +1185,28 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
options,
|
||||
fetchApprovalInfoListImpl: deps.fetchApprovalInfoListImpl || fetchWecomApprovalInfoList,
|
||||
fetchApprovalDetailImpl: deps.fetchApprovalDetailImpl || fetchWecomApprovalDetail,
|
||||
archivePersistence,
|
||||
});
|
||||
archivePersistenceCheck = summarizeArchivePersistence(targetChecks, archivePersistenceCheck);
|
||||
const evidenceCheck = await buildEvidenceCheck({
|
||||
config,
|
||||
prisma: archivePersistence.prisma,
|
||||
preferDb: archivePersistence.enabled,
|
||||
readDbCallbackEvidenceImpl: deps.readDbCallbackEvidenceImpl,
|
||||
readCallbackEvidenceImpl: deps.readCallbackEvidenceImpl,
|
||||
});
|
||||
const compareCheck = compareCallbackEvidence(targetChecks, evidenceCheck);
|
||||
const recordedFindingCount = archivePersistence.enabled && archivePersistenceCheck.status === "OK"
|
||||
? await recordCompareFindings({
|
||||
prisma: archivePersistence.prisma,
|
||||
runId: archivePersistence.runId,
|
||||
compareCheck,
|
||||
targetChecks,
|
||||
})
|
||||
: 0;
|
||||
if (archivePersistenceCheck?.enabled) {
|
||||
archivePersistenceCheck.recordedFindingCount = recordedFindingCount;
|
||||
}
|
||||
const verificationGaps = options.live ? [] : [{
|
||||
id: "live_read",
|
||||
status: "DRY_RUN",
|
||||
@@ -922,7 +1220,22 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
});
|
||||
}
|
||||
const configGaps = collectConfigGaps(targetChecks, verificationGaps, evidenceCheck, compareCheck);
|
||||
appendArchivePersistenceGate({ mainBlockers, configGaps, archivePersistenceCheck });
|
||||
const conclusion = evaluateConclusion({ mainBlockers, configGaps, compareCheck });
|
||||
if (archivePersistence.enabled) {
|
||||
finishRunInput = {
|
||||
prisma: archivePersistence.prisma,
|
||||
runId: archivePersistence.runId,
|
||||
status: mainBlockers.length ? "blocked" : (configGaps.length ? "partial" : (compareCheck.findings.length ? "findings" : "completed")),
|
||||
pageCount: countTargetPages(targetChecks),
|
||||
itemCount: countPulledItems(targetChecks),
|
||||
detailCount: collectPulledDetails(targetChecks).length,
|
||||
findingCount: recordedFindingCount,
|
||||
errorCode: mainBlockers[0]?.reason || null,
|
||||
errorMessage: mainBlockers[0]?.reason || null,
|
||||
};
|
||||
await finishCompensationRun(finishRunInput);
|
||||
}
|
||||
|
||||
return {
|
||||
mode: "qa_only",
|
||||
@@ -933,11 +1246,14 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
scope: {
|
||||
issue: "WACP3",
|
||||
targets: APPROVAL_POC_TARGETS.map((item) => item.key),
|
||||
writeBehavior: "read-only only; no OA writes, no WeCom writes, no template creation, no approval submission",
|
||||
writeBehavior: options.persistToDb
|
||||
? "WeCom read-only; OA archive DB persistence requires --persist-to-db + WECOM_ARCHIVE_WRITE=1"
|
||||
: "read-only only; no OA writes, no WeCom writes, no template creation, no approval submission",
|
||||
liveRead: Boolean(options.live),
|
||||
autoDiscovery: Boolean(options.discoverTemplateIds),
|
||||
archivePersistenceRequested: Boolean(options.persistToDb),
|
||||
deepMirror: "not_started",
|
||||
nextStage: "WREAD1",
|
||||
nextStage: options.persistToDb ? "WARCH1_OFFLINE_ARCHIVE_READ" : "WREAD1_OR_WARCH1_PERSISTENCE",
|
||||
},
|
||||
timeWindow,
|
||||
inputs: {
|
||||
@@ -957,12 +1273,30 @@ async function executeCompensationPoc(options, deps = {}) {
|
||||
callbackEvidence: {
|
||||
...evidenceCheck,
|
||||
bySpNoPrefix: undefined,
|
||||
byFullSpNo: undefined,
|
||||
},
|
||||
callbackCompare: compareCheck,
|
||||
archivePersistence: archivePersistenceCheck,
|
||||
},
|
||||
configGaps,
|
||||
mainBlockers,
|
||||
};
|
||||
} finally {
|
||||
if (finishRunInput && archivePersistence.enabled && archivePersistenceCheck.status === "BLOCKED") {
|
||||
await finishCompensationRun({
|
||||
...finishRunInput,
|
||||
status: "blocked",
|
||||
errorCode: archivePersistenceCheck.reason || "archive_persistence_blocked",
|
||||
errorMessage: archivePersistenceCheck.reason || "archive_persistence_blocked",
|
||||
}).catch(() => null);
|
||||
}
|
||||
if (ownedPrisma?.$disconnect) {
|
||||
await ownedPrisma.$disconnect().catch(() => null);
|
||||
}
|
||||
if (ownedPool?.end) {
|
||||
await ownedPool.end().catch(() => null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureReportDir(reportDir) {
|
||||
@@ -1019,6 +1353,7 @@ module.exports = {
|
||||
discoverTemplateIds,
|
||||
evaluateConclusion,
|
||||
executeCompensationPoc,
|
||||
buildArchivePersistencePlan,
|
||||
parseArgs,
|
||||
statusLabelFromSpStatus,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户