390 lines
15 KiB
JavaScript
390 lines
15 KiB
JavaScript
#!/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),
|
||
}));
|
||
}
|
||
})();
|