From 8ece4b1850538df483158c0b560a7c0ad90d7352 Mon Sep 17 00:00:00 2001 From: caadiq Date: Wed, 22 Apr 2026 11:38:37 +0900 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=EB=B4=87=20=EC=97=B0=EC=86=8D?= =?UTF-8?q?=20=EC=98=A4=EB=A5=98=20=EC=8B=9C=20=EC=9E=90=EB=8F=99=20?= =?UTF-8?q?=EC=A0=95=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - consecutiveErrors 카운터로 실패 횟수 추적 (성공 시 0으로 리셋) - 동일 에러 루프에서 sync/error 로그는 첫 1회만 기록하여 스팸 방지 - 10회 연속 실패 시 stopBot 호출 및 bot/stop 로그 1건 남김 - docs/logs.md, docs/development.md 관련 설명 추가 Co-Authored-By: Claude Opus 4.7 (1M context) --- backend/src/plugins/scheduler.js | 41 +++++++++++++++++++++++++------- docs/development.md | 16 +++++++++++++ docs/logs.md | 9 +++++++ 3 files changed, 58 insertions(+), 8 deletions(-) diff --git a/backend/src/plugins/scheduler.js b/backend/src/plugins/scheduler.js index 3cceb90..2dbad9d 100644 --- a/backend/src/plugins/scheduler.js +++ b/backend/src/plugins/scheduler.js @@ -7,6 +7,7 @@ import { logActivity } from '../utils/log.js'; const REDIS_PREFIX = 'bot:status:'; const TIMEZONE = 'Asia/Seoul'; +const MAX_CONSECUTIVE_ERRORS = 10; async function schedulerPlugin(fastify, opts) { const tasks = new Map(); @@ -121,6 +122,7 @@ async function schedulerPlugin(fastify, opts) { totalAdded: 0, lastSyncDuration: null, errorMessage: null, + consecutiveErrors: 0, }; } @@ -150,6 +152,7 @@ async function schedulerPlugin(fastify, opts) { const updateData = { lastCheckAt: nowKST(), totalAdded: (status.totalAdded || 0) + result.addedCount, + consecutiveErrors: 0, }; if (setRunningStatus) { updateData.status = 'running'; @@ -214,19 +217,41 @@ async function schedulerPlugin(fastify, opts) { }); } } catch (err) { + const prev = await getStatus(botId); + const consecutiveErrors = (prev.consecutiveErrors || 0) + 1; await updateStatus(botId, { status: 'error', lastCheckAt: nowKST(), errorMessage: err.message, + consecutiveErrors, }); - fastify.log.error(`[${botId}] 동기화 오류: ${err.message}`); - logActivity(fastify.db, { - actor: botId, - action: 'error', - category: 'sync', - summary: `${botId} 동기화 오류: ${err.message}`, - details: { error: err.message }, - }); + fastify.log.error(`[${botId}] 동기화 오류 (${consecutiveErrors}/${MAX_CONSECUTIVE_ERRORS}): ${err.message}`); + // 첫 오류만 activity log에 기록 (중복 스팸 방지) + if (consecutiveErrors === 1) { + logActivity(fastify.db, { + actor: botId, + action: 'error', + category: 'sync', + summary: `${botId} 동기화 오류: ${err.message}`, + details: { error: err.message }, + }); + } + // 임계값 도달 시 봇 자동 정지 + if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { + fastify.log.warn(`[${botId}] 연속 ${MAX_CONSECUTIVE_ERRORS}회 실패 - 자동 정지`); + logActivity(fastify.db, { + actor: botId, + action: 'stop', + category: 'bot', + summary: `${botId} 연속 ${MAX_CONSECUTIVE_ERRORS}회 실패로 자동 정지`, + details: { error: err.message, consecutiveErrors }, + }); + try { + await stopBot(botId); + } catch (stopErr) { + fastify.log.error(`[${botId}] 자동 정지 실패: ${stopErr.message}`); + } + } } }, { timezone: TIMEZONE }); diff --git a/docs/development.md b/docs/development.md index 12d17ac..6f5bb91 100644 --- a/docs/development.md +++ b/docs/development.md @@ -283,6 +283,22 @@ queryClient.invalidateQueries(); --- +## X 봇 / Nitter + +X 봇은 `/docker/nitter/`의 Nitter 인스턴스(zedeus/nitter)를 스크래핑하여 트윗을 수집합니다. 백엔드는 `NITTER_URL`(기본값 `http://nitter:8080`)로 접속합니다. + +### 세션 관리 (`sessions.jsonl`) +X는 비로그인 API 접근을 막고 있어, Nitter는 `/docker/nitter/sessions.jsonl`에 저장된 실제 X 계정 쿠키(`auth_token`, `ct0`)로 요청을 보냅니다. + +- 세션이 만료/차단되면 Nitter 측에서 `no sessions available for API` 로그가 찍히고 SIGSEGV로 크래시 → 백엔드에서 `[x-N] 동기화 오류: 요청 타임아웃` 반복 (단, 연속 10회 실패 시 자동 정지 — `logs.md` 참조) +- `renew_sessions.py`가 매시 세션을 점검하지만, 판별 기준(`check_nitter()`)이 약하면 만료 상태에서도 "정상"으로 오판할 수 있음 → 기준은 트윗 본문(`tweet-content` 블록) 렌더 여부로 유지할 것 +- 수동 갱신: `python3 /docker/nitter/create_session_curl.py ` 로 새 쿠키 발급 후 `sessions.jsonl` 두 줄을 덮어쓰고 `docker compose restart nitter` 실행 + +### 포크 관련 메모 +`unixfox/nitter` 같은 구버전 기반 포크는 sessions.jsonl을 아예 인식하지 못해 트윗 수집이 불가능합니다. 교체 시에는 바이너리에 sessions 처리 심볼이 있는지 확인할 것(예: `strings nitter | grep sessions.jsonl`). + +--- + ## 활동 로그 시스템 관리자/봇의 모든 활동을 `logs` 테이블에 기록하고 관리자 페이지에서 조회. diff --git a/docs/logs.md b/docs/logs.md index d3e8854..da2e101 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -100,6 +100,15 @@ logActivity(db, { actor, action, category, targetType, targetId, summary, detail > **봇 로그 전략:** 변화 없는 동기화는 로그 안 남김. `addedCount > 0`이거나 에러인 경우만 기록. +### 연속 오류 시 자동 정지 + +`plugins/scheduler.js`의 `MAX_CONSECUTIVE_ERRORS`(기본 10)로 제어. + +- Redis의 bot status에 `consecutiveErrors` 카운터를 유지. 성공 시 0으로 리셋, 실패 시 +1. +- 동일한 에러 루프에서 `sync/error` 로그는 **첫 1회만** 기록 (로그 테이블 스팸 방지). +- 카운터가 `MAX_CONSECUTIVE_ERRORS`에 도달하면 `stopBot()`을 호출해 cron 태스크를 내리고, `bot/stop` action으로 *"${botId} 연속 N회 실패로 자동 정지"* 로그 1건을 남김. +- 자동 정지된 봇은 원인 조치 후 관리자 UI에서 수동으로 다시 시작해야 함. + --- ## 프론트엔드 구현