2baf29b3ae
Deliver encrypted health zone, observation API, baseline maturity pipeline, page-data bindings, and H5/WeChat channel integration for health P0. Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
4.0 KiB
JavaScript
120 lines
4.0 KiB
JavaScript
export function buildHealthDocumentOcrPrompt() {
|
||
return [
|
||
'你是医疗报告 OCR 助手。从体检单/化验单/影像报告照片中提取结构化信息。',
|
||
'禁止诊断,只提取可见文字与数值。',
|
||
'只输出严格 JSON,不要 Markdown:',
|
||
'{"doc_type":"checkup|lab|imaging|prescription|discharge|other","report_date":"YYYY-MM-DD|null","institution":"","title":"","metrics":[{"name":"","value":"","unit":"","flag":""}],"summary":"","confidence":0.0}',
|
||
'无法识别时 metrics 为空数组,summary 说明原因。',
|
||
].join('\n');
|
||
}
|
||
|
||
export function parseHealthDocumentOcrJson(raw) {
|
||
const text = Array.isArray(raw)
|
||
? raw.map((item) => (typeof item === 'string' ? item : item?.text ?? '')).join('')
|
||
: String(raw ?? '');
|
||
const unfenced = text.trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '');
|
||
const start = unfenced.indexOf('{');
|
||
const end = unfenced.lastIndexOf('}');
|
||
if (start < 0 || end <= start) return null;
|
||
try {
|
||
return JSON.parse(unfenced.slice(start, end + 1));
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function normalizeHealthDocumentOcr(parsed = {}) {
|
||
const docType = String(parsed.doc_type ?? 'other').trim() || 'other';
|
||
const metrics = Array.isArray(parsed.metrics)
|
||
? parsed.metrics
|
||
.filter((item) => item && (item.name || item.value))
|
||
.slice(0, 30)
|
||
.map((item) => ({
|
||
name: String(item.name ?? '').trim(),
|
||
value: String(item.value ?? '').trim(),
|
||
unit: String(item.unit ?? '').trim() || null,
|
||
flag: String(item.flag ?? '').trim() || null,
|
||
}))
|
||
: [];
|
||
const summary = String(parsed.summary ?? '').trim();
|
||
const confidence = Number(parsed.confidence ?? 0);
|
||
const hasSignal = metrics.length > 0 || summary.length > 20;
|
||
return {
|
||
ok: hasSignal,
|
||
docType,
|
||
reportDate: parsed.report_date ?? null,
|
||
institution: String(parsed.institution ?? '').trim() || null,
|
||
title: String(parsed.title ?? '').trim() || '健康报告',
|
||
extractedMetrics: metrics,
|
||
ocrText: summary,
|
||
extractionStatus: metrics.length > 0 ? 'partial' : summary ? 'partial' : 'failed',
|
||
confidence: Number.isFinite(confidence) ? confidence : null,
|
||
};
|
||
}
|
||
|
||
export async function extractHealthDocumentFromVision({
|
||
buffer,
|
||
mimeType = 'image/jpeg',
|
||
analyzeImagesWithVision,
|
||
buildVisionThumbnailBuffer = null,
|
||
} = {}) {
|
||
if (!analyzeImagesWithVision || !Buffer.isBuffer(buffer) || !buffer.length) {
|
||
return { ok: false, error: 'vision_unavailable', message: '报告 OCR 暂不可用' };
|
||
}
|
||
|
||
let visionBuffer = buffer;
|
||
let visionMimeType = mimeType;
|
||
if (buildVisionThumbnailBuffer) {
|
||
try {
|
||
visionBuffer = await buildVisionThumbnailBuffer(buffer, mimeType);
|
||
visionMimeType = 'image/jpeg';
|
||
} catch {
|
||
visionBuffer = buffer;
|
||
visionMimeType = mimeType;
|
||
}
|
||
}
|
||
|
||
let raw;
|
||
try {
|
||
raw = await analyzeImagesWithVision(
|
||
[{
|
||
mimeType,
|
||
visionMimeType,
|
||
data: visionBuffer.toString('base64'),
|
||
}],
|
||
buildHealthDocumentOcrPrompt(),
|
||
);
|
||
} catch (error) {
|
||
return {
|
||
ok: false,
|
||
error: 'vision_failed',
|
||
message: error instanceof Error ? error.message : '报告 OCR 失败',
|
||
};
|
||
}
|
||
|
||
const parsed = parseHealthDocumentOcrJson(raw);
|
||
if (!parsed) {
|
||
return { ok: false, error: 'invalid_response', message: '未能解析报告内容' };
|
||
}
|
||
const normalized = normalizeHealthDocumentOcr(parsed);
|
||
if (!normalized.ok) {
|
||
return {
|
||
ok: false,
|
||
error: 'no_content',
|
||
message: '未能从报告中提取有效文字,原图已归档,关键数值请手输确认。',
|
||
};
|
||
}
|
||
return { ok: true, ...normalized };
|
||
}
|
||
|
||
export async function extractHealthDocumentFromUrl(options = {}) {
|
||
const { fetchHealthImageBuffer } = await import('./health-image-extract.mjs');
|
||
const { buffer, mimeType } = await fetchHealthImageBuffer(options);
|
||
return extractHealthDocumentFromVision({
|
||
buffer,
|
||
mimeType,
|
||
analyzeImagesWithVision: options.analyzeImagesWithVision,
|
||
buildVisionThumbnailBuffer: options.buildVisionThumbnailBuffer,
|
||
});
|
||
}
|