feat: 报销审批流重构与管家计划全链路贯通

- 重构报销状态注册表、审批流路由与平台风险标记
- 完善管家意图规划器与模型计划构建器全链路
- 新增 OCR Worker 脚本、数据库会话管理与通知状态
- 优化文档中心、日志视图、预算中心与员工管理交互
- 增强工作台摘要、图标资源与全局主题样式
- 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
caoxiaozhu
2026-06-06 17:19:07 +08:00
parent f60cebadb8
commit e124e4bbcb
162 changed files with 9161 additions and 1941 deletions

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import json
import uuid
from datetime import UTC, datetime, timedelta
from typing import Any
@@ -24,6 +25,34 @@ logger = get_logger("app.services.agent_runs")
KNOWLEDGE_SYNC_HEARTBEAT_TIMEOUT = timedelta(minutes=30)
KNOWLEDGE_SYNC_JOB_TYPES = {"knowledge_index_sync", "llm_wiki_sync"}
LIST_ROUTE_FIELDS = (
("route_job_type", "job_type"),
("route_task_type", "task_type"),
("route_task_code", "task_code"),
("route_task_name", "task_name"),
("route_task_title", "task_title"),
("route_asset_name", "asset_name"),
("route_selected_agent", "selected_agent"),
("route_phase", "phase"),
("route_stage", "stage"),
("route_report_type", "report_type"),
("route_snapshot_key", "snapshot_key"),
("route_folder", "folder"),
("route_heartbeat_at", "heartbeat_at"),
)
LIST_ONTOLOGY_FIELDS = (
("ontology_scenario", "scenario"),
("ontology_intent", "intent"),
("ontology_parse_strategy", "parse_strategy"),
)
LIST_PROGRESS_FIELDS = {
"percent",
"total_documents",
"completed_documents",
"failed_documents",
"skipped_documents",
"current_stage",
}
class AgentRunService:
@@ -41,8 +70,22 @@ class AgentRunService:
) -> list[AgentRunRead]:
self._ensure_ready()
self._reconcile_stale_knowledge_index_runs()
runs = self.repository.list(agent=agent, status=status, source=source, limit=limit)
return [self._serialize_run(item) for item in runs]
rows = self.repository.list_light(
agent=agent,
status=status,
source=source,
limit=limit,
)
tool_calls_by_run_id = self._group_light_tool_calls(
self.repository.list_light_tool_calls([str(item["run_id"]) for item in rows])
)
return [
self._serialize_run_list_item(
item,
tool_calls_by_run_id.get(str(item["run_id"]), []),
)
for item in rows
]
def get_run(self, run_id: str) -> AgentRunRead | None:
self._ensure_ready()
@@ -435,3 +478,99 @@ class AgentRunService:
if semantic_parse
else None,
)
def _serialize_run_list_item(
self,
row: dict[str, Any],
tool_calls: list[dict[str, Any]],
) -> AgentRunRead:
return AgentRunRead(
id=str(row["id"]),
run_id=str(row["run_id"]),
agent=str(row["agent"]),
source=str(row["source"]),
user_id=row.get("user_id"),
task_id=row.get("task_id"),
ontology_json=self._build_list_ontology_json(row),
route_json=self._build_list_route_json(row),
permission_level=str(row["permission_level"]),
status=str(row["status"]),
result_summary=row.get("result_summary"),
error_message=row.get("error_message"),
started_at=row["started_at"],
finished_at=row.get("finished_at"),
tool_calls=[self._serialize_light_tool_call(item) for item in tool_calls],
semantic_parse=None,
)
def _build_list_route_json(self, row: dict[str, Any]) -> dict[str, Any]:
payload: dict[str, Any] = {}
for source_key, target_key in LIST_ROUTE_FIELDS:
self._set_if_present(payload, target_key, row.get(source_key))
progress = self._coerce_json_object(row.get("route_progress"))
compact_progress = {
key: value
for key, value in progress.items()
if key in LIST_PROGRESS_FIELDS and self._is_scalar_json_value(value)
}
if compact_progress:
payload["progress"] = compact_progress
return payload
def _build_list_ontology_json(self, row: dict[str, Any]) -> dict[str, Any]:
payload: dict[str, Any] = {}
for source_key, target_key in LIST_ONTOLOGY_FIELDS:
self._set_if_present(payload, target_key, row.get(source_key))
return payload
def _serialize_light_tool_call(self, row: dict[str, Any]) -> AgentToolCallRead:
return AgentToolCallRead(
id=str(row["id"]),
run_id=str(row["run_id"]),
tool_type=str(row["tool_type"]),
tool_name=str(row["tool_name"]),
request_json={},
response_json={},
status=str(row["status"]),
duration_ms=int(row.get("duration_ms") or 0),
error_message=row.get("error_message"),
created_at=row["created_at"],
)
@staticmethod
def _group_light_tool_calls(
tool_calls: list[dict[str, Any]],
) -> dict[str, list[dict[str, Any]]]:
grouped: dict[str, list[dict[str, Any]]] = {}
for tool_call in tool_calls:
grouped.setdefault(str(tool_call.get("run_id") or ""), []).append(tool_call)
return grouped
@staticmethod
def _coerce_json_object(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return value
if isinstance(value, str):
normalized = value.strip()
if normalized.startswith("{") and normalized.endswith("}"):
try:
loaded = json.loads(normalized)
except json.JSONDecodeError:
return {}
return loaded if isinstance(loaded, dict) else {}
return {}
@staticmethod
def _set_if_present(payload: dict[str, Any], key: str, value: Any) -> None:
if value is None:
return
if isinstance(value, str) and not value.strip():
return
if not AgentRunService._is_scalar_json_value(value):
return
payload[key] = value
@staticmethod
def _is_scalar_json_value(value: Any) -> bool:
return value is None or isinstance(value, str | int | float | bool)