Files
signalscout-backend/app/api/routes.py
T
2026-07-08 23:39:41 +08:00

123 lines
4.4 KiB
Python

from __future__ import annotations
from typing import Literal, Optional
from fastapi import APIRouter, Depends
from fastapi.responses import HTMLResponse
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.core.config import Settings, get_settings
from app.db.models import AgentRun, Report, Signal, Topic
from app.db.session import get_db
from app.schemas.agent import (
DashboardRead,
ReportRead,
RunRead,
SignalRead,
TopicCreate,
TopicRead,
)
from app.ui.home import HOME_PAGE_HTML
router = APIRouter()
@router.get("/", response_class=HTMLResponse)
def home() -> HTMLResponse:
# 根路径返回业务工作台,避免用户直接打开域名时看到接口404。
return HTMLResponse(HOME_PAGE_HTML)
@router.get("/health")
def health(settings: Settings = Depends(get_settings)) -> dict:
# 健康检查只返回服务身份,避免暴露密钥和内部地址。
return {"status": "ok", "app": settings.app_name, "env": settings.env}
@router.get("/agent/runs", response_model=list[RunRead])
def list_runs(db: Session = Depends(get_db)) -> list[AgentRun]:
# 运行列表保持紧凑,方便仪表盘和命令行查看最近状态。
return list(db.scalars(select(AgentRun).order_by(AgentRun.id.desc()).limit(20)).all())
@router.get("/signals", response_model=list[SignalRead])
def list_signals(
limit: int = 100,
source_type: Optional[Literal["news", "github"]] = None,
db: Session = Depends(get_db),
) -> list[Signal]:
# 情报列表按采集时间倒序返回,是仪表盘的主要阅读入口。
safe_limit = max(1, min(limit, 200))
return list(
db.scalars(
_visible_signals_query(source_type=source_type)
.order_by(Signal.id.desc())
.limit(safe_limit)
).all()
)
@router.get("/reports/latest", response_model=Optional[ReportRead])
def latest_report(db: Session = Depends(get_db)) -> Optional[Report]:
# 最新日报提供一份可直接阅读的每日简报。
return db.scalar(select(Report).order_by(Report.id.desc()).limit(1))
@router.get("/dashboard", response_model=DashboardRead)
def dashboard(db: Session = Depends(get_db)) -> DashboardRead:
# 单个仪表盘接口返回运行、信号、日报和主题,前端不需要理解内部调度细节。
runs = list(db.scalars(select(AgentRun).order_by(AgentRun.id.desc()).limit(10)).all())
news_signals = list(
db.scalars(
_visible_signals_query(source_type="news").order_by(Signal.id.desc()).limit(80)
).all()
)
github_signals = list(
db.scalars(
_visible_signals_query(source_type="github").order_by(Signal.id.desc()).limit(80)
).all()
)
report = db.scalar(select(Report).order_by(Report.id.desc()).limit(1))
topics = list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
return DashboardRead(
runs=runs,
news_signals=news_signals,
github_signals=github_signals,
latest_report=report,
topics=topics,
)
@router.get("/logs/recent", response_model=list[str])
def recent_logs(settings: Settings = Depends(get_settings)) -> list[str]:
# 最近日志用于定位Agent和模型供应商调用问题。
if not settings.log_file.exists():
return []
lines = settings.log_file.read_text(encoding="utf-8", errors="replace").splitlines()
return lines[-settings.log_tail_lines :]
@router.post("/topics", response_model=TopicRead)
def create_topic(payload: TopicCreate, db: Session = Depends(get_db)) -> Topic:
# 主题让Agent可以在不改代码的情况下扩展关注范围。
topic = Topic(name=payload.name, description=payload.description, enabled=payload.enabled)
db.add(topic)
db.commit()
db.refresh(topic)
return topic
@router.get("/topics", response_model=list[TopicRead])
def list_topics(db: Session = Depends(get_db)) -> list[Topic]:
# 主题列表让操作者确认当前情报目标。
return list(db.scalars(select(Topic).order_by(Topic.id.asc())).all())
def _visible_signals_query(source_type: Optional[Literal["news", "github"]] = None):
# 运行中的信号和完成后的信号走同一张表,查询函数保留统一排序入口。
query = select(Signal).join(AgentRun, Signal.run_id == AgentRun.id)
if source_type is not None:
query = query.where(Signal.source_type == source_type)
return query