Skill: adminlove520/clash-auto-switch (version 1.0.2)

# clash-auto-switch > Clash 代理自动切换 Skill for OpenClaw 自动检测代理健康状态并在故障时自动切换到最佳节点。 ## 配置 首次使用需要配置 Clash API： ```bash # 设置 API 地址和密钥 /clash config set --api http://127.0.0.1:58871 --secret 你的密钥 ``` 或通过环境变量： - `CLASH_API`: Clash API 地址 - `CLASH_SECRET`: API 密钥 - `CLASH_PROXY`: 代理地址 (默认: http://127.0.0.1:7890) ## 功能 ### 健康检查 检查当前代理是否正常工作： ``` /clash health ``` 返回示例： ``` ✓ 代理健康 (3/3) - Telegram: OK - Google: OK - Anthropic: OK ``` ### 列出节点 查看所有代理组和可用节点： ``` /clash list ``` ### 自动切换 自动检测并切换到最佳节点： ``` /clash auto ``` 逻辑： 1. 测试 Telegram / Google / Anthropic 连通性 2. 如果不健康，遍历所有节点 3. 测试延迟，优先选择新加坡/日本/香港/美国节点 4. 切换到最佳节点 ### 手动切换 切换到指定节点： ``` /clash switch ChatGPT 新加坡-优化-Gemini-GPT ``` ### 区域切换 快速切换到指定区域： ``` /clash sg # 切换到新加坡 /clash us # 切换到美国 /clash jp # 切换到日本 /clash hk # 切换到香港 ``` ### 状态查询 查看当前代理状态： ``` /clash status ``` ## 在 Cron 中使用 添加到 cron job 实现定时健康检查： ```json { "name": "clash-health-check", "schedule": { "kind": "every", "everyMs": 900000 }, "payload": { "kind": "agentTurn", "message": "/clash auto" }, "sessionTarget": "isolated" } ``` ## 在 Heartbeat 中使用 在 heartbeat 中添加健康检查： ``` # 在 HEARTBEAT.md 中添加 /clash health ``` ## 支持的代理组 - ChatGPT - Copilot - GLOBAL - Netflix - Steam - Telegram - TikTok - Twitter - WhatsApp - 境内使用 - 海外使用 - 节点选择 - 谷歌服务 - 微软服务 - 苹果服务 ## 示例对话 **用户**: 检查代理状态 **小溪**: 调用 `/clash health` → 返回健康度 **用户**: 代理好像很慢 **小溪**: 调用 `/clash auto` → 自动切换到最佳节点 **用户**: 切换到新加坡节点 **小溪**: 调用 `/clash sg` → 切换完成 ## 实现原理 1. 调用 Clash API (`/proxies`) 获取所有代理组 2. 对每个 Selector 类型的代理组执行健康检查 3. 如果不健康，测试所有节点延迟 4. 优先选择优先区域的节点 5. 调用 Clash API (`/proxies/{group}`) 切换节点

path: CHANGELOG.md
content: # Changelog

All notable changes to this project will be documented in this file.

## [1.0.0] - 2026-03-07

### Added
- 初始版本发布
- Bash 版本 (`clash-switch.sh`)
- 增强版 Bash (`clash-switch-v2.sh`) - 支持日志和状态追踪
- PowerShell 版本 (`clash-switch.ps1`) - Windows 支持
- Python 跨平台版 (`clash-switch.py`) - 推荐使用
- OpenClaw Skill - 支持 `/clash` 命令
- 配置示例文件
- 完整的 README 文档
- GitHub Actions 部署配置

### Features
- 健康检查 (测试 Telegram/Google/Anthropic)
- 自动切换到最佳节点
- 区域优先 (新加坡/日本/香港/美国)
- 手动切换指定节点
- 定时任务支持
- 日志记录
- 状态追踪

### Platforms
- ✅ Linux / macOS (Bash)
- ✅ Windows (PowerShell)
- ✅ 跨平台 (Python)
- ✅ OpenClaw Skill

---

## [Unreleased]

### Planned
- [ ] 添加通知支持 (Telegram/Discord 通知)
- [ ] Web 界面
- [ ] 统计面板 (节点使用时间/流量)
- [ ] 支持更多测试目标
- [ ] 智能学习 (根据使用习惯自动选择节点)


path: clash-switch-v2.sh
content: #!/bin/bash

################################################################################
# Clash 代理自动切换脚本 - 增强版
# 版本: 2.0
# 新增功能:
#   1. 日志记录 - 记录切换历史
#   2. 状态文件 - 保存当前状态
#   3. 通知支持 - 切换成功/失败时发送通知
#   4. 多测试目标 - 更全面的健康检查
################################################################################

set -e

# ========== 配置变量 ==========
CLASH_API="http://127.0.0.1:58871"
CLASH_SECRET="6434ff5a-5b0f-4598-99ec-83ca96c77167"
PROXY_URL="http://127.0.0.1:7890"

# 日志和状态文件
LOG_FILE="/var/log/clash-switch.log"
STATE_FILE="/tmp/clash-switch-state.json"

# 测试目标（扩展版）
TEST_TARGETS=(
    "https://api.telegram.org"
    "https://api.anthropic.com"
    "https://www.google.com"
    "https://api.openai.com"
    "https://api.github.com"
)

# 区域优先级
PREFERRED_REGIONS=("新加坡" "SG" "香港" "HK" "日本" "JP" "美国" "US")

# ========== 函数 ==========

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

log_success() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✓ $1" | tee -a "$LOG_FILE"
}

log_error() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] ✗ $1" | tee -a "$LOG_FILE"
}

# 保存状态
save_state() {
    local current="$1"
    local status="$2"
    echo "{\"timestamp\":\"$(date -Iseconds)\",\"current\":\"$current\",\"status\":\"$status\"}" > "$STATE_FILE"
}

