230 lines
8.1 KiB
Python
230 lines
8.1 KiB
Python
from __future__ import annotations
|
||
|
||
from .shared import *
|
||
|
||
from .signal_core import _as_dict
|
||
|
||
async def _project_task_metrics(
|
||
db: AsyncSession,
|
||
*,
|
||
tenant_id: int,
|
||
project_id: int,
|
||
) -> dict[str, int]:
|
||
result = await db.execute(
|
||
select(Task.status).where(
|
||
and_(
|
||
Task.project_id == project_id,
|
||
Task.tenant_id == tenant_id,
|
||
)
|
||
)
|
||
)
|
||
statuses = [str(row[0] or "").lower() for row in result.all()]
|
||
total = len(statuses)
|
||
done = len([s for s in statuses if s in DONE_STATUSES])
|
||
return {"total": total, "done": done}
|
||
|
||
async def _build_governance_snapshot(
|
||
db: AsyncSession,
|
||
*,
|
||
tenant_id: int,
|
||
project_id: int,
|
||
charter_id: int | None = None,
|
||
) -> dict[str, Any]:
|
||
charter: TruthNode | None = None
|
||
if charter_id:
|
||
charter = (
|
||
await db.execute(
|
||
select(TruthNode).where(
|
||
and_(
|
||
TruthNode.id == charter_id,
|
||
TruthNode.project_id == project_id,
|
||
TruthNode.tenant_id == tenant_id,
|
||
TruthNode.node_type == "charter",
|
||
)
|
||
)
|
||
)
|
||
).scalar_one_or_none()
|
||
if charter is None:
|
||
charter = (
|
||
await db.execute(
|
||
select(TruthNode)
|
||
.where(
|
||
and_(
|
||
TruthNode.project_id == project_id,
|
||
TruthNode.tenant_id == tenant_id,
|
||
TruthNode.node_type == "charter",
|
||
)
|
||
)
|
||
.order_by(TruthNode.created_at.desc())
|
||
.limit(1)
|
||
)
|
||
).scalar_one_or_none()
|
||
|
||
confidence_score = None
|
||
confidence_risk = False
|
||
charter_id_value = int(charter.id) if charter else None
|
||
if charter:
|
||
charter_content = _as_dict(charter.content)
|
||
metadata = charter_content.get("metadata")
|
||
if isinstance(metadata, dict):
|
||
raw_score = metadata.get("confidence_score")
|
||
try:
|
||
confidence_score = float(raw_score)
|
||
except (TypeError, ValueError):
|
||
confidence_score = None
|
||
confidence_risk = confidence_score is not None and confidence_score < 0.8
|
||
|
||
binding = None
|
||
if charter_id_value:
|
||
binding = (
|
||
await db.execute(
|
||
select(ModelRequirementBinding, ModelVersion)
|
||
.join(
|
||
ModelVersion,
|
||
ModelVersion.id == ModelRequirementBinding.model_version_id,
|
||
isouter=True,
|
||
)
|
||
.where(
|
||
and_(
|
||
ModelRequirementBinding.tenant_id == tenant_id,
|
||
ModelRequirementBinding.truth_node_id == charter_id_value,
|
||
)
|
||
)
|
||
.order_by(ModelRequirementBinding.created_at.desc())
|
||
.limit(1)
|
||
)
|
||
).first()
|
||
|
||
model_block = False
|
||
model_info: dict[str, Any] = {
|
||
"bound": False,
|
||
"compatibility_score": None,
|
||
"model_version_id": None,
|
||
"model_name": None,
|
||
"provider": None,
|
||
}
|
||
est_cost = 0.01
|
||
if binding:
|
||
binding_row, model_row = binding
|
||
score = int(binding_row.compatibility_score or 0)
|
||
model_block = score < 70
|
||
model_info = {
|
||
"bound": True,
|
||
"compatibility_score": score,
|
||
"model_version_id": int(binding_row.model_version_id),
|
||
"model_name": getattr(model_row, "name", None),
|
||
"provider": getattr(model_row, "provider", None),
|
||
}
|
||
try:
|
||
est_cost = max(float(binding_row.estimated_cost or 0.01), 0.01)
|
||
except (TypeError, ValueError):
|
||
est_cost = 0.01
|
||
|
||
budget_service = BudgetEnforcementService(db)
|
||
budget_check = await budget_service.check_budget_before_ai_call(
|
||
tenant_id=tenant_id,
|
||
estimated_cost_usd=est_cost,
|
||
project_id=project_id,
|
||
)
|
||
|
||
budget_block = not bool(budget_check.get("allowed", True))
|
||
intervention_required = confidence_risk or model_block or budget_block
|
||
blockers: list[str] = []
|
||
warnings: list[str] = []
|
||
if confidence_risk:
|
||
warnings.append("宪章 AI 置信度低于 0.80,建议补充关键事实后再推进。")
|
||
if model_block:
|
||
blockers.append("模型匹配分低于 70,禁止自动推进发布验证。")
|
||
elif not model_info["bound"]:
|
||
warnings.append("当前宪章未绑定模型版本,建议先完成模型能力矩阵校验。")
|
||
if budget_block:
|
||
blockers.append(f"预算管控阻断:{budget_check.get('reason') or 'budget_blocked'}")
|
||
elif str(budget_check.get("reason") or "") == "within_budget":
|
||
usage = budget_check.get("usage_percent")
|
||
if isinstance(usage, (int, float)) and float(usage) >= 80:
|
||
warnings.append("预算已接近阈值,建议执行成本优化方案。")
|
||
|
||
return {
|
||
"project_id": project_id,
|
||
"charter_id": charter_id_value,
|
||
"intervention_required": intervention_required,
|
||
"blockers": blockers,
|
||
"warnings": warnings,
|
||
"confidence": {
|
||
"score": confidence_score,
|
||
"risk": confidence_risk,
|
||
},
|
||
"model_binding": model_info,
|
||
"budget": budget_check,
|
||
}
|
||
|
||
async def _assert_release_transition_allowed(
|
||
db: AsyncSession,
|
||
*,
|
||
tenant_id: int,
|
||
project_id: int,
|
||
release_node: TruthNode,
|
||
target_status: str,
|
||
) -> None:
|
||
current = str(release_node.status or "pending").lower()
|
||
target = str(target_status or "").lower()
|
||
if target == "verified":
|
||
raise HTTPException(status_code=409, detail="请通过验证完成接口标记为已验证")
|
||
if target not in RELEASE_STATUS_TRANSITIONS.get(current, set()):
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail=f"非法状态流转:{current} -> {target}",
|
||
)
|
||
|
||
content = _as_dict(release_node.content)
|
||
if target in {"releasing", "online"}:
|
||
task_ids = [int(tid) for tid in (content.get("task_ids") or []) if str(tid).isdigit()]
|
||
if not task_ids:
|
||
raise HTTPException(status_code=409, detail="发布计划未绑定任务")
|
||
task_result = await db.execute(
|
||
select(Task.id, Task.status).where(
|
||
and_(
|
||
Task.tenant_id == tenant_id,
|
||
Task.project_id == project_id,
|
||
Task.id.in_(task_ids),
|
||
)
|
||
)
|
||
)
|
||
task_rows = list(task_result.all())
|
||
if len(task_rows) != len(task_ids):
|
||
raise HTTPException(status_code=409, detail="发布计划存在无效任务绑定")
|
||
not_done = [row for row in task_rows if str(row[1] or "").lower() not in DONE_STATUSES]
|
||
if not_done:
|
||
raise HTTPException(status_code=409, detail="存在未完成任务,禁止进入发布流程")
|
||
|
||
governance = await _build_governance_snapshot(
|
||
db,
|
||
tenant_id=tenant_id,
|
||
project_id=project_id,
|
||
charter_id=int(content.get("charter_id") or 0) or None,
|
||
)
|
||
if governance.get("blockers"):
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail=";".join([str(item) for item in governance.get("blockers", [])]),
|
||
)
|
||
|
||
verification_items = content.get("verification_items")
|
||
if not isinstance(verification_items, list) or len(verification_items) == 0:
|
||
raise HTTPException(status_code=409, detail="请先生成发布验证清单")
|
||
|
||
def _assert_charter_aligned(charter: TruthNode) -> None:
|
||
if (charter.status or "").lower() not in ALIGNED_STATUSES:
|
||
raise HTTPException(
|
||
status_code=409,
|
||
detail="宪章未对齐,任务环节未解锁",
|
||
)
|
||
|
||
def _is_manager(user: User) -> bool:
|
||
return has_any_role(str(user.role or ""), ["BOSS", "ADMIN"])
|
||
|
||
def _require_manager(user: User) -> None:
|
||
if not _is_manager(user):
|
||
raise HTTPException(status_code=403, detail="仅项目负责人或管理员可执行该操作")
|
||
|