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)}近 14 日记录
${metricLines
.map((line) => `
说明