ForcePilot/backend/package/yuxi/channels/adapters/whatsapp/poll.py
Kris 5c3611ff19 refactor(whatsapp): 整理WhatsApp适配器代码结构并修复多线程安全问题
主要变更:
1. 重构导入顺序,统一模块导入规范
2. 提取通用方法到session模块,减少代码重复
3. 为缓存类添加线程/异步锁,修复并发安全问题
4. 新增入站处理器和发送管理器模块,拆分业务逻辑
5. 优化凭证队列,改为异步实现
6. 移除废弃的SSE_POLLING能力标识
7. 修复轮询投票解析逻辑
8. 优化Markdown转换规则,避免格式冲突
9. 完善连接控制器的异常处理
10. 新增发送静默消息的API支持
2026-05-13 16:17:30 +08:00

120 lines
3.7 KiB
Python

from __future__ import annotations
from typing import Any
from yuxi.channels.models import (
Attachment,
ChannelIdentity,
ChannelMessage,
ChannelType,
ChatType,
EventType,
MessageType,
)
from yuxi.utils.logging_config import logger
from .session import _extract_sender_number
def normalize_poll_input(poll: dict[str, Any], max_options: int = 12) -> dict[str, Any] | None:
name = poll.get("name", "").strip()
if not name:
logger.warning("Poll normalization: empty name")
return None
options = poll.get("options", [])
if isinstance(options, str):
options = [o.strip() for o in options.split(",") if o.strip()]
options = [o.strip() for o in options if o.strip()]
seen = set()
unique_options = []
for o in options:
if o not in seen:
seen.add(o)
unique_options.append(o)
options = unique_options
if len(options) < 2:
logger.warning("Poll normalization: less than 2 valid options")
return None
if len(options) > max_options:
logger.warning(f"Poll normalization: {len(options)} options exceeds max {max_options}, truncating")
options = options[:max_options]
selectable_count = poll.get("selectableCount", poll.get("selectable_count", 1))
try:
selectable_count = min(max(int(selectable_count), 1), len(options))
except (TypeError, ValueError):
selectable_count = 1
return {
"name": name,
"options": options,
"selectable_count": selectable_count,
}
def parse_poll_vote(raw_payload: dict[str, Any], channel_id: str) -> ChannelMessage | None:
key = raw_payload.get("key", {})
msg = raw_payload.get("message", {})
poll_update = msg.get("pollUpdateMessage", {})
if not poll_update:
return None
poll_creation_key = poll_update.get("pollCreationMessageKey", {})
vote_info = poll_update.get("vote", {})
remote_jid = key.get("remoteJid", "unknown")
msg_id = key.get("id")
selected_options = []
if isinstance(vote_info, dict):
selected = vote_info.get("selectedOptions", [])
if isinstance(selected, list):
selected_options = selected
elif isinstance(vote_info, list):
selected_options = vote_info
option_names = [o.get("name", str(o)) if isinstance(o, dict) else str(o) for o in selected_options]
vote_text = f"[Poll Vote] {' | '.join(option_names)}" if option_names else "[Poll Vote]"
return ChannelMessage(
identity=ChannelIdentity(
channel_id=channel_id,
channel_type=ChannelType.WHATSAPP,
channel_user_id=_extract_sender_number(remote_jid),
channel_chat_id=remote_jid,
channel_message_id=msg_id,
),
event_type=EventType.MESSAGE_RECEIVED,
message_type=MessageType.TEXT,
chat_type=_jid_to_chat_type(remote_jid),
content=vote_text,
metadata={
"raw_type": "poll_vote",
"remote_jid": remote_jid,
"poll_msg_id": poll_creation_key.get("id"),
"poll_remote_jid": poll_creation_key.get("remoteJid"),
"selected_options": selected_options,
},
attachments=[
Attachment(
type="poll_vote",
file_id=msg_id,
metadata={
"poll_msg_id": poll_creation_key.get("id"),
"poll_remote_jid": poll_creation_key.get("remoteJid"),
"selected": option_names,
},
)
],
)
def _jid_to_chat_type(jid: str) -> ChatType:
if "@g.us" in jid:
return ChatType.GROUP
return ChatType.DIRECT