feat: 新增轮足机器人导航MCP服务器
添加 lzwcai_temi_mcp 包,实现基于MQTT的轮足机器人导航控制服务器。包含以下功能: - 通过 MQTT 发布机器人控制命令 - 提供 goto、speak、reception、repose、patrol 等导航工具 - 支持通过 MCP 协议与AI助手集成 - 更新 lzwcai_lark_mcp 版本至 0.1.11
This commit is contained in:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "lzwcai-lark-mcp"
|
name = "lzwcai-lark-mcp"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
description = "Lark MCP server"
|
description = "Lark MCP server"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ def main() -> None:
|
|||||||
mcp_module.types = types_module
|
mcp_module.types = types_module
|
||||||
sys.modules["mcp"] = mcp_module
|
sys.modules["mcp"] = mcp_module
|
||||||
sys.modules["mcp.types"] = types_module
|
sys.modules["mcp.types"] = types_module
|
||||||
from lzwcai_lark_mcp.tools import send_asset_confirmation_card
|
from lzwcai_lark_mcp.tools import send_stranger_card
|
||||||
app_id = os.getenv("app_id", "")
|
app_id = os.getenv("app_id", "")
|
||||||
app_secret = os.getenv("app_secret", "")
|
app_secret = os.getenv("app_secret", "")
|
||||||
auth_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
|
auth_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
|
||||||
@@ -53,17 +53,18 @@ def main() -> None:
|
|||||||
if not token:
|
if not token:
|
||||||
raise RuntimeError(f"lark auth response missing token: {data}")
|
raise RuntimeError(f"lark auth response missing token: {data}")
|
||||||
user_id = "gegg1d78"
|
user_id = "gegg1d78"
|
||||||
result = send_asset_confirmation_card(
|
result = send_stranger_card(
|
||||||
token,
|
token,
|
||||||
user_id,
|
user_id,
|
||||||
"CONF-20260301-001",
|
"CONF-20260301-001",
|
||||||
"2026-03-01 10:30:00",
|
"2026-03-01 10:30:00",
|
||||||
[
|
[
|
||||||
{"华为手机": "huawei_phone"},
|
{"华为手机": "huawei_phone"},
|
||||||
{"红米手机": "redmi_phone"},
|
{"红米手机": "redmi_phone"}
|
||||||
"MacBook Pro"
|
|
||||||
],
|
],
|
||||||
"如有误报请点击反馈"
|
face_cap="img_v3_02vj_b25b040f-b6c1-49f4-a29d-a02c99a13a9g",
|
||||||
|
user_ids=["347f5e71", "gegg1d78"],
|
||||||
|
remark="如有误报请点击反馈"
|
||||||
)
|
)
|
||||||
print(result)
|
print(result)
|
||||||
|
|
||||||
|
|||||||
0
lzwcai_temi_mcp/__init__.py
Normal file
0
lzwcai_temi_mcp/__init__.py
Normal file
0
lzwcai_temi_mcp/lzwcai_temi_mcp/__init__.py
Normal file
0
lzwcai_temi_mcp/lzwcai_temi_mcp/__init__.py
Normal file
151
lzwcai_temi_mcp/lzwcai_temi_mcp/main.py
Normal file
151
lzwcai_temi_mcp/lzwcai_temi_mcp/main.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
from typing import Sequence
|
||||||
|
import logging
|
||||||
|
import asyncio
|
||||||
|
from mcp.server import Server
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
from mcp.types import Tool, TextContent
|
||||||
|
from .mcp_mqtt import get_mcpmqtt_handler
|
||||||
|
from .nav_server import NavServer
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
async def serve() -> None:
|
||||||
|
server = Server("terminal_temi_mcp")
|
||||||
|
mmhandler = get_mcpmqtt_handler()
|
||||||
|
nav_server = NavServer(mmhandler)
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def list_tools() -> list[Tool]:
|
||||||
|
"""列出所有工具"""
|
||||||
|
return [
|
||||||
|
Tool(
|
||||||
|
name="goto",
|
||||||
|
description="轮足机器人导航到指定地点为用户引路。触发关键词:带我去、导航、引路、带路、怎么走、在哪里。",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标地点名称",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["location"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="speak",
|
||||||
|
description="轮足机器人进行语音播报:告诉、提醒、告知、提示、通知",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"speech": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "要播报的语音内容",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["speech"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="reception",
|
||||||
|
description="轮足机器人去接待客人:去接人、请迎接客人、去接待、迎接一下、带人过来",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"location": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "引导接待客人到这个位置",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "客人姓名",
|
||||||
|
"minLength": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["location", "name"]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="repose",
|
||||||
|
description="轮足机器人、助手、机器人去重新定位",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"required": []
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Tool(
|
||||||
|
name="patrol",
|
||||||
|
description="轮足机器人、助手、机器人去巡逻:巡逻、巡查、去检查一下、去看看、去巡视",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"locations": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "机器人巡逻经过的地点列表",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["locations"]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def call_tool(name: str, arguments: dict) -> Sequence[TextContent]:
|
||||||
|
"""处理工具调用"""
|
||||||
|
try:
|
||||||
|
result = ""
|
||||||
|
if name == "goto":
|
||||||
|
if "location" not in arguments:
|
||||||
|
raise ValueError("缺少必要参数: location")
|
||||||
|
result = await nav_server.goto(
|
||||||
|
location=arguments["location"]
|
||||||
|
)
|
||||||
|
elif name == "speak":
|
||||||
|
if "speech" not in arguments:
|
||||||
|
raise ValueError("缺少必要参数: speech")
|
||||||
|
result = await nav_server.speak(
|
||||||
|
speech=arguments["speech"]
|
||||||
|
)
|
||||||
|
elif name == "reception":
|
||||||
|
if "location" not in arguments:
|
||||||
|
raise ValueError("缺少必要参数: location")
|
||||||
|
result = await nav_server.reception(
|
||||||
|
location=arguments["location"],
|
||||||
|
name=arguments.get("name", "贵宾")
|
||||||
|
)
|
||||||
|
elif name == "repose":
|
||||||
|
result = await nav_server.repose()
|
||||||
|
elif name == "patrol":
|
||||||
|
if "locations" not in arguments:
|
||||||
|
raise ValueError("缺少必要参数: locations")
|
||||||
|
result = await nav_server.patrol(
|
||||||
|
locations=arguments["locations"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ValueError(f"未知工具: {name}")
|
||||||
|
return [TextContent(type="text", text=result)]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"工具调用失败: {str(e)}")
|
||||||
|
raise ValueError(f"执行失败: {str(e)}")
|
||||||
|
|
||||||
|
options = server.create_initialization_options()
|
||||||
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(read_stream, write_stream, options)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
asyncio.run(serve())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
113
lzwcai_temi_mcp/lzwcai_temi_mcp/mcp_mqtt.py
Normal file
113
lzwcai_temi_mcp/lzwcai_temi_mcp/mcp_mqtt.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
from typing import Optional
|
||||||
|
import uuid
|
||||||
|
import threading
|
||||||
|
import requests
|
||||||
|
from os import getenv
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
MQTT_CLIENT_ID = f"MCPMQTT-{uuid.uuid4().hex[:8]}"
|
||||||
|
|
||||||
|
def getConfig_url(array):
|
||||||
|
url = "http://lzwcai-demp-corp-manager:8086/system/config/getConfig"
|
||||||
|
data = [array]
|
||||||
|
try:
|
||||||
|
response = requests.post(url, json=data, timeout=5)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()['data']
|
||||||
|
return data[0]['configValue']
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching config for {array}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
class MQTTHandler:
|
||||||
|
def __init__(self):
|
||||||
|
self.client = mqtt.Client()
|
||||||
|
self.tasks = {} # {task_id: task_data}
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
# 获取MQTT配置,提供默认值
|
||||||
|
self.mqtt_username = getenv('MQTT_USERNAME') or 'lzwc'
|
||||||
|
self.mqtt_password = getenv('MQTT_PASSWORD') or 'Lzwc@4187.'
|
||||||
|
mqtt_broker_raw = getenv('MQTT_BROKER') or 'emqx'
|
||||||
|
# 移除协议前缀,只保留主机名
|
||||||
|
self.mqtt_broker = mqtt_broker_raw.replace('tcp://', '').replace('mqtt://', '')
|
||||||
|
mqtt_port_str = getenv('MQTT_PORT')
|
||||||
|
self.mqtt_port = int(mqtt_port_str) if mqtt_port_str else 1883
|
||||||
|
|
||||||
|
# 记录配置信息
|
||||||
|
logger.info(f"MQTT配置 - Broker: {self.mqtt_broker}, Port: {self.mqtt_port}, Username: {self.mqtt_username}")
|
||||||
|
if not getenv('MQTT_BROKER'):
|
||||||
|
logger.warning("MQTT_BROKER环境变量未设置,使用默认值")
|
||||||
|
if not mqtt_port_str:
|
||||||
|
logger.warning("MQTT_PORT环境变量未设置,使用默认端口1883")
|
||||||
|
|
||||||
|
self.client.username_pw_set(self.mqtt_username, self.mqtt_password)
|
||||||
|
self.client.on_connect = self._on_connect
|
||||||
|
self.client.on_message = self._on_message
|
||||||
|
self.client.on_disconnect = self._on_disconnect
|
||||||
|
try:
|
||||||
|
logger.info(f"正在连接 MQTT 代理: {self.mqtt_broker}:{self.mqtt_port}")
|
||||||
|
self.client.connect(self.mqtt_broker, self.mqtt_port, 60)
|
||||||
|
self.client.loop_start()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"连接 MQTT 失败: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def _on_connect(self, client, userdata, flags, rc):
|
||||||
|
"""MQTT 连接回调"""
|
||||||
|
if rc == 0:
|
||||||
|
logger.info("已成功连接到 MQTT 代理服务器")
|
||||||
|
else:
|
||||||
|
logger.error(f"连接 MQTT 代理服务器失败,返回码: {rc}")
|
||||||
|
|
||||||
|
def get_status(self, task_id: str = None):
|
||||||
|
"""获取任务状态
|
||||||
|
Args:
|
||||||
|
task_id: 可选参数,指定任务ID。如果为None则返回所有任务状态
|
||||||
|
Returns:
|
||||||
|
如果指定task_id则返回该任务的状态,否则返回所有任务状态
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
if task_id:
|
||||||
|
return self.tasks.get(task_id, {}).copy()
|
||||||
|
return {k: v.copy() for k, v in self.tasks.items()}
|
||||||
|
|
||||||
|
def _on_message(self, client, userdata, msg):
|
||||||
|
try:
|
||||||
|
payload = json.loads(msg.payload.decode())
|
||||||
|
logger.info(f"[MQTT] topic={msg.topic} msg: {payload}")
|
||||||
|
|
||||||
|
# task_id = payload.get('task_id')
|
||||||
|
# if task_id and task_id in self.tasks:
|
||||||
|
# with self._lock:
|
||||||
|
# self.tasks[task_id].update({
|
||||||
|
# 'status': payload.get('status', 'UNKNOWN'),
|
||||||
|
# 'description': payload.get('description', '')
|
||||||
|
# })
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[错误] 处理消息失败: {e}")
|
||||||
|
|
||||||
|
def _on_disconnect(self, client, userdata, rc):
|
||||||
|
"""MQTT 断开连接回调"""
|
||||||
|
if rc != 0:
|
||||||
|
logger.warning("意外断开与 MQTT 代理服务器的连接。")
|
||||||
|
try:
|
||||||
|
logger.info("尝试重新连接 MQTT...")
|
||||||
|
client.reconnect()
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重新连接 MQTT 失败: {e}")
|
||||||
|
|
||||||
|
# 单例模式
|
||||||
|
_instance: Optional[MQTTHandler] = None
|
||||||
|
|
||||||
|
def get_mcpmqtt_handler() -> MQTTHandler:
|
||||||
|
"""获取单例"""
|
||||||
|
global _instance
|
||||||
|
if _instance is None:
|
||||||
|
_instance = MQTTHandler()
|
||||||
|
return _instance
|
||||||
102
lzwcai_temi_mcp/lzwcai_temi_mcp/nav_server.py
Normal file
102
lzwcai_temi_mcp/lzwcai_temi_mcp/nav_server.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
from typing import Optional, Sequence, List, Dict, Any
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from .mcp_mqtt import get_mcpmqtt_handler
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class NavServer:
|
||||||
|
def __init__(self, mmhandler=None):
|
||||||
|
self.mmhandler = mmhandler or get_mcpmqtt_handler()
|
||||||
|
|
||||||
|
async def pub_cmd(self, device_id: str, action: str, params: Dict[str, Any]):
|
||||||
|
"""
|
||||||
|
发送MQTT命令
|
||||||
|
:param device_id: 设备ID
|
||||||
|
:param action: 动作指令 (对应 Kotlin 中的 action/cmd/type)
|
||||||
|
:param params: 其他参数 (将被合并到根 JSON 对象中)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"device_id": device_id,
|
||||||
|
"action": action
|
||||||
|
}
|
||||||
|
if params:
|
||||||
|
payload.update(params)
|
||||||
|
|
||||||
|
logger.info(f"Publishing command: {action}, payload: {payload}")
|
||||||
|
self.mmhandler.client.publish("robot/cmd", json.dumps(payload), qos=2)
|
||||||
|
return f"{action} 任务已经下达完成"
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to publish command: {str(e)}", exc_info=True)
|
||||||
|
return f"Failed to publish command: {str(e)}"
|
||||||
|
|
||||||
|
async def goto(self, location: str, flag: bool = False):
|
||||||
|
"""轮足导航到指定位置"""
|
||||||
|
try:
|
||||||
|
if not location:
|
||||||
|
return "Location is not specified."
|
||||||
|
|
||||||
|
# Kotlin 端对应 action: "goto"
|
||||||
|
# Kotlin 端参数: location (or target)
|
||||||
|
params = {
|
||||||
|
"location": location
|
||||||
|
}
|
||||||
|
return await self.pub_cmd("temi-test", "goto", params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to call navigation mcp-tool: {str(e)} ", exc_info=True)
|
||||||
|
return f"Failed to call navigation mcp-tool: {str(e)}"
|
||||||
|
|
||||||
|
async def speak(self, speech: str):
|
||||||
|
"""轮足机器人语音播报"""
|
||||||
|
try:
|
||||||
|
if not speech:
|
||||||
|
return "Speech content is not specified."
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"text": speech,
|
||||||
|
"lang": "zh"
|
||||||
|
}
|
||||||
|
return await self.pub_cmd("temi-test", "speak", params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to call speak mcp-tool: {str(e)} ", exc_info=True)
|
||||||
|
return f"Failed to call speak mcp-tool: {str(e)}"
|
||||||
|
|
||||||
|
async def reception(self, location: str, name: str = "贵宾"):
|
||||||
|
"""轮足机器人移动到指定位置迎宾"""
|
||||||
|
try:
|
||||||
|
if not location:
|
||||||
|
return "Location is not specified."
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"location": location,
|
||||||
|
"text": f"您好,{name},我是接待机器人。",
|
||||||
|
}
|
||||||
|
return await self.pub_cmd("temi-test", "reception", params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to call reception mcp-tool: {str(e)} ", exc_info=True)
|
||||||
|
return f"Failed to call reception mcp-tool: {str(e)}"
|
||||||
|
|
||||||
|
async def repose(self):
|
||||||
|
"""轮足机器人重新定位"""
|
||||||
|
try:
|
||||||
|
params = {}
|
||||||
|
return await self.pub_cmd("temi-test", "repose", params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to call repose mcp-tool: {str(e)} ", exc_info=True)
|
||||||
|
return f"Failed to call repose mcp-tool: {str(e)}"
|
||||||
|
|
||||||
|
async def patrol(self, locations: list):
|
||||||
|
"""轮足机器人巡逻"""
|
||||||
|
try:
|
||||||
|
if not locations:
|
||||||
|
return "locations is not specified."
|
||||||
|
params = {
|
||||||
|
"flag": False,
|
||||||
|
"locations": locations
|
||||||
|
}
|
||||||
|
return await self.pub_cmd("temi-test", "patrol", params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to call patrol mcp-tool: {str(e)} ", exc_info=True)
|
||||||
|
return f"Failed to call patrol mcp-tool: {str(e)}"
|
||||||
|
|
||||||
13
lzwcai_temi_mcp/mcp-server.json
Normal file
13
lzwcai_temi_mcp/mcp-server.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"lzwcai_temi_mcp": {
|
||||||
|
"disabled": false,
|
||||||
|
"type": "stdio",
|
||||||
|
"timeout": 30,
|
||||||
|
"command": "uvx",
|
||||||
|
"args": [
|
||||||
|
"lzwcai_temi_mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
lzwcai_temi_mcp/pyproject.toml
Normal file
26
lzwcai_temi_mcp/pyproject.toml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "lzwcai_temi_mcp"
|
||||||
|
version = "0.1.1"
|
||||||
|
description = "MQTT-based navigation server for robot"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.95.0",
|
||||||
|
"uvicorn>=0.21.0",
|
||||||
|
"paho-mqtt>=2.0.0",
|
||||||
|
"pydantic>=1.10.0",
|
||||||
|
"python-dotenv>=0.21.0",
|
||||||
|
"mcp[cli]>=1.6.0",
|
||||||
|
"requests>=2.25.0"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
lzwcai_temi_mcp = "lzwcai_temi_mcp.main:main"
|
||||||
|
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["lzwcai_temi_mcp"]
|
||||||
|
|
||||||
Reference in New Issue
Block a user