import fs from 'node:fs'; import path from 'node:path'; import { buildHealthAssessSummary } from './health-assess-summary.mjs'; import { buildHealthTimeline } from './health-timeline.mjs'; import { buildMindSpacePublicUrlForUser } from './mindspace-runtime-config.mjs'; import { resolvePublishDir } from './user-publish.mjs'; import { looksLikeHealthReportPageRequest } from './health-channel-aliases.mjs'; export { looksLikeHealthReportPageRequest } from './health-channel-aliases.mjs'; /** 健康页落盘目录:remote MindSpace 交付时须与 MINDSPACE_SERVICE_H5_ROOT 一致。 */ export function resolveHealthMaterializeH5Root(portalH5Root = process.cwd(), env = process.env) { const portalRoot = path.resolve(portalH5Root); const adapter = String(env.MINDSPACE_SERVER_ADAPTER ?? 'local').trim().toLowerCase(); if (adapter === 'local') return portalRoot; const sharedRoot = String( env.MINDSPACE_SERVICE_H5_ROOT ?? env.MEMIND_SHARED_PUBLISH_ROOT ?? env.GOOSED_SANDBOX_PUBLISH_ROOT ?? '', ).trim(); if (sharedRoot) { const resolved = path.resolve(sharedRoot); if (fs.existsSync(resolved)) return resolved; } return portalRoot; } export const HEALTH_REPORT_PAGE_PREFIX = 'health-report'; export const HEALTH_REPORT_FILENAME_PATTERN = /^health-report-\d{4}-\d{2}-\d{2}\.html$/i; export const HEALTH_PUBLIC_HTML_PATTERN = /^public\/health-[a-z0-9-]+\.html$/i; export function writeHealthPublicHtmlPage({ h5Root = process.cwd(), userId, relativePath, html, minSize = 64, } = {}) { if (!userId || !relativePath || !html) { const error = Object.assign(new Error('缺少写入参数'), { code: 'invalid_input' }); throw error; } const clean = String(relativePath).replace(/^\/+/, ''); if (!HEALTH_PUBLIC_HTML_PATTERN.test(clean)) { const error = Object.assign(new Error('健康页路径不合法'), { code: 'invalid_health_page_path' }); throw error; } const publishDir = resolvePublishDir(h5Root, { id: String(userId) }); const absolutePath = path.join(publishDir, clean); fs.mkdirSync(path.dirname(absolutePath), { recursive: true }); fs.writeFileSync(absolutePath, html, 'utf8'); if (!fs.existsSync(absolutePath) || fs.statSync(absolutePath).size < minSize) { const error = Object.assign(new Error('健康页写入失败'), { code: 'health_page_write_failed' }); throw error; } return { relativePath: clean, absolutePath, size: fs.statSync(absolutePath).size, }; } export function defaultHealthReportFilename(now = Date.now()) { const date = new Date(now); const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, '0'); const d = String(date.getDate()).padStart(2, '0'); return `${HEALTH_REPORT_PAGE_PREFIX}-${y}-${m}-${d}.html`; } function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function formatMetricLines(timeline) { return timeline.slice(0, 14).flatMap((day) => { const metrics = Object.values(day.metrics ?? {}) .map((item) => `${item.label} ${item.value}`) .join(';'); if (!metrics) return []; return [`${day.date}:${metrics}`]; }); } export function buildHealthReportHtml(observations = [], { now = Date.now(), title = '本人健康报告' } = {}) { const timeline = buildHealthTimeline(observations, { limitDays: 30 }); const summary = buildHealthAssessSummary(observations, { now }); const metricLines = formatMetricLines(timeline); const generatedAt = new Date(now).toISOString().slice(0, 10); const sections = [ `

概况

${escapeHtml(summary)}
`, ]; if (metricLines.length > 0) { sections.push( `

近 14 日记录

`, ); } sections.push( '

说明

本页基于你确认过的健康记录生成,仅供本人回顾,' + '不构成医疗诊断。如有不适请及时就医。

', ); return ` ${escapeHtml(title)}

${escapeHtml(title)}

生成日期:${escapeHtml(generatedAt)} · MeMind Health · 加密健康档案区

${sections.join('\n ')} `; } export function writeHealthReportPage({ h5Root = process.cwd(), userId, observations = [], now = Date.now(), filename = null, } = {}) { if (!userId) { const error = Object.assign(new Error('缺少 userId'), { code: 'invalid_input' }); throw error; } const relativePath = `public/${filename ?? defaultHealthReportFilename(now)}`; if (!HEALTH_REPORT_FILENAME_PATTERN.test(path.posix.basename(relativePath))) { const error = Object.assign(new Error('健康报告文件名不合法'), { code: 'invalid_health_report_filename' }); throw error; } const html = buildHealthReportHtml(observations, { now }); const written = writeHealthPublicHtmlPage({ h5Root, userId, relativePath, html, }); return written; } export function healthReportPageExists(h5Root, userId, relativePath) { const clean = String(relativePath ?? '').replace(/^\/+/, ''); if (!clean.startsWith('public/') || !HEALTH_REPORT_FILENAME_PATTERN.test(path.posix.basename(clean))) { return false; } const publishDir = resolvePublishDir(h5Root, { id: String(userId) }); const target = path.join(publishDir, clean); return fs.existsSync(target) && fs.statSync(target).isFile() && fs.statSync(target).size >= 64; } export function buildHealthReportPublicUrl({ h5Root = process.cwd(), env = process.env, userId, username = null, relativePath, } = {}) { return buildMindSpacePublicUrlForUser({ h5Root, env, user: { id: userId, username }, relativePath, }); }