Files
aihot/backend/app/api/admin.py
2026-05-24 01:16:07 +08:00

183 lines
6.3 KiB
Python

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
]