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

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

View File

@@ -86,7 +86,6 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
*,
citations: list[UserAgentCitation],
) -> str | None:
return None
if payload.ontology.scenario != "knowledge":
return None
if str(payload.tool_payload.get("result_type") or "").strip() != "knowledge_search":
@@ -130,7 +129,10 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
primary_heading = self._format_knowledge_heading_label(
str(primary_item.get("heading") or "").strip()
)
primary_lines = self._collect_direct_knowledge_answer_lines(ordered_evidence_items)
primary_lines = self._collect_direct_knowledge_answer_lines(
ordered_evidence_items,
query_terms=query_terms,
)
lines: list[str] = []
if user_name:
@@ -139,20 +141,42 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
if primary_heading:
source_prefix = f"{source_prefix}{primary_heading}"
conclusion_lines: list[str] = []
evidence_lines: list[str] = []
if str(primary_item.get("kind") or "") == "table":
lines.append(f"{source_prefix},当前能直接确认的是:")
lines.append(self._extract_relevant_table_preview(str(primary_item.get("content") or ""), query_terms))
table_content = str(primary_item.get("content") or "")
if self._question_requests_broad_knowledge_table(question):
table_preview = table_content.strip()
else:
table_preview = self._extract_relevant_table_preview(
table_content,
query_terms,
preferred_terms=self._build_knowledge_table_preferred_terms(payload),
)
table_summary = self._summarize_knowledge_table_preview(table_preview)
conclusion_lines.append(f"{source_prefix}{table_summary}")
evidence_lines.append(table_preview)
else:
if not primary_lines:
lines.append(
summary = self._summarize_knowledge_evidence_content(primary_item, query_terms)
conclusion_lines.append(
f"{source_prefix},当前能直接确认的是:"
f"{self._summarize_knowledge_evidence_content(primary_item, query_terms)}"
f"{summary}"
)
elif len(primary_lines) == 1:
lines.append(f"{source_prefix},当前能直接确认的是:{primary_lines[0].strip()}")
conclusion_lines.append(f"{source_prefix},当前能直接确认的是:{primary_lines[0].strip()}")
evidence_lines.extend(primary_lines)
else:
lines.append(f"{source_prefix},当前能直接确认的是:")
lines.extend(primary_lines)
subject = self._build_knowledge_answer_subject(question, primary_heading)
summary = self._summarize_knowledge_lines_conclusion(
primary_lines,
heading=subject,
)
if summary:
conclusion_lines.append(f"{source_prefix}{summary}")
else:
conclusion_lines.append(f"{source_prefix},当前能直接确认的是:")
evidence_lines.extend(primary_lines)
notes: list[str] = []
location_note = self._build_missing_location_grounding_note(question, evidence_items)
@@ -161,14 +185,64 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
if self._question_requires_explicit_condition(question) and not self._answer_evidence_has_numeric_or_condition(evidence_items):
notes.append("当前命中的证据更偏规则说明或流程约束,还没有直接给出可立即套用的数值或完整条件。")
self._append_markdown_section(lines, "结论", conclusion_lines)
self._append_markdown_section(lines, "依据", evidence_lines)
if notes:
lines.append("")
lines.append("说明:")
lines.extend(f"- {note}" for note in notes)
self._append_markdown_section(lines, "说明", [f"- {note}" for note in notes])
return "\n".join(line for line in lines if line is not None).strip()
@staticmethod
def _append_markdown_section(lines: list[str], title: str, body_lines: list[str]) -> None:
cleaned = [str(line or "").rstrip() for line in body_lines if str(line or "").strip()]
if not cleaned:
return
if lines and lines[-1] != "":
lines.append("")
lines.append(f"## {title}")
lines.append("")
lines.extend(cleaned)
@staticmethod
def _build_knowledge_answer_subject(question: str, heading: str = "") -> str:
clean_heading = str(heading or "").strip()
if clean_heading and not any(
marker in clean_heading
for marker in ("问答线索补充", "结构化表格补充", "重点章节摘录", "章节导航")
):
return clean_heading
normalized = re.sub(r"\s+", "", str(question or "").strip())
normalized = re.sub(r"[?。.!]+$", "", normalized)
normalized = re.sub(r"(是什么|有哪些|是多少|如何|怎么|吗|呢)$", "", normalized)
return normalized.strip(":,。.")
@staticmethod
def _build_knowledge_table_preferred_terms(payload: UserAgentRequest) -> list[str]:
terms: list[str] = []
context = payload.context_json or {}
for key in ("grade", "position", "job_grade", "rank", "level"):
value = str(context.get(key) or "").strip()
if value and value not in terms:
terms.append(value)
grade_match = re.fullmatch(r"[Pp](\d{1,2})", str(context.get("grade") or "").strip())
if grade_match:
grade = int(grade_match.group(1))
for start in range(max(0, grade - 4), grade + 1):
for end in range(grade, min(12, grade + 4) + 1):
if start >= end:
continue
for separator in ("", "~", "-", ""):
term = f"P{start}{separator}P{end}"
if term not in terms:
terms.append(term)
return terms
@staticmethod
def _resolve_knowledge_question(payload: UserAgentRequest) -> str:
return str(payload.context_json.get("user_input_text") or payload.message or "").strip()
@@ -484,6 +558,8 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
def _collect_direct_knowledge_answer_lines(
self,
ordered_evidence_items: list[dict[str, Any]],
*,
query_terms: list[str] | None = None,
) -> list[str]:
if not ordered_evidence_items:
return []
@@ -509,8 +585,18 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
lines: list[str] = []
seen: set[str] = set()
for item in related_items:
rendered = self._render_knowledge_evidence_text(item)
for line in rendered.splitlines():
item_kind = str(item.get("kind") or "").strip()
item_content = str(item.get("content") or "")
if item_kind == "paragraph" or self._has_inline_numbered_knowledge_items(item_content):
rendered = self._focus_knowledge_segment_content(
item_content,
query_terms or [],
)
rendered_lines = self._split_inline_numbered_knowledge_items(rendered)
else:
rendered = self._render_knowledge_evidence_text(item)
rendered_lines = rendered.splitlines()
for line in rendered_lines:
normalized = str(line or "").strip()
if not normalized or normalized in seen:
continue
@@ -573,13 +659,21 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
or "相关制度"
).strip()
user_name = str(payload.context_json.get("name") or "").strip()
prefix = f"{user_name},您好。\n" if user_name else ""
answer_lines: list[str] = []
if user_name:
answer_lines.append(f"{user_name},您好。")
if not hits:
return (
f"{prefix}我已经从《{title}》中检索到与你这次问题相关的制度依据,"
"但本次答案生成环节暂时没有成功返回。请稍后重试一次;如果仍然失败,"
"建议先检查主对话模型的连通性。"
self._append_markdown_section(
answer_lines,
"结论",
[f"当前没有拿到可用于回答这个问题的《{title}》知识库命中。"],
)
self._append_markdown_section(
answer_lines,
"说明",
["- 我不会用相似主题或外部常识硬凑答案;请补充更具体的关键词后再试一次。"],
)
return "\n".join(answer_lines).strip()
evidence_lines: list[str] = []
for item in evidence_items[:3]:
@@ -614,19 +708,28 @@ class UserAgentKnowledgeMixin(UserAgentKnowledgeHelpersMixin):
evidence_lines.append(f"- **《{item_title}》**{excerpt}")
if not evidence_lines:
return (
f"{prefix}当前《{title}》里可用于回答的关键条款还不够明确。"
"请补充费用类型、适用地区、职级或具体业务场景,我再继续帮你缩小范围。"
self._append_markdown_section(
answer_lines,
"结论",
[f"当前《{title}》里可用于回答这个问题的关键条款还不够明确。"],
)
self._append_markdown_section(
answer_lines,
"说明",
["- 请补充费用类型、适用地区、职级或具体业务场景,我再继续帮你缩小范围。"],
)
return "\n".join(answer_lines).strip()
return "\n".join(
[
f"{prefix}我先根据当前制度依据给出可以确认的部分。",
"",
"**依据**",
*evidence_lines,
"",
"**说明**:以上只使用当前命中的知识库证据;没有在证据中出现的适用条件或金额,我不会替你默认补齐。",
]
).strip()
self._append_markdown_section(
answer_lines,
"结论",
["我先根据当前制度依据给出可以确认的部分。"],
)
self._append_markdown_section(answer_lines, "依据", evidence_lines)
self._append_markdown_section(
answer_lines,
"说明",
["- 以上只使用当前命中的知识库证据;没有在证据中出现的适用条件或金额,我不会替你默认补齐。"],
)
return "\n".join(answer_lines).strip()