修复后台聊天表情发送易卡住问题\n\n- 将后台表情发送改为异步提交,避免请求线程同步等待导致卡住\n- 增加按 md5 反查历史表情 total_length 的兜底逻辑\n- 为 SendEmoji 增加超时与详细日志,便于定位接口无响应问题

This commit is contained in:
liuwei
2026-04-27 11:21:11 +08:00
parent 7d2ad5b3d8
commit 19411d19c8
3 changed files with 125 additions and 17 deletions

View File

@@ -203,6 +203,73 @@ def _extract_emoji_meta(attachment_url: str, image_path: str):
return md5, total_length
def _parse_positive_int(value):
"""将任意输入尽量解析为正整数,失败时返回 0。
说明:
1. 前端可能传 total_length / totalLength / len类型也可能是字符串
2. 统一在这里收口,避免每个分支都重复写 try/except。
"""
try:
parsed = int(value)
except Exception:
return 0
return parsed if parsed > 0 else 0
def _get_emoji_asset_by_md5(message_storage, md5: str):
"""从消息存储中按 md5 反查表情原始记录。
说明:
1. 优先走 message_storage 自身方法,兼容未来把查询逻辑上移;
2. 若当前实例没有该方法,则回退到底层 message_db
3. 查不到时返回 None让上层决定是否报错。
"""
if not message_storage or not md5:
return None
if hasattr(message_storage, "get_emoji_asset_by_md5"):
return message_storage.get_emoji_asset_by_md5(md5)
message_db = getattr(message_storage, "message_db", None)
if message_db and hasattr(message_db, "get_emoji_asset_by_md5"):
return message_db.get_emoji_asset_by_md5(md5)
return None
def _resolve_emoji_send_meta(message_storage, md5: str, total_length: int):
"""补全发送表情所需的 md5 与 total_length。
说明:
1. wechat_ipad 的 SendEmoji 接口并不是只要 md5还必须带 TotalLen
2. 当前端只传了 md5 或长度为空时,这里尝试从历史消息里反查原始 XML
3. 返回值始终是“规整后的 md5 + total_length”方便发送分支直接使用。
"""
normalized_md5 = _safe_text(md5).strip().lower()
normalized_total_length = _parse_positive_int(total_length)
if not re.fullmatch(r"[0-9a-f]{16,64}", normalized_md5):
return "", 0
if normalized_total_length > 0:
return normalized_md5, normalized_total_length
asset = _get_emoji_asset_by_md5(message_storage, normalized_md5)
if not asset:
return normalized_md5, 0
resolved_md5, resolved_total_length = _extract_emoji_meta(
_safe_text(asset.get("attachment_url")),
_safe_text(asset.get("image_path"))
)
if resolved_md5 and resolved_md5 != normalized_md5:
# 历史数据如果出现大小写或异常值,以前端传入的 md5 为准,避免串表情。
logger.warning(f"表情参数回填命中 md5 不一致request_md5={normalized_md5}, record_md5={resolved_md5}")
return normalized_md5, _parse_positive_int(resolved_total_length)
def _normalize_recent_message(server, raw_message: dict, chat_type: str, target_wxid: str):
sender = _safe_text(raw_message.get("sender")).strip()
message_type = str(raw_message.get("message_type", ""))
@@ -830,20 +897,30 @@ def api_send_message():
elif msg_type == 'emoji':
if not isinstance(content, dict):
return jsonify({'success': False, 'message': '表情参数格式错误'})
md5 = _safe_text(content.get('md5')).strip().lower()
total_length = int(content.get('total_length') or 0)
if not re.fullmatch(r"[0-9a-f]{16,64}", md5) or total_length <= 0:
return jsonify({'success': False, 'message': '缺少表情 md5 或长度'})
try:
loop = get_or_create_loop()
future = asyncio.run_coroutine_threadsafe(server.client.send_emoji_message(wxid, md5, total_length), loop)
future.result(timeout=20)
except Exception as e:
logger.error(f"发送表情失败 md5={md5} len={total_length} wxid={wxid}: {e}")
return jsonify({'success': False, 'message': f'表情发送失败: {str(e)}'}), 500
# 表情发送必须同时具备 md5 和 total_length。
# 当前前端有时只拿得到 md5因此这里优先使用请求体里的长度
# 拿不到时再去历史消息表里反查,避免“参数明明看起来对,但接口还是发不出去”。
md5, total_length = _resolve_emoji_send_meta(
getattr(server, "message_storage", None),
content.get('md5'),
content.get('total_length') or content.get('totalLength') or content.get('len')
)
if not md5:
return jsonify({'success': False, 'message': '表情 md5 格式不正确'})
if total_length <= 0:
return jsonify({'success': False, 'message': '该表情缺少 total_length无法仅凭 md5 发送'})
# 表情发送改为和文本/图片一致的异步提交通道,避免 HTTP 请求线程
# 同步等待队列结果导致“高概率卡住”的体验问题。
logger.info(f"提交表情发送任务 wxid={wxid} md5={md5} total_length={total_length}")
send_message_in_thread(server.client.send_emoji_message, wxid, md5, total_length)
return jsonify({
'success': True,
'message': '表情发送成功'
'message': '表情消息已提交到 iPad 通道',
'data': {
'md5': md5,
'total_length': total_length
}
})
else: