import fs from 'fs'; import path from 'path'; import { spawnSync } from 'child_process'; type CheckResult = { name: string; ok: boolean; detail: string; }; function parseEnvFile(filePath: string) { const env: Record = {}; if (!fs.existsSync(filePath)) return env; const content = fs.readFileSync(filePath, 'utf8'); for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) continue; const eqIndex = line.indexOf('='); if (eqIndex <= 0) continue; const key = line.slice(0, eqIndex).trim(); let value = line.slice(eqIndex + 1).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } env[key] = value; } return env; } async function httpJson(url: string) { const response = await fetch(url, { method: 'GET' }); const text = await response.text(); let json: unknown = null; try { json = text ? JSON.parse(text) : null; } catch { json = text; } return { ok: response.ok, status: response.status, body: json }; } function findBrowserExecutable() { const candidates = [ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe', 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe', 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe', ]; return candidates.find((item) => fs.existsSync(item)) ?? null; } function runBrowserDomCheck(url: string) { const browserPath = findBrowserExecutable(); if (!browserPath) { return { ok: false, detail: 'no Chrome/Edge executable found for browser smoke test', }; } const result = spawnSync( browserPath, [ '--headless=new', '--disable-gpu', '--virtual-time-budget=8000', '--dump-dom', url, ], { encoding: 'utf8', timeout: 30000, }, ); if (result.error) { return { ok: false, detail: result.error.message, }; } if (result.status !== 0) { return { ok: false, detail: result.stderr?.trim() || `browser exited with code ${result.status ?? 'unknown'}`, }; } const dom = result.stdout ?? ''; const hasBackendUnavailable = dom.includes('暂时无法连接后端服务'); const hasLoopbackLeak = dom.includes('127.0.0.1:3001') || dom.includes('localhost:3001'); const hasAlertsView = dom.includes('告警事件') && dom.includes('当前告警池') && !dom.includes('Internal server error'); if (!hasAlertsView || hasBackendUnavailable || hasLoopbackLeak) { return { ok: false, detail: hasBackendUnavailable || hasLoopbackLeak ? 'browser rendered backend-unavailable state or loopback API leak' : 'browser DOM did not reach the alerts screen', }; } return { ok: true, detail: `browser rendered alerts page successfully via ${url}`, }; } async function checkDatabase(env: Record) { const mysql = await import('mysql2/promise'); const connection = await mysql.createConnection({ host: env.DB_HOST ?? '127.0.0.1', port: Number(env.DB_PORT ?? '3306'), user: env.DB_USERNAME ?? 'sentai', password: env.DB_PASSWORD ?? 'sentai123', database: env.DB_DATABASE ?? 'sentai', }); const requiredTables = [ 'monitor_agent', 'monitor_target', 'monitor_run', 'monitor_log_event', 'monitor_alert', ]; const results: CheckResult[] = []; for (const table of requiredTables) { const [rows] = await connection.query(`SHOW TABLES LIKE '${table}'`); results.push({ name: `db:${table}`, ok: Array.isArray(rows) && rows.length > 0, detail: Array.isArray(rows) && rows.length > 0 ? 'table exists' : 'table missing', }); } await connection.end(); return results; } async function main() { const root = process.cwd(); const env = { ...parseEnvFile(path.join(root, '.env')), ...process.env, } as Record; const apiBase = (env.NEXT_PUBLIC_API_BASE_URL || env.APP_API_BASE_URL || 'http://127.0.0.1:3001').replace( /\/$/, '', ); const proxyBase = (env.SELF_CHECK_PROXY_BASE_URL || 'http://127.0.0.1:3010').replace(/\/$/, ''); const browserUrl = (env.SELF_CHECK_BROWSER_URL || `${proxyBase}/alerts`).replace(/\/$/, ''); const results: CheckResult[] = []; try { results.push(...(await checkDatabase(env))); } catch (error) { results.push({ name: 'db:connection', ok: false, detail: error instanceof Error ? error.message : 'database check failed', }); } const endpoints = [ { name: 'api:health', url: `${apiBase}/health` }, { name: 'api:monitoring-summary', url: `${apiBase}/api/monitoring/summary` }, { name: 'api:monitoring-alerts', url: `${apiBase}/api/monitoring/alerts?limit=5` }, { name: 'api:monitoring-runs', url: `${apiBase}/api/monitoring/runs?limit=5` }, { name: 'proxy:health', url: `${proxyBase}/health` }, { name: 'proxy:monitoring-alerts', url: `${proxyBase}/api/monitoring/alerts?limit=5` }, ]; for (const endpoint of endpoints) { try { const response = await httpJson(endpoint.url); results.push({ name: endpoint.name, ok: response.ok, detail: response.ok ? `HTTP ${response.status}` : `HTTP ${response.status} ${typeof response.body === 'string' ? response.body : JSON.stringify(response.body)}`, }); } catch (error) { results.push({ name: endpoint.name, ok: false, detail: error instanceof Error ? error.message : 'request failed', }); } } const browserCheck = runBrowserDomCheck(browserUrl); results.push({ name: 'browser:alerts-page', ok: browserCheck.ok, detail: browserCheck.detail, }); const failed = results.filter((item) => !item.ok); const passed = results.filter((item) => item.ok); console.log('SentAI Self Check'); console.log(`API Base: ${apiBase}`); console.log(`Proxy Base: ${proxyBase}`); console.log(`Browser URL: ${browserUrl}`); console.log(''); for (const item of results) { console.log(`${item.ok ? '[PASS]' : '[FAIL]'} ${item.name} - ${item.detail}`); } console.log(''); console.log(`Summary: ${passed.length} passed, ${failed.length} failed`); if (failed.length) { process.exitCode = 1; } } void main();