#!/usr/bin/env node import fsp from 'node:fs/promises'; import path from 'node:path'; import { execFile } from 'node:child_process'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const monitorDir = __dirname; const dataDir = path.join(monitorDir, 'data'); const envFile = path.join(monitorDir, '.env'); function parseEnv(content) { const env = {}; for (const line of content.split(/\r?\n/)) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) { continue; } const index = trimmed.indexOf('='); if (index === -1) { continue; } const key = trimmed.slice(0, index).trim(); const value = trimmed.slice(index + 1).trim().replace(/^['"]|['"]$/g, ''); env[key] = value; } return env; } async function loadEnv() { const content = await fsp.readFile(envFile, 'utf8'); return { ...process.env, ...parseEnv(content), }; } async function readJson(filePath, fallback) { try { return JSON.parse(await fsp.readFile(filePath, 'utf8')); } catch (error) { return fallback; } } async function writeJson(filePath, value) { await fsp.mkdir(path.dirname(filePath), { recursive: true }); await fsp.writeFile(filePath, JSON.stringify(value, null, 2)); } function execFileAsync(command, args, options = {}) { return new Promise((resolve, reject) => { execFile(command, args, options, (error, stdout, stderr) => { if (error) { error.stdout = stdout; error.stderr = stderr; reject(error); return; } resolve({ stdout, stderr }); }); }); } function shellQuote(value) { return `'${String(value).replace(/'/g, `'\\''`)}'`; } function makeUrl(baseUrl, params) { const url = new URL(baseUrl); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null && value !== '') { url.searchParams.set(key, value); } } return url.toString(); } async function fetchViaSsh(env, url) { const sshHost = env.ERROR_LOG_SSH_HOST; if (!sshHost) { throw new Error('ERROR_LOG_SSH_HOST is required for one-click check'); } const remoteCommand = [ 'curl', '-sS', '--max-time', String(env.ERROR_LOG_CURL_TIMEOUT || 15), '-H', `x-internal-token: ${env.ERROR_LOG_API_TOKEN || env.INTERNAL_API_TOKEN || ''}`, url, ].map(shellQuote).join(' '); const sshArgs = [ '-o', `ConnectTimeout=${env.ERROR_LOG_SSH_CONNECT_TIMEOUT || 8}`, sshHost, remoteCommand, ]; const { stdout } = await execFileAsync('ssh', sshArgs); return JSON.parse(stdout); } function formatLog(item) { const location = item.FilePath ? `${item.FilePath}:${item.LineNumber || 0}:${item.ColumnNumber || 0}` : 'unknown location'; const request = item.RequestUrl ? `${item.RequestMethod || 'REQUEST'} ${item.RequestUrl}` : ''; const time = item.LastSeenAt || item.CreateTime || ''; return [ `#${item.ID} ${item.Source || 'error'} ${item.Status || ''} x${item.Count || 1} ${time}`, item.Message || '', location, request, ].filter(Boolean).join('\n'); } async function main() { const env = await loadEnv(); const token = env.ERROR_LOG_API_TOKEN || env.INTERNAL_API_TOKEN || ''; if (!token) { throw new Error('ERROR_LOG_API_TOKEN is required in local-error-monitor/.env'); } const stateFile = env.ERROR_LOG_CHECK_STATE_FILE || path.join(dataDir, 'check-state.json'); const outputFile = env.ERROR_LOG_CHECK_OUTPUT_FILE || path.join(dataDir, 'recent-error-check.txt'); const state = await readJson(stateFile, { sinceId: Number(env.ERROR_LOG_SINCE_ID || 0) }); const limit = Math.min(Math.max(Number(env.ERROR_LOG_CHECK_LIMIT || 20), 1), 100); const apiBaseUrl = env.ERROR_LOG_REMOTE_API_URL || 'http://127.0.0.1:3050/internal/error-logs'; const url = makeUrl(apiBaseUrl, { since_id: state.sinceId || 0, status: env.ERROR_LOG_CHECK_STATUS || 'open,reopened', limit, }); const data = await fetchViaSsh(env, url); if (data.errcode !== 10000) { throw new Error(data.errMsg || `Unexpected response: ${JSON.stringify(data)}`); } const list = data.result?.list || []; const checkedAt = new Date().toISOString(); let output; if (list.length === 0) { output = `[${checkedAt}] No new server errors since ID ${state.sinceId || 0}.`; } else { const nextSinceId = list.reduce((maxId, item) => Math.max(maxId, Number(item.ID || 0)), Number(state.sinceId || 0)); state.sinceId = nextSinceId; await writeJson(stateFile, state); output = [ `[${checkedAt}] Found ${list.length} new server error(s). Next since ID: ${nextSinceId}`, '', ...list.map(formatLog), ].join('\n\n'); } await fsp.mkdir(dataDir, { recursive: true }); await fsp.writeFile(outputFile, output + '\n'); console.log(output); console.log(`\nSaved to: ${outputFile}`); } main().catch((error) => { console.error('[error-log-check] failed:', error.message || error); if (error.stderr) { console.error(error.stderr); } process.exit(1); });