diff --git a/pyproject.toml b/pyproject.toml index 3c15813..d70c389 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = [ "textual>=3.0", "httpx>=0.27", "websockets>=12.0", + "wecom-aibot-sdk>=1.0.8", ] [project.scripts] diff --git a/sample.env b/sample.env index 67c4e35..903585c 100644 --- a/sample.env +++ b/sample.env @@ -9,3 +9,8 @@ ENABLE_TOOL_SEARCH=0 CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 + +# WeCom (企业微信) Bot WebSocket channel +# Run: uv run agent-box --wecom +WECOM_BOT_ID= +WECOM_SECRET= \ No newline at end of file diff --git a/src/agent_box/agents/claude_code.py b/src/agent_box/agents/claude_code.py index 03202d4..4739655 100644 --- a/src/agent_box/agents/claude_code.py +++ b/src/agent_box/agents/claude_code.py @@ -410,6 +410,12 @@ def _build_options(self) -> ClaudeAgentOptions: ) if self.project.session_id: opts.resume = self.project.session_id + # Conditionally add wecom_mcp tool when WeCom channel is active + from ..tools.wecom_mcp import is_wecom_mcp_enabled + if is_wecom_mcp_enabled(): + from ..tools.wecom_mcp import create_wecom_mcp_server + opts.mcp_servers = {"wecom_mcp": create_wecom_mcp_server()} + opts.allowed_tools = ["wecom_mcp"] return opts async def _ensure_client(self) -> ClaudeSDKClient: diff --git a/src/agent_box/channels/wecom.py b/src/agent_box/channels/wecom.py new file mode 100644 index 0000000..3b56516 --- /dev/null +++ b/src/agent_box/channels/wecom.py @@ -0,0 +1,297 @@ +"""WeCom (企业微信) channel adapter — WebSocket long connection mode. + +Uses the official ``wecom-aibot-sdk`` Python SDK which connects via WebSocket +to wss://openws.work.weixin.qq.com. Only requires two credentials: + +Required config: + WECOM_BOT_ID — 机器人 ID (from 企业微信管理后台 → 智能机器人) + WECOM_SECRET — 机器人 Secret +""" + +from __future__ import annotations + +import asyncio +import logging +import mimetypes +from pathlib import Path +from typing import Any + +from anyio.abc import ObjectSendStream + +from wecom_aibot_sdk import WSClient, generate_req_id +from wecom_aibot_sdk.types import WsFrame, MessageType as WeComMsgType + +from ..config import settings +from ..models import IncomingMessage, MessageType, OutgoingMessage +from .base import BaseChannel + +log = logging.getLogger(__name__) + +_IMAGE_EXTS = frozenset({".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"}) +_AUDIO_EXTS = frozenset({".mp3", ".wav", ".ogg", ".flac", ".aac", ".m4a", ".amr"}) + + +def _detect_media_type(file_path: str) -> str: + ext = Path(file_path).suffix.lower() + if ext in _IMAGE_EXTS: + return "image" + if ext in _AUDIO_EXTS: + return "voice" + return "file" + + +# ── Message parsing helpers ────────────────────────────────────────────────── + +def _parse_message_body(body: dict[str, Any]) -> tuple[str, list[str]]: + """Parse WeCom message body into (text, image_urls). + + Returns extracted text and list of image URLs that need to be downloaded. + """ + msgtype = body.get("msgtype", "") + text_parts: list[str] = [] + image_urls: list[str] = [] + + if msgtype == "mixed" and body.get("mixed"): + # 图文混排消息 + for item in body["mixed"].get("msg_item", []): + if item.get("msgtype") == "text" and item.get("text", {}).get("content"): + text_parts.append(item["text"]["content"]) + elif item.get("msgtype") == "image" and item.get("image", {}).get("url"): + image_urls.append(item["image"]["url"]) + else: + # 单条消息 + if body.get("text", {}).get("content"): + text_parts.append(body["text"]["content"]) + if msgtype == "voice" and body.get("voice", {}).get("content"): + # 语音转文字 + text_parts.append(body["voice"]["content"]) + if body.get("image", {}).get("url"): + image_urls.append(body["image"]["url"]) + + # 处理引用消息 + quote = body.get("quote") + if quote: + if quote.get("msgtype") == "text" and quote.get("text", {}).get("content"): + if not text_parts: + text_parts.append(quote["text"]["content"]) + elif quote.get("msgtype") == "voice" and quote.get("voice", {}).get("content"): + if not text_parts: + text_parts.append(quote["voice"]["content"]) + elif quote.get("msgtype") == "image" and quote.get("image", {}).get("url"): + image_urls.append(quote["image"]["url"]) + + text = "\n".join(text_parts).strip() + + # 对于纯媒体消息(无文本),生成描述 + if not text and (image_urls or msgtype in ("file", "video")): + label = {"image": "图片", "voice": "语音", "video": "视频", "file": "文件"}.get(msgtype, "文件") + text = f"[用户发送了{label}]" + + return text, image_urls + + +# ── WecomChannel ───────────────────────────────────────────────────────────── + +class WecomChannel(BaseChannel): + """WeCom Bot WebSocket channel using official wecom-aibot-sdk.""" + + def __init__(self, send_stream: ObjectSendStream[IncomingMessage]) -> None: + super().__init__(send_stream) + self._client: WSClient | None = None + self._download_dir = settings.config_dir / "channels" / "wecom" / "downloads" + self._download_dir.mkdir(parents=True, exist_ok=True) + + def _check_config(self) -> bool: + missing = [ + name for name, val in [ + ("WECOM_BOT_ID", settings.wecom_bot_id), + ("WECOM_SECRET", settings.wecom_secret), + ] if not val + ] + if missing: + log.error("WeCom channel: missing config: %s", ", ".join(missing)) + return False + return True + + async def start(self) -> None: + if not self._check_config(): + await self.send_stream.aclose() + return + + self._client = WSClient( + bot_id=settings.wecom_bot_id, + secret=settings.wecom_secret, + heartbeat_interval=30000, + max_reconnect_attempts=10, + max_auth_failure_attempts=5, + ) + + # Register event handlers + self._client.on("connected", self._on_connected) + self._client.on("authenticated", self._on_authenticated) + self._client.on("disconnected", self._on_disconnected) + self._client.on("error", self._on_error) + self._client.on("message", self._on_message) + self._client.on("event", self._on_event) + + try: + await self._client.connect() + # Keep running until cancelled + while True: + await asyncio.sleep(1) + except asyncio.CancelledError: + pass + except Exception: + log.exception("wecom: unexpected error in start loop") + finally: + if self._client: + await self._client.disconnect() + await self.send_stream.aclose() + + # ── Event handlers ──────────────────────────────────────────────── + + def _on_connected(self) -> None: + log.info("wecom: WebSocket connected") + + def _on_authenticated(self) -> None: + log.info("wecom: authenticated successfully") + # Expose WSClient to the wecom_mcp tool + from ..tools.wecom_mcp import set_ws_client + set_ws_client(self._client) + + def _on_disconnected(self, reason: str) -> None: + log.warning("wecom: disconnected: %s", reason) + from ..tools.wecom_mcp import set_ws_client + set_ws_client(None) + + def _on_error(self, error: Exception) -> None: + log.error("wecom: error: %s", error) + + async def _on_message(self, frame: WsFrame) -> None: + """Handle incoming message callback.""" + body = frame.get("body") or {} + msgtype = body.get("msgtype", "") + from_user = body.get("from", {}).get("userid", "") + chat_id = body.get("chatid") or from_user + + if not from_user: + return + + text, image_urls = _parse_message_body(body) + + # Download images to local files + if image_urls: + for url in image_urls: + aes_key = body.get("image", {}).get("aeskey") + local_path = await self._download_media(url, aes_key) + if local_path: + text += f"\n文件路径: {local_path}" + + if not text: + return + + log.debug("wecom: inbound from %s: %s", from_user, text[:80]) + await self.send_stream.send( + IncomingMessage( + text=text, + user_id=from_user, + channel="wecom", + raw={ + "frame": frame, + "chat_id": chat_id, + "chat_type": body.get("chattype", "single"), + }, + ) + ) + + async def _on_event(self, frame: WsFrame) -> None: + """Handle event callbacks (template card clicks, etc.).""" + body = frame.get("body") or {} + event = body.get("event", {}) + event_type = event.get("eventtype", "") + + # For now, log events but don't route them + log.debug("wecom: event received: type=%s", event_type) + + # ── Media download ──────────────────────────────────────────────── + + async def _download_media(self, url: str, aes_key: str | None = None) -> str | None: + """Download and optionally decrypt a media file.""" + if not self._client: + return None + try: + result = await self._client.download_file(url, aes_key) + buffer: bytes = result["buffer"] + filename: str | None = result.get("filename") + + if not filename: + # Guess extension from content + import magic # noqa: F401 - optional + ext = ".bin" + filename = f"media_{generate_req_id('dl')}{ext}" + + dest = self._download_dir / filename + dest.write_bytes(buffer) + return str(dest) + except ImportError: + # No python-magic, use a generic extension + pass + except Exception: + log.exception("wecom: failed to download media from %s", url) + return None + + # ── Outbound reply ──────────────────────────────────────────────── + + async def send_reply(self, msg: OutgoingMessage) -> None: + if not self._client or not self._client.is_connected: + log.warning("wecom: cannot send reply, not connected") + return + + if msg.type != MessageType.text: + return + + raw = msg.data or {} + chat_id = raw.get("chat_id") or msg.user_id + file_path: str | None = raw.get("file_path") or raw.get("image_path") + + if file_path: + await self._send_file(chat_id, file_path) + return + + # Send text as markdown via proactive send + try: + await self._client.send_message(chat_id, { + "msgtype": "markdown", + "markdown": {"content": msg.text}, + }) + except Exception: + log.exception("wecom: failed to send text to %s", chat_id) + + async def _send_file(self, chat_id: str, file_path: str) -> None: + """Upload and send a file/image.""" + if not self._client: + return + + try: + p = Path(file_path) + if not p.exists(): + log.error("wecom: file not found: %s", file_path) + return + + media_type = _detect_media_type(file_path) + file_data = p.read_bytes() + + upload_result = await self._client.upload_media( + file_data, + type=media_type, # type: ignore[arg-type] + filename=p.name, + ) + media_id = upload_result["media_id"] + + await self._client.send_media_message( + chat_id, + media_type=media_type, # type: ignore[arg-type] + media_id=media_id, + ) + except Exception: + log.exception("wecom: failed to send file %s to %s", file_path, chat_id) diff --git a/src/agent_box/config.py b/src/agent_box/config.py index 52aee95..69060ed 100644 --- a/src/agent_box/config.py +++ b/src/agent_box/config.py @@ -20,6 +20,10 @@ class Settings(BaseSettings): qqbot_app_id: str = "" qqbot_client_secret: str = "" + # WeCom (企业微信) Bot WebSocket channel (long connection mode) + wecom_bot_id: str = "" + wecom_secret: str = "" + # GLM (ZhipuAI) ASR — voice-to-text for audio attachments. Empty skips transcription. glm_api_key: str = "" glm_asr_model: str = "glm-asr-2512" diff --git a/src/agent_box/main.py b/src/agent_box/main.py index 2d9cac4..97a1637 100644 --- a/src/agent_box/main.py +++ b/src/agent_box/main.py @@ -23,6 +23,7 @@ def __init__(self) -> None: self.sessions = SessionManager(settings.workspace_dir) self.router = Router(self.sessions) self.agents: dict[str, BaseAgent] = {} + self.channel_types: list[str] = [] def _get_or_create_agent(self, name: str) -> BaseAgent: if name not in self.agents: @@ -39,6 +40,9 @@ def _create_channel(self, channel_type: str, send_in: anyio.abc.ObjectSendStream elif channel_type == "qq": from .channels.qq import QQChannel return QQChannel(send_in) + elif channel_type == "wecom": + from .channels.wecom import WecomChannel + return WecomChannel(send_in) else: from .channels.weixin import WeixinChannel return WeixinChannel(send_in) @@ -95,6 +99,13 @@ async def run(self, channel_types: list[str] | None = None) -> None: if not channel_types: channel_types = ["weixin"] + self.channel_types = channel_types + + # Enable wecom_mcp tool when wecom channel is active + if "wecom" in channel_types: + from .tools.wecom_mcp import set_wecom_mcp_enabled + set_wecom_mcp_enabled(True) + send_in, recv_in = anyio.create_memory_object_stream[IncomingMessage](16) send_out, recv_out = anyio.create_memory_object_stream[OutgoingMessage](16) @@ -237,6 +248,32 @@ def _setup_logging(channel: str) -> None: def main() -> None: + if "--help" in sys.argv or "-h" in sys.argv: + print("""agent-box — IM → Router → Agent pipeline for managing coding projects via chat + +Usage: agent-box [OPTIONS] + +Channel options (at least one required, defaults to --weixin): + --weixin Enable WeChat (微信) channel + --wecom Enable WeCom (企业微信) WebSocket channel + --qq Enable QQ Bot channel + --tui Enable terminal UI channel (for local testing) + +Multiple channels can be enabled simultaneously: + agent-box --wecom --tui + +Other options: + --test-router Launch interactive router REPL for testing + -h, --help Show this help message + +Environment variables (see sample.env): + WECOM_BOT_ID, WECOM_SECRET WeCom bot credentials + QQBOT_APP_ID, QQBOT_CLIENT_SECRET QQ bot credentials + WEIXIN_ACCOUNT_ID WeChat account ID + ANTHROPIC_AUTH_TOKEN Anthropic API key +""") + return + if "--test-router" in sys.argv: _setup_logging("test-router") try: @@ -245,12 +282,14 @@ def main() -> None: pass return - # Parse channel flags: --qq --weixin --tui + # Parse channel flags: --qq --weixin --tui --wecom channel_types: list[str] = [] if "--qq" in sys.argv: channel_types.append("qq") if "--tui" in sys.argv: channel_types.append("tui") + if "--wecom" in sys.argv: + channel_types.append("wecom") if "--weixin" in sys.argv or not channel_types: channel_types.append("weixin") diff --git a/src/agent_box/tools/__init__.py b/src/agent_box/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/agent_box/tools/wecom_mcp.py b/src/agent_box/tools/wecom_mcp.py new file mode 100644 index 0000000..8462d48 --- /dev/null +++ b/src/agent_box/tools/wecom_mcp.py @@ -0,0 +1,349 @@ +"""wecom_mcp — SDK MCP tool for calling WeCom MCP Server. + +Provides two actions: + - list: List available tools in a given category (doc, contact, schedule, etc.) + - call: Call a specific tool method with JSON arguments + +The tool communicates with the WeCom MCP Server via: +1. WebSocket: send `aibot_get_mcp_config` to get the HTTP URL for a category +2. HTTP JSON-RPC: call the MCP Server using Streamable HTTP protocol + +This module exposes: + - `create_wecom_mcp_server()` — returns an McpSdkServerConfig for ClaudeAgentOptions + - `set_ws_client()` / `get_ws_client()` — manage the global WSClient reference + - `wecom_mcp_enabled` / `set_wecom_mcp_enabled()` — flag for whether wecom channel is active +""" + +from __future__ import annotations + +import json +import logging +from typing import Any + +import httpx + +from claude_agent_sdk import tool, create_sdk_mcp_server +from claude_agent_sdk.types import McpSdkServerConfig + +from wecom_aibot_sdk import WSClient, generate_req_id +from wecom_aibot_sdk.types import WsCmd + +log = logging.getLogger(__name__) + +# ── Enabled flag ───────────────────────────────────────────────────────────── +# Set by App.run() when wecom channel is in the active channel list. + +_enabled: bool = False + + +def set_wecom_mcp_enabled(enabled: bool) -> None: + """Set whether wecom_mcp tool should be active.""" + global _enabled + _enabled = enabled + + +def is_wecom_mcp_enabled() -> bool: + """Check if wecom_mcp tool is currently enabled.""" + return _enabled + +# ── Global WSClient reference ──────────────────────────────────────────────── +# Set by WecomChannel when the WebSocket connection is established. + +_ws_client: WSClient | None = None + + +def set_ws_client(client: WSClient | None) -> None: + """Store the active WSClient for use by the MCP tool.""" + global _ws_client + _ws_client = client + + +def get_ws_client() -> WSClient | None: + """Get the currently active WSClient.""" + return _ws_client + + +# ── MCP Config cache ───────────────────────────────────────────────────────── + +_mcp_config_cache: dict[str, str] = {} # category → URL + + +def clear_mcp_cache() -> None: + """Clear all cached MCP config URLs.""" + _mcp_config_cache.clear() + + +async def _get_mcp_url(category: str) -> str: + """Fetch MCP Server URL for a category via WSClient. + + Sends `aibot_get_mcp_config` command through the WebSocket connection + to retrieve the HTTP endpoint for the given MCP category. + """ + if category in _mcp_config_cache: + return _mcp_config_cache[category] + + client = get_ws_client() + if not client or not client.is_connected: + raise RuntimeError("WeCom WebSocket 未连接,无法获取 MCP 配置") + + req_id = generate_req_id("mcp_config") + response = await client.reply( + {"headers": {"req_id": req_id}}, + {"biz_type": category}, + "aibot_get_mcp_config", + ) + + errcode = response.get("errcode", -1) + if errcode != 0: + raise RuntimeError( + f"MCP 配置请求失败: errcode={errcode}, errmsg={response.get('errmsg', '')}" + ) + + body = response.get("body") or {} + url = body.get("url") + if not url: + raise RuntimeError(f"MCP 配置响应缺少 url 字段 (category={category!r})") + + _mcp_config_cache[category] = url + log.info("wecom_mcp: config fetched for category=%s url=%s", category, url) + return url + + +# ── Streamable HTTP session management ─────────────────────────────────────── + +# category → session_id (None = stateless) +_sessions: dict[str, str | None] = {} +_stateless_categories: set[str] = set() + +_HTTP_TIMEOUT = 30.0 +_INIT_TIMEOUT = 15.0 + + +async def _send_jsonrpc( + url: str, + method: str, + params: dict[str, Any] | None = None, + session_id: str | None = None, + timeout: float = _HTTP_TIMEOUT, +) -> tuple[Any, str | None]: + """Send a JSON-RPC request to the MCP Server. + + Returns (result, new_session_id). + """ + body: dict[str, Any] = { + "jsonrpc": "2.0", + "id": generate_req_id("mcp_rpc"), + "method": method, + } + if params is not None: + body["params"] = params + + headers: dict[str, str] = { + "Content-Type": "application/json", + "Accept": "application/json, text/event-stream", + } + if session_id: + headers["Mcp-Session-Id"] = session_id + + async with httpx.AsyncClient(timeout=timeout) as client: + resp = await client.post(url, json=body, headers=headers) + + new_session_id = resp.headers.get("mcp-session-id") + + if resp.status_code == 204 or not resp.text.strip(): + return None, new_session_id + + if not resp.is_success: + raise RuntimeError(f"MCP HTTP 请求失败: {resp.status_code} {resp.reason_phrase}") + + content_type = resp.headers.get("content-type", "") + + # Handle SSE response + if "text/event-stream" in content_type: + result = _parse_sse(resp.text) + return result, new_session_id + + # Normal JSON response + data = resp.json() + if "error" in data and data["error"]: + err = data["error"] + raise RuntimeError(f"MCP 调用错误 [{err.get('code', '?')}]: {err.get('message', '')}") + return data.get("result"), new_session_id + + +def _parse_sse(text: str) -> Any: + """Parse SSE response, extract last event's data as JSON-RPC result.""" + lines = text.split("\n") + current_parts: list[str] = [] + last_data = "" + + for line in lines: + if line.startswith("data: "): + current_parts.append(line[6:]) + elif line.startswith("data:"): + current_parts.append(line[5:]) + elif line.strip() == "" and current_parts: + last_data = "\n".join(current_parts).strip() + current_parts = [] + + if current_parts: + last_data = "\n".join(current_parts).strip() + + if not last_data: + raise RuntimeError("SSE 响应中未包含有效数据") + + rpc = json.loads(last_data) + if "error" in rpc and rpc["error"]: + err = rpc["error"] + raise RuntimeError(f"MCP 调用错误 [{err.get('code', '?')}]: {err.get('message', '')}") + return rpc.get("result") + + +async def _ensure_session(category: str, url: str) -> str | None: + """Ensure MCP session is initialized for a category. Returns session_id.""" + if category in _stateless_categories: + return None + if category in _sessions: + return _sessions[category] + + # Initialize session + init_params = { + "protocolVersion": "2025-03-26", + "capabilities": {}, + "clientInfo": {"name": "wecom_mcp", "version": "1.0.0"}, + } + _, session_id = await _send_jsonrpc( + url, "initialize", init_params, timeout=_INIT_TIMEOUT + ) + + if not session_id: + # Stateless server + _stateless_categories.add(category) + _sessions[category] = None + return None + + # Send initialized notification + notify_body: dict[str, Any] = { + "jsonrpc": "2.0", + "method": "notifications/initialized", + } + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "Mcp-Session-Id": session_id, + } + async with httpx.AsyncClient(timeout=_INIT_TIMEOUT) as client: + await client.post(url, json=notify_body, headers=headers) + + _sessions[category] = session_id + log.info("wecom_mcp: session established for category=%s", category) + return session_id + + +async def _mcp_request(category: str, method: str, params: dict[str, Any] | None = None) -> Any: + """High-level MCP request with session management and retry on 404.""" + url = await _get_mcp_url(category) + session_id = await _ensure_session(category, url) + + try: + result, new_sid = await _send_jsonrpc(url, method, params, session_id) + if new_sid: + _sessions[category] = new_sid + return result + except RuntimeError as e: + # Session expired (404) — rebuild and retry once + if "404" in str(e): + _sessions.pop(category, None) + session_id = await _ensure_session(category, url) + result, new_sid = await _send_jsonrpc(url, method, params, session_id) + if new_sid: + _sessions[category] = new_sid + return result + raise + + +# ── Tool definitions ───────────────────────────────────────────────────────── + +@tool( + "wecom_mcp", + "调用企业微信 MCP 工具。支持两种操作:\n" + " - list: 列出指定品类的所有可用工具及其参数定义\n" + " - call: 调用指定品类的某个工具方法\n\n" + "品类包括: contact(通讯录), doc(文档), schedule(日程), " + "meeting(会议), todo(待办), msg(消息), smartsheet(智能表格)\n\n" + "示例:\n" + " list contact → 列出通讯录相关的所有工具\n" + " call doc createDocument {\"title\": \"会议纪要\"} → 创建文档", + { + "action": str, + "category": str, + "method": str, + "args": str, + }, +) +async def wecom_mcp_tool(args: dict[str, Any]) -> dict[str, Any]: + """Execute a wecom_mcp action (list or call).""" + action = args.get("action", "") + category = args.get("category", "") + method = args.get("method", "") + raw_args = args.get("args", "") + + if not category: + return _text_result({"error": "缺少 category 参数"}) + + try: + if action == "list": + result = await _mcp_request(category, "tools/list") + tools = (result or {}).get("tools", []) + return _text_result({ + "category": category, + "count": len(tools), + "tools": [ + { + "name": t.get("name"), + "description": t.get("description", ""), + "inputSchema": t.get("inputSchema"), + } + for t in tools + ], + }) + + elif action == "call": + if not method: + return _text_result({"error": "action 为 call 时必须提供 method 参数"}) + + # Parse args + call_args: dict[str, Any] = {} + if raw_args: + if isinstance(raw_args, str): + try: + call_args = json.loads(raw_args) + except json.JSONDecodeError as e: + return _text_result({"error": f"args 不是合法 JSON: {e}"}) + elif isinstance(raw_args, dict): + call_args = raw_args + + result = await _mcp_request(category, "tools/call", { + "name": method, + "arguments": call_args, + }) + return _text_result(result) + + else: + return _text_result({"error": f"未知操作: {action},支持 list 和 call"}) + + except Exception as e: + log.exception("wecom_mcp: action=%s category=%s method=%s failed", action, category, method) + return _text_result({"error": str(e)}) + + +def _text_result(data: Any) -> dict[str, Any]: + """Format result as MCP tool response.""" + return {"content": [{"type": "text", "text": json.dumps(data, ensure_ascii=False, indent=2)}]} + + +# ── Public API ─────────────────────────────────────────────────────────────── + +def create_wecom_mcp_server() -> McpSdkServerConfig: + """Create the wecom_mcp SDK MCP server config for ClaudeAgentOptions.""" + return create_sdk_mcp_server("wecom_mcp", tools=[wecom_mcp_tool]) diff --git a/uv.lock b/uv.lock index 027cea6..17daca1 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,7 @@ dependencies = [ { name = "pydantic-settings" }, { name = "textual" }, { name = "websockets" }, + { name = "wecom-aibot-sdk" }, ] [package.dev-dependencies] @@ -38,6 +39,7 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.0" }, { name = "textual", specifier = ">=3.0" }, { name = "websockets", specifier = ">=12.0" }, + { name = "wecom-aibot-sdk", specifier = ">=1.0.8" }, ] [package.metadata.requires-dev] @@ -1060,3 +1062,17 @@ wheels = [ { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, ] + +[[package]] +name = "wecom-aibot-sdk" +version = "1.0.8" +source = { registry = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple/" } +dependencies = [ + { name = "cryptography" }, + { name = "httpx" }, + { name = "websockets" }, +] +sdist = { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/45/72/e14f62362ecee2bae1fb13e7f48e000a39514c883f2b49568294624e0241/wecom_aibot_sdk-1.0.8.tar.gz", hash = "sha256:dd8ac2917e0b23a190a718b2c13f717acd242af487e2bf98a5ca40a117c133e6", size = 64863, upload-time = "2026-06-02T09:43:50.569Z" } +wheels = [ + { url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/packages/a8/82/8e342b64b29c52d9e92fa6f3596d53df5d2dd3dab871493ce0f871b01914/wecom_aibot_sdk-1.0.8-py3-none-any.whl", hash = "sha256:9577742c1b9d76232ece675342871e26c3f5a9d9617c7f860f951e2c6265c184", size = 28069, upload-time = "2026-06-02T09:43:49.316Z" }, +]