# 加载状态
load_state() {
    if [ -f "$STATE_FILE" ]; then
        cat "$STATE_FILE"
    else
        echo "{}"
    fi
}

# 健康检查
health_check() {
    local success=0
    local total=${#TEST_TARGETS[@]}
    
    for target in "${TEST_TARGETS[@]}"; do
        if curl -x "$PROXY_URL" -s -m 5 -I "$target" >/dev/null 2>&1; then
            ((success++))
        fi
    done
    
    local rate=$((success * 100 / total))
    echo $rate
}

# 获取所有节点
get_all_proxies() {
    curl -s -H "Authorization: Bearer ${CLASH_SECRET}" "${CLASH_API}/proxies" | jq -r '.proxies | keys[]' 2>/dev/null | grep -v "^GLOBAL$\|^ DIRECT$\|^REJECT$\|^PROXY$"
}

# 获取当前节点
get_current() {
    curl -s -H "Authorization: Bearer ${CLASH_SECRET}" "${CLASH_API}/proxies/ChatGPT" | jq -r '.now' 2>/dev/null
}

# 切换节点
switch_to() {
    local proxy="$1"
    curl -s -X PUT -H "Authorization: Bearer ${CLASH_SECRET}" \
         -H "Content-Type: application/json" \
         -d "{\"name\":\"$proxy\"}" \
         "${CLASH_API}/proxies/ChatGPT" >/dev/null 2>&1
}

# 测试延迟
test_delay() {
    local proxy="$1"
    local encoded=$(echo "$proxy" | jq -sRr @uri)
    local delay=$(curl -s -H "Authorization: Bearer ${CLASH_SECRET}" \
        "${CLASH_API}/proxies/${encoded}/delay?timeout=5000&url=http://www.gstatic.com/generate_204" | \
        jq -r '.delay' 2>/dev/null)
    
    if [ "$delay" != "null" ] && [ -n "$delay" ]; then
        echo $delay
    else
        echo "99999"
    fi
}

# 找最佳节点
find_best() {
    local best=""
    local best_delay=99999
    
    for proxy in $(get_all_proxies | grep -v "节点选择"); do
        local delay=$(test_delay "$proxy")
        log "测试 $proxy: ${delay}ms"
        
        # 优先区域检查
        local is_preferred=0
        for region in "${PREFERRED_REGIONS[@]}"; do
            if echo "$proxy" | grep -qi "$region"; then
                is_preferred=1
                break
            fi
        done
        
        if [ $is_preferred -eq 1 ] && [ $delay -lt $best_delay ]; then
            best="$proxy"
            best_delay=$delay
        elif [ $is_preferred -eq 0 ] && [ $best_delay -ge 500 ] && [ $delay -lt $best_delay ]; then
            best="$proxy"
            best_delay=$delay
        fi
    done
    
    echo "$best"
}

# ========== 主逻辑 ==========

case "${1:-help}" in
    check)
        rate=$(health_check)
        log "健康检查: ${rate}%"
        if [ $rate -ge 60 ]; then
            log_success "代理健康"
            exit 0
        else
            log_error "代理不健康"
            exit 1
        fi
        ;;
    
    auto)
        current=$(get_current)
        rate=$(health_check)
        
        log "========== 自动切换 =========="
        log "当前节点: $current"
        log "健康度: ${rate}%"
        
        if [ $rate -ge 60 ]; then
            log_success "代理健康，无需切换"
            save_state "$current" "healthy"
            exit 0
        fi
        
        log_error "代理不健康，开始自动切换..."
        best=$(find_best)
        
        if [ -n "$best" ]; then
            switch_to "$best"
            sleep 3
            
            new_rate=$(health_check)
            if [ $new_rate -ge 60 ]; then
                log_success "切换成功! 新节点: $best, 健康度: ${new_rate}%"
                save_state "$best" "fixed"
            else
                log_error "切换后仍不健康"
                save_state "$best" "failed"
            fi
        else
            log_error "未找到可用节点"
            save_state "$current" "failed"
        fi
        ;;
    
    list)
        echo "========== 代理组 =========="
        curl -s -H "Authorization: Bearer ${CLASH_SECRET}" "${CLASH_API}/proxies" | \
            jq -r '.proxies | to_entries[] | select(.value.type=="Selector") | "\(.key): \(.value.now)"'
        ;;
    
    status)
        cat $(load_state)
        ;;
    
    *)
        echo "Clash Auto Switch v2.0"
        echo ""
        echo "用法: $0 <命令>"
        echo ""
        echo "命令:"
        echo "  check   - 健康检查"
        echo "  auto   - 自动切换到最佳节点"
        echo "  list   - 列出所有代理组"
        echo "  status - 显示当前状态"
        echo ""
        ;;
esac


path: clash-switch.py
content: #!/usr/bin/env python3
"""
Clash Auto Switch - 跨平台版本
支持: Linux, macOS, Windows
"""

import os
import sys
import json
import time
import argparse
from typing import List, Dict, Any, Optional

try:
    import requests
except ImportError:
    print("请安装 requests: pip install requests")
    sys.exit(1)


