fix: 双Claw能力对齐 + MCP/LLM代理绕过(P0止血)
- 新增 app/core/capability_discovery.py:统一能力发现层,Web/IM 双Claw共用单一实现 (三级降级:用户安装 → 平台published → 相关性裁剪;MCP 自动挂载已启用服务) - im_llm.py:IM Bot Claw 改用统一发现层,恢复 skills(12) + MCP(26工具) 能力, 修复 #866 部署回退导致的 agents=[]/mcp=[] 回归;mcp__ 前缀分流到 call_mcp_tool - mcp_client.py:注入 httpx_client_factory(trust_env=False) 绕过容器 HTTP_PROXY, 修复外部 MCP(xiaobenyang.com / anysearch.com) tools/list 全部静默失败(mcp_tools 0→26) - llm_client.py / im_bot_user.py:2026-07-31 代理劫持内部请求 + 媒体断连修复(随镜像已上线)
This commit is contained in:
1 parent
9a43ee244d
commit
29a1ce2e54
5 files changed
+575
-55
No files matched your search
@@ -143,12 +143,33 @@ def _qihua_reply(text: str, user_id: str = None) -> str:
|
||||
f"{hist}\n\n"
|
||||
f"用户当前问题:{text}"
|
||||
)
|
||||
# 磁盘调试日志(不依赖 uvicorn logger)
|
||||
try:
|
||||
with open("/tmp/im_llm_debug.log", "a", encoding="utf-8") as _f:
|
||||
_f.write(_beijing_now().isoformat() + f" _qihua_reply user={user_id} prompt_len={len(prompt)} prompt={repr(prompt)[:500]}\n")
|
||||
except Exception:
|
||||
pass
|
||||
db = None
|
||||
try:
|
||||
from app.api.im_llm import _call_real_llm
|
||||
db = SessionLocal()
|
||||
return _call_real_llm(db, None, prompt, "个人微信", user_id=user_id)
|
||||
result = _call_real_llm(db, None, prompt, "个人微信", user_id=user_id)
|
||||
try:
|
||||
with open("/tmp/im_llm_debug.log", "a", encoding="utf-8") as _f:
|
||||
_f.write(_beijing_now().isoformat() + f" _qihua_reply RESULT len={len(result) if result else 0} preview={repr(result)[:200]}\n")
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
except Exception as e:
|
||||
import traceback as _tb2
|
||||
import logging
|
||||
_log = logging.getLogger('im_bot_user')
|
||||
_log.warning(f'[_qihua_reply] 异常: {e}\n{_tb2.format_exc()[:1000]}')
|
||||
try:
|
||||
with open("/tmp/im_llm_debug.log", "a", encoding="utf-8") as _f:
|
||||
_f.write(_beijing_now().isoformat() + f" _qihua_reply EXCEPTION: {e}\n{_tb2.format_exc()[:800]}\n")
|
||||
except Exception:
|
||||
pass
|
||||
return f"(AI 回复生成失败:{e})"
|
||||
finally:
|
||||
if db:
|
||||
@@ -164,7 +185,7 @@ def _wechat_get_updates(baseurl: str, bot_token: str, cursor: str):
|
||||
resp = requests.post(
|
||||
baseurl.rstrip("/") + "/ilink/bot/getupdates",
|
||||
headers=_wechat_headers(bot_token),
|
||||
json={"get_updates_buf": cursor, "base_info": {"channel_version": "1.0.2"}},
|
||||
json={"get_updates_buf": cursor},
|
||||
timeout=40,
|
||||
)
|
||||
if not resp.ok:
|
||||
@@ -178,20 +199,26 @@ def _wechat_get_updates(baseurl: str, bot_token: str, cursor: str):
|
||||
def _wechat_post_msg(baseurl: str, bot_token: str, to_user_id: str, context_token: str, item_list: list):
|
||||
"""向 iLink 发送一条消息(item_list 可含文本/图片/视频等)。返回 (ok, ret, body)。
|
||||
ret=-14=会话过期需重扫;ret=-2=token 失效。"""
|
||||
import uuid as _uuid
|
||||
payload = {
|
||||
"msg": {
|
||||
"from_user_id": "",
|
||||
"to_user_id": to_user_id,
|
||||
"client_id": "py-" + str(_uuid.uuid4()),
|
||||
"message_type": 2,
|
||||
"message_state": 2,
|
||||
"context_token": context_token,
|
||||
"item_list": item_list,
|
||||
},
|
||||
"base_info": {"channel_version": "1.0.2"},
|
||||
}
|
||||
}
|
||||
last_body = {}
|
||||
# 07-30 diagnostic
|
||||
import json as _json3
|
||||
try:
|
||||
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")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for _ in range(2):
|
||||
try:
|
||||
resp = requests.post(
|
||||
@@ -239,21 +266,21 @@ def _platform_relay_url(raw_url: str) -> str:
|
||||
|
||||
def _wechat_send_media(baseurl: str, bot_token: str, to_user_id: str, context_token: str,
|
||||
media_type: str, url: str):
|
||||
"""发送媒体(图片/视频)并在微信内联渲染。
|
||||
"""发送媒体(图片/视频)。
|
||||
|
||||
问题:AGNES 媒体 URL 位于境外,iLink(中国大陆服务器)直连常被墙/超时,
|
||||
微信里只能看到"链接"而无法内联播放。
|
||||
解决:改写为平台中转地址(经香港代理可达 AGNES),微信即可内联显示图片/视频。
|
||||
问题:AGNES 媒体 URL 位于境外,iLink(中国大陆服务器)直连常被墙/超时。
|
||||
解决:改写为平台中转地址(经香港代理可达 AGNES)。
|
||||
|
||||
2026-07-31 兼容修复:iLink 对 type=2/5 的媒体 item 返回 ret=-2 invalid arguments,
|
||||
且旧代码把 ret=-2 误判为 token 失效,导致连接假性断开。先以纯文本链接形式发送,
|
||||
确保用户可点击访问且不会破坏会话;待拿到 iLink 媒体协议文档后再恢复内联 item。
|
||||
"""
|
||||
relay_url = _platform_relay_url(url)
|
||||
if media_type == "image":
|
||||
media_item = {"type": 2, "image_item": {"url": relay_url}}
|
||||
caption = "🖼️ 图片已生成(若未直接显示,请点此查看):" + relay_url
|
||||
caption = "🖼️ 图片已生成,点击查看:" + relay_url
|
||||
else:
|
||||
media_item = {"type": 5, "video_item": {"url": relay_url}}
|
||||
caption = "🎬 视频已生成(若未直接播放,请点此查看):" + relay_url
|
||||
return _wechat_post_msg(baseurl, bot_token, to_user_id, context_token,
|
||||
[media_item, {"type": 1, "text_item": {"text": caption}}])
|
||||
caption = "🎬 视频已生成,点击查看:" + relay_url
|
||||
return _wechat_send_message(baseurl, bot_token, to_user_id, context_token, caption)
|
||||
|
||||
|
||||
def _wechat_extract_text(msg: dict) -> str:
|
||||
@@ -326,7 +353,6 @@ def _get_or_create_wechat_conv(db: Session, user_id: str) -> str:
|
||||
), {"uid": user_id})
|
||||
if rows:
|
||||
return _row_to_dict(rows[0])["id"]
|
||||
import uuid as _uuid
|
||||
cid = str(_uuid.uuid4())
|
||||
now = _beijing_now()
|
||||
_execute_raw(db, text(
|
||||
@@ -338,7 +364,6 @@ def _get_or_create_wechat_conv(db: Session, user_id: str) -> str:
|
||||
|
||||
|
||||
def _append_ai_message(db: Session, conv_id: str, role: str, content: str, created=None):
|
||||
import uuid as _uuid
|
||||
_execute_raw(db, text(
|
||||
"INSERT INTO messages (id, conversation_id, role, content, token_used, created_at) "
|
||||
"VALUES (:id, :cid, :role, :content, 0, :c)"
|
||||
@@ -425,7 +450,14 @@ def _wechat_receiver_loop(account_id: int):
|
||||
except Exception:
|
||||
pass
|
||||
# 先生成回复(此时历史尚未包含本条,避免重复)
|
||||
import logging as _lrx, inspect as _insp
|
||||
_lrx.warning(f'[receiver] calling _qihua_reply({repr(content)}, {repr(owner_uid)})')
|
||||
# 验证使用的 _call_real_llm 版本
|
||||
from app.api.im_llm import _call_real_llm as _crl
|
||||
_src = _insp.getsource(_crl)
|
||||
_lrx.warning(f'[receiver] _call_real_llm source len={len(_src)}, has _try_models={"_try_models" in _src}')
|
||||
reply = _qihua_reply(content, owner_uid)
|
||||
_lrx.warning(f'[receiver] reply ({type(reply).__name__}): {repr(reply)[:200]}')
|
||||
# 媒体结构化返回:_call_real_llm 生成图片/视频时返回
|
||||
# {"__media__": True, "media_type": "image"/"video", "url": "..."}
|
||||
if isinstance(reply, dict) and reply.get("__media__"):
|
||||
@@ -457,11 +489,23 @@ def _wechat_receiver_loop(account_id: int):
|
||||
% (ret, repr(sender)))
|
||||
except Exception:
|
||||
pass
|
||||
if ret in (-2, -14):
|
||||
# 会话/鉴权失效:标记需重扫但继续循环,让下一轮 poll 自然检测过期
|
||||
if ret == -14:
|
||||
# -14=会话过期,需重扫
|
||||
_mark_wechat_expired(d["id"])
|
||||
elif ret == -2:
|
||||
# -2=参数错误(如媒体消息格式不被 iLink 接受),并非 token 失效。
|
||||
# 标记 expired 会导致连接假性断开,后续消息全部丢失,故仅记录。
|
||||
pass
|
||||
time.sleep(1)
|
||||
except Exception:
|
||||
# 2026-07-31 修复:此前裸 except 静默吞掉所有异常,导致接收器出错时毫无日志、
|
||||
# 完全无法排查。改为记录异常堆栈到磁盘,便于定位。
|
||||
import traceback as _tb
|
||||
try:
|
||||
with open("/tmp/wechat_receiver_err.log", "a", encoding="utf-8") as _f:
|
||||
_f.write(_beijing_now().isoformat() + " RECEIVER EXCEPTION:\n" + _tb.format_exc() + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(5)
|
||||
finally:
|
||||
db.close()
|
||||
@@ -555,6 +599,13 @@ async def wechat_qrcode_status(
|
||||
data = j.get("data", j) if isinstance(j, dict) else j
|
||||
status = data.get("status")
|
||||
if status == "confirmed":
|
||||
# 07-30 diagnostic: log full iLink confirm response
|
||||
import json as _json
|
||||
try:
|
||||
with open("/tmp/wechat_confirm.log", "a", encoding="utf-8") as _f:
|
||||
_f.write(_beijing_now().isoformat() + " CONFIRM_RAW=" + _json.dumps(j, ensure_ascii=False)[:2000] + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
creds = data.get("credentials", {}) or {}
|
||||
bot_token = data.get("bot_token") or creds.get("bot_token")
|
||||
baseurl = data.get("baseurl") or creds.get("baseurl") or ILINK_BASE
|
||||
@@ -590,7 +641,15 @@ async def wechat_qrcode_status(
|
||||
"updated_at": _beijing_now(), "id": _row_to_dict(row)["id"],
|
||||
})
|
||||
row = _wechat_row_for_user(db, user.id)
|
||||
aid = _row_to_dict(row)["id"] if row else 1
|
||||
saved = _row_to_dict(row) if row else {}
|
||||
import json as _json2
|
||||
try:
|
||||
with open("/tmp/wechat_confirm.log", "a", encoding="utf-8") as _f:
|
||||
_f.write(_beijing_now().isoformat() + " SAVED bot_token=" + repr(saved.get("bot_token", "NULL")[:20])
|
||||
+ " ilink_bot_id=" + repr(saved.get("ilink_bot_id")) + "\n")
|
||||
except Exception:
|
||||
pass
|
||||
aid = saved.get("id", 1)
|
||||
_start_wechat_receiver(aid)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
Reference in new issue
Block a user