初始提交: Qihu-AIFlow v2.0.1 完整项目

- 前端 React+TypeScript (源码+构建产物)
- 后端 Python FastAPI
- 数据库迁移 001-015
- Docker部署配置
- 微信ClawBot API频道绑定
- 项目文档 README.md
This commit is contained in:
qyyfw committed 2026-07-02 14:40:46 +08:00
commit 78ff261b5a
199 files changed
+69126

No files matched your search

@@ -0,0 +1,18 @@
"""
平台适配器初始化 - 导入所有适配器触发注册
"""
from .wecom_adapter import WecomPlatformAdapter
from .feishu_adapter import FeishuPlatformAdapter
from .dingtalk_adapter import DingtalkPlatformAdapter
from .telegram_adapter import TelegramPlatformAdapter
from .slack_adapter import SlackPlatformAdapter
from .whatsapp_adapter import WhatsAppPlatformAdapter
__all__ = [
"WecomPlatformAdapter",
"FeishuPlatformAdapter",
"DingtalkPlatformAdapter",
"TelegramPlatformAdapter",
"SlackPlatformAdapter",
"WhatsAppPlatformAdapter",
]
@@ -0,0 +1,125 @@
"""
钉钉平台适配器
"""
import hashlib
import hmac
import base64
import json
import logging
import time
from typing import Any
from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata
logger = logging.getLogger(__name__)
@register_platform_adapter(
name="dingtalk",
display_name="钉钉",
description="钉钉开放平台消息适配器,支持文本、图片、文件等消息类型",
support_streaming=False,
support_image=True,
support_file=True,
support_voice=False,
)
class DingtalkPlatformAdapter(BasePlatformAdapter):
"""钉钉平台适配器"""
def get_metadata(self) -> PlatformMetadata:
from ..platform_adapter import platform_registry
return platform_registry["dingtalk"]
def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool:
"""钉钉签名验证: HMAC-SHA256 Base64"""
if not signature or not timestamp:
return True # 无签名默认通过
app_secret = self.config.get("app_secret", "")
if not app_secret:
logger.warning("[DingTalk] 未配置app_secret,跳过签名验证")
return True
try:
# 钉钉签名格式: timestamp + "\n" + appSecret
sign_str = f"{timestamp}\n{app_secret}"
hmac_code = hmac.new(
app_secret.encode("utf-8"),
sign_str.encode("utf-8"),
hashlib.sha256,
).digest()
expected = base64.b64encode(hmac_code).decode("utf-8")
return signature == expected
except Exception as e:
logger.error(f"[DingTalk] 签名验证异常: {e}")
return False
def parse_webhook(self, payload: dict) -> dict:
"""解析钉钉webhook payload为统一消息格式"""
msg_type = payload.get("msgtype", "text")
content = ""
content_type = "text"
sender_id = ""
sender_name = ""
message_id = ""
if msg_type == "text":
text_obj = payload.get("text", {})
content = text_obj.get("content", "")
content_type = "text"
elif msg_type == "image":
image_obj = payload.get("image", {})
content = f"[图片] media_id={image_obj.get('mediaId', '')}"
content_type = "image"
elif msg_type == "file":
file_obj = payload.get("file", {})
content = f"[文件] {file_obj.get('fileName', 'unknown')} media_id={file_obj.get('mediaId', '')}"
content_type = "file"
elif msg_type == "link":
link_obj = payload.get("link", {})
content = f"[链接] {link_obj.get('title', '')} url={link_obj.get('messageUrl', '')}"
content_type = "link"
elif msg_type == "markdown":
md_obj = payload.get("markdown", {})
content = md_obj.get("title", "")
content_type = "markdown"
elif msg_type == "actionCard":
card = payload.get("actionCard", {})
content = f"[卡片] {card.get('title', '')}"
content_type = "actionCard"
else:
content = json.dumps(payload, ensure_ascii=False)
content_type = msg_type
# 提取发送者信息
sender_id = payload.get("senderId", payload.get("senderStaffId", ""))
sender_name = payload.get("senderNick", payload.get("senderCorpId", ""))
message_id = str(payload.get("msgId", ""))
# 群聊信息
group_id = payload.get("conversationId", payload.get("chatId", None))
group_name = payload.get("conversationTitle", None)
return {
"content_type": content_type,
"content": content,
"sender_id": str(sender_id),
"sender_name": str(sender_name),
"message_id": message_id,
"timestamp": int(payload.get("createAt", time.time() * 1000)) // 1000,
"group_id": group_id,
"group_name": group_name,
"raw": payload,
}
def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict:
"""构建钉钉回复消息"""
webhook_url = channel_config.get("webhook_url", "")
if not webhook_url:
logger.warning("[DingTalk] 未配置webhook_url,无法构建回复")
return {}
return {
"api_url": webhook_url,
"headers": {"Content-Type": "application/json"},
"body": {"msgtype": "text", "text": {"content": content}},
}
@@ -0,0 +1,136 @@
"""
飞书平台适配器
"""
import hashlib
import hmac
import json
import logging
from typing import Any
from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata
logger = logging.getLogger(__name__)
@register_platform_adapter(
name="feishu",
display_name="飞书",
description="飞书开放平台消息适配器,支持文本、图片、文件、富文本消息",
support_streaming=True,
support_image=True,
support_file=True,
support_voice=False,
)
class FeishuPlatformAdapter(BasePlatformAdapter):
"""飞书平台适配器"""
def get_metadata(self) -> PlatformMetadata:
from ..platform_adapter import platform_registry
return platform_registry["feishu"]
def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool:
"""飞书签名验证: HMAC-SHA256"""
if not signature or not timestamp:
# 飞书首次URL验证(challenge)不需要签名验证
if payload.get("challenge"):
return True
return True # 宽松模式
app_secret = self.config.get("app_secret", "")
if not app_secret:
logger.warning("[Feishu] 未配置app_secret,跳过签名验证")
return True
# 飞书签名格式: timestamp + nonce + body
try:
body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
nonce = timestamp # 飞书用timestamp作为nonce的一部分
sign_str = f"{timestamp}\n{nonce}\n{body_str}"
expected = hmac.new(
app_secret.encode("utf-8"),
sign_str.encode("utf-8"),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
except Exception as e:
logger.error(f"[Feishu] 签名验证异常: {e}")
return False
def parse_webhook(self, payload: dict) -> dict:
"""解析飞书webhook payload为统一消息格式"""
# 飞书URL验证challenge已在外部处理,这里处理正常消息
event = payload.get("event", {})
message = event.get("message", {})
msg_type = message.get("message_type", "text")
raw_content = message.get("content", "{}")
content = ""
content_type = "text"
sender_id = event.get("sender", {}).get("sender_id", {}).get("open_id", "")
sender_name = event.get("sender", {}).get("sender_id", {}).get("open_id", "")
message_id = event.get("header", {}).get("message_id", "")
try:
if isinstance(raw_content, str):
content_obj = json.loads(raw_content)
else:
content_obj = raw_content if isinstance(raw_content, dict) else {}
if msg_type == "text":
content = content_obj.get("text", "")
content_type = "text"
elif msg_type == "image":
image_key = message.get("image_key", "")
content = f"[图片] image_key={image_key}"
content_type = "image"
elif msg_type == "file":
file_key = message.get("file_key", "")
file_name = content_obj.get("file_name", "unknown")
content = f"[文件] {file_name} file_key={file_key}"
content_type = "file"
elif msg_type == "post":
# 富文本消息,提取text内容
post_content = content_obj.get("content", [])
text_parts = []
for paragraph in post_content:
if isinstance(paragraph, list):
for element in paragraph:
if isinstance(element, dict) and element.get("tag") == "text":
text_parts.append(element.get("text", ""))
content = "".join(text_parts)
content_type = "post"
else:
content = str(content_obj)
content_type = msg_type
except (json.JSONDecodeError, TypeError) as e:
logger.warning(f"[Feishu] 解析content失败: {e}, raw={str(raw_content)[:200]}")
content = str(raw_content)
return {
"content_type": content_type,
"content": content,
"sender_id": sender_id,
"sender_name": sender_name,
"message_id": message_id,
"timestamp": event.get("header", {}).get("create_time", 0),
"group_id": event.get("chat_id", None),
"group_name": None,
"raw": payload,
}
def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict:
"""构建飞书回复消息"""
app_id = channel_config.get("app_id", "")
app_secret = channel_config.get("app_secret", "")
return {
"api_url": "https://open.feishu.cn/open-apis/im/v1/messages",
"headers": {
"Content-Type": "application/json",
"Authorization": f"Bearer {channel_config.get('tenant_access_token', '')}",
},
"body": {
"msg_type": "text",
"content": json.dumps({"text": content}),
},
}
@@ -0,0 +1,146 @@
"""
Slack平台适配器
"""
import hashlib
import hmac
import json
import logging
import time
from typing import Any
from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata
logger = logging.getLogger(__name__)
@register_platform_adapter(
name="slack",
display_name="Slack",
description="Slack Events API消息适配器,支持文本消息和URL验证",
support_streaming=True,
support_image=True,
support_file=True,
support_voice=False,
)
class SlackPlatformAdapter(BasePlatformAdapter):
"""Slack平台适配器 - 签名验证 + challenge响应"""
def get_metadata(self) -> PlatformMetadata:
from ..platform_adapter import platform_registry
return platform_registry["slack"]
def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool:
"""Slack签名验证: v0=HMAC-SHA256(SigningSecret, 'v0:timestamp:body')"""
if not signature or not timestamp:
return True # 无签名默认通过
signing_secret = self.config.get("signing_secret", "")
if not signing_secret:
logger.warning("[Slack] 未配置signing_secret,跳过签名验证")
return True
try:
# Slack签名格式: v0=XXXX
# 签名基础字符串: v0:timestamp:body
body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
sig_basestring = f"v0:{timestamp}:{body_str}"
computed_sig = "v0=" + hmac.new(
signing_secret.encode("utf-8"),
sig_basestring.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(computed_sig, signature)
except Exception as e:
logger.error(f"[Slack] 签名验证异常: {e}")
return False
def parse_webhook(self, payload: dict) -> dict:
"""解析Slack webhook payload为统一消息格式"""
# Slack URL验证 challenge 在外部处理
event = payload.get("event", {})
event_type = event.get("type", "message")
subtype = event.get("subtype", "")
content = ""
content_type = "text"
sender_id = ""
sender_name = ""
message_id = ""
group_id = None
group_name = None
if event_type == "message":
text = event.get("text", "")
user = event.get("user", "")
channel = event.get("channel", "")
ts = event.get("ts", "")
content = text
content_type = "text"
sender_id = user
sender_name = event.get("user_profile", {}).get("display_name", user)
message_id = ts
# 区分私聊/群聊
channel_type = event.get("channel_type", "")
if channel or channel_type == "channel":
group_id = channel
elif channel:
group_id = channel
elif event_type == "app_mention":
text = event.get("text", "")
user = event.get("user", "")
channel = event.get("channel", "")
ts = event.get("ts", "")
# 去除@bot标记
content = text
content_type = "text"
sender_id = user
sender_name = user
message_id = ts
group_id = channel
else:
content = json.dumps(event, ensure_ascii=False)
content_type = event_type
return {
"content_type": content_type,
"content": content,
"sender_id": sender_id,
"sender_name": sender_name,
"message_id": message_id,
"timestamp": int(float(message_id.split(".")[0]) if message_id else time.time()),
"group_id": group_id,
"group_name": group_name,
"raw": payload,
}
def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict:
"""构建Slack回复消息"""
bot_token = channel_config.get("bot_token", "")
channel = channel_config.get("channel", "")
if not bot_token:
logger.warning("[Slack] 未配置bot_token,无法构建回复")
return {}
body = {
"channel": channel,
"text": content,
}
if reply_to:
body["thread_ts"] = reply_to
return {
"api_url": "https://slack.com/api/chat.postMessage",
"headers": {
"Content-Type": "application/json",
"Authorization": f"Bearer {bot_token}",
},
"body": body,
}
@@ -0,0 +1,154 @@
"""
Telegram平台适配器
"""
import hashlib
import hmac
import json
import logging
from typing import Any
from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata
logger = logging.getLogger(__name__)
@register_platform_adapter(
name="telegram",
display_name="Telegram",
description="Telegram Bot API消息适配器,支持文本、图片、文件、语音、视频、贴纸等消息类型",
support_streaming=True,
support_image=True,
support_file=True,
support_voice=True,
)
class TelegramPlatformAdapter(BasePlatformAdapter):
"""Telegram平台适配器"""
def get_metadata(self) -> PlatformMetadata:
from ..platform_adapter import platform_registry
return platform_registry["telegram"]
def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool:
"""Telegram签名验证: HMAC-SHA256(BotToken, 可选)"""
if not signature:
return True # Telegram不使用签名头验证,通过setWebhook时设置secret_token
bot_token = self.config.get("bot_token", "")
if not bot_token:
logger.warning("[Telegram] 未配置bot_token,跳过签名验证")
return True
try:
# Telegram支持通过 X-Telegram-Bot-Api-Secret-Token 头验证
# 新式验证:secret_token = SHA256(bot_token)
expected = hashlib.sha256(bot_token.encode("utf-8")).hexdigest()
return signature == expected
except Exception as e:
logger.error(f"[Telegram] 签名验证异常: {e}")
return False
def parse_webhook(self, payload: dict) -> dict:
"""解析Telegram webhook payload为统一消息格式"""
message = payload.get("message", payload.get("edited_message", {}))
if not message:
# 可能是callback_query等其他更新
callback = payload.get("callback_query", {})
if callback:
message = callback.get("message", {})
message["_callback_data"] = callback.get("data", "")
chat = message.get("chat", {})
from_user = message.get("from", {})
chat_id = str(chat.get("id", ""))
chat_type = chat.get("type", "private")
chat_title = chat.get("title", "")
sender_id = str(from_user.get("id", ""))
sender_name = from_user.get("first_name", "") or from_user.get("username", "")
content = ""
content_type = "text"
if "text" in message:
content = message.get("text", "")
content_type = "text"
elif "photo" in message:
photos = message.get("photo", [])
largest = photos[-1] if photos else {}
content = f"[图片] file_id={largest.get('file_id', '')}"
content_type = "image"
elif "document" in message:
doc = message.get("document", {})
content = f"[文件] {doc.get('file_name', 'unknown')} file_id={doc.get('file_id', '')}"
content_type = "file"
elif "voice" in message:
voice = message.get("voice", {})
content = f"[语音] file_id={voice.get('file_id', '')} duration={voice.get('duration', 0)}s"
content_type = "voice"
elif "video" in message:
video = message.get("video", {})
content = f"[视频] file_id={video.get('file_id', '')}"
content_type = "video"
elif "sticker" in message:
sticker = message.get("sticker", {})
content = f"[贴纸] emoji={sticker.get('emoji', '')} set={sticker.get('set_name', '')}"
content_type = "sticker"
elif "location" in message:
loc = message.get("location", {})
content = f"[位置] lat={loc.get('latitude')} lng={loc.get('longitude')}"
content_type = "location"
elif "contact" in message:
contact = message.get("contact", {})
content = f"[联系人] {contact.get('first_name', '')} phone={contact.get('phone_number', '')}"
content_type = "contact"
elif "animation" in message:
anim = message.get("animation", {})
content = f"[动画] file_id={anim.get('file_id', '')}"
content_type = "animation"
else:
content = json.dumps(message, ensure_ascii=False)
content_type = "unknown"
# 群聊/频道标识
group_id = None
group_name = None
if chat_type in ("group", "supergroup"):
group_id = chat_id
group_name = chat_title
elif chat_type == "channel":
group_id = chat_id
group_name = chat_title
return {
"content_type": content_type,
"content": content,
"sender_id": sender_id,
"sender_name": sender_name,
"message_id": str(message.get("message_id", "")),
"timestamp": message.get("date", 0),
"group_id": group_id,
"group_name": group_name,
"raw": payload,
}
def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict:
"""构建Telegram回复消息"""
bot_token = channel_config.get("bot_token", "")
chat_id = channel_config.get("chat_id", "")
if not bot_token:
logger.warning("[Telegram] 未配置bot_token,无法构建回复")
return {}
body = {
"chat_id": chat_id,
"text": content,
"parse_mode": "HTML",
}
if reply_to:
body["reply_to_message_id"] = reply_to
return {
"api_url": f"https://api.telegram.org/bot{bot_token}/sendMessage",
"headers": {"Content-Type": "application/json"},
"body": body,
}
@@ -0,0 +1,92 @@
"""
企业微信平台适配器
"""
import hashlib
import json
import logging
from typing import Any
from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata
logger = logging.getLogger(__name__)
@register_platform_adapter(
name="wecom",
display_name="企业微信",
description="企业微信消息平台适配器,支持文本、图片、文件、语音消息",
support_streaming=False,
support_image=True,
support_file=True,
support_voice=True,
)
class WecomPlatformAdapter(BasePlatformAdapter):
"""企业微信平台适配器"""
def get_metadata(self) -> PlatformMetadata:
from ..platform_adapter import platform_registry
return platform_registry["wecom"]
def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool:
"""企业微信签名验证: SHA1(token + timestamp + nonce)"""
if not signature or not timestamp:
return True # 无签名要求默认通过
token = self.config.get("token", "")
encoding_aes_key = self.config.get("encoding_aes_key", "")
nonce = (encoding_aes_key or token or "defaultnonce")[:16]
tmp = "".join(sorted([token, timestamp, nonce]))
expected = hashlib.sha1(tmp.encode()).hexdigest()
return signature == expected
def parse_webhook(self, payload: dict) -> dict:
"""解析企业微信webhook payload为统一消息格式"""
msg_type = payload.get("MsgType", "text")
content = ""
content_type = "text"
if msg_type == "text":
content = payload.get("Text", {}).get("Content", "")
content_type = "text"
elif msg_type == "image":
content = payload.get("Image", {}).get("MediaId", "")
content_type = "image"
elif msg_type == "voice":
content = payload.get("Voice", {}).get("MediaId", "")
content_type = "voice"
elif msg_type == "video":
content = payload.get("Video", {}).get("MediaId", "")
content_type = "video"
elif msg_type == "file":
content = payload.get("FileName", "")
content_type = "file"
elif msg_type == "location":
loc = payload.get("Location", {})
content = json.dumps(loc, ensure_ascii=False)
content_type = "location"
elif msg_type == "event":
content = json.dumps({"event": payload.get("Event", ""), "event_key": payload.get("EventKey", "")})
content_type = "event"
else:
content = json.dumps(payload, ensure_ascii=False)
content_type = msg_type
return {
"content_type": content_type,
"content": content,
"sender_id": payload.get("FromUserName", ""),
"sender_name": payload.get("FromUserName", ""),
"message_id": str(payload.get("MsgId", "")),
"timestamp": int(payload.get("CreateTime", 0)),
"group_id": None,
"group_name": None,
"raw": payload,
}
def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict:
"""构建企业微信回复消息"""
webhook_key = channel_config.get("webhook_key", "")
return {
"api_url": f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={webhook_key}",
"headers": {"Content-Type": "application/json"},
"body": {"msgtype": "text", "text": {"content": content}},
}
@@ -0,0 +1,201 @@
"""
WhatsApp平台适配器
"""
import hashlib
import hmac
import json
import logging
import time
from typing import Any
from ..platform_adapter import register_platform_adapter, BasePlatformAdapter, PlatformMetadata
logger = logging.getLogger(__name__)
@register_platform_adapter(
name="whatsapp",
display_name="WhatsApp",
description="WhatsApp Cloud API消息适配器,支持文本、图片、文件、音频、视频、位置等消息类型",
support_streaming=False,
support_image=True,
support_file=True,
support_voice=True,
)
class WhatsAppPlatformAdapter(BasePlatformAdapter):
"""WhatsApp平台适配器"""
def get_metadata(self) -> PlatformMetadata:
from ..platform_adapter import platform_registry
return platform_registry["whatsapp"]
def verify_signature(self, payload: dict, signature: str, timestamp: str) -> bool:
"""WhatsApp签名验证: HMAC-SHA256(AppSecret, payload)"""
if not signature:
# WhatsApp可使用 verify_token 进行webhook验证
app_secret = self.config.get("app_secret", "")
if app_secret:
return False
return True # 无签名默认通过
app_secret = self.config.get("app_secret", "")
if not app_secret:
logger.warning("[WhatsApp] 未配置app_secret,跳过签名验证")
return True
try:
# WhatsApp使用 SHA256 HMAC 验证payload
body_str = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
expected = "sha256=" + hmac.new(
app_secret.encode("utf-8"),
body_str.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, signature)
except Exception as e:
logger.error(f"[WhatsApp] 签名验证异常: {e}")
return False
def parse_webhook(self, payload: dict) -> dict:
"""解析WhatsApp Cloud API webhook payload为统一消息格式
WhatsApp Cloud API格式:
{
"entry": [{
"changes": [{
"value": {
"messaging_product": "whatsapp",
"metadata": {"phone_number_id": "..."},
"messages": [{"from": "...", "type": "text", "text": {"body": "..."}}]
}
}]
}]
}
"""
entries = payload.get("entry", [])
content = ""
content_type = "text"
sender_id = ""
sender_name = ""
message_id = ""
timestamp = int(time.time())
for entry in entries:
changes = entry.get("changes", [])
for change in changes:
value = change.get("value", {})
messages = value.get("messages", [])
metadata = value.get("metadata", {})
phone_number_id = metadata.get("phone_number_id", "")
for msg in messages:
msg_from = msg.get("from", "")
msg_type = msg.get("type", "text")
msg_id = msg.get("id", "")
sender_id = msg_from
sender_name = msg_from
message_id = msg_id
timestamp = int(msg.get("timestamp", time.time()))
if msg_type == "text":
body = msg.get("text", {}).get("body", "")
content = body
content_type = "text"
elif msg_type == "image":
image = msg.get("image", {})
content = f"[图片] id={image.get('id', '')} caption={image.get('caption', '')}"
content_type = "image"
elif msg_type == "document":
doc = msg.get("document", {})
content = f"[文件] {doc.get('filename', 'unknown')} id={doc.get('id', '')}"
content_type = "file"
elif msg_type == "audio":
audio = msg.get("audio", {})
content = f"[语音] id={audio.get('id', '')}"
content_type = "voice"
elif msg_type == "video":
video = msg.get("video", {})
content = f"[视频] id={video.get('id', '')}"
content_type = "video"
elif msg_type == "location":
loc = msg.get("location", {})
content = f"[位置] lat={loc.get('latitude')} lng={loc.get('longitude')}"
content_type = "location"
elif msg_type == "contacts":
contacts = msg.get("contacts", [])
names = [c.get("name", {}).get("formatted_name", "") for c in contacts]
content = f"[联系人] {', '.join(names)}"
content_type = "contacts"
elif msg_type == "button":
button = msg.get("button", {})
content = f"[按钮] text={button.get('text', '')} payload={button.get('payload', '')}"
content_type = "button"
elif msg_type == "interactive":
interactive = msg.get("interactive", {})
list_reply = interactive.get("list_reply", {})
button_reply = interactive.get("button_reply", {})
if list_reply:
content = f"[列表回复] {list_reply.get('title', '')}: {list_reply.get('id', '')}"
elif button_reply:
content = f"[按钮回复] {button_reply.get('title', '')}: {button_reply.get('id', '')}"
else:
content = f"[交互] {json.dumps(interactive)}"
content_type = "interactive"
elif msg_type == "sticker":
sticker = msg.get("sticker", {})
content = f"[贴纸] id={sticker.get('id', '')}"
content_type = "sticker"
elif msg_type == "reaction":
reaction = msg.get("reaction", {})
content = f"[反应] emoji={reaction.get('emoji', '')}"
content_type = "reaction"
else:
content = json.dumps(msg, ensure_ascii=False)
content_type = msg_type
if not content:
# 可能是状态更新(无消息内容)
content = json.dumps(payload, ensure_ascii=False)
content_type = "status"
return {
"content_type": content_type,
"content": content,
"sender_id": sender_id,
"sender_name": sender_name,
"message_id": message_id,
"timestamp": timestamp,
"group_id": None, # WhatsApp主要是私聊
"group_name": None,
"raw": payload,
}
def build_reply(self, channel_config: dict, content: str, reply_to: str = None) -> dict:
"""构建WhatsApp回复消息"""
phone_number_id = channel_config.get("phone_number_id", "")
access_token = channel_config.get("access_token", "")
recipient = channel_config.get("recipient_id", "")
if not phone_number_id or not access_token:
logger.warning("[WhatsApp] 未配置phone_number_id或access_token,无法构建回复")
return {}
body = {
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": recipient,
"type": "text",
"text": {"preview_url": False, "body": content},
}
return {
"api_url": f"https://graph.facebook.com/v18.0/{phone_number_id}/messages",
"headers": {
"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
},
"body": body,
}