feat: 报销审批流重构与管家计划全链路贯通
- 重构报销状态注册表、审批流路由与平台风险标记 - 完善管家意图规划器与模型计划构建器全链路 - 新增 OCR Worker 脚本、数据库会话管理与通知状态 - 优化文档中心、日志视图、预算中心与员工管理交互 - 增强工作台摘要、图标资源与全局主题样式 - 补充审批路由、状态注册、OCR 服务与管家规划器测试覆盖
This commit is contained in:
@@ -52,6 +52,39 @@ REIMBURSEMENT_PATTERN = re.compile(r"(?:我要报销|还需要报销|需要报
|
||||
MONTH_DAY_PATTERN = re.compile(r"(?P<month>\d{1,2})\s*月\s*(?P<day>\d{1,2})\s*(?:日|号)?")
|
||||
ISO_DATE_PATTERN = re.compile(r"(?P<year>\d{4})[-/年](?P<month>\d{1,2})[-/月](?P<day>\d{1,2})(?:日)?")
|
||||
|
||||
BUSINESS_FIELD_LABELS = {
|
||||
"expense_type": "费用类型",
|
||||
"time_range": "时间",
|
||||
"location": "地点",
|
||||
"reason": "事由",
|
||||
"amount": "金额",
|
||||
"transport_mode": "出行方式",
|
||||
"attachments": "附件/凭证",
|
||||
"customer_name": "客户或项目对象",
|
||||
"merchant_name": "商户/开票方",
|
||||
"department_name": "所属部门",
|
||||
"employee_name": "申请人",
|
||||
"employee_no": "员工编号",
|
||||
}
|
||||
|
||||
EXPENSE_TYPE_LABELS = {
|
||||
"travel": "差旅",
|
||||
"transport": "交通费",
|
||||
"entertainment": "业务招待费",
|
||||
"office": "办公用品",
|
||||
"meeting": "会议费",
|
||||
"training": "培训费",
|
||||
"other": "其他费用",
|
||||
}
|
||||
|
||||
TRANSPORT_MODE_LABELS = {
|
||||
"train": "火车/高铁",
|
||||
"flight": "飞机",
|
||||
"taxi": "出租车/网约车",
|
||||
"subway": "地铁",
|
||||
"other": "其他交通方式",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlannedTaskDraft:
|
||||
@@ -372,6 +405,8 @@ class StewardPlannerService:
|
||||
required = ["expense_type", "time_range", "reason"]
|
||||
if task_type == "expense_application":
|
||||
required.append("location")
|
||||
if fields.get("expense_type") in {"travel", "transport"}:
|
||||
required.append("transport_mode")
|
||||
return [key for key in required if not str(fields.get(key) or "").strip()]
|
||||
|
||||
@staticmethod
|
||||
@@ -543,10 +578,13 @@ class StewardPlannerService:
|
||||
StewardThinkingEvent(
|
||||
event_id="intent_ontology_mapping",
|
||||
stage="ontology_mapping",
|
||||
title="映射业务本体字段",
|
||||
title="核对业务要素",
|
||||
content=ontology_summary,
|
||||
),
|
||||
]
|
||||
gap_event = self._build_business_gap_thinking_event(tasks)
|
||||
if gap_event:
|
||||
events.append(gap_event)
|
||||
if attachments:
|
||||
events.append(
|
||||
StewardThinkingEvent(
|
||||
@@ -580,23 +618,82 @@ class StewardPlannerService:
|
||||
if fields.get("location"):
|
||||
anchors.append(fields["location"])
|
||||
if fields.get("expense_type"):
|
||||
anchors.append(fields["expense_type"])
|
||||
anchors.append(StewardPlannerService._format_business_field_value("expense_type", fields["expense_type"]))
|
||||
anchor_text = "、".join(anchors) if anchors else "待补充关键字段"
|
||||
parts.append(f"{task_label}:{task.title}({anchor_text})")
|
||||
return ";".join(parts)
|
||||
|
||||
@staticmethod
|
||||
def _summarize_ontology_coverage(tasks: list[StewardTask]) -> str:
|
||||
canonical_keys = []
|
||||
missing_keys = []
|
||||
mapped_labels = []
|
||||
missing_labels = []
|
||||
for task in tasks:
|
||||
canonical_keys.extend(task.ontology_fields.keys())
|
||||
missing_keys.extend(task.missing_fields)
|
||||
unique_keys = sorted({item for item in canonical_keys if item})
|
||||
unique_missing = sorted({item for item in missing_keys if item})
|
||||
mapped = "、".join(unique_keys) if unique_keys else "暂无稳定字段"
|
||||
missing = ";缺失字段:" + "、".join(unique_missing) if unique_missing else ""
|
||||
return f"已使用 canonical ontology fields:{mapped}{missing}。兼容字段只作为输入别名,不直接进入业务逻辑。"
|
||||
mapped_labels.extend(StewardPlannerService._business_field_label(key) for key in task.ontology_fields.keys())
|
||||
missing_labels.extend(StewardPlannerService._business_field_label(key) for key in task.missing_fields)
|
||||
mapped = "、".join(dict.fromkeys(label for label in mapped_labels if label)) or "暂无稳定业务要素"
|
||||
missing = ";还缺少:" + "、".join(dict.fromkeys(label for label in missing_labels if label)) if missing_labels else ""
|
||||
return f"已把用户输入归一为业务要素:{mapped}{missing}。后续执行仍会先让用户确认。"
|
||||
|
||||
@staticmethod
|
||||
def _build_business_gap_thinking_event(tasks: list[StewardTask]) -> StewardThinkingEvent | None:
|
||||
gap_lines = []
|
||||
for task in tasks:
|
||||
if not task.missing_fields:
|
||||
continue
|
||||
missing_labels = [
|
||||
StewardPlannerService._business_field_label(key)
|
||||
for key in task.missing_fields
|
||||
if key
|
||||
]
|
||||
if not missing_labels:
|
||||
continue
|
||||
if task.task_type == "expense_application" and "transport_mode" in task.missing_fields:
|
||||
gap_lines.append(
|
||||
(
|
||||
f"{task.title}已识别到{StewardPlannerService._summarize_known_business_points(task)},"
|
||||
"但用户没有说明出行方式;出行方式会影响交通费用测算,进入申请单核对后需要先追问火车、飞机或轮船。"
|
||||
)
|
||||
)
|
||||
else:
|
||||
gap_lines.append(
|
||||
(
|
||||
f"{task.title}还缺少{'、'.join(dict.fromkeys(missing_labels))},"
|
||||
"需要在对应步骤里继续向用户确认,不能直接执行入库或提交。"
|
||||
)
|
||||
)
|
||||
if not gap_lines:
|
||||
return None
|
||||
return StewardThinkingEvent(
|
||||
event_id="intent_business_gap_check",
|
||||
stage="business_gap_check",
|
||||
title="判断待补充信息",
|
||||
content=";".join(gap_lines),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _summarize_known_business_points(task: StewardTask) -> str:
|
||||
parts = []
|
||||
for key in ("time_range", "location", "reason", "expense_type"):
|
||||
value = str(task.ontology_fields.get(key) or "").strip()
|
||||
if value:
|
||||
parts.append(
|
||||
f"{StewardPlannerService._business_field_label(key)}为"
|
||||
f"{StewardPlannerService._format_business_field_value(key, value)}"
|
||||
)
|
||||
return "、".join(parts) or "部分业务要素"
|
||||
|
||||
@staticmethod
|
||||
def _business_field_label(key: str) -> str:
|
||||
return BUSINESS_FIELD_LABELS.get(str(key or "").strip(), str(key or "").strip())
|
||||
|
||||
@staticmethod
|
||||
def _format_business_field_value(key: str, value: str) -> str:
|
||||
cleaned = str(value or "").strip()
|
||||
if key == "expense_type":
|
||||
return EXPENSE_TYPE_LABELS.get(cleaned, cleaned)
|
||||
if key == "transport_mode":
|
||||
return TRANSPORT_MODE_LABELS.get(cleaned, cleaned)
|
||||
return cleaned
|
||||
|
||||
@staticmethod
|
||||
def _summarize_attachment_correlation(
|
||||
|
||||
Reference in New Issue
Block a user