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.

This commit is contained in:
QihuAI Design committed 2026-08-04 11:14:02 +08:00
1 parent f4e18bafe4
commit 34648e4810
2 files changed
+69 -4

No files matched your search

+62 -4
View File
@@ -47,6 +47,10 @@ def _beijing_now() -> datetime:
# ============================================================ # ============================================================
ILINK_BASE = "https://ilinkai.weixin.qq.com" ILINK_BASE = "https://ilinkai.weixin.qq.com"
# 微信/iLink 单条文本消息长度软上限。超过则分段发送,避免 iLink 返回 200
# 但用户侧静默收不到(实测 1698 字符 HTML 链接、3011 字符 Markdown 均丢失)。
WECHAT_TEXT_MAX_LEN = 1500
# 每个已连接账号一个后台接收线程,key = 账号表主键 id # 每个已连接账号一个后台接收线程,key = 账号表主键 id
_wechat_receiver_threads: dict[int, threading.Thread] = {} _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 # 07-30 diagnostic
import json as _json3 import json as _json3
try: 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: 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") _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") + " 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: except Exception:
pass 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): def _wechat_send_message(baseurl: str, bot_token: str, to_user_id: str, context_token: str, text: str):
"""发送纯文本回复。""" """发送纯文本回复。长文本按 WECHAT_TEXT_MAX_LEN 分段,避免 iLink 200 但微信不显示。"""
return _wechat_post_msg(baseurl, bot_token, to_user_id, context_token, if not text:
[{"type": 1, "text_item": {"text": 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: 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。 确保用户可点击访问且不会破坏会话;待拿到 iLink 媒体协议文档后再恢复内联 item。
""" """
relay_url = _platform_relay_url(url) 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": if media_type == "image":
caption = "🖼️ 图片已生成,点击查看:" + relay_url caption = "🖼️ 图片已生成,点击查看:" + relay_url
else: else:
+7
View File
@@ -191,6 +191,13 @@ async def lifespan(app: FastAPI):
except Exception as e: except Exception as e:
logger.warning(f"[EventBus] 启动失败(不影响正常服务): {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 启动完成") logger.info("Qihu-AIFlow v2.0.0 启动完成")
yield yield