Skill: elegant1998/xiaozhi-mcp-server (version 2.0.8)

--- name: xiaozhi-mcp-server version: "2.0.8" description: 让小智智能盒子连接OpenClaw，成为你的智能语音助手 author: elegant1998 license: MIT --- # xiaozhi-mcp-server 让小智智能盒子连接OpenClaw，成为你的智能语音助手。 ## 一句话说明 安装后，小智说的话会传给OpenClaw处理，小智再把结果念给你听。 ## 功能 - 小智语音 → OpenClaw处理 → 语音播报结果 - 支持各种任务：查天气、写文档、搜信息... - 异步处理，长任务不超时 ## 安装 ```bash clawhub install xiaozhi-mcp-server ``` ## 使用 ### 1. 启动服务 ```bash ./scripts/start.sh ``` ### 2. 获取连接码 ```bash cat ~/.config/openclaw-mcp/token ``` ### 3. 小智配置 1. 瞄小智启动后，跟瞄小智说：微信绑定 ，这时候会出现绑定二维码，微信扫码，绑定 瞄小智服务号 2. 在瞄小智服务号的配置界面填入： - 服务器地址：你的服务器IP - 连接码：刚才获取的token - 端口：28765 3. 完成！现在对小智说话，欧克劳（OpenClaw）会帮你处理。 ## 示例对话 | 你对小智说 | OpenClaw(欧克劳)回复 | | -------------------------------- | ------------------------ | | "问下欧克劳，那边今天天气怎么样" | "北京今天晴，气温11度" | | "告诉欧克劳帮我写个会议纪要" | "会议纪要已写好，请查看" | | "欧克劳，查一下快递" | "您的快递正在派送中" | ## 配置 编辑 `config.yaml`： - `port`：端口号（默认28765） - `target_session`：OpenClaw会话地址 ## 文件 ``` xiaozhi-mcp-server/ ├── SKILL.md ├── README.md ├── config.yaml ├── package.json └── scripts/ ├── server.py ├── start.sh └── stop.sh ``` ## 作者 无敌哥@瞄小智 ## 许可证 MIT

path: README.md
content: # xiaozhi-mcp-server

让小智智能盒子连接欧克劳（OpenClaw），让欧克劳（OpenClaw）成为你的智能助手。

## 一句话说明

安装这个服务后，你对小智说的话会传给欧克劳（OpenClaw）处理，小智再把结果念给你听。

##### 你就真的多了个语音智能助理！

## 快速开始

### 1. 安装

```bash
clawhub install xiaozhi-mcp-server
```

### 2. 启动

```bash
cd ~/.openclaw/skills/xiaozhi-mcp-server
./scripts/start.sh
```

### 3. 获取连接码

```bash
cat ~/.config/openclaw-mcp/token
```

### 4. 瞄小智配置

1. 瞄小智启动后，跟瞄小智说：微信绑定  ，这时候会出现绑定二维码，微信扫码，绑定 瞄小智服务号

2. 在瞄小智服务号的配置界面填入：

- 服务器地址：你的服务器IP
- 连接码：刚才获取的token
- 端口：28765

3. 完成！现在对小智说话，欧克劳（OpenClaw）会帮你处理。

## 示例对话

| 你对小智说 | OpenClaw(欧克劳)回复 |
|-----------|---------|
| "问下欧克劳，那边今天天气怎么样" | "北京今天晴，气温11度" |
| "告诉欧克劳帮我写个会议纪要" | "会议纪要已写好，请查看" |
| "欧克劳，查一下快递" | "您的快递正在派送中" |

## 常见问题

**Q: 连接不上怎么办？**
检查防火墙是否开放 28765 端口。

**Q: 小智没反应？**
确认小智配置的服务器地址和连接码(token)是否正确。

**Q: 想换个端口？**
编辑 `config.yaml` 修改端口号。

## 相关链接

