落地空运群内协同与锁舱出站。
群里按 BOT 通道激活当前工单、航线报价和成交跟进;没有 TMS 报价编号时用工单字段补齐仍调锁舱,避免被本地规则拦住。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -155,18 +155,117 @@ def extract_collab_fields(
|
||||
return out
|
||||
|
||||
|
||||
def extract_air_collab_fields(
|
||||
text: str,
|
||||
*,
|
||||
allow_b: bool = False,
|
||||
injected: Optional[dict[str, str]] = None,
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
空运群只认是否含电、是否含磁。
|
||||
|
||||
复用海运口语收割,再滤成这两项。
|
||||
"""
|
||||
keys = ("是否含电", "是否含磁")
|
||||
if injected:
|
||||
return {
|
||||
k: str(v).strip()
|
||||
for k, v in (injected or {}).items()
|
||||
if k in keys and str(v or "").strip()
|
||||
}
|
||||
got = extract_collab_fields(text, allow_b=allow_b)
|
||||
return {k: got[k] for k in keys if str(got.get(k) or "").strip()}
|
||||
|
||||
|
||||
_RE_MONEY = re.compile(
|
||||
r"(?:USD|CNY|RMB|美金|美元|人民币)?\s*(\d+(?:\.\d+)?)\s*(?:USD|CNY|RMB|美金|美元|人民币|元)?"
|
||||
r"|(?:USD|CNY|RMB|美金|美元)\s*(\d+(?:\.\d+)?)",
|
||||
re.I,
|
||||
)
|
||||
_RE_FEE_LINE = re.compile(
|
||||
r"(?P<name>[\u4e00-\u9fffA-Za-z][\u4e00-\u9fffA-Za-z0-9/()()++\--]{0,24}费|[A-Za-z]{2,10})"
|
||||
r"\s*(?:改为|改成|调整为|调整成)?"
|
||||
r"\s*[::=]?"
|
||||
r"\s*(?:(?P<ccy1>USD|CNY|RMB|美金|美元|人民币)\s+)?"
|
||||
r"(?P<amount>\d+(?:\.\d+)?)"
|
||||
r"\s*(?P<ccy2>USD|CNY|RMB|美金|美元|人民币|元)?",
|
||||
re.I,
|
||||
)
|
||||
# 「文件发改为:500」漏了「费」、中间还有冒号,不能丢掉。
|
||||
_RE_FEE_CHANGE = re.compile(
|
||||
r"(?P<name>[\u4e00-\u9fff]{2,16}?)"
|
||||
r"\s*(?:改为|改成|调整为|调整成)"
|
||||
r"\s*[::=]?"
|
||||
r"\s*(?P<amount>\d+(?:\.\d+)?)"
|
||||
r"\s*(?P<ccy>USD|CNY|RMB|美金|美元|人民币|元)?",
|
||||
re.I,
|
||||
)
|
||||
_SKIP_FEE_NAMES = frozenset({"usd", "cny", "rmb", "美金", "美元", "人民币", "元"})
|
||||
_FULLWIDTH_NUM = str.maketrans("0123456789.", "0123456789.")
|
||||
|
||||
|
||||
def _ascii_amount(raw: str) -> str:
|
||||
"""全角数字折成半角,方便和模板金额列对齐。"""
|
||||
return (raw or "").translate(_FULLWIDTH_NUM).strip()
|
||||
|
||||
|
||||
def _currency_of(text: str, hint: str = "") -> str:
|
||||
blob = f"{hint} {text}".upper()
|
||||
if any(x in blob for x in ("CNY", "RMB", "人民币", "元")):
|
||||
return "CNY"
|
||||
if any(x in blob for x in ("USD", "美金", "美元")):
|
||||
return "USD"
|
||||
return hint or "USD"
|
||||
|
||||
|
||||
def _parse_fee_rows(text: str) -> list[dict[str, str]]:
|
||||
"""
|
||||
按行抽费用:码头操作费100CNY、报关费改为300CNY。
|
||||
|
||||
只认带「费」的中文名或短英文费用码,避免把工单号当金额。
|
||||
"""
|
||||
raw = _ascii_amount(text or "")
|
||||
rows: list[dict[str, str]] = []
|
||||
seen: set[str] = set()
|
||||
|
||||
def _add(name: str, amount: str, ccy: str) -> None:
|
||||
if not name or not amount or name.lower() in _SKIP_FEE_NAMES:
|
||||
return
|
||||
# 「WO202609160001」会被 [A-Za-z]{2,10}+数字 抽成费用名 WO,群里发工单号不能当报价。
|
||||
if name.upper() == "WO":
|
||||
return
|
||||
if name in seen:
|
||||
return
|
||||
seen.add(name)
|
||||
rows.append(
|
||||
{
|
||||
"name": name,
|
||||
"amount": amount,
|
||||
"currency": _currency_of(raw, ccy),
|
||||
}
|
||||
)
|
||||
|
||||
for hit in _RE_FEE_LINE.finditer(raw):
|
||||
_add(
|
||||
(hit.group("name") or "").strip(),
|
||||
_ascii_amount(hit.group("amount") or ""),
|
||||
hit.group("ccy1") or hit.group("ccy2") or "",
|
||||
)
|
||||
for hit in _RE_FEE_CHANGE.finditer(raw):
|
||||
_add(
|
||||
(hit.group("name") or "").strip(),
|
||||
_ascii_amount(hit.group("amount") or ""),
|
||||
hit.group("ccy") or "",
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def extract_product_quote(text: str, *, injected: Optional[dict[str, Any]] = None) -> dict[str, Any]:
|
||||
"""
|
||||
产品文字报价:抽出合计/币别。注入优先。
|
||||
产品文字报价:先按费用行抽,再退回合计/币别。注入优先。
|
||||
|
||||
认不出价格返回空 dict,调用方回「没有识别到报价」。
|
||||
群里常见「码头操作费100CNY / 报关费改为300CNY」,不能只把第一笔数当成海运费。
|
||||
"""
|
||||
if isinstance(injected, dict) and (injected.get("total") or injected.get("fee_rows")):
|
||||
out = dict(injected)
|
||||
@@ -175,19 +274,38 @@ def extract_product_quote(text: str, *, injected: Optional[dict[str, Any]] = Non
|
||||
raw = (text or "").strip()
|
||||
if not raw:
|
||||
return {}
|
||||
hit = _RE_MONEY.search(raw)
|
||||
compact = raw.replace(" ", "").replace(" ", "")
|
||||
if re.fullmatch(r"WO\d{12}", compact, re.I):
|
||||
return {}
|
||||
rows = _parse_fee_rows(raw)
|
||||
if rows:
|
||||
ccy = rows[0].get("currency") or _currency_of(raw)
|
||||
total = rows[0]["amount"]
|
||||
if len({x.get("currency") for x in rows}) == 1:
|
||||
try:
|
||||
total = str(sum(float(x["amount"]) for x in rows))
|
||||
except ValueError:
|
||||
total = rows[0]["amount"]
|
||||
return {
|
||||
"total": total,
|
||||
"currency": ccy,
|
||||
"source_label": "产品报价",
|
||||
"raw_text": raw,
|
||||
"fee_rows": rows,
|
||||
"fee_lines": list(rows),
|
||||
}
|
||||
hit = _RE_MONEY.search(_ascii_amount(raw))
|
||||
if not hit:
|
||||
return {}
|
||||
amount = (hit.group(1) or hit.group(2) or "").strip()
|
||||
amount = _ascii_amount(hit.group(1) or hit.group(2) or "")
|
||||
if not amount:
|
||||
return {}
|
||||
ccy = "USD"
|
||||
if any(x in raw.upper() for x in ("CNY", "RMB", "人民币", "元")):
|
||||
ccy = "CNY"
|
||||
ccy = _currency_of(raw)
|
||||
return {
|
||||
"total": amount,
|
||||
"currency": ccy,
|
||||
"source_label": "产品报价",
|
||||
"raw_text": raw,
|
||||
"fee_rows": [{"name": "海运费", "amount": amount, "currency": ccy}],
|
||||
"fee_lines": [{"name": "海运费", "amount": amount, "currency": ccy}],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user