d820247aa1
Memind CI / Test, build, and release guards (push) Successful in 3m32s
Treat deferred/skipped customer-service sends as failures so reminder worker does not mark reminders sent, and add a 103 read-only health check script. Co-authored-by: Cursor <cursoragent@cursor.com>
253 lines
7.6 KiB
JavaScript
253 lines
7.6 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* 103 服务号定时提醒健康巡检(只读)
|
|
*
|
|
* 用法:
|
|
* node scripts/check-schedule-reminder-health-103.mjs
|
|
* DATABASE_URL=... node scripts/check-schedule-reminder-health-103.mjs
|
|
*
|
|
* 在 103 上可配合:
|
|
* cd /Users/john/Project/Memind && node scripts/check-schedule-reminder-health-103.mjs
|
|
*/
|
|
import process from 'node:process';
|
|
import mysql from 'mysql2/promise';
|
|
import { isScheduledTaskWorkerEnabled } from '../scheduled-task-worker-config.mjs';
|
|
import { isWechatNewsMorningDraftWorkerEnabled } from '../wechat-news-morning-draft-worker-config.mjs';
|
|
import { loadH5Environment } from './load-env.mjs';
|
|
|
|
loadH5Environment(import.meta.dirname);
|
|
|
|
const env = process.env;
|
|
const now = Date.now();
|
|
const dayMs = 24 * 60 * 60 * 1000;
|
|
const hourMs = 60 * 60 * 1000;
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
let warned = 0;
|
|
|
|
function pass(label, detail = '') {
|
|
passed += 1;
|
|
console.log(`✔ ${label}${detail ? `: ${detail}` : ''}`);
|
|
}
|
|
|
|
function fail(label, detail = '') {
|
|
failed += 1;
|
|
console.error(`✘ ${label}${detail ? `: ${detail}` : ''}`);
|
|
}
|
|
|
|
function warn(label, detail = '') {
|
|
warned += 1;
|
|
console.warn(`△ ${label}${detail ? `: ${detail}` : ''}`);
|
|
}
|
|
|
|
function envFlag(name) {
|
|
return String(env[name] ?? '').trim();
|
|
}
|
|
|
|
function envEnabled(name) {
|
|
return envFlag(name) === '1';
|
|
}
|
|
|
|
async function scalar(pool, sql, params = []) {
|
|
const [rows] = await pool.query(sql, params);
|
|
return Number(rows?.[0]?.c ?? rows?.[0]?.count ?? 0);
|
|
}
|
|
|
|
async function checkEnvironment() {
|
|
console.log('\n=== 环境变量 ===\n');
|
|
|
|
if (envEnabled('H5_WECHAT_MP_ENABLED')) pass('H5_WECHAT_MP_ENABLED=1');
|
|
else fail('H5_WECHAT_MP_ENABLED=1');
|
|
|
|
if (envEnabled('H5_SCHEDULE_ENABLED')) pass('H5_SCHEDULE_ENABLED=1');
|
|
else fail('H5_SCHEDULE_ENABLED=1');
|
|
|
|
if (envEnabled('H5_REMINDER_WORKER_ENABLED')) pass('H5_REMINDER_WORKER_ENABLED=1');
|
|
else fail('H5_REMINDER_WORKER_ENABLED=1');
|
|
|
|
if (isScheduledTaskWorkerEnabled(env)) {
|
|
pass('Scheduled task worker enabled');
|
|
} else {
|
|
warn('Scheduled task worker disabled', 'H5_SCHEDULED_TASK_WORKER_ENABLED=0 且 H5_REMINDER_WORKER_ENABLED≠1');
|
|
}
|
|
|
|
if (isWechatNewsMorningDraftWorkerEnabled(env)) {
|
|
pass('News morning draft worker enabled');
|
|
} else {
|
|
warn('News morning draft worker disabled');
|
|
}
|
|
|
|
if (envFlag('H5_DEFAULT_TIMEZONE')) {
|
|
pass('H5_DEFAULT_TIMEZONE', envFlag('H5_DEFAULT_TIMEZONE'));
|
|
} else {
|
|
warn('H5_DEFAULT_TIMEZONE 未设置', '默认 Asia/Shanghai');
|
|
}
|
|
|
|
const passiveCandidate =
|
|
envFlag('MEMIND_PORTAL_RUNTIME_ROLE') === 'candidate'
|
|
&& envFlag('MEMIND_CANARY_PASSIVE_RUNTIME') !== '0';
|
|
if (passiveCandidate) {
|
|
fail('Passive canary runtime', 'worker 会被禁用,不应承载定时提醒');
|
|
} else {
|
|
pass('非 passive canary runtime');
|
|
}
|
|
}
|
|
|
|
async function checkDatabase(pool) {
|
|
console.log('\n=== 数据库指标(只读)===\n');
|
|
|
|
const overduePending = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_schedule_reminders
|
|
WHERE status = 'pending' AND remind_at < ?`,
|
|
[now - hourMs],
|
|
);
|
|
if (overduePending === 0) pass('无 overdue pending 提醒');
|
|
else fail('overdue pending 提醒', String(overduePending));
|
|
|
|
const stuckLocked = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_schedule_reminders
|
|
WHERE status = 'locked' AND locked_until IS NOT NULL AND locked_until < ?`,
|
|
[now],
|
|
);
|
|
if (stuckLocked === 0) pass('无 stuck locked 提醒');
|
|
else fail('stuck locked 提醒', String(stuckLocked));
|
|
|
|
const failedReminders24h = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_schedule_reminders
|
|
WHERE status = 'failed' AND updated_at >= ?`,
|
|
[now - dayMs],
|
|
);
|
|
if (failedReminders24h === 0) pass('24h 内无 failed 提醒');
|
|
else warn('24h 内 failed 提醒', String(failedReminders24h));
|
|
|
|
const deliveryFailed24h = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_schedule_delivery_logs
|
|
WHERE status = 'failed' AND created_at >= ?`,
|
|
[now - dayMs],
|
|
);
|
|
if (deliveryFailed24h === 0) pass('24h 内无 failed delivery log');
|
|
else warn('24h 内 failed delivery log', String(deliveryFailed24h));
|
|
|
|
const deliverySuccess24h = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_schedule_delivery_logs
|
|
WHERE status = 'success' AND created_at >= ?`,
|
|
[now - dayMs],
|
|
);
|
|
pass('24h delivery success 计数', String(deliverySuccess24h));
|
|
|
|
const deferredQueue = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_wechat_mp_deferred_delivery`,
|
|
);
|
|
if (deferredQueue === 0) pass('deferred 队列为空');
|
|
else warn('deferred 队列积压', String(deferredQueue));
|
|
|
|
const activeDigests = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_schedule_digest_subscriptions WHERE status = 'active'`,
|
|
);
|
|
pass('active 待办摘要订阅', String(activeDigests));
|
|
|
|
const activeScheduledTasks = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_scheduled_tasks WHERE status = 'active'`,
|
|
);
|
|
pass('active 定时自动任务', String(activeScheduledTasks));
|
|
|
|
const failedTasks24h = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_scheduled_tasks
|
|
WHERE status = 'failed' AND updated_at >= ?`,
|
|
[now - dayMs],
|
|
);
|
|
if (failedTasks24h === 0) pass('24h 内无 failed 定时任务');
|
|
else warn('24h 内 failed 定时任务', String(failedTasks24h));
|
|
|
|
const morningReminders = await scalar(
|
|
pool,
|
|
`SELECT COUNT(*) AS c FROM h5_schedule_items
|
|
WHERE status = 'active'
|
|
AND deleted_at IS NULL
|
|
AND JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.source')) = 'subscribe_morning_reminder'`,
|
|
);
|
|
pass('active 早安提醒订阅', String(morningReminders));
|
|
|
|
const [recentFailedDeliveries] = await pool.query(
|
|
`SELECT d.created_at, d.error_message, u.username, d.channel
|
|
FROM h5_schedule_delivery_logs d
|
|
JOIN h5_users u ON u.id = d.user_id
|
|
WHERE d.status = 'failed' AND d.created_at >= ?
|
|
ORDER BY d.created_at DESC
|
|
LIMIT 5`,
|
|
[now - dayMs],
|
|
);
|
|
if (recentFailedDeliveries.length > 0) {
|
|
console.log('\n--- 最近失败投递(最多 5 条)---');
|
|
for (const row of recentFailedDeliveries) {
|
|
console.log(
|
|
` ${new Date(Number(row.created_at)).toISOString()} ${row.username} ${row.channel} ${row.error_message ?? ''}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const [overdueSamples] = await pool.query(
|
|
`SELECT r.id, r.remind_at, r.attempts, r.last_error, u.username, i.title
|
|
FROM h5_schedule_reminders r
|
|
JOIN h5_schedule_items i ON i.id = r.item_id
|
|
JOIN h5_users u ON u.id = r.user_id
|
|
WHERE r.status = 'pending' AND r.remind_at < ?
|
|
ORDER BY r.remind_at ASC
|
|
LIMIT 5`,
|
|
[now - hourMs],
|
|
);
|
|
if (overdueSamples.length > 0) {
|
|
console.log('\n--- overdue pending 样本(最多 5 条)---');
|
|
for (const row of overdueSamples) {
|
|
console.log(
|
|
` ${row.username} "${row.title}" attempts=${row.attempts} remind_at=${new Date(Number(row.remind_at)).toISOString()} ${row.last_error ?? ''}`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
console.log('=== 103 服务号定时提醒健康巡检 ===');
|
|
console.log(`时间: ${new Date(now).toISOString()}`);
|
|
|
|
await checkEnvironment();
|
|
|
|
if (!env.DATABASE_URL) {
|
|
fail('DATABASE_URL', '未配置,跳过数据库检查');
|
|
summarize();
|
|
process.exit(1);
|
|
}
|
|
|
|
const pool = mysql.createPool({ uri: env.DATABASE_URL, connectionLimit: 2 });
|
|
try {
|
|
await checkDatabase(pool);
|
|
} finally {
|
|
await pool.end();
|
|
}
|
|
|
|
summarize();
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
function summarize() {
|
|
console.log('\n=== 汇总 ===');
|
|
console.log(`通过: ${passed}`);
|
|
console.log(`警告: ${warned}`);
|
|
console.log(`失败: ${failed}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|