self-check.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import fs from 'fs';
  2. import path from 'path';
  3. import { spawnSync } from 'child_process';
  4. type CheckResult = {
  5. name: string;
  6. ok: boolean;
  7. detail: string;
  8. };
  9. function parseEnvFile(filePath: string) {
  10. const env: Record<string, string> = {};
  11. if (!fs.existsSync(filePath)) return env;
  12. const content = fs.readFileSync(filePath, 'utf8');
  13. for (const rawLine of content.split(/\r?\n/)) {
  14. const line = rawLine.trim();
  15. if (!line || line.startsWith('#')) continue;
  16. const eqIndex = line.indexOf('=');
  17. if (eqIndex <= 0) continue;
  18. const key = line.slice(0, eqIndex).trim();
  19. let value = line.slice(eqIndex + 1).trim();
  20. if (
  21. (value.startsWith('"') && value.endsWith('"')) ||
  22. (value.startsWith("'") && value.endsWith("'"))
  23. ) {
  24. value = value.slice(1, -1);
  25. }
  26. env[key] = value;
  27. }
  28. return env;
  29. }
  30. async function httpJson(url: string) {
  31. const response = await fetch(url, { method: 'GET' });
  32. const text = await response.text();
  33. let json: unknown = null;
  34. try {
  35. json = text ? JSON.parse(text) : null;
  36. } catch {
  37. json = text;
  38. }
  39. return { ok: response.ok, status: response.status, body: json };
  40. }
  41. function findBrowserExecutable() {
  42. const candidates = [
  43. 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
  44. 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
  45. 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
  46. 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
  47. ];
  48. return candidates.find((item) => fs.existsSync(item)) ?? null;
  49. }
  50. function runBrowserDomCheck(url: string) {
  51. const browserPath = findBrowserExecutable();
  52. if (!browserPath) {
  53. return {
  54. ok: false,
  55. detail: 'no Chrome/Edge executable found for browser smoke test',
  56. };
  57. }
  58. const result = spawnSync(
  59. browserPath,
  60. [
  61. '--headless=new',
  62. '--disable-gpu',
  63. '--virtual-time-budget=8000',
  64. '--dump-dom',
  65. url,
  66. ],
  67. {
  68. encoding: 'utf8',
  69. timeout: 30000,
  70. },
  71. );
  72. if (result.error) {
  73. return {
  74. ok: false,
  75. detail: result.error.message,
  76. };
  77. }
  78. if (result.status !== 0) {
  79. return {
  80. ok: false,
  81. detail: result.stderr?.trim() || `browser exited with code ${result.status ?? 'unknown'}`,
  82. };
  83. }
  84. const dom = result.stdout ?? '';
  85. const hasBackendUnavailable = dom.includes('暂时无法连接后端服务');
  86. const hasLoopbackLeak = dom.includes('127.0.0.1:3001') || dom.includes('localhost:3001');
  87. const hasAlertsView =
  88. dom.includes('告警事件') &&
  89. dom.includes('当前告警池') &&
  90. !dom.includes('Internal server error');
  91. if (!hasAlertsView || hasBackendUnavailable || hasLoopbackLeak) {
  92. return {
  93. ok: false,
  94. detail: hasBackendUnavailable || hasLoopbackLeak
  95. ? 'browser rendered backend-unavailable state or loopback API leak'
  96. : 'browser DOM did not reach the alerts screen',
  97. };
  98. }
  99. return {
  100. ok: true,
  101. detail: `browser rendered alerts page successfully via ${url}`,
  102. };
  103. }
  104. async function checkDatabase(env: Record<string, string>) {
  105. const mysql = await import('mysql2/promise');
  106. const connection = await mysql.createConnection({
  107. host: env.DB_HOST ?? '127.0.0.1',
  108. port: Number(env.DB_PORT ?? '3306'),
  109. user: env.DB_USERNAME ?? 'sentai',
  110. password: env.DB_PASSWORD ?? 'sentai123',
  111. database: env.DB_DATABASE ?? 'sentai',
  112. });
  113. const requiredTables = [
  114. 'monitor_agent',
  115. 'monitor_target',
  116. 'monitor_run',
  117. 'monitor_log_event',
  118. 'monitor_alert',
  119. ];
  120. const results: CheckResult[] = [];
  121. for (const table of requiredTables) {
  122. const [rows] = await connection.query(`SHOW TABLES LIKE '${table}'`);
  123. results.push({
  124. name: `db:${table}`,
  125. ok: Array.isArray(rows) && rows.length > 0,
  126. detail:
  127. Array.isArray(rows) && rows.length > 0
  128. ? 'table exists'
  129. : 'table missing',
  130. });
  131. }
  132. await connection.end();
  133. return results;
  134. }
  135. async function main() {
  136. const root = process.cwd();
  137. const env = {
  138. ...parseEnvFile(path.join(root, '.env')),
  139. ...process.env,
  140. } as Record<string, string>;
  141. const apiBase =
  142. (env.NEXT_PUBLIC_API_BASE_URL || env.APP_API_BASE_URL || 'http://127.0.0.1:3001').replace(
  143. /\/$/,
  144. '',
  145. );
  146. const proxyBase = (env.SELF_CHECK_PROXY_BASE_URL || 'http://127.0.0.1:3010').replace(/\/$/, '');
  147. const browserUrl = (env.SELF_CHECK_BROWSER_URL || `${proxyBase}/alerts`).replace(/\/$/, '');
  148. const results: CheckResult[] = [];
  149. try {
  150. results.push(...(await checkDatabase(env)));
  151. } catch (error) {
  152. results.push({
  153. name: 'db:connection',
  154. ok: false,
  155. detail: error instanceof Error ? error.message : 'database check failed',
  156. });
  157. }
  158. const endpoints = [
  159. { name: 'api:health', url: `${apiBase}/health` },
  160. { name: 'api:monitoring-summary', url: `${apiBase}/api/monitoring/summary` },
  161. { name: 'api:monitoring-alerts', url: `${apiBase}/api/monitoring/alerts?limit=5` },
  162. { name: 'api:monitoring-runs', url: `${apiBase}/api/monitoring/runs?limit=5` },
  163. { name: 'proxy:health', url: `${proxyBase}/health` },
  164. { name: 'proxy:monitoring-alerts', url: `${proxyBase}/api/monitoring/alerts?limit=5` },
  165. ];
  166. for (const endpoint of endpoints) {
  167. try {
  168. const response = await httpJson(endpoint.url);
  169. results.push({
  170. name: endpoint.name,
  171. ok: response.ok,
  172. detail: response.ok
  173. ? `HTTP ${response.status}`
  174. : `HTTP ${response.status} ${typeof response.body === 'string' ? response.body : JSON.stringify(response.body)}`,
  175. });
  176. } catch (error) {
  177. results.push({
  178. name: endpoint.name,
  179. ok: false,
  180. detail: error instanceof Error ? error.message : 'request failed',
  181. });
  182. }
  183. }
  184. const browserCheck = runBrowserDomCheck(browserUrl);
  185. results.push({
  186. name: 'browser:alerts-page',
  187. ok: browserCheck.ok,
  188. detail: browserCheck.detail,
  189. });
  190. const failed = results.filter((item) => !item.ok);
  191. const passed = results.filter((item) => item.ok);
  192. console.log('SentAI Self Check');
  193. console.log(`API Base: ${apiBase}`);
  194. console.log(`Proxy Base: ${proxyBase}`);
  195. console.log(`Browser URL: ${browserUrl}`);
  196. console.log('');
  197. for (const item of results) {
  198. console.log(`${item.ok ? '[PASS]' : '[FAIL]'} ${item.name} - ${item.detail}`);
  199. }
  200. console.log('');
  201. console.log(`Summary: ${passed.length} passed, ${failed.length} failed`);
  202. if (failed.length) {
  203. process.exitCode = 1;
  204. }
  205. }
  206. void main();