이 저장소가 대체 무엇인가.
ai-goofish-monitor는 闲鱼(셴위, 알리바바가 운영하는 중국 최대 개인 간 중고거래 앱)에서 특정 키워드의 새 매물을 실시간·예약으로 감시하고, 각 매물을 비전 LLM(이미지 판독이 되는 대형언어모델) 이 사용자가 설정한 구매 기준으로 자동 심사해, "살 만하다"고 판단한 매물만 휴대폰 알림으로 보내주는 프로그램이다. 모든 조작은 웹 관리 UI(대시보드)에서 이뤄진다.
중고장터에서 좋은 물건을 잡으려면 남보다 빨라야 한다. 하지만 사람은 잠도 자야 하고, 매물 사진과 판매자 이력을 일일이 뜯어볼 시간도 없다. 이 도구는 장터 입구(검색 결과)에 잠복한 감정사다. 새 물건이 들어오는 즉시 사진을 살펴보고, 판매자의 거래 이력·신용도까지 조회한 뒤, "이건 급매 진품, 즉시 연락하세요" 같은 판정을 문자로 보내준다.
핵심은 그 감정사가 사람이 아니라 멀티모달 LLM이라는 점, 그리고 장터가 "자동화 봇 출입금지" 팻말(안티봇)을 붙여둬서, 감정사가 손님인 척 위장(실제 브라우저 지문 복제)하고 들어가야 한다는 점이다.
코드 내부 버전은 2.0.0(FastAPI 앱 타이틀 기준)이며, AI 심사 페르소나의 코드네임은 EagleEye-V6.4다. 라이선스는 관대한 MIT(원저작권자 dingyufei615, 현재 Usagi-org 조직 호스팅). 별 13,200개·포크 1,900개로 인기가 검증됐지만, 현재 GitHub에서 저장소가 archived(보관·읽기전용) 상태라는 점은 유의해야 한다(추가 업데이트·보안 패치가 멈춤).
이 도구는 마켓플레이스의 비공개 내부 API를 가로채고, 로그인 쿠키를 복제하며, 안티봇 탐지를 능동적으로 회피한다. 이는 闲鱼/타오바오 이용약관 위반일 가능성이 크고, 과도한 요청은 계정 정지·서버 부하를 부른다. 저장소 자체도 DISCLAIMER.md에서 "학습·연구 목적, robots.txt·약관 준수, 불법 사용 금지"를 명시한다. 이 문서는 비동기 스크래핑 · 안티봇 공방 · LLM 파이프라인 · 풀스택 아키텍처를 학습하기 위한 자료이며, 특정 플랫폼을 무단 수집하라는 뜻이 아니다.
트렌딩 이유와, 흔한 "크롤러 스크립트"와의 결정적 차이.
대부분의 매물 알리미는 키워드·가격 범위 같은 단순 규칙으로 거른다. 이 프로젝트는 한 발 더 나아가, 매물 사진 + 상세정보 + 판매자 이력을 통째로 비전 LLM에게 주고 "사람처럼 판단"하게 한다. 예컨대 "겉보기 개인 판매자지만 실제로는 되팔이(업자)인지"를 판매자의 구매·판매·평가 이력에서 행동 서사를 재구성해 가려낸다. 프롬프트에는 "PERSONA-FIRST 원칙", "이미지 우선 원칙", "한 방에 탈락시키는 deal-breaker 규칙" 같은 실전 프롬프트 엔지니어링이 담겨 있다.
보통 알리미 = "삼성 냉장고 30만원 이하"라고 필터를 걸어두면 조건만 맞으면 다 알림이 온다(사기·업자·하자 매물 포함). 이 프로젝트 = "냉장고 사진의 성에·녹, 판매자가 최근 5개를 팔았는지, 신용등급은 어떤지"까지 감정사가 눈으로 보고 걸러, 진짜 살 만한 것만 통과시킨다.
闲鱼는 자동화를 막으려고 여러 방어선을 친다. 이 프로젝트는 그걸 계층적으로 돌파한다.
page.on("response")로 낚아챔(견고)즉, "서버가 막으면 어떻게 우회하나"의 여러 계층을 한 코드베이스에서 관찰할 수 있다.
루트의 spider_v2.py 한 파일이 전부가 아니다. 112개 Python + 79개 Vue + 46개 TypeScript 파일이 api → services → domain → infrastructure 4계층(DDD풍)으로 나뉘고, WebSocket 실시간 로그, APScheduler 크론 예약, 6종 알림 채널, SQLite 저장, Docker 멀티스테이지 빌드, 40여 개 테스트(실트래픽 스모크 포함)까지 갖췄다. "크롤러 하나"를 어떻게 운영 가능한 제품으로 키우는지 보여주는 표본이다.
대상이 闲鱼라는 점은 부차적이다. 진짜 가치는 비동기 스크래핑 · 멀티모달 LLM 통합(및 호환성 레이어) · 프로세스 격리 · 계층형 백엔드 · Vue 3 SPA · 실시간 관제(WebSocket) · 안티봇 대응을 실전 규모의 코드로 통째로 볼 수 있다는 것.
무엇으로 만들어졌나 — 계층별로. (requirements.txt·package.json에서 실제 확인)
| 기술 | 역할 | 왜 이걸 썼나 |
|---|---|---|
| Python 3.10+ | 백엔드 언어 | 스크래핑·AI 생태계가 가장 풍부(Docker는 3.11-slim) |
| FastAPI + uvicorn | 웹 API + SPA 서빙 | async 네이티브, 하나의 src/app.py가 API와 Vue 빌드물을 함께 제공 |
| Playwright (async) | 스크래핑 엔진 | 실제 Chromium을 몰아 안티봇 우회·API 가로채기 |
| openai SDK (AsyncOpenAI) | LLM 호출 | OpenAI 호환 엔드포인트면 무엇이든(모델스코프/제미니/딥시크…) 붙음 |
| APScheduler (AsyncIOScheduler) | 예약 실행 | 크론식으로 감시 작업을 주기 실행 |
| pydantic / pydantic-settings | 설정·도메인 모델 | 타입 안전한 env 설정과 Task 엔티티 검증 |
| SQLite (stdlib) | 주 저장소 | tasks·result_items·price_snapshots를 파일 DB로(무설치) |
그 밖에 requests(이미지 다운로드를 executor로), httpx[socks]+python-socks(프록시), aiofiles, Pillow, pyzbar+qrcode(QR 로그인 처리), jinja2를 쓴다. 테스트는 pytest·pytest-asyncio·coverage.
| 기술 | 역할 | 메모 |
|---|---|---|
| Vue 3 (^3.5) | SPA 프레임워크 | Composition API 기반, 79개 .vue 파일 |
| TypeScript + Vite 7 | 타입·빌드 | vue-tsc로 타입체크 후 번들 |
| vue-router / vue-i18n | 라우팅·다국어 | HTML5 history 모드, zh-CN + en-US 로케일 |
| Tailwind CSS 3 + reka-ui | 스타일·컴포넌트 | Radix-Vue 후속인 reka-ui + shadcn-vue풍 컴포넌트 |
| @vueuse/core / lucide | 유틸·아이콘 | WebSocket 실시간 갱신(services/websocket.ts) |
화면은 Dashboard·Tasks·Results·Accounts·Settings·Logs·Login 뷰로 구성된다. Vite 개발 서버는 /api·/auth·/ws를 백엔드(:8000)로 프록시한다.
| 기술 | 역할 | 메모 |
|---|---|---|
| Docker (멀티스테이지) | 배포 | node:22 빌드→python:3.11-slim, Chromium 프리베이크 |
| Chrome Extension (MV3) | 로그인 상태 추출 | chrome.cookies로 HttpOnly 쿠키까지 캡처 |
| ntfy·Bark·Gotify·WeCom·Telegram·Webhook | 알림 팬아웃 | 추천 매물에 대해 asyncio.gather로 동시 발송 |
| desktop_launcher.py | 데스크톱 래퍼 | webview로 감싼 로컬 실행 모드 |
감시 작업 하나가 처음부터 끝까지 어떻게 흐르는가.
이 설계의 첫 번째 핵심은 "웹서버 프로세스"와 "감시 작업 프로세스"가 완전히 분리돼 있다는 것이다. 웹서버(python -m src.app)는 API를 제공하고 예약을 관리할 뿐, 실제 스크래핑은 작업마다 별도 OS 서브프로세스(spider_v2.py --task-name)로 띄운다. 한 작업이 캡차에 막히거나 죽어도 서버·다른 작업은 멀쩡하다.
...idlemtopsearch.pc.search 등)을 page.on("response")로 도청한다. 필터(신규·개인판매·지역·가격)는 실제 UI 버튼을 클릭해 걸고, 갱신된 API 응답을 기다린다. 화면이 아니라 데이터를 잡으므로 훨씬 견고하다.ItemAnalysisDispatcher는 asyncio.Semaphore(기본 2, AI_ANALYSIS_CONCURRENCY)로 동시에 처리할 매물 수를 제한한다. 빠른 "수집 루프"와 느린 "AI 심사·판매자조회·이미지다운로드"를 분리(decouple)해, 수집이 심사를 기다리며 막히지 않게 한다. 전형적인 생산자-소비자 + 백프레셔 패턴.response_format(구조화 출력)·temperature·enable_thinking 같은 미지원 파라미터를 스스로 끄고 재시도한다. 여러 모델 제공사를 한 코드로 감당하는 방어적 설계.AI 심사는 이 프로젝트의 백미다. 프롬프트는 고정된 "출력 형식 계약"과, 작업별 "구매 기준"이 런타임에 합쳐지는 2층 구조다.
# prompts/base_prompt.txt — 모든 작업이 공유하는 출력 형식 계약
너는 EagleEye-V6.4. 반드시 아래 JSON 스키마로만 답하라:
{
"prompt_version": ...,
"is_recommended": true|false, # 이 불리언 하나로 알림 여부가 갈린다
"reason": "...",
"risk_tags": ["...", ...],
"criteria_analysis": { # 기준별 세부 판정
"model_chip": ..., "battery_health": ...,
"condition": ..., "seller_type": { "analysis_details": ... },
"shipping": ..., "seller_credit": ...
}
}
{{CRITERIA_SECTION}} # ← 여기에 작업별 기준이 주입됨
런타임에 spider_v2.py가 base_prompt.replace("{{CRITERIA_SECTION}}", 기준텍스트)로 합친다. 기준 파일(예: prompts/macbook_criteria.txt)에는 "겉보기 개인/실제 업자 구분", "한 방 탈락 규칙", "이미지 우선", 사후 확인이 필요한 항목은 NEEDS_MANUAL_CHECK로 보류 같은 실전 프롬프트 규칙이 들어있다.
요청 조립(ai_message_builder.py)은 매물 JSON(판매자·가격 참조 포함)을 시스템 프롬프트와 합치고, 매물 이미지들을 data:image/jpeg;base64,… image_url 파트로 붙인 멀티모달 메시지를 만든다. temperature=0.1(재시도 시 0.05), 최대 4회 재시도, 필수 필드 누락 시 재요청하는 검증기(validate_ai_response_format)까지 붙는다.
사용자가 "이런 조건의 물건 찾아줘"라고 평범한 문장으로 적으면, prompt_utils.py의 메타-프롬프트("너는 세계적 프롬프트 엔지니어…")가 그 문장 + 참조 기준파일을 LLM에 넘겨 새 카테고리용 기준 파일을 자동 생성한다. 이 작업은 백그라운드 잡(POST /api/tasks/generate → 202 + job id, 프런트가 진행률 폴링)으로 돈다. LLM으로 "LLM 프롬프트를 짓는" 재귀적 자동화.
중국 마켓플레이스의 강한 안티봇과 쿠키 만료에 대응하는 장치가 촘촘하다. RotationPool은 여러 계정/프록시를 TTL 블랙리스트와 함께 돌려쓰고, FailureGuard는 연속 실패(TASK_FAILURE_THRESHOLD 기본 3회)가 나면 작업을 TASK_FAILURE_PAUSE_SECONDS(기본 86400초=24시간) 정지시킨 뒤 쿠키 갱신 시 자동 복구한다. 캡차 오버레이(baxia-dialog)나 FAIL_SYS_USER_VALIDATE 응답을 만나면 RiskControlError를 던지고 무모한 재시도 없이 즉시 중단한다.
어디에 무엇이 있나 — 계층형(DDD풍) 백엔드 + Vue SPA.
진입점을 읽는 순서를 권한다: ① src/app.py(서버가 뜰 때 무슨 일이 일어나나) → ② spider_v2.py(작업 하나가 어떻게 실행되나) → ③ src/scraper.py의 scrape_xianyu()(수집 루프) → ④ item_analysis_dispatcher.py + ai_handler.py(심사) → ⑤ ai_request_compat.py(LLM 호환) 순.
이 코드를 뜯어 무엇을 내 것으로 만들 수 있나.
asyncio.create_subprocess_exec로 작업을 독립 프로세스로 띄우고, os.setsid로 프로세스 그룹을 분리해 SIGTERM으로 통째 정리하는 패턴. 크래시 격리·개별 취소가 필요한 워커 시스템의 정석이다. page.on("response")로 네트워크 응답을 도청하는 스크래핑 기법도 여기서 배운다.
이미지+텍스트를 하나의 메시지로 조립하고, 구조화 JSON 출력을 강제·검증·재시도하는 전 과정. 특히 ai_request_compat.py의 "에러 메시지를 보고 미지원 파라미터를 스스로 끄는" 방식은 여러 LLM 제공사를 지원해야 하는 실무에서 그대로 쓸 수 있다.
api → services → domain → infrastructure 분리, 리포지토리 패턴(SqliteTaskRepository), src/api/dependencies.py의 DI. "크롤러 스크립트"를 "유지보수 가능한 서비스"로 승격시키는 구조를 실물로 관찰할 수 있다.
백엔드가 작업 상태를 /ws로 브로드캐스트하고 Vue 프런트가 실시간 갱신하는 전형. 단일 FastAPI 서버가 API와 SPA 정적파일을 함께 서빙하고, catch-all 라우트로 SPA 히스토리 모드를 받치는 배포 구성도 배울 만하다.
스크래핑 대상을 합법적인 공개 API(예: 공공데이터포털, GitHub API, RSS 피드)로 바꿔, "수집 → LLM 심사 → 알림" 3단 파이프라인만 그대로 이식해보라. 안티봇 부분을 걷어내면 이 프로젝트의 아키텍처만 깔끔히 재사용할 수 있다.
돌리려면 무엇이 필요한가.
| 항목 | 요구사항 |
|---|---|
| 실행 방식 | Docker 권장 (docker compose up -d) — 이미지 ghcr.io/usagi-org/ai-goofish:latest |
| 포트 | 웹 UI :8000 |
| 런타임 | Python 3.10+ / Node 22(프런트 빌드 시). Docker 이미지에 Chromium 프리베이크 |
| 필수 env | OPENAI_API_KEY · OPENAI_BASE_URL · OPENAI_MODEL_NAME (모델은 비전 지원 필수) |
| 기본 모델 | .env.example 기준 ModelScope의 XiaomiMiMo/MiMo-V2-Flash (교체 가능) |
| 로그인 | 크롬 확장으로 闲鱼 로그인 상태를 추출해 state/acc_1.json에 저장 |
| 웹 인증 | WEB_USERNAME/WEB_PASSWORD (기본 admin/admin123 — 반드시 변경) |
| 저장 볼륨 | ./data ./state ./prompts ./logs ./images 등 마운트(compose에 정의) |
웹 UI를 외부에 노출할 거라면 기본 자격증명을 즉시 바꾸고, 가능하면 리버스 프록시·VPN 뒤에 둬라. state/*.json에는 내 로그인 쿠키가 통째로 들어있으니 이 파일 유출은 곧 계정 탈취와 같다.
난이도별로. (합법·안전한 범위 내에서 아키텍처를 익히는 데 초점)
src/infrastructure/external/notification_clients/의 기존 클라이언트(예: ntfy)를 본떠, Discord Webhook 클라이언트를 추가하고 factory에 등록하라. "추천 매물이 뜨면 내 디스코드로 온다"까지 완성하면 알림 팬아웃 구조가 손에 잡힌다.
validate_ai_response_format에 가격 범위·필드 타입 검증을 추가하고, 실패 시 재시도 프롬프트에 "무엇이 틀렸는지"를 되먹여(feedback) 정확도를 높여보라. LLM 구조화 출력의 실전 견고화 기법을 익힌다.
스크래핑 계층(scraper.py)을 공공데이터/RSS/GitHub API 수집기로 갈아끼우고, 하위의 Dispatcher·AI 심사·알림·SQLite 저장은 그대로 재사용하라. "수집원만 바꿔도 나머지 파이프라인이 재사용되는" 계층 분리의 위력을 체감한다.
price_snapshots 테이블을 활용해 키워드별 시세 추이 차트를 Vue 프런트에 추가하고, WebSocket으로 실시간 갱신되게 하라. 백엔드 집계 API + 프런트 차트 + 실시간 스트림을 한 번에 다루는 풀스택 과제.
ai_request_compat.py를 독립 라이브러리로 떼어내, 임의의 OpenAI 호환 게이트웨이(vLLM, Ollama, LiteLLM 등)에 대해 파라미터 자동 협상이 되는지 테스트 매트릭스를 짜라. 여러 추론 백엔드를 감당하는 방어적 클라이언트 설계를 깊이 파고든다.
이 레포를 축으로 4주 커리큘럼.
| 주차 | 주제 | 배울 것 · 실습 |
|---|---|---|
| 1주차 | 비동기 파이썬 & Playwright | asyncio 이벤트 루프·세마포어, Playwright async로 페이지 몰기·응답 가로채기. scrape_xianyu() 정독 |
| 2주차 | 멀티모달 LLM 통합 | 이미지 base64 메시지 조립, 구조화 JSON 강제·검증·재시도, 호환성 레이어(Chat vs Responses). ai_handler.py+ai_request_compat.py |
| 3주차 | FastAPI 계층형 백엔드 | DDD풍 레이어링·DI·리포지토리, APScheduler 예약, WebSocket 실시간, SQLite 스키마·마이그레이션 |
| 4주차 | Vue 3 관제 UI & 배포 | Vue 3 + Vite + Tailwind SPA, WebSocket 클라이언트, Docker 멀티스테이지로 프런트+백 한 이미지에 담기 |
문서에 나온 용어를 한 번에.
| 용어 | 뜻 |
|---|---|
| 闲鱼 / Goofish | 알리바바가 운영하는 중국 최대 개인 간 중고거래 플랫폼. 이 도구의 감시 대상 |
| API 인터셉션 | DOM을 긁는 대신, 페이지가 호출하는 내부 JSON API 응답을 page.on("response")로 가로채 데이터를 얻는 견고한 스크래핑 기법 |
| mtop | 타오바오/闲鱼가 쓰는 모바일 게이트웨이 API 프로토콜. 검색·상세·판매자 정보가 여기로 흐른다 |
| 멀티모달 LLM | 텍스트뿐 아니라 이미지도 입력으로 받는 대형언어모델. 매물 사진을 "보고" 판단 |
| 구조화 출력 | LLM이 자유 문장 대신 정해진 JSON 스키마로만 답하게 강제하는 것. is_recommended 같은 필드로 후속 로직이 분기 |
| 경계 동시성 | Semaphore로 동시 처리량을 제한해, 느린 단계가 시스템을 폭주시키지 않게 하는 백프레셔 패턴 |
| 프로세스 격리 | 작업마다 별도 OS 프로세스로 실행해, 하나가 죽어도 전체가 안 죽게 하는 설계 |
| 안티봇 / 리스크컨트롤 | 자동화 요청을 탐지·차단하는 서버 방어(캡차·서명·지문). 이 도구는 실제 브라우저 지문 복제로 우회 |
| RotationPool / FailureGuard | 계정·프록시를 TTL 블랙리스트와 함께 돌려쓰고, 연속 실패 시 작업을 일시정지해 밴을 피하는 운영 장치 |
| APScheduler | 파이썬 크론 스케줄러. "매 N분마다 감시" 같은 주기 실행을 담당 |
| MV3 (Manifest V3) | 크롬 확장 최신 규격. 여기선 로그인 쿠키·브라우저 환경을 추출하는 데 사용 |
더 파고들 곳.