2baf29b3ae
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>
192 lines
6.2 KiB
JavaScript
192 lines
6.2 KiB
JavaScript
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');
|
|
}
|