#!/usr/bin/env node import fsp from 'node:fs/promises'; import path from 'node:path'; import { execFile } from 'node:child_process'; const apiUrl = process.env.ERROR_LOG_API_URL || 'http://127.0.0.1:3050/internal/error-logs'; const token = process.env.ERROR_LOG_API_TOKEN || process.env.INTERNAL_API_TOKEN || ''; const stateFile = process.env.ERROR_LOG_POLL_STATE_FILE || path.join(process.cwd(), '.error-log-poll-state.json'); const pollIntervalMs = Number(process.env.ERROR_LOG_POLL_INTERVAL_MS || 60000); const limit = Number(process.env.ERROR_LOG_POLL_LIMIT || 50); const enableNotify = process.env.ERROR_LOG_NOTIFY === 'true'; const startFromBeginning = process.env.ERROR_LOG_START_FROM_BEGINNING === 'true'; async function loadState() { try { return { isNew: false, state: JSON.parse(await fsp.readFile(stateFile, 'utf8')), }; } catch (error) { return { isNew: true, state: { sinceId: process.env.ERROR_LOG_SINCE_ID === undefined ? null : Number(process.env.ERROR_LOG_SINCE_ID || 0), }, }; } } async function saveState(state) { await fsp.writeFile(stateFile, JSON.stringify(state, null, 2)); } 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}` : ''; return [ `[${item.ID}] ${item.Source} ${item.Status} x${item.Count}`, item.Message, location, request.trim(), ].filter(Boolean).join('\n'); } function notify(item) { if (!enableNotify || process.platform !== 'darwin') { return; } const title = `New ${item.Source} error #${item.ID}`; const message = String(item.Message || '').replace(/"/g, '\\"').slice(0, 180); execFile('osascript', [ '-e', `display notification "${message}" with title "${title}"`, ]); } function buildApiUrl({ sinceId = 0, latest = false } = {}) { const url = new URL(apiUrl); if (latest) { url.pathname = url.pathname.replace(/\/$/, '') + '/latest'; } else { url.searchParams.set('since_id', sinceId); } url.searchParams.set('status', 'all'); url.searchParams.set('limit', limit); return url; } async function fetchErrorLogs(url) { if (!token) { throw new Error('ERROR_LOG_API_TOKEN or INTERNAL_API_TOKEN is required'); } const response = await fetch(url, { headers: { 'x-internal-token': token, }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${await response.text()}`); } const data = await response.json(); if (data.errcode !== 10000) { throw new Error(data.errMsg || `Unexpected response: ${JSON.stringify(data)}`); } return data.result?.list || []; } async function fetchLatestSinceId() { const list = await fetchErrorLogs(buildApiUrl({ latest: true })); return list.reduce((maxId, item) => Math.max(maxId, Number(item.ID || 0)), 0); } async function pollOnce(state) { const list = await fetchErrorLogs(buildApiUrl({ sinceId: state.sinceId || 0 })); if (list.length === 0) { return; } for (const item of list) { console.log('\n' + formatLog(item)); notify(item); state.sinceId = Math.max(Number(state.sinceId || 0), Number(item.ID || 0)); } await saveState(state); } async function main() { const loaded = await loadState(); const state = loaded.state; if (loaded.isNew && state.sinceId === null) { state.sinceId = startFromBeginning ? 0 : await fetchLatestSinceId(); await saveState(state); } console.log(`[error-log-poller] polling ${apiUrl} every ${pollIntervalMs}ms from ID ${state.sinceId || 0}`); await pollOnce(state); setInterval(() => { pollOnce(state).catch((error) => { console.error('[error-log-poller] poll failed:', error.message || error); }); }, pollIntervalMs); } main().catch((error) => { console.error('[error-log-poller] fatal:', error.message || error); process.exit(1); });