168 lines
5.5 KiB
JavaScript
168 lines
5.5 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();
|
|||
|
|
if (!raw) return {};
|
|||
|
|
return JSON.parse(raw);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function normalizeSide(side) {
|
|||
|
|
const value = String(side || 'front').toLowerCase();
|
|||
|
|
if (['front', 'back'].includes(value)) return value;
|
|||
|
|
if (['正面', '人像面'].includes(value)) return 'front';
|
|||
|
|
if (['反面', '国徽面'].includes(value)) return 'back';
|
|||
|
|
return 'front';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function normalizeDate(value) {
|
|||
|
|
const text = String(value || '').trim();
|
|||
|
|
if (!text) return '';
|
|||
|
|
const compact = text.replace(/[.年/]/g, '-').replace(/月/g, '-').replace(/日/g, '').replace(/\s+/g, '');
|
|||
|
|
const match = compact.match(/^(\d{4})-?(\d{1,2})-?(\d{1,2})$/);
|
|||
|
|
if (!match) return text;
|
|||
|
|
const [, y, m, d] = match;
|
|||
|
|
return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function parseJsonText(value) {
|
|||
|
|
if (!value) return null;
|
|||
|
|
if (typeof value === 'object') return value;
|
|||
|
|
const text = String(value).trim();
|
|||
|
|
if (!text) return null;
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(text);
|
|||
|
|
} catch {
|
|||
|
|
const match = text.match(/\{[\s\S]*\}/);
|
|||
|
|
if (!match) return null;
|
|||
|
|
try {
|
|||
|
|
return JSON.parse(match[0]);
|
|||
|
|
} catch {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function firstNonEmpty(...values) {
|
|||
|
|
for (const value of values) {
|
|||
|
|
if (value !== undefined && value !== null && value !== '') return value;
|
|||
|
|
}
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function readField(source, names) {
|
|||
|
|
if (!source || typeof source !== 'object') return '';
|
|||
|
|
for (const name of names) {
|
|||
|
|
const value = source[name];
|
|||
|
|
if (value === undefined || value === null || value === '') continue;
|
|||
|
|
if (typeof value === 'object') {
|
|||
|
|
return firstNonEmpty(value.words, value.word, value.value, value.text);
|
|||
|
|
}
|
|||
|
|
return String(value).trim();
|
|||
|
|
}
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getCandidate(input) {
|
|||
|
|
return parseJsonText(input.modelResult)
|
|||
|
|
|| parseJsonText(input.ocrResult)
|
|||
|
|
|| parseJsonText(input.mockResult)
|
|||
|
|
|| parseJsonText(input.result)
|
|||
|
|
|| (input.fields ? { fields: input.fields } : null);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function normalizeIdCard(candidate, side) {
|
|||
|
|
const root = candidate && typeof candidate === 'object' ? candidate : {};
|
|||
|
|
const fields = root.fields && typeof root.fields === 'object' ? root.fields : root;
|
|||
|
|
const words = root.words_result && typeof root.words_result === 'object' ? root.words_result : fields;
|
|||
|
|
|
|||
|
|
const normalizedFields = {
|
|||
|
|
name: readField(words, ['name', '姓名']),
|
|||
|
|
idNumber: readField(words, ['idNumber', 'idNo', 'cardNumber', '公民身份号码', '身份号码', '身份证号', '证件号码']),
|
|||
|
|
gender: readField(words, ['gender', 'sex', '性别']),
|
|||
|
|
ethnicity: readField(words, ['ethnicity', 'nation', '民族']),
|
|||
|
|
birthDate: normalizeDate(readField(words, ['birthDate', 'birthday', '出生', '出生日期'])),
|
|||
|
|
address: readField(words, ['address', '住址', '地址']),
|
|||
|
|
issueAuthority: readField(words, ['issueAuthority', 'authority', '签发机关', '签发单位']),
|
|||
|
|
validFrom: normalizeDate(readField(words, ['validFrom', 'validStart', '有效期开始', '签发日期'])),
|
|||
|
|
validTo: readField(words, ['validTo', 'validEnd', '有效期结束', '失效日期']) || readField(words, ['有效期限', '有效期']),
|
|||
|
|
};
|
|||
|
|
if (normalizedFields.validTo && normalizedFields.validTo !== '长期') {
|
|||
|
|
normalizedFields.validTo = normalizeDate(normalizedFields.validTo);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: true,
|
|||
|
|
side: root.side || side,
|
|||
|
|
fields: normalizedFields,
|
|||
|
|
quality: root.quality || {},
|
|||
|
|
risk: root.risk || {},
|
|||
|
|
raw: root,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildPrompt(input, side) {
|
|||
|
|
return [
|
|||
|
|
'请使用多模态大模型识别中国居民身份证图片,只输出合法JSON,不输出解释。',
|
|||
|
|
'无法确认的字段输出空字符串,不要猜测。',
|
|||
|
|
'日期统一 YYYY-MM-DD;有效期长期输出“长期”。',
|
|||
|
|
`当前识别面:${side === 'back' ? '反面/国徽面' : '正面/人像面'}。`,
|
|||
|
|
'输出schema:{"fields":{"name":"","idNumber":"","gender":"","ethnicity":"","birthDate":"","address":"","issueAuthority":"","validFrom":"","validTo":""},"quality":{},"risk":{}}',
|
|||
|
|
input.imageUrl ? `图片URL:${input.imageUrl}` : '',
|
|||
|
|
input.imageBase64 ? '已提供 imageBase64,请直接看图识别。' : '',
|
|||
|
|
].filter(Boolean).join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async function run(input) {
|
|||
|
|
const side = normalizeSide(input.side);
|
|||
|
|
const candidate = getCandidate(input);
|
|||
|
|
if (candidate) {
|
|||
|
|
emit('normalize', '归一化多模态身份证识别结果', 'completed');
|
|||
|
|
return normalizeIdCard(candidate, side);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
success: false,
|
|||
|
|
needsModelVision: true,
|
|||
|
|
side,
|
|||
|
|
error: '未提供多模态模型识别结果。请先让多模态模型查看 imageUrl/imageBase64,并把模型返回JSON作为 modelResult 传入本skill。',
|
|||
|
|
prompt: buildPrompt(input, side),
|
|||
|
|
fields: {
|
|||
|
|
name: '',
|
|||
|
|
idNumber: '',
|
|||
|
|
gender: '',
|
|||
|
|
ethnicity: '',
|
|||
|
|
birthDate: '',
|
|||
|
|
address: '',
|
|||
|
|
issueAuthority: '',
|
|||
|
|
validFrom: '',
|
|||
|
|
validTo: '',
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
(async () => {
|
|||
|
|
try {
|
|||
|
|
const input = readInput();
|
|||
|
|
process.stdout.write(JSON.stringify(await run(input)));
|
|||
|
|
} catch (err) {
|
|||
|
|
process.stdout.write(JSON.stringify({
|
|||
|
|
success: false,
|
|||
|
|
error: err && err.message ? err.message : String(err),
|
|||
|
|
}));
|
|||
|
|
}
|
|||
|
|
})();
|