#!/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), })); } })();