feat(platform): close AI expense value loop

Add tenant-safe value, telemetry, connector, commercial, and production-readiness foundations.
This commit is contained in:
caoxiaozhu
2026-07-17 14:14:08 +08:00
parent 242d68c36f
commit 787bc3a481
507 changed files with 82072 additions and 6344 deletions

View File

@@ -0,0 +1,201 @@
from __future__ import annotations
from typing import Any
from fastapi import HTTPException, status
from sqlalchemy import and_, func, not_, or_, select
from sqlalchemy.orm import Session
from app.api.deps import CurrentUserContext
from app.models.agent_run import AgentRun
from app.schemas.agent_run import AgentRunRead
from app.services.finance_dashboard_access_policy import FinanceDashboardAccessPolicy
from app.services.finance_dashboard_scope import (
FINANCE_DASHBOARD_TASK_TYPE,
resolve_finance_dashboard_data_scope,
)
class AgentRunAccessPolicy:
"""对租户边界和敏感领域权限做返回前的第二层校验。"""
@classmethod
def build_query_scope(cls, current_user: CurrentUserContext) -> Any:
"""把财务快照领域门禁下推到 limit 之前,避免不可见记录挤占窗口。"""
tenant_id = cls.require_current_tenant_id(current_user)
route_task_type = func.coalesce(
AgentRun.route_json["task_type"].as_string(),
"",
)
route_job_type = func.coalesce(
AgentRun.route_json["job_type"].as_string(),
"",
)
is_finance_snapshot = or_(
route_task_type == FINANCE_DASHBOARD_TASK_TYPE,
route_job_type == FINANCE_DASHBOARD_TASK_TYPE,
)
if not FinanceDashboardAccessPolicy.can_read(current_user):
return not_(is_finance_snapshot)
expected_data_scope = resolve_finance_dashboard_data_scope(tenant_id)
valid_finance_scope = and_(
AgentRun.route_json["tenant_id"].as_string() == tenant_id,
AgentRun.ontology_json["tenant_id"].as_string() == tenant_id,
AgentRun.route_json["data_scope"].as_string() == expected_data_scope,
AgentRun.ontology_json["data_scope"].as_string() == expected_data_scope,
)
return or_(not_(is_finance_snapshot), valid_finance_scope)
@classmethod
def filter_list_items(
cls,
runs: list[AgentRunRead],
current_user: CurrentUserContext,
db: Session,
) -> list[AgentRunRead]:
payloads_by_run_id = cls._run_scope_payloads(
db,
[run.run_id for run in runs],
)
current_tenant_id = cls.require_current_tenant_id(current_user)
visible: list[AgentRunRead] = []
for run in runs:
payloads = payloads_by_run_id.get(run.run_id)
if payloads is None:
continue
route_json, ontology_json = payloads
if cls._tenant_scope_from_payloads(route_json, ontology_json) != current_tenant_id:
continue
if cls.is_finance_dashboard_snapshot(run) and not cls._can_read_finance_snapshot_scope(
cls._finance_scope_from_payloads(route_json, ontology_json),
current_user,
):
continue
visible.append(run)
return visible
@classmethod
def _run_scope_payloads(
cls,
db: Session,
run_ids: list[str],
) -> dict[str, tuple[object, object]]:
if not run_ids:
return {}
rows = db.execute(
select(AgentRun.run_id, AgentRun.route_json, AgentRun.ontology_json).where(
AgentRun.run_id.in_(run_ids)
)
).all()
return {
str(run_id): (route_json, ontology_json) for run_id, route_json, ontology_json in rows
}
@classmethod
def require_detail_read(
cls,
run: AgentRunRead,
current_user: CurrentUserContext,
) -> None:
current_tenant_id = cls.require_current_tenant_id(current_user)
if cls._tenant_scope_from_payloads(run.route_json, run.ontology_json) != current_tenant_id:
cls._raise_not_found()
if not cls.is_finance_dashboard_snapshot(run):
return
run_scope = cls._finance_scope_from_payloads(run.route_json, run.ontology_json)
expected_scope = (
resolve_finance_dashboard_data_scope(current_tenant_id) if current_tenant_id else None
)
if (
current_tenant_id is None
or run_scope is None
or run_scope != (current_tenant_id, expected_scope)
):
cls._raise_not_found()
FinanceDashboardAccessPolicy.require_read(current_user)
@classmethod
def is_finance_dashboard_snapshot(cls, run: AgentRunRead) -> bool:
route = run.route_json if isinstance(run.route_json, dict) else {}
return any(
str(route.get(key) or "").strip() == FINANCE_DASHBOARD_TASK_TYPE
for key in ("task_type", "job_type")
)
@classmethod
def _can_read_finance_snapshot_scope(
cls,
run_scope: tuple[str, str] | None,
current_user: CurrentUserContext,
) -> bool:
current_tenant_id = cls._normalized_tenant_id(current_user.tenant_id)
expected_scope = (
resolve_finance_dashboard_data_scope(current_tenant_id)
if current_tenant_id is not None
else None
)
return bool(
current_tenant_id
and run_scope
and run_scope == (current_tenant_id, expected_scope)
and FinanceDashboardAccessPolicy.can_read(current_user)
)
@classmethod
def _finance_scope_from_payloads(
cls,
*payloads: object,
) -> tuple[str, str] | None:
tenant_ids: list[str] = []
data_scopes: list[str] = []
for payload in payloads:
if not isinstance(payload, dict):
return None
tenant_id = cls._normalized_tenant_id(payload.get("tenant_id"))
data_scope = str(payload.get("data_scope") or "").strip()
if tenant_id is None or not data_scope:
return None
tenant_ids.append(tenant_id)
data_scopes.append(data_scope)
if not tenant_ids or len(set(tenant_ids)) != 1 or len(set(data_scopes)) != 1:
return None
return tenant_ids[0], data_scopes[0]
@classmethod
def _tenant_scope_from_payloads(
cls,
*payloads: object,
) -> str | None:
tenant_ids: list[str] = []
for payload in payloads:
if not isinstance(payload, dict) or "tenant_id" not in payload:
return None
tenant_id = cls._normalized_tenant_id(payload.get("tenant_id"))
if tenant_id is None:
return None
tenant_ids.append(tenant_id)
if len(tenant_ids) != len(payloads) or len(set(tenant_ids)) != 1:
return None
return tenant_ids[0]
@classmethod
def require_current_tenant_id(cls, current_user: CurrentUserContext) -> str:
tenant_id = cls._normalized_tenant_id(current_user.tenant_id)
if tenant_id is None:
cls._raise_not_found()
return tenant_id
@staticmethod
def _raise_not_found() -> None:
# 统一按不存在处理,避免 run_id 或作用域标记成为租户探针。
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Run not found",
)
@staticmethod
def _normalized_tenant_id(value: object) -> str | None:
normalized = str(value or "").strip()
return normalized or None