class ClashAutoSwitch:
    """Clash 代理自动切换工具"""
    
    def __init__(self, api_url: str, secret: str, proxy_url: str = "http://127.0.0.1:7890"):
        self.api_url = api_url.rstrip('/')
        self.[REDACTED_SECRET]
        self.proxy_url = proxy_url
        self.headers = {"Authorization": f"Bearer {secret}"}
        
    def _request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict]:
        """发送 API 请求"""
        url = f"{self.api_url}{endpoint}"
        try:
            if method.upper() == "GET":
                r = requests.get(url, headers=self.headers, timeout=10, **kwargs)
            elif method.upper() == "PUT":
                r = requests.put(url, headers=self.headers, timeout=10, **kwargs)
            else:
                return None
            return r.json() if r.status_code in [200, 204] else None
        except Exception as e:
            print(f"请求错误: {e}")
            return None
    
    def get_proxies(self) -> Dict[str, Any]:
        """获取所有代理"""
        return self._request("GET", "/proxies") or {}
    
    def get_proxy_group(self, name: str) -> Dict[str, Any]:
        """获取代理组详情"""
        return self._request("GET", f"/proxies/{name}") or {}
    
    def set_proxy(self, group: str, node: str) -> bool:
        """切换节点"""
        result = self._request("PUT", f"/proxies/{group}", json={"name": node})
        return result is not None
    
    def test_delay(self, node: str) -> int:
        """测试节点延迟"""
        encoded = requests.utils.quote(node)
        url = f"{self.api_url}/proxies/{encoded}/delay?timeout=5000&url=http://www.gstatic.com/generate_204"
        try:
            r = requests.get(url, headers=self.headers, timeout=10)
            data = r.json()
            return data.get("delay", 99999)
        except:
            return 99999
    
    def health_check(self, targets: List[str]) -> tuple:
        """健康检查"""
        success = 0
        proxies = {"http": self.proxy_url, "https": self.proxy_url}
        
        for target in targets:
            try:
                r = requests.get(target, proxies=proxies, timeout=5)
                if r.status_code < 500:
                    success += 1
            except:
                pass
        
        return success, len(targets)
    
    def list_groups(self) -> Dict[str, str]:
        """列出所有代理组"""
        data = self.get_proxies()
        groups = {}
        for name, info in data.get("proxies", {}).items():
            if info.get("type") == "Selector":
                groups[name] = info.get("now", "未知")
        return groups
    
    def auto_switch(self, preferred_regions: List[str] = None, test_targets: List[str] = None) -> bool:
        """自动切换到最佳节点"""
        if test_targets is None:
            test_targets = [
                "https://api.telegram.org",
                "https://api.anthropic.com", 
                "https://www.google.com"
            ]
        
        # 健康检查
        success, total = self.health_check(test_targets)
        rate = success * 100 // total
        print(f"健康度: {success}/{total} ({rate}%)")
        
        if rate >= 60:
            print("✓ 代理健康，无需切换")
            return True
        
        print("✗ 代理不健康，开始自动切换...")
        
        # 找最佳节点
        best_node = None
        best_delay = 99999
        
        groups = self.list_groups()
        for group, current in groups.items():
            if current == "节点选择":
                info = self.get_proxy_group(group)
                nodes = info.get("all", [])
                
                for node in nodes:
                    if node == "节点选择":
                        continue
                    
                    delay = self.test_delay(node)
                    print(f"  {node}: {delay}ms")
                    
                    # 检查是否优先区域
                    is_preferred = any(r.lower() in node.lower() for r in (preferred_regions or []))
                    
                    if is_preferred and delay < best_delay:
                        best_node = node
                        best_delay = delay
                    elif not best_node and delay < best_delay:
                        best_node = node
                        best_delay = delay
                
                if best_node:
                    print(f"切换到: {best_node} ({best_delay}ms)")
                    self.set_proxy(group, best_node)
        
        return best_node is not None


def main():
    parser = argparse.ArgumentParser(description="Clash Auto Switch")
    parser.add_argument("--api", "-a", default="http://127.0.0.1:58871", help="Clash API 地址")
    parser.add_argument("--secret", "-s", default="", help="API 密钥")
    parser.add_argument("--proxy", "-p", default="http://127.0.0.1:7890", help="代理地址")
    parser.add_argument("--list", "-l", action="store_true", help="列出所有代理组")
    parser.add_argument("--check", "-c", action="store_true", help="健康检查")
    parser.add_argument("--auto", action="store_true", help="自动切换")
    parser.add_argument("--group", "-g", help="指定代理组")
    parser.add_argument("--node", "-n", help="切换到指定节点")
    
    args = parser.parse_args()
    
    # 如果没有提供 secret，从环境变量读取
    [REDACTED_SECRET] or os.environ.get("CLASH_SECRET", "")
    if not [REDACTED_SECRET]"请提供 --secret 或设置 CLASH_SECRET 环境变量")
        sys.exit(1)
    
    clash = ClashAutoSwitch(args.api, secret, args.proxy)
    
    if args.list:
        print("========== 代理组 ==========")
        for group, current in clash.list_groups().items():
            print(f"{group}: {current}")
    
    elif args.check:
        clash.health_check([
            "https://api.telegram.org",
            "https://api.anthropic.com",
            "https://www.google.com"
        ])
    
    elif args.auto:
        clash.auto_switch()
    
    elif args.group and args.node:
        if clash.set_proxy(args.group, args.node):
            print(f"✓ 已切换 {args.group} -> {args.node}")
        else:
            print(f"✗ 切换失败")
    
    else:
        parser.print_help()


if __name__ == "__main__":
    main()


path: clash-switch.sh
content: #!/bin/bash

################################################################################
# Clash 代理自动切换脚本
# 版本: 1.0
# 功能:
#   1. 健康检查：测试当前代理连通性
#   2. 自动切换：检测到故障时自动切换节点
#   3. 区域优先：优先选择美国/新加坡节点
################################################################################

set -e

# 配置变量
CLASH_API="http://127.0.0.1:58871"
CLASH_SECRET="6434ff5a-5b0f-4598-99ec-83ca96c77167"
PROXY_URL="http://127.0.0.1:7890"

# 测试目标（用于验证代理连通性）
TEST_TARGETS=(
    "https://api.telegram.org"
    "https://api.anthropic.com"
    "https://www.google.com"
)

