From 34648e48108a53ada6cf8d52cd239f6f51d64b0c Mon Sep 17 00:00:00 2001 From: QihuAI Design Date: Tue, 4 Aug 2026 11:08:53 +0800 Subject: [PATCH] fix(im_bot): segment long WeChat text replies + enhance send diagnostics (#878)\n\niLink returns HTTP 200 but WeChat silently drops long messages (1698-char\nHTML image link, 3011-char skill list). Add WECHAT_TEXT_MAX_LEN=1500 and\nsegment long text into multiple items. Also log payload item lengths and\nmedia URL lengths for future diagnosis. --- backend/python/app/api/im_bot_user.py | 66 +++++++++++++++++++++++++-- backend/python/main.py | 7 +++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/backend/python/app/api/im_bot_user.py b/backend/python/app/api/im_bot_user.py index 9b18389..71f1140 100644 --- a/backend/python/app/api/im_bot_user.py +++ b/backend/python/app/api/im_bot_user.py @@ -47,6 +47,10 @@ def _beijing_now() -> datetime: # ============================================================ ILINK_BASE = "https://ilinkai.weixin.qq.com" +# 微信/iLink 单条文本消息长度软上限。超过则分段发送,避免 iLink 返回 200 +# 但用户侧静默收不到(实测 1698 字符 HTML 链接、3011 字符 Markdown 均丢失)。 +WECHAT_TEXT_MAX_LEN = 1500 + # 每个已连接账号一个后台接收线程,key = 账号表主键 id _wechat_receiver_threads: dict[int, threading.Thread] = {} @@ -212,10 +216,19 @@ def _wechat_post_msg(baseurl: str, bot_token: str, to_user_id: str, context_toke # 07-30 diagnostic import json as _json3 try: + # 增强诊断:记录每条消息类型、长度、内容摘要,用于定位“iLink 200 但用户收不到” + _item_summaries = [] + for _it in item_list: + _t = _it.get("type") + _txt = "" + if _t == 1 and _it.get("text_item"): + _txt = _it["text_item"].get("text", "") + _item_summaries.append(f"type={_t} len={len(_txt)} text={repr(_txt[:80])}") with open("/tmp/wechat_send_debug.log", "a", encoding="utf-8") as _f: _f.write(_beijing_now().isoformat() + " SEND_DEBUG token=" + repr(bot_token[:20] if bot_token else "NULL") + " to=" + repr(to_user_id[:30]) + " ctx=" + repr(context_token[:30]) + "\n") - _f.write(" payload_keys=" + str(list(payload.get("msg", {}).keys())) + "\n") + _f.write(" payload_keys=" + str(list(payload.get("msg", {}).keys())) + + " items=[" + "; ".join(_item_summaries) + "]\n") except Exception: pass @@ -249,9 +262,49 @@ def _wechat_post_msg(baseurl: str, bot_token: str, to_user_id: str, context_toke def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str): - """发送纯文本回复。""" - return _wechat_post_msg(baseurl, bot_token, to_user_id, context_token, - [{"type": 1, "text_item": {"text": text}}]) + """发送纯文本回复。长文本按 WECHAT_TEXT_MAX_LEN 分段,避免 iLink 200 但微信不显示。""" + if not text: + return _wechat_post_msg(baseurl, bot_token, to_user_id, context_token, + [{"type": 1, "text_item": {"text": "(无内容)"}}]) + # 单条不超过软上限;保留完整段落优先在换行处切分 + chunks = [] + start = 0 + while start < len(text): + end = min(start + WECHAT_TEXT_MAX_LEN, len(text)) + if end < len(text): + # 尽量在换行处切断 + _nl = text.rfind("\n", start, end) + if _nl > start + WECHAT_TEXT_MAX_LEN // 2: + end = _nl + 1 + chunks.append(text[start:end]) + start = end + + all_ok = True + last_ret = None + for idx, chunk in enumerate(chunks): + suffix = "" + if len(chunks) > 1: + if idx == 0: + suffix = "\n(内容较长,分条发送)" + if idx == len(chunks) - 1: + suffix = "\n(已发送完毕;完整内容请访问平台 AI 聊天的 WeXin Bot 窗口)" + ok, ret, _ = _wechat_post_msg( + baseurl, bot_token, to_user_id, context_token, + [{"type": 1, "text_item": {"text": chunk + suffix}}] + ) + if not ok: + all_ok = False + last_ret = ret + # 记录失败但不中断,尽量把已切好的消息发完 + try: + with open("/tmp/wechat_send.log", "a", encoding="utf-8") as _f: + _f.write(_beijing_now().isoformat() + " SEGMENT_FAIL idx=%s ret=%s\n" % (idx, ret)) + except Exception: + pass + # 分段之间稍作间隔,降低频率/风控影响 + if idx < len(chunks) - 1: + time.sleep(0.5) + return all_ok, last_ret, {} def _platform_relay_url(raw_url: str) -> str: @@ -276,6 +329,11 @@ def _wechat_send_media(baseurl: str, bot_token: str, to_user_id: str, context_to 确保用户可点击访问且不会破坏会话;待拿到 iLink 媒体协议文档后再恢复内联 item。 """ relay_url = _platform_relay_url(url) + try: + with open("/tmp/wechat_send_debug.log", "a", encoding="utf-8") as _f: + _f.write(_beijing_now().isoformat() + " MEDIA_URL_LEN=%s type=%s\n" % (len(relay_url), media_type)) + except Exception: + pass if media_type == "image": caption = "🖼️ 图片已生成,点击查看:" + relay_url else: diff --git a/backend/python/main.py b/backend/python/main.py index 74e7035..f326625 100644 --- a/backend/python/main.py +++ b/backend/python/main.py @@ -191,6 +191,13 @@ async def lifespan(app: FastAPI): except Exception as e: logger.warning(f"[EventBus] 启动失败(不影响正常服务): {e}") + # 启动个人微信接收线程(之前仅在扫码确认时启动,重启后丢失) + try: + from app.api.im_bot_user import start_all_wechat_receivers + start_all_wechat_receivers() + except Exception as e: + logger.warning(f"[IM个人微信] 启动恢复失败(不影响正常服务): {e}") + logger.info("Qihu-AIFlow v2.0.0 启动完成") yield