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>
238 lines
7.0 KiB
JavaScript
238 lines
7.0 KiB
JavaScript
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' }],
|
|
};
|
|
}
|