- 新增本体字段注册表与字段治理审计脚本 - 重构风险规则模板执行器、DSL 验证与清单分类器 - 完善票据夹服务与差旅请求详情页交互 - 优化趋势图表与总览页数据展示 - 增强报销平台风险分级与模拟公司筛选 - 补充本体字段、风险规则生成与票据夹服务测试覆盖
616 lines
24 KiB
Python
616 lines
24 KiB
Python
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from datetime import UTC, datetime, timedelta
|
||
from decimal import Decimal, InvalidOperation
|
||
from typing import Any
|
||
|
||
from sqlalchemy import or_, select
|
||
from sqlalchemy.orm import selectinload
|
||
|
||
from app.api.deps import CurrentUserContext
|
||
from app.core.agent_enums import AgentAssetStatus, AgentAssetType
|
||
from app.models.employee import Employee
|
||
from app.models.financial_record import ExpenseClaim
|
||
from app.schemas.agent_asset import AgentAssetListItem
|
||
from app.schemas.reimbursement import TravelReimbursementCalculatorRequest
|
||
from app.schemas.user_agent import (
|
||
UserAgentCitation,
|
||
UserAgentDraftPayload,
|
||
UserAgentExpenseQueryRecord,
|
||
UserAgentQueryPayload,
|
||
UserAgentQueryStatusGroup,
|
||
UserAgentReviewAction,
|
||
UserAgentReviewClaimGroup,
|
||
UserAgentReviewDocumentCard,
|
||
UserAgentReviewDocumentField,
|
||
UserAgentReviewEditField,
|
||
UserAgentReviewPayload,
|
||
UserAgentReviewRiskBrief,
|
||
UserAgentReviewSlotCard,
|
||
UserAgentRequest,
|
||
UserAgentSuggestedAction,
|
||
)
|
||
from app.services.agent_assets import AgentAssetService
|
||
from app.services.expense_claims import ExpenseClaimService
|
||
from app.services.expense_rule_runtime import ExpenseRuleRuntimeService, RuntimeTravelPolicy, resolve_document_type_label
|
||
from app.services.ontology_field_registry import normalize_ontology_form_values
|
||
from app.services.risk_ontology_bridge import resolve_rule_codes_for_risk_check
|
||
from app.services.travel_reimbursement_calculator import TravelReimbursementCalculatorService
|
||
from app.services.user_agent_constants import *
|
||
|
||
|
||
class UserAgentReviewTravelReceiptMixin:
|
||
|
||
def _is_travel_review_context(
|
||
self,
|
||
payload: UserAgentRequest,
|
||
document_cards: list[UserAgentReviewDocumentCard],
|
||
claim_groups: list[UserAgentReviewClaimGroup],
|
||
) -> bool:
|
||
entity_expense_type = self._collect_entity_values(payload).get("expense_type_code", "")
|
||
review_form_values = self._resolve_review_form_values(payload)
|
||
form_expense_type = str(review_form_values.get("expense_type") or "").strip()
|
||
message_context = " ".join(
|
||
[
|
||
str(payload.message or ""),
|
||
str(payload.context_json.get("user_input_text") or ""),
|
||
str(payload.context_json.get("expense_type") or ""),
|
||
form_expense_type,
|
||
]
|
||
)
|
||
if entity_expense_type in {"travel", "hotel", "transport"}:
|
||
return True
|
||
if any(group.group_code == "travel" or group.expense_type in {"travel", "hotel", "transport"} for group in claim_groups):
|
||
return True
|
||
if any(card.suggested_expense_type in {"travel", "hotel", "transport"} for card in document_cards):
|
||
return True
|
||
return any(keyword in message_context for keyword in ("差旅", "出差", "机票", "火车", "高铁", "酒店", "住宿"))
|
||
|
||
|
||
def _build_travel_receipt_state(
|
||
self,
|
||
payload: UserAgentRequest,
|
||
*,
|
||
document_cards: list[UserAgentReviewDocumentCard],
|
||
claim_groups: list[UserAgentReviewClaimGroup],
|
||
) -> dict[str, Any]:
|
||
empty_state: dict[str, Any] = {
|
||
"is_travel_context": False,
|
||
"has_long_distance_ticket": False,
|
||
"ticket_type_label": "",
|
||
"ticket_amount": Decimal("0.00"),
|
||
"destination": "",
|
||
"days": 1,
|
||
"has_hotel_invoice": False,
|
||
"has_local_transport": False,
|
||
"required_missing_labels": [],
|
||
"optional_missing_labels": [],
|
||
"blocks_next_step": False,
|
||
}
|
||
if not document_cards or not self._is_travel_review_context(payload, document_cards, claim_groups):
|
||
return empty_state
|
||
|
||
long_distance_cards = [card for card in document_cards if self._is_long_distance_travel_card(card)]
|
||
if not long_distance_cards:
|
||
return {
|
||
**empty_state,
|
||
"is_travel_context": True,
|
||
}
|
||
|
||
has_hotel_invoice = any(self._is_review_hotel_card(card) for card in document_cards)
|
||
required_missing_labels = [] if has_hotel_invoice else ["酒店的报销票据待上传(必须)"]
|
||
ticket_amount = sum(
|
||
(self._extract_amount_decimal_from_card(card) or Decimal("0.00"))
|
||
for card in long_distance_cards
|
||
).quantize(Decimal("0.01"))
|
||
|
||
return {
|
||
**empty_state,
|
||
"is_travel_context": True,
|
||
"has_long_distance_ticket": True,
|
||
"ticket_type_label": self._resolve_travel_ticket_type_label(long_distance_cards),
|
||
"ticket_amount": ticket_amount,
|
||
"destination": self._resolve_travel_receipt_destination(payload, long_distance_cards),
|
||
"days": self._resolve_travel_receipt_days(payload, long_distance_cards),
|
||
"has_hotel_invoice": has_hotel_invoice,
|
||
"has_local_transport": any(self._is_local_transport_receipt_card(card) for card in document_cards),
|
||
"required_missing_labels": required_missing_labels,
|
||
"optional_missing_labels": [],
|
||
"blocks_next_step": bool(required_missing_labels),
|
||
}
|
||
|
||
|
||
@staticmethod
|
||
def _is_long_distance_travel_card(card: UserAgentReviewDocumentCard) -> bool:
|
||
document_type = str(card.document_type or "").strip().lower()
|
||
return document_type in {"train_ticket", "flight_itinerary"}
|
||
|
||
|
||
@staticmethod
|
||
def _is_local_transport_receipt_card(card: UserAgentReviewDocumentCard) -> bool:
|
||
document_type = str(card.document_type or "").strip().lower()
|
||
suggested_type = str(card.suggested_expense_type or "").strip().lower()
|
||
return document_type in {"taxi_receipt", "parking_toll_receipt", "transport_receipt"} or (
|
||
suggested_type == "transport" and document_type not in {"train_ticket", "flight_itinerary"}
|
||
)
|
||
|
||
|
||
@staticmethod
|
||
def _resolve_travel_ticket_type_label(cards: list[UserAgentReviewDocumentCard]) -> str:
|
||
labels: list[str] = []
|
||
for card in cards:
|
||
document_type = str(card.document_type or "").strip().lower()
|
||
if document_type == "train_ticket" and "火车" not in labels:
|
||
labels.append("火车")
|
||
if document_type == "flight_itinerary" and "飞机" not in labels:
|
||
labels.append("飞机")
|
||
return "/".join(labels) if labels else "交通"
|
||
|
||
|
||
def _resolve_travel_receipt_destination(
|
||
self,
|
||
payload: UserAgentRequest,
|
||
long_distance_cards: list[UserAgentReviewDocumentCard],
|
||
) -> str:
|
||
for card in long_distance_cards:
|
||
for field in card.fields:
|
||
if str(field.label or "").strip() not in {"行程", "路线"}:
|
||
continue
|
||
destination = self._extract_travel_destination_from_route(field.value)
|
||
if destination:
|
||
return self._normalize_travel_destination(destination)
|
||
|
||
card_text = self._build_review_document_card_text(card)
|
||
route_match = TRAVEL_ROUTE_PATTERN.search(card_text)
|
||
if route_match:
|
||
return self._normalize_travel_destination(route_match.group(2))
|
||
|
||
location = self._resolve_location_value(payload)
|
||
if location:
|
||
return self._normalize_travel_destination(location)
|
||
return ""
|
||
|
||
|
||
@staticmethod
|
||
def _extract_travel_destination_from_route(value: str) -> str:
|
||
route_text = str(value or "").strip()
|
||
if not route_text:
|
||
return ""
|
||
route_match = TRAVEL_ROUTE_PATTERN.search(route_text)
|
||
if route_match:
|
||
return route_match.group(2).strip()
|
||
parts = [
|
||
item.strip()
|
||
for item in re.split(r"\s*(?:至|到|→|->|-|—|~|~)\s*", route_text)
|
||
if item.strip()
|
||
]
|
||
return parts[-1] if len(parts) >= 2 else ""
|
||
|
||
|
||
def _normalize_travel_destination(self, value: str) -> str:
|
||
candidate = re.sub(
|
||
r"(?:火车站|高铁站|动车站|车站|站|机场|航站楼)$",
|
||
"",
|
||
str(value or "").strip(),
|
||
)
|
||
if not candidate:
|
||
return ""
|
||
try:
|
||
policy = ExpenseRuleRuntimeService(self.db).load_catalog().travel_policy
|
||
except Exception:
|
||
policy = None
|
||
if policy is not None:
|
||
policy_city = self._extract_policy_city_from_text(candidate, policy)
|
||
if policy_city:
|
||
return policy_city
|
||
return candidate
|
||
|
||
|
||
def _resolve_travel_receipt_days(
|
||
self,
|
||
payload: UserAgentRequest,
|
||
long_distance_cards: list[UserAgentReviewDocumentCard],
|
||
) -> int:
|
||
dates: list[datetime] = []
|
||
for card in long_distance_cards:
|
||
card_text = self._build_review_document_card_text(card)
|
||
dates.extend(self._extract_dates_from_text(card_text))
|
||
|
||
if dates:
|
||
return max(1, (max(dates).date() - min(dates).date()).days + 1)
|
||
|
||
start_date = self._parse_date_text(payload.ontology.time_range.start_date or "")
|
||
end_date = self._parse_date_text(payload.ontology.time_range.end_date or "")
|
||
if start_date and end_date:
|
||
return max(1, (end_date.date() - start_date.date()).days + 1)
|
||
return 1
|
||
|
||
|
||
@staticmethod
|
||
def _extract_dates_from_text(text: str) -> list[datetime]:
|
||
dates: list[datetime] = []
|
||
for match in DATE_TEXT_PATTERN.finditer(str(text or "")):
|
||
parsed = UserAgentReviewTravelReceiptMixin._parse_date_text(match.group(1))
|
||
if parsed is not None:
|
||
dates.append(parsed)
|
||
return dates
|
||
|
||
|
||
@staticmethod
|
||
def _parse_date_text(value: str) -> datetime | None:
|
||
raw_value = str(value or "").strip()
|
||
if not raw_value:
|
||
return None
|
||
normalized = (
|
||
raw_value.replace("年", "-")
|
||
.replace("月", "-")
|
||
.replace("/", "-")
|
||
.replace("日", "")
|
||
.strip()
|
||
)
|
||
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 _build_travel_receipt_briefs(
|
||
self,
|
||
travel_receipt_state: dict[str, Any],
|
||
) -> list[UserAgentReviewRiskBrief]:
|
||
if not travel_receipt_state.get("has_long_distance_ticket"):
|
||
return []
|
||
|
||
required_labels = [
|
||
str(item).strip()
|
||
for item in travel_receipt_state.get("required_missing_labels", [])
|
||
if str(item).strip()
|
||
]
|
||
if not required_labels:
|
||
return []
|
||
|
||
required_text = ";".join(required_labels)
|
||
return [
|
||
UserAgentReviewRiskBrief(
|
||
title="差旅票据待补充",
|
||
level="warning",
|
||
content=required_text,
|
||
detail=(
|
||
"系统已识别到长途交通票据,会按差旅报销口径核对住宿、交通等票据完整性。"
|
||
+ f"当前必须补充:{required_text}。"
|
||
),
|
||
suggestion="请先补充酒店住宿发票或住宿清单;在补齐前只能保存为草稿。",
|
||
)
|
||
]
|
||
|
||
|
||
def _resolve_review_travel_allowance_standard(
|
||
self,
|
||
policy: RuntimeTravelPolicy,
|
||
*,
|
||
declared_city: str,
|
||
card_text: str,
|
||
) -> tuple[str, Decimal] | None:
|
||
meal_limits = getattr(policy, "allowance_limits", {}).get("meal", {})
|
||
if not meal_limits:
|
||
return None
|
||
|
||
region_label = self._resolve_review_travel_allowance_region(
|
||
" ".join([declared_city or "", card_text or ""])
|
||
)
|
||
amount = meal_limits.get(region_label)
|
||
if amount is None and region_label != "其他地区":
|
||
amount = meal_limits.get("其他地区")
|
||
region_label = "其他地区"
|
||
if amount is None:
|
||
return None
|
||
return region_label, Decimal(amount).quantize(Decimal("0.01"))
|
||
|
||
|
||
@staticmethod
|
||
def _resolve_review_travel_allowance_region(text: str) -> str:
|
||
normalized = re.sub(r"\s+", "", str(text or ""))
|
||
if not normalized:
|
||
return "其他地区"
|
||
if any(keyword in normalized for keyword in ("境外", "国外", "海外")):
|
||
return "国外"
|
||
if any(keyword in normalized for keyword in ("香港", "澳门", "台湾", "港澳台")):
|
||
return "港澳台"
|
||
if "乌鲁木齐" in normalized:
|
||
return "新疆-乌鲁木齐"
|
||
if "新疆" in normalized:
|
||
return "新疆-其他"
|
||
if any(keyword in normalized for keyword in ("西藏", "拉萨")):
|
||
return "西藏"
|
||
if any(keyword in normalized for keyword in ("北京", "上海", "天津", "重庆", "深圳", "珠海", "汕头", "厦门")):
|
||
return "直辖市/特区"
|
||
return "其他地区"
|
||
|
||
|
||
def _resolve_review_amount_scene_code(
|
||
self,
|
||
card: UserAgentReviewDocumentCard,
|
||
payload: UserAgentRequest,
|
||
) -> str:
|
||
document_type = str(card.document_type or "").strip().lower()
|
||
suggested_type = str(card.suggested_expense_type or "").strip().lower()
|
||
if document_type in {"taxi_receipt", "parking_toll_receipt", "transport_receipt"}:
|
||
return "transport"
|
||
if document_type == "meal_receipt":
|
||
entity_values = self._collect_entity_values(payload)
|
||
if suggested_type == "entertainment" or entity_values.get("expense_type_code") == "entertainment":
|
||
return "entertainment"
|
||
return "meal"
|
||
if document_type == "hotel_invoice" or suggested_type == "hotel":
|
||
return "hotel"
|
||
if suggested_type in {
|
||
"travel",
|
||
"transport",
|
||
"meal",
|
||
"entertainment",
|
||
"office",
|
||
"meeting",
|
||
"training",
|
||
"communication",
|
||
"welfare",
|
||
"other",
|
||
}:
|
||
return suggested_type
|
||
return self._collect_entity_values(payload).get("expense_type_code") or "other"
|
||
|
||
|
||
@staticmethod
|
||
def _resolve_review_scene_amount_limit(scene_policy: Any | None) -> Any | None:
|
||
if scene_policy is None:
|
||
return None
|
||
return getattr(scene_policy, "item_amount_limit", None) or getattr(scene_policy, "claim_amount_limit", None)
|
||
|
||
|
||
@staticmethod
|
||
def _resolve_scene_standard_amount(limit_config: Any | None) -> Decimal | None:
|
||
if limit_config is None:
|
||
return None
|
||
warn_amount = getattr(limit_config, "warn_amount", None)
|
||
block_amount = getattr(limit_config, "block_amount", None)
|
||
amount = warn_amount if warn_amount is not None else block_amount
|
||
if amount is None:
|
||
return None
|
||
try:
|
||
return Decimal(amount).quantize(Decimal("0.01"))
|
||
except (InvalidOperation, ValueError):
|
||
return None
|
||
|
||
|
||
@staticmethod
|
||
def _evaluate_review_scene_amount(
|
||
*,
|
||
amount: Decimal,
|
||
limit_config: Any,
|
||
reason_text: str,
|
||
) -> tuple[str, Decimal] | None:
|
||
block_amount = getattr(limit_config, "block_amount", None)
|
||
warn_amount = getattr(limit_config, "warn_amount", None)
|
||
exception_keywords = list(getattr(limit_config, "exception_keywords", []) or [])
|
||
has_exception = UserAgentReviewTravelReceiptMixin._text_contains_any(reason_text, exception_keywords)
|
||
|
||
if block_amount is not None and amount > Decimal(block_amount):
|
||
return ("high", Decimal(block_amount).quantize(Decimal("0.01")))
|
||
if warn_amount is not None and amount > Decimal(warn_amount):
|
||
return ("high", Decimal(warn_amount).quantize(Decimal("0.01")))
|
||
return None
|
||
|
||
|
||
def _resolve_review_employee_grade(self, payload: UserAgentRequest, *, employee: Employee | None) -> str:
|
||
if employee is not None and employee.grade:
|
||
return str(employee.grade).strip()
|
||
review_form_values = self._resolve_review_form_values(payload)
|
||
for source in (
|
||
review_form_values,
|
||
payload.context_json,
|
||
payload.tool_payload,
|
||
):
|
||
for key in ("employee_grade", "grade", "user_grade", "position_grade"):
|
||
value = str(source.get(key) or "").strip() if isinstance(source, dict) else ""
|
||
if value:
|
||
return value
|
||
return ""
|
||
|
||
|
||
def _build_review_reason_corpus(self, payload: UserAgentRequest) -> str:
|
||
review_form_values = normalize_ontology_form_values(self._resolve_review_form_values(payload))
|
||
parts = [
|
||
str(payload.message or ""),
|
||
str(payload.context_json.get("user_input_text") or ""),
|
||
str(review_form_values.get("reason") or ""),
|
||
str(review_form_values.get("location") or ""),
|
||
]
|
||
return "\n".join(part.strip() for part in parts if part and part.strip())
|
||
|
||
|
||
def _resolve_declared_travel_city(self, payload: UserAgentRequest, policy: RuntimeTravelPolicy) -> str:
|
||
review_form_values = normalize_ontology_form_values(self._resolve_review_form_values(payload))
|
||
candidates = [
|
||
str(review_form_values.get("location") or ""),
|
||
self._resolve_location_value(payload),
|
||
str(payload.message or ""),
|
||
]
|
||
for candidate in candidates:
|
||
city = self._extract_policy_city_from_text(candidate, policy)
|
||
if city:
|
||
return city
|
||
return ""
|
||
|
||
|
||
@staticmethod
|
||
def _build_review_document_card_text(card: UserAgentReviewDocumentCard) -> str:
|
||
field_text = " ".join(f"{field.label}:{field.value}" for field in card.fields)
|
||
return " ".join(
|
||
[
|
||
str(card.filename or ""),
|
||
str(card.document_type or ""),
|
||
str(card.scene_label or ""),
|
||
str(card.summary or ""),
|
||
field_text,
|
||
]
|
||
).strip()
|
||
|
||
|
||
@staticmethod
|
||
def _is_review_hotel_card(card: UserAgentReviewDocumentCard) -> bool:
|
||
document_type = str(card.document_type or "").strip().lower()
|
||
suggested_type = str(card.suggested_expense_type or "").strip().lower()
|
||
scene_label = str(card.scene_label or "").strip()
|
||
return document_type == "hotel_invoice" or suggested_type == "hotel" or "住宿" in scene_label
|
||
|
||
|
||
@staticmethod
|
||
def _extract_amount_decimal_from_card(card: UserAgentReviewDocumentCard) -> Decimal | None:
|
||
for field in card.fields:
|
||
if field.label != "金额":
|
||
continue
|
||
normalized = str(field.value or "").replace("元", "").replace("¥", "").replace("¥", "").replace(",", "").strip()
|
||
try:
|
||
amount = Decimal(normalized).quantize(Decimal("0.01"))
|
||
except (InvalidOperation, ValueError):
|
||
continue
|
||
if amount > Decimal("0.00"):
|
||
return amount
|
||
return None
|
||
|
||
|
||
@staticmethod
|
||
def _extract_review_hotel_night_count(card: UserAgentReviewDocumentCard) -> int:
|
||
text = f"{card.summary or ''} {' '.join(f'{field.label}:{field.value}' for field in card.fields)}"
|
||
match = TRAVEL_REVIEW_HOTEL_NIGHT_PATTERN.search(text)
|
||
if not match:
|
||
return 1
|
||
try:
|
||
return max(1, int(match.group(1)))
|
||
except (TypeError, ValueError):
|
||
return 1
|
||
|
||
|
||
@staticmethod
|
||
def _extract_policy_city_from_text(text: str, policy: RuntimeTravelPolicy) -> str:
|
||
normalized = str(text or "").strip()
|
||
if not normalized:
|
||
return ""
|
||
city_names = set(policy.city_tiers.keys())
|
||
city_names.update(getattr(policy, "hotel_city_limits", {}).keys())
|
||
for city in sorted(city_names, key=lambda item: len(item), reverse=True):
|
||
if city in normalized:
|
||
return city
|
||
return ""
|
||
|
||
|
||
@staticmethod
|
||
def _format_travel_city_tier(city_tier: str) -> str:
|
||
return {
|
||
"tier_1": "一线城市",
|
||
"tier_2": "重点城市",
|
||
"tier_3": "其他城市",
|
||
}.get(str(city_tier or "").strip(), "当前城市")
|
||
|
||
|
||
@staticmethod
|
||
def _resolve_review_hotel_cap(
|
||
policy: RuntimeTravelPolicy,
|
||
*,
|
||
grade_band: str,
|
||
city: str,
|
||
city_tier: str,
|
||
) -> Decimal:
|
||
normalized_city = str(city or "").strip()
|
||
if normalized_city and getattr(policy, "hotel_city_limits", None):
|
||
city_limits = policy.hotel_city_limits.get(normalized_city, {})
|
||
city_cap = city_limits.get(grade_band)
|
||
if city_cap is not None:
|
||
return Decimal(city_cap).quantize(Decimal("0.01"))
|
||
return Decimal(policy.hotel_limits.get(grade_band, {}).get(city_tier, Decimal("0.00"))).quantize(
|
||
Decimal("0.01")
|
||
)
|
||
|
||
|
||
def _detect_review_transport_class(
|
||
self,
|
||
card: UserAgentReviewDocumentCard,
|
||
policy: RuntimeTravelPolicy,
|
||
) -> tuple[str, str, int] | None:
|
||
document_type = str(card.document_type or "").strip().lower()
|
||
text = re.sub(r"\s+", "", self._build_review_document_card_text(card))
|
||
if not text:
|
||
return None
|
||
|
||
if document_type == "flight_itinerary" or any(keyword in text for keyword in ("机票", "航班", "登机牌")):
|
||
for config in policy.flight_classes:
|
||
label = str(config.keyword or "").strip()
|
||
if label and label in text:
|
||
return "flight", label, int(config.level)
|
||
|
||
if document_type == "train_ticket" or any(keyword in text for keyword in ("火车", "高铁", "动车", "铁路")):
|
||
for config in policy.train_classes:
|
||
label = str(config.keyword or "").strip()
|
||
if label and label in text:
|
||
return "train", label, int(config.level)
|
||
return None
|
||
|
||
|
||
@staticmethod
|
||
def _text_contains_any(text: str, keywords: list[str] | tuple[str, ...]) -> bool:
|
||
compact = re.sub(r"\s+", "", str(text or ""))
|
||
return bool(compact) and any(str(keyword or "").strip() and str(keyword).strip() in compact for keyword in keywords)
|
||
|
||
|
||
@staticmethod
|
||
def _resolve_submission_blocked_reasons(payload: UserAgentRequest) -> list[str]:
|
||
raw_reasons = payload.tool_payload.get("submission_blocked_reasons")
|
||
submission_blocked = bool(payload.tool_payload.get("submission_blocked"))
|
||
if raw_reasons is None and submission_blocked:
|
||
raw_reasons = payload.tool_payload.get("missing_fields")
|
||
if raw_reasons is None and not submission_blocked:
|
||
return []
|
||
|
||
reasons: list[str] = []
|
||
if isinstance(raw_reasons, list):
|
||
reasons.extend(str(item or "").strip() for item in raw_reasons)
|
||
elif isinstance(raw_reasons, str):
|
||
reasons.extend(
|
||
item.strip()
|
||
for item in re.split(r"[;;\n]+", raw_reasons)
|
||
if item.strip()
|
||
)
|
||
|
||
if not reasons and submission_blocked:
|
||
message = str(payload.tool_payload.get("message") or "").strip()
|
||
for prefix in (
|
||
"提交前请先补全信息:",
|
||
"自动检测暂未通过,原因如下:",
|
||
"自动检测未通过,原因如下:",
|
||
"自动检测暂未通过:",
|
||
"自动检测未通过:",
|
||
"AI预审暂未通过,原因如下:",
|
||
"AI预审未通过,原因如下:",
|
||
"AI预审暂未通过:",
|
||
"AI预审未通过:",
|
||
):
|
||
if message.startswith(prefix):
|
||
message = message[len(prefix):].strip()
|
||
break
|
||
if message:
|
||
reasons.extend(
|
||
item.strip()
|
||
for item in re.split(r"[;;\n]+", message)
|
||
if item.strip()
|
||
and not item.strip().startswith("AI预审暂未通过")
|
||
and not item.strip().startswith("自动检测暂未通过")
|
||
)
|
||
|
||
return list(dict.fromkeys(reason for reason in reasons if reason))
|
||
|