- [OpenClaw官网](https://openclaw.ai)
- [ClawHub技能市场](https://clawhub.com)
- [GitHub仓库](https://github.com/elegant1998/xiaozhi-mcp-server)

## 作者

无敌哥@瞄小智

## 许可证

MIT

path: _meta.json
content: ownerId: kn7daabf3nvty9rnz9ntcrre2h83bv5f
slug: xiaozhi-mcp-server
version: 2.0.6
publishedAt: 1774189942406

path: config.yaml
content: # xiaozhi-mcp-server Configuration
port: 28765
token: ""
target_session: "qqbot:c2c:1C9C1F3BE0CF030A30712F8DEC4D78D4"
task_timeout: 180
log_level: info


path: esp32/OpenClawMCP.h
content: /**
 * OpenClawMCP.h - OpenClaw MCP Client for ESP32
 * 
 * 用法：
 * 1. 在 xiaozhi-esp32 项目中添加此文件
 * 2. 修改配置参数
 * 3. 在主程序中调用 OpenClawMCP::begin()
 * 4. 在 loop() 中调用 OpenClawMCP::loop()
 */

#ifndef OPENCLAW_MCP_H
#define OPENCLAW_MCP_H

#include <WiFi.h>
#include <WebSocketsClient.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

class OpenClawMCP {
private:
    String host;
    int port;
    String token;
    WebSocketsClient webSocket;
    bool connected;
    unsigned long lastHeartbeat;
    
    // 心跳间隔
    const unsigned long HEARTBEAT_INTERVAL = 30000;
    
    // 回调函数
    typedef void (*MessageCallback)(const char* type, const char* taskId, const char* result);
    MessageCallback onMessage;
    
    // WebSocket事件处理
    static void webSocketEvent(WStype_t type, uint8_t * payload, size_t length) {
        OpenClawMCP* instance = (OpenClawMCP*)payload; // 简化处理
    }
    
public:
    OpenClawMCP() : connected(false), lastHeartbeat(0), onMessage(nullptr) {}
    
    /**
     * 初始化MCP客户端
     * @param serverHost 服务器IP
     * @param serverPort 服务器端口
     * @param authToken 认证Token
     */
    void begin(const char* serverHost, int serverPort, const char* authToken) {
        host = serverHost;
        port = serverPort;
        [REDACTED_SECRET];
        
        // 设置WebSocket
        String url = "/ws?token=" + String(token);
        webSocket.begin(host.c_str(), port, url);
        webSocket.onEvent([this](WStype_t type, uint8_t * payload, size_t length) {
            this->handleWsEvent(type, payload, length);
        });
        webSocket.setReconnectInterval(5000);
        webSocket.enableHeartbeat(30000, 10000, 2);
        
        Serial.println("[OpenClawMCP] 初始化完成");
    }
    
    /**
     * 主循环，需要在Arduino loop()中调用
     */
    void loop() {
        webSocket.loop();
        
        // 心跳
        unsigned long now = millis();
        if (connected && (now - lastHeartbeat > HEARTBEAT_INTERVAL)) {
            sendPing();
            lastHeartbeat = now;
        }
    }
    
    /**
     * 设置消息回调
     */
    void setCallback(MessageCallback callback) {
        onMessage = callback;
    }
    
    /**
     * 调用MCP工具
     * @param toolName 工具名称
     * @param message 消息内容
     * @param asyncMode 是否异步
     * @return 响应JSON字符串
     */
    String callTool(String toolName, String message, bool asyncMode = true) {
        HTTPClient http;
        String url = "http://" + host + ":" + String(port) + "/mcp";
        
        http.begin(url);
        http.addHeader("Authorization", "Bearer " + token);
        http.addHeader("Content-Type", "application/json");
        http.setTimeout(10000);
        
        // 构建请求
        StaticJsonDocument<512> doc;
        doc["jsonrpc"] = "2.0";
        doc["id"] = 1;
        doc["method"] = "tools/call";
        
        JsonObject params = doc.createNestedObject("params");
        params["name"] = toolName;
        
        JsonObject arguments = params.createNestedObject("arguments");
        arguments["message"] = message;
        arguments["async"] = asyncMode;
        
        String body;
        serializeJson(doc, body);
        
        int code = http.POST(body);
        String response = "";
        
        if (code == HTTP_CODE_OK) {
            response = http.getString();
        } else {
            Serial.printf("[OpenClawMCP] HTTP错误: %d\n", code);
        }
        
        http.end();
        return response;
    }
    
    /**
     * 是否已连接
     */
    bool isConnected() {
        return connected;
    }
    
private:
    void handleWsEvent(WStype_t type, uint8_t * payload, size_t length) {
        switch(type) {
            case WStype_CONNECTED:
                Serial.println("[OpenClawMCP] WebSocket连接成功");
                connected = true;
                break;
                
            case WStype_DISCONNECTED:
                Serial.println("[OpenClawMCP] WebSocket断开");
                connected = false;
                break;
                
            case WStype_TEXT:
                processMessage((char*)payload);
                break;
                
            default:
                break;
        }
    }
    
    void processMessage(const char* message) {
        StaticJsonDocument<4096> doc;
        deserializeJson(doc, message);
        
        const char* type = doc["type"];
        
        if (strcmp(type, "task_completed") == 0) {
            const char* taskId = doc["task_id"];
            const char* result = doc["result"];
            
            Serial.printf("[OpenClawMCP] 任务完成: %s\n", taskId);
            
            // 回调
            if (onMessage) {
                onMessage(type, taskId, result);
            }
        }
    }
    
    void sendPing() {
        if (!connected) return;
        
        StaticJsonDocument<64> doc;
        doc["type"] = "ping";
        
        char buffer[64];
        serializeJson(doc, buffer);
        webSocket.sendTXT(buffer);
        
        Serial.println("[OpenClawMCP] 发送心跳");
    }
};

#endif // OPENCLAW_MCP_H

path: esp32/README.md
content: # ESP32 WebSocket客户端

## 文件说明

| 文件 | 说明 |
|------|------|
| `xiaozhi_websocket_client.ino` | 完整Arduino示例 |
| `OpenClawMCP.h` | 可复用的类库 |

## 依赖库

在Arduino IDE中安装：

```
WebSocketsClient by Markus Sattler
ArduinoJson by Benoit Blanchon
```

## 使用方法

### 方式1：直接使用示例

1. 打开 `xiaozhi_websocket_client.ino`
2. 修改配置：
   ```cpp
   const char* WIFI_SSID = "你的WiFi";
   const char* WIFI_PASSWORD = "WiFi密码";
   const char* MCP_SERVER_HOST = "你的服务器IP";
   const char* MCP_TOKEN = "你的Token";
   ```
3. 上传到ESP32

### 方式2：集成到xiaozhi-esp32项目

1. 复制 `OpenClawMCP.h` 到项目目录
2. 在代码中引用：
   ```cpp
   #include "OpenClawMCP.h"
   
   OpenClawMCP mcp;
   
   void setup() {
       mcp.begin("服务器IP", 9000, "你的Token");
       mcp.setCallback(onMCPMessage);
   }
   
   void loop() {
       mcp.loop();
   }
   
   void onMCPMessage(const char* type, const char* taskId, const char* result) {
       // 收到任务结果，调用TTS播报
       if (strcmp(type, "task_completed") == 0) {
           speakText(result);  // 你的TTS函数
       }
   }
   ```

## 流程

```
用户说话
    ↓
调用 mcp.callTool("run_agent", "用户说的话", true)
    ↓
返回 task_id
    ↓
TTS播报 "任务进行中"
    ↓
WebSocket收到推送
    ↓
回调 onMCPMessage()
    ↓
TTS播报结果
```

## 测试

```bash
# 编译上传
arduino-cli compile --board esp32:esp32:esp32 .
arduino-cli upload --board esp32:esp32:esp32 --port /dev/ttyUSB0 .

# 查看日志
arduino-cli monitor --port /dev/ttyUSB0
```

path: package.json
content: {
  "name": "xiaozhi-mcp-server",
  "version": "2.0.8",
  "description": "Connect xiaozhi smart speaker to OpenClaw AI assistant - MCP Server with WebSocket support",
  "main": "scripts/server.py",
  "scripts": {
    "start": "bash scripts/start.sh",
    "stop": "bash scripts/stop.sh"
  },
  "keywords": [
    "openclaw",
    "xiaozhi",
    "mcp",
    "websocket",
    "smart-speaker",
    "voice-assistant"
  ],
  "author": "elegant1998",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/elegant1998/xiaozhi-mcp-server"
  },
  "engines": {
    "python": ">=3.8"
  },
  "dependencies": {
    "aiohttp": ">=3.8.0",
    "pyyaml": ">=6.0"
  },
  "openclaw": {
    "skillType": "integration",
    "provides": [
      "mcp-server",
      "websocket-push"
    ],
    "configFile": "config.yaml",
    "endpoints": {
      "http": "/mcp",
      "websocket": "/ws"
    }
  }
}

path: scripts/server.py
content: #!/usr/bin/env python3
"""
xiaozhi-esp32 MCP Server - 简化版
只保留run_agent全能工具 + WebSocket实时推送
"""

import asyncio
import json
import subprocess
import shlex
import os
import sys
import uuid
import yaml
import re
from aiohttp import web
from datetime import datetime
from pathlib import Path

CONFIG_DIR = Path.home() / ".config" / "openclaw-mcp"
TOKEN_FILE = CONFIG_DIR / "token"
TASKS_DIR = CONFIG_DIR / "tasks"
CONFIG_FILE = Path(__file__).parent.parent / "config.yaml"

# Maximum message length to prevent abuse
MAX_MESSAGE_LENGTH = 2000

def load_config():
    """Load configuration from config.yaml"""
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, 'r') as f:
            return yaml.safe_load(f) or {}
    return {}

def get_target_session():
    """Get target session from config or return empty"""
    config = load_config()
    return config.get("target_session", "")

def sanitize_message(message: str) -> str:
    """Sanitize message to prevent command injection and abuse"""
    if not isinstance(message, str):
        raise ValueError("Message must be a string")
    
    # Limit length
    if len(message) > MAX_MESSAGE_LENGTH:
        message = message[:MAX_MESSAGE_LENGTH]
    
    # Remove control characters except newline and tab
    message = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', message)
    
    return message.strip()

def get_token():
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    TASKS_DIR.mkdir(parents=True, exist_ok=True)
    if TOKEN_FILE.exists():
        return TOKEN_FILE.read_text().strip()
    [REDACTED_SECRET]).hex()
    TOKEN_FILE.write_text(token)
    print(f"[REDACTED_SECRET]", file=sys.stderr)
    return token

