import { validateMetricValue } from './health-observation-validate.mjs'; import { assertSymptomRecord, HEALTH_SYMPTOM_LABELS, normalizeSymptomCode } from './health-symptoms.mjs'; const METRIC_SET_LABELS = Object.freeze({ blood_pressure: '血压', heart_rate: '心率', weight: '体重', spo2: '血氧', temperature: '体温', sleep: '睡眠', symptom: '症状', }); const METRIC_VALUE_PATTERNS = Object.freeze({ heart_rate: /(?:心率|脉搏)?\s*(\d{2,3})\b/, weight: /(?:体重)?\s*(\d{2,3}(?:\.\d)?)\s*(?:kg|公斤|斤)?/i, spo2: /(?:血氧|SpO2|spo2)?\s*(\d{2,3})\s*%?/i, temperature: /(?:体温)?\s*(3\d(?:\.\d)?)\s*(?:度|℃|°C)?/, sleep: /(?:睡了|睡眠)?\s*(\d+(?:\.\d)?)\s*(?:小时|h|H)/, }); const METRIC_TYPE_BY_SET = Object.freeze({ heart_rate: 'hr', weight: 'weight', spo2: 'spo2', temperature: 'temperature', sleep: 'sleep_minutes', }); export function metricSetLabel(metricSet) { return METRIC_SET_LABELS[metricSet] ?? metricSet; } export function metricValuePrompt(metricSet) { switch (metricSet) { case 'blood_pressure': return '请发送血压数值,如 137/82。'; case 'heart_rate': return '请发送心率(次/分),如 72 或「心率 72」。'; case 'weight': return '请发送体重(kg),如 65.5 或「体重 65.5」。'; case 'spo2': return '请发送血氧(%),如 98。'; case 'temperature': return '请发送体温(℃),如 36.5。'; case 'sleep': return '请发送睡眠时长,如「睡了7小时」。'; case 'symptom': return '请描述症状与严重度,如「头晕 2」(1轻 2中 3重)。'; default: return '请发送数值,或回复「取消」。'; } } export function parseSymptomInput(text) { const raw = String(text ?? '').trim(); if (!raw) return { ok: false, error: 'empty' }; const severityMatch = raw.match(/(\d)\s*$/); const severity = severityMatch ? Number(severityMatch[1]) : 2; const namePart = severityMatch ? raw.slice(0, severityMatch.index).trim() : raw; for (const [code, label] of Object.entries(HEALTH_SYMPTOM_LABELS)) { if (code === 'none' || code === 'other') continue; if (namePart.includes(label) || raw.includes(label)) { return assertSymptomRecord({ code, severity }); } } const normalized = normalizeSymptomCode(namePart); if (normalized && normalized !== 'none') { return assertSymptomRecord({ code: normalized, severity }); } if (/头晕/.test(raw)) return assertSymptomRecord({ code: 'dizziness', severity }); if (/头痛/.test(raw)) return assertSymptomRecord({ code: 'headache', severity }); if (/胸闷/.test(raw)) return assertSymptomRecord({ code: 'chest_tightness', severity }); if (/心慌|心悸/.test(raw)) return assertSymptomRecord({ code: 'palpitation', severity }); if (/乏力/.test(raw)) return assertSymptomRecord({ code: 'fatigue', severity }); if (/气短/.test(raw)) return assertSymptomRecord({ code: 'shortness_of_breath', severity }); return { ok: false, error: 'unknown_symptom' }; } export function parseMetricInput(metricSet, text) { const raw = String(text ?? '').trim(); if (!raw) return { ok: false, error: 'empty' }; if (metricSet === 'symptom') { const symptom = parseSymptomInput(raw); if (!symptom.ok) return symptom; return { ok: true, metricSet: 'symptom', symptomCode: symptom.code, severity: symptom.severity, label: HEALTH_SYMPTOM_LABELS[symptom.code] ?? symptom.code, }; } if (metricSet === 'sleep') { const match = raw.match(METRIC_VALUE_PATTERNS.sleep) ?? raw.match(/^(\d+(?:\.\d)?)$/); const hours = match ? Number(match[1]) : NaN; if (!Number.isFinite(hours)) return { ok: false, error: 'not_numeric' }; const minutes = Math.round(hours * 60); const checked = validateMetricValue('sleep_minutes', minutes); if (!checked.ok) return checked; return { ok: true, metricSet: 'sleep', valueNum: checked.value, unit: checked.unit, label: `${hours} 小时`, }; } const pattern = METRIC_VALUE_PATTERNS[metricSet]; const metricType = METRIC_TYPE_BY_SET[metricSet]; if (!pattern || !metricType) return { ok: false, error: 'unknown_metric_set' }; const match = raw.match(pattern) ?? raw.match(/^(\d+(?:\.\d)?)$/); const value = match ? Number(match[1]) : NaN; if (!Number.isFinite(value)) return { ok: false, error: 'not_numeric' }; const checked = validateMetricValue(metricType, value); if (!checked.ok) return checked; return { ok: true, metricSet, valueNum: checked.value, unit: checked.unit, label: `${checked.value}${checked.unit ? ` ${checked.unit}` : ''}`, }; } export function formatGenericConfirmCard({ metricSet, label, context = 'other' }) { const contextLabel = context === 'morning' ? '晨间' : context === 'evening' ? '晚间' : '其他'; const name = metricSetLabel(metricSet); return [ `已识别 → ${name} ${label}`, metricSet === 'blood_pressure' ? `情境:${contextLabel}` : '', '回复「1」确认保存 | 「2」修改 | 「0」放弃', ] .filter(Boolean) .join('\n'); } export function formatCommittedReply(pendingValues) { if (!pendingValues) return '已记录。回复「0」退出。'; if (pendingValues.metricSet === 'blood_pressure') { return `已记录。收缩压 ${pendingValues.systolic} / 舒张压 ${pendingValues.diastolic}。这不是医疗诊断。回复「1」继续录入,或「0」退出。`; } if (pendingValues.metricSet === 'symptom') { const label = HEALTH_SYMPTOM_LABELS[pendingValues.symptomCode] ?? pendingValues.symptomCode; return `已记录症状:${label}(严重度 ${pendingValues.severity})。这不是医疗诊断。回复「1」继续录入,或「0」退出。`; } const name = metricSetLabel(pendingValues.metricSet); const value = pendingValues.displayLabel ?? pendingValues.valueNum; return `已记录${name}:${value}。这不是医疗诊断。回复「1」继续录入,或「0」退出。`; } export function buildPendingValuesFromParsed(parsed, { context = 'other', observedAt = Date.now() } = {}) { if (parsed.metricSet === 'blood_pressure') { return { metricSet: 'blood_pressure', systolic: parsed.systolic, diastolic: parsed.diastolic, context, observedAt, }; } if (parsed.metricSet === 'symptom') { return { metricSet: 'symptom', symptomCode: parsed.symptomCode, severity: parsed.severity, context: 'other', observedAt, displayLabel: parsed.label, }; } return { metricSet: parsed.metricSet, valueNum: parsed.valueNum, context: parsed.metricSet === 'blood_pressure' ? context : 'any', observedAt, displayLabel: parsed.label, }; }