群里按 BOT 通道激活当前工单、航线报价和成交跟进;没有 TMS 报价编号时用工单字段补齐仍调锁舱,避免被本地规则拦住。 Co-authored-by: Cursor <cursoragent@cursor.com>
155 lines
5.0 KiB
Python
155 lines
5.0 KiB
Python
"""
|
|
微盛群消息 → InboundMessage。
|
|
|
|
本文件职责:从管家 records 抽出 sender_id / chat_id / 正文 / 附件名。
|
|
禁止:把存档席「询价机器人」当成发言人入业务;禁止再向企微换 ID。
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import Any, Mapping, Optional, Sequence
|
|
|
|
from agent.channel.wecom.models import InboundMessage
|
|
|
|
# 存档席在群里的显示名;from 偶发不是 userid 时也要丢掉
|
|
SEAT_DISPLAY_NAMES = frozenset({"询价机器人"})
|
|
_SKIP_TYPES = frozenset({"revoke", "recall", "event", "unknown"})
|
|
_FILE_TYPES = frozenset({"file", "image", "video", "emotion", "voice"})
|
|
|
|
|
|
def _as_mapping(raw: Any) -> dict[str, Any]:
|
|
if isinstance(raw, Mapping):
|
|
return dict(raw)
|
|
if isinstance(raw, str) and raw.strip().startswith("{"):
|
|
try:
|
|
parsed = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
return dict(parsed) if isinstance(parsed, Mapping) else {}
|
|
return {}
|
|
|
|
|
|
def _create_time(msgtime: Any) -> int:
|
|
"""微盛 msgtime 常是毫秒;InboundMessage.create_time 用秒。"""
|
|
try:
|
|
n = int(msgtime or 0)
|
|
except (TypeError, ValueError):
|
|
return 0
|
|
if n > 10**12:
|
|
return n // 1000
|
|
return n
|
|
|
|
|
|
def _unwrap_text(raw: Any) -> str:
|
|
"""
|
|
从字符串或嵌套对象取出正文。
|
|
|
|
管家 content 经常是摘要;企微官方结构是 text.content。空、null 不当正文。
|
|
"""
|
|
if raw is None:
|
|
return ""
|
|
if isinstance(raw, str):
|
|
text = raw.strip()
|
|
if not text or text.lower() in {"null", "none"}:
|
|
return ""
|
|
if text.startswith("{") or text.startswith("["):
|
|
try:
|
|
return _unwrap_text(json.loads(text))
|
|
except json.JSONDecodeError:
|
|
return text
|
|
return text
|
|
if isinstance(raw, Mapping):
|
|
for key in ("content", "text", "msg"):
|
|
got = _unwrap_text(raw.get(key))
|
|
if got:
|
|
return got
|
|
return ""
|
|
return str(raw).strip()
|
|
|
|
|
|
def _best_text(record: Mapping[str, Any], payload: Mapping[str, Any]) -> str:
|
|
"""多处正文里取最长的一份,避免摘要盖掉带金额的原文。"""
|
|
candidates = [
|
|
_unwrap_text(record.get("content")),
|
|
_unwrap_text(payload.get("content")),
|
|
_unwrap_text(payload.get("text")),
|
|
_unwrap_text(payload.get("msg")),
|
|
]
|
|
texts = [x for x in candidates if x]
|
|
if not texts:
|
|
return ""
|
|
return max(texts, key=len)
|
|
|
|
|
|
def _filename(msg_type: str, content: str, payload: Mapping[str, Any]) -> str:
|
|
for key in ("filename", "file_name", "FileName", "title", "name"):
|
|
val = str(payload.get(key) or "").strip()
|
|
if val:
|
|
return val
|
|
if msg_type in _FILE_TYPES:
|
|
return str(content or "").strip()
|
|
return ""
|
|
|
|
|
|
def archive_record_to_inbound(
|
|
record: Mapping[str, Any],
|
|
*,
|
|
seat_userids: Sequence[str] = (),
|
|
) -> Optional[InboundMessage]:
|
|
"""
|
|
一条管家记录变成群入站。
|
|
|
|
返回 None:不是群、没有发言人、发言人是存档席、或撤回类消息。
|
|
message_id 带 wshoto: 前缀,避免和回调 MsgId 撞车。
|
|
"""
|
|
room_id = str(record.get("room_id") or record.get("roomid") or "").strip()
|
|
sender_id = str(record.get("from") or record.get("from_id") or "").strip()
|
|
if not room_id or not sender_id:
|
|
return None
|
|
seats = {str(x).strip() for x in seat_userids if str(x).strip()}
|
|
if sender_id in seats or sender_id in SEAT_DISPLAY_NAMES:
|
|
return None
|
|
|
|
msg_type = str(record.get("msgtype") or record.get("msg_type") or "text").strip().lower()
|
|
if msg_type in _SKIP_TYPES:
|
|
return None
|
|
|
|
payload = _as_mapping(record.get("msg_content") or record.get("msgcontent"))
|
|
content = _best_text(record, payload)
|
|
msgid = str(record.get("msgid") or record.get("msg_id") or "").strip()
|
|
seq = record.get("seq")
|
|
if not msgid:
|
|
msgid = f"{room_id}:{seq or 0}:{sender_id}"
|
|
|
|
media: dict[str, Any] = {}
|
|
inbound_type = "text"
|
|
if msg_type in _FILE_TYPES:
|
|
inbound_type = "file" if msg_type == "file" else msg_type
|
|
media = {
|
|
"filename": _filename(msg_type, content, payload),
|
|
"media_id": str(payload.get("sdkfileid") or payload.get("sdk_file_id") or ""),
|
|
"file_ext": str(payload.get("fileext") or payload.get("file_ext") or ""),
|
|
"content_type": "",
|
|
"filesize": payload.get("filesize") or payload.get("file_size") or 0,
|
|
}
|
|
if not content:
|
|
content = str(media.get("filename") or "")
|
|
|
|
return InboundMessage(
|
|
sender_id=sender_id,
|
|
message_id=f"wshoto:{msgid}",
|
|
content=content,
|
|
msg_type=inbound_type,
|
|
chat_id=room_id,
|
|
chat_type="group",
|
|
create_time=_create_time(record.get("msgtime") or record.get("msg_time")),
|
|
media=media,
|
|
raw={
|
|
"source": "wshoto_archive",
|
|
"seq": seq,
|
|
"msgtype": msg_type,
|
|
"direction": record.get("direction") or "",
|
|
},
|
|
)
|