Files
blog-writer/dashboard/backend/api_connections.py
sinmb79 9b44a07a44 feat: v3.2 — YouTube Shorts 봇 + 수동 어시스트 + 보안 개선
주요 추가 기능:
- bots/shorts/ 서브모듈 7개: tts_engine, script_extractor, asset_resolver,
  stock_fetcher, caption_renderer, video_assembler, youtube_uploader
- bots/shorts_bot.py: 6단계 Shorts 파이프라인 오케스트레이터
  (auto/semi_auto 두 가지 생산 모드, CLI 지원)
- bots/writer_bot.py: 독립 실행형 AI 글쓰기 봇 (대시보드 연동)
- bots/assist_bot.py: URL 기반 수동 어시스트 파이프라인
- config/shorts_config.json: Shorts 전체 설정
- templates/shorts/extract_prompt.txt: LLM 스크립트 추출 프롬프트
- scheduler.py에 shorts 잡(10:35/16:00) + /shorts Telegram 명령 추가

보안 개선:
- .env 파일 외부 경로 참조로 변경 (load_dotenv dotenv_path, 24개 파일)
- .gitignore에 민감 파일/내부 문서/런타임 데이터 항목 추가

문서:
- README.md 전면 재작성 (상세 한글 설명, 설치/설정/사용법 포함)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 17:51:02 +09:00

208 lines
6.6 KiB
Python

"""
dashboard/backend/api_connections.py
Settings > Connections 탭 API — AI 서비스 연결 상태 확인/테스트
"""
import json
import os
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from dotenv import load_dotenv
load_dotenv(dotenv_path='D:/key/blog-writer.env.env')
BASE_DIR = Path(__file__).parent.parent.parent
CONFIG_PATH = BASE_DIR / "config" / "engine.json"
ENV_PATH = BASE_DIR / ".env"
router = APIRouter()
AI_SERVICES = [
{
"id": "claude",
"name": "Claude (Anthropic)",
"env_key": "ANTHROPIC_API_KEY",
"category": "writing",
"description": "글쓰기 엔진 — claude-opus-4-5",
},
{
"id": "gemini",
"name": "Google Gemini",
"env_key": "GEMINI_API_KEY",
"category": "writing",
"description": "글쓰기 엔진 — gemini-2.0-flash",
},
{
"id": "openai",
"name": "OpenAI (GPT + DALL-E + TTS)",
"env_key": "OPENAI_API_KEY",
"category": "multi",
"description": "이미지(DALL-E 3) + TTS(tts-1-hd)",
},
{
"id": "elevenlabs",
"name": "ElevenLabs TTS",
"env_key": "ELEVENLABS_API_KEY",
"category": "tts",
"description": "고품질 한국어 TTS",
},
{
"id": "google_tts",
"name": "Google Cloud TTS",
"env_key": "GOOGLE_TTS_API_KEY",
"category": "tts",
"description": "Google Wavenet TTS",
},
{
"id": "seedance",
"name": "Seedance AI Video",
"env_key": "SEEDANCE_API_KEY",
"category": "video",
"description": "AI 영상 생성 — Seedance 2.0",
},
{
"id": "runway",
"name": "Runway Gen-3",
"env_key": "RUNWAY_API_KEY",
"category": "video",
"description": "AI 영상 생성 — Gen-3 Turbo",
},
]
class ApiKeyUpdate(BaseModel):
api_key: str
def _mask_key(key: str) -> str:
if not key:
return ""
if len(key) <= 8:
return "****"
return key[:4] + "****" + key[-4:]
def _get_connections():
connections = []
for svc in AI_SERVICES:
key = os.getenv(svc["env_key"], "")
connections.append({
**svc,
"connected": bool(key),
"key_masked": _mask_key(key),
})
return connections
@router.get("/connections")
async def get_connections():
return {"connections": _get_connections()}
@router.post("/connections/{service_id}/test")
async def test_connection(service_id: str):
"""서비스 연결 테스트"""
svc = next((s for s in AI_SERVICES if s["id"] == service_id), None)
if not svc:
raise HTTPException(status_code=404, detail="서비스를 찾을 수 없습니다.")
api_key = os.getenv(svc["env_key"], "")
if not api_key:
return {"success": False, "message": "API 키가 설정되지 않았습니다."}
# 간단한 연결 테스트
try:
if service_id == "claude":
import anthropic
client = anthropic.Anthropic(api_key=api_key)
# 모델 목록으로 연결 테스트
client.messages.create(
model="claude-haiku-4-5",
max_tokens=10,
messages=[{"role": "user", "content": "ping"}],
)
return {"success": True, "message": "Claude 연결 성공"}
elif service_id == "openai":
from openai import OpenAI
client = OpenAI(api_key=api_key)
client.models.list()
return {"success": True, "message": "OpenAI 연결 성공"}
elif service_id == "gemini":
import google.generativeai as genai
genai.configure(api_key=api_key)
model = genai.GenerativeModel("gemini-2.0-flash")
model.generate_content("ping", generation_config={"max_output_tokens": 5})
return {"success": True, "message": "Gemini 연결 성공"}
elif service_id in ("elevenlabs", "seedance", "runway", "google_tts"):
import requests
test_urls = {
"elevenlabs": "https://api.elevenlabs.io/v1/models",
"google_tts": f"https://texttospeech.googleapis.com/v1/voices?key={api_key}",
"seedance": "https://api.seedance2.ai/v1/models",
"runway": "https://api.runwayml.com/v1/organization",
}
headers_map = {
"elevenlabs": {"xi-api-key": api_key},
"runway": {"Authorization": f"Bearer {api_key}"},
}
url = test_urls.get(service_id, "")
headers = headers_map.get(service_id, {})
if url:
resp = requests.get(url, headers=headers, timeout=10)
if resp.status_code < 400:
return {"success": True, "message": f"{svc['name']} 연결 성공"}
else:
return {"success": False, "message": f"HTTP {resp.status_code}"}
return {"success": True, "message": "키 존재 확인됨 (심층 테스트 미지원)"}
except ImportError as e:
return {"success": False, "message": f"라이브러리 미설치: {e}"}
except Exception as e:
return {"success": False, "message": str(e)[:200]}
@router.put("/connections/{service_id}")
async def update_api_key(service_id: str, req: ApiKeyUpdate):
"""API 키를 .env 파일에 저장"""
svc = next((s for s in AI_SERVICES if s["id"] == service_id), None)
if not svc:
raise HTTPException(status_code=404, detail="서비스를 찾을 수 없습니다.")
env_key = svc["env_key"]
api_key = req.api_key.strip()
try:
# .env 파일 읽기
if ENV_PATH.exists():
lines = ENV_PATH.read_text(encoding="utf-8").splitlines()
else:
lines = []
# 기존 키 교체 또는 추가
updated = False
new_lines = []
for line in lines:
if line.startswith(f"{env_key}=") or line.startswith(f"{env_key} ="):
new_lines.append(f"{env_key}={api_key}")
updated = True
else:
new_lines.append(line)
if not updated:
new_lines.append(f"{env_key}={api_key}")
ENV_PATH.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
# 현재 프로세스 환경 변수도 업데이트
os.environ[env_key] = api_key
return {"success": True, "message": f"{env_key} 키 저장 완료"}
except Exception as e:
raise HTTPException(status_code=500, detail=f"키 저장 실패: {e}")