;'s'
This commit is contained in:
102
main.py
102
main.py
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user