def verify_token(request):
    url_token = request.query.get("token", "")
    if url_token and url_token == get_token():
        return True
    auth_header = request.headers.get("Authorization", "")
    if auth_header.startswith("Bearer "):
        return auth_header[7:] == get_token()
    return False

class TaskManager:
    def __init__(self):
        self.tasks = {}

    def create_task(self, name: str, params: dict) -> str:
        task_id = str(uuid.uuid4())[:8]
        self.tasks[task_id] = {
            "id": task_id,
            "name": name,
            "params": params,
            "status": "pending",
            "result": None,
            "error": None,
            "created_at": datetime.now().isoformat()
        }
        return task_id

    def update_task(self, task_id: str, status: str, result=None, error=None, summary=None):
        if task_id in self.tasks:
            self.tasks[task_id].update({
                "status": status,
                "result": result,
                "error": error,
                "updated_at": datetime.now().isoformat()
            })
            if summary:
                self.tasks[task_id]["summary"] = summary

    def get_task(self, task_id: str) -> dict:
        return self.tasks.get(task_id)

    def list_tasks(self, limit=10):
        return sorted(self.tasks.values(), key=lambda x: x.get("updated_at", ""), reverse=True)[:limit]

task_manager = TaskManager()

class OpenClawMCPServer:
    def __init__(self):
        self.name = "openclaw"
        self.version = "2.0.0"
        self.[REDACTED_SECRET])
        self.ws_clients = set()

    async def handle_call_tool(self, name: str, arguments: dict) -> str:
        if name == "run_agent":
            return await self.run_agent(arguments)
        elif name == "get_task":
            return self.get_task_tool(arguments)
        elif name == "list_tasks":
            return self.list_tasks_tool(arguments)
        else:
            return json.dumps({"error": f"Unknown tool: {name}. Use run_agent for all tasks."})

    async def run_agent(self, args: dict) -> str:
        message = args.get("message", "")
        if not message:
            return json.dumps({"error": "message required"})

        task_id = task_manager.create_task("run_agent", args)
        async_mode = args.get("async", True)

        if async_mode:
            asyncio.create_task(self._run_agent_background(task_id, message))
            return json.dumps({
                "async": True,
                "task_id": task_id,
                "status": "pending",
                "ws_endpoint": "/ws",
                "message": "任务已提交，结果将通过WebSocket推送"
            }, ensure_ascii=False)
        else:
            result = await self._run_agent_background(task_id, message)
            return result

    async def _run_agent_background(self, task_id: str, message: str):
        task_manager.update_task(task_id, "running")
        try:
            # Sanitize message to prevent injection
            safe_message = sanitize_message(message)
            
            # Build command safely using list (no shell=True)
            # 正确顺序: openclaw agent --to SESSION --message MSG --deliver
            cmd = ['timeout', '180', 'openclaw', 'agent']
            
            # Get target session from config if available
            target_session = get_target_session()
            if target_session:
                # Validate target_session format (basic check)
                if re.match(r'^[a-z]+:[a-z0-9]+:[A-Za-z0-9]+$', target_session):
                    cmd.extend(['--to', target_session])
            
            cmd.extend(['--message', safe_message, '--deliver'])
            
            # Run without shell=True for security
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=200)
            output = result.stdout.strip()
            summary = self.summarize_for_voice(output)
            task_manager.update_task(task_id, "completed", result=output, summary=summary)

            if self.ws_clients:
                await self.notify_task_completed(task_id, summary)

            return json.dumps({
                "async": False,
                "task_id": task_id,
                "status": "completed",
                "result": output,
                "summary": summary
            }, ensure_ascii=False)
        except Exception as e:
            task_manager.update_task(task_id, "failed", error=str(e))
            if self.ws_clients:
                await self.notify_task_completed(task_id, f"错误: {str(e)}")
            return json.dumps({
                "async": False,
                "task_id": task_id,
                "status": "failed",
                "error": str(e)
            }, ensure_ascii=False)

    def summarize_for_voice(self, text: str, max_length: int = 140) -> str:
        if len(text) <= max_length:
            return text

        # 优先提取结论
        conclusion_keywords = ["运行正常", "成功", "完成", "就绪", "无异常", "正常", "✅"]
        lines = text.strip().split("\n")
        last_line = lines[-1].strip() if lines else ""
        
        for kw in conclusion_keywords:
            if kw in last_line and len(last_line) < max_length:
                return last_line

        # 查找待确认事项
        action_items = []
        action_keywords = ["需要补充", "需要确认", "待确认", "需要您", "请提供", "请确认", "缺少"]
        for keyword in action_keywords:
            if keyword in text:
                idx = text.find(keyword)
                remaining = text[idx:]
                for line in remaining.split("\n")[1:]:
                    line = line.strip()
                    if not line:
                        continue
                    if " - " in line:
                        point = line.split(" - ")[0].strip()
                        if point and len(point) < 15:
                            action_items.append(point)
                if action_items:
                    break

        first_para = text.split("\n\n")[0] if "\n\n" in text else text[:60]
        if len(first_para) > 60:
            first_para = first_para[:60] + "..."

        if action_items:
            items_str = "、".join(action_items[:3])
            summary = f"{first_para} 另外需确认{items_str}等。"
        else:
            summary = first_para

        if len(summary) < max_length - 20:
            summary += " 详情请查看Channel或邮件。"

        return summary[:max_length]

    async def notify_task_completed(self, task_id: str, result: str):
        message = {
            "type": "task_completed",
            "task_id": task_id,
            "result": result,
            "timestamp": datetime.now().isoformat()
        }
        disconnected = []
        for ws in list(self.ws_clients):
            try:
                await ws.send_json(message)
            except:
                disconnected.append(ws)
        for ws in disconnected:
            self.ws_clients.discard(ws)

    def get_task_tool(self, args: dict) -> str:
        task_id = args.get("task_id", "")
        task = task_manager.get_task(task_id)
        if not task:
            return json.dumps({"error": f"Task not found: {task_id}"})
        return json.dumps(task, ensure_ascii=False)

    def list_tasks_tool(self, args: dict) -> str:
        limit = args.get("limit", 10)
        tasks = task_manager.list_tasks(limit)
        return json.dumps({"tasks": tasks}, ensure_ascii=False)

    async def handle_websocket(self, request):
        if not verify_token(request):
            ws = web.WebSocketResponse()
            await ws.prepare(request)
            await ws.close(code=4001, message=b"Unauthorized")
            return ws

        ws = web.WebSocketResponse()
        await ws.prepare(request)
        self.ws_clients.add(ws)
        client_id = str(uuid.uuid4())[:8]
        print(f"[WS] 客户端连接: {client_id}, 总数: {len(self.ws_clients)}", file=sys.stderr)

        try:
            async for msg in ws:
                if msg.type == web.WSMsgType.TEXT:
                    try:
                        data = json.loads(msg.data)
                        if data.get("type") == "ping":
                            await ws.send_json({"type": "pong"})
                    except:
                        pass
        finally:
            self.ws_clients.discard(ws)
            print(f"[WS] 客户端断开: {client_id}", file=sys.stderr)
        return ws

    async def handle_http_post(self, request):
        if not verify_token(request):
            return web.json_response({"error": "Unauthorized"}, status=401)

        try:
            request_obj = await request.json()
            if request_obj.get("method") == "tools/call":
                result = await self.handle_call_tool(
                    request_obj["params"]["name"],
                    request_obj["params"].get("arguments", {})
                )
                response = {"jsonrpc": "2.0", "id": request_obj.get("id"),
                    "result": {"content": [{"type": "text", "text": result}]}}
            else:
                response = {"jsonrpc": "2.0", "id": request_obj.get("id"), "result": {}}
            return web.json_response(response)
        except Exception as e:
            return web.json_response({"error": str(e)})

    async def handle_index(self, request):
        return web.json_response({
            "name": self.name,
            "version": self.version,
            "description": "xiaozhi-esp32 MCP Server - 简化版",
            "auth": "Bearer Token required",
            "endpoints": {"http": "/mcp", "websocket": "/ws"},
            "tools": ["run_agent", "get_task", "list_tasks"]
        })

    async def run_http(self, host="0.0.0.0", port=9000):
        app = web.Application()
        app.router.add_get('/', self.handle_index)
        app.router.add_post('/mcp', self.handle_http_post)
        app.router.add_get('/ws', self.handle_websocket)

        print(f"Server: http://{host}:{port}", file=sys.stderr)
        print(f"WebSocket: ws://{host}:{port}/ws", file=sys.stderr)
        print(f"[REDACTED_SECRET]", file=sys.stderr)

        runner = web.AppRunner(app)
        await runner.setup()
        site = web.TCPSite(runner, host, port)
        await site.start()

