#!/usr/bin/env node /** * Standalone 103 production repair (no heavy module imports). * Dry-run by default; pass --apply to mutate database rows. */ import fs from 'node:fs'; import path from 'node:path'; import process from 'node:process'; const TANG_USER_ID = 'a70ff537-8908-486e-9b6c-042e07cc25db'; const TASKS_TO_CANCEL = [ '781a77e5-a787-4f37-b864-e21d7e918855', '9df3f89a-3a06-434d-8b4c-17b1aeb8911f', '7f953d5f-6fc3-4191-b52a-bffcef48e77f', '9b537fe8-eea2-4e39-8bcb-e8f169986327', ]; function normalizeDeliveryRelativePath(value) { const raw = String(value ?? '').trim().replace(/^\/+/, ''); if (!raw) return ''; return raw.startsWith('public/') ? raw : `public/${raw}`; } async function loadPool() { const databaseUrl = process.env.DATABASE_URL; if (!databaseUrl) throw new Error('缺少 DATABASE_URL'); const { default: mysql } = await import('mysql2/promise'); return mysql.createPool({ uri: databaseUrl, connectionLimit: 2 }); } async function markContractFailed(pool, { userId, relativePath, reason }) { const now = Date.now(); await pool.query( `UPDATE h5_page_delivery_contracts SET status = 'failed', updated_at = ?, last_error = ? WHERE user_id = ? AND workspace_relative_path = ? AND status = 'preparing'`, [now, reason, userId, relativePath], ); } async function markContractReady(pool, { userId, relativePath }) { const now = Date.now(); await pool.query( `UPDATE h5_page_delivery_contracts SET status = 'ready', updated_at = ?, last_error = NULL WHERE user_id = ? AND workspace_relative_path = ? AND status = 'preparing'`, [now, userId, relativePath], ); } async function reconcileStuckContracts(pool, h5Root, apply, limit = 50) { const [rows] = await pool.query( `SELECT user_id, workspace_relative_path FROM h5_page_delivery_contracts WHERE status = 'preparing' AND data_mode = 'static' ORDER BY updated_at ASC LIMIT ?`, [limit], ); const summary = []; for (const row of rows ?? []) { const relativePath = normalizeDeliveryRelativePath(row.workspace_relative_path); if (!relativePath) continue; const filePath = path.join(h5Root, 'MindSpace', row.user_id, relativePath); const exists = fs.existsSync(filePath); const action = exists ? 'ready' : 'failed'; summary.push({ userId: row.user_id, relativePath, fileExists: exists, action: apply ? action : `would_${action}` }); if (apply) { if (exists) { await markContractReady(pool, { userId: row.user_id, relativePath }); } else { await markContractFailed(pool, { userId: row.user_id, relativePath, reason: 'reconcile: static html missing', }); } } } return summary; } async function main() { const apply = process.argv.includes('--apply'); const h5Root = process.env.H5_ROOT || '/Users/john/Project/Memind'; const pool = await loadPool(); const now = Date.now(); const actions = []; for (const taskId of TASKS_TO_CANCEL) { const [rows] = await pool.query( `SELECT id, user_id, title, status, hour, minute FROM h5_scheduled_tasks WHERE id = ? LIMIT 1`, [taskId], ); const row = rows?.[0]; if (!row) { actions.push({ taskId, action: 'skip', reason: 'not_found' }); continue; } if (row.status === 'cancelled') { actions.push({ taskId, action: 'skip', reason: 'already_cancelled', title: row.title }); continue; } actions.push({ taskId, action: apply ? 'cancel' : 'would_cancel', title: row.title, status: row.status, schedule: `${row.hour ?? '?'}:${String(row.minute ?? 0).padStart(2, '0')}`, }); if (apply) { await pool.query( `UPDATE h5_scheduled_tasks SET status = 'cancelled', updated_at = ?, last_error = NULL WHERE id = ? AND user_id = ?`, [now, taskId, TANG_USER_ID], ); } } const [activeWeather] = await pool.query( `SELECT id, title, hour, minute, status, last_run_at FROM h5_scheduled_tasks WHERE user_id = ? AND title LIKE '%天气预报%' AND status = 'active' ORDER BY hour, created_at`, [TANG_USER_ID], ); const reconcileSummary = await reconcileStuckContracts(pool, h5Root, apply); console.log(JSON.stringify({ mode: apply ? 'apply' : 'dry-run', cancelledTasks: actions, remainingActiveWeatherTasks: activeWeather, deliveryContracts: reconcileSummary, }, null, 2)); await pool.end(); } main().catch((error) => { console.error(error); process.exit(1); });