机器人显示名与站点标题可配;多段联运列表投影;协同群监听对账;工单附件落存;模板与工单后台补强
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -137,6 +137,7 @@ _SHEET_CLEAN_KEYS = frozenset(
|
||||
"整柜或拼柜",
|
||||
"箱型箱量",
|
||||
"贸易条款",
|
||||
"增值服务",
|
||||
"货好时间",
|
||||
"运输分类",
|
||||
"报价日期",
|
||||
@@ -280,7 +281,8 @@ def parse_clarify_paste_facts(text: str) -> dict[str, str]:
|
||||
continue
|
||||
if not val or _CLARIFY_EXAMPLE_LINE.match(val):
|
||||
continue
|
||||
facts[name] = clip_inquiry_value(val)
|
||||
clipped = clip_inquiry_value(val)
|
||||
facts[name] = clipped
|
||||
return {key: val for key, val in facts.items() if val}
|
||||
|
||||
|
||||
@@ -522,6 +524,14 @@ def extract_inquiry_snapshot(
|
||||
# 模型常把「郑州-吉隆坡」猜成空运,不能拿来默认。
|
||||
source = "deepseek_b"
|
||||
facts = harvest_oral_measures(text, normalize_facts(facts))
|
||||
if (mode or "").upper() == "LAND":
|
||||
from agent.schema.field_validate import (
|
||||
harvest_oral_land_vehicle,
|
||||
promote_land_vehicle_from_container,
|
||||
)
|
||||
|
||||
facts = promote_land_vehicle_from_container(facts)
|
||||
facts = harvest_oral_land_vehicle(text, facts)
|
||||
facts = _merge_spoken_collab(text, facts)
|
||||
facts = _merge_optional_inquiry_labels(text, facts)
|
||||
facts = apply_cargo_handover_synonym(text, facts, business_line=mode)
|
||||
@@ -570,12 +580,35 @@ def harvest_inquiry_optional_labels(text: str) -> dict[str, str]:
|
||||
return _harvest_labels(text, _OPTIONAL_INQUIRY_LABELS)
|
||||
|
||||
|
||||
def is_clarify_ui_prompt(raw: str | None) -> bool:
|
||||
"""
|
||||
补问/核对卡上的系统提示,不是任何询价字段的值。
|
||||
|
||||
空「货物品名(必填):」后若没换行,下一句「请确认并补充以下信息」会粘成品名
|
||||
(多段陆运核对卡运行证据)。报价日期误收「请复制粘贴…」同属此类。
|
||||
"""
|
||||
text = str(raw or "").strip().strip("::")
|
||||
if not text:
|
||||
return False
|
||||
if "请复制粘贴" in text or "告知我以下字段" in text:
|
||||
return True
|
||||
if "请确认并补充以下信息" in text:
|
||||
return True
|
||||
if "请复制以下内容" in text:
|
||||
return True
|
||||
if re.match(r"^Step\s*[12]\s*:", text, flags=re.I):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def clip_inquiry_value(raw: str) -> str:
|
||||
"""
|
||||
字段值收到下一个编号项为止。
|
||||
|
||||
图上常在货好时间后面接着「4. 希望送到…」「6. 送货地址」。
|
||||
那些不是询价字段,不能进补问。地址里的「602、603」没有点号后空格,会留下。
|
||||
补问复制清单「10.增值服务」点后可无空格;空贸易条款不能把下一栏标签收成值。
|
||||
空报价日期不能把下一行「请复制粘贴…」收成日期值。
|
||||
"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
@@ -583,6 +616,20 @@ def clip_inquiry_value(raw: str) -> str:
|
||||
text = re.sub(r"[((]如\s*[::][^))]*[))]?\s*$", "", text).strip()
|
||||
if _CLARIFY_EXAMPLE_LINE.match(text):
|
||||
return ""
|
||||
# 补问 UI 提示不是任何字段的值(空栏后没换行会粘上标题)。
|
||||
if is_clarify_ui_prompt(text):
|
||||
return ""
|
||||
# 整段就是下一栏「10.增值服务」或「增值服务(非必填):…」→ 本栏未填。
|
||||
if _starts_with_inquiry_label(text):
|
||||
return ""
|
||||
# 值中间换行后出现「10.增值服务」:切到下一栏之前(点后可无空格)。
|
||||
cut_at = -1
|
||||
for match in re.finditer(r"(?m)^\s*\d+[\.、.]\s*", text):
|
||||
if match.start() > 0 and _starts_with_inquiry_label(text[match.start() :]):
|
||||
cut_at = match.start()
|
||||
break
|
||||
if cut_at > 0:
|
||||
text = text[:cut_at]
|
||||
hit = _NUMBERED_ITEM.search(text)
|
||||
if hit and hit.start() > 0:
|
||||
text = text[: hit.start()]
|
||||
@@ -594,6 +641,37 @@ def clip_inquiry_value(raw: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def _starts_with_inquiry_label(text: str) -> bool:
|
||||
"""
|
||||
值是否以「另一询价栏目标签」开头(可带补问编号、冒号、必填标记)。
|
||||
|
||||
空起运地被误收成「目的地:CRK」时走这里丢掉,避免 TMS 按脏起运地查价。
|
||||
「10.增值服务」「10.增值服务(非必填):卡转」也算下一栏,不能进贸易条款。
|
||||
「目的地仓库」这种地名不含冒号/必填标记,不算标签,照常保留。
|
||||
"""
|
||||
raw = (text or "").lstrip()
|
||||
if not raw:
|
||||
return False
|
||||
# 补问复制清单编号「10.」可无空格,先剥掉再认标签。
|
||||
raw = _CLARIFY_INDEX.sub("", raw, count=1).lstrip()
|
||||
if not raw:
|
||||
return False
|
||||
labels = sorted(_SHEET_CLEAN_KEYS | _CLARIFY_NAMES, key=len, reverse=True)
|
||||
for label in labels:
|
||||
if len(label) < 2 or not raw.startswith(label):
|
||||
continue
|
||||
if not _label_is_token(raw, 0, label):
|
||||
continue
|
||||
rest = raw[len(label) :]
|
||||
if re.match(r"^\s*$", rest):
|
||||
return True
|
||||
if re.match(r"^\s*[::]", rest):
|
||||
return True
|
||||
if re.match(r"^\s*[((][^))]*(?:必填|非必填)", rest):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def clip_sheet_field_value(raw: str) -> str:
|
||||
"""
|
||||
报价单格子:去掉 /POL、(必填说明)、Terms 和同一行下一列表头,只留紧挨的值。
|
||||
@@ -612,6 +690,9 @@ def clip_sheet_field_value(raw: str) -> str:
|
||||
if "必填" in text:
|
||||
text = re.sub(r"^[^)\n]*必填[^)\n]*)\s*", "", text)
|
||||
text = re.sub(r"^[\s::]+", "", text)
|
||||
# 本栏为空却吞进「目的地:CRK」时,整段丢掉(标签在 pos=0,后面切栏逻辑碰不到)。
|
||||
if _starts_with_inquiry_label(text):
|
||||
return ""
|
||||
for label in _SHEET_CLEAN_KEYS:
|
||||
idx = 0
|
||||
while True:
|
||||
@@ -673,7 +754,11 @@ def harvest_sheet_inquiry_cells(text: str) -> dict[str, str]:
|
||||
for i, (pos, label) in enumerate(kept):
|
||||
begin = pos + len(label)
|
||||
end = kept[i + 1][0] if i + 1 < len(kept) else len(raw)
|
||||
val = clip_sheet_field_value(raw[begin:end])
|
||||
raw_chunk = raw[begin:end]
|
||||
# 报价日期只认同行:空栏后的粘贴提示不能进格子值。
|
||||
if label in {"报价日期", *QUOTE_DATE_SYNONYMS}:
|
||||
raw_chunk = raw_chunk.split("\n", 1)[0]
|
||||
val = clip_sheet_field_value(raw_chunk)
|
||||
if val:
|
||||
out[label] = val
|
||||
return out
|
||||
@@ -773,18 +858,21 @@ def _harvest_labels(text: str, labels: tuple[str, ...]) -> dict[str, str]:
|
||||
begin = pos + len(label)
|
||||
end = kept[i + 1][0] if i + 1 < len(kept) else len(raw)
|
||||
chunk = raw[begin:end]
|
||||
# 增值服务只认冒号后同一行。空行不能把下一行「报价日期(非必填):」收成服务。
|
||||
if label.startswith("增值服务"):
|
||||
# 增值服务 / 报价日期只认冒号后同一行。空行不能把下一行「请复制粘贴…」收成日期。
|
||||
if label.startswith("增值服务") or label in {"报价日期", *QUOTE_DATE_SYNONYMS}:
|
||||
chunk = chunk.split("\n", 1)[0]
|
||||
same_line = chunk.split("\n", 1)[0]
|
||||
same_line_body = re.sub(r"^[\s::,,、]+", "", same_line).strip()
|
||||
# 冒号后本行为空 = 未填。禁止跨行把下一栏「目的地:吉隆坡」收成起运地。
|
||||
# 核对卡复制粘贴时非必填常整行只有「起运地(非必填):」,下一行才是必填目的地。
|
||||
if not same_line_body:
|
||||
# 核对卡无换行粘贴时「起运地(非必填): 目的地(必填):CRK」也视为未填。
|
||||
if not same_line_body or _starts_with_inquiry_label(same_line_body):
|
||||
chunk = ""
|
||||
else:
|
||||
chunk = re.sub(r"^[\s::,,、]+", "", chunk)
|
||||
chunk = re.sub(r"[\s,,、]+$", "", chunk)
|
||||
# 报价日期同行若误收成「请复制粘贴…」补问提示,必须丢掉(WO202610170068)。
|
||||
if label in {"报价日期", *QUOTE_DATE_SYNONYMS}:
|
||||
chunk = clip_inquiry_value(chunk)
|
||||
if chunk:
|
||||
out[label] = chunk
|
||||
return out
|
||||
@@ -899,11 +987,17 @@ _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*[::=]?"
|
||||
rf"\s*(?:(?P<ccy1>{_CCY_TOKEN})\s+)?"
|
||||
# 群里常写「陆运费 CNY1000」币种与数字贴在一起;\s+ 会漏匹配再退回把「段1」当金额。
|
||||
rf"\s*(?:(?P<ccy1>{_CCY_TOKEN})\s*)?"
|
||||
r"(?P<amount>\d+(?:\.\d+)?)"
|
||||
rf"\s*(?P<ccy2>{_CCY_TOKEN}|元)?",
|
||||
re.I,
|
||||
)
|
||||
# 多段报价「段1:」「第一段」只是段号,数字不能当费用金额。
|
||||
_RE_SEGMENT_INDEX_LABEL = re.compile(
|
||||
r"(?:段\s*\d+\s*[::]?|第\s*[一二三四五六七八九\d]+\s*段\s*[::]?)",
|
||||
re.I,
|
||||
)
|
||||
# 「文件发改为:500」漏了「费」、中间还有冒号,不能丢掉。
|
||||
_RE_FEE_CHANGE = re.compile(
|
||||
r"(?P<name>[\u4e00-\u9fff]{2,16}?)"
|
||||
@@ -998,14 +1092,15 @@ def _normalize_fee_project(raw: str) -> str:
|
||||
|
||||
def _strip_group_bot_mentions(text: str) -> str:
|
||||
"""
|
||||
去掉群里 @询价小助手 前缀,避免挡住句首「起运港、」分段标签。
|
||||
去掉群里 @机器人显示名 前缀,避免挡住句首「起运港、」分段标签。
|
||||
|
||||
企微协同句常以 @ 开头;不剥的话 project 抽不到,出单只能靠附加费默认「起运港」,
|
||||
且二次盖价时更容易丢掉分段。
|
||||
且二次盖价时更容易丢掉分段。显示名以后台为准,过渡期仍剥旧名。
|
||||
副作用:无。
|
||||
"""
|
||||
cleaned = (text or "").replace("@询价小助手", "").replace("询价小助手", "")
|
||||
return cleaned.strip()
|
||||
from agent.policy.bot_name import strip_bot_mention
|
||||
|
||||
return strip_bot_mention(text or "").strip()
|
||||
|
||||
|
||||
# 报价日期整段:标签+日期。费用抽取前剥掉,避免把年份 2026 当成金额。
|
||||
@@ -1110,6 +1205,16 @@ def _split_fee_project_prefix(text: str) -> tuple[str, str]:
|
||||
return project, rest
|
||||
|
||||
|
||||
def _strip_segment_index_labels(text: str) -> str:
|
||||
"""
|
||||
去掉「段1:」「第一段」段号标签,避免金额正则吃到段序号。
|
||||
|
||||
运行证据:@机器人 段1:陆运费 CNY1000 → 曾抽成 amount=1。
|
||||
副作用:无。
|
||||
"""
|
||||
return _RE_SEGMENT_INDEX_LABEL.sub(" ", text or "").strip()
|
||||
|
||||
|
||||
def _parse_fee_rows(text: str, baseline_currency: str = "") -> list[dict[str, str]]:
|
||||
"""
|
||||
按行抽费用:码头操作费100CNY、报关费改为300CNY。
|
||||
@@ -1233,8 +1338,9 @@ def extract_product_quote(
|
||||
# 费率句若仍落到普通费用行,会把「燃油附加费」配上句首公斤数。先剥掉再抽。
|
||||
# 必须用剥掉 @询价小助手 后的正文,否则句首「起运港、」分段标签匹配不到。
|
||||
# 顺带剥掉报价日期/有效期/时效要求,避免年份、天数再抽成金额。
|
||||
scrubbed = _strip_rate_and_band_noise(
|
||||
_strip_non_fee_meta_chunks(stripped or raw)
|
||||
# 多段「段1:」先去掉,禁止把段序号当成陆运费/空运费金额。
|
||||
scrubbed = _strip_segment_index_labels(
|
||||
_strip_rate_and_band_noise(_strip_non_fee_meta_chunks(stripped or raw))
|
||||
)
|
||||
rows = _parse_fee_rows(scrubbed, baseline_currency=base_ccy) if scrubbed.strip() else []
|
||||
if rows:
|
||||
@@ -1291,7 +1397,6 @@ def extract_product_quote(
|
||||
return out
|
||||
|
||||
|
||||
|
||||
def _format_percent_rate(raw: str) -> str:
|
||||
"""费率展示统一带 %;已有 % 保留。"""
|
||||
text = (raw or "").strip()
|
||||
|
||||
Reference in New Issue
Block a user