commit a6a76b4d2b2adacf38d37bb6b72560947c6e9282 Author: 罡悠 Date: Mon May 18 19:15:44 2026 +0800 first commit diff --git a/py/Dockerfile b/py/Dockerfile new file mode 100644 index 0000000..80379e9 --- /dev/null +++ b/py/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.10-slim +WORKDIR /app +COPY py/requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple +COPY py/ ./ +EXPOSE 8000 +CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/py/docker-compose.yaml b/py/docker-compose.yaml new file mode 100644 index 0000000..c07fb05 --- /dev/null +++ b/py/docker-compose.yaml @@ -0,0 +1,23 @@ +version: '3.8' + +services: + fastapi: + build: + context: . + dockerfile: Dockerfile + ports: + - '8123:8123' + networks: + - 1panel-network + restart: always + environment: + - TZ=Asia/Shanghai + logging: + driver: json-file + options: + max-size: '10m' + max-file: '3' + +networks: + 1panel-network: + external: true diff --git a/py/requirements.txt b/py/requirements.txt new file mode 100644 index 0000000..89ba710 --- /dev/null +++ b/py/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.110.0 +uvicorn==0.27.1 +websockets==12.0 +mcp==1.26.0 +httpx==0.27.0 \ No newline at end of file diff --git a/py/server.py b/py/server.py new file mode 100644 index 0000000..b91aeea --- /dev/null +++ b/py/server.py @@ -0,0 +1,113 @@ +import random +import asyncio +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request +from fastapi.middleware.cors import CORSMiddleware +from starlette.routing import Route, Mount +from mcp.server import Server +from mcp.server.sse import SseServerTransport +from mcp.types import Tool, TextContent + +connected_clients: list[WebSocket] = [] + +mcp_server = Server("cherry-studio-park-mcp") + +# 1. MCP SSE 传输配置 +# Cherry Studio 连接 /sse,消息路由 /messages/ +sse = SseServerTransport("/messages/") + +async def mcp_endpoint(request: Request): + # 标准 SSE 握手,连接 MCP Server + async with sse.connect_sse(request.scope, request.receive, request._send) as (read_stream, write_stream): + await mcp_server.run(read_stream, write_stream, mcp_server.create_initialization_options()) + +# 2. 核心逻辑:发送告警到前端 +async def send_alert(floor: str): + lights = random.sample(["f9-sphere1", "f9-sphere2", "f9-sphere3", "f9-sphere4"], k=random.randint(1, 2)) + msg = { + "action": "show_alert", + "floor": floor, + "lights": lights + } + # 发送给所有连接的 Vue 客户端 + for client in list(connected_clients): + try: + await client.send_json(msg) + except: + pass + return msg + +# 3. MCP 工具定义 +@mcp_server.list_tools() +async def list_tools(): + return [ + Tool( + name="query_crowded_floor", + description="查询哪一层出现拥挤情况,返回楼层编号", + inputSchema={"type": "object", "properties": {}, "required": []} + ), + Tool( + name="show_alert_on_floor", + description="在三维平台展示告警效果,跳转到指定楼层并显示拥挤情况", + inputSchema={ + "type": "object", + "properties": { + "floor": {"type": "string", "description": "楼层编号,如 F9、F8、B1 等"} + }, + "required": ["floor"] + } + ) + ] + +@mcp_server.call_tool() +async def call_tool(name: str, arguments: dict): + try: + if name == "query_crowded_floor": + return [TextContent(type="text", text="第九层")] + + if name == "show_alert_on_floor": + floor = arguments.get("floor", "F9") + msg = await send_alert(floor) + return [TextContent(type="text", text=f"已在 {floor} 层触发告警,点亮了 {msg['lights']}")] + + return [TextContent(type="text", text="未知工具")] + except Exception as e: + return [TextContent(type="text", text=f"执行失败: {str(e)}")] + +# 4. FastAPI 路由注册 +app = FastAPI(title="园区3D控制MCP服务") +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) + +# 注册 SSE 端点 (Cherry Studio 连接此地址) +app.add_route("/sse", mcp_endpoint, methods=["GET"]) +# 别名,防止 Cherry Studio 尝试 /mcp/sse +app.add_route("/mcp/sse", mcp_endpoint, methods=["GET"]) +# 挂载消息处理路由 +app.mount("/messages/", sse.handle_post_message) +app.mount("/mcp/messages/", sse.handle_post_message) + +# WebSocket 端点 (Vue 前端连接此地址) +@app.websocket("/ws") +async def websocket_endpoint(websocket: WebSocket): + await websocket.accept() + connected_clients.append(websocket) + try: + while True: + await websocket.receive_text() + except WebSocketDisconnect: + if websocket in connected_clients: + connected_clients.remove(websocket) + +# HTTP 测试接口 +@app.get("/") +async def root(): + return {"message": "MCP SSE 服务运行中", "ws_clients": len(connected_clients)} + +@app.get("/trigger_alert/{floor}") +async def http_trigger(floor: str): + """供网页直接测试触发""" + msg = await send_alert(floor) + return msg + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file