async def main():
    server = OpenClawMCPServer()
    port = int(sys.argv[1]) if len(sys.argv) > 1 else 9000
    await server.run_http(port=port)
    while True:
        await asyncio.sleep(3600)

if __name__ == "__main__":
    asyncio.run(main())

path: scripts/start.sh
content: #!/bin/bash
# xiaozhi-mcp-server Start Script

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVER="$SCRIPT_DIR/server.py"
CONFIG_FILE="$SCRIPT_DIR/../config.yaml"
PORT=${XIAOZHI_MCP_PORT:-28765}
LOG_FILE="/tmp/xiaozhi-mcp.log"
PID_FILE="/tmp/xiaozhi-mcp.pid"

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

echo_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
echo_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
echo_error() { echo -e "${RED}[ERROR]${NC} $1"; }
echo_prompt() { echo -e "${BLUE}[?]${NC} $1"; }

# Check Python
if ! command -v python3 &> /dev/null; then
    echo_error "Python3 not found"
    exit 1
fi

# Check dependencies
if ! python3 -c "import aiohttp" 2>/dev/null; then
    echo_info "Installing aiohttp..."
    pip3 install aiohttp --break-system-packages -q
fi

if ! python3 -c "import yaml" 2>/dev/null; then
    echo_info "Installing pyyaml..."
    pip3 install pyyaml --break-system-packages -q
