修好多段含空运的群跟单、锁舱话术、相册报价通道,以及附件上传成功后电脑端关不掉 H5。
多段激活后无需再带工单号;含空运群回复走机器人;复制补问清单时空货好时间不再吃成下一行编号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
本文件职责:给附件上传成功、选线路成功页一段共用脚本。
|
||||
手机端只调一次 WeixinJSBridge.closeWindow,关掉当前 H5 后留在询价对话里。
|
||||
再调 ww/wx/window.close 会把询价小助手整个退出,回到企业微信首页。
|
||||
电脑企微没有这套桥,加载 wecom-jssdk、用 /inquiry-form/jsapi-signature 注册后再关一次。
|
||||
电脑企微即使注入了 WeixinJSBridge,这一下也关不掉当前页,必须按电脑走 JS-SDK。
|
||||
失败、过期页不要挂这段。
|
||||
副作用:只产出 HTML 片段;关窗签名走浏览器短请求,不占智能体请求线程做慢活。
|
||||
"""
|
||||
@@ -24,7 +24,8 @@ def wecom_auto_close_script() -> str:
|
||||
成功页脚:加载企微 JS-SDK,register 后关掉当前 H5 窗口。
|
||||
|
||||
线程:浏览器端执行。签名接口与复制页共用,ticket 已缓存在 Redis。
|
||||
为何这样写:上一版只调 WeixinJSBridge,电脑端企微关不掉,会停在成功页。
|
||||
为何这样写:电脑企微也会带上 WeixinJSBridge,但关不掉当前页。
|
||||
若一发现桥就只走手机关闭,成功页会停在「正在返回企微」。手机仍只关一次。
|
||||
"""
|
||||
settings = get_settings()
|
||||
corp = escape((settings.wecom_corp_id or "").strip(), quote=True)
|
||||
@@ -49,6 +50,10 @@ def wecom_auto_close_script() -> str:
|
||||
|
||||
var closed = false;
|
||||
|
||||
function isPhone() {{
|
||||
return /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent || "");
|
||||
}}
|
||||
|
||||
function closeOnce(fn) {{
|
||||
if (closed) return;
|
||||
closed = true;
|
||||
@@ -60,7 +65,8 @@ def wecom_auto_close_script() -> str:
|
||||
closeOnce(function () {{ WeixinJSBridge.call("closeWindow"); }});
|
||||
}}
|
||||
|
||||
// 电脑企微没有 WeixinJSBridge,注册 JS-SDK 后只关一次。
|
||||
// 电脑企微即使有 WeixinJSBridge,也关不掉当前页。注册 JS-SDK 后再关一次。
|
||||
// 不要调 window.close:手机会把询价小助手整个退出。
|
||||
function closeDesktop() {{
|
||||
closeOnce(function () {{
|
||||
if (window.ww && typeof ww.closeWindow === "function") ww.closeWindow();
|
||||
@@ -69,15 +75,16 @@ def wecom_auto_close_script() -> str:
|
||||
}}
|
||||
|
||||
function boot() {{
|
||||
if (typeof WeixinJSBridge !== "undefined") {{
|
||||
setTimeout(closeMobile, {delay});
|
||||
if (isPhone()) {{
|
||||
if (typeof WeixinJSBridge !== "undefined") {{
|
||||
setTimeout(closeMobile, {delay});
|
||||
return;
|
||||
}}
|
||||
document.addEventListener("WeixinJSBridgeReady", function () {{
|
||||
setTimeout(closeMobile, {delay});
|
||||
}}, false);
|
||||
return;
|
||||
}}
|
||||
var sawBridge = false;
|
||||
document.addEventListener("WeixinJSBridgeReady", function () {{
|
||||
sawBridge = true;
|
||||
setTimeout(closeMobile, {delay});
|
||||
}}, false);
|
||||
var ready = Promise.resolve();
|
||||
if (window.ww && ww.register && corpId) {{
|
||||
ready = ww.register({{
|
||||
@@ -88,18 +95,14 @@ def wecom_auto_close_script() -> str:
|
||||
getAgentConfigSignature: sign("agent")
|
||||
}}).catch(function () {{ return null; }});
|
||||
}}
|
||||
ready.then(function () {{
|
||||
setTimeout(function () {{
|
||||
if (sawBridge || typeof WeixinJSBridge !== "undefined") {{
|
||||
if (!closed) closeMobile();
|
||||
return;
|
||||
}}
|
||||
closeDesktop();
|
||||
}}, {delay});
|
||||
var timed = Promise.race([
|
||||
ready,
|
||||
new Promise(function (resolve) {{ setTimeout(resolve, 2500); }})
|
||||
]);
|
||||
timed.then(function () {{
|
||||
setTimeout(closeDesktop, {delay});
|
||||
}}).catch(function () {{
|
||||
setTimeout(function () {{
|
||||
if (!closed && typeof WeixinJSBridge === "undefined") closeDesktop();
|
||||
}}, {delay});
|
||||
setTimeout(closeDesktop, {delay});
|
||||
}});
|
||||
}}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
意图:空运协同群入站(BOT @ 之后的文字或附件)。
|
||||
|
||||
本文件职责:激活/切单后交给 air_group_ops。海运应用群不进这里。
|
||||
禁止:存档文字当业务;锁舱用当前工单去猜;套 sea_group_ops 建群。
|
||||
多段单已在本群激活后,没带工单号的后续消息跟这张多段单,不再要求重发工单号。
|
||||
禁止:存档文字当业务;纯空运锁舱用当前工单去猜;套 sea_group_ops 建群。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -56,6 +57,54 @@ from agent.routing.air_group_intent import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _follow_activated_multi(
|
||||
engine,
|
||||
*,
|
||||
chat_id: str,
|
||||
sender_id: str,
|
||||
text: str,
|
||||
injected_quote: Optional[dict[str, Any]],
|
||||
named_wo: str = "",
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
多段单激活后,后续消息跟这张单,不必再带工单号。
|
||||
|
||||
句中写了工单号就认那张;没写就认本群已经绑上的多段单。
|
||||
纯空运、陆运旧单返回 None,仍走空运自己的当前单。
|
||||
副作用:可能发群消息、写入分段报价。在入站线程调用。
|
||||
"""
|
||||
from agent.policy.multi_group_ops import handle_multi_inbound, ticket_is_multi
|
||||
from agent.policy.sea_group_ops import ticket_no
|
||||
|
||||
ticket = None
|
||||
named = (named_wo or "").strip()
|
||||
if named:
|
||||
getter = getattr(engine.ledger, "get_ticket", None)
|
||||
raw = getter(work_order_no=named) if callable(getter) else None
|
||||
if raw is not None and ticket_is_multi(raw):
|
||||
ticket = raw
|
||||
else:
|
||||
bound = current_ticket(engine, chat_id)
|
||||
if bound is not None and ticket_is_multi(bound):
|
||||
ticket = bound
|
||||
if ticket is None:
|
||||
return None
|
||||
logger.info(
|
||||
"air_group 多段跟单 wo=%s chat=%s named=%s",
|
||||
ticket_no(ticket),
|
||||
chat_id,
|
||||
"1" if named else "0",
|
||||
)
|
||||
return handle_multi_inbound(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=sender_id,
|
||||
text=text,
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
|
||||
|
||||
def _attach_bot_client(engine, message: InboundMessage) -> None:
|
||||
"""
|
||||
每次入站换一次 BOT 出站客户端。
|
||||
@@ -116,20 +165,18 @@ def handle_air_group(
|
||||
return "duplicate_inbound"
|
||||
mark_air_text(chat_id=chat_id, sender_id=message.sender_id, text=text)
|
||||
wo = copy.extract_work_order_no(text)
|
||||
# 句中有多段工单号,或本群已经激活过多段单:跟这张单,不必每句再带工单号。
|
||||
followed = _follow_activated_multi(
|
||||
engine,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=text,
|
||||
injected_quote=injected_quote,
|
||||
named_wo=wo,
|
||||
)
|
||||
if followed is not None:
|
||||
return followed
|
||||
if wo:
|
||||
from agent.policy.multi_group_ops import handle_multi_inbound, ticket_is_multi
|
||||
|
||||
getter = getattr(engine.ledger, "get_ticket", None)
|
||||
raw_ticket = getter(work_order_no=wo) if callable(getter) else None
|
||||
if raw_ticket is not None and ticket_is_multi(raw_ticket):
|
||||
return handle_multi_inbound(
|
||||
flow=engine,
|
||||
ticket=raw_ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=text,
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
from agent.policy.handoff_human import try_commit_air_group_handoff
|
||||
|
||||
handed = try_commit_air_group_handoff(engine, chat_id=chat_id, work_order_no=wo)
|
||||
@@ -212,6 +259,17 @@ def _handle_file(engine, message: InboundMessage) -> str:
|
||||
wo = copy.extract_work_order_no(
|
||||
" ".join([message.content or "", filename])
|
||||
)
|
||||
# 多段已激活的附件也跟当前单,不能再要求先发工单号。
|
||||
followed = _follow_activated_multi(
|
||||
engine,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=" ".join(part for part in ((message.content or "").strip(), filename) if part),
|
||||
injected_quote=None,
|
||||
named_wo=wo,
|
||||
)
|
||||
if followed is not None:
|
||||
return followed
|
||||
if wo:
|
||||
ticket, phase = activate_ticket(
|
||||
flow=engine, chat_id=chat_id, work_order_no=wo, speak=False
|
||||
|
||||
@@ -103,7 +103,7 @@ def handle_group_inbound(
|
||||
or ""
|
||||
).upper()
|
||||
if line == "MULTI":
|
||||
from agent.policy.multi_group_ops import handle_multi_inbound, sender_mode
|
||||
from agent.policy.multi_group_ops import bot_sender_when_air_multi, handle_multi_inbound, sender_mode
|
||||
|
||||
if not (
|
||||
_sender_allowed(ticket, message.sender_id)
|
||||
@@ -111,14 +111,16 @@ def handle_group_inbound(
|
||||
):
|
||||
logger.info("group_inbound 多段忽略非成员 sender=%s", message.sender_id)
|
||||
return "group_ignore"
|
||||
return handle_multi_inbound(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=message.content or "",
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
# 会话存档听到的报价也走这里。含空运时群是机器人建的,必须改走桥才能发得出去。
|
||||
with bot_sender_when_air_multi(ticket):
|
||||
return handle_multi_inbound(
|
||||
flow=engine,
|
||||
ticket=ticket,
|
||||
chat_id=chat_id,
|
||||
sender_id=message.sender_id,
|
||||
text=message.content or "",
|
||||
injected_quote=injected_quote,
|
||||
)
|
||||
ticket = hydrate_ticket(engine, ticket)
|
||||
if not _sender_allowed(ticket, message.sender_id):
|
||||
logger.info("group_inbound 忽略非成员 sender=%s chat=%s", message.sender_id, chat_id)
|
||||
|
||||
@@ -477,8 +477,14 @@ def harvest_collab_labels(text: str) -> dict[str, str]:
|
||||
|
||||
「客户名称张三 包装方式编织袋 货值 10万」切成三项。
|
||||
只说「纸箱」没有标签则不收。值取到下一个标签之前。
|
||||
「(如:随时可提)」整行丢掉,避免补问示例被当成已填。
|
||||
"""
|
||||
return _harvest_labels(text, _COLLAB_LABELS)
|
||||
kept = "\n".join(
|
||||
line
|
||||
for line in (text or "").splitlines()
|
||||
if not _CLARIFY_EXAMPLE_LINE.match(line.strip())
|
||||
)
|
||||
return _harvest_labels(kept, _COLLAB_LABELS)
|
||||
|
||||
|
||||
def harvest_inquiry_optional_labels(text: str) -> dict[str, str]:
|
||||
@@ -508,7 +514,11 @@ def clip_inquiry_value(raw: str) -> str:
|
||||
if hit and hit.start() > 0:
|
||||
text = text[: hit.start()]
|
||||
text = re.sub(r"(?:\n|\r)+\s*\d+[\.、.]\s*$", "", text)
|
||||
return text.strip()
|
||||
text = text.strip()
|
||||
# 复制补问清单时空着的下一行是「4.提货地址」。切到下一栏后只剩编号,不能当成货好时间。
|
||||
if re.fullmatch(r"\d+[\.、.]", text):
|
||||
return ""
|
||||
return text
|
||||
|
||||
|
||||
def clip_sheet_field_value(raw: str) -> str:
|
||||
@@ -549,9 +559,12 @@ def clip_sheet_field_value(raw: str) -> str:
|
||||
if not parts:
|
||||
return ""
|
||||
# 格子里只剩下一个字段名(体积、时效要求)时,这一栏是空的,不能把字段名当成值。
|
||||
# 只剩「4.」也是空的:那是下一行「4.提货地址」被切掉字段名后留下的编号。
|
||||
token = parts[0].strip().strip("::")
|
||||
if token in _SHEET_CLEAN_KEYS or token in _TIGHTEN_LABELS:
|
||||
return ""
|
||||
if re.fullmatch(r"\d+[\.、.]", token):
|
||||
return ""
|
||||
return parts[0].strip()
|
||||
|
||||
|
||||
|
||||
@@ -873,6 +873,14 @@ def apply_air_lock(
|
||||
if str(out.get("error") or "") == "no_quote":
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_lock_need_quote(no))
|
||||
return "no_quote"
|
||||
# 多段是产品限制,不是 TMS 故障。只回原因,不标系统异常、不暂停工单。
|
||||
if str(out.get("error") or "") == "multi_no_lock":
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
chat_id,
|
||||
copy.multi_air_lock_unsupported(work_order_no=no, operator=name),
|
||||
)
|
||||
return "multi_lock_unsupported"
|
||||
why = copy.air_cabin_fail_reason(
|
||||
tms_reason=str(out.get("failReason") or out.get("error") or ""),
|
||||
flight_date=_ticket_flight_date(ticket),
|
||||
|
||||
@@ -2384,8 +2384,14 @@ def adjust_ineligible(reason: str) -> str:
|
||||
return "当前工单还没有可调的报价,不能按这套调价。"
|
||||
|
||||
|
||||
def adjust_only_sales() -> str:
|
||||
return "只有这张单的销售能调价。航线/产品请继续报价,价格未改。"
|
||||
def adjust_only_sales(*, air_room: bool = False) -> str:
|
||||
"""
|
||||
群里不是这张单的销售在发调价。价格不改。
|
||||
|
||||
空运群称呼航线;海运、陆运称呼产品。两句用句号分开,避免连成一句。
|
||||
"""
|
||||
who = "航线" if air_room else "产品"
|
||||
return f"只有这张单的销售能调价。{who}请继续报价,价格未改。"
|
||||
|
||||
|
||||
def adjust_group_body(quote: dict[str, Any] | None, business_line: str = "") -> str:
|
||||
@@ -3071,6 +3077,32 @@ def air_lock_ok(
|
||||
)
|
||||
|
||||
|
||||
# 多段里有空运段时,航线锁舱的失败原因。产品暂不支持,不当 TMS 故障。
|
||||
MULTI_LOCK_UNSUPPORTED = "多段询价,暂时不支持锁舱"
|
||||
# 后台锁舱失败模板末尾常有「已通知IT…请稍等」。多段这条要整句拿掉。
|
||||
_IT_NOTICE_LINE = re.compile(r"已通知\s*IT[^\n。]*。?")
|
||||
|
||||
|
||||
def multi_air_lock_unsupported(*, work_order_no: str, operator: str) -> str:
|
||||
"""
|
||||
多段含空运、航线锁舱时的群回复。
|
||||
|
||||
抬头、操作人、失败原因仍走锁舱失败模板,只去掉通知 IT 那句。
|
||||
不打主账、不标系统异常。
|
||||
"""
|
||||
text = air_lock_fail(
|
||||
work_order_no=work_order_no,
|
||||
operator=operator,
|
||||
reason=MULTI_LOCK_UNSUPPORTED,
|
||||
)
|
||||
kept: list[str] = []
|
||||
for line in text.splitlines():
|
||||
cleaned = _IT_NOTICE_LINE.sub("", line).strip()
|
||||
if cleaned:
|
||||
kept.append(cleaned)
|
||||
return "\n".join(kept).strip()
|
||||
|
||||
|
||||
def air_lock_fail(*, work_order_no: str, operator: str, reason: str) -> str:
|
||||
"""空运锁舱失败。后台 inquiring.air_lock_fail。"""
|
||||
why = (reason or "").strip() or "TMS未返回原因"
|
||||
@@ -3256,7 +3288,7 @@ def system_exception_alert_payload(
|
||||
"""
|
||||
系统异常通知卡:走询价消息服务,不走询价小助手。
|
||||
|
||||
五行对齐现网示例:异常类型、接口/服务、错误码、影响范围、发生时间。
|
||||
五行:异常类型、接口/服务、报错原因、影响范围、发生时间。
|
||||
标题不加 [test]:运维要看的就是这张卡本身,test_prefix 仅保留兼容入参。
|
||||
"""
|
||||
no = (work_order_no or "").strip() or "无"
|
||||
@@ -3265,7 +3297,7 @@ def system_exception_alert_payload(
|
||||
rows = [
|
||||
{"keyname": "异常类型", "value": (exception_type or "").strip() or "-"},
|
||||
{"keyname": "接口/服务", "value": (service or "").strip() or "-"},
|
||||
{"keyname": "错误码", "value": (error_code or "").strip() or "-"},
|
||||
{"keyname": "报错原因", "value": (error_code or "").strip() or "-"},
|
||||
{"keyname": "影响范围", "value": system_exception_impact(step)},
|
||||
{"keyname": "发生时间", "value": (occurred_at or "").strip() or "-"},
|
||||
]
|
||||
@@ -3306,7 +3338,7 @@ def system_exception_alert_text(
|
||||
f"关联工单 {no}",
|
||||
f"异常类型:{(exception_type or '').strip() or '-'}",
|
||||
f"接口/服务:{(service or '').strip() or '-'}",
|
||||
f"错误码:{(error_code or '').strip() or '-'}",
|
||||
f"报错原因:{(error_code or '').strip() or '-'}",
|
||||
f"影响范围:{system_exception_impact(step)}",
|
||||
f"发生时间:{(occurred_at or '').strip() or '-'}",
|
||||
]
|
||||
|
||||
@@ -11,7 +11,8 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Callable, Optional
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Callable, Iterator, Optional
|
||||
|
||||
from agent.llm.extract_text import extract_collab_fields, extract_product_quote
|
||||
from agent.policy import inquiry_copy as copy
|
||||
@@ -20,6 +21,7 @@ from agent.policy.sea_group_ops import (
|
||||
apply_group_deal,
|
||||
group_client_of,
|
||||
is_sales,
|
||||
override_group_client,
|
||||
send_filled_sheet,
|
||||
send_group_text,
|
||||
ticket_no,
|
||||
@@ -33,6 +35,24 @@ _ROLE_MODE = {"sea": "SEA", "land": "LAND", "air": "AIR"}
|
||||
SheetFn = Callable[..., dict[str, Any]]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def bot_sender_when_air_multi(ticket: object) -> Iterator[None]:
|
||||
"""
|
||||
含空运的多段用的是机器人原来的群。应用接口发不进这个群,报价和模板都会没反应。
|
||||
|
||||
这一次出站改走机器人桥,发完还原。没有空运段的多段仍用应用自己建的群,这里不动。
|
||||
只影响当前线程,不改流程上缓存的客户端。
|
||||
"""
|
||||
if not includes_air(load_segments(ticket)):
|
||||
yield
|
||||
return
|
||||
from agent.channel.aibot.reply import AibotReplyClient
|
||||
|
||||
logger.info("multi.outbound 含空运走机器人桥 wo=%s", ticket_no(ticket))
|
||||
with override_group_client(AibotReplyClient()):
|
||||
yield
|
||||
|
||||
|
||||
def ticket_is_multi(ticket: object) -> bool:
|
||||
"""主账业务线是多段联运。"""
|
||||
view = ticket_view(ticket)
|
||||
@@ -257,8 +277,17 @@ def _save_segment_quote(
|
||||
|
||||
|
||||
def _cabin(flow: Any, ticket: object, chat_id: str, sender_id: str, text: str, intent: str) -> str:
|
||||
"""空运段的锁舱、释放走现有空运规则,不挡住其他段报价。"""
|
||||
from agent.policy.air_group_ops import apply_air_lock, apply_air_release
|
||||
"""
|
||||
多段含空运时的锁舱、释放。
|
||||
|
||||
锁舱暂不支持:只回复失败原因,不打 TMS,也不标系统异常。
|
||||
释放仍走空运规则。调用方已确认本单含空运段。群消息在 Worker 线程发出。
|
||||
"""
|
||||
from agent.policy.air_group_ops import (
|
||||
apply_air_release,
|
||||
is_airline,
|
||||
operator_name,
|
||||
)
|
||||
|
||||
if intent == "release":
|
||||
return apply_air_release(
|
||||
@@ -268,13 +297,20 @@ def _cabin(flow: Any, ticket: object, chat_id: str, sender_id: str, text: str, i
|
||||
chat_id=chat_id,
|
||||
instruction=text,
|
||||
)
|
||||
return apply_air_lock(
|
||||
flow=flow,
|
||||
ticket=ticket,
|
||||
sender_id=sender_id,
|
||||
chat_id=chat_id,
|
||||
instruction=text,
|
||||
if not is_airline(flow, sender_id):
|
||||
send_group_text(group_client_of(flow), chat_id, copy.air_airline_only_cabin())
|
||||
return "not_airline"
|
||||
no = ticket_no(ticket)
|
||||
send_group_text(
|
||||
group_client_of(flow),
|
||||
chat_id,
|
||||
copy.multi_air_lock_unsupported(
|
||||
work_order_no=no,
|
||||
operator=operator_name(flow, sender_id),
|
||||
),
|
||||
)
|
||||
logger.info("multi.lock unsupported wo=%s sender=%s", no, sender_id)
|
||||
return "multi_lock_unsupported"
|
||||
|
||||
|
||||
def _cabin_intent(text: str) -> str:
|
||||
|
||||
@@ -260,7 +260,14 @@ def apply_group_adjust(
|
||||
send_group_text(client, room, copy.adjust_ineligible("no_quote"))
|
||||
return "adjust_blocked"
|
||||
if not is_sales(ticket, sender_id):
|
||||
send_group_text(client, room, copy.adjust_only_sales())
|
||||
line = str(view.get("business_line") or view.get("businessLine") or "").upper()
|
||||
air_room = line == "AIR"
|
||||
if line == "MULTI":
|
||||
from agent.policy.multi_group_ops import load_segments
|
||||
from agent.policy.multi_segments import includes_air
|
||||
|
||||
air_room = includes_air(load_segments(ticket))
|
||||
send_group_text(client, room, copy.adjust_only_sales(air_room=air_room))
|
||||
return "adjust_not_sales"
|
||||
current = _current_quote(view, sess)
|
||||
names = [str(r.get("name") or "") for r in adjustable_rows(current)]
|
||||
|
||||
@@ -9,7 +9,9 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from typing import Any, Iterator, Optional
|
||||
|
||||
from agent.llm.extract_text import extract_collab_fields, extract_product_quote
|
||||
from agent.policy.quote_overlay import overlay_labeled_facts
|
||||
@@ -244,14 +246,40 @@ def filename_is_excel_pdf(name: str) -> bool:
|
||||
return any(raw.endswith(ext) for ext in EXCEL_PDF)
|
||||
|
||||
|
||||
# 这一次群出站临时改用的客户端。只在当前线程有效,不改流程单例上的企微客户端。
|
||||
_outbound_client: ContextVar[Any] = ContextVar("inquiry_group_outbound_client", default=None)
|
||||
|
||||
|
||||
def group_client_of(flow: Any) -> Any:
|
||||
"""正式路径没有注入客户端时,走流程自己的默认企微客户端。"""
|
||||
"""
|
||||
取这一次群出站用的客户端。
|
||||
|
||||
有临时指定时用指定的(含空运的多段要走机器人桥)。
|
||||
没有时走流程自己的客户端;正式路径默认是应用发群。
|
||||
"""
|
||||
override = _outbound_client.get()
|
||||
if override is not None:
|
||||
return override
|
||||
getter = getattr(flow, "group_client", None)
|
||||
if callable(getter):
|
||||
return getter()
|
||||
return getattr(flow, "_group_client", None)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def override_group_client(client: Any) -> Iterator[None]:
|
||||
"""
|
||||
这一次群出站改用指定客户端,用完还原。
|
||||
|
||||
只影响当前线程。不改流程单例,海运自己建的群不会被别的工单抢走客户端。
|
||||
"""
|
||||
token = _outbound_client.set(client)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_outbound_client.reset(token)
|
||||
|
||||
|
||||
def _archive_seat_ids(flow: Any) -> set[str]:
|
||||
"""会话存档账号(测服 AI001 / 询价机器人)不能出现在群 @ 里。"""
|
||||
seats: set[str] = {"AI001", "询价机器人"}
|
||||
|
||||
@@ -520,9 +520,10 @@ def harvest_oral_collab(text: str) -> dict[str, str]:
|
||||
群里口语协同:含油/含电/含磁、货好时间、是否为危险品。
|
||||
|
||||
只认原话里已经说死的词(不含电、随时可提、普货),不猜贸易条款和海关编码。
|
||||
「(如:随时可提)」这种补问示例行不算销售说了货好时间。
|
||||
危险品:否词(不是危险品/非危/普货)先于是词,避免「不是危险品」收成是。
|
||||
"""
|
||||
raw = text or ""
|
||||
raw = _without_clarify_examples(text or "")
|
||||
out: dict[str, str] = {}
|
||||
yes_no = (
|
||||
("是否含电", ("不含电", "不带电", "无电"), ("含电", "带电")),
|
||||
|
||||
Reference in New Issue
Block a user