's'
This commit is contained in:
@@ -14,6 +14,7 @@ from Crypto.Cipher import AES
|
||||
from fastapi import APIRouter, BackgroundTasks, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
|
||||
from ..services.chatbot_miniprogram_card import build_miniprogram_card_msg, normalize_miniprogrampage_msg
|
||||
from ..services.mp_recommend import is_mp_recommend_webhook_body, try_persist_from_like_body
|
||||
from ..services.wechat_private_user_pb import (
|
||||
contact_avatar_url,
|
||||
@@ -114,12 +115,14 @@ def aes_encrypt_base64(plaintext: str, encoding_aes_key: str, app_id: str | None
|
||||
|
||||
|
||||
class SharedChatbotReply(NamedTuple):
|
||||
"""全公众号配置共用的解析结果:要么 H5 图文,要么兜底纯文本(含超链)。"""
|
||||
"""全公众号配置共用的解析结果:H5 图文、小程序卡片,或兜底纯文本(含超链)。"""
|
||||
|
||||
h5_message: Optional[dict]
|
||||
"""与「发送客服消息」H5 一致:{\"news\": {\"articles\": [...]}};关键词为 Query 子串包含命中,未命中时为 None。"""
|
||||
text_fallback: str
|
||||
"""未命中关键词时的 short_answer 文案(可含 <a href>)。"""
|
||||
miniprogram_message: Optional[dict]
|
||||
"""与「发送客服消息」小程序卡片一致:{\"miniprogrampage\": {...}};未命中「小程序卡片」时为 None。"""
|
||||
|
||||
|
||||
def _normalize_h5_msg_per_wechat_doc(h5_message: dict) -> dict:
|
||||
@@ -155,6 +158,7 @@ def shared_build_h5_keyword_message(query: str, user_id: str, profile_key: str)
|
||||
|
||||
命中规则:对用户 Query 首尾 trim 后,**包含**配置词作为连续子串即命中(子串匹配),
|
||||
不要求用户整句与关键词全等。群卡片统一按「群」子串命中。
|
||||
「小程序卡片」在 shared_resolve_reply 中优先处理,不在此函数返回。
|
||||
"""
|
||||
q = (query or "").strip()
|
||||
uid = (user_id or "").strip()
|
||||
@@ -192,7 +196,7 @@ def shared_build_h5_keyword_message(query: str, user_id: str, profile_key: str)
|
||||
|
||||
def build_fallback_reply_plain_text(user_id: str) -> str:
|
||||
"""
|
||||
兜底回复(单独构建):未命中「电子书」「群」等关键词时使用。
|
||||
兜底回复(单独构建):未命中「小程序卡片」「电子书」「群」等关键词时使用。
|
||||
/chatbot、/hackrobot 共用同一文案与链接逻辑,仅解密密钥因 profile 不同。
|
||||
"""
|
||||
uid = (user_id or "").strip()
|
||||
@@ -201,13 +205,16 @@ def build_fallback_reply_plain_text(user_id: str) -> str:
|
||||
|
||||
|
||||
def shared_resolve_reply(data: dict, profile_key: str) -> SharedChatbotReply:
|
||||
"""根据解密后的 data 与 profile 决定 H5 或兜底;关键词按 Query 子串包含判断,无命中时走 build_fallback_reply_plain_text。"""
|
||||
"""根据解密后的 data 与 profile 决定小程序卡片、H5 或兜底;关键词按 Query 子串包含判断,无命中时走 build_fallback_reply_plain_text。"""
|
||||
user_id = str(data.get("UserId", "") or data.get("userId", "") or "").strip()
|
||||
query = str(data.get("Query", "") or "").strip()
|
||||
if "小程序卡片" in query:
|
||||
mp = build_miniprogram_card_msg(user_id=user_id)
|
||||
return SharedChatbotReply(None, "", mp)
|
||||
h5 = shared_build_h5_keyword_message(query, user_id, profile_key)
|
||||
if h5:
|
||||
return SharedChatbotReply(h5, "")
|
||||
return SharedChatbotReply(None, build_fallback_reply_plain_text(user_id))
|
||||
return SharedChatbotReply(h5, "", None)
|
||||
return SharedChatbotReply(None, build_fallback_reply_plain_text(user_id), None)
|
||||
|
||||
|
||||
def shared_build_openapi_text_answer(short_answer: str) -> dict:
|
||||
@@ -225,7 +232,8 @@ def _xml_channel_h5_envelope(data: dict, h5_message: dict) -> dict:
|
||||
|
||||
def _openapi_json_h5_answer_per_sendmsg_doc(msg: dict) -> dict:
|
||||
"""
|
||||
开放 API JSON 入站命中 H5:与文档「发送客服消息」一致,msg 为 { news: { articles: [...] } }。
|
||||
开放 API JSON 入站命中 H5 / 小程序卡片:与文档「发送客服消息」一致。
|
||||
msg 为 { news: { articles: [...] } } 或 { miniprogrampage: {...} }。
|
||||
第三方回调侧将该 msg 序列化为 JSON 字符串,放入 answer_type=text 的 text_info.short_answer
|
||||
(与客服接口里 msg 对象同源结构,由网关解析为卡片)。
|
||||
"""
|
||||
@@ -261,6 +269,13 @@ def shared_build_plain_body_for_wechat_callback(
|
||||
fmt = (payload_format or "").strip().lower()
|
||||
is_xml = fmt == "xml"
|
||||
|
||||
if reply.miniprogram_message:
|
||||
msg = normalize_miniprogrampage_msg(reply.miniprogram_message)
|
||||
if is_xml:
|
||||
return _xml_channel_h5_envelope(data, msg)
|
||||
body = _openapi_json_h5_answer_per_sendmsg_doc(msg)
|
||||
return _openapi_json_merge_request_echo(data, body)
|
||||
|
||||
if reply.h5_message:
|
||||
msg = _normalize_h5_msg_per_wechat_doc(reply.h5_message)
|
||||
if is_xml:
|
||||
|
||||
70
app/services/chatbot_miniprogram_card.py
Normal file
70
app/services/chatbot_miniprogram_card.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
微信开放平台「小程序卡片」客服消息 msg 结构(与发送客服消息文档一致)。
|
||||
|
||||
字段:title, appid, pagepath, thumb_media_id, thumb_url。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Mapping, Optional
|
||||
from urllib.parse import quote
|
||||
|
||||
# 默认值可通过环境变量覆盖,便于不同部署环境配置素材与小程序信息
|
||||
_DEFAULT_TITLE = (os.getenv("CHATBOT_MINIPROGRAM_TITLE") or "openai对话插件").strip()
|
||||
_DEFAULT_APPID = (os.getenv("CHATBOT_MINIPROGRAM_APPID") or "wx8c631f7e9f2465e1").strip()
|
||||
_DEFAULT_PAGEPATH = (os.getenv("CHATBOT_MINIPROGRAM_PAGEPATH") or "pages/index/index").strip()
|
||||
_DEFAULT_THUMB_MEDIA_ID = (
|
||||
os.getenv("CHATBOT_MINIPROGRAM_THUMB_MEDIA_ID") or "KegpipQG9t-klMo25My4e8zpBjhjg3JMrMSpgjikB4U"
|
||||
).strip()
|
||||
_DEFAULT_THUMB_URL = (
|
||||
os.getenv("CHATBOT_MINIPROGRAM_THUMB_URL")
|
||||
or "http://mmbiz.qpic.cn/mmbiz_png/W3gQtpV3j8BYhWgfHT5Hfg6auN94c2ec4BBhDOMtPQrx6vEMc1rR4iaDKxDLOfZ1jBUqIEEY4YpvEj6ktSyXT7g/0?wx_fmt=png"
|
||||
).strip()
|
||||
|
||||
|
||||
def normalize_miniprogrampage_msg(msg: Mapping[str, Any]) -> dict:
|
||||
"""仅保留文档约定的 miniprogrampage 五字段,避免多余键导致客户端不渲染。"""
|
||||
raw = msg.get("miniprogrampage") if isinstance(msg, dict) else None
|
||||
inner = raw if isinstance(raw, dict) else {}
|
||||
return {
|
||||
"miniprogrampage": {
|
||||
"title": str(inner.get("title", "")),
|
||||
"appid": str(inner.get("appid", "")),
|
||||
"pagepath": str(inner.get("pagepath", "")),
|
||||
"thumb_media_id": str(inner.get("thumb_media_id", "")),
|
||||
"thumb_url": str(inner.get("thumb_url", "")),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def build_miniprogram_card_msg(
|
||||
*,
|
||||
user_id: str = "",
|
||||
title: Optional[str] = None,
|
||||
appid: Optional[str] = None,
|
||||
pagepath: Optional[str] = None,
|
||||
thumb_media_id: Optional[str] = None,
|
||||
thumb_url: Optional[str] = None,
|
||||
append_userid_to_pagepath: bool = True,
|
||||
) -> dict:
|
||||
"""
|
||||
返回完整客服 msg:{\"miniprogrampage\": {...}}。
|
||||
|
||||
若 append_userid_to_pagepath 且 user_id 非空,在 pagepath 上追加 ?userid=(已有查询串则用 &)。
|
||||
"""
|
||||
uid = (user_id or "").strip()
|
||||
base_path = (pagepath if pagepath is not None else _DEFAULT_PAGEPATH).strip()
|
||||
if append_userid_to_pagepath and uid:
|
||||
sep = "&" if "?" in base_path else "?"
|
||||
base_path = f"{base_path}{sep}userid={quote(uid, safe='')}"
|
||||
|
||||
return {
|
||||
"miniprogrampage": {
|
||||
"title": str(title if title is not None else _DEFAULT_TITLE),
|
||||
"appid": str(appid if appid is not None else _DEFAULT_APPID),
|
||||
"pagepath": base_path,
|
||||
"thumb_media_id": str(thumb_media_id if thumb_media_id is not None else _DEFAULT_THUMB_MEDIA_ID),
|
||||
"thumb_url": str(thumb_url if thumb_url is not None else _DEFAULT_THUMB_URL),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user