앱
이 저장소가 대체 무엇인가.
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를 던지고 무모한 재시도 없이 즉시 중단한다.
2026-10-29 공식 관측의 archived 값은 false이며 해당 소스는 공개돼 있다. 이전의 아카이브·개발 중단 안내를 제거했다. 문서의 Docker 예시는 Node 22 계열로 프런트를 빌드하고 Python 3.11 환경에 Chromium을 포함한다. 로컬 프런트의 잠금 파일은 Vite에 Node ^20.19.0 || >=22.12.0를 요구한다. README에 적힌 과거 Node 20.18.3 빌드 관측을 현재 최소 조건으로 사용하면 안 된다.
sqlite_bootstrap.py는 스키마를 초기화하고 작업·결과·가격 스냅샷의 과거 파일을 각각 최초 가져오기한다. 마이그레이션 완료 마커가 있거나 작업 테이블이 이미 차 있으면 기존 데이터를 덮어 가져오지 않는다. 현재 조회의 정본은 data/app.sqlite3이며 state/ 로그인 상태·프롬프트·로그·임시 이미지는 파일로 남는다.
AI 작업 생성의 진행 상태는 TaskGenerationService 안의 메모리 딕셔너리에 있고 별도 스레드의 이벤트 루프가 작업을 수행한다. SQLite에 저장된 감시 작업과 진행 중인 생성 job은 수명이 다르다. API의 202 응답을 작업 저장 완료로 읽지 말고 job 상태를 계속 확인해야 한다.
어디에 무엇이 있나 — 계층형(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+(README), 프런트는 Vite 잠금 기준 Node ^20.19.0 또는 ≥22.12.0. 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) | 크롬 확장 최신 규격. 여기선 로그인 쿠키·브라우저 환경을 추출하는 데 사용 |
2026-10-29 재검토: 공식 소스 커밋 77f8d75869acd65f9dba72731b95c7c49ea67bdf. 아래 초판 기록과 현재 소스 개정 내용을 구분한다.
더 파고들 곳.