PMFlow/backend/app/api/scheduler.py

174 lines
6.1 KiB
Python

"""Scheduler API 路由。"""
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends
from sqlalchemy import and_, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from ..core.database import get_db
from ..dependencies import require_manager_user
from ..models import KnowledgeBaseDoc, Project, User
from ..services.adaptive_rag_service import AdaptiveRAGService
from ..services.ai_pmo.ai_pmo_cadence import AIPMOCadenceService
from ..services.project_knowledge_graph_service import ProjectKnowledgeGraphService
from ..services.signal.symbol_index import SymbolIndexService
from ..services import workload as workload_service
router = APIRouter(prefix="/scheduler", tags=["scheduler"])
@router.get("/health")
async def scheduler_health() -> dict:
"""调度模块健康检查。"""
return {"code": 0, "message": "success", "data": {"status": "ok"}}
@router.post("/workloads/recalc")
async def trigger_workload_recalc(
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""手动触发一次负载重算。"""
_ = current_user
tenant_ids = await workload_service.list_tenant_ids(db)
if not tenant_ids:
tenant_ids = [1]
total = 0
for tenant_id in tenant_ids:
total += await workload_service.recalculate_all_users_workload_ratio(db, tenant_id=tenant_id)
return {
"code": 0,
"message": "success",
"data": {"tenant_count": len(tenant_ids), "user_count": total},
}
@router.post("/knowledge-base/urls/refresh")
async def trigger_knowledge_url_refresh(
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""手动触发 URL 知识库回抓。"""
tenant_id = int(current_user.tenant_id)
cutoff = datetime.now() - timedelta(days=7)
docs = (
await db.execute(
select(KnowledgeBaseDoc).where(
and_(
KnowledgeBaseDoc.tenant_id == tenant_id,
KnowledgeBaseDoc.source_type == "url",
or_(
KnowledgeBaseDoc.last_fetched_at.is_(None),
KnowledgeBaseDoc.last_fetched_at <= cutoff,
),
)
)
)
).scalars().all()
refreshed = 0
failed = 0
service = AdaptiveRAGService(db, tenant_id=tenant_id)
for doc in docs:
try:
await service.refresh_url_doc(doc_id=int(doc.id), project_id=None)
refreshed += 1
except Exception:
failed += 1
return {
"code": 0,
"message": "success",
"data": {"candidate_count": len(docs), "refreshed": refreshed, "failed": failed},
}
@router.post("/knowledge-graph/incremental")
async def trigger_knowledge_graph_incremental(
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""手动触发一次知识图谱增量流水线。"""
tenant_id = int(current_user.tenant_id)
service = ProjectKnowledgeGraphService(db, tenant_id=tenant_id)
data = await service.run_incremental_cycle(run_source="manual_scheduler_api")
return {
"code": 0,
"message": "success",
"data": data,
}
@router.post("/symbols/sync")
async def trigger_symbol_index_sync(
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""手动触发一次 Symbol 索引同步。"""
tenant_id = int(current_user.tenant_id)
service = SymbolIndexService(db, tenant_id=tenant_id)
data = await service.sync_codebase_symbols(max_files=800, force_reindex=False)
return {
"code": 0,
"message": "success",
"data": data,
}
@router.post("/features/catalog/sync")
async def trigger_feature_catalog_sync(
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""手动触发一次 Feature Catalog -> KG 同步。"""
tenant_id = int(current_user.tenant_id)
service = ProjectKnowledgeGraphService(db, tenant_id=tenant_id)
data = await service.sync_feature_catalog_now()
return {
"code": 0,
"message": "success",
"data": data,
}
@router.post("/ai-pmo/cadence/run")
async def trigger_ai_pmo_cadence_run(
cadence_type: str = "all",
project_id: int | None = None,
current_user: User = Depends(require_manager_user),
db: AsyncSession = Depends(get_db),
) -> dict:
"""手动触发 AI PMO cadence snapshot。"""
tenant_id = int(current_user.tenant_id)
service = AIPMOCadenceService(db, tenant_id=tenant_id)
if cadence_type != "all":
snapshot = await service.generate_snapshot(cadence_type=cadence_type, project_id=project_id)
await db.commit()
return {"code": 0, "message": "success", "data": {"count": 1, "items": [snapshot]}}
items = [await service.generate_snapshot(cadence_type="risk_review", project_id=project_id)]
if project_id is None:
items.append(await service.generate_snapshot(cadence_type="daily_standup", project_id=None))
items.append(await service.generate_snapshot(cadence_type="milestone_check", project_id=None))
if datetime.now().weekday() == 0:
items.append(await service.generate_snapshot(cadence_type="weekly_review", project_id=None))
project_ids = (
await db.execute(
select(Project.id).where(
and_(
Project.tenant_id == tenant_id,
Project.status.in_(["active", "planning", "doing", "todo"]),
)
)
)
).scalars().all()
for pid in project_ids:
items.append(await service.generate_snapshot(cadence_type="risk_review", project_id=int(pid)))
else:
items.append(await service.generate_snapshot(cadence_type="daily_standup", project_id=int(project_id)))
items.append(await service.generate_snapshot(cadence_type="milestone_check", project_id=int(project_id)))
await db.commit()
return {"code": 0, "message": "success", "data": {"count": len(items), "items": items}}