# 区域优先级（新加坡 > 香港 > 日本 > 其他）- 美国延迟高
PREFERRED_REGIONS=("新加坡" "🇸🇬" "SG" "Singapore" "香港" "🇭🇰" "HK" "Hong Kong" "日本" "🇯🇵" "JP" "Japan")

# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'

# 日志函数
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }
log_success() { echo -e "${GREEN}[OK]${NC} $(date '+%Y-%m-%d %H:%M:%S') $1"; }

# URL 编码函数
urlencode() {
    local string="$1"
    python3 -c "import urllib.parse; print(urllib.parse.quote('''$string'''))" 2>/dev/null || \
    echo "$string" | sed 's/ /%20/g'
}

# Clash API 调用
clash_api_get() {
    local endpoint="$1"
    curl -s -H "Authorization: Bearer ${CLASH_SECRET}" "${CLASH_API}${endpoint}"
}

clash_api_put() {
    local endpoint="$1"
    local data="$2"
    curl -s -X PUT -H "Authorization: Bearer ${CLASH_SECRET}" \
         -H "Content-Type: application/json" \
         -d "${data}" "${CLASH_API}${endpoint}"
}

# 测试代理连通性
test_proxy() {
    local target="$1"
    local timeout=10

    if curl -x "${PROXY_URL}" -s -m ${timeout} -I "${target}" >/dev/null 2>&1; then
        return 0
    else
        return 1
    fi
}

# 全面健康检查
health_check() {
    log_info "开始健康检查..."

    local success_count=0
    local total_count=${#TEST_TARGETS[@]}

    for target in "${TEST_TARGETS[@]}"; do
        if test_proxy "$target"; then
            log_success "✓ ${target} 可达"
            ((success_count++))
        else
            log_error "✗ ${target} 不可达"
        fi
    done

    local success_rate=$((success_count * 100 / total_count))
    log_info "健康检查完成: ${success_count}/${total_count} (${success_rate}%)"

    if [ $success_rate -ge 66 ]; then
        return 0  # 健康
    else
        return 1  # 不健康
    fi
}

# 获取所有可用节点
get_all_proxies() {
    clash_api_get "/proxies" | jq -r '.proxies | keys[]' 2>/dev/null | grep -v "^GLOBAL$\|^DIRECT$\|^REJECT$\|^PROXY$\|^♻️\|^🔰\|^⚓️\|^✈️\|^🎬\|^🎮\|^🍎\|^🎨\|^❗\|^🚀"
}

# 获取当前选中的节点
get_current_proxy() {
    clash_api_get "/proxies/PROXY" | jq -r '.now' 2>/dev/null
}

# 切换到指定节点
switch_proxy() {
    local proxy_name="$1"
    local result=$(clash_api_put "/proxies/PROXY" "{\"name\":\"${proxy_name}\"}")

    if [ $? -eq 0 ]; then
        log_success "已切换到节点: ${proxy_name}"
        return 0
    else
        log_error "切换失败: ${proxy_name}"
        return 1
    fi
}

# 测试节点延迟
test_proxy_delay() {
    local proxy_name="$1"
    local encoded_name=$(urlencode "$proxy_name")
    local delay=$(clash_api_get "/proxies/${encoded_name}/delay?timeout=5000&url=http://www.gstatic.com/generate_204" | jq -r '.delay' 2>/dev/null)

    if [ "$delay" != "null" ] && [ -n "$delay" ] && [ "$delay" != "0" ]; then
        echo "$delay"
        return 0
    else
        echo "9999"  # 超时或不可用
        return 1
    fi
}

# 检查节点是否属于优先区域
is_preferred_region() {
    local proxy_name="$1"

    for region in "${PREFERRED_REGIONS[@]}"; do
        if echo "$proxy_name" | grep -qi "$region"; then
            return 0
        fi
    done

    return 1
}

# 获取最佳节点（优先区域 + 最低延迟）
get_best_proxy() {
    log_info "正在测试所有节点..."

    local best_proxy=""
    local best_delay=9999
    local preferred_proxy=""
    local preferred_delay=9999

    # 获取所有节点
    local proxies=$(get_all_proxies | grep -v "DIRECT\|REJECT\|PROXY\|GLOBAL")

    while IFS= read -r proxy; do
        [ -z "$proxy" ] && continue

        log_info "测试节点: ${proxy}"
        local delay=$(test_proxy_delay "$proxy")

        log_info "  延迟: ${delay}ms"

        # 检查是否属于优先区域
        if is_preferred_region "$proxy"; then
            log_info "  ★ 优先区域节点"
            if [ "$delay" -lt "$preferred_delay" ]; then
                preferred_proxy="$proxy"
                preferred_delay="$delay"
            fi
        fi

        # 记录全局最佳节点
        if [ "$delay" -lt "$best_delay" ]; then
            best_proxy="$proxy"
            best_delay="$delay"
        fi
    done <<< "$proxies"

    # 优先返回优先区域的最佳节点
    if [ -n "$preferred_proxy" ] && [ "$preferred_delay" -lt 3000 ]; then
        echo "$preferred_proxy"
        log_success "选择优先区域节点: ${preferred_proxy} (${preferred_delay}ms)"
        return 0
    elif [ -n "$best_proxy" ]; then
        echo "$best_proxy"
        log_success "选择最佳节点: ${best_proxy} (${best_delay}ms)"
        return 0
    else
        log_error "未找到可用节点"
        return 1
    fi
}

# 自动切换到最佳节点
auto_switch() {
    log_info "========== Clash 自动切换 =========="

    # 1. 检查当前健康状态
    local current_proxy=$(get_current_proxy)
    log_info "当前节点: ${current_proxy}"

    if health_check; then
        log_success "当前代理健康，无需切换"
        return 0
    fi

    log_warn "当前代理不健康，开始寻找最佳节点..."

    # 2. 寻找最佳节点
    local best_proxy=$(get_best_proxy)

    if [ -z "$best_proxy" ]; then
        log_error "未找到可用节点"
        return 1
    fi

    # 3. 切换到最佳节点
    if [ "$best_proxy" != "$current_proxy" ]; then
        switch_proxy "$best_proxy"

        # 4. 等待并重新检查
        log_info "等待5秒后重新检查..."
        sleep 5

        if health_check; then
            log_success "切换成功！代理已恢复正常"
            return 0
        else
            log_error "切换后仍不健康，可能需要手动干预"
            return 1
        fi
    else
        log_info "当前已是最佳节点，但连接仍有问题"
        return 1
    fi
}

# 列出所有节点
list_proxies() {
    log_info "========== 可用节点列表 =========="

    local current_proxy=$(get_current_proxy)
    local proxies=$(get_all_proxies | grep -v "DIRECT\|REJECT\|PROXY\|GLOBAL")

    while IFS= read -r proxy; do
        [ -z "$proxy" ] && continue

        local marker=" "
        [ "$proxy" = "$current_proxy" ] && marker="★"

        local region_marker=""
        if is_preferred_region "$proxy"; then
            region_marker=" ${GREEN}[优先]${NC}"
        fi

        echo -e "${marker} ${proxy}${region_marker}"
    done <<< "$proxies"
}

# 手动切换节点
manual_switch() {
    local target_proxy="$1"

    if [ -z "$target_proxy" ]; then
        log_error "请指定节点名称"
        list_proxies
        return 1
    fi

    switch_proxy "$target_proxy"

    log_info "等待5秒后检查..."
    sleep 5

    health_check
}

# 区域切换（美国/新加坡）
region_switch() {
    local region="$1"

    log_info "正在搜索 ${region} 区域节点..."

    local proxies=$(get_all_proxies | grep -v "DIRECT\|REJECT\|PROXY\|GLOBAL")
    local region_proxies=""

    case "$region" in
        "us"|"USA"|"美国")
            region_proxies=$(echo "$proxies" | grep -E "美国|🇺🇲")
            ;;
        "sg"|"SG"|"新加坡")
            region_proxies=$(echo "$proxies" | grep -E "新加坡|🇸🇬")
            ;;
        *)
            log_error "不支持的区域: ${region}"
            log_info "支持的区域: us (美国), sg (新加坡)"
            return 1
            ;;
    esac

    if [ -z "$region_proxies" ]; then
        log_error "未找到 ${region} 区域节点"
        return 1
    fi

    log_info "找到以下节点："
    echo "$region_proxies" | while read -r proxy; do
        echo "  - ${proxy}"
    done

    # 测试并选择最佳节点
    local best_proxy=""
    local best_delay=9999

    while IFS= read -r proxy; do
        [ -z "$proxy" ] && continue

        local delay=$(test_proxy_delay "$proxy")
        log_info "${proxy}: ${delay}ms"

        if [ "$delay" -lt "$best_delay" ]; then
            best_proxy="$proxy"
            best_delay="$delay"
        fi
    done <<< "$region_proxies"

    if [ -n "$best_proxy" ]; then
        switch_proxy "$best_proxy"
        sleep 5
        health_check
    else
        log_error "未找到可用节点"
        return 1
    fi
}

