落地空运单聊询价字段与线路选择。

必填核对后再建单;多条线路走 H5 点选;费用只展示该线路 TMS 回包,不套用整单空运费。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 15:02:32 +08:00
co-authored by Cursor
parent 8afe909230
commit cfbce2fd69
53 changed files with 3470 additions and 372 deletions
@@ -56,6 +56,9 @@ def normalize_aibot_payload(body: dict[str, Any]) -> AibotInboundBody:
raw.setdefault("source", "aibot")
if data.get("response_url"):
raw.setdefault("response_url", data.get("response_url"))
req_id = str(data.get("aibot_req_id") or raw.get("aibot_req_id") or "").strip()
if req_id:
raw["aibot_req_id"] = req_id
return AibotInboundBody(
sender_id=sender,
message_id=mid,
+24 -39
View File
@@ -1,15 +1,15 @@
"""
空运群 BOT 回话。
本文件职责:有即时 `response_url` 就流式回;没有(发文件走存档)就打本机桥 `/send-group`,
由长连接 `aibot_send_msg` 主动往群里说。
禁止:走海运 appchat/send;在回调线程等 LLM。
本文件职责:打本机桥 `/send`(骨架 `/send-group`)。
有这次 @ 的 `aibot_req_id` 时,桥走 `aibot_respond_msg`,群里才会引用原消息;
没有 req_id(典型:会话存档听到附件)才走 `aibot_send_msg` 主动发言。
禁止:走海运 appchat/send;用企微 stream 回复(会带「内容由AI生成」和赞踩);在回调线程等 LLM。
"""
from __future__ import annotations
import logging
import uuid
from typing import Any
import httpx
@@ -23,13 +23,14 @@ class AibotReplyClient:
"""
空运群出站客户端。
@ 带了 response_url:流式只收口这次 @;正文走桥主动发言,和发模板同一条通道。
存档听到附件时没有 url:直接走桥。该群须先被 @ 过,企微才允许主动说。
空运群出站只走桥。带上这次 @ 的 req_id,桥才能引用原消息。
禁止 POST 企微 stream(群里会显示「内容由AI生成」和赞踩)。
"""
def __init__(self, response_url: str = "", timeout_sec: float = 8.0) -> None:
def __init__(self, response_url: str = "", timeout_sec: float = 8.0, callback_req_id: str = "") -> None:
self._url = (response_url or "").strip()
self._timeout = timeout_sec
self._callback_req_id = str(callback_req_id or "").strip()
self.sent: list[dict[str, Any]] = []
def send_group(
@@ -45,41 +46,15 @@ class AibotReplyClient:
if not text:
return {"ok": False, "error": "empty"}
room = str(chat_id or "").strip()
# @ 当场流式回话群里经常看不见长摘要,模板却走主动发言,就会只剩一张表。
# 工单信息必须再走桥,和模板同一条通道;流式只收口这次 @。
if self._url:
self._send_stream(room, text.split("\n", 1)[0][:80])
# 企微 stream 会自带「内容由AI生成」和赞踩,空运群业务话术不要这条通道。
# 工单正文只走桥主动发言(markdown),和发模板同一条通道。
if room:
out = self._send_proactive(room, text)
if out.get("ok") or not self._url:
return out
if self._url:
return self._send_stream(room, text)
return self._send_proactive(room, text)
return {"ok": False, "error": "no_chat_id"}
def _send_stream(self, chat_id: str, text: str) -> dict[str, Any]:
"""@ 当场回话:POST 企微给的 response_url。"""
body = {
"msgtype": "stream",
"stream": {
"id": uuid.uuid4().hex,
"finish": True,
"content": text,
},
}
try:
resp = httpx.post(self._url, json=body, timeout=self._timeout)
ok = 200 <= resp.status_code < 300
if not ok:
logger.warning("aibot 回话 HTTP=%s chat=%s", resp.status_code, chat_id)
return {"ok": ok, "status": resp.status_code}
except Exception:
logger.exception("aibot 回话网络失败 chat=%s", chat_id)
return {"ok": False, "error": "network"}
def _send_proactive(self, chat_id: str, text: str) -> dict[str, Any]:
"""
没有 response_url(典型:会话存档听到附件)时,让桥主动往群里说。
把正文交给桥。有 callback_req_id 时桥引用这次 @;没有则主动发言。
副作用:回环 HTTP 打 8913,不直连企微。桥未连上或该群从未 @ 过,企微不会投递。
"""
@@ -100,14 +75,24 @@ class AibotReplyClient:
# 现网官方桥是 POST /send;本仓库骨架桥是 /send-group。两个都试,先打官方口径。
resp = httpx.post(
f"{base}/send",
json={"chatid": chat_id, "operation": "text", "content": text},
json={
"chatid": chat_id,
"operation": "text",
"content": text,
"aibot_req_id": self._callback_req_id,
},
headers=headers,
timeout=self._timeout,
)
if resp.status_code == 404:
resp = httpx.post(
f"{base}/send-group",
json={"chat_id": chat_id, "content": text, "chat_type": 2},
json={
"chat_id": chat_id,
"content": text,
"chat_type": 2,
"aibot_req_id": self._callback_req_id,
},
headers=headers,
timeout=self._timeout,
)