Files
memind/health-baseline-job.mjs
T
john 2baf29b3ae 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>
2026-09-09 18:04:57 +08:00

131 lines
3.8 KiB
JavaScript

import { computeHealthBaselines } from './health-baseline-engine.mjs';
import { evaluateHealthEvents } from './health-event-engine.mjs';
import { baselineToApiShape } from './health-baseline-serialize.mjs';
import { isMemindHealthEnabled } from './health-feature.mjs';
export async function recomputeUserHealthBaselines(
userId,
{
observationStore,
baselineStore,
eventStore,
eventNotificationService = null,
now = Date.now(),
observationLimit = 500,
eventMode = 'active',
} = {},
) {
if (!userId || !observationStore || !baselineStore) {
return { ok: false, skipped: true, reason: 'missing_dependencies' };
}
const observations = await observationStore.list(userId, { limit: observationLimit });
if (!observations.length) {
return { ok: true, skipped: true, reason: 'no_observations', userId };
}
const computed = computeHealthBaselines(observations, { now });
const persisted = await baselineStore.upsertMany(userId, computed, { computedAt: now });
let newEvents = 0;
let notificationsSent = 0;
if (eventStore) {
const detected = evaluateHealthEvents(observations, computed, { now, mode: eventMode });
for (const event of detected) {
const result = await eventStore.insertIfNew(userId, event, { now });
if (result.created) {
newEvents += 1;
if (eventNotificationService) {
const notifyResult = await eventNotificationService.notifyNewEvent(userId, event, {
eventId: result.row?.id ?? null,
});
if (notifyResult?.webCreated || notifyResult?.wechatSent) {
notificationsSent += 1;
}
}
}
}
}
return {
ok: true,
skipped: false,
userId,
observationCount: observations.length,
baselineCount: persisted.length,
newEvents,
notificationsSent,
baselines: persisted.map(baselineToApiShape),
};
}
export async function listHealthBaselineJobUserIds({
observationStore,
pool = null,
limit = 500,
} = {}) {
if (typeof observationStore?.listUserIds === 'function') {
return observationStore.listUserIds().slice(0, limit);
}
if (!pool) return [];
try {
const [rows] = await pool.query(
`SELECT id FROM h5_users WHERE status = 'active' ORDER BY updated_at DESC LIMIT ?`,
[Math.min(Math.max(limit, 1), 2000)],
);
return rows.map((row) => String(row.id));
} catch {
return [];
}
}
export async function runHealthBaselineJob({
userIds = [],
observationStore,
baselineStore,
eventStore,
eventNotificationService = null,
now = Date.now(),
env = process.env,
logger = console,
} = {}) {
if (!isMemindHealthEnabled(env)) {
return { ok: false, skipped: true, reason: 'health_disabled', results: [] };
}
if (!observationStore || !baselineStore) {
return { ok: false, skipped: true, reason: 'missing_dependencies', results: [] };
}
const targets = [...new Set((userIds ?? []).map((id) => String(id)).filter(Boolean))];
const results = [];
for (const userId of targets) {
try {
const result = await recomputeUserHealthBaselines(userId, {
observationStore,
baselineStore,
eventStore,
eventNotificationService,
now,
});
results.push(result);
} catch (error) {
logger.warn?.(`Health baseline job failed for ${userId}:`, error);
results.push({
ok: false,
skipped: false,
userId,
error: error instanceof Error ? error.message : String(error),
});
}
}
return {
ok: true,
processed: results.length,
updated: results.filter((item) => item.ok && !item.skipped).length,
newEvents: results.reduce((sum, item) => sum + Number(item.newEvents ?? 0), 0),
notificationsSent: results.reduce((sum, item) => sum + Number(item.notificationsSent ?? 0), 0),
results,
};
}