| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130 |
- """
- mcp_pipe.py — 小智AI MCP 接入点桥接(v8 in-process)
- 不用 subprocess。直接 import audio_server 的工具,
- 在主进程里用 websockets.sync 跟 xiaozhi.me 通信。
- 完全绕开 Windows + Python 3.14 的 stdio pipe 怪行为。
- """
- from __future__ import annotations
- import json
- import logging
- import os
- import sys
- import time
- import websockets.sync.client as ws_sync
- import audio_server
- logger = logging.getLogger("MCP_PIPE")
- HERE = os.path.dirname(os.path.abspath(__file__))
- RECONNECT_DELAY = 5 # 断开后等 N 秒重连
- def run_session(endpoint: str, raw_log) -> bool:
- """单次 WS 会话,返回 True=正常退出(KeyboardInterrupt),False=断线需要重连"""
- try:
- ws = ws_sync.connect(endpoint, max_size=4 * 1024 * 1024)
- except Exception as e:
- logger.warning("WS connect failed: %s, will retry in %ds", e, RECONNECT_DELAY)
- return False
- logger.info("WS connected")
- logger.info("READY: ready to receive xiaozhi MCP requests")
- try:
- while True:
- try:
- raw = ws.recv(timeout=300)
- except TimeoutError:
- logger.info("WS recv timeout 5min, reconnecting...")
- return False
- except Exception as e:
- logger.warning("WS recv failed: %s", e)
- return False
- if not raw:
- logger.warning("WS recv empty (EOF)")
- return False
- if isinstance(raw, bytes):
- raw = raw.decode("utf-8", errors="replace")
- logger.info("RAW: %s", raw[:300])
- raw_log.write(f"[<-] {raw[:300]}\n")
- raw_log.flush()
- try:
- req = json.loads(raw)
- except Exception as e:
- logger.warning("parse err: %s", e)
- continue
- method = req.get("method")
- rid = req.get("id")
- logger.info("<- method=%s id=%s", method, rid)
- if rid is None and method and method.startswith("notifications/"):
- logger.info(" (notification, no response)")
- continue
- resp = audio_server.handle_request(req)
- if resp is None:
- continue
- line = (json.dumps(resp, ensure_ascii=False, separators=(",", ":")) + "\n").encode("utf-8")
- logger.info("-> response for %s id=%s (%d bytes)", method, rid, len(line))
- raw_log.write(f"[->] {line.decode('utf-8', errors='replace')[:300]}\n")
- raw_log.flush()
- try:
- ws.send(line)
- except Exception as e:
- logger.warning("WS send failed (connection likely closed): %s", e)
- return False
- except KeyboardInterrupt:
- return True
- except Exception as e:
- logger.warning("Unexpected error in session: %s", e)
- return False
- finally:
- try:
- ws.close()
- except Exception:
- pass
- return False
- def main() -> int:
- for k in ("HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY"):
- os.environ.pop(k, None)
- endpoint = os.environ.get("MCP_ENDPOINT")
- if not endpoint:
- print("ERROR: MCP_ENDPOINT 必填")
- return 2
- logging.basicConfig(
- level=os.environ.get("MCP_LOG_LEVEL", "INFO"),
- format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
- )
- raw_log_path = os.path.join(HERE, "mcp-pipe-utf8.log")
- raw_log = open(raw_log_path, "a", encoding="utf-8")
- raw_log.write(f"=== mcp_pipe started (auto-reconnect) ===\n")
- raw_log.flush()
- logger.info("AudioBook MCP server connecting to %s", endpoint)
- logger.info("API base = %s", audio_server.API_BASE)
- while True:
- should_exit = run_session(endpoint, raw_log)
- if should_exit:
- logger.info("KeyboardInterrupt, exiting")
- break
- logger.info("Disconnected, reconnecting in %ds...", RECONNECT_DELAY)
- time.sleep(RECONNECT_DELAY)
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|