's'
This commit is contained in:
@@ -31,6 +31,7 @@ from .routers import (
|
||||
common_router,
|
||||
meetup_router,
|
||||
chatbot_router,
|
||||
hackrobot_router,
|
||||
)
|
||||
from .routers.salon_meetup import router as salon_meetup_router
|
||||
|
||||
@@ -177,6 +178,7 @@ app.include_router(common_router)
|
||||
app.include_router(meetup_router)
|
||||
app.include_router(salon_meetup_router)
|
||||
app.include_router(chatbot_router)
|
||||
app.include_router(hackrobot_router)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
||||
@@ -13,7 +13,7 @@ from .nomadlms import router as nomadlms_router
|
||||
from .legacy import router as legacy_router
|
||||
from .common import router as common_router
|
||||
from .meetup import router as meetup_router
|
||||
from .chatbot import router as chatbot_router
|
||||
from .chatbot import hackrobot_router, router as chatbot_router
|
||||
|
||||
__all__ = [
|
||||
"nomadvip_router",
|
||||
@@ -24,4 +24,5 @@ __all__ = [
|
||||
"common_router",
|
||||
"meetup_router",
|
||||
"chatbot_router",
|
||||
"hackrobot_router",
|
||||
]
|
||||
|
||||
@@ -7,8 +7,8 @@ import struct
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from urllib.parse import parse_qs
|
||||
from typing import NamedTuple, Optional
|
||||
from urllib.parse import parse_qs, quote
|
||||
|
||||
import httpx
|
||||
from Crypto.Cipher import AES
|
||||
@@ -18,7 +18,7 @@ from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse
|
||||
from ..config import PB_ADMIN_EMAIL, PB_ADMIN_PASSWORD, PB_URL
|
||||
from ..services.mp_recommend import is_mp_recommend_webhook_body, try_persist_from_like_body
|
||||
|
||||
router = APIRouter(prefix="/chatbot", tags=["chatbot"])
|
||||
router = APIRouter(tags=["chatbot"])
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_processed_callback_cache: dict[str, int] = {}
|
||||
@@ -45,6 +45,17 @@ APP_ID = OPEN_API_PROFILES[ACTIVE_PROFILE]["APP_ID"]
|
||||
TOKEN = OPEN_API_PROFILES[ACTIVE_PROFILE]["TOKEN"]
|
||||
ENCODING_AES_KEY = OPEN_API_PROFILES[ACTIVE_PROFILE]["ENCODING_AES_KEY"]
|
||||
|
||||
HACKROBOT_PROFILE = "xidu"
|
||||
|
||||
|
||||
def _resolve_profile_key(profile_key: str) -> str:
|
||||
k = (profile_key or "").strip().lower()
|
||||
return k if k in OPEN_API_PROFILES else "eric"
|
||||
|
||||
|
||||
def _profile_creds(profile_key: str) -> dict:
|
||||
return OPEN_API_PROFILES[_resolve_profile_key(profile_key)]
|
||||
|
||||
PM_SYNC_AUTHTOKEN = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyaWQiOjE5MzE1MjcsIm9wZW5pZCI6Im85VS04NW1DSXZ0eDZ0a0pteTQyMkkzcUNHVWciLCJzaWduZXRpbWUiOjE3NzQ5MzM0NDQ4MTgsImlhdCI6MTc3NDkzMzQ0NCwiZXhwIjoxNzc3NTI1NDQ0fQ.iC9vdjBqa2LGlqA6F-Dg4GcfDyv3Rp8JwpBaO3j_eE4"
|
||||
PM_SYNC_DEFAULT_WXBOT_BID = "75021053883a9090c67d5becde7f4b0a"
|
||||
PB_COLLECTION = "wechat_private_users"
|
||||
@@ -106,8 +117,48 @@ def aes_encrypt_base64(plaintext: str, encoding_aes_key: str, app_id: str | None
|
||||
return base64.b64encode(cipher.encrypt(padded)).decode("utf-8")
|
||||
|
||||
|
||||
def _build_keyword_h5_obj(query: str, user_id: str) -> Optional[dict]:
|
||||
class SharedChatbotReply(NamedTuple):
|
||||
"""全公众号配置共用的解析结果:要么 H5 图文,要么兜底纯文本(含超链)。"""
|
||||
|
||||
h5_message: Optional[dict]
|
||||
"""与「发送客服消息」H5 一致:{\"news\": {\"articles\": [...]}},未命中关键词时为 None。"""
|
||||
text_fallback: str
|
||||
"""未命中关键词时的 short_answer 文案(可含 <a href>)。"""
|
||||
|
||||
|
||||
def _normalize_h5_msg_per_wechat_doc(h5_message: dict) -> dict:
|
||||
"""
|
||||
与文档「发送客服消息」H5 一致:msg = { news: { articles: [ { title, description, url, picurl, type } ] } }。
|
||||
仅保留这五个字段,避免多余键导致客户端不渲染。
|
||||
"""
|
||||
raw = h5_message.get("news") if isinstance(h5_message, dict) else None
|
||||
arts = raw.get("articles") if isinstance(raw, dict) else None
|
||||
if not isinstance(arts, list):
|
||||
arts = []
|
||||
norm_articles: list[dict] = []
|
||||
for a in arts:
|
||||
if not isinstance(a, dict):
|
||||
continue
|
||||
t = str(a.get("type", "h5") or "h5").strip().lower()
|
||||
if t not in ("h5", "mp"):
|
||||
t = "h5"
|
||||
norm_articles.append(
|
||||
{
|
||||
"title": str(a.get("title", "")),
|
||||
"description": str(a.get("description", "")),
|
||||
"url": str(a.get("url", "")),
|
||||
"picurl": str(a.get("picurl", "")),
|
||||
"type": t,
|
||||
}
|
||||
)
|
||||
return {"news": {"articles": norm_articles}}
|
||||
|
||||
|
||||
def shared_build_h5_keyword_message(query: str, user_id: str, profile_key: str) -> Optional[dict]:
|
||||
"""关键词命中时返回文档约定的 H5 msg(再经 _normalize 输出)。异度(/hackrobot) 群卡片仅认「群」;Eric(/chatbot) 另认「加群」。"""
|
||||
q = (query or "").strip()
|
||||
uid = (user_id or "").strip()
|
||||
prof = _resolve_profile_key(profile_key)
|
||||
if "电子书" in q:
|
||||
return {
|
||||
"news": {
|
||||
@@ -115,21 +166,26 @@ def _build_keyword_h5_obj(query: str, user_id: str) -> Optional[dict]:
|
||||
{
|
||||
"title": "数字游民",
|
||||
"description": "地理套利与自动化杠杆",
|
||||
"url": f"https://vip.hackrobot.cn/ebook?userid={user_id}",
|
||||
"url": f"https://vip.hackrobot.cn/ebook?userid={uid}",
|
||||
"picurl": "https://www.hackrobot.cn/static/images/images/dgnomad.png",
|
||||
"type": "h5",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
if "群" in q:
|
||||
if prof == HACKROBOT_PROFILE:
|
||||
# 「加群」含「群」子串,异度仅按「群」触发时需排除「加群」整词
|
||||
group_hit = ("群" in q) and ("加群" not in q)
|
||||
else:
|
||||
group_hit = ("群" in q) or ("加群" in q)
|
||||
if group_hit:
|
||||
return {
|
||||
"news": {
|
||||
"articles": [
|
||||
{
|
||||
"title": "异度星球",
|
||||
"description": "电子书、视频教程、私密社群",
|
||||
"url": f"https://vip.hackrobot.cn?userid={user_id}",
|
||||
"url": f"https://vip.hackrobot.cn?userid={uid}",
|
||||
"picurl": "https://www.hackrobot.cn/static/images/images/dgnomad.png",
|
||||
"type": "h5",
|
||||
}
|
||||
@@ -139,6 +195,102 @@ def _build_keyword_h5_obj(query: str, user_id: str) -> Optional[dict]:
|
||||
return None
|
||||
|
||||
|
||||
def build_fallback_reply_plain_text(user_id: str) -> str:
|
||||
"""
|
||||
兜底回复(单独构建):未命中「电子书」「群」等关键词时使用。
|
||||
/chatbot、/hackrobot 共用同一文案与链接逻辑,仅解密密钥因 profile 不同。
|
||||
"""
|
||||
uid = (user_id or "").strip()
|
||||
href = f"https://vip.hackrobot.cn/?userid={quote(uid, safe='')}" if uid else "https://vip.hackrobot.cn/"
|
||||
return "🤖消息已收到,稍晚看到后回复\n参与交流,👉可<a href=\"" + href + "\">加群</a>"
|
||||
|
||||
|
||||
def shared_resolve_reply(data: dict, profile_key: str) -> SharedChatbotReply:
|
||||
"""根据解密后的 data 与 profile 决定 H5 或兜底;无关键词时一律走 build_fallback_reply_plain_text。"""
|
||||
user_id = str(data.get("UserId", "") or data.get("userId", "") or "").strip()
|
||||
query = str(data.get("Query", "") or "").strip()
|
||||
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))
|
||||
|
||||
|
||||
def shared_build_openapi_text_answer(short_answer: str) -> dict:
|
||||
"""开放 API 文档「输出」:文本 / 含超链兜底。"""
|
||||
return {"answer_type": "text", "text_info": {"short_answer": short_answer}}
|
||||
|
||||
|
||||
def _xml_channel_h5_envelope(data: dict, h5_message: dict) -> dict:
|
||||
"""XML 入站(如 Eric /chatbot)命中关键词:userid 信封 + content.news,与线上一致。"""
|
||||
user_id = str(data.get("UserId", "") or data.get("userId", "") or "").strip()
|
||||
appid = str(data.get("ChannelId", "") or data.get("appid", "")).strip()
|
||||
channel = int(str(data.get("channel", "0") or "0"))
|
||||
return {"userid": user_id, "appid": appid, "content": h5_message, "channel": channel, "from": 1}
|
||||
|
||||
|
||||
def _openapi_json_h5_answer_per_sendmsg_doc(msg: dict) -> dict:
|
||||
"""
|
||||
开放 API JSON 入站命中 H5:与文档「发送客服消息」一致,msg 为 { news: { articles: [...] } }。
|
||||
第三方回调侧将该 msg 序列化为 JSON 字符串,放入 answer_type=text 的 text_info.short_answer
|
||||
(与客服接口里 msg 对象同源结构,由网关解析为卡片)。
|
||||
"""
|
||||
compact = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
|
||||
return {"answer_type": "text", "text_info": {"short_answer": compact}}
|
||||
|
||||
|
||||
def _openapi_json_merge_request_echo(data: dict, body: dict) -> dict:
|
||||
"""回传 RequestId,便于开放平台把应答与请求关联(避免未命中/空白展示)。"""
|
||||
rid = data.get("RequestId")
|
||||
if rid is None:
|
||||
return body
|
||||
s = str(rid).strip()
|
||||
if not s:
|
||||
return body
|
||||
out = dict(body)
|
||||
out["RequestId"] = rid
|
||||
return out
|
||||
|
||||
|
||||
def shared_build_plain_body_for_wechat_callback(
|
||||
*,
|
||||
payload_format: str,
|
||||
reply: SharedChatbotReply,
|
||||
data: dict,
|
||||
) -> dict:
|
||||
"""
|
||||
回调加密前的明文 JSON 统一入口(关键词 H5 与兜底共用一套解析结果 reply)。
|
||||
|
||||
- /chatbot 多为 XML 入站:H5 用 userid 信封;兜底改用 answer_type=text(信封里 content.msg 对超链异常)。
|
||||
- /hackrobot 多为 JSON 入站:兜底用 answer_type=text;H5 用同结构的 msg JSON 串作为 short_answer(见发送客服消息文档)。
|
||||
"""
|
||||
fmt = (payload_format or "").strip().lower()
|
||||
is_xml = fmt == "xml"
|
||||
|
||||
if reply.h5_message:
|
||||
msg = _normalize_h5_msg_per_wechat_doc(reply.h5_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)
|
||||
|
||||
body = shared_build_openapi_text_answer(reply.text_fallback)
|
||||
if not is_xml:
|
||||
body = _openapi_json_merge_request_echo(data, body)
|
||||
return body
|
||||
|
||||
|
||||
def _ensure_user_id_from_session(data: dict) -> None:
|
||||
"""开放 API JSON 可能只带 SessionId(如 0_xxx|openid),补全 UserId 供关键词链接与兜底文案使用。"""
|
||||
uid = str(data.get("UserId", "") or data.get("userId", "") or "").strip()
|
||||
if uid:
|
||||
return
|
||||
sid = str(data.get("SessionId", "") or "").strip()
|
||||
if "|" in sid:
|
||||
tail = sid.rsplit("|", 1)[-1].strip()
|
||||
if tail:
|
||||
data["UserId"] = tail
|
||||
|
||||
|
||||
def _parse_decrypted_payload(decrypted_text: str) -> dict:
|
||||
text = (decrypted_text or "").strip()
|
||||
# 兼容 BOM / NUL / 控制字符前缀
|
||||
@@ -217,15 +369,6 @@ def _parse_decrypted_payload(decrypted_text: str) -> dict:
|
||||
raise ValueError("unsupported payload format")
|
||||
|
||||
|
||||
def _build_xml_callback_reply(data: dict, answer_text: str) -> dict:
|
||||
user_id = 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(str(data.get("Query", "")), user_id)
|
||||
content = msg if msg else {"msg": answer_text}
|
||||
return {"userid": user_id, "appid": appid, "content": content, "channel": channel, "from": 1}
|
||||
|
||||
|
||||
def _safe_filter_value(value: str) -> str:
|
||||
return (value or "").replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
@@ -241,12 +384,12 @@ def _build_callback_dedupe_key(data: dict) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _is_duplicate_callback(data: dict, ttl_seconds: int = 3600) -> bool:
|
||||
def _is_duplicate_callback(data: dict, profile_key: str, ttl_seconds: int = 3600) -> bool:
|
||||
now_ts = int(time.time())
|
||||
for k, exp in list(_processed_callback_cache.items()):
|
||||
if exp <= now_ts:
|
||||
_processed_callback_cache.pop(k, None)
|
||||
key = _build_callback_dedupe_key(data)
|
||||
key = _resolve_profile_key(profile_key) + "|" + _build_callback_dedupe_key(data)
|
||||
if not key.strip("|"):
|
||||
return False
|
||||
if key in _processed_callback_cache:
|
||||
@@ -326,13 +469,13 @@ async def _pb_upsert_user_profile(record: dict) -> None:
|
||||
await client.post(f"{pb_url}/api/collections/{PB_COLLECTION}/records", json=record, headers=headers)
|
||||
|
||||
|
||||
async def _sync_user_profile_from_b(data: dict) -> None:
|
||||
async def _sync_user_profile_from_b(data: dict, profile_key: str) -> None:
|
||||
authtoken = (PM_SYNC_AUTHTOKEN or "").strip()
|
||||
wxbot_bid = (PM_SYNC_DEFAULT_WXBOT_BID or "").strip()
|
||||
if not authtoken or not wxbot_bid:
|
||||
return
|
||||
target_userid = str(data.get("UserId", "")).strip()
|
||||
if not target_userid or _is_duplicate_callback(data):
|
||||
if not target_userid or _is_duplicate_callback(data, profile_key):
|
||||
return
|
||||
result = await query_private_messages(
|
||||
authtoken=authtoken,
|
||||
@@ -364,25 +507,30 @@ async def _sync_user_profile_from_b(data: dict) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def _safe_sync_user_profile_from_b(data: dict) -> None:
|
||||
async def _safe_sync_user_profile_from_b(data: dict, profile_key: str) -> None:
|
||||
try:
|
||||
await _sync_user_profile_from_b(data)
|
||||
await _sync_user_profile_from_b(data, profile_key)
|
||||
except Exception:
|
||||
logger.error("chatbot后台同步失败(已忽略)")
|
||||
|
||||
|
||||
async def handle_business(data: dict) -> str:
|
||||
user_id = str(data.get("UserId", "")).strip()
|
||||
query = str(data.get("Query", "")).strip()
|
||||
msg = _build_keyword_h5_obj(query, user_id)
|
||||
if msg:
|
||||
return json.dumps(msg, ensure_ascii=False)
|
||||
return ""
|
||||
|
||||
|
||||
async def _handle_wechat_request(request: Request, app_id: Optional[str], background_tasks: BackgroundTasks) -> str:
|
||||
if app_id and app_id != APP_ID:
|
||||
logger.warning("chatbot app_id mismatch expected=%s actual=%s", APP_ID, app_id)
|
||||
async def _handle_wechat_request(
|
||||
request: Request,
|
||||
app_id: Optional[str],
|
||||
background_tasks: BackgroundTasks,
|
||||
*,
|
||||
profile_key: str,
|
||||
) -> str:
|
||||
creds = _profile_creds(profile_key)
|
||||
expected_app_id = creds["APP_ID"]
|
||||
encoding_key = creds["ENCODING_AES_KEY"]
|
||||
if app_id and app_id != expected_app_id:
|
||||
logger.warning(
|
||||
"chatbot app_id mismatch profile=%s expected=%s actual=%s",
|
||||
profile_key,
|
||||
expected_app_id,
|
||||
app_id,
|
||||
)
|
||||
raw_body = (await request.body()).decode("utf-8", errors="ignore").strip()
|
||||
if not raw_body:
|
||||
raise HTTPException(status_code=400, detail="empty body")
|
||||
@@ -410,15 +558,18 @@ async def _handle_wechat_request(request: Request, app_id: Optional[str], backgr
|
||||
|
||||
cipher_b64 = "".join(str(cipher_b64).split()).strip()
|
||||
try:
|
||||
decrypted = aes_decrypt_base64(cipher_b64, ENCODING_AES_KEY)
|
||||
decrypted = aes_decrypt_base64(cipher_b64, encoding_key)
|
||||
logger.info(
|
||||
"chatbot decrypted preview app_id=%s preview=%s",
|
||||
"chatbot decrypted preview profile=%s app_id=%s preview=%s",
|
||||
profile_key,
|
||||
app_id or "",
|
||||
(decrypted or "")[:200],
|
||||
)
|
||||
data = _parse_decrypted_payload(decrypted)
|
||||
_ensure_user_id_from_session(data)
|
||||
logger.info(
|
||||
"chatbot payload parsed app_id=%s format=%s user=%s query=%s",
|
||||
"chatbot payload parsed profile=%s app_id=%s format=%s user=%s query=%s",
|
||||
profile_key,
|
||||
app_id or "",
|
||||
data.get("_payload_format", ""),
|
||||
str(data.get("UserId", "") or data.get("userId", ""))[:80],
|
||||
@@ -426,7 +577,8 @@ async def _handle_wechat_request(request: Request, app_id: Optional[str], backgr
|
||||
)
|
||||
except Exception as error:
|
||||
logger.error(
|
||||
"chatbot decrypt failed app_id=%s content_type=%s body_prefix=%s err=%s",
|
||||
"chatbot decrypt failed profile=%s app_id=%s content_type=%s body_prefix=%s err=%s",
|
||||
profile_key,
|
||||
app_id or "",
|
||||
request.headers.get("content-type", ""),
|
||||
raw_body[:160],
|
||||
@@ -434,35 +586,67 @@ async def _handle_wechat_request(request: Request, app_id: Optional[str], backgr
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="decrypt error")
|
||||
|
||||
answer_text = await handle_business(data)
|
||||
background_tasks.add_task(_safe_sync_user_profile_from_b, data)
|
||||
reply = shared_resolve_reply(data, profile_key)
|
||||
background_tasks.add_task(_safe_sync_user_profile_from_b, data, profile_key)
|
||||
|
||||
if str(data.get("_payload_format", "")) == "xml":
|
||||
resp_plain = json.dumps(_build_xml_callback_reply(data, answer_text), ensure_ascii=False)
|
||||
response_app_id = str(data.get("ChannelId", "") or data.get("appid", "")).strip() or APP_ID
|
||||
fmt = str(data.get("_payload_format", "") or "")
|
||||
plain_obj = shared_build_plain_body_for_wechat_callback(payload_format=fmt, reply=reply, data=data)
|
||||
resp_plain = json.dumps(plain_obj, ensure_ascii=False)
|
||||
logger.info(
|
||||
"chatbot reply_plain profile=%s fmt=%s shape=%s",
|
||||
profile_key,
|
||||
fmt,
|
||||
list(plain_obj.keys()),
|
||||
)
|
||||
|
||||
if fmt == "xml":
|
||||
response_app_id = str(data.get("ChannelId", "") or data.get("appid", "")).strip() or expected_app_id
|
||||
else:
|
||||
resp_plain = json.dumps({"answer_type": "text", "text_info": {"short_answer": answer_text}}, ensure_ascii=False)
|
||||
# 与原始 chatbot 服务保持一致:thirdapi(URL 带 app_id)回包使用纯 AES,不拼 app_id
|
||||
response_app_id = None if app_id else APP_ID
|
||||
response_app_id = None if app_id else expected_app_id
|
||||
|
||||
return aes_encrypt_base64(resp_plain, ENCODING_AES_KEY, response_app_id)
|
||||
return aes_encrypt_base64(resp_plain, encoding_key, response_app_id)
|
||||
|
||||
|
||||
@router.post("/", response_class=PlainTextResponse)
|
||||
@router.post("/chatbot", response_class=PlainTextResponse)
|
||||
@router.post("/chatbot/", response_class=PlainTextResponse)
|
||||
async def chatbot_root(request: Request, background_tasks: BackgroundTasks, app_id: Optional[str] = None):
|
||||
return await _handle_wechat_request(request, app_id, background_tasks)
|
||||
return await _handle_wechat_request(request, app_id, background_tasks, profile_key=ACTIVE_PROFILE)
|
||||
|
||||
|
||||
@router.post("/wechat/thirdapi", response_class=PlainTextResponse)
|
||||
@router.post("/chatbot/wechat/thirdapi", response_class=PlainTextResponse)
|
||||
@router.post("/chatbot/wechat/thirdapi/", response_class=PlainTextResponse)
|
||||
async def chatbot_thirdapi(request: Request, background_tasks: BackgroundTasks, app_id: Optional[str] = None):
|
||||
return await _handle_wechat_request(request, app_id, background_tasks)
|
||||
return await _handle_wechat_request(request, app_id, background_tasks, profile_key=ACTIVE_PROFILE)
|
||||
|
||||
|
||||
@router.get("/health")
|
||||
@router.get("/chatbot/health")
|
||||
@router.get("/chatbot/health/")
|
||||
def chatbot_health():
|
||||
return {"status": "ok", "module": "chatbot-clone", "profile": ACTIVE_PROFILE}
|
||||
|
||||
|
||||
hackrobot_router = APIRouter(tags=["hackrobot"])
|
||||
|
||||
|
||||
@hackrobot_router.post("/hackrobot", response_class=PlainTextResponse)
|
||||
@hackrobot_router.post("/hackrobot/", response_class=PlainTextResponse)
|
||||
async def hackrobot_root(request: Request, background_tasks: BackgroundTasks, app_id: Optional[str] = None):
|
||||
return await _handle_wechat_request(request, app_id, background_tasks, profile_key=HACKROBOT_PROFILE)
|
||||
|
||||
|
||||
@hackrobot_router.post("/hackrobot/wechat/thirdapi", response_class=PlainTextResponse)
|
||||
@hackrobot_router.post("/hackrobot/wechat/thirdapi/", response_class=PlainTextResponse)
|
||||
async def hackrobot_thirdapi(request: Request, background_tasks: BackgroundTasks, app_id: Optional[str] = None):
|
||||
return await _handle_wechat_request(request, app_id, background_tasks, profile_key=HACKROBOT_PROFILE)
|
||||
|
||||
|
||||
@hackrobot_router.get("/hackrobot/health")
|
||||
@hackrobot_router.get("/hackrobot/health/")
|
||||
def hackrobot_health():
|
||||
return {"status": "ok", "module": "chatbot-clone", "profile": HACKROBOT_PROFILE}
|
||||
|
||||
|
||||
def _log_like_incoming(request: Request, body: bytes) -> None:
|
||||
"""SmsForwarder Webhook 调试:打印完整入参(公众号助手等通知转发)。"""
|
||||
client_host = request.client.host if request.client else None
|
||||
@@ -514,7 +698,8 @@ def _client_ip_for_like(request: Request) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
@router.post("/like")
|
||||
@router.post("/chatbot/like")
|
||||
@hackrobot_router.post("/hackrobot/like")
|
||||
async def like_webhook_sink(request: Request):
|
||||
"""接收 SmsForwarder 等 Webhook POST,详细打日志;公众号助手通知写入 PocketBase。"""
|
||||
body = await request.body()
|
||||
@@ -538,7 +723,8 @@ async def like_webhook_sink(request: Request):
|
||||
return JSONResponse(status_code=200, content=payload)
|
||||
|
||||
|
||||
@router.post("/api/private-message/query")
|
||||
@router.post("/chatbot/api/private-message/query")
|
||||
@hackrobot_router.post("/hackrobot/api/private-message/query")
|
||||
async def chatbot_private_message_query(payload: dict):
|
||||
authtoken = str(payload.get("authtoken", "")).strip()
|
||||
wxbot_bid = str(payload.get("wxbot_bid", "")).strip()
|
||||
@@ -570,7 +756,8 @@ async def chatbot_private_message_query(payload: dict):
|
||||
}
|
||||
|
||||
|
||||
@router.post("/api/private-message/analyze")
|
||||
@router.post("/chatbot/api/private-message/analyze")
|
||||
@hackrobot_router.post("/hackrobot/api/private-message/analyze")
|
||||
async def chatbot_private_message_analyze(payload: dict):
|
||||
authtoken = str(payload.get("authtoken", "")).strip()
|
||||
if not authtoken:
|
||||
@@ -605,9 +792,8 @@ async def chatbot_private_message_analyze(payload: dict):
|
||||
}
|
||||
|
||||
|
||||
@router.get("/private-message", response_class=HTMLResponse)
|
||||
def chatbot_private_message_page():
|
||||
return """
|
||||
def _private_message_debug_html(api_prefix: str) -> str:
|
||||
base = """
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
@@ -718,7 +904,7 @@ def chatbot_private_message_page():
|
||||
page: Number($("page").value || 0),
|
||||
size: Number($("size").value || 30),
|
||||
};
|
||||
const res = await fetch("/chatbot/api/private-message/query", {
|
||||
const res = await fetch("__API_PREFIX__/api/private-message/query", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -737,7 +923,7 @@ def chatbot_private_message_page():
|
||||
try{
|
||||
setStatus("分析中...");
|
||||
const payload = { authtoken: $("authtoken").value.trim() };
|
||||
const res = await fetch("/chatbot/api/private-message/analyze", {
|
||||
const res = await fetch("__API_PREFIX__/api/private-message/analyze", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
@@ -757,4 +943,14 @@ def chatbot_private_message_page():
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
return base.replace("__API_PREFIX__", (api_prefix or "").rstrip("/"))
|
||||
|
||||
|
||||
@router.get("/chatbot/private-message")
|
||||
@router.get("/chatbot/private-message/")
|
||||
@hackrobot_router.get("/hackrobot/private-message")
|
||||
@hackrobot_router.get("/hackrobot/private-message/")
|
||||
def chatbot_private_message_page(request: Request):
|
||||
api_prefix = "/hackrobot" if str(request.url.path).startswith("/hackrobot") else "/chatbot"
|
||||
return HTMLResponse(_private_message_debug_html(api_prefix))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user