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:
QihuAI Design committed 2026-08-04 10:21:19 +08:00
1 parent 9a43ee244d
commit 29a1ce2e54
5 files changed
+575 -55

No files matched your search

+80 -19
View File
@@ -83,6 +83,11 @@ def _create_kwargs(request_body: dict, stream: bool = False) -> dict:
if tools:
kw["tools"] = tools
kw["tool_choice"] = request_body.get("tool_choice", "auto")
# 2026-07-30: 将未在白名单的额外字段(如 enable_thinking)作为 extra_body 传递
_whitelist = {"model", "messages", "stream", "temperature", "top_p", "max_tokens", "stop", "tools", "tool_choice"}
_extras = {k: v for k, v in request_body.items() if k not in _whitelist}
if _extras:
kw["extra_body"] = _extras
return kw
@@ -223,29 +228,85 @@ def build_openai_client_sync(api_url, api_key, *, proxy=None, timeout=120, auth_
return OpenAI(base_url=base, api_key=api_key or "EMPTY", timeout=timeout, http_client=http_client, default_headers=_hdrs if _hdrs else None)
def chat_complete(api_url, api_key, request_body, *, proxy=None, timeout=120):
"""同步版(与 achat_complete 行为/返回形状完全一致),供非 async 上下文使用。"""
client = build_openai_client_sync(api_url, api_key, proxy=proxy, timeout=timeout, auth_header=auth_header)
with client:
resp = client.chat.completions.create(**_create_kwargs(request_body))
msg = resp.choices[0].message
return {
"choices": [
{
"message": {
"content": msg.content or "",
"tool_calls": _normalize_tool_calls(getattr(msg, "tool_calls", None)),
"reasoning_content": getattr(msg, "reasoning_content", None),
}
def chat_complete(api_url, api_key, request_body, *, proxy=None, timeout=120, auth_header=None):
"""同步版(与 achat_complete 行为/返回形状完全一致),供非 async 上下文使用。
2026-07-30 重写:直接使用 requests 替代 OpenAI SDK/httpx。
OpenAI SDK 的 httpx 连接池在慢长轮询(如 iLink QR 状态 12-19s)后会出现
Connection error,已实测复现。requests 库没有这个问题。
"""
import requests
import base64
import random
body = dict(request_body)
body["stream"] = False
# enable_thinking 等额外字段已在 request_body 中
# Headers: Bearer token + X-WECHAT-UIN (兼容 iLink)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}" if api_key else "Bearer empty",
}
if auth_header and ":" in auth_header:
_k, _v = auth_header.split(":", 1)
headers[_k.strip()] = _v.strip()
else:
# X-WECHAT-UIN 防重放
headers["X-WECHAT-UIN"] = base64.b64encode(str(random.randint(0, 2**32-1)).encode()).decode()
# 2026-07-31 修复根因: proxy 为 None 时传 proxies={} 而非 None。
# requests 在 proxies=None 时会继续读取环境 HTTP_PROXY(trust_env 默认 True),
# 而 uvicorn 进程(run.sh)带有 HTTP_PROXY=172.18.0.1:7890, 该代理无法解析
# Docker 内部域名(如 qihu-aiflow-litellm), 导致所有内部模型调用静默失败、返回空。
# 显式 proxies={} + trust_env=False 可完全绕过环境代理, 使 _proxy_for_url 的
# 决策(返回 None=不代理)真正生效。
_proxies = {"http": proxy, "https": proxy} if proxy else {}
_session = requests.Session()
_session.trust_env = False
last_body = {}
for attempt in range(2):
try:
resp = _session.post(
api_url.rstrip("/"),
headers=headers,
json=body,
proxies=_proxies,
timeout=timeout,
)
try:
last_body = resp.json() if resp.content else {}
except Exception:
last_body = {}
if resp.ok:
import logging as _l6
_l6.warning(f'[chat_complete] body keys={list(last_body.keys()) if isinstance(last_body, dict) else type(last_body)}, content_len={len(resp.content)}, content_preview={str(resp.content)[:200]}')
return _shape_response(last_body)
except Exception:
pass
return {"choices": [{"message": {"content": "", "tool_calls": None, "reasoning_content": None}}]}
def _shape_response(body: dict) -> dict:
"""Normalize OpenAI-compatible response to standard shape."""
choices = body.get("choices", [])
import logging as _l7
if choices:
msg = choices[0].get("message", {})
result = {
"choices": [{
"message": {
"content": msg.get("content") or "",
"tool_calls": _normalize_tool_calls(msg.get("tool_calls")),
"reasoning_content": msg.get("reasoning_content") or "",
}
],
"usage": (resp.usage.model_dump() if resp.usage else {}),
}]
}
_l7.warning(f'[_shape] content_len={len(result["choices"][0]["message"]["content"])}, keys={list(result["choices"][0]["message"].keys())}')
return result
_l7.warning(f'[_shape] NO choices in body, keys={list(body.keys())[:5]}')
return body
# ---------------------------------------------------------------------------
# 嵌入(Embeddings):OpenAI 兼容 /v1/embeddings,收敛散落的裸 httpx 嵌入调用
# ---------------------------------------------------------------------------
def _embed_kwargs(input, model):
return {"model": model, "input": input if isinstance(input, list) else [input]}