feat: 新增风险图谱算法与系统仪表盘及操作反馈体系

后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL
校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计,
优化 agent 运行和编排执行链路,清理旧开发文档,前端新增
系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈
对话框和工作台日期选择器,优化报销创建和审批详情交互,
补充单元测试覆盖。
This commit is contained in:
caoxiaozhu
2026-05-30 15:46:51 +08:00
parent 4c59941ec6
commit 7989f3a159
314 changed files with 30073 additions and 20626 deletions

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from dataclasses import dataclass
from http import HTTPStatus
from time import monotonic, sleep
from typing import Any
@@ -27,6 +28,39 @@ DEFAULT_RUNTIME_CHAT_FAILURE_COOLDOWN_SECONDS = 90
_slot_failure_until: dict[str, float] = {}
@dataclass(slots=True)
class RuntimeChatCallTrace:
slot: str
provider: str
model: str
attempt: int
status: str
duration_ms: int = 0
error_message: str | None = None
skipped_reason: str | None = None
def model_dump(self) -> dict[str, Any]:
return {
"slot": self.slot,
"provider": self.provider,
"model": self.model,
"attempt": self.attempt,
"status": self.status,
"duration_ms": self.duration_ms,
"error_message": self.error_message,
"skipped_reason": self.skipped_reason,
}
@dataclass(slots=True)
class RuntimeChatResult:
text: str | None
calls: list[RuntimeChatCallTrace]
def calls_as_dicts(self) -> list[dict[str, Any]]:
return [item.model_dump() for item in self.calls]
class RuntimeChatService:
def __init__(self, db: Session) -> None:
self.db = db
@@ -43,11 +77,47 @@ class RuntimeChatService:
slot_timeouts: dict[str, int] | None = None,
max_attempts: int | None = None,
) -> str | None:
configs = [
config
for slot in slot_priority
if (config := self._load_chat_slot(slot)) is not None
]
return self.complete_with_trace(
messages,
slot_priority=slot_priority,
max_tokens=max_tokens,
temperature=temperature,
timeout_seconds=timeout_seconds,
slot_timeouts=slot_timeouts,
max_attempts=max_attempts,
).text
def complete_with_trace(
self,
messages: list[dict[str, Any]],
*,
slot_priority: tuple[str, ...] = ("main", "backup"),
max_tokens: int = 500,
temperature: float = 0.2,
timeout_seconds: int | None = None,
slot_timeouts: dict[str, int] | None = None,
max_attempts: int | None = None,
) -> RuntimeChatResult:
configs: list[dict[str, str]] = []
calls: list[RuntimeChatCallTrace] = []
for slot in slot_priority:
config = self._load_chat_slot(slot)
if config is None:
calls.append(
RuntimeChatCallTrace(
slot=slot,
provider="",
model="",
attempt=0,
status="skipped",
skipped_reason="not_configured",
)
)
continue
configs.append(config)
if not configs:
return RuntimeChatResult(None, calls)
resolved_timeout_seconds = timeout_seconds or DEFAULT_RUNTIME_CHAT_TIMEOUT_SECONDS
resolved_slot_timeouts = dict(slot_timeouts or {})
resolved_max_attempts = max_attempts or DEFAULT_RUNTIME_CHAT_RETRY_ATTEMPTS
@@ -61,7 +131,18 @@ class RuntimeChatService:
config["slot"],
config["provider"],
)
calls.append(
RuntimeChatCallTrace(
slot=config["slot"],
provider=config["provider"],
model=config["model"],
attempt=attempt,
status="skipped",
skipped_reason="cooldown",
)
)
continue
started = monotonic()
try:
response_text = self._request_chat_completion(
config,
@@ -73,13 +154,47 @@ class RuntimeChatService:
resolved_timeout_seconds,
),
)
duration_ms = int((monotonic() - started) * 1000)
if response_text:
_slot_failure_until.pop(cache_key, None)
return response_text.strip()
calls.append(
RuntimeChatCallTrace(
slot=config["slot"],
provider=config["provider"],
model=config["model"],
attempt=attempt,
status="succeeded",
duration_ms=duration_ms,
)
)
return RuntimeChatResult(response_text.strip(), calls)
calls.append(
RuntimeChatCallTrace(
slot=config["slot"],
provider=config["provider"],
model=config["model"],
attempt=attempt,
status="empty",
duration_ms=duration_ms,
error_message="模型返回空内容。",
)
)
except Exception as exc:
duration_ms = int((monotonic() - started) * 1000)
_slot_failure_until[cache_key] = (
monotonic() + DEFAULT_RUNTIME_CHAT_FAILURE_COOLDOWN_SECONDS
)
calls.append(
RuntimeChatCallTrace(
slot=config["slot"],
provider=config["provider"],
model=config["model"],
attempt=attempt,
status="failed",
duration_ms=duration_ms,
error_message=str(exc),
)
)
logger.warning(
"Runtime chat request failed slot=%s provider=%s attempt=%s/%s: %s",
config["slot"],
@@ -91,7 +206,7 @@ class RuntimeChatService:
if attempt < resolved_max_attempts:
sleep(DEFAULT_RUNTIME_CHAT_RETRY_DELAY_SECONDS)
return None
return RuntimeChatResult(None, calls)
@staticmethod
def _build_slot_cache_key(config: dict[str, str]) -> str: