"""知识摘要 API — 管理会计OS持久记忆 提供: - 查询最近摘要列表 - 查询单个摘要详情 - 手动触发各层级摘要生成 - 查询未摘要的事件 """ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from sqlalchemy import func, desc from datetime import datetime, timedelta from typing import Optional from app.database import get_db from app.auth_middleware import require_role from app.models import KnowledgeEvent, KnowledgeSummary from app.services.knowledge_service import ( generate_summary_sync, generate_daily_sync, generate_weekly_sync, generate_monthly_sync, get_last_summary, extract_events, ) import logging logger = logging.getLogger("cma.knowledge_api") router = APIRouter(prefix="/api/cma/knowledge", tags=["知识摘要"], dependencies=[Depends(require_role("ceo", "finance", "business", "it"))], ) def summary_to_dict(s: KnowledgeSummary) -> dict: return { "id": s.id, "level": s.level, "period_key": s.period_key, "title": s.title, "content": s.content, "kpi_changes": s.kpi_changes, "decision_points": s.decision_points, "key_metrics": s.key_metrics, "prev_summary_id": s.prev_summary_id, "model": s.model, "is_stale": s.is_stale, "created_at": s.created_at.isoformat() if s.created_at else None, } # ── 查询 ── @router.get("/summaries") def list_summaries( level: Optional[str] = None, limit: int = 20, offset: int = 0, db: Session = Depends(get_db), ): """获取摘要列表,按层级筛选,按时间倒序""" query = db.query(KnowledgeSummary) if level: query = query.filter(KnowledgeSummary.level == level) query = query.order_by(desc(KnowledgeSummary.id)).offset(offset).limit(limit) total = db.query(func.count(KnowledgeSummary.id)).select_from(KnowledgeSummary) if level: total = total.filter(KnowledgeSummary.level == level) total = total.scalar() return { "total": total, "items": [summary_to_dict(s) for s in query.all()], } @router.get("/summaries/latest") def latest_summary( level: str = Query("daily", description="层级: daily/weekly/monthly/cumulative"), db: Session = Depends(get_db), ): """获取指定层级的最新摘要""" s = get_last_summary(db, level) if not s: return {"detail": f"没有{level}层级的摘要"}, 404 return summary_to_dict(s) @router.get("/summaries/{summary_id}") def get_summary(summary_id: int, db: Session = Depends(get_db)): """获取单条摘要详情""" s = db.query(KnowledgeSummary).filter(KnowledgeSummary.id == summary_id).first() if not s: raise HTTPException(status_code=404, detail="摘要不存在") return summary_to_dict(s) # ── 事件查询 ── @router.get("/events") def list_events( since: Optional[str] = None, until: Optional[str] = None, limit: int = 50, db: Session = Depends(get_db), ): """查询未摘要的原始事件 如果不传时间,默认返回最近7天的操作记录和预警。 """ try: dt_since = datetime.fromisoformat(since) if since else datetime.utcnow() - timedelta(days=7) dt_until = datetime.fromisoformat(until) if until else datetime.utcnow() except ValueError: raise HTTPException(status_code=400, detail="时间格式错误,请使用 ISO 格式如 2026-06-01T00:00:00") events = extract_events(db, dt_since, dt_until) return {"since": dt_since.isoformat(), "until": dt_until.isoformat(), "total": len(events), "events": events[:limit]} # ── 手动触发 ── @router.post("/generate/daily") def trigger_daily_summary(db: Session = Depends(get_db)): """手动触发每日摘要生成""" try: result = generate_daily_sync(db) return {"message": "每日摘要已生成", "summary": result} except Exception as e: logger.exception("每日摘要生成失败") raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}") @router.post("/generate/weekly") def trigger_weekly_summary(db: Session = Depends(get_db)): """手动触发周度摘要生成""" try: result = generate_weekly_sync(db) return {"message": "周度摘要已生成", "summary": result} except Exception as e: logger.exception("周度摘要生成失败") raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}") @router.post("/generate/monthly") def trigger_monthly_summary(db: Session = Depends(get_db)): """手动触发月度摘要生成""" try: result = generate_monthly_sync(db) return {"message": "月度摘要已生成", "summary": result} except Exception as e: logger.exception("月度摘要生成失败") raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")