# 使用说明
usage() {
    cat << EOF
Clash 代理自动切换脚本 v1.0

用法: $0 [命令] [参数]

命令:
  check       健康检查当前代理
  auto        自动切换到最佳节点（推荐）
  list        列出所有可用节点
  switch      手动切换到指定节点
  us          切换到美国节点
  sg          切换到新加坡节点
  help        显示此帮助信息

示例:
  $0 check                    # 检查当前代理健康状态
  $0 auto                     # 自动切换到最佳节点
  $0 list                     # 列出所有节点
  $0 switch "美国 01"         # 切换到指定节点
  $0 us                       # 切换到最佳美国节点
  $0 sg                       # 切换到最佳新加坡节点

配置:
  Clash API: ${CLASH_API}
  代理地址:  ${PROXY_URL}
  控制密钥:  ${CLASH_SECRET}

EOF
}

# 主函数
main() {
    local command="${1:-help}"

    case "$command" in
        check)
            health_check
            ;;
        auto)
            auto_switch
            ;;
        list)
            list_proxies
            ;;
        switch)
            manual_switch "$2"
            ;;
        us|usa|美国)
            region_switch "us"
            ;;
        sg|singapore|新加坡)
            region_switch "sg"
            ;;
        help|--help|-h)
            usage
            ;;
        *)
            log_error "未知命令: $command"
            echo ""
            usage
            exit 1
            ;;
    esac
}

# 执行主函数
main "$@"


path: config.example.sh
content: # Clash Auto Switch 配置

# API 配置
CLASH_API="http://127.0.0.1:58871"
CLASH_SECRET="your-secret-here"
PROXY_URL="http://127.0.0.1:7890"

# 测试目标 (用逗号分隔)
TEST_TARGETS="https://api.telegram.org,https://api.anthropic.com,https://www.google.com,https://api.openai.com"

# 健康检查阈值 (百分比)
HEALTH_THRESHOLD=60

# 优先区域 (用逗号分隔)
PREFERRED_REGIONS="新加坡,香港,日本,美国"

# 日志文件
LOG_FILE="/var/log/clash-switch.log"

# 状态文件
STATE_FILE="/tmp/clash-switch-state.json"

# 切换后等待秒数
WAIT_AFTER_SWITCH=5

# 测试超时 (秒)
TEST_TIMEOUT=5


path: docs/openclaw.md
content: # OpenClaw Skill 安装指南

