From 86370d58249a60944777348fde7faae960cdf318 Mon Sep 17 00:00:00 2001 From: "mula.liu" Date: Sat, 14 Mar 2026 13:03:22 +0800 Subject: [PATCH] v0.1.4-p2 --- backend/main.py | 30 +++++++--- backend/services/topic_runtime/bridge.py | 4 +- backend/services/topic_service.py | 12 ++++ frontend/src/App.css | 7 ++- frontend/src/App.tsx | 29 ++++++--- .../modules/dashboard/BotDashboardModule.css | 59 ++++++++++++++++++- .../modules/dashboard/BotDashboardModule.tsx | 54 +++++++++++++---- 7 files changed, 161 insertions(+), 34 deletions(-) diff --git a/backend/main.py b/backend/main.py index 76548b9..ff5fbf2 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1062,6 +1062,14 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict }, } + def _with_body_preview(message: str, preview: Any) -> str: + text = str(message or "").strip() + body = " ".join(str(preview or "").strip().split()) + if not body: + return text + body = body[:240] + return f"{text}: {body}" if text else body + if bot_id: if transport_type == "sse": probe_headers = dict(headers) @@ -1077,16 +1085,17 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict status_code = probe.get("status_code") content_type = str(probe.get("content_type") or "") message = str(probe.get("message") or "").strip() + body_preview = probe.get("body_preview") if status_code in {401, 403}: return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Auth failed for MCP SSE endpoint", "content_type": content_type, "probe_from": "bot-container"} if status_code == 404: return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint not found", "content_type": content_type, "probe_from": "bot-container"} if isinstance(status_code, int) and status_code >= 500: - return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint server error", "content_type": content_type, "probe_from": "bot-container"} + return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("MCP SSE endpoint server error", body_preview), "content_type": content_type, "probe_from": "bot-container"} if not probe.get("ok"): - return {"ok": False, "transport": transport_type, "status_code": status_code, "message": message or "Failed to connect MCP SSE endpoint from bot container", "content_type": content_type, "probe_from": "bot-container"} + return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview(message or "Failed to connect MCP SSE endpoint from bot container", body_preview), "content_type": content_type, "probe_from": "bot-container"} if "text/event-stream" not in content_type.lower(): - return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Endpoint reachable, but content-type is not text/event-stream", "content_type": content_type, "probe_from": "bot-container"} + return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("Endpoint reachable, but content-type is not text/event-stream", body_preview), "content_type": content_type, "probe_from": "bot-container"} return {"ok": True, "transport": transport_type, "status_code": status_code, "message": "MCP SSE endpoint is reachable", "content_type": content_type, "probe_from": "bot-container"} probe_headers = dict(headers) @@ -1102,16 +1111,17 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict ) status_code = probe.get("status_code") message = str(probe.get("message") or "").strip() + body_preview = probe.get("body_preview") if status_code in {401, 403}: return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "Auth failed for MCP endpoint", "probe_from": "bot-container"} if status_code == 404: return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP endpoint not found", "probe_from": "bot-container"} if isinstance(status_code, int) and status_code >= 500: - return {"ok": False, "transport": transport_type, "status_code": status_code, "message": "MCP endpoint server error", "probe_from": "bot-container"} + return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview("MCP endpoint server error", body_preview), "probe_from": "bot-container"} if probe.get("ok") and status_code in {200, 201, 202, 204, 400, 405, 415, 422}: reachability_msg = "MCP endpoint is reachable" if status_code in {200, 201, 202, 204} else "MCP endpoint is reachable (request format not fully accepted by probe)" return {"ok": True, "transport": transport_type, "status_code": status_code, "message": reachability_msg, "probe_from": "bot-container"} - return {"ok": False, "transport": transport_type, "status_code": status_code, "message": message or "Unexpected response from MCP endpoint", "probe_from": "bot-container"} + return {"ok": False, "transport": transport_type, "status_code": status_code, "message": _with_body_preview(message or "Unexpected response from MCP endpoint", body_preview), "probe_from": "bot-container"} try: with httpx.Client(timeout=httpx.Timeout(timeout_s), follow_redirects=True) as client: @@ -1120,6 +1130,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict req_headers.setdefault("Accept", "text/event-stream") resp = client.get(url, headers=req_headers) content_type = str(resp.headers.get("content-type") or "") + body_preview = resp.text[:512] if resp.status_code in {401, 403}: return { "ok": False, @@ -1143,7 +1154,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict "ok": False, "transport": transport_type, "status_code": resp.status_code, - "message": "MCP SSE endpoint server error", + "message": _with_body_preview("MCP SSE endpoint server error", body_preview), "content_type": content_type, "probe_from": "backend-host", } @@ -1152,7 +1163,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict "ok": False, "transport": transport_type, "status_code": resp.status_code, - "message": "Endpoint reachable, but content-type is not text/event-stream", + "message": _with_body_preview("Endpoint reachable, but content-type is not text/event-stream", body_preview), "content_type": content_type, "probe_from": "backend-host", } @@ -1169,6 +1180,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict req_headers.setdefault("Content-Type", "application/json") req_headers.setdefault("Accept", "application/json, text/event-stream") resp = client.post(url, headers=req_headers, json=probe_payload) + body_preview = resp.text[:512] if resp.status_code in {401, 403}: return { "ok": False, @@ -1190,7 +1202,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict "ok": False, "transport": transport_type, "status_code": resp.status_code, - "message": "MCP endpoint server error", + "message": _with_body_preview("MCP endpoint server error", body_preview), "probe_from": "backend-host", } if resp.status_code in {200, 201, 202, 204}: @@ -1213,7 +1225,7 @@ def _probe_mcp_server(cfg: Dict[str, Any], bot_id: Optional[str] = None) -> Dict "ok": False, "transport": transport_type, "status_code": resp.status_code, - "message": "Unexpected response from MCP endpoint", + "message": _with_body_preview("Unexpected response from MCP endpoint", body_preview), "probe_from": "backend-host", } except httpx.TimeoutException: diff --git a/backend/services/topic_runtime/bridge.py b/backend/services/topic_runtime/bridge.py index de1cf7b..c516e04 100644 --- a/backend/services/topic_runtime/bridge.py +++ b/backend/services/topic_runtime/bridge.py @@ -3,7 +3,7 @@ from typing import Any, Dict, Optional from sqlmodel import Session -from services.topic_service import _topic_publish_internal +from services.topic_service import _has_topic_mcp_server, _topic_publish_internal from .publisher import build_topic_publish_payload @@ -19,6 +19,8 @@ def publish_runtime_topic_packet( packet_type = str(packet.get("type") or "").strip().upper() if packet_type not in {"ASSISTANT_MESSAGE", "BUS_EVENT"} or not persisted_message_id: return + if not _has_topic_mcp_server(bot_id): + return topic_payload = build_topic_publish_payload( bot_id, diff --git a/backend/services/topic_service.py b/backend/services/topic_service.py index d9cc8d9..54f2958 100644 --- a/backend/services/topic_service.py +++ b/backend/services/topic_service.py @@ -151,6 +151,18 @@ def _resolve_topic_mcp_bot_id_by_token(session: Session, token: str) -> Optional return None +def _has_topic_mcp_server(bot_id: str) -> bool: + config_data = _read_bot_config(bot_id) + tools_cfg = config_data.get("tools") + if not isinstance(tools_cfg, dict): + return False + mcp_servers = tools_cfg.get("mcpServers") + if not isinstance(mcp_servers, dict): + return False + token = _extract_topic_mcp_token(mcp_servers.get(TOPIC_MCP_SERVER_NAME)) + return bool(token) + + def _normalize_topic_key(raw: Any) -> str: value = str(raw or "").strip().lower() if not value: diff --git a/frontend/src/App.css b/frontend/src/App.css index 434a30a..3e41db8 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -332,7 +332,8 @@ body { } .grid-ops.grid-ops-compact { - grid-template-columns: minmax(0, 1fr) minmax(280px, 420px); + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(0, 1fr); } .stack { @@ -1197,12 +1198,12 @@ body { @media (max-width: 980px) { .grid-ops.grid-ops-compact { grid-template-columns: 1fr; - grid-template-rows: auto auto; + grid-template-rows: minmax(0, 1fr); } .app-shell-compact .grid-ops.grid-ops-compact { grid-template-columns: 1fr; - grid-template-rows: minmax(0, 1fr) auto; + grid-template-rows: minmax(0, 1fr); height: 100%; min-height: 0; } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 72ddf3e..0c87eb5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -35,8 +35,8 @@ function AuthenticatedApp({ const [singleBotSubmitting, setSingleBotSubmitting] = useState(false); useBotsSync(forcedBotId); const t = pickLocale(locale, { 'zh-cn': appZhCn, en: appEn }); - const isSingleBotCompactView = compactMode && Boolean(String(forcedBotId || '').trim()); - const [headerCollapsed, setHeaderCollapsed] = useState(isSingleBotCompactView); + const isCompactShell = compactMode; + const [headerCollapsed, setHeaderCollapsed] = useState(isCompactShell); const forced = String(forcedBotId || '').trim(); const forcedBot = forced ? activeBots[forced] : undefined; const shouldPromptSingleBotPassword = Boolean(forced && forcedBot?.has_access_password && !singleBotUnlocked); @@ -51,8 +51,8 @@ function AuthenticatedApp({ }, [forced, forcedBot?.name, t.title]); useEffect(() => { - setHeaderCollapsed(isSingleBotCompactView); - }, [isSingleBotCompactView, forcedBotId]); + setHeaderCollapsed(isCompactShell); + }, [isCompactShell, forcedBotId]); useEffect(() => { setSingleBotUnlocked(false); @@ -114,9 +114,9 @@ function AuthenticatedApp({
{ - if (isSingleBotCompactView && headerCollapsed) setHeaderCollapsed(false); + if (isCompactShell && headerCollapsed) setHeaderCollapsed(false); }} >
@@ -124,7 +124,7 @@ function AuthenticatedApp({ Nanobot

{t.title}

- {isSingleBotCompactView ? ( + {isCompactShell ? (