Files
memind/health-observation-draft-service.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

129 lines
4.0 KiB
JavaScript

import { inferBloodPressureContext } from './health-observation-validate.mjs';
import { HEALTH_DRAFT_TTL_MS, createDraftKey } from './health-observation-draft-store.mjs';
function extractionToCommitBody(extracted = {}, { observedAt = Date.now(), source = 'photo', sourceRef = null } = {}) {
const metricSet = extracted.metricSet ?? extracted.metric_set;
if (metricSet === 'blood_pressure') {
return {
confirmed: true,
metricSet: 'blood_pressure',
systolic: extracted.values?.systolic ?? extracted.systolic,
diastolic: extracted.values?.diastolic ?? extracted.diastolic,
context: extracted.context ?? inferBloodPressureContext(observedAt),
observedAt,
source,
sourceRef,
qualityFlag: extracted.lowConfidence ? 'suspect' : 'ok',
rawPayload: extracted.raw ?? extracted,
};
}
if (metricSet === 'symptom') {
return {
confirmed: true,
metricSet: 'symptom',
symptomCode: extracted.symptomCode ?? extracted.code,
severity: extracted.severity,
observedAt,
source,
sourceRef,
};
}
const valueNum =
extracted.values?.valueNum
?? extracted.values?.hr
?? extracted.values?.weight
?? extracted.values?.spo2
?? extracted.values?.temperature
?? extracted.valueNum;
return {
confirmed: true,
metricSet,
valueNum,
observedAt,
source,
sourceRef,
context: 'any',
qualityFlag: extracted.lowConfidence ? 'suspect' : 'ok',
rawPayload: extracted.raw ?? extracted,
};
}
export function createHealthObservationDraftService({ draftStore, observationService } = {}) {
if (!draftStore || !observationService) {
throw new Error('createHealthObservationDraftService requires draftStore and observationService');
}
return {
draftStore,
observationService,
async saveExtractionDraft(userId, {
extraction,
channel = 'h5',
sourceRef = null,
now = Date.now(),
} = {}) {
if (!extraction?.ok) {
const error = Object.assign(new Error('抽取结果无效,无法创建草稿'), { code: 'invalid_extraction' });
throw error;
}
if (sourceRef) {
const existing = await draftStore.findPendingBySourceRef(userId, sourceRef);
if (existing) return existing;
}
const draftKey = createDraftKey({
userId,
sourceRef: sourceRef ?? `manual-${now}`,
metricSet: extraction.metricSet,
now,
});
return draftStore.insert(userId, {
draftKey,
metricSet: extraction.metricSet,
sourceRef,
channel,
expiresAt: now + HEALTH_DRAFT_TTL_MS,
extracted: {
metricSet: extraction.metricSet,
values: extraction.values,
lowConfidence: extraction.lowConfidence ?? false,
raw: extraction.raw ?? null,
sourceRef,
},
validation: {
requireConfirm: true,
ok: true,
},
});
},
async getDraft(userId, draftKey) {
const draft = await draftStore.findByKey(userId, draftKey);
if (!draft) return null;
if (draft.expiresAt <= Date.now()) return null;
return draft;
},
async commitDraft(userId, draftKey, { source = 'photo' } = {}) {
const draft = await this.getDraft(userId, draftKey);
if (!draft) {
const error = Object.assign(new Error('草稿不存在或已过期'), { code: 'draft_not_found' });
throw error;
}
const body = extractionToCommitBody(draft.extracted, {
observedAt: Date.now(),
source,
sourceRef: draft.sourceRef,
});
const result = await observationService.commit(userId, body);
await draftStore.update(userId, draftKey, {
status: 'committed',
committedAt: Date.now(),
});
return { draft, ...result };
},
async discardDraft(userId, draftKey) {
const draft = await draftStore.findByKey(userId, draftKey);
if (!draft) return null;
return draftStore.update(userId, draftKey, { status: 'discarded' });
},
};
}