落地陆运群内协同:按填写样例拉群并只列本单协同项,进群把私聊已抽到的字段带上。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -155,7 +155,25 @@ def send_air_brief_and_sheet(
|
||||
if room and not view.get("collab_chat_id"):
|
||||
view["collab_chat_id"] = room
|
||||
facts = dict(view.get("facts") or {})
|
||||
from agent.schema.field_validate import AIR_COLLAB_FIELDS, pick_collab_from_facts
|
||||
|
||||
seeded = pick_collab_from_facts(facts, AIR_COLLAB_FIELDS)
|
||||
collab = dict(view.get("collab_facts") or {})
|
||||
if seeded:
|
||||
changed = False
|
||||
for k, v in seeded.items():
|
||||
if v and not str(collab.get(k) or "").strip():
|
||||
collab[k] = v
|
||||
changed = True
|
||||
if changed:
|
||||
view["collab_facts"] = collab
|
||||
patcher = getattr(getattr(flow, "ledger", None), "patch_collab_facts", None)
|
||||
if callable(patcher):
|
||||
patcher(
|
||||
work_order_no=no,
|
||||
facts=seeded,
|
||||
sender_id=str(view.get("sales_wecom_id") or view.get("salesWecomId") or ""),
|
||||
)
|
||||
view["collab_facts"] = collab
|
||||
view_q = dict(view.get("quote") or {})
|
||||
override = dict(quote_override or {})
|
||||
|
||||
@@ -17,9 +17,10 @@ from agent.schema.field_validate import (
|
||||
AIR_COLLAB_FIELDS,
|
||||
AIR_REQUIRED_FIELDS,
|
||||
LAND_DISPLAY,
|
||||
SEA_COLLAB_DISPLAY,
|
||||
SEA_COLLAB_FIELDS,
|
||||
SEA_REQUIRED_FIELDS,
|
||||
collab_display_name,
|
||||
collab_fields_for_ticket,
|
||||
display_name,
|
||||
)
|
||||
from agent.schema.land_options import option_prompt as _land_option_prompt
|
||||
@@ -101,6 +102,30 @@ _CONTINUE_HINT = (
|
||||
ASK_TRANSPORT = "请补充运输方式:海运 / 空运 / 陆运。"
|
||||
# 旧一句只问运输类型;销售要一次看到类型、线路、分类。残留调用也走完整清单。
|
||||
ASK_LAND_SUBTYPE = _land_option_prompt(current={})
|
||||
|
||||
|
||||
def land_sample_image_payload() -> dict[str, Any]:
|
||||
"""
|
||||
填写样例表图片出站。
|
||||
|
||||
走企微 image,不走文件气泡。路径随发布包,出站线程再读字节。
|
||||
禁止在回调线程上传。
|
||||
"""
|
||||
from agent.schema.land_options import sample_image_path
|
||||
|
||||
path = sample_image_path()
|
||||
return {
|
||||
"msgtype": "image",
|
||||
"filename": "填写样例.png",
|
||||
"filepath": str(path),
|
||||
}
|
||||
|
||||
|
||||
def land_sample_image_fallback() -> str:
|
||||
"""图片失败时回文字样例,避免销售什么都看不到。"""
|
||||
from agent.schema.land_options import sample_fallback_text
|
||||
|
||||
return sample_fallback_text()
|
||||
FIRST_CHAT = "你好,我是询价机器人。请直接描述客户需求,例如「上海到洛杉矶空运,3 件 180kg,下周三前到」,我会解析需求并查询 TMS 标准报价。"
|
||||
ROUTE_MODEL_FAILED = "刚才没听清,请再说一次:是新询价,还是要接着办哪张工单?"
|
||||
LAND_CONFIRM_TAIL = "请回复“确定”进行TMS查价,内容有误时,请重新发内容。"
|
||||
@@ -111,9 +136,8 @@ LAND_TMS_RETRYING = "正在重新查询TMS报价。工单:{wo}"
|
||||
_LAND_LCL_KEYS = ("起运港", "目的港", "品名", "体积", "毛重", "报价日期")
|
||||
_LAND_FTL_KEYS = ("起运港", "目的港", "品名", "车型/数量", "报价日期")
|
||||
_LAND_HK_FULL_KEYS = ("起运港", "目的港", "品名", "报价日期")
|
||||
_LAND_LCL_TYPES = frozenset({"国内运输拼车", "跨境集拼"})
|
||||
_LAND_FTL_TYPES = frozenset({"国内运输整车", "跨境整车"})
|
||||
_LAND_CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港"})
|
||||
_LAND_FTL_TYPES = frozenset({"国内运输整车", "跨境整车", "中港零担/集拼"})
|
||||
_LAND_CUSTOMS_TYPES = frozenset({"跨境集拼", "跨境整车", "中港整车", "中港零担/集拼"})
|
||||
_LAND_EXAMPLES = {
|
||||
"起运港": "广州",
|
||||
"目的港": "深圳",
|
||||
@@ -127,11 +151,8 @@ _LAND_EXAMPLES = {
|
||||
|
||||
def _land_cargo_keys(land_type: str, facts: dict[str, str] | None = None) -> tuple[str, ...]:
|
||||
"""当前运输类型要展示的货物内部键。中港整车不要车型/数量。"""
|
||||
if land_type == "中港":
|
||||
load = str((facts or {}).get("中港装载方式") or "").strip()
|
||||
if load == "零担/集拼":
|
||||
return _LAND_FTL_KEYS
|
||||
# 整车或还没选定装载:卡片/补问都不出现车型/数量
|
||||
_ = facts
|
||||
if land_type == "中港整车":
|
||||
return _LAND_HK_FULL_KEYS
|
||||
if land_type in _LAND_FTL_TYPES:
|
||||
return _LAND_FTL_KEYS
|
||||
@@ -142,8 +163,9 @@ def land_confirm_card(facts: dict[str, str] | None) -> str:
|
||||
"""
|
||||
确定查价前的核对卡。没有工单号。
|
||||
|
||||
行列出运输类型/线路/分类(中港再加装载)和该类型货物字段。
|
||||
行列出运输类型/线路/分类和该类型货物字段。
|
||||
国内不出现通关口岸;跨境/中港通关口岸非必填,没填留空行。
|
||||
不再单独列出中港装载方式。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
land_type = (src.get("运输类型") or "").strip()
|
||||
@@ -152,8 +174,6 @@ def land_confirm_card(facts: dict[str, str] | None) -> str:
|
||||
f"线路类别(必填):{(src.get('线路类别') or '').strip()}",
|
||||
f"运输分类(必填):{(src.get('运输分类') or '').strip()}",
|
||||
]
|
||||
if land_type == "中港":
|
||||
lines.append(f"中港装载方式(必填):{(src.get('中港装载方式') or '').strip()}")
|
||||
for key in _land_cargo_keys(land_type, src):
|
||||
label = LAND_DISPLAY.get(key, key)
|
||||
val = (src.get(key) or "").strip()
|
||||
@@ -170,6 +190,11 @@ def land_group_no_staff() -> str:
|
||||
return "没有可拉的产品人员。请先在后台「员工管理」维护本线路的陆运产品岗后再试。"
|
||||
|
||||
|
||||
def land_group_pair_rejected() -> str:
|
||||
"""类型+线路不在填写样例里,禁止拉群。"""
|
||||
return "拉群失败,请排查填写内容,并重新发起工单。"
|
||||
|
||||
|
||||
def ask_land_fields(
|
||||
*,
|
||||
facts: dict[str, str] | None = None,
|
||||
@@ -182,7 +207,7 @@ def ask_land_fields(
|
||||
报价日期不进待补充。
|
||||
"""
|
||||
src = dict(facts or {})
|
||||
skip = {"报价日期", "运输类型", "线路类别", "运输分类", "中港装载方式"}
|
||||
skip = {"报价日期", "运输类型", "线路类别", "运输分类"}
|
||||
missing = [k for k in missing_keys if k and k not in skip]
|
||||
land_type = (land_subtype or src.get("运输类型") or "").strip()
|
||||
lines = ["请确认并补充以下信息:", "运输方式:陆运"]
|
||||
@@ -373,10 +398,6 @@ def inquiry_field_lines(
|
||||
f"线路类别:{(facts.get('线路类别') or '-').strip() or '-'}",
|
||||
f"运输分类:{(facts.get('运输分类') or '-').strip() or '-'}",
|
||||
]
|
||||
if land_type == "中港":
|
||||
lines.append(
|
||||
f"中港装载方式:{(facts.get('中港装载方式') or '-').strip() or '-'}"
|
||||
)
|
||||
for key in _land_cargo_keys(land_type, facts):
|
||||
label = LAND_DISPLAY.get(key, key)
|
||||
val = (facts.get(key) or "-").strip() or "-"
|
||||
@@ -1151,8 +1172,10 @@ def sea_group_brief(
|
||||
lines.append("")
|
||||
lines.append("协同补充字段:")
|
||||
collab = dict(collab_facts or {})
|
||||
for key in SEA_COLLAB_FIELDS:
|
||||
label = SEA_COLLAB_DISPLAY.get(key, key)
|
||||
line = "LAND" if mode == "陆运" else "SEA"
|
||||
collab_keys = collab_fields_for_ticket(facts, line) or SEA_COLLAB_FIELDS
|
||||
for key in collab_keys:
|
||||
label = collab_display_name(key, line)
|
||||
val = (collab.get(key) or collab.get(label) or "").strip() or "-"
|
||||
lines.append(f"{label}:{val}")
|
||||
tms = dict(tms_quote) if tms_quote is not None else {}
|
||||
@@ -1298,14 +1321,10 @@ def scheme_horizontal_rows(
|
||||
|
||||
|
||||
def _land_scheme_type_line(facts: dict[str, str]) -> str:
|
||||
"""方案卡「类型」格:运输类型、中港装载、线路类别、运输分类。"""
|
||||
"""方案卡「类型」格:运输类型、线路类别、运输分类。"""
|
||||
src = facts or {}
|
||||
land_type = (src.get("运输类型") or "-").strip() or "-"
|
||||
bits = [land_type]
|
||||
if land_type == "中港":
|
||||
load = (src.get("中港装载方式") or "").strip()
|
||||
if load:
|
||||
bits.append(load)
|
||||
route = (src.get("线路类别") or "-").strip() or "-"
|
||||
klass = (src.get("运输分类") or "-").strip() or "-"
|
||||
bits.extend([route, klass])
|
||||
@@ -1576,7 +1595,15 @@ def group_quote_not_recognized() -> str:
|
||||
return "没有识别到报价,请产品再发一次价格或 Excel/PDF"
|
||||
|
||||
|
||||
def group_fields_not_recognized() -> str:
|
||||
def group_fields_not_recognized(
|
||||
keys: tuple[str, ...] | None = None,
|
||||
business_line: str = "",
|
||||
) -> str:
|
||||
"""没认出协同字段。陆运列出本单可补项;海运保持原六项口。"""
|
||||
if keys:
|
||||
line = (business_line or "").strip().upper() or "SEA"
|
||||
names = [collab_display_name(k, line) for k in keys]
|
||||
return "听到了。请直接说" + "、".join(names) + "。"
|
||||
return "听到了。请直接说贸易条款、货好时间、商品海关编码、是否含油/含电/含磁。"
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
陆运文字询价确定性流程。
|
||||
|
||||
本文件职责:选项联动 → 按类型补货物 → 无工单号核对卡 →(确定后)建单查 TMS。
|
||||
本文件职责:三项选项各自必填(不联动)→ 按类型补货物 → 无工单号核对卡 →(确定后)建单查 TMS。
|
||||
查价后半段复用海运拉群/报价单/成交,拉人按线路类别,禁止陆运岗全员。
|
||||
禁止:字段齐了就建单;直连 TMS;把海运「出卡即查价」改掉。
|
||||
调用:handler 在 Worker/inbox 线程;禁止回调线程同步跑。
|
||||
@@ -23,6 +23,8 @@ from agent.schema.land_options import (
|
||||
harvest_land_options,
|
||||
looks_like_confirm,
|
||||
option_prompt,
|
||||
option_prompt_needs_sample,
|
||||
sample_pair_allowed,
|
||||
validate_land_options,
|
||||
)
|
||||
from agent.schema.tms_land_query import assemble_land_query
|
||||
@@ -112,7 +114,7 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
subtype = str(snap.get("land_subtype") or facts.get("运输类型") or "").strip()
|
||||
if subtype and not str(facts.get("运输类型") or "").strip():
|
||||
facts["运输类型"] = subtype
|
||||
option_keys = ("运输类型", "线路类别", "运输分类", "中港装载方式")
|
||||
option_keys = ("运输类型", "线路类别", "运输分类")
|
||||
base = dict(sess.facts) if sess else {}
|
||||
cargo = {k: v for k, v in facts.items() if k not in option_keys}
|
||||
merged = dict(base)
|
||||
@@ -184,6 +186,23 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
sess=sess,
|
||||
)
|
||||
|
||||
def _reply_land_options(
|
||||
self,
|
||||
reply: ReplyFn,
|
||||
facts: dict[str, str],
|
||||
*,
|
||||
message: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
选项补问:先发文字清单,需要样例时再发表格图。
|
||||
|
||||
图片失败由出站回退文字样例。不在本方法上传素材。
|
||||
"""
|
||||
body = (message or "").strip() or option_prompt(current=facts)
|
||||
reply(body)
|
||||
if option_prompt_needs_sample(current=facts):
|
||||
reply(copy.land_sample_image_fallback(), copy.land_sample_image_payload())
|
||||
|
||||
def _confirm_or_hold(
|
||||
self,
|
||||
*,
|
||||
@@ -221,7 +240,7 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
"""
|
||||
销售确定之后才建陆运工单并查 TMS。
|
||||
|
||||
核对卡阶段没有工单号。运输分类、中港装载方式写入工单 facts,不进入 assemble 出站。
|
||||
核对卡阶段没有工单号。运输分类写入工单 facts,不进入 assemble 出站。
|
||||
"""
|
||||
import uuid
|
||||
|
||||
@@ -363,10 +382,10 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
facts=merged, business_line="LAND", land_subtype=land_type
|
||||
)
|
||||
cargo = dict(check["facts"])
|
||||
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类", "中港装载方式") if merged.get(k)})
|
||||
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类") if merged.get(k)})
|
||||
tms = assemble_land_query(cargo)
|
||||
cargo = dict(tms.get("facts") or cargo)
|
||||
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类", "中港装载方式") if merged.get(k)})
|
||||
cargo.update({k: merged[k] for k in ("运输类型", "线路类别", "运输分类") if merged.get(k)})
|
||||
missing = list(check.get("missing") or [])
|
||||
for key in tms.get("missing") or []:
|
||||
if key not in missing and key not in {"运输类型", "线路类别"}:
|
||||
@@ -412,7 +431,11 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
return "wait_confirm"
|
||||
if state.get("stage") == "options_error":
|
||||
err = (state["opt"].get("errors") or [{}])[0]
|
||||
reply(str(err.get("message") or option_prompt(current=facts)))
|
||||
self._reply_land_options(
|
||||
reply,
|
||||
facts,
|
||||
message=str(err.get("message") or ""),
|
||||
)
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
@@ -427,12 +450,11 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
self._clear_parked_confirm(sender_id)
|
||||
return "need_land_options"
|
||||
if state["stage"] == "options_missing":
|
||||
body = option_prompt(current=facts)
|
||||
logger.info(
|
||||
"land.options_missing missing=%s",
|
||||
(state.get("opt") or {}).get("missing") or [],
|
||||
)
|
||||
reply(body)
|
||||
self._reply_land_options(reply, facts)
|
||||
self._save(
|
||||
FlowSession(
|
||||
sender_id=sender_id,
|
||||
@@ -574,6 +596,32 @@ class LandTextInquiryFlow(SeaTextInquiryFlow):
|
||||
|
||||
clear_pending_land_confirm(sender_id)
|
||||
|
||||
def _pull_collab_group(
|
||||
self,
|
||||
sess: FlowSession,
|
||||
reply: ReplyFn,
|
||||
card_meta: Optional[dict[str, Any]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
点「拉产品进群协同」:先核填写样例,再按线路拉人建群。
|
||||
|
||||
类型+线路对不上样例:不建群,告诉销售重新开单。按钮保持可点。
|
||||
对得上则走海运同一套建群。
|
||||
"""
|
||||
facts = dict(sess.facts or {})
|
||||
land_type = str(facts.get("运输类型") or "").strip()
|
||||
route = str(facts.get("线路类别") or "").strip()
|
||||
if not sample_pair_allowed(land_type, route):
|
||||
logger.info(
|
||||
"land.group.reject pair type=%s route=%s wo=%s",
|
||||
land_type or "-",
|
||||
route or "-",
|
||||
sess.work_order_no or "-",
|
||||
)
|
||||
reply(copy.land_group_pair_rejected())
|
||||
return sess.phase or "wait_collab"
|
||||
return super()._pull_collab_group(sess, reply, card_meta=card_meta)
|
||||
|
||||
def _match_sea_staff(self, sess: FlowSession) -> list[dict[str, Any]]:
|
||||
"""按本单线路类别整词匹配产品·陆运,禁止全员。"""
|
||||
matcher = getattr(self._ledger, "match_land_staff", None)
|
||||
|
||||
@@ -24,6 +24,10 @@ _COLLAB_LABEL_MARKERS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
||||
("是否含油", ("是否含油", "has_oil")),
|
||||
("是否含电", ("是否含电", "has_battery")),
|
||||
("是否含磁", ("是否含磁", "has_magnet")),
|
||||
("客户名称", ("客户名称", "customer_name")),
|
||||
("货值", ("货值", "cargo_value")),
|
||||
("是否为危险品", ("是否为危险品", "is_dangerous")),
|
||||
("包装方式", ("包装方式", "包装类型")),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -14,7 +14,12 @@ from typing import Any, Optional
|
||||
from agent.llm.extract_text import extract_collab_fields, extract_product_quote
|
||||
from agent.policy.quote_overlay import overlay_labeled_facts
|
||||
from agent.policy import inquiry_copy as copy
|
||||
from agent.schema.field_validate import SEA_COLLAB_DISPLAY, SEA_COLLAB_FIELDS
|
||||
from agent.schema.field_validate import (
|
||||
SEA_COLLAB_DISPLAY,
|
||||
SEA_COLLAB_FIELDS,
|
||||
collab_display_name,
|
||||
collab_fields_for_ticket,
|
||||
)
|
||||
|
||||
# 主账填表按英文码取格子;中文键必须同时带上,否则货好时间/条款写不进表。
|
||||
_COLLAB_FILL_ALIASES = {
|
||||
@@ -24,6 +29,10 @@ _COLLAB_FILL_ALIASES = {
|
||||
"是否含油": ("是否含油", "has_oil"),
|
||||
"是否含电": ("是否含电", "has_battery"),
|
||||
"是否含磁": ("是否含磁", "has_magnet"),
|
||||
"客户名称": ("客户名称", "customer_name"),
|
||||
"货值": ("货值", "cargo_value"),
|
||||
"是否为危险品": ("是否为危险品", "is_dangerous"),
|
||||
"包装方式": ("包装方式", "包装类型"),
|
||||
}
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -91,6 +100,24 @@ def ticket_no(ticket: object) -> str:
|
||||
return str(ticket_view(ticket).get("work_order_no") or "").strip()
|
||||
|
||||
|
||||
def _collab_keys_of(ticket: object, sess: Any = None) -> tuple[str, tuple[str, ...]]:
|
||||
"""本单业务线 + 允许写入的协同内部键。"""
|
||||
view = ticket_view(ticket)
|
||||
facts = dict(view.get("facts") or {})
|
||||
if sess is not None and getattr(sess, "facts", None):
|
||||
facts.update(dict(sess.facts or {}))
|
||||
line = str(
|
||||
view.get("business_line")
|
||||
or view.get("businessLine")
|
||||
or (getattr(sess, "business_line", "") if sess is not None else "")
|
||||
or "SEA"
|
||||
).strip().upper() or "SEA"
|
||||
keys = collab_fields_for_ticket(facts, line)
|
||||
if not keys and line != "LAND":
|
||||
keys = SEA_COLLAB_FIELDS
|
||||
return line, keys
|
||||
|
||||
|
||||
def is_terminal(ticket: object) -> bool:
|
||||
return ticket_status(ticket) in TERMINAL
|
||||
|
||||
@@ -378,6 +405,13 @@ def send_brief_and_sheet(
|
||||
quote = dict(override or sess_q or view_q or tms or manual)
|
||||
sales_name, products, mention_ids = resolve_group_mentions(flow, ticket, sess)
|
||||
has_tms = bool(tms.get("total") or _quote_fee_rows(tms))
|
||||
line = str(
|
||||
view.get("business_line")
|
||||
or view.get("businessLine")
|
||||
or (sess.business_line if sess is not None else "")
|
||||
or "SEA"
|
||||
).strip().upper()
|
||||
mode = "陆运" if line == "LAND" else "海运"
|
||||
brief = copy.sea_group_brief(
|
||||
work_order_no=no,
|
||||
facts=facts,
|
||||
@@ -388,9 +422,16 @@ def send_brief_and_sheet(
|
||||
has_price=has_tms,
|
||||
tms_quote=tms,
|
||||
product_quote=manual,
|
||||
transport_mode=mode,
|
||||
)
|
||||
send_group_text(client, chat_id, brief, mention_userids=mention_ids)
|
||||
send_filled_sheet(flow=flow, ticket=ticket, sess=sess, quote_override=quote)
|
||||
send_filled_sheet(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
sess=sess,
|
||||
quote_override=quote,
|
||||
business_line=line,
|
||||
)
|
||||
return "brief_and_sheet"
|
||||
|
||||
|
||||
@@ -400,7 +441,7 @@ def send_filled_sheet(
|
||||
ticket: object,
|
||||
sess: Any = None,
|
||||
quote_override: Optional[dict[str, Any]] = None,
|
||||
business_line: str = "SEA",
|
||||
business_line: str = "",
|
||||
chat_id: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
@@ -429,7 +470,7 @@ def send_filled_sheet(
|
||||
flow=flow,
|
||||
chat_id=room,
|
||||
work_order_no=no,
|
||||
facts=_facts_for_fill(facts, collab),
|
||||
facts=_facts_for_fill(facts, collab, business_line=line or "SEA"),
|
||||
quote=quote,
|
||||
sales_id=str(
|
||||
(sess.sender_id if sess else "")
|
||||
@@ -441,15 +482,20 @@ def send_filled_sheet(
|
||||
)
|
||||
|
||||
|
||||
def _collab_value(collab: dict[str, str], key: str) -> str:
|
||||
"""协同六项:内部键、对外展示名都能取到。"""
|
||||
def _collab_value(collab: dict[str, str], key: str, business_line: str = "SEA") -> str:
|
||||
"""协同项:内部键、对外展示名都能取到。"""
|
||||
src = dict(collab or {})
|
||||
val = str(src.get(key) or "").strip()
|
||||
if val:
|
||||
return val
|
||||
label = SEA_COLLAB_DISPLAY.get(key, "")
|
||||
label = collab_display_name(key, business_line)
|
||||
if label:
|
||||
val = str(src.get(label) or "").strip()
|
||||
if val:
|
||||
return val
|
||||
sea_label = SEA_COLLAB_DISPLAY.get(key, "")
|
||||
if sea_label:
|
||||
val = str(src.get(sea_label) or "").strip()
|
||||
return val
|
||||
|
||||
|
||||
@@ -466,15 +512,22 @@ def _oil_elec_mag_text(collab: dict[str, str]) -> str:
|
||||
return f"油:{oil or '-'};电:{elec or '-'};磁:{mag or '-'}"
|
||||
|
||||
|
||||
def _facts_for_fill(facts: dict[str, str], collab: dict[str, str]) -> dict[str, str]:
|
||||
def _facts_for_fill(
|
||||
facts: dict[str, str],
|
||||
collab: dict[str, str],
|
||||
business_line: str = "SEA",
|
||||
) -> dict[str, str]:
|
||||
"""
|
||||
询价七项 + 协同六项。中文键和填表英文码一起带上。
|
||||
询价字段 + 本单协同项。中文键和填表英文码一起带上。
|
||||
主账 writeFactsByLabel 只认 cargo_ready_date / trade_terms,只传「货好时间」会空着。
|
||||
模板没有格子的新项照样带上,overlay 找不到标签就跳过。
|
||||
"""
|
||||
merged = dict(facts or {})
|
||||
src = dict(collab or {})
|
||||
for key in SEA_COLLAB_FIELDS:
|
||||
val = _collab_value(src, key)
|
||||
line = (business_line or "SEA").strip().upper() or "SEA"
|
||||
keys = collab_fields_for_ticket(facts, line) or SEA_COLLAB_FIELDS
|
||||
for key in keys:
|
||||
val = _collab_value(src, key, line)
|
||||
if not val:
|
||||
continue
|
||||
for alias in _COLLAB_FILL_ALIASES.get(key, (key,)):
|
||||
@@ -549,7 +602,7 @@ def apply_collab_update(
|
||||
sender_id: str,
|
||||
sess: Any = None,
|
||||
) -> str:
|
||||
"""补协同六项。终态要说明原因。"""
|
||||
"""补本单协同项。终态要说明原因。不该有的项当没补到。"""
|
||||
ticket = hydrate_ticket(flow, ticket)
|
||||
no = ticket_no(ticket)
|
||||
chat_id = str((sess.collab_chat_id if sess else "") or ticket_view(ticket).get("collab_chat_id") or "")
|
||||
@@ -557,11 +610,21 @@ def apply_collab_update(
|
||||
if is_terminal(ticket):
|
||||
send_group_text(client, chat_id, copy.ticket_locked(work_order_no=no, status=ticket_status(ticket)))
|
||||
return "ticket_locked"
|
||||
fields = extract_collab_fields(text, allow_b=True)
|
||||
line, keys = _collab_keys_of(ticket, sess)
|
||||
fields = extract_collab_fields(text, allow_b=True, allowed_keys=keys)
|
||||
if not fields:
|
||||
send_group_text(client, chat_id, copy.group_fields_not_recognized())
|
||||
send_group_text(
|
||||
client,
|
||||
chat_id,
|
||||
copy.group_fields_not_recognized(keys=keys or None, business_line=line),
|
||||
)
|
||||
return "collab_not_recognized"
|
||||
flow.apply_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
|
||||
flow.apply_collab_facts(
|
||||
work_order_no=no,
|
||||
facts=fields,
|
||||
sender_id=sender_id,
|
||||
allowed_keys=keys,
|
||||
)
|
||||
if sess is not None:
|
||||
merged = dict(sess.collab_facts)
|
||||
merged.update(fields)
|
||||
@@ -603,9 +666,15 @@ def apply_product_quote(
|
||||
if not is_product(ticket, sender_id, flow=flow):
|
||||
send_group_text(client, chat_id, copy.group_only_product_can_quote())
|
||||
return "quote_not_product"
|
||||
fields = extract_collab_fields(text)
|
||||
fields = extract_collab_fields(text, allowed_keys=_collab_keys_of(ticket, sess)[1])
|
||||
if fields:
|
||||
flow.apply_collab_facts(work_order_no=no, facts=fields, sender_id=sender_id)
|
||||
line, keys = _collab_keys_of(ticket, sess)
|
||||
flow.apply_collab_facts(
|
||||
work_order_no=no,
|
||||
facts=fields,
|
||||
sender_id=sender_id,
|
||||
allowed_keys=keys,
|
||||
)
|
||||
if sess is not None:
|
||||
merged = dict(sess.collab_facts)
|
||||
merged.update(fields)
|
||||
@@ -772,7 +841,7 @@ def _persist_quote_sheet(
|
||||
result = renderer(
|
||||
work_order_no=no,
|
||||
business_line=line,
|
||||
facts=_facts_for_fill(facts, collab),
|
||||
facts=_facts_for_fill(facts, collab, business_line=line or "SEA"),
|
||||
quote=quote,
|
||||
format="xlsx",
|
||||
sales_wecom_id=str(
|
||||
|
||||
@@ -514,6 +514,24 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
product_ids=product_ids,
|
||||
)
|
||||
|
||||
from agent.schema.field_validate import collab_fields_for_ticket, pick_collab_from_facts
|
||||
|
||||
line = (sess.business_line or "SEA").upper()
|
||||
keys = collab_fields_for_ticket(sess.facts, line)
|
||||
seeded = pick_collab_from_facts(sess.facts, keys)
|
||||
if seeded:
|
||||
merged = dict(sess.collab_facts or {})
|
||||
for k, v in seeded.items():
|
||||
if v and not str(merged.get(k) or "").strip():
|
||||
merged[k] = v
|
||||
sess.collab_facts = merged
|
||||
self.apply_collab_facts(
|
||||
work_order_no=no,
|
||||
facts=seeded,
|
||||
sender_id=sess.sender_id,
|
||||
allowed_keys=keys,
|
||||
)
|
||||
|
||||
brief = copy.sea_group_brief(
|
||||
work_order_no=no,
|
||||
facts=sess.facts,
|
||||
@@ -638,6 +656,7 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
work_order_no: str,
|
||||
facts: dict[str, str],
|
||||
sender_id: str,
|
||||
allowed_keys: tuple[str, ...] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
群里静默写入协同字段。不改六态、不回复、不出报价版本。
|
||||
@@ -647,10 +666,11 @@ class SeaTextInquiryFlow(AirTextInquiryFlow):
|
||||
from agent.schema.field_validate import SEA_COLLAB_FIELDS
|
||||
|
||||
no = (work_order_no or "").strip()
|
||||
keys = tuple(allowed_keys) if allowed_keys is not None else SEA_COLLAB_FIELDS
|
||||
cleaned = {
|
||||
k: str(v).strip()
|
||||
for k, v in (facts or {}).items()
|
||||
if k in SEA_COLLAB_FIELDS and str(v or "").strip()
|
||||
if k in keys and str(v or "").strip()
|
||||
}
|
||||
if not no or not cleaned:
|
||||
return {"ok": False, "updated": []}
|
||||
|
||||
Reference in New Issue
Block a user