From d0e946cf477478236cfa74b38f1cc5b2a168f054 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Mon, 25 May 2026 13:35:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=AD=E5=BF=83=E4=B8=8E=E6=8A=A5=E9=94=80=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E4=BA=A4=E4=BA=92=E5=8F=8A=E4=BE=A7=E8=BE=B9=E6=A0=8F=E9=87=8D?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端优化编排器报销查询和本体检测精度,增强报销单草稿保 存和附件回填逻辑,前端重构侧边栏组件支持折叠和图标导 航,完善文档中心状态筛选和详情提示,报销创建和审批详情 页优化会话管理和费用明细交互,新增助手应用服务和预设动 作工具函数,补充单元测试覆盖。 --- .../ui/personal-workbench-home-reference.html | 746 ++++++++++++++ .../ui/personal-workbench-home-reference.png | Bin 0 -> 125711 bytes .../app/api/v1/endpoints/reimbursements.py | 6 +- .../app/services/agent_asset_onlyoffice.py | 7 +- .../src/app/services/agent_conversations.py | 2 + server/src/app/services/expense_claims.py | 107 +- server/src/app/services/ontology_detection.py | 4 +- .../src/app/services/ontology_extraction.py | 4 +- server/src/app/services/orchestrator.py | 93 +- server/src/app/services/user_agent.py | 9 +- .../app/services/user_agent_application.py | 958 ++++++++++++++++++ server/tests/test_agent_asset_service.py | 2 +- server/tests/test_expense_claim_service.py | 127 +++ server/tests/test_ontology_service.py | 34 + server/tests/test_orchestrator_review_flow.py | 267 +++++ server/tests/test_reimbursement_endpoints.py | 64 ++ server/tests/test_user_agent_service.py | 245 +++++ web/src/assets/styles/app.css | 57 +- .../styles/views/documents-center-view.css | 27 + .../travel-reimbursement-create-view.css | 11 + .../components/business/PersonalWorkbench.vue | 4 +- web/src/components/layout/SidebarRail.vue | 503 +++++++-- web/src/composables/useAppShell.js | 123 ++- web/src/composables/useRequests.js | 119 ++- web/src/utils/assistantSessionScope.js | 173 ++++ .../utils/assistantSuggestedActionPrefill.js | 46 + web/src/utils/detailAlerts.js | 30 + web/src/utils/documentCenterNewState.js | 71 ++ web/src/utils/expenseApplicationOntology.js | 178 +++- web/src/utils/markdown.js | 1 + web/src/utils/reimbursementTextInference.js | 14 +- web/src/utils/requestViewModel.js | 24 + web/src/views/AppShellRouteView.vue | 58 +- web/src/views/DocumentsCenterView.vue | 74 +- .../views/TravelReimbursementCreateView.vue | 24 +- web/src/views/TravelRequestDetailView.vue | 40 +- .../scripts/TravelReimbursementCreateView.js | 152 ++- .../views/scripts/TravelRequestDetailView.js | 86 +- .../travelReimbursementConversationModel.js | 171 +++- .../travelRequestDetailExpenseModel.js | 70 +- .../useTravelReimbursementComposerTools.js | 4 +- .../scripts/useTravelReimbursementFlow.js | 16 +- .../useTravelReimbursementSessionState.js | 86 +- .../useTravelReimbursementSubmitComposer.js | 38 +- web/tests/app-shell-detail-alerts.test.mjs | 26 + ...p-shell-financial-assistant-entry.test.mjs | 71 ++ .../assistant-session-draft-delete.test.mjs | 4 +- ...ssistant-suggested-action-prefill.test.mjs | 43 + web/tests/document-center-new-state.test.mjs | 60 ++ .../documents-center-status-filter.test.mjs | 86 +- .../expense-application-ontology.test.mjs | 69 ++ ...e-application-submit-rich-confirm.test.mjs | 46 + .../personal-workbench-assistant.test.mjs | 3 +- web/tests/reimbursementTextInference.test.mjs | 9 + web/tests/requestProgressSteps.test.mjs | 77 ++ web/tests/sidebar-collapse.test.mjs | 41 + ...avel-reimbursement-composer-tools.test.mjs | 22 + .../travel-reimbursement-guided-flow.test.mjs | 62 +- ...travel-request-detail-risk-advice.test.mjs | 39 +- 59 files changed, 5117 insertions(+), 416 deletions(-) create mode 100644 document/development/ui/personal-workbench-home-reference.html create mode 100644 document/development/ui/personal-workbench-home-reference.png create mode 100644 server/src/app/services/user_agent_application.py create mode 100644 web/src/utils/assistantSessionScope.js create mode 100644 web/src/utils/assistantSuggestedActionPrefill.js create mode 100644 web/src/utils/documentCenterNewState.js create mode 100644 web/tests/app-shell-financial-assistant-entry.test.mjs create mode 100644 web/tests/assistant-suggested-action-prefill.test.mjs create mode 100644 web/tests/document-center-new-state.test.mjs create mode 100644 web/tests/expense-application-ontology.test.mjs create mode 100644 web/tests/expense-application-submit-rich-confirm.test.mjs create mode 100644 web/tests/sidebar-collapse.test.mjs diff --git a/document/development/ui/personal-workbench-home-reference.html b/document/development/ui/personal-workbench-home-reference.html new file mode 100644 index 0000000..634d783 --- /dev/null +++ b/document/development/ui/personal-workbench-home-reference.html @@ -0,0 +1,746 @@ + + + + + + X-Financial 个人工作台首页参考稿 + + + +
+ + +
+
+
+ 个人工作台 + 把费用申请、报销处理、进度查询和制度问答集中到一个入口。 +
+
+ A + admin +
+
+ +
+
+
+ +
+ +
+

嗨,admin,描述您想做的事,AI 会直接帮您处理

+

+ 我会自动识别您的意图,协助完成费用申请、报销、查询和制度问答等业务工作, + 并把事情推进到可执行的下一步。 +

+ +
+
例如:帮我查一下上周提交的差旅报销到哪一步了
+ + +
+
+
+ +
+
+
+ + +
+

费用申请

+

发起招待、差旅、采购等费用事项

+
+ +
+
+ + +
+

报销处理

+

上传票据,生成草稿并核对材料

+
+ +
+
+ + +
+

进度查询

+

查询单据状态、审批节点和到账情况

+
+ +
+
+ + +
+

制度问答

+

咨询标准、附件要求和可报销边界

+
+
+ +
+
+
+

报销待办

+ 查看全部 +
+
+
+ +
+ 业务招待报销建议补参与人员 + AI 建议:补充客户单位、客户人数、我方陪同人员 +
+ 去补充 +
+
+ +
+ 差旅报销单待提交 + 补齐出发交通,可直接生成报销单 +
+ 继续填 +
+
+ +
+ 有 5 张票据未关联报销单 + 其中 3 张疑似交通费,可合并生成交通报销 +
+ 去整理 +
+
+
+ +
+
+

报销进度

+ 查看全部 +
+
+
+ +
+ 差旅报销 + 提交时间:2026-05-03 +
+
¥3,280主管审批中
+
+
+ +
+ 交通报销 + 提交时间:2026-05-02 +
+
¥126财务复核中
+
+
+ +
+ 办公采购 + 提交时间:2026-05-01 +
+
¥458已到账
+
+
+
+
+ +
+
+
+

智能概览

+ 本月 +
+
+
12待处理事项
+
86%材料完整率
+
2.4天平均审批时长
+
+
+ +
+
+

最新报销制度