fi

# Check config
check_target_session() {
    if [ ! -f "$CONFIG_FILE" ]; then
        echo_warn "config.yaml not found, creating default..."
        cat > "$CONFIG_FILE" << 'EOF'
# xiaozhi-mcp-server Configuration

# Server port (冷门端口避免冲突)
port: 28765

# Token (auto-generated if empty)
# Set your own token or leave empty for auto-generation
token: ""

# Target session for OpenClaw responses
# Format: channel:type:ID
# Examples:
#   QQ:         qqbot:c2c:你的QQ号 / qqbot:group:群号
#   飞书:       feishu:c2c:用户OpenID / feishu:group:群ID
#   钉钉:       ddingtalk:c2c:用户ID / ddingtalk:group:群ID
#   企业微信:   wecom:c2c:用户ID
#   Telegram:   telegram:c2c:用户ID
#   Discord:    discord:c2c:用户ID
target_session: ""

# Task timeout in seconds
task_timeout: 180

# Log level: debug, info, warn, error
log_level: info
EOF
    fi

    # Check if target_session is empty
    TARGET_SESSION=$(grep "^target_session:" "$CONFIG_FILE" | sed 's/target_session: *//' | tr -d '"')

    if [ -z "$TARGET_SESSION" ] || [ "$TARGET_SESSION" = '""' ]; then
        echo ""
        echo_warn "target_session 未配置！"
        echo ""
        echo "  target_session 决定了 OpenClaw 的回复发送到哪个会话。"
        echo ""
        echo "  支持的平台格式："
        echo "    QQ私聊:     qqbot:c2c:你的QQ号"
        echo "    QQ群:       qqbot:group:群号"
        echo "    飞书私聊:   feishu:c2c:用户OpenID"
        echo "    飞书群:     feishu:group:群ID"
        echo "    钉钉私聊:   ddingtalk:c2c:用户ID"
        echo "    钉钉群:     ddingtalk:group:群ID"
        echo "    企业微信:   wecom:c2c:用户ID"
        echo "    Telegram:   telegram:c2c:用户ID"
        echo "    Discord:    discord:c2c:用户ID"
        echo ""
        echo_prompt "请输入你的 target_session (留空跳过，稍后手动配置): "
        read -r USER_INPUT

        if [ -n "$USER_INPUT" ]; then
            # Update config file
            if [[ "$OSTYPE" == "darwin"* ]]; then
                # macOS
                sed -i '' "s/^target_session:.*$/target_session: \"$USER_INPUT\"/" "$CONFIG_FILE"
            else
                # Linux
                sed -i "s/^target_session:.*$/target_session: \"$USER_INPUT\"/" "$CONFIG_FILE"
            fi
            echo_info "已保存 target_session: $USER_INPUT"
        else
            echo_warn "跳过配置，请稍后手动编辑 config.yaml"
            echo ""
            echo "  配置文件: $CONFIG_FILE"
            echo ""
        fi
    fi
}

