export const HEALTH_CHANNEL_STEPS = Object.freeze({ IDLE: 'idle', AWAIT_ACTION_CHOICE: 'await_action_choice', AWAIT_METRIC_TYPE: 'await_metric_type', AWAIT_CONTEXT: 'await_context', AWAIT_VALUE: 'await_value', CONFIRM: 'confirm', COMMITTED: 'committed', EXITED: 'exited', }); export const HEALTH_ACTIONS = Object.freeze({ RECORD: 'record', ASSESS: 'assess', DOCUMENT: 'document', ARCHIVE: 'archive', }); export const HEALTH_METRIC_SETS = Object.freeze([ 'blood_pressure', 'heart_rate', 'weight', 'spo2', 'temperature', 'sleep', 'symptom', 'medication', ]); const ACTION_BY_CHOICE = Object.freeze({ 1: HEALTH_ACTIONS.RECORD, 2: HEALTH_ACTIONS.ASSESS, 3: HEALTH_ACTIONS.DOCUMENT, 4: HEALTH_ACTIONS.ARCHIVE, }); const METRIC_BY_CHOICE = Object.freeze({ 1: 'blood_pressure', 2: 'heart_rate', 3: 'weight', 4: 'spo2', 5: 'temperature', 6: 'sleep', 7: 'symptom', 8: 'medication', }); export function createIdleHealthChannelState({ channel = 'h5' } = {}) { return { sessionKind: 'health', channel, step: HEALTH_CHANNEL_STEPS.IDLE, action: null, metricSet: null, context: null, pendingDraftId: null, lastPrompt: null, }; } export function healthChannelUnexpectedPrompt(state) { if (state.step === HEALTH_CHANNEL_STEPS.AWAIT_VALUE) { if (state.metricSet === 'blood_pressure') { return [ '当前正在录入「血压」。', '- 直接发送数值,如 137/82', '- 或拍照上传血压计读数', '- 回复「取消」放弃本次录入', '- 回复「0」退出健康通道', ].join('\n'); } const labels = { heart_rate: '心率', weight: '体重', spo2: '血氧', temperature: '体温', sleep: '睡眠', symptom: '症状', }; const label = labels[state.metricSet] ?? '指标'; return `当前正在录入「${label}」。请按提示发送数值,或回复「取消」/「0」退出。`; } return '当前操作无法识别。请回复数字选择,或回复「0」退出健康通道。'; } export function reduceHealthChannel(state, event) { const current = state ?? createIdleHealthChannelState(); const type = String(event?.type ?? '').trim(); if (type === 'exit' || event?.choice === 0 || event?.text === '0' || event?.text === '退出') { return { state: { ...createIdleHealthChannelState({ channel: current.channel }), step: HEALTH_CHANNEL_STEPS.EXITED }, actions: [{ type: 'exit_channel' }], }; } if (type === 'timeout') { return { state: { ...createIdleHealthChannelState({ channel: current.channel }), step: HEALTH_CHANNEL_STEPS.EXITED }, actions: [{ type: 'timeout_exit' }], }; } if (type === 'cancel' || event?.text === '取消') { return { state: createIdleHealthChannelState({ channel: current.channel }), actions: [{ type: 'discard_draft' }], }; } if (current.step === HEALTH_CHANNEL_STEPS.IDLE) { if (type === 'select_action' && event.action === HEALTH_ACTIONS.RECORD) { return { state: { ...current, step: HEALTH_CHANNEL_STEPS.AWAIT_METRIC_TYPE, action: HEALTH_ACTIONS.RECORD }, actions: [{ type: 'prompt_metric_type' }], }; } if (type === 'select_action' && event.action === HEALTH_ACTIONS.ASSESS) { return { state: { ...current, step: HEALTH_CHANNEL_STEPS.IDLE, action: HEALTH_ACTIONS.ASSESS }, actions: [{ type: 'run_assessment' }], }; } if (type === 'select_action' && (event.action === HEALTH_ACTIONS.DOCUMENT || event.action === HEALTH_ACTIONS.ARCHIVE)) { return { state: { ...current, step: HEALTH_CHANNEL_STEPS.IDLE, action: event.action }, actions: [{ type: event.action === HEALTH_ACTIONS.DOCUMENT ? 'await_document' : 'show_archive' }], }; } if (type === 'ambiguous') { return { state: { ...current, step: HEALTH_CHANNEL_STEPS.AWAIT_ACTION_CHOICE }, actions: [{ type: 'prompt_action_choice' }], }; } if (type === 'unexpected') { return { state: current, actions: [{ type: 'prompt_unexpected', prompt: healthChannelUnexpectedPrompt(current) }], }; } } if (current.step === HEALTH_CHANNEL_STEPS.AWAIT_ACTION_CHOICE && type === 'choose') { const action = ACTION_BY_CHOICE[Number(event.choice)]; if (!action) { return { state: current, actions: [{ type: 'prompt_action_choice' }], }; } return reduceHealthChannel( { ...current, step: HEALTH_CHANNEL_STEPS.IDLE }, { type: 'select_action', action }, ); } if (current.step === HEALTH_CHANNEL_STEPS.AWAIT_METRIC_TYPE && (type === 'pick_metric' || type === 'choose')) { const metricSet = event.metricSet || METRIC_BY_CHOICE[Number(event.choice)]; if (!HEALTH_METRIC_SETS.includes(metricSet)) { return { state: current, actions: [{ type: 'prompt_metric_type' }], }; } const next = { ...current, metricSet, step: metricSet === 'blood_pressure' ? HEALTH_CHANNEL_STEPS.AWAIT_CONTEXT : HEALTH_CHANNEL_STEPS.AWAIT_VALUE, }; return { state: next, actions: [{ type: next.step === HEALTH_CHANNEL_STEPS.AWAIT_CONTEXT ? 'prompt_context' : 'prompt_value' }], }; } if (current.step === HEALTH_CHANNEL_STEPS.AWAIT_CONTEXT && type === 'pick_context') { return { state: { ...current, context: event.context || 'other', step: HEALTH_CHANNEL_STEPS.AWAIT_VALUE, }, actions: [{ type: 'prompt_value' }], }; } if (current.step === HEALTH_CHANNEL_STEPS.AWAIT_VALUE) { if (type === 'submit_value' || type === 'image' || type === 'voice') { return { state: { ...current, step: HEALTH_CHANNEL_STEPS.CONFIRM, pendingDraftId: event.draftId ?? current.pendingDraftId ?? 'draft', }, actions: [{ type: 'prompt_confirm', draft: event.draft ?? null, requireConfirm: true }], }; } if (type === 'unexpected') { return { state: current, actions: [{ type: 'prompt_unexpected', prompt: healthChannelUnexpectedPrompt(current) }], }; } } if (current.step === HEALTH_CHANNEL_STEPS.CONFIRM) { if (type === 'commit') { return { state: { ...current, step: HEALTH_CHANNEL_STEPS.COMMITTED }, actions: [{ type: 'commit_observation', draftId: current.pendingDraftId }], }; } if (type === 'revise') { return { state: { ...current, step: HEALTH_CHANNEL_STEPS.AWAIT_VALUE }, actions: [{ type: 'prompt_value' }], }; } if (type === 'discard') { return { state: createIdleHealthChannelState({ channel: current.channel }), actions: [{ type: 'discard_draft', draftId: current.pendingDraftId }], }; } } if (current.step === HEALTH_CHANNEL_STEPS.COMMITTED && type === 'continue') { return { state: createIdleHealthChannelState({ channel: current.channel }), actions: [{ type: 'reset' }], }; } return { state: current, actions: [{ type: 'noop' }], }; }