修好多段含空运的群跟单、锁舱话术、相册报价通道,以及附件上传成功后电脑端关不掉 H5。

多段激活后无需再带工单号;含空运群回复走机器人;复制补问清单时空货好时间不再吃成下一行编号。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-08 15:23:06 +08:00
co-authored by Cursor
parent 419b4a1bf7
commit 43e633f359
18 changed files with 484 additions and 69 deletions
@@ -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),
+37 -5
View File
@@ -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 '-'}",
]
+45 -9
View File
@@ -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)]
+30 -2
View File
@@ -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", "询价机器人"}