本指南详细介绍如何在 OpenClaw 中安装和使用 clash-auto-switch Skill。

## 安装步骤

### 1. 安装 Skill

```bash
npx clawdhub install clash-auto-switch
```

### 2. 配置 Clash API

需要在 OpenClaw 配置文件中添加环境变量。

找到你的 `openclaw.json` 配置文件，添加以下内容：

```json
{
  "env": {
    "CLASH_API": "http://127.0.0.1:58871",
    "CLASH_SECRET": "你的Clash密钥",
    "CLASH_PROXY": "http://127.0.0.1:7890"
  }
}
```

> ⚠️ **注意**：端口可能因人而异，请查看你自己 Clash for Windows 的配置：
> - 打开 Clash for Windows
> - 点击设置 → 找到「实验性功能」中的 `external-controller`
> - 格式通常是 `127.0.0.1:端口号`

### 3. 重启 OpenClaw

配置完成后，重启 OpenClaw 使配置生效。

## 使用方法

安装并配置完成后，直接对 AI 说：

### 基础命令

| 命令 | 说明 |
|------|------|
| `/clash health` | 健康检查 |
| `/clash list` | 列出所有代理组和节点 |
| `/clash status` | 查看当前状态 |
| `/clash auto` | 自动切换到最佳节点 |

### 快速切换

| 命令 | 说明 |
|------|------|
| `/clash sg` | 切换到新加坡节点 |
| `/clash us` | 切换到美国节点 |
| `/clash jp` | 切换到日本节点 |
| `/clash hk` | 切换到香港节点 |

### 手动切换

```
/clash switch ChatGPT 新加坡-优化-Gemini-GPT
```

格式：`/clash switch <代理组名> <节点名>`

## 获取 Clash API 信息

### 方法 1：查看 Clash for Windows 配置

1. 打开 Clash for Windows
2. 点击左上角菜单 → 设置
3. 找到「外部控制」
4. 会看到类似 `127.0.0.1:58871` 的地址

### 方法 2：查看配置文件

打开 Clash 配置文件（config.yaml），查找：

```yaml
external-controller: 127.0.0.1:58871
secret: 你的密钥
```

## 定时自动切换

### 创建 Cron Job

在 OpenClaw 中创建定时任务，自动检查并切换：

```json
{
  "name": "clash-auto-switch",
  "schedule": {
    "kind": "every",
    "everyMs": 900000
  },
  "payload": {
    "kind": "agentTurn",
    "message": "/clash auto"
  },
  "sessionTarget": "isolated"
}
```

这会每 15 分钟自动检查一次，如果代理不健康则自动切换。

## 常见问题

### Q: 为什么健康检查失败？

A: 检查以下几点：
1. Clash for Windows 是否正在运行
2. API 地址和端口是否正确
3. API 密钥是否正确
4. 代理是否已连接

### Q: 切换后仍然不健康怎么办？

A: 可以尝试：
1. 手动指定节点：`/clash switch ChatGPT 美国LA-优化-GPT`
2. 检查是否是目标网站本身的问题
3. 查看日志了解具体错误

### Q: 如何查看当前使用的节点？

A: 使用 `/clash status` 命令

## 示例对话

**用户**: 检查下代理状态  
**AI**: 调用 `/clash health` → 返回 `✓ 代理健康 (3/3)`

**用户**: 代理好慢  
**AI**: 调用 `/clash auto` → 自动切换到最佳节点

**用户**: 切换到新加坡  
**AI**: 调用 `/clash sg` → 切换完成

---

## 相关链接

