修好多段含空运的群跟单、锁舱话术、相册报价通道,以及附件上传成功后电脑端关不掉 H5。
多段激活后无需再带工单号;含空运群回复走机器人;复制补问清单时空货好时间不再吃成下一行编号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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", "询价机器人"}
|
||||
|
||||
Reference in New Issue
Block a user