机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
linmianxun
2026-10-19 09:41:59 +08:00
co-authored by Cursor
parent 277cdc1c28
commit 2b528758b4
119 changed files with 6760 additions and 3516 deletions
+87 -2
View File
@@ -279,6 +279,15 @@ def normalize_facts(facts: dict[str, Any] | None) -> dict[str, str]:
val = str(raw_val).strip()
if not val or val in _PLACEHOLDER_DASHES:
continue
# 补问提示「请复制粘贴…」等不是日期,入库前丢掉,避免当成已填走出「线路」。
if key == "报价日期" and is_blank_quote_date(val):
continue
# 「请确认并补充以下信息」等标题不是品名(空栏后没换行会粘上)。
if key in {"品名", "货物品名"}:
from agent.llm.extract_text import is_clarify_ui_prompt
if is_clarify_ui_prompt(val):
continue
out[key] = val
_fold_land_vehicle_qty(out)
return out
@@ -319,6 +328,29 @@ def _harvest_packaging(raw: str) -> str:
return last
def is_blank_quote_date(raw: str | None) -> bool:
"""
报价日期是否等于「没填」。
核对卡空行会写成「报价日期(非必填):」。若把「(非必填):」收成字段值,
会挡住选舱写回航班日期(WO202610160033 运行证据)。横杠、装饰词都当空。
补问提示「请复制粘贴…」曾被跨行收成日期(核对卡挤行),一律当空。
"""
text = str(raw or "").strip()
if not text:
return True
if text in {"-", "—", "-", "/", "无", "没有", "暂无"}:
return True
# 补问 UI 文案、下一栏编号,不是日期。
if "请复制粘贴" in text or "告知我以下字段" in text:
return True
if re.match(r"^\d+[\.、.]", text):
return True
cleaned = re.sub(r"[((]\s*非必填\s*[))]", "", text)
cleaned = cleaned.strip(" \t::,,。;;、")
return not cleaned
def _quote_date_labeled_value(raw: str) -> str:
"""
原话里「报价日期」或同义词后面的值。
@@ -327,20 +359,25 @@ def _quote_date_labeled_value(raw: str) -> str:
能写成 yyyy-MM-dd 就规范;「明天」这类留原文。
没有这些标签则空串,交给句子里的裸日期。
同一句出现多次,用最后一次,避免前面的货好时间把日期抢走。
标签后可带「(非必填)」;值为空或只剩装饰词时不当作已填。
只认冒号后同一行:空行后面的「请复制粘贴…」不能跨行收成日期。
"""
text = raw or ""
labels = ("报价日期",) + QUOTE_DATE_SYNONYMS
best_at = -1
best_val = ""
for label in labels:
# 允许「报价日期(非必填):」;禁止把「(非必填):」收成日期值。
# [^\S\n] 只吃同行空白,避免 \s 跨行吞粘贴提示。
pattern = re.compile(
rf"(?<![\u4e00-\u9fffA-Za-z0-9]){re.escape(label)}\s*[::]?\s*(\S+)"
rf"(?<![\u4e00-\u9fffA-Za-z0-9]){re.escape(label)}"
rf"(?:[^\S\n]*[((]\s*非必填\s*[))])?[^\S\n]*[::]?[^\S\n]*([^\s\n]*)"
)
for hit in pattern.finditer(text):
if hit.start() >= best_at:
best_at = hit.start()
best_val = hit.group(1).strip().strip(",,。;;、")
if not best_val:
if is_blank_quote_date(best_val):
return ""
return _harvest_quote_date(best_val) or _excel_serial_date(best_val) or best_val
@@ -603,6 +640,16 @@ def harvest_spoken_trade_term(text: str) -> dict[str, str]:
_CONTAINER_SPEC = re.compile(
r"(?i)(20|40|45)\s*['\"′尺]?\s*(HQ|GP|NOR|RH|HC|高柜|平柜|柜)"
)
# 陆运车型口语:45HQ(13.5厢车)、9米6/1、13.5厢车、3T。不收 9.11 日期、20件。
_RE_LAND_VEHICLE = re.compile(
r"(?i)(?:"
r"(?:20|40|45)\s*(?:HQ|GP|HC|NOR|RH|高柜|平柜)"
r"(?:\s*[((][^))]{1,24}[))])?"
r"|(?:\d+(?:\.\d+)?)\s*米\s*[0-9六]?(?:\s*[//xX×*]\s*\d+)?"
r"|(?:\d+(?:\.\d+)?)\s*厢车"
r"|(?<![\d.])(\d+(?:\.\d+)?)\s*[Tt](?![A-Za-z])"
r")"
)
def looks_like_container_spec(text: str) -> bool:
@@ -610,6 +657,42 @@ def looks_like_container_spec(text: str) -> bool:
return bool(_CONTAINER_SPEC.search(text or ""))
def promote_land_vehicle_from_container(facts: dict[str, str] | None) -> dict[str, str]:
"""
陆运时把误落入「箱型箱量」的车型改写到「车型/数量」。
DeepSeek B 常把 45HQ/厢车当成海运箱型;陆运必填是车型/数量。
已有车型则不覆盖。返回新 dict,不改入参。
"""
merged = dict(facts or {})
vehicle = (merged.get("车型/数量") or merged.get("车型数量") or "").strip()
container = (merged.get("箱型箱量") or "").strip()
if not vehicle and container:
merged["车型/数量"] = container
return merged
def harvest_oral_land_vehicle(text: str, facts: dict[str, str] | None = None) -> dict[str, str]:
"""
从陆运原话机读车型/数量。
已有「车型/数量」不覆盖。命中 45HQ(13.5厢车)、9米6、厢车、3T 等。
只填空字段,不猜港口。返回新 dict。
"""
merged = dict(facts or {})
if (merged.get("车型/数量") or "").strip():
return merged
raw = _without_clarify_examples(text or "")
hit = _RE_LAND_VEHICLE.search(raw)
if not hit:
return merged
token = hit.group(0).strip()
if not token:
return merged
merged["车型/数量"] = token
return merged
def project_sea_cargo_qty(facts: dict[str, str] | None) -> dict[str, str]:
"""
海运货物数量投影。
@@ -655,6 +738,8 @@ def shape_facts_for_chosen_mode(facts: dict[str, str] | None, business_line: str
merged.pop(key, None)
return merged
if line == "LAND":
# 先把误入箱型箱量的车型改写过来,再删海运外键。
merged = promote_land_vehicle_from_container(merged)
for key in _LAND_DROP_FOREIGN:
merged.pop(key, None)
return merged