| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- #!/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);
- });
|