| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233 |
- 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<string, string> = {};
- 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<string, string>) {
- 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<string, string>;
- 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();
|