- [GitHub 仓库](https://github.com/adminlove520/clash-auto-switch)
- [ClawHub](https://clawhub.com/package/clash-auto-switch)
- [原始 README](./README.md)


path: README.md
content: # Clash Auto Switch

> 自动切换 Clash 代理节点的脚本工具，支持 Linux / macOS / Windows 和 OpenClaw Skill

[English](./README.md) | [中文](./README_zh.md)

## 功能

- ✅ 健康检查：测试当前代理连通性
- ✅ 自动切换：检测到故障时自动切换到最佳节点
- ✅ 区域优先：优先选择新加坡/日本/香港/美国节点
- ✅ 手动切换：支持切换到指定节点或区域
- ✅ OpenClaw Skill：支持在 OpenClaw 中直接调用

## 支持平台

| 平台 | 脚本 | 说明 |
|------|------|------|
| 🐧 Linux / macOS | `clash-switch.sh` | Bash 脚本 |
| 🐧 Linux / macOS | `clash-switch-v2.sh` | 增强版 (日志+状态) |
| 🪟 Windows | `clash-switch.ps1` | PowerShell 脚本 |
| 🌐 跨平台 | `clash-switch.py` | Python 脚本 (推荐) |
| 🤖 OpenClaw | Skill | `/clash` 命令 |

## 快速开始

### Python (推荐，跨平台)

```bash
# 安装依赖
pip install requests

# 设置环境变量
export CLASH_API="http://127.0.0.1:58871"
export CLASH_SECRET="your-secret"

# 列出代理组
python clash-switch.py --list

# 健康检查
python clash-switch.py --health

# 自动切换
python clash-switch.py --auto
```

### OpenClaw Skill

```bash
/clash health    # 健康检查
/clash list     # 列出节点
/clash auto     # 自动切换
/clash status   # 查看状态
/clash sg       # 切换到新加坡
```

## 安装

### OpenClaw Skill

```bash
npx clawdhub install clash-auto-switch
```

### 手动安装

```bash
# Clone 仓库
git clone https://github.com/adminlove520/clash-auto-switch.git
cd clash-auto-switch

# 使用 Python 版本
pip install requests
python clash-switch.py --help
```

## 配置

### 环境变量

| 变量 | 默认值 | 说明 |
|------|---------|------|
| `CLASH_API` | `http://127.0.0.1:58871` | Clash API 地址 |
| `CLASH_SECRET` | - | API 密钥 (必填) |
| `CLASH_PROXY` | `http://127.0.0.1:7890` | 代理地址 |

### 配置文件

复制 `config.example.sh` 为 `config.sh` 并修改：

```bash
CLASH_API="http://127.0.0.1:58871"
CLASH_SECRET="your-secret"
```

## 使用方法

### Python 版本

```bash
# 列出所有代理组
python clash-switch.py --list

# 健康检查
python clash-switch.py --health

# 自动切换到最佳节点
python clash-switch.py --auto

# 查看状态
python clash-switch.py --status

# 切换到指定节点
python clash-switch.py --switch ChatGPT "新加坡-优化-Gemini-GPT"

# 区域快速切换
python clash-switch.py --sg    # 新加坡
python clash-switch.py --us    # 美国
python clash-switch.py --jp    # 日本
python clash-switch.py --hk    # 香港
```

### OpenClaw Skill

```
/clash health    # 健康检查
/clash list     # 列出节点
/clash auto     # 自动切换
/clash status   # 查看状态
/clash sg       # 切换到新加坡
/clash us       # 切换到美国
```

### Bash 版本 (Linux/macOS)

```bash
chmod +x clash-switch.sh

./clash-switch.sh check    # 健康检查
./clash-switch.sh auto    # 自动切换
./clash-switch.sh list   # 列出节点
```

### PowerShell 版本 (Windows)

```powershell
.\clash-switch.ps1 check   # 健康检查
.\clash-switch.ps1 auto    # 自动切换
.\clash-switch.ps1 list    # 列出节点
```

## 定时任务

### Linux crontab

```bash
# 每 15 分钟检查一次
*/15 * * * * /path/to/clash-switch.sh auto >> /var/log/clash-switch.log 2>&1
```

### Windows 计划任务

```powershell
Register-ScheduledTask -TaskName "ClashAutoSwitch" -Trigger (New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 15)) -Action (New-ScheduledTaskAction -Execute "python.exe" -Argument "C:\path\to\clash-switch.py --auto") -RunLevel Highest
```

### OpenClaw Cron

```json
{
  "name": "clash-health-check",
  "schedule": {
    "kind": "every",
    "everyMs": 900000
  },
  "payload": {
    "kind": "agentTurn",
    "message": "/clash auto"
  },
  "sessionTarget": "isolated"
}
```

## 支持的代理组

- ChatGPT
- Copilot
- GLOBAL
- Netflix
- Steam
- Telegram
- TikTok
- Twitter
- WhatsApp
- 境内使用
- 海外使用
- 节点选择
- 谷歌服务
- 微软服务
- 苹果服务

## 自动切换逻辑

1. 检查当前代理健康状态（测试 Telegram/Google/Anthropic）
2. 如果不健康，遍历所有节点
3. 测试每个节点的延迟
4. 优先选择优先区域的节点（日本/新加坡/香港/美国）
5. 切换到最佳节点
6. 重新检查健康状态

## 项目结构

```
clash-auto-switch/
├── clash-switch.sh        # Bash 原版
├── clash-switch-v2.sh    # Bash 增强版
├── clash-switch.ps1      # PowerShell 版
├── clash-switch.py      # Python 跨平台版 (推荐)
├── config.example.sh     # 配置示例
├── SKILL.md             # OpenClaw Skill 文档
├── skills/
│   └── clash-auto-switch/
│       ├── SKILL.md
│       ├── clash.py
│       └── requirements.txt
├── README.md            # English
├── README_zh.md        # 中文
└── CHANGELOG.md        # 更新日志
```

## License

MIT

## 作者

- GitHub: [@adminlove520](https://github.com/adminlove520)

## 更新日志

See [CHANGELOG.md](./CHANGELOG.md)

## 文档

- [OpenClaw Skill 安装指南](./docs/openclaw.md) - 详细的 OpenClaw 安装配置
- [原始 README](./README.md)


path: skills/clash-auto-switch/clash.py
content: #!/usr/bin/env python3
"""
Clash Auto Switch - OpenClaw Skill 实现
"""

import json
import os
import sys
import argparse
from typing import Optional, Dict, List, Tuple

try:
    import requests
except ImportError:
    print("请安装 requests: pip install requests")
    sys.exit(1)


class ClashAutoSwitch:
    """Clash 代理自动切换工具"""
    
    def __init__(self, api_url: str = None, secret: str = None, proxy_url: str = None):
        self.api_url = api_url or os.environ.get("CLASH_API", "http://127.0.0.1:58871")
        self.[REDACTED_SECRET] or os.environ.get("CLASH_SECRET", "")
        self.proxy_url = proxy_url or os.environ.get("CLASH_PROXY", "http://127.0.0.1:7890")
        
        if not self.[REDACTED_SECRET]"错误: 请设置 CLASH_SECRET 环境变量或提供 --secret 参数")
            sys.exit(1)
            
        self.headers = {"Authorization": f"Bearer {self.secret}"}
    
    def _request(self, method: str, endpoint: str, **kwargs) -> Optional[Dict]:
        """发送 API 请求"""
        url = f"{self.api_url.rstrip('/')}{endpoint}"
        try:
            if method.upper() == "GET":
                r = requests.get(url, headers=self.headers, timeout=10, **kwargs)
            elif method.upper() == "PUT":
                r = requests.put(url, headers=self.headers, timeout=10, **kwargs)
            else:
                return None
            return r.json() if r.status_code in [200, 204] else None
        except Exception as e:
            return None
    
    def get_proxies(self) -> Dict:
        """获取所有代理"""
        return self._request("GET", "/proxies") or {}
    
    def get_proxy_group(self, name: str) -> Dict:
        """获取代理组详情"""
        return self._request("GET", f"/proxies/{name}") or {}
    
    def set_proxy(self, group: str, node: str) -> bool:
        """切换节点"""
        result = self._request("PUT", f"/proxies/{group}", json={"name": node})
        return result is not None
    
    def test_delay(self, node: str) -> int:
        """测试节点延迟"""
        encoded = requests.utils.quote(node)
        url = f"{self.api_url}/proxies/{encoded}/delay?timeout=5000&url=http://www.gstatic.com/generate_204"
        try:
            r = requests.get(url, headers=self.headers, timeout=10)
            data = r.json()
            return data.get("delay", 99999)
        except:
            return 99999
    
    def health_check(self, targets: List[str] = None) -> Tuple[int, int]:
        """健康检查"""
        if targets is None:
            targets = [
                "https://api.telegram.org",
                "https://api.anthropic.com",
                "https://www.google.com"
            ]
        
        success = 0
        proxies = {"http": self.proxy_url, "https": self.proxy_url}
        
        for target in targets:
            try:
                r = requests.get(target, proxies=proxies, timeout=5)
                if r.status_code < 500:
                    success += 1
            except:
                pass
        
        return success, len(targets)
    
    def list_groups(self) -> Dict[str, str]:
        """列出所有代理组"""
        data = self.get_proxies()
        groups = {}
        for name, info in data.get("proxies", {}).items():
            if info.get("type") == "Selector":
                groups[name] = info.get("now", "未知")
        return groups
    
    def find_best_node(self, preferred_regions: List[str] = None) -> Tuple[Optional[str], Optional[str], int]:
        """找最佳节点"""
        if preferred_regions is None:
            preferred_regions = ["新加坡", "香港", "日本", "美国", "SG", "HK", "JP", "US"]
        
        best_node = None
        best_group = None
        best_delay = 99999
        
        groups = self.list_groups()
        
        for group, current in groups.items():
            if current == "节点选择":
                info = self.get_proxy_group(group)
                nodes = info.get("all", [])
                
                for node in nodes:
                    if node == "节点选择":
                        continue
                    
                    delay = self.test_delay(node)
                    
                    # 检查是否优先区域
                    is_preferred = any(r.lower() in node.lower() for r in preferred_regions)
                    
                    if is_preferred and delay < best_delay:
                        best_node = node
                        best_group = group
                        best_delay = delay
                    elif not best_node and delay < best_delay:
                        best_node = node
                        best_group = group
                        best_delay = delay
        
        return best_node, best_group, best_delay
    
    def auto_switch(self, preferred_regions: List[str] = None) -> str:
        """自动切换"""
        # 健康检查
        success, total = self.health_check()
        rate = success * 100 // total if total > 0 else 0
        
        if rate >= 60:
            return f"✓ 代理健康 ({success}/{total})"
        
        # 找最佳节点
        node, group, delay = self.find_best_node(preferred_regions)
        
        if node and group:
            self.set_proxy(group, node)
            return f"✓ 已切换 {group} -> {node} ({delay}ms)"
        else:
            return "✗ 未找到可用节点"


def main():
    parser = argparse.ArgumentParser(description="Clash Auto Switch - OpenClaw Skill")
    parser.add_argument("--api", default=os.environ.get("CLASH_API"), help="Clash API 地址")
    parser.add_argument("--secret", "-s", default=os.environ.get("CLASH_SECRET"), help="API 密钥")
    parser.add_argument("--proxy", default=os.environ.get("CLASH_PROXY"), help="代理地址")
    parser.add_argument("--list", "-l", action="store_true", help="列出代理组")
    parser.add_argument("--health", action="store_true", help="健康检查")
    parser.add_argument("--auto", "-a", action="store_true", help="自动切换")
    parser.add_argument("--status", action="store_true", help="查看状态")
    parser.add_argument("--switch", nargs=2, metavar=("GROUP", "NODE"), help="切换节点")
    parser.add_argument("--sg", action="store_true", help="切换到新加坡")
    parser.add_argument("--us", action="store_true", help="切换到美国")
    parser.add_argument("--jp", action="store_true", help="切换到日本")
    parser.add_argument("--hk", action="store_true", help="切换到香港")
    
    args = parser.parse_args()
    
    clash = ClashAutoSwitch(args.api, args.secret, args.proxy)
    
    # 健康检查
    if args.health:
        success, total = clash.health_check()
        rate = success * 100 // total if total > 0 else 0
        if rate >= 60:
            print(f"✓ 代理健康 ({success}/{total})")
        else:
            print(f"✗ 代理不健康 ({success}/{total})")
    
    # 列出节点
    elif args.list:
        for group, current in clash.list_groups().items():
            print(f"{group}: {current}")
    
    # 自动切换
    elif args.auto:
        print(clash.auto_switch())
    
    # 状态
    elif args.status:
        success, total = clash.health_check()
        print(f"健康度: {success}/{total}")
        print("\n代理组:")
        for group, current in clash.list_groups().items():
            print(f"  {group}: {current}")
    
    # 手动切换
    elif args.switch:
        group, node = args.switch
        if clash.set_proxy(group, node):
            print(f"✓ 已切换 {group} -> {node}")
        else:
            print(f"✗ 切换失败")
    
    # 区域切换
    elif args.sg:
        print(clash.auto_switch(["新加坡", "SG"]))
    elif args.us:
        print(clash.auto_switch(["美国", "US", "LA"]))
    elif args.jp:
        print(clash.auto_switch(["日本", "JP"]))
    elif args.hk:
        print(clash.auto_switch(["香港", "HK"]))
    
    else:
        parser.print_help()


if __name__ == "__main__":
    main()


path: skills/clash-auto-switch/requirements.txt
content: requests>=2.28.0


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