Files
group_xinghuo_jinrong/app/api/simulate.py
T
GaoYiYuan_0626 66ec7eba08 feat: C6 FR-10 代理人行为链 RISK-008
代理人异常行为链识别(RISK-008)落地:
- 新增 agent_behavior_service:三条件证据聚合(A 诱导调仓/B AUTH_403_SCOPE 越权试探/C AUTH_403_NOT_OWNER|NOT_ASSIGNED 越权查询),按代理人维度独立出 pattern 单,payload.actor_id 指向代理人,审计仅 INSERT event_type=agent_behavior_detected。
- risk_repository 新增 list_audit_events / find_agent_behavior_alert / merge_agent_behavior_payload(同日同代理人一张单,证据并集)。
- trade_gateway.submit_trade 补 actor_id 透传(代理人发起交易归属发起人,缺省 SYSTEM);simulate 路由传入 auth.actor_id。
- chat_tools 新增 query_agent_behavior 只读 Tool(agent_id 过滤 + 客户脱敏),tool_service 补意图词与摘要。
- scripts/cron/agent_behavior_scan.py 定时扫描脚本。
- 修复 append_alert_event 序列化缺 default=str(C6 evidence 含 datetime 字段)。
- 单测 12 例(_count_induce 边界 / 三条件 / 出单去重 / payload 归属 / Tool 过滤脱敏)。
全量 pytest 482 passed 0 failed(470 基线 + 12 C6)。
2026-09-07 19:53:39 +08:00

54 lines
2.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""模拟交易网关路由(PRD FR-1 · 薄路由,不含业务)。
鉴权:`Depends(get_auth_context)`(B6 回挂,评审 P2-2)——一期接受
risk_demo 演示账号或客户本人(auth.customer_id == 请求 customer_id,
PRD FR-1 §鉴权);越权经 deps.deny 审计后 403。T-01 后工厂内部换 JWT。
trace:main 中间件贯通(B7),响应头 X-Trace-Id 回写;service 层 ensure_trace
仍兜底脚本/测试直调场景。
挂载:main.py include(B7)。错误体统一 ApiError → 手册 §10 结构(挂账④),
convert 400 / 资源 404 不变 HTTP 语义。
"""
from __future__ import annotations
from decimal import Decimal
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from app.api.deps import AuthContext, deny, get_auth_context
from app.gateway.trade_gateway import UnsupportedTradeType, submit_trade
from app.repository.risk_repository import RiskRepository
from app.utils.exceptions import ApiError
router = APIRouter(prefix="/api/simulate", tags=["simulate"])
def _repo() -> RiskRepository:
"""审计仓储(deny 留痕用;测试 monkeypatch 点)。"""
return RiskRepository()
class TradeRequest(BaseModel):
customer_id: str = Field(..., min_length=1)
product_id: str = Field(..., min_length=1)
trade_type: str = Field(..., max_length=16, description="subscribe | redeem;convert 显式拒绝")
amount: Decimal = Field(..., gt=0, description="交易金额(元),必须为正数")
@router.post("/trade")
def submit_trade_api(req: TradeRequest, auth: AuthContext = Depends(get_auth_context)) -> dict:
"""模拟交易(FR-1):适当性阻断或放行+引擎判定,返回 blocked + trade_id。"""
if not (auth.has_role("risk_demo") or (auth.is_customer() and auth.customer_id == req.customer_id)):
deny(
auth, "AUTH_403_ROLE", _repo(),
customer_id=req.customer_id, message="risk_demo or owner customer only",
agent_type="platform", # 网关越权与放行审计同口径(复审 P3)
)
try:
return submit_trade(req.model_dump(), actor_id=auth.actor_id)
except UnsupportedTradeType as exc:
raise ApiError(400, "BAD_REQUEST", str(exc)) from exc
except LookupError as exc: # NotFoundError 亦为其子类;已统一(B6 评审 P3-5)
raise ApiError(404, "NOT_FOUND", str(exc)) from exc