Files
memind/health-channel-state.mjs
T
john 2baf29b3ae feat(health): complete P0 health channel — baseline engine, page-data, MindSpace UI
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>
2026-09-09 18:04:57 +08:00

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' }],
};
}