2026-05-20 21:39:12 +08:00

390 lines
15 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
'use strict';
const fs = require('fs');
function emit(stage, message, status = 'running', extra = {}) {
process.stderr.write(JSON.stringify({
type: 'process_event',
stage,
message,
status,
timestamp: new Date().toISOString(),
...extra,
}) + '\n');
}
function readInput() {
const raw = String(process.argv[2] || process.env.SKILL_INPUT || process.env.AIFLOW_SKILL_INPUT || fs.readFileSync(0, 'utf8')).trim();
return raw ? JSON.parse(raw) : {};
}
function str(value) {
if (value === undefined || value === null || value === '') return '-';
return String(value).replace(/\|/g, '\\|').replace(/\r?\n/g, '<br>');
}
function raw(value) {
if (value === undefined || value === null || value === '') return '';
return String(value);
}
function hasText(value) {
return value !== undefined && value !== null && String(value).trim() !== '' && String(value).trim() !== 'null';
}
function normalizeStatus(status) {
const text = raw(status).trim().replace(/[-\s]/g, '_').toLowerCase();
if (['pass', 'passed', 'approved', 'approve', 'success', 'ok', '审核通过', '通过'].includes(text)) return 'PASS';
if (['reject', 'rejected', 'deny', 'denied', 'refuse', 'refused', '驳回', '拒绝', '不通过'].includes(text)) return 'REJECT';
if (['manual_review', 'manual', 'review', '人工', '转人工', '人工复核', '人工复审'].includes(text)) return 'MANUAL_REVIEW';
if (['failed', 'fail', 'failure', 'error', '异常', '失败'].includes(text)) return 'MANUAL_REVIEW';
return '';
}
function inferStatus(audit) {
const normalized = normalizeStatus(audit.status || audit.decision || audit.auditDecision || audit.result);
if (normalized) return normalized;
const rules = Array.isArray(audit.rules) ? audit.rules : [];
const fieldChecks = Array.isArray(audit.fieldChecks) ? audit.fieldChecks : [];
if (rules.some(r => normalizeStatus(r.status) === 'REJECT')) return 'REJECT';
if (rules.some(r => normalizeStatus(r.status) === 'MANUAL_REVIEW')) return 'MANUAL_REVIEW';
if (rules.length && rules.every(r => normalizeStatus(r.status) === 'PASS')) return 'PASS';
if (fieldChecks.some(r => normalizeRuleStatusForReport(r.result) === 'REJECT')) return 'REJECT';
if (fieldChecks.some(r => {
const result = normalizeRuleStatusForReport(r.result);
return result === 'MANUAL_REVIEW' || result === 'REVIEW';
})) return 'MANUAL_REVIEW';
if (fieldChecks.length && fieldChecks.every(r => {
const result = normalizeRuleStatusForReport(r.result);
return result === 'PASS' || result === 'SKIP';
})) return 'PASS';
const reason = raw(audit.reason || audit.summary);
if (/审核通过|建议通过|通过/.test(reason)) return 'PASS';
if (/驳回|拒绝|不通过/.test(reason)) return 'REJECT';
if (/转人工|人工复核|人工复审/.test(reason)) return 'MANUAL_REVIEW';
return 'MANUAL_REVIEW';
}
function formatLocalGeneratedAt(value) {
const explicit = raw(value);
if (explicit) return explicit.replace(/T(\d{2}:\d{2}:\d{2})(?:\.\d{3})?Z$/, ' $1');
const date = new Date();
const parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).formatToParts(date).reduce((acc, part) => {
acc[part.type] = part.value;
return acc;
}, {});
return `${parts.year}-${parts.month}-${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`;
}
function businessText(value) {
return raw(value).replace(/(公户车检查|购买方名称比对|购车时间超期|身份证比对|发票比对|车辆类型判断|PlusC车龄限制):/g, '$1');
}
function statusLabel(status) {
status = normalizeStatus(status) || status;
if (status === 'PASS') return '通过';
if (status === 'REJECT') return '驳回';
if (status === 'MANUAL_REVIEW') return '转人工';
return status || '未知';
}
function statusIcon(status) {
status = normalizeStatus(status) || status;
if (status === 'PASS') return 'PASS';
if (status === 'REJECT') return 'REJECT';
if (status === 'MANUAL_REVIEW') return 'MANUAL';
return 'MANUAL';
}
function ruleStatus(status) {
status = normalizeStatus(status) || status;
if (status === 'PASS') return '通过';
if (status === 'REJECT') return '驳回';
if (status === 'MANUAL_REVIEW') return '转人工';
return str(status);
}
function firstValue(obj, keys) {
if (!obj || typeof obj !== 'object') return '';
for (const key of keys) {
const value = obj[key];
if (hasText(value)) return value;
}
return '';
}
const EVIDENCE_LABELS = {
carHost: '车主类型',
buyerName: '发票购买方',
companyNames: '疑似企业名称',
orderName: '订单客户姓名',
invoiceBuyerName: '发票购买方',
invoiceIssueDate: '发票开票日期',
orderPurchaseDate: '订单购车日期',
referenceDate: '审核基准日期',
maxPurchaseAgeDays: '允许最长间隔',
ageDays: '实际间隔天数',
productName: '产品名称',
packageType: '权益套餐类型',
isPlusC: '是否PlusC/代步权益',
vehicleAgeStartSource: '车龄起算字段',
vehicleAgeStartDate: '车龄起算日期',
plusCMaxVehicleAgeYears: 'PlusC允许最长车龄',
vehicleAgeDays: '车辆实际车龄天数',
vehicleAgeMonths: '车辆实际车龄月数',
vehicleAgeYears: '车辆实际车龄年数',
ocrName: '身份证OCR姓名',
orderIdNumber: '订单身份证号',
ocrIdNumber: '身份证OCR号码',
orderVin: '订单VIN/车架号',
invoiceVin: '发票VIN/车架号',
orderEngineNo: '订单发动机号/电机号',
invoiceEngineNo: '发票发动机号/电机号',
orderModel: '订单车型/产品',
invoiceBrandModel: '发票厂牌型号',
sellerName: '销货单位',
vehicleType: '发票车辆类型',
brandModel: '发票厂牌型号',
invoiceTotalAmount: '发票价税合计',
orderVehiclePrice: '订单车辆价格',
};
function evidenceLabel(key) {
return EVIDENCE_LABELS[key] || key;
}
function evidenceValue(key, value) {
if (value === undefined || value === null || value === '') return '';
if (Array.isArray(value)) return value.filter(v => v !== undefined && v !== null && String(v) !== '').join('、');
if (key === 'carHost') {
const text = String(value).trim();
if (text === '0' || text === '个人' || text.toLowerCase() === 'false') return '个人';
if (text === '1') return '企业/公户';
return text;
}
if (key === 'maxPurchaseAgeDays') return `${value}`;
if (key === 'ageDays') return `${value}`;
if (key === 'isPlusC') return value ? '是' : '否';
if (key === 'packageType') return String(value) === '3' ? 'PlusC/代步权益' : value;
if (key === 'plusCMaxVehicleAgeYears') return `${value}`;
if (key === 'vehicleAgeDays') return `${value}`;
if (key === 'vehicleAgeMonths') return `${value}个月`;
if (key === 'vehicleAgeYears') return `${value}`;
if (key === 'vehicleAgeStartSource') {
const labels = {
firstRegistrationDate: '首次注册日期',
orderPurchaseDate: '订单购车日期',
invoiceIssueDate: '发票开票日期',
};
return labels[value] || value;
}
return value;
}
function renderEvidence(evidence) {
if (!evidence || typeof evidence !== 'object' || !Object.keys(evidence).length) return '-';
return Object.entries(evidence)
.map(([key, value]) => [evidenceLabel(key), evidenceValue(key, value)])
.filter(([, value]) => value !== undefined && value !== null && String(value) !== '')
.map(([label, value]) => `${label}${Array.isArray(value) ? value.join('、') : value}`)
.join('<br>') || '-';
}
function renderOrderTable(audit, order) {
const n = audit.normalized || {};
const rows = [
['订单号', firstValue(order, ['orderNo', 'orderNum']) || n.orderNo],
['业务单号', firstValue(order, ['businessNo', 'orderBusinessNo'])],
['客户姓名', n.orderName || firstValue(order, ['cardName', 'ownerName', 'customerName'])],
['身份证号', n.orderIdNumber || firstValue(order, ['cardNumber', 'idNumber'])],
['VIN/车架号', n.orderVin || firstValue(order, ['carFrame', 'vin'])],
['车辆类型', n.vehicleType || firstValue(order, ['vehicleType', 'carType'])],
['厂牌型号', n.brandModel || firstValue(order, ['modelName', 'carModel'])],
['计划号', firstValue(order, ['productNo', 'planNo', 'seresPlanNo'])],
['产品名称', firstValue(order, ['productName', 'goodsName'])],
['门店名称', firstValue(order, ['factroyName', 'factoryName', 'dealerName'])],
['车牌号', firstValue(order, ['carPlate', 'plateNo'])],
['购车日期', firstValue(order, ['carPurchaseTime', 'purchaseTime', 'purchaseDate'])],
['里程', firstValue(order, ['mileage', 'kilometers'])],
['车辆价格', firstValue(order, ['vehiclePrice'])],
['车主类型', firstValue(order, ['carHost'])],
['保单号', firstValue(order, ['policyNo', 'policyno', 'commercialInsuranceNo', 'seresInsuranceNo'])],
['保险公司', firstValue(order, ['insuranceCompany'])],
['商业险日期', firstValue(order, ['commercialInsuranceDate', 'effectiveDate', 'seresEffectiveDate'])],
['商业险URL', firstValue(order, ['commercialInsuranceUrl', 'pingAnUrl'])],
['附件模式', firstValue(order, ['seresAttachmentMode', 'attachmentExpectation'])],
['发票URL', firstValue(order, ['billUrl', 'carBill'])],
['身份证URL', firstValue(order, ['cardUrl'])],
['发票日期', n.invoiceIssueDate || firstValue(order, ['invoiceIssueDate', 'seresInvoiceDate'])],
].filter(([, v]) => hasText(v));
return [
'| 项目 | 内容 |',
'| --- | --- |',
...rows.map(([k, v]) => `| ${str(k)} | ${str(v)} |`),
].join('\n');
}
function normalizeRuleStatusForReport(status) {
const normalized = normalizeStatus(status);
if (normalized === 'PASS') return 'PASS';
if (normalized === 'REJECT') return 'REJECT';
if (normalized === 'MANUAL_REVIEW') return 'MANUAL_REVIEW';
const text = raw(status).trim().toUpperCase();
if (['SKIP', 'IGNORED', 'N_A', 'NA', 'NOT_APPLICABLE'].includes(text)) return 'SKIP';
return text || '';
}
function renderRulesTable(rules, fieldChecks) {
if (Array.isArray(rules) && rules.length > 0) {
return [
'| 序号 | 规则 | 结论 | 说明 | 证据 |',
'| --- | --- | --- | --- | --- |',
...rules.map((r, idx) => `| ${idx + 1} | ${str(r.name || r.id)} | ${ruleStatus(r.status)} | ${str(businessText(r.reason))} | ${renderEvidence(r.evidence)} |`),
].join('\n');
}
if (Array.isArray(fieldChecks) && fieldChecks.length > 0) {
return [
'| 序号 | 字段 | 期望 | 实际 | 结论 | 说明 |',
'| --- | --- | --- | --- | --- | --- |',
...fieldChecks.map((check, idx) => {
const result = normalizeRuleStatusForReport(check.result);
const statusText =
result === 'PASS' ? '通过'
: result === 'REJECT' ? '驳回'
: result === 'MANUAL_REVIEW' ? '转人工'
: result === 'SKIP' ? '不适用'
: str(check.result);
return `| ${idx + 1} | ${str(check.field)} | ${str(check.expected)} | ${str(check.actual)} | ${statusText} | ${str(check.message)} |`;
}),
].join('\n');
}
return [
'暂无独立规则明细;本次报告按结构化订单数据生成综合结论。',
].join('\n');
}
function findRule(rules, id) {
return (rules || []).find(r => r.id === id) || {};
}
function renderCompareSection(audit) {
const rules = audit.rules || [];
const idRule = findRule(rules, 'R4_ID_CARD');
const invoiceRule = findRule(rules, 'R5_INVOICE');
const id = idRule.evidence || {};
const inv = invoiceRule.evidence || {};
const sections = [];
if ([id.orderName, id.ocrName, id.orderIdNumber, id.ocrIdNumber].some(hasText)) {
sections.push([
'## 身份证比对详情',
'',
'| 字段 | 订单录入 | OCR识别 |',
'| --- | --- | --- |',
`| 姓名 | ${str(id.orderName)} | ${str(id.ocrName)} |`,
`| 身份证号 | ${str(id.orderIdNumber)} | ${str(id.ocrIdNumber)} |`,
].join('\n'));
}
if ([inv.orderVin, inv.invoiceVin, inv.orderEngineNo, inv.invoiceEngineNo, inv.orderModel, inv.invoiceBrandModel, inv.sellerName].some(hasText)) {
sections.push([
'## 发票比对详情',
'',
'| 字段 | 订单录入 | 发票OCR |',
'| --- | --- | --- |',
`| VIN/车架号 | ${str(inv.orderVin)} | ${str(inv.invoiceVin)} |`,
`| 发动机号 | ${str(inv.orderEngineNo)} | ${str(inv.invoiceEngineNo)} |`,
`| 车型/产品 | ${str(inv.orderModel)} | ${str(inv.invoiceBrandModel)} |`,
`| 销货单位 | - | ${str(inv.sellerName)} |`,
].join('\n'));
}
return sections.join('\n\n');
}
function renderMarkdown(input) {
const audit = input.auditResult;
if (!audit || typeof audit !== 'object') throw new Error('auditResult is required');
const companyName = raw(input.companyName) || '车主权益管理系统';
const generatedAt = formatLocalGeneratedAt(input.generatedAt);
const logo = raw(input.logoUrl);
const status = inferStatus(audit);
const score = audit.score ?? '-';
const order = input.order || {};
const compareSection = renderCompareSection(audit);
const summaryText = raw(audit.summary || audit.reason || '').trim();
const rules = Array.isArray(audit.rules) ? audit.rules : [];
const fieldChecks = Array.isArray(audit.fieldChecks) ? audit.fieldChecks : [];
const failedCount = rules.length
? rules.filter(r => normalizeRuleStatusForReport(r.status) !== 'PASS').length
: fieldChecks.filter(r => {
const result = normalizeRuleStatusForReport(r.result);
return result === 'REJECT' || result === 'MANUAL_REVIEW';
}).length;
const totalCount = rules.length || fieldChecks.length;
const title = logo
? `<p align="center"><img src="${logo}" alt="${companyName}" height="48"></p>\n\n# ${companyName}投保审核报告`
: `# ${companyName}投保审核报告`;
const summary = summaryText || (failedCount > 0
? `${failedCount}项规则未通过,存在需驳回或人工复核事项。`
: `${totalCount}项规则均通过,未发现需驳回或人工复核事项。`);
return [
title,
'',
`> 生成时间:${generatedAt}`,
'',
'## 审核结论',
'',
`| 结论 | 评分 | 结论码 | 主要原因 |`,
`| --- | ---: | --- | --- |`,
`| ${statusLabel(status)} | ${score} | ${statusIcon(status)} | ${str(businessText(audit.reason) || summary)} |`,
'',
'## 订单信息',
'',
renderOrderTable(audit, order),
'',
'## 规则校验',
'',
renderRulesTable(rules, fieldChecks),
'',
compareSection,
compareSection ? '' : null,
'## 综合摘要',
'',
`${str(businessText(summary))}`,
'',
'> 本报告由AI审核流程自动生成结论基于订单录入数据及已提供的结构化/OCR结果。转人工项需以业务人员复核结果为准。',
'',
].filter(line => line !== null).join('\n');
}
(async () => {
try {
const input = readInput();
emit('render', '生成Markdown审核报告');
const reportMarkdown = renderMarkdown(input);
const audit = input.auditResult || {};
emit('complete', '审核报告生成完成', 'completed');
process.stdout.write(JSON.stringify({
success: true,
reportMarkdown,
summary: businessText(audit.reason || audit.summary || ''),
status: inferStatus(audit),
score: audit.score,
}));
} catch (err) {
process.stdout.write(JSON.stringify({
success: false,
error: err && err.message ? err.message : String(err),
}));
}
})();