from fastapi import APIRouter, Depends, HTTPException, Header from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from pydantic import BaseModel from typing import Optional import asyncio from ..database import get_db from ..models.news import LLMConfig, NewsSource, SystemLog, RawNews, ProcessedNews from ..config import settings router = APIRouter() def verify_admin(authorization: str = Header(...)): token = authorization.removeprefix("Bearer ").strip() if token != settings.admin_token: raise HTTPException(status_code=401, detail="Invalid admin token") # ── LLM Config ──────────────────────────────────────────────────────────────── class LLMConfigIn(BaseModel): name: str provider: str api_key: str base_url: str model_name: str @router.get("/llm-config", dependencies=[Depends(verify_admin)]) async def get_llm_config(db: AsyncSession = Depends(get_db)): result = await db.execute(select(LLMConfig).where(LLMConfig.is_active == True).limit(1)) cfg = result.scalar_one_or_none() if not cfg: return None return { "id": cfg.id, "name": cfg.name, "provider": cfg.provider, "api_key": "***" + cfg.api_key[-4:] if len(cfg.api_key) > 4 else "****", "base_url": cfg.base_url, "model_name": cfg.model_name, } @router.post("/llm-config", dependencies=[Depends(verify_admin)]) async def save_llm_config(body: LLMConfigIn, db: AsyncSession = Depends(get_db)): await db.execute( LLMConfig.__table__.update().values(is_active=False) ) cfg = LLMConfig(**body.model_dump(), is_active=True) db.add(cfg) await db.commit() return {"ok": True, "id": cfg.id} @router.post("/llm-config/test", dependencies=[Depends(verify_admin)]) async def test_llm_config(body: LLMConfigIn): from ..ai.llm_client import LLMClient client = LLMClient( provider=body.provider, api_key=body.api_key, base_url=body.base_url, model=body.model_name, ) try: reply = await client.complete( system_prompt="你是一个助手。", user_prompt="请回复'连接正常',不要说其他内容。", ) return {"ok": True, "reply": reply} except Exception as e: return {"ok": False, "error": str(e)} # ── News Sources ────────────────────────────────────────────────────────────── class SourceIn(BaseModel): name: str url: str source_type: str = "rss" language: str = "zh" category: Optional[str] = None @router.get("/sources", dependencies=[Depends(verify_admin)]) async def get_sources(db: AsyncSession = Depends(get_db)): result = await db.execute(select(NewsSource).order_by(NewsSource.id)) sources = result.scalars().all() return [ {"id": s.id, "name": s.name, "url": s.url, "source_type": s.source_type, "language": s.language, "category": s.category, "is_active": s.is_active} for s in sources ] @router.post("/sources", dependencies=[Depends(verify_admin)]) async def add_source(body: SourceIn, db: AsyncSession = Depends(get_db)): src = NewsSource(**body.model_dump()) db.add(src) await db.commit() return {"ok": True, "id": src.id} @router.put("/sources/{source_id}", dependencies=[Depends(verify_admin)]) async def toggle_source(source_id: int, body: dict, db: AsyncSession = Depends(get_db)): result = await db.execute(select(NewsSource).where(NewsSource.id == source_id)) src = result.scalar_one_or_none() if not src: raise HTTPException(status_code=404) if "is_active" in body: src.is_active = body["is_active"] await db.commit() return {"ok": True} @router.delete("/sources/{source_id}", dependencies=[Depends(verify_admin)]) async def delete_source(source_id: int, db: AsyncSession = Depends(get_db)): result = await db.execute(select(NewsSource).where(NewsSource.id == source_id)) src = result.scalar_one_or_none() if src: await db.delete(src) await db.commit() return {"ok": True} # ── Trigger & Stats ─────────────────────────────────────────────────────────── _pipeline_running = False @router.post("/crawl/trigger", dependencies=[Depends(verify_admin)]) async def trigger_crawl(): global _pipeline_running if _pipeline_running: return {"ok": False, "message": "Pipeline already running"} _pipeline_running = True asyncio.create_task(_run_pipeline()) return {"ok": True, "message": "Pipeline started"} async def _run_pipeline(): global _pipeline_running from ..scheduler import trigger_now try: await trigger_now() finally: _pipeline_running = False @router.get("/stats", dependencies=[Depends(verify_admin)]) async def get_stats(db: AsyncSession = Depends(get_db)): from datetime import date today = date.today() raw_today = (await db.execute( select(func.count(RawNews.id)).where(func.date(RawNews.crawled_at) == today) )).scalar_one() processed_today = (await db.execute( select(func.count(ProcessedNews.id)).where(func.date(ProcessedNews.processed_at) == today) )).scalar_one() featured_today = (await db.execute( select(func.count(ProcessedNews.id)) .where(func.date(ProcessedNews.processed_at) == today) .where(ProcessedNews.is_featured == True) )).scalar_one() return { "raw_today": raw_today, "processed_today": processed_today, "featured_today": featured_today, "pipeline_running": _pipeline_running, } @router.get("/logs", dependencies=[Depends(verify_admin)]) async def get_logs(limit: int = 100, db: AsyncSession = Depends(get_db)): result = await db.execute( select(SystemLog).order_by(SystemLog.created_at.desc()).limit(limit) ) logs = result.scalars().all() return [ {"id": l.id, "level": l.level, "event_type": l.event_type, "message": l.message, "created_at": l.created_at.isoformat()} for l in logs ]