feat: 新增预算费控模型与报销审批流引擎

后端新增预算费控服务和报销单审批流模块,引入申请人费用画像
算法,优化知识库 RAG 运行时和同步逻辑,完善报销单工作流常
量和明细同步,更新差旅报销规则电子表格,前端新增预算分析
组件和数字员工模型,完善审批对话框和洞察面板交互,优化侧
边栏和顶栏样式,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-27 17:31:27 +08:00
parent cbb98f4469
commit d4d5d40569
75 changed files with 5393 additions and 686 deletions

View File

@@ -4,8 +4,9 @@ import os
import re
import socket
import threading
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Any
from typing import Any, Callable
from sqlalchemy.orm import Session
@@ -89,8 +90,10 @@ STRUCTURED_APPENDIX_LEADING_MARKERS = (
)
STRUCTURED_APPENDIX_LEADING_WINDOW = 220
_runtime_lock = threading.RLock()
_runtime_instances: dict[int, _LightRagRuntime] = {}
_runtime_signatures: dict[int, tuple[Any, ...]] = {}
_runtime_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="knowledge-rag-runtime")
_runtime_instances: dict[str, _LightRagRuntime] = {}
_runtime_signatures: dict[str, tuple[Any, ...]] = {}
_RUNTIME_CACHE_KEY = "lightrag"
class KnowledgeRagService:
@@ -133,21 +136,26 @@ class KnowledgeRagService:
runtime_hits: list[dict[str, Any]] = []
runtime_references: list[str] = []
try:
runtime = self._get_runtime()
raw = runtime.query_data(rewritten_query, conversation_history=conversation_history)
data = raw.get("data") if isinstance(raw, dict) else {}
chunks = list(data.get("chunks") or []) if isinstance(data, dict) else []
entities = list(data.get("entities") or []) if isinstance(data, dict) else []
runtime_references = list(data.get("references") or []) if isinstance(data, dict) else []
runtime_hits = self._build_hits_from_query_data(
query=rewritten_query,
chunks=chunks,
entities=entities,
limit=limit,
)
except Exception as exc:
logger.warning("Knowledge query failed: %s", exc)
if not local_result.confident:
try:
raw = self._run_runtime_operation(
lambda runtime: runtime.query_data(
rewritten_query,
conversation_history=conversation_history,
)
)
data = raw.get("data") if isinstance(raw, dict) else {}
chunks = list(data.get("chunks") or []) if isinstance(data, dict) else []
entities = list(data.get("entities") or []) if isinstance(data, dict) else []
runtime_references = list(data.get("references") or []) if isinstance(data, dict) else []
runtime_hits = self._build_hits_from_query_data(
query=rewritten_query,
chunks=chunks,
entities=entities,
limit=limit,
)
except Exception as exc:
logger.warning("Knowledge query failed: %s", exc)
all_hits: dict[str, dict[str, Any]] = {}
for hit in local_result.hits:
@@ -189,7 +197,7 @@ class KnowledgeRagService:
],
"raw_references": runtime_references,
"metadata": {
"retrieval_strategy": "fusion",
"retrieval_strategy": "fusion" if runtime_hits else "local_text_chunks",
"local_total_chunks": local_result.total_chunks,
"local_best_score": local_result.best_score,
},
@@ -244,14 +252,17 @@ class KnowledgeRagService:
file_paths: list[str] = []
document_summaries: list[dict[str, Any]] = []
runtime = self._get_runtime()
existing_statuses = runtime.get_document_statuses(normalized_ids)
existing_statuses = self._run_runtime_operation(
lambda runtime: runtime.get_document_statuses(normalized_ids)
)
for document_id in normalized_ids:
entry = knowledge_service.get_document_entry(document_id)
if force and document_id in existing_statuses:
try:
runtime.delete_document(document_id)
self._run_runtime_operation(
lambda runtime, target_id=document_id: runtime.delete_document(target_id)
)
except Exception as exc:
logger.warning(
"Delete existing LightRAG document failed doc_id=%s: %s", document_id, exc
@@ -277,13 +288,17 @@ class KnowledgeRagService:
)
)
track_id = runtime.insert_documents(
texts=texts,
document_ids=normalized_ids,
file_paths=file_paths,
track_id = self._run_runtime_operation(
lambda runtime: runtime.insert_documents(
texts=texts,
document_ids=normalized_ids,
file_paths=file_paths,
)
)
statuses = runtime.get_document_statuses(normalized_ids)
statuses = self._run_runtime_operation(
lambda runtime: runtime.get_document_statuses(normalized_ids)
)
succeeded_document_ids: list[str] = []
failed_documents: list[dict[str, str]] = []
summary_by_id = {
@@ -344,7 +359,9 @@ class KnowledgeRagService:
if not target_ids:
return {}
try:
statuses = self._get_runtime().get_document_statuses(target_ids)
statuses = self._run_runtime_operation(
lambda runtime: runtime.get_document_statuses(target_ids)
)
except Exception as exc:
logger.warning("Load LightRAG document statuses failed: %s", exc)
return {}
@@ -358,16 +375,40 @@ class KnowledgeRagService:
if not normalized_id:
return
try:
self._get_runtime().delete_document(normalized_id)
self._run_runtime_operation(
lambda runtime: runtime.delete_document(normalized_id)
)
except Exception as exc:
logger.warning("Delete LightRAG document ignored doc_id=%s: %s", normalized_id, exc)
def _get_runtime(self) -> _LightRagRuntime:
def _run_runtime_operation(self, operation: Callable[[_LightRagRuntime], Any]) -> Any:
signature, runtime_kwargs = self._build_runtime_signature()
thread_id = threading.get_ident()
return _runtime_executor.submit(
self._execute_runtime_operation,
signature,
runtime_kwargs,
operation,
).result()
def _execute_runtime_operation(
self,
signature: tuple[Any, ...],
runtime_kwargs: dict[str, Any],
operation: Callable[[_LightRagRuntime], Any],
) -> Any:
return operation(self._get_runtime(signature=signature, runtime_kwargs=runtime_kwargs))
def _get_runtime(
self,
*,
signature: tuple[Any, ...] | None = None,
runtime_kwargs: dict[str, Any] | None = None,
) -> _LightRagRuntime:
if signature is None or runtime_kwargs is None:
signature, runtime_kwargs = self._build_runtime_signature()
with _runtime_lock:
runtime = _runtime_instances.get(thread_id)
if runtime is not None and _runtime_signatures.get(thread_id) == signature:
runtime = _runtime_instances.get(_RUNTIME_CACHE_KEY)
if runtime is not None and _runtime_signatures.get(_RUNTIME_CACHE_KEY) == signature:
return runtime
if runtime is not None:
@@ -377,8 +418,8 @@ class KnowledgeRagService:
logger.warning("Finalize previous LightRAG runtime failed: %s", exc)
runtime = _LightRagRuntime(**runtime_kwargs)
_runtime_instances[thread_id] = runtime
_runtime_signatures[thread_id] = signature
_runtime_instances[_RUNTIME_CACHE_KEY] = runtime
_runtime_signatures[_RUNTIME_CACHE_KEY] = signature
return runtime
def _build_runtime_signature(self) -> tuple[tuple[Any, ...], dict[str, Any]]:
@@ -633,6 +674,10 @@ class KnowledgeRagService:
def shutdown_knowledge_rag_runtime() -> None:
_runtime_executor.submit(_shutdown_runtime_instances).result()
def _shutdown_runtime_instances() -> None:
with _runtime_lock:
for runtime in list(_runtime_instances.values()):
try: