This commit is contained in:
2026-05-24 01:16:07 +08:00
commit 2ece5174a7
35 changed files with 2583 additions and 0 deletions

182
backend/app/api/admin.py Normal file
View File

@@ -0,0 +1,182 @@
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
]