This commit is contained in:
eric
2026-03-31 11:21:17 -05:00
parent c98bd91fb9
commit d16f188845
5 changed files with 227 additions and 128 deletions

102
main.py
View File

@@ -2,7 +2,6 @@ import json
import logging
import base64
import time
from html import escape
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from typing import Optional
@@ -131,60 +130,30 @@ def _build_keyword_h5_reply(query: str, user_id: str) -> Optional[str]:
return json.dumps(msg, ensure_ascii=False) if msg else None
async def _send_kefu_msg_for_xml_callback(data: dict) -> None:
def _build_xml_callback_reply(data: dict, answer_text: str) -> dict:
"""
XML 回调渠道下,主动调用 sendmsg 下发消息(回调响应体不会直接展示给用户
XML 回调直接回包给平台,由平台转发给用户。
返回结构按“userid/appid/content/channel/from”组织。
"""
query = str(data.get("Query", "")).strip()
user_id = str(data.get("UserId", "")).strip()
userid = str(data.get("UserId", "")).strip()
appid = str(data.get("ChannelId", "") or data.get("appid", "")).strip()
channel = int(str(data.get("channel", "0") or "0"))
msg = _build_keyword_h5_obj(query, user_id)
if not msg:
return
channel_raw = str(data.get("channel", "0") or "0")
try:
channel = int(channel_raw)
except Exception:
channel = 0
url = f"https://chatbot.weixin.qq.com/openapi/sendmsg/{TOKEN}"
body = {
# 关键词命中返回 H5 结构;其他走文本结构
keyword_msg = _build_keyword_h5_obj(str(data.get("Query", "")), userid)
content = keyword_msg if keyword_msg else {"msg": answer_text}
return {
"userid": userid,
"appid": appid,
"openid": user_id,
"msg": msg,
"content": content,
"channel": channel,
"from": 1,
}
async with httpx.AsyncClient(timeout=10, verify=False) as client:
# 1) 先尝试明文 JSON 发送(多数场景可直接工作)
resp = await client.post(url, json=body, headers={"content-type": "application/json"})
resp.raise_for_status()
payload = resp.json() if resp.content else {}
errcode = int(payload.get("errcode", -1)) if isinstance(payload, dict) else -1
if errcode == 0:
logger.info("关键词命中sendmsg 明文发送成功")
return
logger.warning("sendmsg 明文返回非成功,准备加密重试:%s", payload)
# 2) 按文档加密格式重试body = {"encrypt": "..."}
msg_text = json.dumps(msg, ensure_ascii=False)
xml_data = (
"<xml>"
f"<appid><![CDATA[{escape(appid)}]]></appid>"
f"<openid><![CDATA[{escape(user_id)}]]></openid>"
f"<msg><![CDATA[{msg_text}]]></msg>"
f"<channel>{channel}</channel>"
"</xml>"
)
encrypted = aes_encrypt_base64(xml_data, ENCODING_AES_KEY, APP_ID)
enc_resp = await client.post(
url,
json={"encrypt": encrypted},
headers={"content-type": "application/json"},
)
enc_resp.raise_for_status()
enc_payload = enc_resp.json() if enc_resp.content else {}
enc_errcode = int(enc_payload.get("errcode", -1)) if isinstance(enc_payload, dict) else -1
if enc_errcode == 0:
logger.info("关键词命中sendmsg 加密发送成功")
return
logger.error("sendmsg 加密发送失败:%s", enc_payload)
def _safe_filter_value(value: str) -> str:
@@ -471,7 +440,7 @@ async def handle_business(data: dict) -> str:
keyword_reply = _build_keyword_h5_reply(query, user_id)
if keyword_reply:
return keyword_reply
return "已收到你的消息,输入“电子书”或“群”可获取对应卡片。"
return "1111111"
def _decode_jwt_payload_without_verify(token: str) -> dict:
@@ -610,29 +579,30 @@ async def _handle_wechat_request(request: Request, app_id: Optional[str], backgr
logger.exception("业务处理异常,将返回兜底文案")
answer_text = "系统繁忙,请稍后再试。"
# XML 回调场景需要主动 sendmsg 才能给用户展示消息
if str(data.get("_payload_format", "")) == "xml":
try:
await _send_kefu_msg_for_xml_callback(data)
except Exception:
logger.error("sendmsg 下发失败不影响回调200")
# 7.1 主回复确定后再后台触发 B 同步,避免影响回复链路耗时
background_tasks.add_task(_safe_sync_user_profile_from_b, data)
resp_obj = {
# 文本类型,内容为可被 JSON.parse 的 H5 结构字符串
"answer_type": "text",
"text_info": {
"short_answer": answer_text
if str(data.get("_payload_format", "")) == "xml":
# XML 回调按“回调直接回复”处理,不依赖 sendmsg 权限
resp_json_str = json.dumps(_build_xml_callback_reply(data, answer_text), ensure_ascii=False)
else:
resp_obj = {
# 文本类型,内容为可被 JSON.parse 的 H5 结构字符串
"answer_type": "text",
"text_info": {
"short_answer": answer_text
}
}
}
resp_json_str = json.dumps(resp_obj, ensure_ascii=False)
resp_json_str = json.dumps(resp_obj, ensure_ascii=False)
logger.info("回包明文(加密前): %s", resp_json_str)
# 8. 加密响应 JSON
# 8. 加密响应
# XML 回调场景使用当前会话 appid公众号 appid加密避免与开放 API appid 不一致导致平台解密失败。
response_app_id = APP_ID
if str(data.get("_payload_format", "")) == "xml":
response_app_id = str(data.get("ChannelId", "") or data.get("appid", "")).strip() or APP_ID
try:
encrypted_resp = aes_encrypt_base64(resp_json_str, ENCODING_AES_KEY, APP_ID)
encrypted_resp = aes_encrypt_base64(resp_json_str, ENCODING_AES_KEY, response_app_id)
except Exception as e:
logger.exception("响应 AES 加密失败")
raise HTTPException(status_code=500, detail="encrypt error") from e