import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { ensureHealthPageDataForUser } from './health-page-data-bootstrap.mjs'; import { createUserDataSpaceService } from './user-data-space-service.mjs'; import { isMemindHealthEnabled } from './health-feature.mjs'; import { writeHealthPublicHtmlPage, resolveHealthMaterializeH5Root } from './health-report-page.mjs'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url))); export const HEALTH_SHARE_SNAPSHOTS_DATASET = Object.freeze({ name: 'health_share_snapshots', table: 'health_share_snapshots', description: 'MeMind Health 脱敏分享摘要(公开页只读)', actions: ['read', 'insert'], columns: { read: ['id', 'snapshot_key', 'scope', 'period_start', 'period_end', 'payload', 'created_at'], insert: ['snapshot_key', 'scope', 'period_start', 'period_end', 'payload'], }, }); export const HEALTH_SHARE_SNAPSHOTS_DDL_PG = `CREATE TABLE IF NOT EXISTS health_share_snapshots ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, snapshot_key TEXT NOT NULL UNIQUE, scope TEXT NOT NULL, period_start DATE NOT NULL, period_end DATE NOT NULL, payload JSONB NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP )`; export const HEALTH_SHARE_SNAPSHOTS_DDL_SQLITE = `CREATE TABLE IF NOT EXISTS health_share_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_key TEXT NOT NULL UNIQUE, scope TEXT NOT NULL, period_start TEXT NOT NULL, period_end TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) )`; export async function ensureHealthShareSnapshotDataset(userDataSpace) { const backend = userDataSpace.privateDataDb ? 'sqlite' : 'postgres'; const ddl = backend === 'sqlite' ? HEALTH_SHARE_SNAPSHOTS_DDL_SQLITE : HEALTH_SHARE_SNAPSHOTS_DDL_PG; await userDataSpace.executeSql(ddl); await userDataSpace.upsertDataset(HEALTH_SHARE_SNAPSHOTS_DATASET); } export const HEALTH_TIMELINE_PAGE_TEMPLATE_ID = 'health-timeline-summary'; export const HEALTH_TIMELINE_PAGE_TITLE = '健康 Timeline 摘要'; export const HEALTH_TIMELINE_PUBLIC_RELATIVE_PATH = 'public/health-timeline-summary.html'; export async function ensureHealthTimelinePage({ userId, createHealthSystemPage = null, listPages = null, h5Root = process.cwd(), env = process.env, } = {}) { if (!userId) { return { ok: false, skipped: true, reason: 'missing_user' }; } if (typeof listPages === 'function') { const existing = await listPages(userId, { categoryCode: 'health', limit: 20, }); const match = (existing?.items ?? []).find( (page) => page.templateId === HEALTH_TIMELINE_PAGE_TEMPLATE_ID || page.title === HEALTH_TIMELINE_PAGE_TITLE, ); if (match) { return { ok: true, skipped: true, pageId: match.id, reason: 'already_exists' }; } } const template = loadHealthTimelinePageTemplate(); if (!template) { return { ok: false, skipped: true, reason: 'template_missing' }; } if (typeof createHealthSystemPage === 'function') { const page = await createHealthSystemPage(userId, { title: HEALTH_TIMELINE_PAGE_TITLE, summary: '近 14 天确认记录(加密分区,禁止完全公开)', content: template, templateId: HEALTH_TIMELINE_PAGE_TEMPLATE_ID, contentFormat: 'html', pageType: 'html', categoryCode: 'health', }); return { ok: true, skipped: false, pageId: page?.id ?? null, mode: 'mindspace_page' }; } try { const written = writeHealthPublicHtmlPage({ h5Root: resolveHealthMaterializeH5Root(h5Root, env), userId, relativePath: HEALTH_TIMELINE_PUBLIC_RELATIVE_PATH, html: template, minSize: 128, }); return { ok: true, skipped: false, pageId: null, mode: 'workspace_file', relativePath: written.relativePath, }; } catch (error) { return { ok: false, skipped: true, reason: error instanceof Error ? error.message : 'timeline_file_failed', }; } } export async function bootstrapHealthWorkspace({ userId, resolveWorkspaceRoot, createHealthSystemPage = null, listPages = null, env = process.env, } = {}) { if (!isMemindHealthEnabled(env)) { return { ok: false, skipped: true, reason: 'health_disabled' }; } if (!userId || typeof resolveWorkspaceRoot !== 'function') { return { ok: false, skipped: true, reason: 'missing_context' }; } const workspaceRoot = await resolveWorkspaceRoot(userId); if (!workspaceRoot) { return { ok: false, skipped: true, reason: 'workspace_not_found' }; } const userDataSpace = createUserDataSpaceService({ workspaceRoot, userId: String(userId) }); await ensureHealthPageDataForUser(userDataSpace, userId); await ensureHealthShareSnapshotDataset(userDataSpace); const markerDir = path.join(workspaceRoot, '.mindspace', 'health'); const markerPath = path.join(markerDir, 'workspace-bootstrapped.json'); let already = false; try { if (fs.existsSync(markerPath)) already = true; } catch { already = false; } let timelinePage = null; try { timelinePage = await ensureHealthTimelinePage({ userId, createHealthSystemPage, listPages, h5Root: env.MEMIND_H5_ROOT ?? process.cwd(), env, }); } catch (error) { timelinePage = { ok: false, skipped: true, reason: error instanceof Error ? error.message : 'timeline_page_failed', }; } if (!already) { fs.mkdirSync(markerDir, { recursive: true }); fs.writeFileSync( markerPath, `${JSON.stringify({ bootstrappedAt: Date.now(), datasets: ['health_share_snapshots'], timelinePageId: timelinePage?.pageId ?? null, }, null, 2)}\n`, ); } return { ok: true, workspaceRoot, datasets: ['health_observations', 'health_documents', 'health_observation_drafts', 'health_share_snapshots'], firstBootstrap: !already, timelinePage, }; } export function loadHealthTimelinePageTemplate() { const templatePath = path.join(repoRoot, 'templates', 'health-timeline-summary.html'); if (!fs.existsSync(templatePath)) return null; return fs.readFileSync(templatePath, 'utf8'); }