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

@@ -167,7 +167,9 @@ def test_user_agent_knowledge_prompt_enforces_knowledge_boundary() -> None:
assert "不能用常识、外部知识或主观推断补齐缺失条件" in messages[0]["content"]
assert "不能只依赖排在最前面的片段" in messages[0]["content"]
assert "不能把第一列的数值直接套给后面的列名" in messages[0]["content"]
assert "最终答复必须像助手在认真回答问题" in messages[0]["content"]
assert "最终答复必须使用 Markdown" in messages[0]["content"]
assert "## 结论" in messages[0]["content"]
assert "如果不能,必须明确说当前知识库没有找到直接依据" in messages[0]["content"]
assert "禁止使用“已命中”“答案整理阶段”“稍后重试”" in messages[0]["content"]
assert "knowledge_evidence_blocks" in messages[0]["content"]
assert '"knowledge_answer_evidence": []' in messages[1]["content"]
@@ -435,6 +437,9 @@ def test_user_agent_knowledge_fallback_is_honest_and_personalized() -> None:
)
assert answer.startswith("张三,您好。")
assert "## 结论" in answer
assert "## 依据" in answer
assert "## 说明" in answer
assert "我先根据当前制度依据给出可以确认的部分" in answer
assert "已命中" not in answer
assert "答案整理阶段本轮没有及时返回" not in answer
@@ -477,8 +482,8 @@ def test_user_agent_knowledge_answer_generation_uses_fast_timeouts(monkeypatch)
)
assert answer == "测试回答"
assert captured["timeout_seconds"] == 5
assert captured["slot_timeouts"] == {"main": 3, "backup": 5}
assert captured["timeout_seconds"] == 30
assert captured["slot_timeouts"] == {"main": 20, "backup": 30}
assert captured["max_attempts"] == 1
@@ -549,7 +554,7 @@ def test_user_agent_knowledge_terms_keep_accounting_subject_in_long_query() -> N
assert "会计科目" in terms
def test_user_agent_uses_fast_knowledge_answer_without_model(monkeypatch) -> None:
def test_user_agent_knowledge_answer_uses_model_after_retrieval(monkeypatch) -> None:
session_factory = build_session_factory()
with session_factory() as db:
ontology = SemanticOntologyService(db).parse(
@@ -560,11 +565,14 @@ def test_user_agent_uses_fast_knowledge_answer_without_model(monkeypatch) -> Non
)
)
service = UserAgentService(db)
monkeypatch.setattr(
service,
"_generate_answer_with_model",
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("model should not be called")),
)
captured: dict[str, object] = {}
def fake_generate_answer(payload, **kwargs):
captured["payload"] = payload
captured.update(kwargs)
return "## 结论\n\n员工应在费用发生后 30 日内提交报销申请。"
monkeypatch.setattr(service, "_generate_answer_with_model", fake_generate_answer)
response = service.respond(
UserAgentRequest(
@@ -593,10 +601,11 @@ def test_user_agent_uses_fast_knowledge_answer_without_model(monkeypatch) -> Non
)
)
assert response.answer.startswith("张三,您好。")
assert "**结论**" in response.answer
assert captured["payload"].ontology.scenario == "knowledge"
assert "费用报销制度" in captured["fallback_answer"]
assert "## 依据" in captured["fallback_answer"]
assert response.answer.startswith("## 结论")
assert "30 日内提交报销申请" in response.answer
assert "## 依据" not in response.answer
assert "答案整理阶段本轮没有及时返回" not in response.answer
@@ -804,7 +813,8 @@ def test_user_agent_fast_knowledge_answer_renders_relevant_table_preview() -> No
assert "| 项目 | 港澳台 | 其他地区 | 国外 |" in answer
assert "| 餐补 | 75 | 55 | 140 |" in answer
assert "餐补的标准为" in answer
assert "## 依据" not in answer
assert "## 结论" in answer
assert "## 依据" in answer
def test_user_agent_fast_knowledge_answer_uses_user_grade_for_table_row() -> None:
@@ -906,8 +916,8 @@ def test_user_agent_fast_knowledge_answer_notes_missing_location_grounding() ->
assert answer is not None
assert "没有直接写出“北京”对应的地区档位或映射关系" in answer
assert "**说明**" in answer
assert "## 依据" not in answer
assert "## 说明" in answer
assert "## 依据" in answer
def test_user_agent_fast_knowledge_answer_expands_lead_in_list_items() -> None:
@@ -952,12 +962,12 @@ def test_user_agent_fast_knowledge_answer_expands_lead_in_list_items() -> None:
)
assert answer is not None
assert "**结论**" in answer
assert "## 结论" in answer
assert "登机牌、高速道路通行记录" in answer
assert "支付记录" in answer
assert "出差审批邮件、短信、微信等" in answer
assert "3" not in answer
assert "## 依据" not in answer
assert "## 依据" in answer
def test_user_agent_model_prompt_supports_contextual_personalization() -> None: