feat: upstream v3.2.1 기반으로 업그레이드 + eli 블로그 커스터마이징

- upstream sinmb79/blog-writer v3.2.1 코드 베이스 적용
- config_resolver, CLI, writer_bot, shorts pipeline 등 신규 기능 포함
- load_dotenv Windows 경로 → Docker 호환 load_dotenv() 변경 (25개 파일)
- runtime_guard.py Docker 환경 bypass 추가
- config/blogs.json: eli-ai 블로그 정체성 (8개 카테고리)
- config/sources.json: 38개 RSS 소스 유지
- config/engine.json: writing provider → gemini (2.5-flash)
- config/safety_keywords.json: 모든 글 수동 승인 (score 101)
- bots/scheduler.py: 시스템 프롬프트 eli 블로그 기준으로 업데이트
- bots/publisher_bot.py: .env refresh token OAuth 폴백 로직 추가
- requirements.txt: google-generativeai, groq 활성화
- Dockerfile + docker-compose.yml: NAS Docker 배포 설정
- CLAUDE.md: 프로젝트 메타데이터

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
JOUNGWOOK KWON
2026-03-30 09:21:14 +09:00
parent 66be55ba8a
commit 3e2405dff9
36 changed files with 3380 additions and 84 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
DATA_DIR = BASE_DIR / 'data'
+1 -1
View File
@@ -23,7 +23,7 @@ from typing import Optional
import requests
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
ASSIST_DIR = BASE_DIR / 'data' / 'assist'
+1 -1
View File
@@ -17,7 +17,7 @@ import requests
from bs4 import BeautifulSoup
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
CONFIG_DIR = BASE_DIR / 'config'
+1 -1
View File
@@ -32,7 +32,7 @@ from typing import Optional
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
LOG_DIR = BASE_DIR / 'logs'
+1 -1
View File
@@ -22,7 +22,7 @@ from typing import Optional
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
LOG_DIR = BASE_DIR / 'logs'
+1 -1
View File
@@ -19,7 +19,7 @@ from pathlib import Path
import requests
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
LOG_DIR = BASE_DIR / 'logs'
+1 -1
View File
@@ -19,7 +19,7 @@ from pathlib import Path
import requests
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
LOG_DIR = BASE_DIR / 'logs'
+1 -1
View File
@@ -16,7 +16,7 @@ from pathlib import Path
import requests
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
LOG_DIR = BASE_DIR / 'logs'
+1 -1
View File
@@ -16,7 +16,7 @@ import requests
from dotenv import load_dotenv
from requests_oauthlib import OAuth1
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
LOG_DIR = BASE_DIR / 'logs'
+1 -1
View File
@@ -14,7 +14,7 @@ from pathlib import Path
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
LOG_DIR = BASE_DIR / 'logs'
+1 -1
View File
@@ -20,7 +20,7 @@ from typing import Any, Optional
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
CONFIG_PATH = BASE_DIR / 'config' / 'engine.json'
+1 -1
View File
@@ -25,7 +25,7 @@ from pathlib import Path
import requests
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
DATA_DIR = BASE_DIR / 'data'
+1 -1
View File
@@ -16,7 +16,7 @@ import requests
from bs4 import BeautifulSoup
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
CONFIG_DIR = BASE_DIR / 'config'
+1 -1
View File
@@ -12,7 +12,7 @@ from pathlib import Path
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
sys.path.insert(0, str(BASE_DIR / 'bots'))
+1 -1
View File
@@ -11,7 +11,7 @@ from pathlib import Path
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
sys.path.insert(0, str(BASE_DIR / 'bots'))
+1 -1
View File
@@ -18,7 +18,7 @@ from pathlib import Path
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent.parent
sys.path.insert(0, str(BASE_DIR / 'bots'))
+1 -1
View File
@@ -13,7 +13,7 @@ from pathlib import Path
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
# novel/ 폴더 기준으로 BASE_DIR 설정
BASE_DIR = Path(__file__).parent.parent.parent
+20 -1
View File
@@ -25,7 +25,7 @@ from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
CONFIG_DIR = BASE_DIR / 'config'
@@ -63,6 +63,7 @@ def load_config(filename: str) -> dict:
def get_google_credentials() -> Credentials:
creds = None
# 1) token.json 파일 우선
if TOKEN_PATH.exists():
creds = Credentials.from_authorized_user_file(str(TOKEN_PATH), SCOPES)
if not creds or not creds.valid:
@@ -70,6 +71,24 @@ def get_google_credentials() -> Credentials:
creds.refresh(Request())
with open(TOKEN_PATH, 'w') as f:
f.write(creds.to_json())
# 2) .env의 GOOGLE_REFRESH_TOKEN으로 직접 생성 (Docker 환경 대응)
if not creds or not creds.valid:
refresh_token = os.getenv('GOOGLE_REFRESH_TOKEN', '')
client_id = os.getenv('GOOGLE_CLIENT_ID', '')
client_secret = os.getenv('GOOGLE_CLIENT_SECRET', '')
if refresh_token and client_id and client_secret:
creds = Credentials(
token=None,
refresh_token=refresh_token,
token_uri='https://oauth2.googleapis.com/token',
client_id=client_id,
client_secret=client_secret,
scopes=SCOPES,
)
creds.refresh(Request())
with open(TOKEN_PATH, 'w') as f:
f.write(creds.to_json())
logger.info("Google 인증 성공 (.env refresh token)")
if not creds or not creds.valid:
raise RuntimeError("Google 인증 실패. scripts/get_token.py 를 먼저 실행하세요.")
return creds
+1 -1
View File
@@ -13,7 +13,7 @@ from telegram import Update
from telegram.ext import Application, MessageHandler, CommandHandler, filters, ContextTypes
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', '')
+11 -13
View File
@@ -27,7 +27,7 @@ from telegram.ext import Application, CommandHandler, MessageHandler, filters, C
import anthropic
import re
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
CONFIG_DIR = BASE_DIR / 'config'
@@ -55,28 +55,25 @@ ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY', '')
_claude_client: anthropic.Anthropic | None = None
_conversation_history: dict[int, list] = {}
CLAUDE_SYSTEM_PROMPT = """당신은 The 4th Path 블로그 자동 수익 엔진의 AI 어시스턴트입니다.
CLAUDE_SYSTEM_PROMPT = """당신은 "AI? 그게 뭔데?" 블로그의 운영 어시스턴트입니다.
블로그 운영자 eli가 Telegram으로 명령하면 도와주는 역할입니다.
슬로건: "어렵지 않아요, 그냥 읽어봐요"
블로그 주소: eli-ai.blogspot.com
시스템(v3) 4계층 구조로 운영됩니다:
[LAYER 1] AI 콘텐츠 생성: OpenClaw(GPT-5.4) 원본 마크다운 1 생성
[LAYER 1] AI 콘텐츠 생성: Gemini 2.5-flash 원본 마크다운 1 생성
[LAYER 2] 변환 엔진: 원본 블로그HTML / 인스타카드 / X스레드 / 뉴스레터 자동 변환
[LAYER 3] 배포 엔진: Blogger / Instagram / X / TikTok / YouTube 순차 발행
[LAYER 4] 분석봇: 성과 수집 + 주간 리포트 + 피드백 루프
구성:
- collector_bot: 트렌드/RSS 수집 (07:00)
- ai_writer: OpenClaw 작성 트리거 (08:00)
- blog_converter: 마크다운HTML (08:30)
- card_converter: 인스타 카드 1080×1080 (08:30)
- thread_converter: X 스레드 변환 (08:30)
- publisher_bot: Blogger 발행 (09:00)
- instagram_bot: 인스타 발행 (10:00)
- x_bot: X 스레드 게시 (11:00)
- analytics_bot: 분석/리포트 (22:00)
8 카테고리: AI인사이트, 여행맛집, 스타트업, 제품리뷰, 생활꿀팁, 앱추천, 재테크절약, 팩트체크
사용 가능한 텔레그램 명령:
/status 상태
/topics 오늘 수집된 글감
/collect 글감 즉시 수집
/write [번호] [방향] 특정 글감으로 작성
/pending 검토 대기 목록
/approve [번호] 승인 발행
/reject [번호] 거부
@@ -87,6 +84,7 @@ CLAUDE_SYSTEM_PROMPT = """당신은 The 4th Path 블로그 자동 수익 엔진
/novel_gen [novel_id] 에피소드 즉시 생성
/novel_status 소설 파이프라인 진행 현황
모든 글은 발행 운영자 승인이 필요합니다.
사용자의 자연어 요청을 이해하고 적절히 안내하거나 답변해주세요.
한국어로 간결하게 답변하세요."""
IMAGE_MODE = os.getenv('IMAGE_MODE', 'manual').lower()
+1 -1
View File
@@ -28,7 +28,7 @@ from typing import Optional
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
sys.path.insert(0, str(BASE_DIR))
+1 -1
View File
@@ -20,7 +20,7 @@ from pathlib import Path
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
load_dotenv()
BASE_DIR = Path(__file__).parent.parent
sys.path.insert(0, str(BASE_DIR))