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