完善 readiness、发布追溯与敏感治理(07-15-production-trust-closeout-cleanup)

这个提交包含在:
2026-07-16 05:17:38 +08:00
父节点 abb6a22f5c
当前提交 28e8e746b7
共修改 29 个文件,包含 1849 行新增和 233 行删除
+116 -65
查看文件
@@ -1,78 +1,129 @@
#!/usr/bin/env node
import path from "node:path";
import process from "node:process";
import { PrismaClient } from "../../server/node_modules/@prisma/client/index.js";
import { fileURLToPath } from "node:url";
function parseThresholds() {
const arg = process.argv.find((item) => item.startsWith("--thresholds="));
export function parseThresholds(argv = process.argv.slice(2)) {
const arg = argv.find((item) => item.startsWith("--thresholds="));
const raw = arg ? arg.slice("--thresholds=".length) : "2,5,10";
return raw
.split(",")
.map((item) => Number(item.trim()))
.filter((item) => Number.isFinite(item) && item > 0)
.sort((a, b) => a - b);
return Array.from(new Set(
raw
.split(",")
.map((item) => Number(item.trim()))
.filter((item) => Number.isFinite(item) && item > 0)
.map((item) => Math.floor(item)),
)).sort((a, b) => a - b);
}
function minutesAgo(minutes) {
return new Date(Date.now() - minutes * 60_000);
function minutesAgo(minutes, now) {
return new Date(now.getTime() - minutes * 60_000);
}
export async function buildPendingOcrSummary({
databaseReady,
thresholds,
countPending,
now = new Date(),
}) {
if (!databaseReady) {
return {
mode: "pending-ocr-readiness",
status: "SKIP",
findingCount: 1,
findings: [{ code: "database_not_configured", severity: "manual_or_config" }],
thresholds: [],
checkedAt: now.toISOString(),
};
}
const thresholdResults = [];
for (const thresholdMinutes of thresholds) {
const count = Number(await countPending({
status: "pending_ocr",
created_at: { lte: minutesAgo(thresholdMinutes, now) },
}));
thresholdResults.push({
thresholdMinutes,
count: Number.isFinite(count) && count > 0 ? Math.floor(count) : 0,
});
}
const findingCount = thresholdResults.reduce((maximum, item) => Math.max(maximum, item.count), 0);
return {
mode: "pending-ocr-readiness",
status: findingCount > 0 ? "PASS_WITH_FINDINGS" : "PASS",
findingCount,
findings: findingCount > 0
? [{ code: "pending_ocr_detected", severity: "finding", count: findingCount }]
: [],
thresholds: thresholdResults,
checkedAt: now.toISOString(),
};
}
export async function executePendingOcrCheck({
databaseReady = Boolean(process.env.DATABASE_URL?.trim()),
thresholds = parseThresholds(),
} = {}) {
if (!databaseReady) {
return buildPendingOcrSummary({ databaseReady: false, thresholds, countPending: async () => 0 });
}
const prismaClientModule = await import("../../server/src/prismaClient.js");
const createPrismaClient = prismaClientModule.createPrismaClient
?? prismaClientModule.default?.createPrismaClient;
if (typeof createPrismaClient !== "function") {
throw new Error("Prisma client factory is unavailable");
}
const { prisma, pool } = createPrismaClient();
try {
return await buildPendingOcrSummary({
databaseReady: true,
thresholds,
countPending: (where) => prisma.invoicePool.count({ where }),
});
} finally {
try {
await prisma.$disconnect();
} finally {
await pool.end();
}
}
}
function printSummary(summary, jsonOnly) {
if (jsonOnly) {
console.log(JSON.stringify(summary, null, 2));
return;
}
console.log(`[pending-ocr][${summary.status}] findings=${summary.findingCount}`);
for (const item of summary.thresholds) {
console.log(`[pending-ocr] thresholdMinutes=${item.thresholdMinutes} count=${item.count}`);
}
}
async function main() {
if (!process.env.DATABASE_URL) {
console.log("[pending-ocr][SKIP] missing DATABASE_URL");
return;
}
const thresholds = parseThresholds();
const prisma = new PrismaClient();
const jsonOnly = process.argv.includes("--json");
try {
const summary = [];
for (const minutes of thresholds) {
const where = {
status: "pending_ocr",
created_at: { lte: minutesAgo(minutes) },
};
const count = await prisma.invoicePool.count({ where });
const rows = await prisma.invoicePool.findMany({
where,
orderBy: { created_at: "asc" },
take: 50,
select: {
id: true,
created_at: true,
updated_at: true,
ocr_attempts: true,
ocr_last_requested_at: true,
ocr_last_error: true,
ocr_locked_at: true,
ocr_lock_owner: true,
},
});
summary.push({
thresholdMinutes: minutes,
count,
sampleLimit: 50,
items: rows.map((item) => ({
id: item.id,
createdAt: item.created_at?.toISOString?.() ?? null,
updatedAt: item.updated_at?.toISOString?.() ?? null,
attempts: item.ocr_attempts ?? 0,
lastRequestedAt: item.ocr_last_requested_at?.toISOString?.() ?? null,
lastError: item.ocr_last_error ?? null,
lockedAt: item.ocr_locked_at?.toISOString?.() ?? null,
lockOwner: item.ocr_lock_owner ?? null,
})),
});
}
console.log(JSON.stringify({ ok: true, thresholds: summary }, null, 2));
} finally {
await prisma.$disconnect();
const summary = await executePendingOcrCheck();
printSummary(summary, jsonOnly);
} catch {
const summary = {
mode: "pending-ocr-readiness",
status: "FAIL",
findingCount: 1,
findings: [{ code: "pending_ocr_check_failed", severity: "blocker" }],
thresholds: [],
checkedAt: new Date().toISOString(),
};
printSummary(summary, jsonOnly);
console.error("[pending-ocr][FAIL] database check failed");
process.exitCode = 1;
}
}
main().catch((error) => {
console.error(`[pending-ocr][FAIL] ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
});
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
await main();
}