feat: 新增归档中心页面并完善知识库与报销查询能力
新增前端归档中心视图及相关工具函数,扩充知识库文档分类和 提取器支持多种格式,增强编排器报销查询的多维度检索,优 化本体规则和用户代理审核消息,前端完善报销创建和审批详 情交互细节,补充单元测试覆盖。
This commit is contained in:
@@ -228,6 +228,60 @@ def test_conversation_hydration_does_not_reuse_review_type_for_fresh_expense_pro
|
||||
assert continued_context["review_form_values"]["expense_type"] == "差旅费"
|
||||
|
||||
|
||||
def test_conversation_scope_creates_new_session_for_different_claim() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
service = AgentConversationService(db)
|
||||
old_conversation = service.get_or_create_conversation(
|
||||
conversation_id="conv-old-claim-scope",
|
||||
user_id="emp-scope@example.com",
|
||||
source="user_message",
|
||||
context_json={
|
||||
"session_type": "expense",
|
||||
"draft_claim_id": "claim-old",
|
||||
"attachment_names": ["old-hotel.pdf"],
|
||||
"attachment_count": 1,
|
||||
"review_form_values": {
|
||||
"expense_type": "住宿票",
|
||||
"merchant_name": "旧酒店",
|
||||
},
|
||||
},
|
||||
)
|
||||
service.append_message(
|
||||
conversation_id=old_conversation.conversation_id,
|
||||
role="user",
|
||||
content="继续补充旧酒店发票",
|
||||
)
|
||||
|
||||
scoped_conversation = service.get_or_create_conversation(
|
||||
conversation_id=old_conversation.conversation_id,
|
||||
user_id="emp-scope@example.com",
|
||||
source="user_message",
|
||||
context_json={
|
||||
"session_type": "expense",
|
||||
"draft_claim_id": "claim-current",
|
||||
},
|
||||
)
|
||||
conflict_context = service.hydrate_context_json(
|
||||
conversation=old_conversation,
|
||||
context_json={"draft_claim_id": "claim-current"},
|
||||
message="继续补充当前单据的火车票",
|
||||
)
|
||||
scoped_context = service.hydrate_context_json(
|
||||
conversation=scoped_conversation,
|
||||
context_json={"draft_claim_id": "claim-current"},
|
||||
message="继续补充当前单据的火车票",
|
||||
)
|
||||
|
||||
db.refresh(old_conversation)
|
||||
assert scoped_conversation.conversation_id != old_conversation.conversation_id
|
||||
assert scoped_conversation.draft_claim_id == "claim-current"
|
||||
assert old_conversation.draft_claim_id == "claim-old"
|
||||
assert conflict_context == {"draft_claim_id": "claim-current"}
|
||||
assert scoped_context["draft_claim_id"] == "claim-current"
|
||||
assert scoped_context["conversation_history"] == []
|
||||
|
||||
|
||||
def test_orchestrator_history_query_filters_location_time_and_returns_real_amount(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
@@ -322,6 +376,89 @@ def test_orchestrator_history_query_filters_location_time_and_returns_real_amoun
|
||||
assert "321.45" in response.result["answer"]
|
||||
|
||||
|
||||
def test_orchestrator_archive_query_filters_archived_claims_and_limits_preview(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
monkeypatch.setattr(
|
||||
"app.services.runtime_chat.RuntimeChatService.complete",
|
||||
lambda *_args, **_kwargs: None,
|
||||
)
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
employee = Employee(
|
||||
id="emp-archive-query",
|
||||
employee_no="E9021",
|
||||
name="归档员工",
|
||||
email="archive-query@example.com",
|
||||
)
|
||||
claims = []
|
||||
for index in range(6):
|
||||
claims.append(
|
||||
ExpenseClaim(
|
||||
id=f"claim-archive-query-{index}",
|
||||
claim_no=f"EXP-ARCHIVE-{index + 1:03d}",
|
||||
employee=employee,
|
||||
employee_id=employee.id,
|
||||
employee_name="归档员工",
|
||||
department_name="交付部",
|
||||
expense_type="travel",
|
||||
reason=f"归档差旅 {index + 1}",
|
||||
location="上海",
|
||||
amount=Decimal("100.00") + Decimal(index),
|
||||
currency="CNY",
|
||||
invoice_count=1,
|
||||
occurred_at=datetime(2026, 2, index + 1, 9, 0, tzinfo=UTC),
|
||||
submitted_at=datetime(2026, 2, index + 2, 10, 0, tzinfo=UTC),
|
||||
status="approved",
|
||||
approval_stage="归档入账",
|
||||
)
|
||||
)
|
||||
draft_claim = ExpenseClaim(
|
||||
id="claim-archive-query-draft",
|
||||
claim_no="EXP-ARCHIVE-DRAFT",
|
||||
employee=employee,
|
||||
employee_id=employee.id,
|
||||
employee_name="归档员工",
|
||||
department_name="交付部",
|
||||
expense_type="travel",
|
||||
reason="未归档草稿",
|
||||
location="上海",
|
||||
amount=Decimal("999.00"),
|
||||
currency="CNY",
|
||||
invoice_count=0,
|
||||
occurred_at=datetime(2026, 3, 1, 9, 0, tzinfo=UTC),
|
||||
submitted_at=None,
|
||||
status="draft",
|
||||
approval_stage="待提交",
|
||||
)
|
||||
db.add_all([employee, *claims, draft_claim])
|
||||
db.commit()
|
||||
|
||||
response = OrchestratorService(db).run(
|
||||
OrchestratorRequest(
|
||||
source="user_message",
|
||||
user_id="archive-query@example.com",
|
||||
message="帮我查询一下我的归档的单据有哪些?",
|
||||
context_json={
|
||||
"client_now_iso": "2026-05-21T04:00:00.000Z",
|
||||
"client_timezone_offset_minutes": -480,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
query_payload = response.result["query_payload"]
|
||||
assert response.status == "succeeded"
|
||||
assert response.trace_summary.intent == "query"
|
||||
assert query_payload["record_count"] == 6
|
||||
assert query_payload["preview_count"] == 5
|
||||
assert query_payload["preview_limit"] == 5
|
||||
assert query_payload["title"] == "最近 5 条你的归档报销单"
|
||||
assert all(record["status"] == "approved" for record in query_payload["records"])
|
||||
assert "EXP-ARCHIVE-DRAFT" not in [record["claim_no"] for record in query_payload["records"]]
|
||||
assert response.result["suggested_actions"] == []
|
||||
assert "下面先列出最近 5 条记录" in response.result["answer"]
|
||||
|
||||
|
||||
def test_orchestrator_expense_preview_does_not_persist_claim_before_user_action(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user