check_target_session

# Stop old process
if [ -f "$PID_FILE" ]; then
    OLD_PID=$(cat "$PID_FILE")
    if ps -p "$OLD_PID" &> /dev/null; then
        echo_info "Stopping old process (PID: $OLD_PID)"
        kill "$OLD_PID" 2>/dev/null || true
        sleep 1
    fi
fi

pkill -f "server.py.*28765" 2>/dev/null || true
sleep 1

# Start
echo_info "Starting xiaozhi-mcp-server on port $PORT..."
nohup python3 "$SERVER" "$PORT" > "$LOG_FILE" 2>&1 &
NEW_PID=$!
echo "$NEW_PID" > "$PID_FILE"

sleep 2

if ps -p "$NEW_PID" &> /dev/null; then
    TOKEN=$(cat ~/.config/openclaw-mcp/token 2>/dev/null || echo "Check log")
    IP=$(curl -s ifconfig.me 2>/dev/null || hostname -I 2>/dev/null | awk '{print $1}' || echo "localhost")

    echo ""
    echo "=============================================="
    echo "  xiaozhi-mcp-server Started"
    echo "=============================================="
    echo ""
    echo "  HTTP:  http://$IP:$PORT/mcp"
    echo "  WS:    ws://$IP:$PORT/ws"
    echo "  [REDACTED_SECRET]"
    echo ""
    echo "  Log: $LOG_FILE"
    echo "  PID: $NEW_PID"
    echo ""
    echo "  在瞄小智服务号配置中填入："
    echo "  - 服务器地址: $IP"
    echo "  - 端口: $PORT"
    echo "  - 连接码: $TOKEN"
    echo "=============================================="
else
    echo_error "Failed to start"
    echo_error "Check log: tail -f $LOG_FILE"
    exit 1
fi

path: scripts/stop.sh
content: #!/bin/bash
# xiaozhi-mcp-server Stop Script

PID_FILE="/tmp/xiaozhi-mcp.pid"

if [ -f "$PID_FILE" ]; then
    PID=$(cat "$PID_FILE")
    if ps -p "$PID" &> /dev/null; then
        kill "$PID" 2>/dev/null
        echo "Stopped (PID: $PID)"
    fi
    rm -f "$PID_FILE"
fi

pkill -f "server.py.*http" 2>/dev/null || true
echo "xiaozhi-mcp-server stopped"

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)