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>
This commit is contained in:
@@ -1,12 +1,67 @@
|
||||
import { isMemindHealthEnabled } from '../health-feature.mjs';
|
||||
import { extractHealthImageFromUrl, extractHealthImageFromVision, fetchHealthImageBuffer } from '../health-image-extract.mjs';
|
||||
import { extractHealthDocumentFromUrl } from '../health-document-ocr.mjs';
|
||||
import { hashHealthImageBuffer } from '../health-image-hash.mjs';
|
||||
import { parseHealthAssetIdFromUrl } from '../health-document-store.mjs';
|
||||
import { buildVisionThumbnailBuffer } from '../vision-image-thumb.mjs';
|
||||
import { computeHealthBaselines } from '../health-baseline-engine.mjs';
|
||||
import { evaluateHealthEvents } from '../health-event-engine.mjs';
|
||||
import { buildHealthAssessSummary } from '../health-assess-summary.mjs';
|
||||
import { buildHealthAgentContext } from '../health-agent-context.mjs';
|
||||
import { baselineToApiShape } from '../health-baseline-serialize.mjs';
|
||||
import { recomputeUserHealthBaselines } from '../health-baseline-job.mjs';
|
||||
import { bootstrapHealthWorkspace } from '../health-workspace-bootstrap.mjs';
|
||||
import { listHealthConnectors } from '../health-connector-registry.mjs';
|
||||
import { buildMedicationTimelineHint } from '../health-share-service.mjs';
|
||||
import {
|
||||
buildHealthReportPublicUrl,
|
||||
resolveHealthMaterializeH5Root,
|
||||
writeHealthReportPage,
|
||||
} from '../health-report-page.mjs';
|
||||
import { markPageDeliveryContractReady } from '../mindspace-delivery-contract.mjs';
|
||||
|
||||
export function attachPortalHealthRoutes({
|
||||
api,
|
||||
observationStore,
|
||||
healthDataRuntime = null,
|
||||
observationStore = null,
|
||||
observationService = null,
|
||||
documentStore = null,
|
||||
shareService = null,
|
||||
getLlmProviderService = () => null,
|
||||
getMindSpaceAssets = () => null,
|
||||
getMindSpacePages = () => null,
|
||||
getUserAuth = () => null,
|
||||
getAuthPool = () => null,
|
||||
h5Root = process.cwd(),
|
||||
env = process.env,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
if (!api || !observationStore) {
|
||||
const runtime = healthDataRuntime ?? {
|
||||
get observationStore() {
|
||||
return observationStore;
|
||||
},
|
||||
get observationService() {
|
||||
return observationService;
|
||||
},
|
||||
get documentStore() {
|
||||
return documentStore;
|
||||
},
|
||||
get shareService() {
|
||||
return shareService;
|
||||
},
|
||||
get draftService() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
const store = runtime.observationStore;
|
||||
const service = runtime.observationService;
|
||||
const docs = runtime.documentStore;
|
||||
const shares = runtime.shareService;
|
||||
const drafts = runtime.draftService ?? null;
|
||||
const baselineStore = runtime.baselineStore ?? null;
|
||||
const eventStore = runtime.eventStore ?? null;
|
||||
|
||||
if (!api || !store) {
|
||||
throw new Error('attachPortalHealthRoutes requires route dependencies');
|
||||
}
|
||||
|
||||
@@ -23,11 +78,256 @@ export function attachPortalHealthRoutes({
|
||||
return userId;
|
||||
};
|
||||
|
||||
api.get('/health/timeline', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId || !service) return;
|
||||
try {
|
||||
const timeline = await service.listTimeline(userId, {
|
||||
limitDays: Math.min(Math.max(Number(req.query?.days) || 30, 1), 90),
|
||||
});
|
||||
res.json({ timeline });
|
||||
} catch (error) {
|
||||
logger.warn?.('List health timeline failed:', error);
|
||||
res.status(500).json({ message: '读取健康 Timeline 失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/health/baselines', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
try {
|
||||
const persisted = baselineStore ? await baselineStore.list(userId) : [];
|
||||
if (persisted.length > 0) {
|
||||
return res.json({ baselines: persisted.map(baselineToApiShape), source: 'persisted' });
|
||||
}
|
||||
const rows = await store.list(userId, { limit: 500 });
|
||||
res.json({ baselines: computeHealthBaselines(rows), source: 'computed' });
|
||||
} catch (error) {
|
||||
logger.warn?.('List health baselines failed:', error);
|
||||
res.status(500).json({ message: '读取基线失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/health/events', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
try {
|
||||
const mode = String(req.query?.mode ?? 'active');
|
||||
const persisted = eventStore
|
||||
? await eventStore.list(userId, { status: 'open', limit: 100 })
|
||||
: [];
|
||||
if (persisted.length > 0) {
|
||||
return res.json({ events: persisted, source: 'persisted' });
|
||||
}
|
||||
const rows = await store.list(userId, { limit: 500 });
|
||||
const baselines = computeHealthBaselines(rows);
|
||||
res.json({ events: evaluateHealthEvents(rows, baselines, { mode }), source: 'computed' });
|
||||
} catch (error) {
|
||||
logger.warn?.('List health events failed:', error);
|
||||
res.status(500).json({ message: '读取健康事件失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/health/alerts/unread', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId || !eventStore) return res.json({ alerts: [] });
|
||||
try {
|
||||
const alerts = await eventStore.listUnreadAlerts(userId, {
|
||||
limit: Math.min(Math.max(Number(req.query?.limit) || 20, 1), 50),
|
||||
});
|
||||
res.json({ alerts });
|
||||
} catch (error) {
|
||||
logger.warn?.('List unread health alerts failed:', error);
|
||||
res.status(500).json({ message: '读取健康提醒失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/events/:eventId/acknowledge', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId || !eventStore) {
|
||||
return res.status(503).json({ message: '健康事件服务未启用' });
|
||||
}
|
||||
try {
|
||||
const event = await eventStore.acknowledge(userId, req.params?.eventId);
|
||||
if (!event) return res.status(404).json({ message: '提醒不存在或已确认' });
|
||||
res.json({ ok: true, event });
|
||||
} catch (error) {
|
||||
logger.warn?.('Acknowledge health event failed:', error);
|
||||
res.status(400).json({
|
||||
message: error instanceof Error ? error.message : '确认提醒失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/alerts/acknowledge-all', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId || !eventStore) {
|
||||
return res.status(503).json({ message: '健康事件服务未启用' });
|
||||
}
|
||||
try {
|
||||
const count = await eventStore.acknowledgeAll(userId);
|
||||
res.json({ ok: true, count });
|
||||
} catch (error) {
|
||||
logger.warn?.('Acknowledge all health alerts failed:', error);
|
||||
res.status(400).json({
|
||||
message: error instanceof Error ? error.message : '确认全部提醒失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/health/assess-summary', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
try {
|
||||
const rows = await store.list(userId, { limit: 500 });
|
||||
res.json({ summary: buildHealthAssessSummary(rows) });
|
||||
} catch (error) {
|
||||
logger.warn?.('Build health assess summary failed:', error);
|
||||
res.status(500).json({ message: '生成健康评估摘要失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/health/agent-context', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
try {
|
||||
const rows = await store.list(userId, { limit: 500 });
|
||||
const documents = docs
|
||||
? await docs.list(userId, { limit: 20 })
|
||||
: [];
|
||||
res.json({ context: buildHealthAgentContext(rows, { documents }) });
|
||||
} catch (error) {
|
||||
logger.warn?.('Build health agent context failed:', error);
|
||||
res.status(500).json({ message: '读取健康 Agent 上下文失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/report-page', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
try {
|
||||
const rows = await store.list(userId, { limit: 500 });
|
||||
const materializeRoot = resolveHealthMaterializeH5Root(h5Root, env);
|
||||
const written = writeHealthReportPage({
|
||||
h5Root: materializeRoot,
|
||||
userId,
|
||||
observations: rows,
|
||||
});
|
||||
const assets = getMindSpaceAssets?.();
|
||||
const pool = getAuthPool?.();
|
||||
if (assets?.syncWorkspaceAssets) {
|
||||
await assets.syncWorkspaceAssets(userId, {
|
||||
categoryCode: 'public',
|
||||
onlyRelativePaths: [written.relativePath],
|
||||
}).catch((error) => {
|
||||
logger.warn?.('Sync health report workspace assets failed:', error);
|
||||
});
|
||||
}
|
||||
if (pool) {
|
||||
await markPageDeliveryContractReady({
|
||||
pool,
|
||||
userId,
|
||||
relativePath: written.relativePath,
|
||||
}).catch(() => {});
|
||||
}
|
||||
const url = buildHealthReportPublicUrl({
|
||||
h5Root: materializeRoot,
|
||||
env,
|
||||
userId,
|
||||
username: req.currentUser?.username ?? null,
|
||||
relativePath: written.relativePath,
|
||||
});
|
||||
res.json({
|
||||
relativePath: written.relativePath,
|
||||
url,
|
||||
summary: buildHealthAssessSummary(rows),
|
||||
size: written.size,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn?.('Create health report page failed:', error);
|
||||
res.status(error?.code === 'invalid_input' ? 400 : 500).json({
|
||||
message: error instanceof Error ? error.message : '生成健康报告页失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/workspace/bootstrap', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
try {
|
||||
const auth = getUserAuth?.();
|
||||
const pages = getMindSpacePages?.();
|
||||
const result = await bootstrapHealthWorkspace({
|
||||
userId,
|
||||
env,
|
||||
resolveWorkspaceRoot: async (id) => {
|
||||
if (req.currentUser?.workspaceRoot) return req.currentUser.workspaceRoot;
|
||||
return auth?.resolveWorkingDir?.(id) ?? null;
|
||||
},
|
||||
createHealthSystemPage: pages?.createHealthSystemPage?.bind(pages) ?? null,
|
||||
listPages: pages?.listPages?.bind(pages) ?? null,
|
||||
});
|
||||
res.json({ bootstrap: result });
|
||||
} catch (error) {
|
||||
logger.warn?.('Bootstrap health workspace failed:', error);
|
||||
res.status(500).json({ message: '初始化健康工作区失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/health/connectors', async (req, res) => {
|
||||
if (!requireHealthUser(req, res)) return;
|
||||
res.json({ connectors: listHealthConnectors() });
|
||||
});
|
||||
|
||||
api.get('/health/medication-hint', async (req, res) => {
|
||||
if (!requireHealthUser(req, res)) return;
|
||||
res.json({ hint: buildMedicationTimelineHint() });
|
||||
});
|
||||
|
||||
api.get('/health/documents', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
if (!docs) return res.json({ documents: [] });
|
||||
try {
|
||||
const documents = await docs.list(userId, {
|
||||
limit: Math.min(Math.max(Number(req.query?.limit) || 50, 1), 200),
|
||||
});
|
||||
res.json({ documents });
|
||||
} catch (error) {
|
||||
logger.warn?.('List health documents failed:', error);
|
||||
res.status(500).json({ message: '读取报告归档失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/share-snapshot', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId || !shares) {
|
||||
return res.status(503).json({ message: '分享服务未启用' });
|
||||
}
|
||||
try {
|
||||
const snapshot = await shares.createReadonlySnapshot(userId);
|
||||
res.json(snapshot);
|
||||
} catch (error) {
|
||||
logger.warn?.('Create health share snapshot failed:', error);
|
||||
res.status(500).json({ message: '创建分享摘要失败' });
|
||||
}
|
||||
});
|
||||
|
||||
api.get('/health/share/:token', async (req, res) => {
|
||||
if (!isMemindHealthEnabled(env)) {
|
||||
return res.status(404).json({ message: '健康助手未启用' });
|
||||
}
|
||||
if (!shares) return res.status(503).json({ message: '分享服务未启用' });
|
||||
const snapshot = shares.getSnapshot(req.params?.token);
|
||||
if (!snapshot) return res.status(404).json({ message: '分享链接无效或已过期' });
|
||||
res.json({ snapshot });
|
||||
});
|
||||
|
||||
api.get('/health/observations', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
try {
|
||||
const rows = await observationStore.list(userId, {
|
||||
const rows = await store.list(userId, {
|
||||
limit: Math.min(Math.max(Number(req.query?.limit) || 50, 1), 200),
|
||||
});
|
||||
res.json({ observations: rows });
|
||||
@@ -42,12 +342,16 @@ export function attachPortalHealthRoutes({
|
||||
if (!userId) return;
|
||||
try {
|
||||
const body = req.body ?? {};
|
||||
if (service) {
|
||||
const result = await service.commit(userId, body);
|
||||
return res.json(result);
|
||||
}
|
||||
if (body.confirmed !== true) {
|
||||
return res.status(400).json({ message: '必须确认后才能保存' });
|
||||
}
|
||||
if (body.metricSet === 'blood_pressure') {
|
||||
const observedAt = body.observedAt ?? Date.now();
|
||||
const systolic = await observationStore.insert(userId, {
|
||||
const systolic = await store.insert(userId, {
|
||||
confirmed: true,
|
||||
observedAt,
|
||||
metricType: 'bp_systolic',
|
||||
@@ -57,7 +361,7 @@ export function attachPortalHealthRoutes({
|
||||
source: body.source ?? 'manual',
|
||||
qualityFlag: 'ok',
|
||||
});
|
||||
const diastolic = await observationStore.insert(userId, {
|
||||
const diastolic = await store.insert(userId, {
|
||||
confirmed: true,
|
||||
observedAt,
|
||||
metricType: 'bp_diastolic',
|
||||
@@ -69,7 +373,7 @@ export function attachPortalHealthRoutes({
|
||||
});
|
||||
return res.json({ observations: [systolic, diastolic] });
|
||||
}
|
||||
const row = await observationStore.insert(userId, {
|
||||
const row = await store.insert(userId, {
|
||||
confirmed: true,
|
||||
observedAt: body.observedAt ?? Date.now(),
|
||||
metricType: body.metricType,
|
||||
@@ -88,4 +392,142 @@ export function attachPortalHealthRoutes({
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/documents', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
if (!docs) {
|
||||
return res.status(503).json({ message: '报告归档服务未启用' });
|
||||
}
|
||||
try {
|
||||
const body = req.body ?? {};
|
||||
if (body.confirmed !== true) {
|
||||
return res.status(400).json({ message: '必须确认后才能归档报告' });
|
||||
}
|
||||
const imageUrl = String(body.imageUrl ?? '').trim();
|
||||
if (!imageUrl) {
|
||||
return res.status(400).json({ message: '缺少报告图片地址' });
|
||||
}
|
||||
const llmProviderService = getLlmProviderService();
|
||||
const analyzeImagesWithVision = llmProviderService?.analyzeImagesWithVision?.bind(llmProviderService);
|
||||
let ocr = null;
|
||||
if (body.runOcr !== false && analyzeImagesWithVision) {
|
||||
ocr = await extractHealthDocumentFromUrl({
|
||||
userId,
|
||||
imageUrl,
|
||||
analyzeImagesWithVision,
|
||||
mindSpaceAssets: getMindSpaceAssets(),
|
||||
buildVisionThumbnailBuffer,
|
||||
});
|
||||
}
|
||||
const row = await docs.insert(userId, {
|
||||
confirmed: true,
|
||||
imageUrl,
|
||||
assetId: parseHealthAssetIdFromUrl(imageUrl),
|
||||
source: body.source ?? 'manual',
|
||||
notes: body.notes ?? ocr?.title ?? null,
|
||||
docType: ocr?.docType ?? 'other',
|
||||
reportDate: ocr?.reportDate ?? null,
|
||||
institution: ocr?.institution ?? null,
|
||||
ocrText: ocr?.ocrText ?? null,
|
||||
extractedMetrics: ocr?.extractedMetrics ?? [],
|
||||
extractionStatus: ocr?.ok ? ocr.extractionStatus : 'pending',
|
||||
});
|
||||
res.json({ document: row, ocr: ocr?.ok ? ocr : null });
|
||||
} catch (error) {
|
||||
logger.warn?.('Insert health document failed:', error);
|
||||
res.status(400).json({
|
||||
message: error instanceof Error ? error.message : '报告归档失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/extract-image', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId) return;
|
||||
const body = req.body ?? {};
|
||||
const imageUrl = String(body.imageUrl ?? '').trim();
|
||||
if (!imageUrl) {
|
||||
return res.status(400).json({ message: '缺少图片地址' });
|
||||
}
|
||||
const llmProviderService = getLlmProviderService();
|
||||
const analyzeImagesWithVision = llmProviderService?.analyzeImagesWithVision?.bind(llmProviderService);
|
||||
if (!analyzeImagesWithVision) {
|
||||
return res.status(503).json({ message: '图片识别服务暂不可用' });
|
||||
}
|
||||
try {
|
||||
const { buffer, mimeType } = await fetchHealthImageBuffer({
|
||||
userId,
|
||||
imageUrl,
|
||||
mindSpaceAssets: getMindSpaceAssets(),
|
||||
});
|
||||
const sourceRef = hashHealthImageBuffer(buffer);
|
||||
const result = await extractHealthImageFromVision({
|
||||
buffer,
|
||||
mimeType,
|
||||
metricSetHint: body.metricSet ?? null,
|
||||
analyzeImagesWithVision,
|
||||
buildVisionThumbnailBuffer,
|
||||
});
|
||||
if (!result.ok) {
|
||||
return res.status(422).json({
|
||||
message: result.message ?? '未能识别图片读数',
|
||||
error: result.error ?? 'extract_failed',
|
||||
});
|
||||
}
|
||||
let draft = null;
|
||||
if (drafts) {
|
||||
draft = await drafts.saveExtractionDraft(userId, {
|
||||
extraction: result,
|
||||
channel: body.channel ?? 'h5',
|
||||
sourceRef,
|
||||
});
|
||||
}
|
||||
return res.json({
|
||||
extraction: result,
|
||||
draftKey: draft?.draftKey ?? null,
|
||||
sourceRef,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn?.('Health image extract failed:', error);
|
||||
return res.status(500).json({
|
||||
message: error instanceof Error ? error.message : '图片识别失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/drafts/:draftKey/commit', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId || !drafts) {
|
||||
return res.status(503).json({ message: '草稿服务未启用' });
|
||||
}
|
||||
try {
|
||||
const result = await drafts.commitDraft(userId, req.params?.draftKey, {
|
||||
source: req.body?.source ?? 'photo',
|
||||
});
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logger.warn?.('Commit health draft failed:', error);
|
||||
res.status(400).json({
|
||||
message: error instanceof Error ? error.message : '草稿提交失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
api.post('/health/drafts/:draftKey/discard', async (req, res) => {
|
||||
const userId = requireHealthUser(req, res);
|
||||
if (!userId || !drafts) {
|
||||
return res.status(503).json({ message: '草稿服务未启用' });
|
||||
}
|
||||
try {
|
||||
const draft = await drafts.discardDraft(userId, req.params?.draftKey);
|
||||
if (!draft) return res.status(404).json({ message: '草稿不存在' });
|
||||
res.json({ ok: true, draft });
|
||||
} catch (error) {
|
||||
logger.warn?.('Discard health draft failed:', error);
|
||||
res.status(400).json({
|
||||
message: error instanceof Error ? error.message : '草稿丢弃失败',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import { createNotificationDispatcher } from '../notification-dispatcher.mjs';
|
||||
import { startScheduleReminderWorker } from '../schedule-reminder-worker.mjs';
|
||||
import { startScheduledTaskWorker } from '../scheduled-task-worker.mjs';
|
||||
import { isScheduledTaskWorkerEnabled } from '../scheduled-task-worker-config.mjs';
|
||||
import { startHealthBaselineWorker } from '../health-baseline-worker.mjs';
|
||||
import { createHealthEventNotificationService } from '../health-event-notification-service.mjs';
|
||||
import { isPassiveCanaryRuntime } from './portal-runtime-role.mjs';
|
||||
import { loadWechatMpModule } from '../wechat-mp-loader.mjs';
|
||||
import { createToolGateway } from '../tool-gateway.mjs';
|
||||
@@ -49,6 +51,10 @@ export async function bootstrapPortalIntegrationServices({
|
||||
logger = console,
|
||||
healthChannelStore = null,
|
||||
healthObservationStore = null,
|
||||
healthObservationService = null,
|
||||
healthDocumentStore = null,
|
||||
healthDataRuntime = null,
|
||||
healthEventStore = null,
|
||||
loadWechatMpModuleFn = loadWechatMpModule,
|
||||
resolveAnalyticsOwnerSegmentFn =
|
||||
resolveAnalyticsOwnerSegment,
|
||||
@@ -63,6 +69,8 @@ export async function bootstrapPortalIntegrationServices({
|
||||
startScheduleReminderWorker,
|
||||
startScheduledTaskWorkerFn =
|
||||
startScheduledTaskWorker,
|
||||
startHealthBaselineWorkerFn =
|
||||
startHealthBaselineWorker,
|
||||
createPageEditSessionServiceFn =
|
||||
createPageEditSessionService,
|
||||
createToolGatewayFn = createToolGateway,
|
||||
@@ -251,6 +259,9 @@ export async function bootstrapPortalIntegrationServices({
|
||||
: null,
|
||||
healthChannelStore: healthChannelStore ?? undefined,
|
||||
healthObservationStore,
|
||||
healthObservationService,
|
||||
healthDocumentStore,
|
||||
healthEventStore: healthEventStore ?? healthDataRuntime?.eventStore ?? null,
|
||||
env,
|
||||
});
|
||||
|
||||
@@ -324,6 +335,25 @@ export async function bootstrapPortalIntegrationServices({
|
||||
);
|
||||
}
|
||||
|
||||
let healthBaselineWorker = null;
|
||||
if (!isPassiveCanaryRuntime(env) && healthDataRuntime) {
|
||||
const healthEventNotificationService = createHealthEventNotificationService({
|
||||
createUserNotification: scheduleService?.createUserNotification?.bind(scheduleService) ?? null,
|
||||
notificationDispatcher,
|
||||
logger,
|
||||
});
|
||||
healthBaselineWorker = startHealthBaselineWorkerFn({
|
||||
healthDataRuntime,
|
||||
eventNotificationService: healthEventNotificationService,
|
||||
pool,
|
||||
env,
|
||||
logger,
|
||||
});
|
||||
if (healthBaselineWorker?.runOnce) {
|
||||
logger.log?.('Health baseline job worker enabled');
|
||||
}
|
||||
}
|
||||
|
||||
let subscriptionExpiryTimer = null;
|
||||
if (subscriptionService && !isPassiveCanaryRuntime(env)) {
|
||||
subscriptionExpiryTimer = setIntervalFn(
|
||||
@@ -382,6 +412,7 @@ export async function bootstrapPortalIntegrationServices({
|
||||
notificationDispatcher,
|
||||
scheduleReminderWorker,
|
||||
scheduledTaskWorker,
|
||||
healthBaselineWorker,
|
||||
subscriptionExpiryTimer,
|
||||
mindSpacePageEditSession,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '../conversation-repair.mjs';
|
||||
import { filterNonemptyUserVisibleMessages } from '../conversation-transcript-persist.mjs';
|
||||
import { sanitizeSessionConversationPublicHtmlLinks } from '../tkmind-proxy.mjs';
|
||||
import { sanitizeHealthAssistantReportDelivery } from '../health-report-finish-guard.mjs';
|
||||
|
||||
function assertRouter(api) {
|
||||
if (
|
||||
@@ -568,6 +569,20 @@ export function attachPortalSessionRoutes(
|
||||
req.currentUser.username,
|
||||
},
|
||||
});
|
||||
const healthReportSanitized = sanitizeHealthAssistantReportDelivery(
|
||||
messages,
|
||||
{
|
||||
userId: uid,
|
||||
username: req.currentUser.username,
|
||||
h5Root: process.cwd(),
|
||||
},
|
||||
);
|
||||
if (healthReportSanitized.changed) {
|
||||
messages = healthReportSanitized.messages;
|
||||
logger.warn?.(
|
||||
`[Health] stripped unverified health report links for session ${sid}`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
Array.isArray(syncResult?.docxSync?.missing) &&
|
||||
syncResult.docxSync.missing.length > 0
|
||||
|
||||
Reference in New Issue
Block a user