153 lines
5.5 KiB
Python
153 lines
5.5 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta
|
|
from typing import Literal, Optional
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
from fastapi.responses import HTMLResponse
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.agent.scheduler import run_scheduled_job
|
|
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,
|
|
RunTriggerRead,
|
|
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.post("/agent/runs/daily", response_model=RunTriggerRead)
|
|
def trigger_daily_run(
|
|
request: Request,
|
|
settings: Settings = Depends(get_settings),
|
|
db: Session = Depends(get_db),
|
|
) -> RunTriggerRead:
|
|
# 手动搜索投递到调度器线程,避免HTTP请求承载长时间Grok搜索任务。
|
|
running = db.scalar(
|
|
select(AgentRun).where(AgentRun.status == "running").order_by(AgentRun.id.desc()).limit(1)
|
|
)
|
|
if running is not None:
|
|
return RunTriggerRead(status="running", run_id=running.id)
|
|
scheduler = getattr(request.app.state, "scheduler", None)
|
|
if scheduler is not None and scheduler.running:
|
|
scheduler.add_job(
|
|
run_scheduled_job,
|
|
trigger="date",
|
|
run_date=datetime.now(scheduler.timezone) + timedelta(seconds=1),
|
|
args=[settings],
|
|
id=f"manual-intelligence-{datetime.now().timestamp()}",
|
|
max_instances=1,
|
|
)
|
|
return RunTriggerRead(status="started")
|
|
run_scheduled_job(settings)
|
|
return RunTriggerRead(status="started")
|
|
|
|
|
|
@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
|