feat(lark_mcp): 更新飞书卡片消息发送工具以支持单接收者

- 将send_card_message工具从接收ID列表改为单个接收者ID
- 修复test.py中导入并调用send_card_message的测试逻辑
- 新增机器人充电、终止任务、通知播放功能
- 扩展迎宾和巡逻功能参数,提供默认值和更灵活的配置
- 更新项目版本号至0.1.13和0.1.9
This commit is contained in:
2026-03-19 10:32:09 +08:00
parent 3d04166314
commit 894ee1dfbf
6 changed files with 135 additions and 50 deletions

View File

@@ -52,16 +52,13 @@ tools = [
), ),
Tool( Tool(
name="send_card_message", name="send_card_message",
description="发送消息卡片入参为receiver_ids、person_id和image_key", description="发送消息卡片入参为receiver_id、person_id和image_key",
inputSchema={ inputSchema={
"type": "object", "type": "object",
"properties": { "properties": {
"receiver_ids": { "receiver_id": {
"type": "array", "type": "string",
"description": "消息接收者ID列表", "description": "消息接收者ID"
"items": {
"type": "string"
}
}, },
"person_id": { "person_id": {
"type": "string", "type": "string",
@@ -72,7 +69,7 @@ tools = [
"description": "图片image_key" "description": "图片image_key"
} }
}, },
"required": ["image_key"] "required": ["receiver_id", "image_key"]
} }
), ),
Tool( Tool(
@@ -1766,18 +1763,7 @@ def send_card_message(token: str, receiver_id: str, person_id: str, image_key: s
return message_id return message_id
def send_card_messages(token: str, receiver_ids: List[str], person_id: str, image_key: str) -> str:
if not receiver_ids:
raise ValueError("missing receiver_ids")
message_ids = []
for receiver_id in receiver_ids:
normalized_receiver_id = str(receiver_id).strip()
if not normalized_receiver_id:
continue
message_ids.append(send_card_message(token, normalized_receiver_id, person_id, image_key))
if not message_ids:
raise ValueError("missing receiver_ids")
return json.dumps(message_ids, ensure_ascii=False)
async def handle_call_tool(name: str, arguments: Dict[str, object], token: str) -> List[TextContent]: async def handle_call_tool(name: str, arguments: Dict[str, object], token: str) -> List[TextContent]:
@@ -1816,16 +1802,13 @@ async def handle_call_tool(name: str, arguments: Dict[str, object], token: str)
result = upload_image_by_file(token, image_source, image_type) result = upload_image_by_file(token, image_source, image_type)
elif name == "send_card_message": elif name == "send_card_message":
image_key = str(arguments.get("image_key", "")).strip() image_key = str(arguments.get("image_key", "")).strip()
receiver_ids = arguments.get("receiver_ids") receiver_id = str(arguments.get("receiver_id", "")).strip()
person_id = str(arguments.get("person_id", "")).strip() person_id = str(arguments.get("person_id", "")).strip()
if not receiver_id:
raise ValueError("missing receiver_id")
if not image_key: if not image_key:
raise ValueError("missing image_key") raise ValueError("missing image_key")
if not person_id: result = send_card_message(token, receiver_id, person_id, image_key)
raise ValueError("missing person_id")
if receiver_ids is not None:
if not isinstance(receiver_ids, list):
raise ValueError("receiver_ids must be a list")
result = send_card_messages(token, receiver_ids, person_id, image_key)
elif name == "send_confirmation_card": elif name == "send_confirmation_card":
user_id = str(arguments.get("user_id", "")).strip() user_id = str(arguments.get("user_id", "")).strip()
order_number = str(arguments.get("order_number", "")).strip() order_number = str(arguments.get("order_number", "")).strip()

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lzwcai-lark-mcp" name = "lzwcai-lark-mcp"
version = "0.1.11" version = "0.1.13"
description = "Lark MCP server" description = "Lark MCP server"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [

View File

@@ -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_stranger_card from lzwcai_lark_mcp.tools import send_card_message, 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,6 +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"
receiver_id = os.getenv("receiver_id", user_id)
person_id = os.getenv("person_id", "")
image_key = os.getenv("image_key", "").strip()
if not image_key:
raise RuntimeError("missing image_key")
card_message_id = send_card_message(
token,
receiver_id,
person_id,
image_key
)
print(card_message_id)
result = send_stranger_card( result = send_stranger_card(
token, token,
user_id, user_id,

View File

@@ -23,6 +23,24 @@ async def serve() -> None:
async def list_tools() -> list[Tool]: async def list_tools() -> list[Tool]:
"""列出所有工具""" """列出所有工具"""
return [ return [
Tool(
name="recharge",
description="轮足机器人充电",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool(
name="terminate",
description="轮足机器人终止当前任务",
inputSchema={
"type": "object",
"properties": {},
"required": []
}
),
Tool( Tool(
name="goto", name="goto",
description="轮足机器人导航到指定地点为用户引路。触发关键词:带我去、导航、引路、带路、怎么走、在哪里。", description="轮足机器人导航到指定地点为用户引路。触发关键词:带我去、导航、引路、带路、怎么走、在哪里。",
@@ -61,16 +79,41 @@ async def serve() -> None:
"properties": { "properties": {
"location": { "location": {
"type": "string", "type": "string",
"description": "引导接待客人到这个位置", "description": "在这个位置接待贵宾",
"minLength": 1 "minLength": 1
}, },
"name": { "name": {
"type": "string", "type": "string",
"description": "客人姓名", "description": "客人姓名",
"minLength": 1 "minLength": 1
},
"destination": {
"type": "string",
"description": "接待贵宾到这个位置",
"minLength": 1
} }
}, },
"required": ["location", "name"] "required": ["location", "name", "destination"]
}
),
Tool(
name="notification",
description="轮足机器人去指定地点播放通知:通知、去那里说、去某地播报、传达消息",
inputSchema={
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "机器人需要前往的目标地点名称",
"minLength": 1
},
"text": {
"type": "string",
"description": "到达地点后需要播放的文本内容",
"minLength": 1
}
},
"required": ["location", "text"]
} }
), ),
Tool( Tool(
@@ -84,19 +127,23 @@ async def serve() -> None:
), ),
Tool( Tool(
name="patrol", name="patrol",
description="轮足机器人、助手、机器人去巡逻:巡逻、巡查、去检查一下、去看看、去巡视", description="轮足机器人、助手、机器人去巡逻:巡逻、巡查、去检查一下、去看看、去巡视。支持按路线巡逻或随机巡逻。",
inputSchema={ inputSchema={
"type": "object", "type": "object",
"properties": { "properties": {
"locations": { "locations": {
"type": "array", "type": "array",
"description": "机器人巡逻经过的地点列表", "description": "机器人巡逻经过的地点列表。如果不提供,则默认进行随机巡逻。",
"items": { "items": {
"type": "string" "type": "string"
} }
},
"flag": {
"type": "boolean",
"description": "是否随机巡逻。True为随机巡逻False为按locations指定的路线巡逻。"
} }
}, },
"required": ["locations"] "required": []
} }
) )
] ]
@@ -106,7 +153,11 @@ async def serve() -> None:
"""处理工具调用""" """处理工具调用"""
try: try:
result = "" result = ""
if name == "goto": if name == "recharge":
result = await nav_server.recharge()
elif name == "terminate":
result = await nav_server.terminate()
elif name == "goto":
if "location" not in arguments: if "location" not in arguments:
raise ValueError("缺少必要参数: location") raise ValueError("缺少必要参数: location")
result = await nav_server.goto( result = await nav_server.goto(
@@ -122,16 +173,25 @@ async def serve() -> None:
if "location" not in arguments: if "location" not in arguments:
raise ValueError("缺少必要参数: location") raise ValueError("缺少必要参数: location")
result = await nav_server.reception( result = await nav_server.reception(
location=arguments.get("location", "前台"),
name=arguments.get("name", "贵宾"),
destination=arguments.get("destination", "会议室")
)
elif name == "notification":
if "location" not in arguments or "text" not in arguments:
raise ValueError("缺少必要参数: location or text")
result = await nav_server.notification(
location=arguments["location"], location=arguments["location"],
name=arguments.get("name", "贵宾") text=arguments["text"]
) )
elif name == "repose": elif name == "repose":
result = await nav_server.repose() result = await nav_server.repose()
elif name == "patrol": elif name == "patrol":
if "locations" not in arguments: locations = arguments.get("locations", [])
raise ValueError("缺少必要参数: locations") flag = arguments.get("flag", True if not locations else False)
result = await nav_server.patrol( result = await nav_server.patrol(
locations=arguments["locations"] locations=locations,
flag=flag
) )
else: else:
raise ValueError(f"未知工具: {name}") raise ValueError(f"未知工具: {name}")

View File

@@ -31,6 +31,25 @@ class NavServer:
logger.error(f"Failed to publish command: {str(e)}", exc_info=True) logger.error(f"Failed to publish command: {str(e)}", exc_info=True)
return f"Failed to publish command: {str(e)}" return f"Failed to publish command: {str(e)}"
async def recharge(self):
"""轮足机器人充电"""
try:
params = {}
return await self.pub_cmd("temi-test", "recharge", params)
except Exception as e:
logger.error(f"Failed to call recharge mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call recharge mcp-tool: {str(e)}"
async def terminate(self):
"""轮足机器人终止当前任务"""
try:
params = {}
return await self.pub_cmd("temi-test", "terminate", params)
except Exception as e:
logger.error(f"Failed to call terminate mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call terminate mcp-tool: {str(e)}"
async def goto(self, location: str, flag: bool = False): async def goto(self, location: str, flag: bool = False):
"""轮足导航到指定位置""" """轮足导航到指定位置"""
try: try:
@@ -62,21 +81,34 @@ class NavServer:
logger.error(f"Failed to call speak mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call speak mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call speak mcp-tool: {str(e)}" return f"Failed to call speak mcp-tool: {str(e)}"
async def reception(self, location: str, name: str = "贵宾"): async def reception(self, location: str = "前台", name: str = "贵宾", destination: str = "会议室"):
"""轮足机器人移动到指定位置迎宾""" """轮足机器人移动到指定位置迎宾"""
try: try:
if not location:
return "Location is not specified."
params = { params = {
"location": location, "location": location,
"text": f"您好,{name},我是接待机器人。", "text": f"您好,{name},我是接待机器人。",
"destination": destination
} }
return await self.pub_cmd("temi-test", "reception", params) return await self.pub_cmd("temi-test", "reception", params)
except Exception as e: except Exception as e:
logger.error(f"Failed to call reception mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call reception mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call reception mcp-tool: {str(e)}" return f"Failed to call reception mcp-tool: {str(e)}"
async def notification(self, location: str, text: str):
"""轮足机器人移动到指定位置并播放通知文本"""
try:
if not location or not text:
return "Location or text is not specified."
params = {
"location": location,
"text": text
}
return await self.pub_cmd("temi-test", "notification", params)
except Exception as e:
logger.error(f"Failed to call notification mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call notification mcp-tool: {str(e)}"
async def repose(self): async def repose(self):
"""轮足机器人重新定位""" """轮足机器人重新定位"""
try: try:
@@ -86,14 +118,12 @@ class NavServer:
logger.error(f"Failed to call repose mcp-tool: {str(e)} ", exc_info=True) logger.error(f"Failed to call repose mcp-tool: {str(e)} ", exc_info=True)
return f"Failed to call repose mcp-tool: {str(e)}" return f"Failed to call repose mcp-tool: {str(e)}"
async def patrol(self, locations: list): async def patrol(self, locations: list = None, flag: bool = False):
"""轮足机器人巡逻""" """轮足机器人巡逻 why !!! """
try: try:
if not locations:
return "locations is not specified."
params = { params = {
"flag": False, "flag": True,
"locations": locations "locations": locations or []
} }
return await self.pub_cmd("temi-test", "patrol", params) return await self.pub_cmd("temi-test", "patrol", params)
except Exception as e: except Exception as e:

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "lzwcai_temi_mcp" name = "lzwcai_temi_mcp"
version = "0.1.1" version = "0.1.9"
description = "MQTT-based navigation server for robot" description = "MQTT-based navigation server for robot"
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [