#!/usr/bin/env node import { createServer } from 'node:http'; const APP_ID = process.env.XIAOSHU_PARSE_APP_ID || '7pIbDBJmKx_main'; const FUNCTION_URL = (process.env.XIAOSHU_FUNCTION_URL || 'https://server.xiaoshu.pro/api/functions').replace(/\/$/, ''); const SESSION_TOKEN = String(process.env.XIAOSHU_SYNC_OPERATOR_TOKEN || '').trim(); const INTERVAL_MS = Math.max(5_000, Number(process.env.XIAOSHU_SYNC_INTERVAL_MS || 5_000)); const HEALTH_PORT = Math.max(0, Number(process.env.XIAOSHU_SYNC_HEALTH_PORT || 9087)); const DATASETS = String(process.env.XIAOSHU_SYNC_DATASETS || 'learning-records,practice-records,memory-records,assessments,course-bindings,appointments,lessons') .split(',').map((value) => value.trim()).filter(Boolean); const ONCE = process.argv.includes('--once'); if (!SESSION_TOKEN) throw new Error('缺少 XIAOSHU_SYNC_OPERATOR_TOKEN;同步运行器必须使用超级管理员会话'); if (!DATASETS.length) throw new Error('XIAOSHU_SYNC_DATASETS 不能为空'); const status = { startedAt: new Date().toISOString(), running: false, lastStartedAt: '', lastFinishedAt: '', lastSuccessAt: '', consecutiveFailures: 0, slowTicks: 0, datasets: {}, }; async function call(params) { const response = await fetch(`${FUNCTION_URL}/xiaoshu/ops/gateway-v3`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': APP_ID }, body: JSON.stringify({ token: SESSION_TOKEN, params }), signal: AbortSignal.timeout(90_000), }); const payload = await response.json().catch(() => ({})); if (!response.ok || payload.success !== true) throw new Error(`${response.status}: ${payload.message || payload.error || '同步云函数执行失败'}`); return payload.data; } async function syncDataset(dataset) { const startedAt = Date.now(); try { const result = await call({ operation: 'ops/sync/run', dataset, maxPages: 2, pageSize: 500 }); status.datasets[dataset] = { ok: true, elapsedMs: Date.now() - startedAt, ...result }; return result; } catch (error) { status.datasets[dataset] = { ok: false, elapsedMs: Date.now() - startedAt, error: String(error?.message || error).slice(0, 500) }; throw error; } } async function tick() { if (status.running) return; const tickStartedAt = Date.now(); status.running = true; status.lastStartedAt = new Date().toISOString(); const results = await Promise.allSettled(DATASETS.map(syncDataset)); const failures = results.filter((result) => result.status === 'rejected'); status.lastFinishedAt = new Date().toISOString(); status.running = false; if (failures.length) { status.consecutiveFailures += 1; console.error(`[legacy-sync] ${status.lastFinishedAt} ${failures.length}/${DATASETS.length} 个数据集失败`); process.exitCode = ONCE ? 1 : 0; } else { status.consecutiveFailures = 0; status.lastSuccessAt = status.lastFinishedAt; const elapsedMs = Date.now() - tickStartedAt; if (elapsedMs > 10_000) { status.slowTicks += 1; console.warn(`[legacy-sync] ${status.lastFinishedAt} 同步耗时 ${elapsedMs}ms,已超过 10 秒目标`); } else { status.slowTicks = 0; } console.log(`[legacy-sync] ${status.lastFinishedAt} ${DATASETS.length} 个数据集同步完成,耗时 ${elapsedMs}ms`); } } if (!ONCE && HEALTH_PORT) { createServer((request, response) => { if (request.url !== '/health') { response.writeHead(404).end(); return; } const lastSuccessAge = status.lastSuccessAt ? Math.floor((Date.now() - new Date(status.lastSuccessAt).getTime()) / 1000) : null; const healthy = lastSuccessAge !== null && lastSuccessAge <= 15 && status.consecutiveFailures === 0; response.writeHead(healthy ? 200 : 503, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' }); response.end(JSON.stringify({ healthy, lastSuccessAge, intervalMs: INTERVAL_MS, ...status })); }).listen(HEALTH_PORT, '127.0.0.1', () => console.log(`[legacy-sync] 健康检查监听 http://127.0.0.1:${HEALTH_PORT}/health`)); } await tick(); if (!ONCE) setInterval(() => void tick(), INTERVAL_MS);