's'
This commit is contained in:
@@ -133,6 +133,29 @@ def _build_keyword_h5_obj(query: str, user_id: str) -> Optional[dict]:
|
|||||||
|
|
||||||
def _parse_decrypted_payload(decrypted_text: str) -> dict:
|
def _parse_decrypted_payload(decrypted_text: str) -> dict:
|
||||||
text = (decrypted_text or "").strip()
|
text = (decrypted_text or "").strip()
|
||||||
|
# 兼容 BOM / NUL / 控制字符前缀
|
||||||
|
text = text.lstrip("\ufeff\x00\r\n\t ")
|
||||||
|
if not text:
|
||||||
|
raise ValueError("empty decrypted payload")
|
||||||
|
|
||||||
|
# 某些上游会把 JSON 再包一层字符串:"{\"UserId\":\"...\"}"
|
||||||
|
if text.startswith('"') and text.endswith('"'):
|
||||||
|
try:
|
||||||
|
unwrapped = json.loads(text)
|
||||||
|
if isinstance(unwrapped, str):
|
||||||
|
text = unwrapped.strip().lstrip("\ufeff\x00\r\n\t ")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 若前面夹杂少量杂字符,尝试定位第一个结构起始位
|
||||||
|
first_json = text.find("{")
|
||||||
|
first_xml = text.find("<")
|
||||||
|
starts = [x for x in [first_json, first_xml] if x >= 0]
|
||||||
|
if starts:
|
||||||
|
first = min(starts)
|
||||||
|
if first > 0:
|
||||||
|
text = text[first:]
|
||||||
|
|
||||||
if text.startswith("{"):
|
if text.startswith("{"):
|
||||||
obj = json.loads(text)
|
obj = json.loads(text)
|
||||||
obj["_payload_format"] = "json"
|
obj["_payload_format"] = "json"
|
||||||
@@ -161,6 +184,28 @@ def _parse_decrypted_payload(decrypted_text: str) -> dict:
|
|||||||
"from": get_text("from", "0"),
|
"from": get_text("from", "0"),
|
||||||
"_payload_format": "xml",
|
"_payload_format": "xml",
|
||||||
}
|
}
|
||||||
|
# 兼容 querystring 明文:UserId=xxx&Query=你好&appid=...
|
||||||
|
if "=" in text and "&" in text:
|
||||||
|
qs = parse_qs(text, keep_blank_values=True)
|
||||||
|
|
||||||
|
def qv(*keys: str) -> str:
|
||||||
|
for key in keys:
|
||||||
|
vals = qs.get(key) or qs.get(key.lower()) or qs.get(key.upper()) or []
|
||||||
|
if vals and str(vals[0]).strip():
|
||||||
|
return str(vals[0]).strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"UserId": qv("UserId", "userid", "userId", "from_user"),
|
||||||
|
"Query": qv("Query", "query", "msg", "content"),
|
||||||
|
"Timestamp": qv("Timestamp", "timestamp", "createTime", "msg_time"),
|
||||||
|
"MsgId": qv("MsgId", "msgId", "msgid"),
|
||||||
|
"ChannelId": qv("ChannelId", "appid", "appId"),
|
||||||
|
"appid": qv("appid", "appId"),
|
||||||
|
"channel": qv("channel"),
|
||||||
|
"from": qv("from"),
|
||||||
|
"_payload_format": "querystring",
|
||||||
|
}
|
||||||
raise ValueError("unsupported payload format")
|
raise ValueError("unsupported payload format")
|
||||||
|
|
||||||
|
|
||||||
@@ -358,7 +403,19 @@ async def _handle_wechat_request(request: Request, app_id: Optional[str], backgr
|
|||||||
cipher_b64 = "".join(str(cipher_b64).split()).strip()
|
cipher_b64 = "".join(str(cipher_b64).split()).strip()
|
||||||
try:
|
try:
|
||||||
decrypted = aes_decrypt_base64(cipher_b64, ENCODING_AES_KEY)
|
decrypted = aes_decrypt_base64(cipher_b64, ENCODING_AES_KEY)
|
||||||
|
logger.info(
|
||||||
|
"chatbot decrypted preview app_id=%s preview=%s",
|
||||||
|
app_id or "",
|
||||||
|
(decrypted or "")[:200],
|
||||||
|
)
|
||||||
data = _parse_decrypted_payload(decrypted)
|
data = _parse_decrypted_payload(decrypted)
|
||||||
|
logger.info(
|
||||||
|
"chatbot payload parsed app_id=%s format=%s user=%s query=%s",
|
||||||
|
app_id or "",
|
||||||
|
data.get("_payload_format", ""),
|
||||||
|
str(data.get("UserId", "") or data.get("userId", ""))[:80],
|
||||||
|
str(data.get("Query", ""))[:80],
|
||||||
|
)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
logger.error(
|
logger.error(
|
||||||
"chatbot decrypt failed app_id=%s content_type=%s body_prefix=%s err=%s",
|
"chatbot decrypt failed app_id=%s content_type=%s body_prefix=%s err=%s",
|
||||||
@@ -467,5 +524,129 @@ async def chatbot_private_message_analyze(payload: dict):
|
|||||||
|
|
||||||
@router.get("/private-message", response_class=HTMLResponse)
|
@router.get("/private-message", response_class=HTMLResponse)
|
||||||
def chatbot_private_message_page():
|
def chatbot_private_message_page():
|
||||||
return "<html><body><h3>chatbot clone ready</h3><p>请使用 /chatbot/api/private-message/query 调试接口</p></body></html>"
|
return """
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Chatbot 私信调试台</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif; margin: 0; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
||||||
|
.wrap { max-width: 980px; margin: 0 auto; display: grid; gap: 12px; }
|
||||||
|
.card { background: #111827; border: 1px solid #334155; border-radius: 10px; padding: 14px; }
|
||||||
|
h2 { margin: 0 0 8px; }
|
||||||
|
label { display:block; margin: 8px 0 6px; font-size: 13px; color: #cbd5e1; }
|
||||||
|
input, textarea { width: 100%; border-radius: 8px; border: 1px solid #475569; background: #0b1220; color: #e2e8f0; padding: 10px; box-sizing: border-box; }
|
||||||
|
textarea { min-height: 88px; }
|
||||||
|
.row { display:grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||||
|
.btns { display:flex; gap:10px; margin-top: 10px; }
|
||||||
|
button { border: 0; background: #2563eb; color: white; border-radius: 8px; padding: 10px 14px; cursor: pointer; }
|
||||||
|
button.alt { background: #4f46e5; }
|
||||||
|
#status { font-size: 13px; color: #93c5fd; }
|
||||||
|
#users { display:grid; grid-template-columns: repeat(auto-fill,minmax(250px,1fr)); gap: 8px; }
|
||||||
|
.u { border:1px solid #334155; border-radius:8px; padding:8px; background:#0b1220; }
|
||||||
|
.uid { color:#93c5fd; font-size:12px; word-break:break-all; }
|
||||||
|
pre { background:#020617; border:1px solid #334155; border-radius:8px; padding:10px; overflow:auto; max-height:420px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<section class="card">
|
||||||
|
<h2>Chatbot 私信调试台</h2>
|
||||||
|
<div id="status">准备就绪</div>
|
||||||
|
<label>authtoken</label>
|
||||||
|
<textarea id="authtoken" placeholder="粘贴抓包 authtoken"></textarea>
|
||||||
|
<label>wxbot_bid</label>
|
||||||
|
<input id="wxbot_bid" placeholder="例如: 75021053883a9090c67d5becde7f4b0a" />
|
||||||
|
<div class="row">
|
||||||
|
<div>
|
||||||
|
<label>page</label>
|
||||||
|
<input id="page" value="0" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>size</label>
|
||||||
|
<input id="size" value="30" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<button onclick="queryPm()">查询私信</button>
|
||||||
|
<button class="alt" onclick="analyzeToken()">分析 Token</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h3 style="margin:0 0 8px;">用户列表</h3>
|
||||||
|
<div id="users"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h3 style="margin:0 0 8px;">原始结果</h3>
|
||||||
|
<pre id="out">{}</pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
function setStatus(s){ $("status").textContent = s; }
|
||||||
|
function renderJson(v){ $("out").textContent = JSON.stringify(v, null, 2); }
|
||||||
|
function renderUsers(users){
|
||||||
|
const box = $("users");
|
||||||
|
if(!Array.isArray(users) || !users.length){
|
||||||
|
box.innerHTML = '<div class="u">暂无数据</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
box.innerHTML = users.map(u => {
|
||||||
|
const nick = (u.nick || "未命名").replace(/</g,"<").replace(/>/g,">");
|
||||||
|
const uid = (u.userId || "").replace(/</g,"<").replace(/>/g,">");
|
||||||
|
const last = (u.lastMsg || "").replace(/</g,"<").replace(/>/g,">");
|
||||||
|
return `<div class="u"><div>${nick}</div><div class="uid">${uid}</div><div style="font-size:12px;color:#94a3b8;margin-top:4px;">${last}</div></div>`;
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function queryPm(){
|
||||||
|
try{
|
||||||
|
setStatus("查询中...");
|
||||||
|
const payload = {
|
||||||
|
authtoken: $("authtoken").value.trim(),
|
||||||
|
wxbot_bid: $("wxbot_bid").value.trim(),
|
||||||
|
page: Number($("page").value || 0),
|
||||||
|
size: Number($("size").value || 30),
|
||||||
|
};
|
||||||
|
const res = await fetch("/chatbot/api/private-message/query", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
setStatus(res.ok ? "查询成功" : "查询失败");
|
||||||
|
renderUsers(data.users || []);
|
||||||
|
renderJson(data);
|
||||||
|
}catch(e){
|
||||||
|
setStatus("请求异常");
|
||||||
|
renderJson({ error: String(e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyzeToken(){
|
||||||
|
try{
|
||||||
|
setStatus("分析中...");
|
||||||
|
const payload = { authtoken: $("authtoken").value.trim() };
|
||||||
|
const res = await fetch("/chatbot/api/private-message/analyze", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
setStatus(res.ok ? "分析成功" : "分析失败");
|
||||||
|
renderJson(data);
|
||||||
|
}catch(e){
|
||||||
|
setStatus("请求异常");
|
||||||
|
renderJson({ error: String(e) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user