员工岗位可重新启用;调价认空运航线;会话存档跳号补拉;陆运报价按车型列回填

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-28 19:48:47 +08:00
co-authored by Cursor
parent 2b528758b4
commit 56ba8e93b5
62 changed files with 2960 additions and 793 deletions
+126 -7
View File
@@ -13,7 +13,6 @@ import json
import logging
import re
import time
from pathlib import Path
from typing import Any, Optional
from agent.llm.extract_text import extract_air_collab_fields, extract_product_quote
@@ -71,6 +70,60 @@ def _ticket_flight_date(ticket: object) -> str:
).strip()
def _quote_date_from_lock_instruction(instruction: str) -> str:
"""
锁舱补参句里的报价日期。
先认「报价日期:…」标签,再机读句末 yyyy-MM-dd(产品示例:
AIR-OPT01,南航,航班CZ327,2026-10-18)。不改主账。
"""
from agent.llm.extract_text import labeled_quote_date
from agent.schema.field_validate import _harvest_quote_date
labeled = labeled_quote_date(instruction or "")
if labeled:
return labeled
return _harvest_quote_date(instruction or "")
def _absorb_lock_quote_date(
*,
flow: Any,
ticket: object,
instruction: str,
work_order_no: str,
) -> tuple[object, str, str]:
"""
工单报价日空、锁舱句里又写了日期时,先写入主账再锁。
返回:(最新 ticket, 生效报价日, 本句抽出的日期)。
写失败不挡缺项提示;生效日仍可用本句日期做本地齐套判断。
"""
from agent.schema.field_validate import is_blank_quote_date
current = _ticket_flight_date(ticket)
said = _quote_date_from_lock_instruction(instruction)
if not said:
return ticket, current, ""
if not is_blank_quote_date(current):
return ticket, current, said
writer = getattr(flow.ledger, "update_facts", None)
write_ok = False
if callable(writer):
try:
out = writer(work_order_no=work_order_no, facts={"报价日期": said}) or {}
write_ok = bool(out.get("ok", True))
except Exception:
logger.exception("锁舱句写报价日期失败 wo=%s", work_order_no)
write_ok = False
if write_ok:
getter = getattr(flow.ledger, "get_ticket", None)
reloaded = getter(work_order_no=work_order_no) if callable(getter) else None
ticket2 = hydrate_ticket(flow, reloaded or ticket)
return ticket2, _ticket_flight_date(ticket2) or said, said
return ticket, said, said
def _lock_receipt_fields(
ticket: object,
quote: dict[str, Any],
@@ -451,6 +504,20 @@ def _total_matches_fee_row_sum(quote: dict[str, Any], total_text: str) -> bool:
return abs(total_num - summed) < 0.01
def _total_is_thin_vs_tms(cur_total: str, tms_total: str) -> bool:
"""
薄报价:明细加总明显小于 TMS 销售价(WO040:200 << 7918)才盖成销售价。
航线在舱位价上再加增值费/增点费时,total=明细之和会大于 salePrice(如 4966 > 3966),
此时必须保留航线总价,禁止再盖回 TMS。
"""
cur = _money_plain_number(cur_total)
tms = _money_plain_number(tms_total)
if cur is None or tms is None:
return False
return cur + 0.01 < tms
def _sheet_quote_with_tms_sale_total(
quote: dict[str, Any],
*,
@@ -490,7 +557,10 @@ def _sheet_quote_with_tms_sale_total(
out[key] = proj_val
if not tms_total:
return out
need_tms_total = (not cur_total) or cur_total == "-" or _total_matches_fee_row_sum(out, cur_total)
# 仅「空/薄报价加总」才盖 TMS;航线加出增值费后 total>salePrice 必须保留。
matches_sum = bool(cur_total) and cur_total != "-" and _total_matches_fee_row_sum(out, cur_total)
thin_vs_tms = matches_sum and _total_is_thin_vs_tms(cur_total, tms_total)
need_tms_total = (not cur_total) or cur_total == "-" or thin_vs_tms
if need_tms_total:
out["total"] = str(projected.get("total") or tms_total).strip()
if projected.get("currency") and not str(out.get("currency") or "").strip():
@@ -500,9 +570,13 @@ def _sheet_quote_with_tms_sale_total(
def resolve_air_mentions(flow: Any, ticket: object) -> tuple[str, list[str], list[str]]:
"""
空运群摘要必须 @ 到本单销售和航线·空运,对上海运 @ 销售/@ 产品。
空运群摘要 @ 本单销售 + 负责本单线路的航线·空运(按后台 routes 过滤)。
对上海运 @ 销售/@ 产品;禁止再按岗位整表广播。
"""
from agent.policy.deal_followup import air_staff_route_matches
view = ticket_view(ticket)
facts = dict(view.get("facts") or {})
sales_id = str(view.get("sales_wecom_id") or view.get("salesWecomId") or "").strip()
sales_name = str(view.get("sales_name") or view.get("salesName") or "").strip()
if not sales_name or sales_name == "销售":
@@ -519,7 +593,25 @@ def resolve_air_mentions(flow: Any, ticket: object) -> tuple[str, list[str], lis
rows = list(lister(role_code="air") or [])
except Exception:
logger.exception("列航线岗失败")
matched_rows: list[dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
if air_staff_route_matches(routes=str(row.get("routes") or ""), facts=facts):
matched_rows.append(row)
# 对不上具体线路时,只保留「空运全线」,避免再 @ 全岗
if not matched_rows:
for row in rows:
if not isinstance(row, dict):
continue
toks = [
t.strip()
for t in str(row.get("routes") or "").replace(",", ",").replace("、", ",").split(",")
if t.strip()
]
if any(t in {"空运全线", "全线"} for t in toks):
matched_rows.append(row)
for row in matched_rows:
uid = str(row.get("wecomId") or row.get("wecom_id") or "").strip()
name = str(row.get("name") or "").strip() or _staff_display_name(flow, uid)
if not uid:
@@ -1062,6 +1154,13 @@ def apply_air_lock(
if blocked:
return blocked
no = ticket_no(ticket)
# 补参句带日期时先写入,避免舱位三项齐了仍反复要报价日期。
ticket, quote_date, _said_quote_date = _absorb_lock_quote_date(
flow=flow,
ticket=ticket,
instruction=instruction,
work_order_no=no,
)
name = operator_name(flow, sender_id)
view_quote = dict(ticket_view(ticket).get("quote") or {})
air_quote = dict((view_quote.get("segment_quotes") or {}).get("AIR") or {})
@@ -1079,7 +1178,9 @@ def apply_air_lock(
origin, destination, airline_label, flight_date = _lock_receipt_fields(
ticket, quote, picked, air_code
)
missing = _missing_lock_labels(picked, air_code, flight)
if not quote_date:
quote_date = _ticket_flight_date(ticket)
missing = _missing_lock_labels(picked, air_code, flight, quote_date=quote_date)
if missing and _quote_ready_for_lock(quote):
_remember_partial_lock(
flow,
@@ -1812,8 +1913,20 @@ def _option_row(quote: dict[str, Any], option_no: str) -> Optional[dict[str, Any
return None
def _missing_lock_labels(option_no: str, airline: str, flight_no: str) -> list[str]:
"""缺的中文名,顺序固定:舱位编号、航司、航班号。"""
def _missing_lock_labels(
option_no: str,
airline: str,
flight_no: str,
*,
quote_date: str = "",
) -> list[str]:
"""
缺的中文名,顺序固定:舱位编号、航司、航班号、报价日期。
报价日期空也要进缺项,避免先只补舱位三项、再被主账单独拦一次。
"""
from agent.schema.field_validate import is_blank_quote_date
missing: list[str] = []
if not (option_no or "").strip():
missing.append("舱位编号")
@@ -1821,12 +1934,18 @@ def _missing_lock_labels(option_no: str, airline: str, flight_no: str) -> list[s
missing.append("航司")
if not _FLIGHT_CODE_RE.match((flight_no or "").strip().upper()):
missing.append("航班号")
if is_blank_quote_date(quote_date):
missing.append("报价日期")
return missing
def _labels_from_incomplete_reason(reason: str) -> list[str]:
"""主账回了「缺少:…」时,按中文名收回缺项,丢掉英文字段名。"""
found = [label for label in ("舱位编号", "航司", "航班号") if label in (reason or "")]
found = [
label
for label in ("舱位编号", "航司", "航班号", "报价日期")
if label in (reason or "")
]
return found or ["舱位编号", "航司", "航班号"]