+ 查看全部 +
+
+
+ +
+ 差旅报销管理办法(2026版) + 更新住宿标准与交通等级规则 +
+ 查看 +
+
+
+
+
+
+
+ + diff --git a/document/development/ui/personal-workbench-home-reference.png b/document/development/ui/personal-workbench-home-reference.png new file mode 100644 index 0000000000000000000000000000000000000000..76215450ef87543975ec163ecfcdcbb1c2aba33f GIT binary patch literal 125711 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 z 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 O4XlbjAVN>R>-!Cy064b* literal 0 HcmV?d00001 diff --git a/server/src/app/api/v1/endpoints/reimbursements.py b/server/src/app/api/v1/endpoints/reimbursements.py index b5f69e0..477a8ac 100644 --- a/server/src/app/api/v1/endpoints/reimbursements.py +++ b/server/src/app/api/v1/endpoints/reimbursements.py @@ -537,12 +537,12 @@ def return_expense_claim( @router.post( "/claims/{claim_id}/approve", response_model=ExpenseClaimRead, - summary="审批通过报销单", - description="直属领导审批通过后流转到财务审批;财务终审通过后进入归档入账。", + summary="审批通过单据", + description="费用申请由直属领导审批通过后完成;报销单直属领导审批后流转到财务审批,财务终审通过后进入归档入账。", responses={ status.HTTP_404_NOT_FOUND: { "model": ErrorResponse, - "description": "报销单不存在。", + "description": "单据不存在。", }, status.HTTP_400_BAD_REQUEST: { "model": ErrorResponse, diff --git a/server/src/app/services/agent_asset_onlyoffice.py b/server/src/app/services/agent_asset_onlyoffice.py index 2c99410..9b8164c 100644 --- a/server/src/app/services/agent_asset_onlyoffice.py +++ b/server/src/app/services/agent_asset_onlyoffice.py @@ -11,7 +11,8 @@ import jwt from app.api.deps import CurrentUserContext from app.core.config import get_settings -from app.schemas.agent_asset import AgentAssetOnlyOfficeConfigRead +from app.models.agent_asset import AgentAsset +from app.schemas.agent_asset import AgentAssetOnlyOfficeConfigRead, AgentAssetRead from app.services.agent_asset_spreadsheet import ( COMPANY_TRAVEL_EXPENSE_RULE_FILENAME, FINANCE_RULES_LIBRARY, @@ -40,9 +41,7 @@ class OnlyOfficeCallbackPayload: class AgentAssetOnlyOfficeMixin: @staticmethod def _resolve_onlyoffice_settings(): - from app.services import agent_assets - - return agent_assets.resolve_onlyoffice_settings() + return resolve_onlyoffice_settings() def build_rule_spreadsheet_onlyoffice_config( self, diff --git a/server/src/app/services/agent_conversations.py b/server/src/app/services/agent_conversations.py index 814e0ff..919ddb2 100644 --- a/server/src/app/services/agent_conversations.py +++ b/server/src/app/services/agent_conversations.py @@ -258,6 +258,8 @@ class AgentConversationService: ) if not should_hydrate_review_flow: for key in REVIEW_FLOW_CONTEXT_KEYS: + if key == "business_time_context" and not self._is_empty_value(merged.get(key)): + continue merged.pop(key, None) merged["conversation_id"] = conversation.conversation_id diff --git a/server/src/app/services/expense_claims.py b/server/src/app/services/expense_claims.py index 338e80f..41c6c85 100644 --- a/server/src/app/services/expense_claims.py +++ b/server/src/app/services/expense_claims.py @@ -143,6 +143,40 @@ class ExpenseClaimService( self._attachment_storage = ExpenseClaimAttachmentStorage() self._attachment_presentation = ExpenseClaimAttachmentPresentation(self._attachment_storage) + @staticmethod + def _is_expense_application_claim(claim: ExpenseClaim) -> bool: + claim_no = str(getattr(claim, "claim_no", "") or "").strip().upper() + expense_type = str(getattr(claim, "expense_type", "") or "").strip().lower() + document_type = str( + getattr(claim, "document_type_code", "") + or getattr(claim, "document_type", "") + or "" + ).strip().lower() + return ( + claim_no.startswith("APP-") + or expense_type == "application" + or expense_type.endswith("_application") + or document_type in {"application", "expense_application"} + ) + + def _validate_application_claim_for_submission(self, claim: ExpenseClaim) -> list[str]: + issues: list[str] = [] + if self._is_missing_value(claim.employee_name): + issues.append("申请人未完善") + if self._is_missing_value(claim.department_name): + issues.append("所属部门未完善") + if self._is_missing_value(claim.expense_type): + issues.append("申请类型未完善") + if self._is_missing_value(claim.reason): + issues.append("申请事由未完善") + if self._is_missing_value(claim.location): + issues.append("业务地点未完善") + if claim.amount is None or claim.amount <= Decimal("0.00"): + issues.append("预计总费用未完善") + if claim.occurred_at is None: + issues.append("申请时间未完善") + return issues + def list_claims(self, current_user: CurrentUserContext) -> list[ExpenseClaim]: stmt = ( select(ExpenseClaim) @@ -389,18 +423,51 @@ class ExpenseClaimService( self._ensure_draft_claim(claim) self._access_policy.backfill_claim_identity_from_current_user(claim, current_user) - self._sync_claim_from_items(claim) - missing_fields = self._validate_claim_for_submission(claim) + is_application_claim = self._is_expense_application_claim(claim) + if not is_application_claim: + self._sync_claim_from_items(claim) + missing_fields = ( + self._validate_application_claim_for_submission(claim) + if is_application_claim + else self._validate_claim_for_submission(claim) + ) if missing_fields: raise ExpenseClaimSubmissionBlockedError(missing_fields) before_json = self._serialize_claim(claim) - review_result = self._run_ai_submission_review(claim) + if is_application_claim: + submitted_at = datetime.now(UTC) + preserved_flags = [ + flag + for flag in list(claim.risk_flags_json or []) + if not ( + isinstance(flag, dict) + and str(flag.get("source") or "").strip() in {"submission_review", "attachment_analysis"} + ) + ] + submit_flag = { + "source": "application_submission", + "event_type": "expense_application_submission", + "severity": "info", + "label": "申请提交", + "message": "费用申请已提交至直属领导审批,并同步纳入预算管理口径。", + "previous_status": str(claim.status or "").strip(), + "previous_approval_stage": str(claim.approval_stage or "").strip(), + "next_status": "submitted", + "next_approval_stage": "直属领导审批", + "created_at": submitted_at.isoformat(), + } + claim.status = "submitted" + claim.approval_stage = "直属领导审批" + claim.risk_flags_json = [*preserved_flags, submit_flag] + claim.submitted_at = submitted_at + else: + review_result = self._run_ai_submission_review(claim) - claim.status = str(review_result.get("status") or "supplement") - claim.approval_stage = str(review_result.get("approval_stage") or "待补充") - claim.risk_flags_json = list(review_result.get("risk_flags") or []) - claim.submitted_at = datetime.now(UTC) if claim.status == "submitted" else None + claim.status = str(review_result.get("status") or "supplement") + claim.approval_stage = str(review_result.get("approval_stage") or "待补充") + claim.risk_flags_json = list(review_result.get("risk_flags") or []) + claim.submitted_at = datetime.now(UTC) if claim.status == "submitted" else None self.db.commit() self.db.refresh(claim) @@ -562,19 +629,29 @@ class ExpenseClaimService( normalized_status = str(claim.status or "").strip().lower() if normalized_status != "submitted": - raise ValueError("只有审批中的报销单可以审批通过。") + raise ValueError("只有审批中的单据可以审批通过。") previous_stage = str(claim.approval_stage or "").strip() + is_application_claim = self._is_expense_application_claim(claim) if previous_stage == "直属领导审批": if not self._access_policy.can_approve_claim(current_user, claim): - raise ValueError("只有当前直属领导审批人可以审批通过该报销单。") + raise ValueError("只有当前直属领导审批人可以审批通过该单据。") approval_source = "manual_approval" - event_type = "expense_claim_approval" - label = "领导审批通过" - next_status = "submitted" - next_stage = "财务审批" - default_message = "{operator} 已审批通过,流转至{next_stage}。" + if is_application_claim: + event_type = "expense_application_approval" + label = "领导审批通过" + next_status = "approved" + next_stage = "审批完成" + default_message = "{operator} 已审批通过,申请流程完成。" + else: + event_type = "expense_claim_approval" + label = "领导审批通过" + next_status = "submitted" + next_stage = "财务审批" + default_message = "{operator} 已审批通过,流转至{next_stage}。" elif previous_stage == "财务审批": + if is_application_claim: + raise ValueError("费用申请无需财务审批,直属领导审批通过后即完成。") if not self._access_policy.can_approve_claim(current_user, claim): raise ValueError("只有财务人员可以完成财务终审。") approval_source = "finance_approval" @@ -606,7 +683,7 @@ class ExpenseClaimService( ], "previous_status": str(claim.status or "").strip(), "previous_approval_stage": previous_stage, - "next_status": "submitted", + "next_status": next_status, "next_approval_stage": next_stage, "created_at": datetime.now(UTC).isoformat(), } diff --git a/server/src/app/services/ontology_detection.py b/server/src/app/services/ontology_detection.py index d91d348..91499e3 100644 --- a/server/src/app/services/ontology_detection.py +++ b/server/src/app/services/ontology_detection.py @@ -78,10 +78,12 @@ class OntologyDetectionMixin: document_type = str(context_json.get("document_type") or "").strip() application_stage = str(context_json.get("application_stage") or "").strip() entry_source = str(context_json.get("entry_source") or "").strip() + session_type = str(context_json.get("session_type") or "").strip() return ( document_type in EXPENSE_APPLICATION_CONTEXT_TYPES or application_stage in EXPENSE_APPLICATION_CONTEXT_TYPES - or entry_source in {"documents_application", "expense_application"} + or session_type in EXPENSE_APPLICATION_CONTEXT_TYPES + or entry_source in {"application", "documents_application", "expense_application"} ) @staticmethod diff --git a/server/src/app/services/ontology_extraction.py b/server/src/app/services/ontology_extraction.py index 219761e..3184282 100644 --- a/server/src/app/services/ontology_extraction.py +++ b/server/src/app/services/ontology_extraction.py @@ -40,10 +40,12 @@ class OntologyExtractionMixin: document_type = str(context_json.get("document_type") or "").strip() application_stage = str(context_json.get("application_stage") or "").strip() entry_source = str(context_json.get("entry_source") or "").strip() + session_type = str(context_json.get("session_type") or "").strip() return ( document_type in EXPENSE_APPLICATION_CONTEXT_TYPES or application_stage in EXPENSE_APPLICATION_CONTEXT_TYPES - or entry_source in {"documents_application", "expense_application"} + or session_type in EXPENSE_APPLICATION_CONTEXT_TYPES + or entry_source in {"application", "documents_application", "expense_application"} ) @staticmethod diff --git a/server/src/app/services/orchestrator.py b/server/src/app/services/orchestrator.py index c556450..55d2fc6 100644 --- a/server/src/app/services/orchestrator.py +++ b/server/src/app/services/orchestrator.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from datetime import UTC, datetime from typing import Any @@ -31,6 +32,11 @@ from app.services.knowledge import KnowledgeService from app.services.ontology import SemanticOntologyService from app.services.orchestrator_execution import ExecutionOutcome, OrchestratorExecutionEngine from app.services.orchestrator_expense_query import OrchestratorDatabaseQueryBuilder +from app.services.user_agent_application import ( + APPLICATION_CONTEXT_VALUES, + APPLICATION_SHORT_CONFIRMATIONS, + APPLICATION_SUBMIT_KEYWORDS, +) from app.services.user_agent import UserAgentService logger = get_logger("app.services.orchestrator") @@ -131,9 +137,11 @@ class OrchestratorService: ) selected_capability_codes = self.execution_engine._flatten_capability_codes(capabilities) is_expense_review_action = self.execution_engine._is_expense_review_action(context_json) + is_expense_application_context = self._is_expense_application_context(context_json) requires_confirmation = ( ontology.permission.level == AgentPermissionLevel.APPROVAL_REQUIRED.value and not is_expense_review_action + and not is_expense_application_context ) route_json = { @@ -188,8 +196,16 @@ class OrchestratorService: "parse_strategy": ontology.parse_strategy, } ) + clarification_status = ( + AgentRunStatus.SUCCEEDED.value + if self._is_application_submit_result(clarification_result) + else AgentRunStatus.BLOCKED.value + ) + if clarification_status == AgentRunStatus.SUCCEEDED.value: + clarification_result["clarification_required"] = False + clarification_result["missing_slots"] = [] outcome = ExecutionOutcome( - status=AgentRunStatus.BLOCKED.value, + status=clarification_status, result=clarification_result, degraded=False, tool_count=0, @@ -233,11 +249,23 @@ class OrchestratorService: context_json=context_json, ) + result_requires_confirmation = bool(outcome.result.get("requires_confirmation")) + response_requires_confirmation = requires_confirmation or ( + is_expense_application_context and result_requires_confirmation + ) final_status = ( AgentRunStatus.BLOCKED.value - if requires_confirmation - and outcome.status == AgentRunStatus.SUCCEEDED.value - and ontology.permission.level == AgentPermissionLevel.APPROVAL_REQUIRED.value + if outcome.status == AgentRunStatus.SUCCEEDED.value + and ( + ( + requires_confirmation + and ontology.permission.level == AgentPermissionLevel.APPROVAL_REQUIRED.value + ) + or ( + is_expense_application_context + and result_requires_confirmation + ) + ) else outcome.status ) response_status = self._normalize_response_status(final_status) @@ -259,7 +287,7 @@ class OrchestratorService: ontology_json=self.execution_engine._build_ontology_json(ontology), route_json={ **route_json, - "requires_confirmation": requires_confirmation, + "requires_confirmation": response_requires_confirmation, "degraded": outcome.degraded, }, permission_level=ontology.permission.level, @@ -297,7 +325,7 @@ class OrchestratorService: "route_reason": route_reason, "permission_level": ontology.permission.level, "status": response_status, - "requires_confirmation": requires_confirmation, + "requires_confirmation": response_requires_confirmation, "trace_summary": trace_summary.model_dump(), "result": outcome.result, }, @@ -311,7 +339,7 @@ class OrchestratorService: permission_level=ontology.permission.level, status=response_status, result=outcome.result, - requires_confirmation=requires_confirmation, + requires_confirmation=response_requires_confirmation, trace_summary=trace_summary, ) except Exception as exc: @@ -462,3 +490,54 @@ class OrchestratorService: if status == AgentRunStatus.BLOCKED.value: return "blocked" return "succeeded" + + @staticmethod + def _is_expense_application_context(context_json: dict[str, Any]) -> bool: + context_json = context_json or {} + context_values = { + str(context_json.get("session_type") or "").strip(), + str(context_json.get("entry_source") or "").strip(), + str(context_json.get("document_type") or "").strip(), + str(context_json.get("application_stage") or "").strip(), + } + conversation_state = context_json.get("conversation_state") + if isinstance(conversation_state, dict): + context_values.update( + { + str(conversation_state.get("session_type") or "").strip(), + str(conversation_state.get("entry_source") or "").strip(), + str(conversation_state.get("document_type") or "").strip(), + str(conversation_state.get("application_stage") or "").strip(), + } + ) + if context_values & APPLICATION_CONTEXT_VALUES: + return True + + history = context_json.get("conversation_history") + if not isinstance(history, list): + return False + current_message = re.sub(r"\s+", "", str(context_json.get("user_input_text") or "")) + looks_like_submit = ( + any(keyword in current_message for keyword in APPLICATION_SUBMIT_KEYWORDS) + or current_message in APPLICATION_SHORT_CONFIRMATIONS + or not current_message + ) + if not looks_like_submit: + return False + for item in history[-6:]: + if not isinstance(item, dict): + continue + content = str(item.get("content") or "") + if "#application-submit" in content or ("费用申请" in content and "确认" in content): + return True + return False + + @staticmethod + def _is_application_submit_result(result: dict[str, Any]) -> bool: + draft_payload = result.get("draft_payload") + return ( + isinstance(draft_payload, dict) + and str(draft_payload.get("draft_type") or "").strip() == "expense_application" + and str(draft_payload.get("status") or "").strip() == "submitted" + and bool(str(draft_payload.get("claim_no") or draft_payload.get("claim_id") or "").strip()) + ) diff --git a/server/src/app/services/user_agent.py b/server/src/app/services/user_agent.py index 1fe9ce2..295ba81 100644 --- a/server/src/app/services/user_agent.py +++ b/server/src/app/services/user_agent.py @@ -44,6 +44,7 @@ from app.services.user_agent_documents import UserAgentDocumentService from app.services.user_agent_knowledge import UserAgentKnowledgeMixin from app.services.user_agent_constants import * +from app.services.user_agent_application import UserAgentApplicationMixin from app.services.user_agent_response import UserAgentResponseMixin from app.services.user_agent_review_core import UserAgentReviewCoreMixin from app.services.user_agent_review_messages import UserAgentReviewMessageMixin @@ -55,6 +56,7 @@ from app.services.user_agent_review_travel_receipts import UserAgentReviewTravel class UserAgentService( UserAgentResponseMixin, + UserAgentApplicationMixin, UserAgentKnowledgeMixin, UserAgentReviewCoreMixin, UserAgentReviewTravelPolicyMixin, @@ -72,6 +74,12 @@ class UserAgentService( def respond(self, payload: UserAgentRequest) -> UserAgentResponse: AgentFoundationService(self.db).ensure_foundation_ready() citations = self._build_citations(payload) + risk_flags = self._resolve_risk_flags(payload) + if self._is_expense_application_request(payload): + return self._build_expense_application_response( + payload, + risk_flags=risk_flags, + ) suggested_actions = self._build_suggested_actions(payload) if self._should_prompt_expense_scene_selection(payload): return UserAgentResponse( @@ -84,7 +92,6 @@ class UserAgentService( risk_flags=[], requires_confirmation=False, ) - risk_flags = self._resolve_risk_flags(payload) query_payload = self._build_query_payload(payload) draft_payload = ( self._build_draft_payload(payload) diff --git a/server/src/app/services/user_agent_application.py b/server/src/app/services/user_agent_application.py new file mode 100644 index 0000000..1b5b53f --- /dev/null +++ b/server/src/app/services/user_agent_application.py @@ -0,0 +1,958 @@ +from __future__ import annotations + +import hashlib +import re +from datetime import UTC, datetime, timedelta +from decimal import Decimal, InvalidOperation + +from sqlalchemy import select + +from app.api.deps import CurrentUserContext +from app.models.financial_record import ExpenseClaim +from app.schemas.user_agent import ( + UserAgentDraftPayload, + UserAgentRequest, + UserAgentResponse, + UserAgentSuggestedAction, +) +from app.services.expense_claim_access_policy import ExpenseClaimAccessPolicy + +APPLICATION_CONTEXT_VALUES = { + "application", + "documents_application", + "expense_application", + "pre_approval", + "preapproval", +} +APPLICATION_BASE_FIELDS = ("time", "location", "reason") +APPLICATION_TRANSPORT_OPTIONS = ("飞机", "火车", "轮船") +APPLICATION_TRANSPORT_KEYWORDS = { + "飞机": ("飞机", "机票", "航班", "乘机", "坐飞机"), + "火车": ("火车", "高铁", "动车", "铁路", "列车"), + "轮船": ("轮船", "船", "客轮", "邮轮", "坐船"), +} +APPLICATION_DESTINATION_PREFIXES = ( + "上海", + "北京", + "广州", + "深圳", + "杭州", + "南京", + "苏州", + "成都", + "重庆", + "武汉", + "西安", + "天津", + "宁波", + "青岛", + "长沙", + "郑州", + "济南", + "合肥", + "福州", + "厦门", + "昆明", + "南昌", + "沈阳", + "大连", + "无锡", + "佛山", + "东莞", +) +APPLICATION_REASON_VERBS = ( + "支撑", + "支持", + "部署", + "上线", + "实施", + "驻场", + "拜访", + "验收", + "会议", + "采购", + "培训", + "协助", + "处理", + "办理", + "参加", + "进行", +) +APPLICATION_SUBMIT_KEYWORDS = ( + "确认提交", + "确认申请", + "提交审核", + "确认无误提交", + "直接提交", +) +APPLICATION_SHORT_CONFIRMATIONS = {"提交", "确认", "是", "好的", "可以", "没问题"} + + +class UserAgentApplicationMixin: + @staticmethod + def _is_expense_application_request(payload: UserAgentRequest) -> bool: + context_json = payload.context_json or {} + context_values = { + str(context_json.get("session_type") or "").strip(), + str(context_json.get("entry_source") or "").strip(), + str(context_json.get("document_type") or "").strip(), + str(context_json.get("application_stage") or "").strip(), + } + conversation_state = context_json.get("conversation_state") + if isinstance(conversation_state, dict): + context_values.update( + { + str(conversation_state.get("session_type") or "").strip(), + str(conversation_state.get("entry_source") or "").strip(), + str(conversation_state.get("document_type") or "").strip(), + str(conversation_state.get("application_stage") or "").strip(), + } + ) + if context_values & APPLICATION_CONTEXT_VALUES: + return True + + history = context_json.get("conversation_history") + if not isinstance(history, list): + return False + compact_message = re.sub(r"\s+", "", str(payload.message or "")) + looks_like_submit = ( + any(keyword in compact_message for keyword in APPLICATION_SUBMIT_KEYWORDS) + or compact_message in APPLICATION_SHORT_CONFIRMATIONS + ) + if not looks_like_submit: + return False + return any( + isinstance(item, dict) + and str(item.get("role") or "").strip() == "assistant" + and ( + "#application-submit" in str(item.get("content") or "") + or ("费用申请" in str(item.get("content") or "") and "确认" in str(item.get("content") or "")) + ) + for item in history[-6:] + ) + + def _build_expense_application_response( + self, + payload: UserAgentRequest, + *, + risk_flags: list[str], + ) -> UserAgentResponse: + facts = self._resolve_expense_application_facts(payload) + step = self._resolve_expense_application_step(payload, facts) + application_claim = None + if step == "submitted": + application_claim = self._create_expense_application_record(payload, facts) + facts["application_no"] = application_claim.claim_no + facts["application_claim_id"] = application_claim.id + facts["manager_name"] = self._resolve_application_manager_name(payload, application_claim) + return UserAgentResponse( + answer=self._build_expense_application_answer(payload, facts=facts, step=step), + citations=[], + suggested_actions=self._build_expense_application_actions(step, facts), + query_payload=None, + draft_payload=self._build_submitted_application_payload(application_claim, facts), + review_payload=None, + risk_flags=risk_flags, + requires_confirmation=step == "preview", + ) + + def _build_expense_application_answer( + self, + payload: UserAgentRequest, + *, + facts: dict[str, str], + step: str, + ) -> str: + recognized_table = self._build_application_summary_table(facts, include_empty=False) + + if step == "ask_missing": + missing_fields = self._resolve_application_missing_fields(facts) + missing_text = "、".join( + self._display_application_slot_label(item) + for item in missing_fields + ) + return "\n\n".join( + [ + "我已按「费用申请 / 事前审批」来处理这条内容。", + "已识别信息:\n" + recognized_table, + f"当前还需要补充:{missing_text}。", + "请一次性补齐上述字段,我会继续生成模拟申请结果并让你确认是否提交。", + ] + ) + + if step == "submitted": + application_no = str(facts.get("application_no") or "").strip() or self._build_application_claim_no(payload, facts) + manager_name = str(facts.get("manager_name") or "").strip() or "直属领导" + return "\n\n".join( + [ + f"当前操作已完成,单据已经推送给 {manager_name} 进行审核,请耐心等待。", + f"申请单号:{application_no}", + "申请信息:\n" + self._build_application_summary_table(facts), + f"当前状态:{manager_name}审核中。", + "预算处理:预计总费用已作为预算占用参考,等待领导审核确认。", + ] + ) + + return "\n\n".join( + [ + "这是模拟的费用申请结果,请核对:", + self._build_application_summary_table(facts), + "请核对上述信息无误,确认无误后 [确认](#application-submit) 提交至审批流程。", + ] + ) + + def _resolve_expense_application_facts(self, payload: UserAgentRequest) -> dict[str, str]: + facts = { + "time": "", + "location": "", + "reason": "", + "days": "", + "transport_mode": "", + "amount": "", + "application_type": "", + } + for message, is_current in self._iter_application_user_messages(payload): + partial = { + "time": self._resolve_application_time(payload, message=message) if is_current else self._resolve_application_time_from_text(message), + "location": self._resolve_application_location(payload, message=message, use_entities=is_current), + "reason": self._resolve_application_reason(message), + "days": self._resolve_application_days(message), + "transport_mode": self._resolve_application_transport_mode(message), + "amount": self._resolve_application_amount(payload, message=message) if is_current else self._resolve_application_amount_from_text(message), + "application_type": self._resolve_application_type_from_text(message), + } + for key, value in partial.items(): + if value: + facts[key] = value + + if not facts["application_type"]: + facts["application_type"] = self._infer_application_type(facts) + facts["time"] = self._expand_application_time_with_days( + facts.get("time", ""), + facts.get("days", ""), + ) + return facts + + def _resolve_expense_application_step( + self, + payload: UserAgentRequest, + facts: dict[str, str], + ) -> str: + if self._resolve_application_missing_base_fields(facts): + return "ask_missing" + if self._resolve_application_missing_followup_fields(facts): + return "ask_missing" + if self._is_application_submit_confirmation(payload): + return "submitted" + return "preview" + + @staticmethod + def _iter_application_user_messages(payload: UserAgentRequest) -> list[tuple[str, bool]]: + messages: list[tuple[str, bool]] = [] + history = (payload.context_json or {}).get("conversation_history") + if isinstance(history, list): + for item in history: + if not isinstance(item, dict): + continue + if str(item.get("role") or "").strip() != "user": + continue + content = str(item.get("content") or "").strip() + if content: + messages.append((content, False)) + current_message = str(payload.message or "").strip() + if current_message: + messages.append((current_message, True)) + return messages + + @staticmethod + def _resolve_application_missing_base_fields(facts: dict[str, str]) -> list[str]: + return [field for field in APPLICATION_BASE_FIELDS if not str(facts.get(field) or "").strip()] + + @staticmethod + def _resolve_application_missing_followup_fields(facts: dict[str, str]) -> list[str]: + return [ + field + for field in ("transport_mode", "amount") + if not str(facts.get(field) or "").strip() + ] + + def _resolve_application_missing_fields(self, facts: dict[str, str]) -> list[str]: + return [ + *self._resolve_application_missing_base_fields(facts), + *self._resolve_application_missing_followup_fields(facts), + ] + + @staticmethod + def _resolve_application_time(payload: UserAgentRequest, *, message: str | None = None) -> str: + if message and UserAgentApplicationMixin._resolve_application_time_from_text(message): + return UserAgentApplicationMixin._resolve_application_time_from_text(message) + + context_time = UserAgentApplicationMixin._resolve_application_time_from_context(payload.context_json or {}) + if context_time: + return context_time + + time_range = payload.ontology.time_range + if time_range.start_date and time_range.end_date: + return ( + time_range.start_date + if time_range.start_date == time_range.end_date + else f"{time_range.start_date} 至 {time_range.end_date}" + ) + return str(time_range.raw or "").strip() + + @staticmethod + def _resolve_application_time_from_text(message: str) -> str: + labeled = UserAgentApplicationMixin._resolve_application_labeled_value( + message, + ("发生时间", "业务发生时间", "申请时间", "时间"), + ) + if labeled: + return labeled + match = re.search( + r"(?P20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?)", + str(message or ""), + ) + return match.group("date").rstrip("日") if match else "" + + @staticmethod + def _resolve_application_time_from_context(context_json: dict[str, object]) -> str: + business_time_context = context_json.get("business_time_context") + if not isinstance(business_time_context, dict): + return "" + start_date = str(business_time_context.get("start_date") or "").strip() + end_date = str(business_time_context.get("end_date") or start_date).strip() + display_value = str(business_time_context.get("display_value") or "").strip() + if start_date and end_date: + return start_date if start_date == end_date else f"{start_date} 至 {end_date}" + return display_value + + @staticmethod + def _resolve_application_labeled_value(message: str, labels: tuple[str, ...]) -> str: + label_pattern = "|".join(re.escape(label) for label in labels) + match = re.search( + rf"(?:{label_pattern})[::]\s*(?P[^\n,。;;]+)", + str(message or ""), + ) + return match.group("value").strip() if match else "" + + def _resolve_application_entity_or_label( + self, + payload: UserAgentRequest, + entity_type: str, + labels: tuple[str, ...], + ) -> str: + entity_value = next( + ( + str(item.normalized_value or item.value or "").strip() + for item in payload.ontology.entities + if item.type == entity_type + and str(item.normalized_value or item.value or "").strip() + ), + "", + ) + return entity_value or self._resolve_application_labeled_value(payload.message, labels) + + def _resolve_application_location( + self, + payload: UserAgentRequest, + *, + message: str, + use_entities: bool, + ) -> str: + entity_or_labeled = ( + self._resolve_application_entity_or_label(payload, "location", ("地点", "业务地点", "发生地点")) + if use_entities + else self._resolve_application_labeled_value(message, ("地点", "业务地点", "发生地点")) + ) + return entity_or_labeled or self._resolve_application_location_from_text(message) + + @staticmethod + def _resolve_application_location_from_text(message: str) -> str: + compact = re.sub(r"\s+", "", str(message or "")) + if not compact: + return "" + + for pattern in ( + r"(?:出差|去|到|赴|前往)(?P[\u4e00-\u9fa5]{1,24})", + r"(?P[\u4e00-\u9fa5]{1,12})(?:出差|驻场)", + ): + match = re.search(pattern, compact) + if not match: + continue + target = str(match.group("target") or "").strip() + location = UserAgentApplicationMixin._normalize_application_location_target(target) + if location: + return location + return "" + + @staticmethod + def _normalize_application_location_target(target: str) -> str: + text = str(target or "").strip("::,,。;;") + if not text: + return "" + known = next((item for item in APPLICATION_DESTINATION_PREFIXES if text.startswith(item)), "") + if known: + return known + + verb_indexes = [ + index + for keyword in APPLICATION_REASON_VERBS + for index in [text.find(keyword)] + if index > 0 + ] + if verb_indexes: + return text[: min(verb_indexes)] + return text[:12] + + @staticmethod + def _resolve_application_days(message: str) -> str: + labeled = UserAgentApplicationMixin._resolve_application_labeled_value( + message, + ("天数", "出差天数", "申请天数"), + ) + if labeled: + return labeled if labeled.endswith("天") else f"{labeled}天" + match = re.search(r"(?P\d+|[一二两三四五六七八九十]{1,3})\s*天", str(message or "")) + return f"{match.group('days')}天" if match else "" + + @staticmethod + def _resolve_application_reason(message: str) -> str: + labeled = UserAgentApplicationMixin._resolve_application_labeled_value( + message, + ("事由", "申请事由", "出差事由", "原因", "用途"), + ) + if labeled: + return labeled + + text = str(message or "").strip() + if not text: + return "" + + candidates: list[str] = [] + for segment in re.split(r"[\n,。;;]+", text): + candidate = UserAgentApplicationMixin._cleanup_application_reason_candidate(segment) + if candidate: + candidates.append(candidate) + + if not candidates: + return "" + return max(candidates, key=len) + + @staticmethod + def _cleanup_application_reason_candidate(segment: str) -> str: + text = str(segment or "").strip() + if not text: + return "" + + text = re.sub( + r"^(?:发生时间|业务发生时间|申请时间|时间|地点|业务地点|发生地点|天数|出差天数|申请天数|出行方式|交通方式|交通工具|预计总费用|预计费用|预计金额|申请金额|预算|金额)[::]\s*", + "", + text, + ) + if re.fullmatch(r"20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?", text): + return "" + if re.fullmatch(r"(?P\d+|[一二两三四五六七八九十]{1,3})\s*天", text): + return "" + if re.fullmatch(r"(?:¥|¥)?\s*\d+(?:\.\d+)?\s*(?:元|块|万元)?", text): + return "" + if "时间" in text and re.search(r"20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}", text): + return "" + if re.fullmatch(r"(?:去|到|前往)?[\u4e00-\u9fa5]{1,8}出差(?P\d+|[一二两三四五六七八九十]{1,3})?天?", text): + return "" + + text = re.sub(r"^.*?(?:出差|前往|去|到|赴)[\u4e00-\u9fa5]{1,8}(?:出差)?(?P\d+|[一二两三四五六七八九十]{1,3})?天?[,,\s]*", "", text) + text = re.sub(r"^(?:出差|申请|费用申请|业务|本次|去|到|前往)\s*", "", text) + text = text.strip(" ::,,。;;") + if not text: + return "" + if re.fullmatch(r"[\u4e00-\u9fa5]{1,8}", text) and not any(keyword in text for keyword in APPLICATION_REASON_VERBS): + return "" + return text + + @staticmethod + def _expand_application_time_with_days(time_text: str, days_text: str) -> str: + normalized_time = str(time_text or "").strip() + if not normalized_time or re.search(r"\s*(?:至|到|~|-{2,}|—)\s*", normalized_time): + return normalized_time + + days = UserAgentApplicationMixin._resolve_application_days_count(days_text) + if not days: + return normalized_time + + match = re.search( + r"(?P20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}日?)", + normalized_time, + ) + if not match: + return normalized_time + + parsed_start = UserAgentApplicationMixin._parse_application_date(match.group("date")) + if parsed_start is None: + return normalized_time + + end_date = parsed_start + timedelta(days=days) + return f"{parsed_start:%Y-%m-%d} 至 {end_date:%Y-%m-%d}" + + @staticmethod + def _resolve_application_days_count(days_text: str) -> int: + text = str(days_text or "").strip() + if not text: + return 0 + digit_match = re.search(r"\d+", text) + if digit_match: + return max(0, int(digit_match.group(0))) + + chinese_match = re.search(r"[一二两三四五六七八九十]{1,3}", text) + if not chinese_match: + return 0 + return UserAgentApplicationMixin._parse_chinese_number(chinese_match.group(0)) + + @staticmethod + def _parse_chinese_number(value: str) -> int: + digits = { + "一": 1, + "二": 2, + "两": 2, + "三": 3, + "四": 4, + "五": 5, + "六": 6, + "七": 7, + "八": 8, + "九": 9, + } + text = str(value or "").strip() + if not text: + return 0 + if text == "十": + return 10 + if "十" in text: + left, _, right = text.partition("十") + tens = digits.get(left, 1) if left else 1 + ones = digits.get(right, 0) if right else 0 + return tens * 10 + ones + return digits.get(text, 0) + + @staticmethod + def _parse_application_date(value: str) -> datetime | None: + normalized = str(value or "").strip().rstrip("日").replace("年", "-").replace("月", "-") + normalized = normalized.replace("/", "-").replace(".", "-") + parts = [part for part in normalized.split("-") if part] + if len(parts) != 3: + return None + try: + year, month, day = (int(part) for part in parts) + return datetime(year, month, day) + except ValueError: + return None + + def _resolve_application_amount( + self, + payload: UserAgentRequest, + *, + message: str | None = None, + ) -> str: + entity_amount = next( + ( + str(item.normalized_value or item.value or "").strip() + for item in payload.ontology.entities + if item.type == "amount" + and str(item.normalized_value or item.value or "").strip() + ), + "", + ) + if entity_amount: + return entity_amount if entity_amount.endswith("元") else f"{entity_amount}元" + return self._resolve_application_amount_from_text(message or payload.message) + + @staticmethod + def _resolve_application_amount_from_text(message: str) -> str: + labeled = UserAgentApplicationMixin._resolve_application_labeled_value( + message, + ("预计总费用", "预计费用", "预计金额", "申请金额", "预算", "费用", "金额"), + ) + if labeled: + return UserAgentApplicationMixin._normalize_application_amount(labeled) + match = re.search( + r"(?P\d+(?:\.\d+)?\s*万?\s*(?:元|块|人民币))", + str(message or ""), + ) + return UserAgentApplicationMixin._normalize_application_amount(match.group("amount")) if match else "" + + @staticmethod + def _normalize_application_amount(value: str) -> str: + normalized = str(value or "").strip() + if not normalized: + return "" + normalized = re.sub(r"\s+", "", normalized) + if normalized.endswith(("元", "块")) or "人民币" in normalized: + return normalized.replace("块", "元").replace("人民币", "") + return f"{normalized}元" + + @staticmethod + def _resolve_application_transport_mode(message: str) -> str: + compact_message = re.sub(r"\s+", "", str(message or "")) + for transport, keywords in APPLICATION_TRANSPORT_KEYWORDS.items(): + if any(keyword in compact_message for keyword in keywords): + return transport + labeled = UserAgentApplicationMixin._resolve_application_labeled_value( + message, + ("出行方式", "交通方式", "交通工具", "出行工具"), + ) + if labeled: + for transport, keywords in APPLICATION_TRANSPORT_KEYWORDS.items(): + if transport in labeled or any(keyword in labeled for keyword in keywords): + return transport + return labeled + return "" + + @staticmethod + def _resolve_application_type_from_text(message: str) -> str: + return UserAgentApplicationMixin._resolve_application_labeled_value( + message, + ("申请类型", "费用类型"), + ) + + @staticmethod + def _resolve_application_missing_slots(payload: UserAgentRequest) -> list[str]: + return [ + str(item or "").strip() + for item in payload.ontology.missing_slots + if str(item or "").strip() + ] + + @staticmethod + def _display_application_slot_label(slot: str) -> str: + return { + "expense_type": "申请类型", + "amount": "预计金额/预算", + "time_range": "发生时间", + "time": "发生时间", + "location": "地点", + "reason": "申请事由", + "days": "天数", + "transport_mode": "出行方式", + "attachments": "申请材料/附件", + "customer_name": "业务对象", + "participants": "参与人员", + }.get(str(slot or "").strip(), str(slot or "").strip()) + + def _build_expense_application_actions( + self, + step: str, + facts: dict[str, str], + ) -> list[UserAgentSuggestedAction]: + if step == "ask_missing": + missing_fields = self._resolve_application_missing_fields(facts) + return [ + UserAgentSuggestedAction( + label="一次性补充申请信息", + action_type="prefill_composer", + description="在输入框预填所有待补充字段,填写后一次提交。", + payload={ + "application_fields": missing_fields, + "prompt_prefill": self._build_application_prefill_template(missing_fields), + "missing_fields": missing_fields, + }, + ) + ] + if step == "preview": + return [] + if step == "submitted": + return [] + return [] + + @staticmethod + def _resolve_application_prefill_config(field: str) -> tuple[str, str]: + config = { + "time": ("补充发生时间", "申请时间段:"), + "location": ("补充地点", "地点:"), + "reason": ("补充申请事由", "事由:"), + "days": ("补充天数", "天数:"), + "transport_mode": ("补充出行方式", "出行方式:"), + "amount": ("补充预计总费用", "预计总费用:"), + } + return config.get(field, ("补充申请信息", "")) + + @classmethod + def _build_application_prefill_template(cls, fields: list[str]) -> str: + lines = [ + prefill + for field in fields + for _, prefill in [cls._resolve_application_prefill_config(field)] + if prefill + ] + return "\n".join(lines) + + @classmethod + def _build_application_prefill_action(cls, field: str) -> UserAgentSuggestedAction: + label, prefill = cls._resolve_application_prefill_config(field) + return UserAgentSuggestedAction( + label=label, + action_type="prefill_composer", + description=f"在输入框预填“{prefill}”,用户补充后再提交。", + payload={ + "application_field": field, + "prompt_prefill": prefill, + "missing_fields": [field], + }, + ) + + @staticmethod + def _infer_application_type(facts: dict[str, str]) -> str: + text = " ".join(str(facts.get(key) or "") for key in ("reason", "transport_mode", "days")) + if "采购" in text: + return "采购费用申请" + if "会议" in text or "会务" in text: + return "会务费用申请" + return "差旅费用申请" + + @staticmethod + def _build_application_summary(facts: dict[str, str]) -> str: + return "\n".join( + f"{label}:{value or '待补充'}" + for label, value in ( + ("申请类型", facts.get("application_type", "")), + ("发生时间", facts.get("time", "")), + ("地点", facts.get("location", "")), + ("事由", facts.get("reason", "")), + ("天数", facts.get("days", "")), + ("出行方式", facts.get("transport_mode", "")), + ("预计总费用", facts.get("amount", "")), + ) + ) + + @staticmethod + def _build_application_summary_table( + facts: dict[str, str], + *, + include_empty: bool = True, + ) -> str: + rows = [ + ("申请类型", facts.get("application_type", "")), + ("发生时间", facts.get("time", "")), + ("地点", facts.get("location", "")), + ("事由", facts.get("reason", "")), + ("天数", facts.get("days", "")), + ("出行方式", facts.get("transport_mode", "")), + ("预计总费用", facts.get("amount", "")), + ] + visible_rows = rows if include_empty else [(label, value) for label, value in rows if str(value or "").strip()] + if not visible_rows: + visible_rows = [("申请描述", "已收到,正在按费用申请上下文继续整理")] + lines = ["| 字段 | 内容 |", "| --- | --- |"] + lines.extend(f"| {label} | {value or '待补充'} |" for label, value in visible_rows) + return "\n".join(lines) + + def _create_expense_application_record( + self, + payload: UserAgentRequest, + facts: dict[str, str], + ) -> ExpenseClaim: + claim_no = self._build_application_claim_no(payload, facts) + existing = self.db.scalar( + select(ExpenseClaim) + .where(ExpenseClaim.claim_no == claim_no) + .limit(1) + ) + if existing is not None: + return existing + + current_user = self._build_application_current_user(payload) + access_policy = ExpenseClaimAccessPolicy(self.db) + employee = access_policy.resolve_current_employee(current_user) + department_name = str(current_user.department_name or "").strip() or "待补充" + department_id = None + employee_id = None + employee_name = str(current_user.username or current_user.name or payload.user_id or "anonymous").strip() + + if employee is not None: + employee_id = employee.id + employee_name = str(employee.name or employee.employee_no or employee.email or employee_name).strip() + department_id = employee.organization_unit_id + if employee.organization_unit is not None and employee.organization_unit.name: + department_name = str(employee.organization_unit.name).strip() + + claim = ExpenseClaim( + claim_no=claim_no, + employee_id=employee_id, + employee_name=employee_name, + department_id=department_id, + department_name=department_name, + project_code=None, + expense_type=self._resolve_application_expense_type_code(facts), + reason=str(facts.get("reason") or "费用申请").strip() or "费用申请", + location=str(facts.get("location") or "待补充").strip() or "待补充", + amount=self._parse_application_amount_to_decimal(facts.get("amount", "")), + currency="CNY", + invoice_count=0, + occurred_at=self._parse_application_occurred_at(facts.get("time", "")), + submitted_at=datetime.now(UTC), + status="submitted", + approval_stage="直属领导审批", + risk_flags_json=[], + ) + self.db.add(claim) + self.db.commit() + self.db.refresh(claim) + return claim + + def _resolve_application_manager_name( + self, + payload: UserAgentRequest, + claim: ExpenseClaim | None = None, + ) -> str: + if claim is not None: + manager_name = ExpenseClaimAccessPolicy.resolve_claim_manager_name(claim) + if manager_name and not ExpenseClaimAccessPolicy.is_missing_value(manager_name): + return manager_name + + context_json = payload.context_json or {} + for key in ("manager_name", "managerName", "direct_manager_name", "directManagerName"): + value = str(context_json.get(key) or "").strip() + if value and not ExpenseClaimAccessPolicy.is_missing_value(value): + return value + return "" + + @staticmethod + def _build_application_current_user(payload: UserAgentRequest) -> CurrentUserContext: + context_json = payload.context_json or {} + raw_role_codes = context_json.get("role_codes") + if isinstance(raw_role_codes, list): + role_codes = [str(item).strip() for item in raw_role_codes if str(item).strip()] + else: + role_codes = [item.strip() for item in str(raw_role_codes or "").split(",") if item.strip()] + username = str( + payload.user_id + or context_json.get("username") + or context_json.get("user_id") + or context_json.get("employee_no") + or context_json.get("name") + or "anonymous" + ).strip() + name = str(context_json.get("name") or context_json.get("user_name") or username).strip() + return CurrentUserContext( + username=username or name or "anonymous", + name=name or username or "anonymous", + role_codes=role_codes, + is_admin=bool(context_json.get("is_admin")), + department_name=str( + context_json.get("department_name") + or context_json.get("department") + or context_json.get("departmentName") + or "" + ).strip(), + ) + + @staticmethod + def _resolve_application_expense_type_code(facts: dict[str, str]) -> str: + application_type = str(facts.get("application_type") or "").strip() + if "差旅" in application_type: + return "travel_application" + if "采购" in application_type: + return "purchase_application" + if "会务" in application_type or "会议" in application_type: + return "meeting_application" + return "expense_application" + + @staticmethod + def _parse_application_amount_to_decimal(amount_text: str) -> Decimal: + normalized = str(amount_text or "").replace(",", "").replace(",", "").strip() + match = re.search(r"\d+(?:\.\d+)?", normalized) + if not match: + return Decimal("0.00") + try: + return Decimal(match.group(0)).quantize(Decimal("0.01")) + except (InvalidOperation, ValueError): + return Decimal("0.00") + + @staticmethod + def _parse_application_occurred_at(time_text: str) -> datetime: + normalized = str(time_text or "") + match = re.search(r"(20\d{2})[-/.年](\d{1,2})[-/.月](\d{1,2})", normalized) + if match: + year, month, day = (int(part) for part in match.groups()) + return datetime(year, month, day, tzinfo=UTC) + return datetime.now(UTC) + + def _build_submitted_application_payload( + self, + claim: ExpenseClaim | None, + facts: dict[str, str], + ) -> UserAgentDraftPayload | None: + if claim is None: + return None + return UserAgentDraftPayload( + draft_type="expense_application", + title=str(facts.get("application_type") or "费用申请").strip() or "费用申请", + body=self._build_application_summary(facts), + confirmation_required=False, + claim_id=claim.id, + claim_no=claim.claim_no, + status=claim.status, + approval_stage=claim.approval_stage, + ) + + def _is_application_submit_confirmation(self, payload: UserAgentRequest) -> bool: + compact_message = re.sub(r"\s+", "", str(payload.message or "")) + if any(keyword in compact_message for keyword in APPLICATION_SUBMIT_KEYWORDS): + return True + if compact_message not in APPLICATION_SHORT_CONFIRMATIONS: + return False + history = (payload.context_json or {}).get("conversation_history") + if not isinstance(history, list): + return False + return any( + isinstance(item, dict) + and str(item.get("role") or "").strip() == "assistant" + and ( + "是否确认提交" in str(item.get("content") or "") + or "当前状态:待确认提交" in str(item.get("content") or "") + or "#application-submit" in str(item.get("content") or "") + or "确认无误后" in str(item.get("content") or "") + ) + for item in history[-4:] + ) + + def _build_simulated_application_no( + self, + payload: UserAgentRequest, + facts: dict[str, str], + ) -> str: + return self._build_simulated_application_no_from_facts( + facts, + fallback_seed=str(payload.run_id or ""), + ) + + @staticmethod + def _build_simulated_application_no_from_facts( + facts: dict[str, str], + *, + fallback_seed: str = "", + ) -> str: + raw_date = str(facts.get("time") or "") + match = re.search(r"20\d{2}[-/.年]\d{1,2}[-/.月]\d{1,2}", raw_date) + date_text = match.group(0) if match else datetime.now().strftime("%Y-%m-%d") + digits = re.sub(r"\D", "", date_text)[:8].ljust(8, "0") + seed = re.sub(r"[^A-Za-z0-9]", "", fallback_seed)[-6:] or "SIM001" + return f"APP-{digits}-{seed.upper()}" + + def _build_application_claim_no( + self, + payload: UserAgentRequest, + facts: dict[str, str], + ) -> str: + context_json = payload.context_json or {} + seed_source = "|".join( + str(item or "").strip() + for item in ( + context_json.get("conversation_id"), + payload.user_id, + facts.get("time"), + facts.get("location"), + facts.get("reason"), + facts.get("amount"), + ) + ) + digest = hashlib.sha1(seed_source.encode("utf-8")).hexdigest()[:6] + return self._build_simulated_application_no_from_facts(facts, fallback_seed=digest) diff --git a/server/tests/test_agent_asset_service.py b/server/tests/test_agent_asset_service.py index a5b75fc..de12707 100644 --- a/server/tests/test_agent_asset_service.py +++ b/server/tests/test_agent_asset_service.py @@ -497,7 +497,7 @@ def test_spreadsheet_change_records_include_all_modified_sheets() -> None: def test_editable_spreadsheet_onlyoffice_config_enables_forcesave(monkeypatch) -> None: with build_session() as db: monkeypatch.setattr( - "app.services.agent_assets.resolve_onlyoffice_settings", + "app.services.agent_asset_onlyoffice.resolve_onlyoffice_settings", lambda: OnlyOfficeRuntimeConfig( enabled=True, public_url="http://onlyoffice.example.com", diff --git a/server/tests/test_expense_claim_service.py b/server/tests/test_expense_claim_service.py index 8da0b30..5867e38 100644 --- a/server/tests/test_expense_claim_service.py +++ b/server/tests/test_expense_claim_service.py @@ -2883,6 +2883,133 @@ def test_direct_manager_can_approve_subordinate_claim_to_finance_review() -> Non ) +def test_application_submit_skips_ai_review_and_receipt_requirements(monkeypatch: pytest.MonkeyPatch) -> None: + current_user = CurrentUserContext( + username="application-owner@example.com", + name="张三", + role_codes=["employee"], + is_admin=True, + ) + + with build_session() as db: + claim = ExpenseClaim( + claim_no="APP-20260525-SUBMIT", + employee_name="张三", + department_name="交付部", + project_code="PRJ-A", + expense_type="travel_application", + reason="支撑国网服务器上线部署", + location="上海", + amount=Decimal("12000.00"), + currency="CNY", + invoice_count=0, + occurred_at=datetime(2026, 5, 25, 9, 0, tzinfo=UTC), + submitted_at=None, + status="draft", + approval_stage="待提交", + risk_flags_json=[ + { + "source": "submission_review", + "severity": "medium", + "message": "旧 AI 预审提示不应保留到申请单提交结果。", + } + ], + ) + db.add(claim) + db.commit() + claim_id = claim.id + service = ExpenseClaimService(db) + + def fail_ai_review(_claim: ExpenseClaim) -> dict[str, object]: + raise AssertionError("费用申请提交不应进入 AI 预审") + + monkeypatch.setattr(service, "_run_ai_submission_review", fail_ai_review) + + submitted = service.submit_claim(claim_id, current_user) + + assert submitted is not None + assert submitted.status == "submitted" + assert submitted.approval_stage == "直属领导审批" + assert submitted.invoice_count == 0 + assert submitted.items == [] + assert not any( + isinstance(flag, dict) and flag.get("source") == "submission_review" + for flag in submitted.risk_flags_json + ) + assert any( + isinstance(flag, dict) + and flag.get("source") == "application_submission" + and flag.get("event_type") == "expense_application_submission" + and flag.get("next_approval_stage") == "直属领导审批" + for flag in submitted.risk_flags_json + ) + + +def test_direct_manager_can_approve_application_claim_to_completed_stage() -> None: + current_user = CurrentUserContext( + username="manager-application-approve@example.com", + name="李经理", + role_codes=["manager"], + is_admin=False, + ) + + with build_session() as db: + manager = Employee( + employee_no="E8112", + name="李经理", + email="manager-application-approve@example.com", + ) + employee = Employee( + employee_no="E8113", + name="张三", + email="zhangsan-application-approve@example.com", + manager=manager, + ) + db.add_all([manager, employee]) + db.flush() + claim = ExpenseClaim( + claim_no="APP-20260525-APPROVE", + employee_id=employee.id, + employee_name="张三", + department_name="交付部", + project_code="PRJ-A", + expense_type="travel_application", + reason="支撑国网服务器上线部署", + location="上海", + amount=Decimal("12000.00"), + currency="CNY", + invoice_count=0, + occurred_at=datetime(2026, 5, 25, 9, 0, tzinfo=UTC), + submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC), + status="submitted", + approval_stage="直属领导审批", + risk_flags_json=[], + ) + db.add(claim) + db.commit() + claim_id = claim.id + + approved = ExpenseClaimService(db).approve_claim( + claim_id, + current_user, + opinion="业务必要,同意申请。", + ) + + assert approved is not None + assert approved.status == "approved" + assert approved.approval_stage == "审批完成" + assert any( + isinstance(flag, dict) + and flag.get("source") == "manual_approval" + and flag.get("event_type") == "expense_application_approval" + and flag.get("opinion") == "业务必要,同意申请。" + and flag.get("previous_approval_stage") == "直属领导审批" + and flag.get("next_status") == "approved" + and flag.get("next_approval_stage") == "审批完成" + for flag in approved.risk_flags_json + ) + + def test_finance_can_approve_claim_to_archive_stage() -> None: current_user = CurrentUserContext( username="finance-approve@example.com", diff --git a/server/tests/test_ontology_service.py b/server/tests/test_ontology_service.py index 3efcb7a..1d6a201 100644 --- a/server/tests/test_ontology_service.py +++ b/server/tests/test_ontology_service.py @@ -649,6 +649,40 @@ def test_semantic_ontology_service_requires_attachment_for_meeting_application() assert "attachments" in result.missing_slots +def test_semantic_ontology_service_treats_application_session_as_application_context() -> None: + session_factory = build_session_factory() + with session_factory() as db: + result = SemanticOntologyService(db).parse( + OntologyParseRequest( + query=( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ), + user_id="pytest", + context_json={ + "session_type": "application", + "entry_source": "application", + "attachment_count": 0, + }, + ) + ) + + assert result.scenario == "expense" + assert result.intent == "draft" + assert any( + item.type == "document_type" and item.normalized_value == "expense_application" + for item in result.entities + ) + assert any( + item.type == "workflow_stage" and item.normalized_value == "pre_approval" + for item in result.entities + ) + assert "expense_type" in result.missing_slots + assert "amount" in result.missing_slots + + def test_semantic_ontology_service_uses_model_parse_when_available(monkeypatch) -> None: session_factory = build_session_factory() with session_factory() as db: diff --git a/server/tests/test_orchestrator_review_flow.py b/server/tests/test_orchestrator_review_flow.py index d8bcc4b..f13fed6 100644 --- a/server/tests/test_orchestrator_review_flow.py +++ b/server/tests/test_orchestrator_review_flow.py @@ -11,6 +11,7 @@ from sqlalchemy.pool import StaticPool from app.db.base import Base from app.models.employee import Employee from app.models.financial_record import ExpenseClaim, ExpenseClaimItem +from app.schemas.ontology import OntologyParseResult, OntologyPermission from app.schemas.orchestrator import OrchestratorRequest from app.services.agent_conversations import AgentConversationService from app.services.orchestrator import OrchestratorService @@ -228,6 +229,50 @@ def test_conversation_hydration_does_not_reuse_review_type_for_fresh_expense_pro assert continued_context["review_form_values"]["expense_type"] == "差旅费" +def test_conversation_hydration_preserves_incoming_application_time_context() -> None: + session_factory = build_session_factory() + with session_factory() as db: + service = AgentConversationService(db) + conversation = service.get_or_create_conversation( + conversation_id="conv-application-time-context", + user_id="emp-application-time@example.com", + source="user_message", + context_json={ + "session_type": "application", + "entry_source": "application", + "business_time_context": { + "mode": "single", + "start_date": "2026-05-01", + "end_date": "2026-05-01", + "display_value": "2026-05-01", + }, + }, + ) + + stale_context = service.hydrate_context_json( + conversation=conversation, + context_json={"session_type": "application", "entry_source": "application"}, + message="apply travel expense", + ) + fresh_context = service.hydrate_context_json( + conversation=conversation, + context_json={ + "session_type": "application", + "entry_source": "application", + "business_time_context": { + "mode": "single", + "start_date": "2026-05-25", + "end_date": "2026-05-25", + "display_value": "2026-05-25", + }, + }, + message="apply travel expense", + ) + + assert "business_time_context" not in stale_context + assert fresh_context["business_time_context"]["start_date"] == "2026-05-25" + + def test_conversation_scope_creates_new_session_for_different_claim() -> None: session_factory = build_session_factory() with session_factory() as db: @@ -543,3 +588,225 @@ def test_orchestrator_prompts_scene_choices_before_review_for_fresh_ambiguous_ex assert result.get("draft_payload") is None assert "请先在下面选择报销场景" in result["answer"] assert [item["label"] for item in result["suggested_actions"][:3]] == ["差旅费", "交通费", "住宿费"] + + +def test_orchestrator_application_session_does_not_use_reimbursement_scene_prompt( + monkeypatch, +) -> None: + monkeypatch.setattr( + "app.services.runtime_chat.RuntimeChatService.complete", + lambda *_args, **_kwargs: None, + ) + session_factory = build_session_factory() + message = ( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ) + with session_factory() as db: + response = OrchestratorService(db).run( + OrchestratorRequest( + source="user_message", + user_id="application-session@example.com", + message=message, + context_json={ + "session_type": "application", + "entry_source": "application", + "name": "申请员工", + }, + ) + ) + + result = response.result + assert response.status == "blocked" + assert response.trace_summary.scenario == "expense" + assert "费用申请" in result["answer"] + assert "| 发生时间 | 2026-05-25" in result["answer"] + assert "请先在下面选择报销场景" not in result["answer"] + assert result.get("review_payload") is None + + +def test_orchestrator_application_session_guides_transport_amount_and_submit( + monkeypatch, +) -> None: + monkeypatch.setattr( + "app.services.runtime_chat.RuntimeChatService.complete", + lambda *_args, **_kwargs: None, + ) + session_factory = build_session_factory() + initial_message = ( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ) + context_json = { + "session_type": "application", + "entry_source": "application", + "name": "申请员工", + "manager_name": "陈硕", + } + with session_factory() as db: + service = OrchestratorService(db) + + first = service.run( + OrchestratorRequest( + source="user_message", + user_id="application-flow@example.com", + message=initial_message, + context_json=context_json, + ) + ) + second = service.run( + OrchestratorRequest( + source="user_message", + user_id="application-flow@example.com", + conversation_id=first.conversation_id, + message="飞机", + context_json=context_json, + ) + ) + third = service.run( + OrchestratorRequest( + source="user_message", + user_id="application-flow@example.com", + conversation_id=first.conversation_id, + message="预计总费用:12000元", + context_json=context_json, + ) + ) + fourth = service.run( + OrchestratorRequest( + source="user_message", + user_id="application-flow@example.com", + conversation_id=first.conversation_id, + message="确认提交", + context_json=context_json, + ) + ) + + assert first.status == "blocked" + assert "当前还需要补充:出行方式、预计金额/预算" in first.result["answer"] + assert [item["label"] for item in first.result["suggested_actions"]] == ["一次性补充申请信息"] + assert first.result["suggested_actions"][0]["payload"]["prompt_prefill"] == "出行方式:\n预计总费用:" + + assert "当前还需要补充:预计金额/预算" in second.result["answer"] + assert [item["label"] for item in second.result["suggested_actions"]] == ["一次性补充申请信息"] + assert second.result["suggested_actions"][0]["action_type"] == "prefill_composer" + assert second.result["suggested_actions"][0]["payload"]["prompt_prefill"] == "预计总费用:" + + assert "这是模拟的费用申请结果" in third.result["answer"] + assert "| 事由 | 支持上海国网服务器部署 |" in third.result["answer"] + assert "请核对上述信息无误" in third.result["answer"] + assert "[确认](#application-submit)" in third.result["answer"] + assert third.status == "blocked" + assert third.result["requires_confirmation"] is True + assert third.result["suggested_actions"] == [] + + assert fourth.status == "succeeded" + assert fourth.result["clarification_required"] is False + assert fourth.result["missing_slots"] == [] + assert "当前操作已完成,单据已经推送给 陈硕 进行审核,请耐心等待" in fourth.result["answer"] + assert "当前状态:陈硕审核中" in fourth.result["answer"] + assert fourth.result["suggested_actions"] == [] + application_claims = [ + claim + for claim in db.query(ExpenseClaim).all() + if claim.claim_no.startswith("APP-20260525-") + ] + assert len(application_claims) == 1 + assert application_claims[0].status == "submitted" + assert application_claims[0].approval_stage == "直属领导审批" + assert fourth.result["draft_payload"]["claim_no"] == application_claims[0].claim_no + + +def test_orchestrator_application_submit_bypasses_generic_operation_block( + monkeypatch, +) -> None: + monkeypatch.setattr( + "app.services.runtime_chat.RuntimeChatService.complete", + lambda *_args, **_kwargs: None, + ) + session_factory = build_session_factory() + initial_message = ( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ) + context_json = { + "session_type": "application", + "entry_source": "application", + "name": "申请员工", + "manager_name": "陈硕", + } + with session_factory() as db: + service = OrchestratorService(db) + + first = service.run( + OrchestratorRequest( + source="user_message", + user_id="application-approval-required@example.com", + message=initial_message, + context_json=context_json, + ) + ) + service.run( + OrchestratorRequest( + source="user_message", + user_id="application-approval-required@example.com", + conversation_id=first.conversation_id, + message="飞机", + context_json=context_json, + ) + ) + preview = service.run( + OrchestratorRequest( + source="user_message", + user_id="application-approval-required@example.com", + conversation_id=first.conversation_id, + message="预计总费用:12000元", + context_json=context_json, + ) + ) + + def approval_required_parse_for_run(self, request, run_id): # noqa: ANN001 + return OntologyParseResult( + scenario="expense", + intent="operate", + entities=[], + permission=OntologyPermission( + level="approval_required", + allowed=False, + reason="操作类请求需要人工审批确认。", + ), + confidence=0.95, + missing_slots=[], + ambiguity=[], + clarification_required=False, + clarification_question=None, + run_id=run_id, + ) + + monkeypatch.setattr( + "app.services.ontology.SemanticOntologyService.parse_for_run", + approval_required_parse_for_run, + ) + submitted = service.run( + OrchestratorRequest( + source="user_message", + user_id="application-approval-required@example.com", + conversation_id=first.conversation_id, + message="确认提交", + context_json=context_json, + ) + ) + + assert preview.status == "blocked" + assert submitted.status == "succeeded" + assert submitted.requires_confirmation is False + assert "操作类请求需要人工审批确认" not in submitted.result["answer"] + assert "当前仅返回确认摘要" not in submitted.result["answer"] + assert "当前操作已完成,单据已经推送给 陈硕 进行审核,请耐心等待" in submitted.result["answer"] + assert submitted.result["draft_payload"]["status"] == "submitted" diff --git a/server/tests/test_reimbursement_endpoints.py b/server/tests/test_reimbursement_endpoints.py index 3679630..399e2cf 100644 --- a/server/tests/test_reimbursement_endpoints.py +++ b/server/tests/test_reimbursement_endpoints.py @@ -364,6 +364,70 @@ def test_approve_claim_endpoint_routes_direct_manager_claim_to_finance_review() assert "manager-approve-api@example.com" not in approval_events[0]["message"] +def test_approve_application_endpoint_completes_after_direct_manager_review() -> None: + client, session_factory = build_client() + with session_factory() as db: + manager = Employee( + id="mgr-application-approve-1", + employee_no="E21002", + name="李经理", + email="manager-application-approve-api@example.com", + ) + employee = Employee( + id="emp-application-approve-1", + employee_no="E11002", + name="张三", + email="zhangsan-application-approve-api@example.com", + manager=manager, + ) + claim = ExpenseClaim( + id="claim-application-approve-1", + claim_no="APP-20260525-API001", + employee_id=employee.id, + employee_name="张三", + department_id="dept-1", + department_name="交付部", + project_code=None, + expense_type="travel_application", + reason="支撑国网服务器上线部署", + location="上海", + amount=Decimal("12000.00"), + currency="CNY", + invoice_count=0, + occurred_at=datetime(2026, 5, 25, tzinfo=UTC), + submitted_at=datetime(2026, 5, 25, 10, 0, tzinfo=UTC), + status="submitted", + approval_stage="直属领导审批", + risk_flags_json=[], + ) + db.add_all([manager, employee, claim]) + db.commit() + + response = client.post( + "/api/v1/reimbursements/claims/claim-application-approve-1/approve", + json={"opinion": "业务必要,同意申请。"}, + headers={ + "X-Auth-Username": "manager-application-approve-api@example.com", + "X-Auth-Name": "manager-application-approve-api@example.com", + "X-Auth-Role-Codes": "manager", + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "approved" + assert payload["approval_stage"] == "审批完成" + assert any( + item["source"] == "manual_approval" + and item["event_type"] == "expense_application_approval" + and item["opinion"] == "业务必要,同意申请。" + and item["operator"] == "李经理" + and item["next_status"] == "approved" + and item["next_approval_stage"] == "审批完成" + for item in payload["risk_flags_json"] + ) + + def test_claim_item_pdf_attachment_preview_returns_generated_image(monkeypatch, tmp_path) -> None: preview_bytes = b"fake-preview-png" preview_data_url = f"data:image/png;base64,{base64.b64encode(preview_bytes).decode('ascii')}" diff --git a/server/tests/test_user_agent_service.py b/server/tests/test_user_agent_service.py index ac7220c..9bc047a 100644 --- a/server/tests/test_user_agent_service.py +++ b/server/tests/test_user_agent_service.py @@ -29,6 +29,41 @@ def build_session_factory() -> sessionmaker[Session]: return sessionmaker(bind=engine, autoflush=False, autocommit=False) +def build_application_user_agent_response( + db: Session, + message: str, + *, + history: list[dict[str, object]] | None = None, + context_overrides: dict[str, object] | None = None, +): + context_json = { + "session_type": "application", + "entry_source": "application", + "attachment_count": 0, + } + if context_overrides: + context_json.update(context_overrides) + if history is not None: + context_json["conversation_history"] = history + ontology = SemanticOntologyService(db).parse( + OntologyParseRequest( + query=message, + user_id="pytest", + context_json=context_json, + ) + ) + return UserAgentService(db).respond( + UserAgentRequest( + run_id=ontology.run_id, + user_id="pytest", + message=message, + ontology=ontology, + context_json=context_json, + tool_payload={"clarification_required": ontology.clarification_required}, + ) + ) + + def test_user_agent_query_returns_readable_answer_and_actions() -> None: session_factory = build_session_factory() with session_factory() as db: @@ -137,6 +172,216 @@ def test_user_agent_knowledge_prompt_enforces_knowledge_boundary() -> None: assert '"knowledge_answer_evidence": []' in messages[1]["content"] +def test_user_agent_application_context_uses_application_language() -> None: + session_factory = build_session_factory() + message = ( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ) + context_json = { + "session_type": "application", + "entry_source": "application", + "attachment_count": 0, + } + with session_factory() as db: + ontology = SemanticOntologyService(db).parse( + OntologyParseRequest( + query=message, + user_id="pytest", + context_json=context_json, + ) + ) + response = UserAgentService(db).respond( + UserAgentRequest( + run_id=ontology.run_id, + user_id="pytest", + message=message, + ontology=ontology, + context_json=context_json, + tool_payload={"clarification_required": True}, + ) + ) + + assert "费用申请" in response.answer + assert "| 字段 | 内容 |" in response.answer + assert "| 发生时间 | 2026-05-25 至 2026-05-28 |" in response.answer + assert "支持上海国网服务器部署" in response.answer + assert "当前还需要补充:出行方式、预计金额/预算" in response.answer + assert "请先在下面选择报销场景" not in response.answer + assert response.review_payload is None + assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"] + assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式:\n预计总费用:" + + +def test_user_agent_application_infers_natural_reason_and_expands_single_date() -> None: + session_factory = build_session_factory() + message = "发生时间:2026-05-25\n去上海出差3天,支撑上海国网服务器部署" + with session_factory() as db: + response = build_application_user_agent_response(db, message) + + assert "| 发生时间 | 2026-05-25 至 2026-05-28 |" in response.answer + assert "| 地点 | 上海 |" in response.answer + assert "| 事由 | 支撑上海国网服务器部署 |" in response.answer + assert "当前还需要先补充:申请事由" not in response.answer + assert "当前还需要补充:出行方式、预计金额/预算" in response.answer + assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"] + + +def test_user_agent_application_uses_selected_time_and_natural_language_fields() -> None: + session_factory = build_session_factory() + message = "出差上海,支撑国网服务器上线部署" + context_json = { + "session_type": "application", + "entry_source": "application", + "business_time_context": { + "mode": "single", + "start_date": "2026-05-25", + "end_date": "2026-05-25", + "display_value": "2026-05-25", + }, + } + with session_factory() as db: + ontology = SemanticOntologyService(db).parse( + OntologyParseRequest( + query=message, + user_id="pytest", + context_json=context_json, + ) + ) + response = UserAgentService(db).respond( + UserAgentRequest( + run_id=ontology.run_id, + user_id="pytest", + message=message, + ontology=ontology, + context_json=context_json, + tool_payload={"clarification_required": True}, + ) + ) + + assert "| 发生时间 | 2026-05-25 |" in response.answer + assert "| 地点 | 上海 |" in response.answer + assert "| 事由 | 支撑国网服务器上线部署 |" in response.answer + assert "当前还需要补充:出行方式、预计金额/预算" in response.answer + assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"] + assert response.suggested_actions[0].action_type == "prefill_composer" + assert response.suggested_actions[0].payload["prompt_prefill"] == "出行方式:\n预计总费用:" + + +def test_user_agent_application_asks_amount_after_transport_choice() -> None: + session_factory = build_session_factory() + initial_message = ( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ) + with session_factory() as db: + response = build_application_user_agent_response( + db, + "飞机", + history=[{"role": "user", "content": initial_message}], + ) + + assert "| 出行方式 | 飞机 |" in response.answer + assert "当前还需要补充:预计金额/预算" in response.answer + assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"] + assert response.suggested_actions[0].action_type == "prefill_composer" + assert response.suggested_actions[0].payload["prompt_prefill"] == "预计总费用:" + + +def test_user_agent_application_missing_base_actions_prefill_composer() -> None: + session_factory = build_session_factory() + with session_factory() as db: + response = build_application_user_agent_response( + db, + "地点:上海\n事由:支撑国网服务器部署\n天数:3天", + ) + + assert "当前还需要补充:发生时间、出行方式、预计金额/预算" in response.answer + assert [item.label for item in response.suggested_actions] == ["一次性补充申请信息"] + assert response.suggested_actions[0].action_type == "prefill_composer" + assert response.suggested_actions[0].payload["prompt_prefill"] == "申请时间段:\n出行方式:\n预计总费用:" + + +def test_user_agent_application_builds_preview_when_amount_is_ready() -> None: + session_factory = build_session_factory() + initial_message = ( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ) + with session_factory() as db: + response = build_application_user_agent_response( + db, + "预计总费用:12000元", + history=[ + {"role": "user", "content": initial_message}, + {"role": "user", "content": "飞机"}, + ], + ) + + assert "这是模拟的费用申请结果" in response.answer + assert "| 字段 | 内容 |" in response.answer + assert "| 事由 | 支持上海国网服务器部署 |" in response.answer + assert "| 出行方式 | 飞机 |" in response.answer + assert "| 预计总费用 | 12000元 |" in response.answer + assert "请核对上述信息无误" in response.answer + assert "[确认](#application-submit)" in response.answer + assert response.requires_confirmation is True + assert response.suggested_actions == [] + + +def test_user_agent_application_submit_enters_leader_review() -> None: + session_factory = build_session_factory() + initial_message = ( + "发生时间:2026-05-25\n" + "地点:上海\n" + "事由:支持上海国网服务器部署\n" + "天数:3天" + ) + preview_answer = ( + "这是模拟的费用申请结果,请核对:\n" + "| 字段 | 内容 |\n" + "| --- | --- |\n" + "| 申请类型 | 差旅费用申请 |\n" + "| 发生时间 | 2026-05-25 |\n" + "| 地点 | 上海 |\n" + "| 事由 | 支持上海国网服务器部署 |\n" + "| 天数 | 3天 |\n" + "| 出行方式 | 飞机 |\n" + "| 预计总费用 | 12000元 |\n\n" + "请核对上述信息无误,确认无误后 [确认](#application-submit) 提交至审批流程。" + ) + with session_factory() as db: + response = build_application_user_agent_response( + db, + "确认提交", + context_overrides={"manager_name": "陈硕"}, + history=[ + {"role": "user", "content": initial_message}, + {"role": "user", "content": "飞机"}, + {"role": "user", "content": "预计总费用:12000元"}, + {"role": "assistant", "content": preview_answer}, + ], + ) + + assert "当前操作已完成,单据已经推送给 陈硕 进行审核,请耐心等待" in response.answer + assert "当前状态:陈硕审核中" in response.answer + assert "预算占用参考" in response.answer + assert "APP-20260525-" in response.answer + assert response.suggested_actions == [] + claim = db.query(ExpenseClaim).filter(ExpenseClaim.claim_no.like("APP-20260525-%")).one() + assert claim.status == "submitted" + assert claim.approval_stage == "直属领导审批" + assert claim.expense_type == "travel_application" + assert claim.amount == Decimal("12000.00") + assert claim.employee_name == "pytest" + + def test_user_agent_knowledge_fallback_is_honest_and_personalized() -> None: session_factory = build_session_factory() with session_factory() as db: diff --git a/web/src/assets/styles/app.css b/web/src/assets/styles/app.css index 2af0ff2..85c9b06 100644 --- a/web/src/assets/styles/app.css +++ b/web/src/assets/styles/app.css @@ -15,13 +15,43 @@ } .app { + --sidebar-expanded-width: 220px; + --sidebar-collapsed-width: 64px; + --sidebar-motion: 320ms cubic-bezier(0.22, 1, 0.36, 1); + height: var(--desktop-stage-height, 100dvh); min-height: var(--desktop-stage-height, 100dvh); - display: grid; - grid-template-columns: 220px minmax(0, 1fr); + display: flex; + align-items: stretch; background: var(--bg); } +.app-sidebar { + flex: 0 0 auto; + width: var(--sidebar-expanded-width); + min-width: 0; + overflow: hidden; + will-change: width; + transition: width var(--sidebar-motion); +} + +.app.sidebar-collapsed .app-sidebar { + width: var(--sidebar-collapsed-width); + overflow: visible; + position: relative; + z-index: 200; +} + +.app.sidebar-collapsed > .main { + position: relative; + z-index: 1; +} + +.app > .main { + flex: 1 1 auto; + min-width: 0; +} + .boot-state { min-height: var(--desktop-stage-height, 100dvh); display: grid; @@ -133,9 +163,28 @@ } @media (max-width: 1180px) { - .app { grid-template-columns: 220px minmax(0, 1fr); } + .app-sidebar { + width: var(--sidebar-expanded-width); + } + + .app.sidebar-collapsed .app-sidebar { + width: var(--sidebar-collapsed-width); + } } @media (max-width: 760px) { - .app { display: block; } + .app { + display: block; + } + + .app-sidebar { + width: 100%; + transition: none; + } .workarea { padding: 18px 16px 28px; } } + +@media (prefers-reduced-motion: reduce) { + .app-sidebar { + transition: none; + } +} diff --git a/web/src/assets/styles/views/documents-center-view.css b/web/src/assets/styles/views/documents-center-view.css index dab36cd..6bf2d2f 100644 --- a/web/src/assets/styles/views/documents-center-view.css +++ b/web/src/assets/styles/views/documents-center-view.css @@ -481,6 +481,33 @@ tbody tr:last-child td { font-weight: 800; } +.new-document-badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + min-width: 30px; + height: 17px; + margin-right: 6px; + padding: 0 5px; + border: 1px solid #fecaca; + border-radius: 999px; + background: #fff5f5; + color: #dc2626; + font-size: 10px; + font-weight: 900; + line-height: 1; + letter-spacing: .2px; +} + +.new-document-badge::before { + content: ""; + width: 5px; + height: 5px; + border-radius: 999px; + background: #ef4444; +} + .doc-kind-tag, .type-tag, .status-tag { diff --git a/web/src/assets/styles/views/travel-reimbursement-create-view.css b/web/src/assets/styles/views/travel-reimbursement-create-view.css index 6d452f8..81e0104 100644 --- a/web/src/assets/styles/views/travel-reimbursement-create-view.css +++ b/web/src/assets/styles/views/travel-reimbursement-create-view.css @@ -567,6 +567,17 @@ color: #059669; } +.shortcut-chip.active { + border-color: rgba(5, 150, 105, 0.38); + background: rgba(16, 185, 129, 0.1); + color: #047857; + box-shadow: none; +} + +.shortcut-chip.active i { + color: #047857; +} + .shortcut-chip:disabled { opacity: 0.48; cursor: not-allowed; diff --git a/web/src/components/business/PersonalWorkbench.vue b/web/src/components/business/PersonalWorkbench.vue index 880c36d..14b4fe5 100644 --- a/web/src/components/business/PersonalWorkbench.vue +++ b/web/src/components/business/PersonalWorkbench.vue @@ -14,8 +14,8 @@
-

嗨,{{ assistantGreetingName }},描述费用或上传票据,AI 直接帮你判断怎么报

-

自动识别报销类别、核对附件完整性,并生成可继续提交的报销草稿。

+

嗨,{{ assistantGreetingName }},描述您想做的事,AI 会直接帮您处理

+

我会自动识别您的意图,协助完成费用申请、报销、查询和制度问答等业务工作,耐心把事情推进到可执行的下一步。

-