Files
inquiry_robot/inquiry-agent/agent/channel/wshoto/parse.py
T
jillion886andCursor d9029dec89 落地空运群内协同与锁舱出站。
群里按 BOT 通道激活当前工单、航线报价和成交跟进;没有 TMS 报价编号时用工单字段补齐仍调锁舱,避免被本地规则拦住。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 17:25:42 +08:00

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 "",
},
)