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

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
+87 -1
View File
@@ -186,6 +186,20 @@ _TIGHTEN_LABELS = tuple(
)
)
_FEE_NAME = re.compile(r"^\*?[\u4e00-\u9fffA-Za-z0-9]{1,12}费$")
# 识图/标签错位:报价编号、工单号、长数字进了起运地/目的地。三字码航线不动。
_QUOTE_LIKE_PLACE = re.compile(
r"^(?:"
r"(?:WO|VT)\d{10,}"
r"|QT[-_]?\d{6,}"
r"|\d{8,}"
r"|[A-Za-z]{1,6}\d{8,}"
r")$",
re.I,
)
_QUOTE_NO_IN_TEXT = re.compile(
r"(?:报价编号|报价单编号|RFQ\s*No\.?|Quote\s*No\.?)\s*[::]?\s*([A-Za-z0-9\-_]{6,})",
re.I,
)
_SHEET_STOP = (
"报价明细",
"报价条件",
@@ -455,6 +469,64 @@ def _drop_fee_place_values(facts: dict[str, str]) -> dict[str, str]:
return out
def looks_like_quote_id_place(raw: str | None) -> bool:
"""
是否像报价编号/工单号,而不是起运地或目的地。
识图常把「报价编号:QT…」错贴到起运地。SZX、南京、SZX-CGK 不算。
"""
text = str(raw or "").strip()
if not text:
return False
if "报价编号" in text or "报价单编号" in text:
return True
# 去掉常见装饰后再判
cleaned = re.sub(r"[((]\s*(?:必填|非必填)\s*[))]", "", text).strip(" \t::,,。;;")
if not cleaned:
return False
if _QUOTE_LIKE_PLACE.match(cleaned):
return True
# 纯数字超过 7 位多半是系统编号,不是港口名
if re.fullmatch(r"\d{8,}", cleaned.replace(" ", "")):
return True
return False
def _quote_nos_mentioned(text: str) -> set[str]:
"""原话/识图正文里出现的报价编号值。"""
return {
m.group(1).strip()
for m in _QUOTE_NO_IN_TEXT.finditer(text or "")
if (m.group(1) or "").strip()
}
def _drop_quote_like_place_values(
facts: dict[str, str],
*,
source_text: str = "",
) -> dict[str, str]:
"""
起运地/目的地若是报价编号、工单号或原话「报价编号:」同一值,丢掉。
副作用:无写库;只清洗本轮 facts。
"""
out = dict(facts or {})
mentioned = _quote_nos_mentioned(source_text)
for key in ("起运港", "起运地", "目的港", "目的地"):
val = str(out.get(key) or "").strip()
if not val:
continue
if not (looks_like_quote_id_place(val) or val in mentioned):
continue
out.pop(key, None)
if key == "起运港":
out.pop("originCode", None)
if key == "目的港":
out.pop("destinationCode", None)
return out
def extract_inquiry_snapshot(
text: str,
*,
@@ -483,6 +555,7 @@ def extract_inquiry_snapshot(
if str(facts.get(key) or "").strip():
facts[key] = clip_inquiry_value(str(facts[key]))
facts = _drop_fee_place_values(_clip_sheet_inquiry_facts(facts))
facts = _drop_quote_like_place_values(facts, source_text=text)
facts = _apply_air_added_services(text, facts, mode)
subtype = str(facts.get("运输类型") or "").strip()
return {
@@ -539,6 +612,7 @@ def extract_inquiry_snapshot(
if str(facts.get(key) or "").strip():
facts[key] = clip_inquiry_value(str(facts[key]))
facts = _drop_fee_place_values(_clip_sheet_inquiry_facts(facts))
facts = _drop_quote_like_place_values(facts, source_text=text)
facts = _apply_air_added_services(text, facts, mode)
land_subtype = str((b_payload or {}).get("land_subtype") or facts.get("运输类型") or "").strip()
if land_subtype and not str(facts.get("运输类型") or "").strip():
@@ -1013,8 +1087,18 @@ _SKIP_FEE_NAMES = frozenset(
# 航线常写「空运费 单价CNY99,数量100,金额CNY199」。单价和金额不是同一个数,必须认金额。
# WO202610130003:协同句常见「空运费, KG, 单价CNY99.00, 数量100, 金额CNY9900」——费名与单价之间夹单位;
# WO202610140008:航线写「增值费_板,单价CNY10…」——下划线单位;漏认会落到金额误抽,出单默认单位「票」。
# WO202610190081:打板费,次,单价…——「次/人」等中文单位不在旧名单里时整句匹配失败,数量落成 1、金额=单价。
# 数量/金额前也可能只有空格无逗号。漏匹配会落到 _RE_FEE_LINE,把单价 99 当成行金额、数量被补成 1。
_FEE_UOM = r"KG|KGS|公斤|千克|TON|CBM|柜|票|板数|板|点|[A-Za-z]{1,8}"
# 业务单位表(长词/带斜杠优先,避免「次/人」被拆成「次」):
# 板数/点/人/天/货值/个/页数/柜/次/份/张/台/平方/件/计重/箱数/数量/千克/立方/卷/
# 单数/票数/次/人/车/小时/人/小时/公里数/月/趟/周/批/项 + 英文重量体积码。
# 末尾短中文通配:表外单位夹在费名与单价之间也不挡抽取。
_FEE_UOM = (
r"次/人|小时/人|板数|页数|箱数|单数|票数|公里数|货值|计重|平方|立方|千克|小时|"
r"数量|KG|KGS|公斤|TON|CBM|柜|票|板|点|次|人|天|个|份|张|台|件|卷|车|月|趟|周|批|项|"
r"托|套|袋|包|箱|"
r"[A-Za-z]{1,8}|[\u4e00-\u9fff]{1,3}"
)
_RE_FEE_PRICED = re.compile(
r"(?P<name>[\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9/()()++\--]{0,24}费)"
rf"(?:(?:\s*[,,、/]\s*|\s+|_)(?P<uom>{_FEE_UOM}))?"
@@ -1251,7 +1335,9 @@ def _parse_fee_rows(text: str, baseline_currency: str = "") -> list[dict[str, st
row[key] = val
rows.append(row)
priced_hits = 0
for hit in _RE_FEE_PRICED.finditer(raw):
priced_hits += 1
# 有「金额」用金额;只有单价时才用单价。禁止把单价误当成这一行的总金额。
amount = (hit.group("amount") or "").strip() or (hit.group("unit") or "").strip()
ccy = hit.group("amt_ccy") or hit.group("unit_ccy") or ""
@@ -117,6 +117,11 @@ def build_adjust_messages(text: str, fee_names: list[str]) -> list[dict[str, str
"只动费用合计(销售也可能说总销售价、总成本价、合计、总价、汇总价):wants_total=true,items 仍要一条(fee_name 可空),带 kind/direction/value。"
"kind 只能 percent 或 target;direction 只能 up/down/set。"
"value 必须来自原话数字;一成=10。禁止编造费用名和金额。"
"语义:上调/加/涨 X% → kind=percent direction=up;"
"下调/减/降 X% → kind=percent direction=down;"
"增加到/提升到/调整到/涨到/升到/增至/变为/上浮到/调到/改为 X% "
"→ kind=percent direction=set(改到原价的 X%,不是再上调 X%);"
"提升到/改为/变为 具体金额(无百分号)→ kind=target direction=set。"
"不是调价(问候、新询价、谢谢)时 is_adjust=false。"
f"当前可调费用名:{names}"
),
@@ -125,7 +125,8 @@ def build_extract_messages(text: str) -> list[dict[str, str]]:
"你是询价字段抽取器。只根据销售原话抽取字段,调用 extract_inquiry_fields。"
"未出现的字段必须空串,禁止编造港口、重量、日期。"
"空运件数/毛重/体积/包装方式;海运货量与箱型箱量;陆运按二级类型。"
"陆运整车/中港零担的车型写入车型/数量:45HQ(13.5厢车)、9米6/1、3T;"
"国内整车/跨境整车的车型写入车型/数量:45HQ(13.5厢车)、9米6/1、3T;"
"中港零担/集拼不要抽车型/数量;中港整车的车型写入车型/数量;"
"陆运不要把 45HQ/厢车 写成箱型箱量。"
"海运时 15件/20吨/3箱 必须写入货量,不要只写到件数把货量留空。"
"原话已带单位必须抽取,不要因为没写「毛重:」就留空:"
@@ -144,6 +145,7 @@ def build_extract_messages(text: str) -> list[dict[str, str]]:
"陆运「A到B」必须写入起运港和目的港。"
"原话是机场三字码或航线就原样记,禁止翻译成城市名:"
"SZX-CGK / SZX/CGK / SZX到CGK → 起运港=SZX、目的港=CGK,不要写成深圳、雅加达。"
"报价编号、报价单编号、工单号(WO/VT…)、长数字编号不是起运港或目的港,必须空串。"
"换城市必须覆盖旧值,不要沿用上一句的港口。"
"运输方式仅在原话能判断时填 AIR/SEA/LAND,否则 UNKNOWN。"
),