Skill: wei-wei-zhao/auto-model-switch (version 1.0.0)

--- name: auto-model-switch slug: auto-model-switch version: 1.0.0 description: "自动切换模型 - 当模型token用完或限流时，自动切换到备用模型，并通知用户。支持配置多个备用模型，智能切换策略。" changelog: "v1.0.0 初始版本：自动检测token消耗和限流，智能切换模型" metadata: clawdbot: emoji: "🔄" requires: bins: ["node"] os: ["linux", "darwin", "win32"] --- # 自动切换模型 (Auto Model Switch) ## 概述 当主模型token用完或遇到限流时，自动切换到备用模型，确保对话不中断。 ## 快速开始 ### 1. 安装依赖 ```bash cd ~/.openclaw/workspace/skills/auto-model-switch npm install ``` ### 2. 配置模型 编辑 `config.yaml`： ```yaml models: - id: primary model: custom-maas-coding-api-cn-huabei-1-xf-yun-com/astron-code-latest name: "Astron Code" daily_limit: 10000000 priority: 1 - id: backup-1 model: zai/glm-5 name: "GLM-4.5" daily_limit: null priority: 2 ``` ### 3. 启用心跳检查 在 `HEARTBEAT.md` 中添加： ```markdown - 检查模型状态: node ~/.openclaw/workspace/skills/auto-model-switch/heartbeat.js ``` ## 命令 | 命令 | 说明 | |------|------| | `node auto_model_switch.js status` | 查看当前模型状态 | | `node auto_model_switch.js switch` | 手动切换模型 | | `node auto_model_switch.js heartbeat` | 心跳检查（自动检测和切换） | | `node auto_model_switch.js sync` | 从网关同步token使用量 | ## 触发条件 - **警告**：Token使用 > 80% - **切换**：Token使用 > 95% 或 API限流 ## 配置说明 ```yaml models: - id: primary # 模型标识 model: model-id # OpenClaw模型ID name: "显示名称" # 友好名称 daily_limit: 10000000 # 每日token限制（null=无限制） priority: 1 # 优先级（数字越小越优先） auto_switch: on_limit_exceeded: true # token用完时切换 on_rate_limit: true # 限流时切换 retry_delay: 60 # 限流后重试延迟（秒） warning_threshold: 0.8 # 警告阈值 critical_threshold: 0.95 # 切换阈值 notification: enabled: true # 启用通知 ``` ## 与OpenClaw集成 设置环境变量以启用网关集成： ```bash export OPENCLAW_GATEWAY_URL="http://localhost:3000" export OPENCLAW_GATEWAY_TOKEN="your-token" ``` ## 示例输出 ``` 📊 模型状态 当前：Astron Code Token：8.5M / 10M (85%) 备用模型： - GLM-4.5 (可用) ``` ## 文件说明 ``` auto-model-switch/ ├── SKILL.md # 本文件 ├── config.yaml # 模型配置 ├── auto_model_switch.js # 主脚本 ├── heartbeat.js # 心跳脚本 ├── package.json # Node.js配置 ├── QUICKSTART.md # 快速开始 └── state/ # 状态目录（自动创建） ├── model-switch.json # 当前状态 └── switch-history.json # 切换历史 ``` --- _🔄 让模型切换自动化，对话永不断线_

path: auto_model_switch.js
content: #!/usr/bin/env node
/**
 * 自动切换模型 - OpenClaw集成版
 * 
 * 功能：
 * - 自动检测token消耗
 * - 自动检测API限流
 * - 智能切换备用模型
 * - 与OpenClaw网关集成
 * - 支持心跳触发
 */

const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');
const http = require('http');
const https = require('https');

class AutoModelSwitch {
  constructor(configPath = null) {
    // 配置路径
    this.configPath = configPath || path.join(__dirname, 'config.yaml');
    this.statePath = path.join(__dirname, 'state', 'model-switch.json');
    this.historyPath = path.join(__dirname, 'state', 'switch-history.json');
    
    // 加载配置和状态
    this.config = this.loadConfig();
    this.state = this.loadState();
    this.history = this.loadHistory();
    
    // OpenClaw网关配置
    this.gatewayUrl = process.env.OPENCLAW_GATEWAY_URL || 'http://localhost:3000';
    this.gatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN || '';
  }

  // ========== 配置和状态管理 ==========

  loadConfig() {
    try {
      if (fs.existsSync(this.configPath)) {
        const content = fs.readFileSync(this.configPath, 'utf8');
        return yaml.load(content);
      }
    } catch (e) {
      console.error('配置加载失败:', e.message);
    }
    
    // 默认配置
    return {
      models: [
        { id: 'primary', model: [REDACTED_SECRET], name: 'Astron Code', daily_limit: 10000000, priority: 1 },
        { id: 'backup-1', model: 'zai/glm-5', name: 'GLM-4.5', daily_limit: null, priority: 2 }
      ],
      notification: { enabled: true },
      auto_switch: { on_limit_exceeded: true, on_rate_limit: true, warning_threshold: 0.8, critical_threshold: 0.95 }
    };
  }

  loadState() {
    try {
      if (fs.existsSync(this.statePath)) {
        let content = fs.readFileSync(this.statePath, 'utf8');
        // 移除BOM
        if (content.charCodeAt(0) === 0xFEFF) {
          content = content.slice(1);
        }
        return JSON.parse(content);
      }
    } catch (e) {
      console.error('状态加载失败:', e.message);
    }
    
    return {
      current_model: null,
      token_usage: {},
      rate_limits: {},
      last_check: null
    };
  }

  saveState() {
    const dir = path.dirname(this.statePath);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    this.state.last_check = new Date().toISOString();
    fs.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
  }

  loadHistory() {
    try {
      if (fs.existsSync(this.historyPath)) {
        let content = fs.readFileSync(this.historyPath, 'utf8');
        // 移除BOM
        if (content.charCodeAt(0) === 0xFEFF) {
          content = content.slice(1);
        }
        return JSON.parse(content);
      }
    } catch (e) {}
    
    return { switches: [] };
  }

  saveHistory(entry) {
    const dir = path.dirname(this.historyPath);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    
    this.history.switches.push({
      timestamp: new Date().toISOString(),
      ...entry
    });
    
    // 只保留最近100条
    if (this.history.switches.length > 100) {
      this.history.switches = this.history.switches.slice(-100);
    }
    
    fs.writeFileSync(this.historyPath, JSON.stringify(this.history, null, 2));
  }

  // ========== 模型管理 ==========

  getCurrentModel() {
    return this.state.current_model || this.config.models[0]?.model;
  }

  getModelConfig(modelId) {
    return this.config.models.find(m => m.model === modelId || m.id === modelId);
  }

  getTokenUsage(model) {
    const today = new Date().toISOString().split('T')[0];
    const usage = this.state.token_usage[today];
    if (!usage) return 0;
    return usage[model] || 0;
  }

  getUsagePercentage(model) {
    const config = this.getModelConfig(model);
    if (!config || !config.daily_limit) return 0;
    return this.getTokenUsage(model) / config.daily_limit;
  }

  // ========== OpenClaw集成 ==========

  async getGatewayStatus() {
    return new Promise((resolve, reject) => {
      const url = new URL('/api/status', this.gatewayUrl);
      const client = url.protocol === 'https:' ? https : http;
      
      const req = client.get(url, {
        headers: {
          'Authorization': `Bearer ${this.gatewayToken}`,
          'Content-Type': 'application/json'
        }
      }, (res) => {
        let data = '';
        res.on('data', chunk => data += chunk);
        res.on('end', () => {
          try {
            resolve(JSON.parse(data));
          } catch (e) {
            reject(e);
          }
        });
      });
      
      req.on('error', reject);
      req.setTimeout(5000, () => {
        req.destroy();
        reject(new Error('Timeout'));
      });
    });
  }

  async switchModelViaGateway(targetModel) {
    return new Promise((resolve, reject) => {
      const url = new URL('/api/config/model', this.gatewayUrl);
      const client = url.protocol === 'https:' ? https : http;
      
      const data = JSON.stringify({ model: targetModel });
      
      const options = {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${this.gatewayToken}`,
          'Content-Type': 'application/json',
          'Content-Length': Buffer.byteLength(data)
        }
      };
      
      const req = client.request(url, options, (res) => {
        let body = '';
        res.on('data', chunk => body += chunk);
        res.on('end', () => {
          if (res.statusCode >= 200 && res.statusCode < 300) {
            resolve({ success: true });
          } else {
            reject(new Error(`HTTP ${res.statusCode}: ${body}`));
          }
        });
      });
      
      req.on('error', reject);
      req.write(data);
      req.end();
    });
  }

  // ========== 切换逻辑 ==========

  shouldSwitch() {
    const current = this.getCurrentModel();
    const percentage = this.getUsagePercentage(current);
    const threshold = this.config.auto_switch?.critical_threshold || 0.95;
    
    return percentage >= threshold;
  }

  shouldWarn() {
    const current = this.getCurrentModel();
    const percentage = this.getUsagePercentage(current);
    const threshold = this.config.auto_switch?.warning_threshold || 0.8;
    
    return percentage >= threshold && percentage < (this.config.auto_switch?.critical_threshold || 0.95);
  }

  getNextModel() {
    const current = this.getCurrentModel();
    const models = [...this.config.models].sort((a, b) => a.priority - b.priority);
    
    for (const model of models) {
      if (model.model === current) continue;
      
      // 检查是否被限流
      if (this.isRateLimited(model.model)) continue;
      
      return model;
    }
    
    return null;
  }

  isRateLimited(model) {
    if (!this.state.rate_limits) return false;
    const limit = this.state.rate_limits[model];
    if (!limit) return false;
    
    // 限流60秒后自动解除
    const retryDelay = this.config.auto_switch?.retry_delay || 60;
    const elapsed = (Date.now() - new Date(limit.timestamp).getTime()) / 1000;
    
    return elapsed < retryDelay;
  }

  recordRateLimit(model) {
    this.state.rate_limits[model] = {
      timestamp: new Date().toISOString()
    };
    this.saveState();
  }

  async switch(reason = 'token_limit') {
    if (!this.shouldSwitch()) {
      return { switched: false, reason: 'no_need' };
    }
    
    const next = this.getNextModel();
    if (!next) {
      this.notify('❌ 所有模型不可用');
      return { switched: false, reason: 'no_available_model' };
    }
    
    const old = this.getCurrentModel();
    
    try {
      // 尝试通过网关切换
      if (this.gatewayToken) {
        await this.switchModelViaGateway(next.model);
      }
      
      // 更新状态
      this.state.current_model = next.model;
      this.saveState();
      
      // 记录历史
      this.saveHistory({
        from: old,
        to: next.model,
        reason: reason,
        token_usage: this.getTokenUsage(old)
      });
      
      this.notify(`✅ 模型切换: ${old} → ${next.name}`);
      
      return { switched: true, from: old, to: next.model };
    } catch (e) {
      console.error('切换失败:', e.message);
      return { switched: false, reason: 'switch_failed', error: e.message };
    }
  }

  // ========== Token更新 ==========

  updateUsage(model, tokens) {
    const today = new Date().toISOString().split('T')[0];
    if (!this.state.token_usage[today]) {
      this.state.token_usage[today] = {};
    }
    this.state.token_usage[today][model] = 
      (this.state.token_usage[today][model] || 0) + tokens;
    this.saveState();
  }

  // 从OpenClaw状态更新token使用量
  async syncFromGateway() {
    try {
      const status = await this.getGatewayStatus();
      if (status.usage) {
        const model = status.model || this.getCurrentModel();
        this.updateUsage(model, status.usage.total_tokens || 0);
        return true;
      }
    } catch (e) {
      console.error('同步失败:', e.message);
    }
    return false;
  }

  // ========== 通知 ==========

  notify(message) {
    if (this.config.notification?.enabled) {
      console.log(`[AutoModelSwitch] ${message}`);
      
      // 可以扩展：发送到飞书、邮件等
      // this.sendToFeishu(message);
    }
  }

  // ========== 状态报告 ==========

  getStatusReport() {
    const current = this.getCurrentModel();
    const currentConfig = this.getModelConfig(current);
    const usage = currentConfig ? this.getTokenUsage(current) : 0;
    const percentage = currentConfig?.daily_limit ? usage / currentConfig.daily_limit : 0;
    
    let report = `📊 模型状态\n`;
    report += `当前：${currentConfig?.name || current}\n`;
    
    if (currentConfig?.daily_limit) {
      const used = (usage / 1000000).toFixed(1);
      const total = (currentConfig.daily_limit / 1000000).toFixed(0);
      const pct = (percentage * 100).toFixed(0);
      report += `Token：${used}M / ${total}M (${pct}%)\n`;
    } else {
      report += `Token：无限制\n`;
    }
    
    // 备用模型状态
    const backups = this.config.models.filter(m => m.model !== current);
    if (backups.length > 0) {
      report += `\n备用模型：\n`;
      for (const backup of backups) {
        const limited = this.isRateLimited(backup.model) ? ' (限流中)' : ' (可用)';
        report += `- ${backup.name}${limited}\n`;
      }
    }
    
    return report;
  }

  // ========== 心跳检查 ==========

  async heartbeat() {
    // 同步token使用量
    await this.syncFromGateway();
    
    // 检查是否需要警告
    if (this.shouldWarn()) {
      const current = this.getCurrentModel();
      const pct = (this.getUsagePercentage(current) * 100).toFixed(0);
      this.notify(`⚠️ Token使用已达 ${pct}%，建议切换模型`);
      return { action: 'warn', percentage: pct };
    }
    
    // 检查是否需要切换
    if (this.shouldSwitch()) {
      const result = await this.switch();
      return { action: 'switch', ...result };
    }
    
    return { action: 'ok' };
  }
}

// ========== CLI入口 ==========

if (require.main === module) {
  const args = process.argv.slice(2);
  const ams = new AutoModelSwitch();
  
  (async () => {
    try {
      switch (args[0]) {
        case 'status':
          console.log(ams.getStatusReport());
          break;
          
        case 'switch':
          const result = await ams.switch(args[1] || 'manual');
          console.log(JSON.stringify(result, null, 2));
          break;
          
        case 'heartbeat':
          const hbResult = await ams.heartbeat();
          console.log(JSON.stringify(hbResult, null, 2));
          break;
          
        case 'sync':
          const synced = await ams.syncFromGateway();
          console.log(synced ? '同步成功' : '同步失败');
          break;
          
        case 'update':
          // 手动更新token: node auto_model_switch.js update <model> <tokens>
          if (args[1] && args[2]) {
            ams.updateUsage(args[1], parseInt(args[2]));
            console.log('已更新');
          } else {
            console.log('用法: node auto_model_switch.js update <model> <tokens>');
          }
          break;
          
        default:
          console.log(`
用法: node auto_model_switch.js <command>

命令:
  status     显示当前模型状态
  switch     手动切换模型
  heartbeat  心跳检查（自动检测和切换）
  sync       从网关同步token使用量
  update     手动更新token使用量

示例:
  node auto_model_switch.js status
  node auto_model_switch.js switch
  node auto_model_switch.js heartbeat
`);
      }
    } catch (e) {
      console.error('错误:', e.message);
      process.exit(1);
    }
  })();
}

module.exports = AutoModelSwitch;


path: auto_model_switch.py
content: #!/usr/bin/env python3
"""
自动切换模型 - 主程序
监控token使用和API限流，自动切换到备用模型
"""

import json
import os
import time
from datetime import datetime
from pathlib import Path


class AutoModelSwitch:
    def __init__(self, config_path="config.yaml"):
        self.config = self.load_config(config_path)
        self.state = self.load_state()
        self.current_model = self.state.get("current_model", self.config["models"][0]["model"])
        self.token_usage = self.state.get("token_usage", {})
        self.rate_limits = self.state.get("rate_limits", {})
        
    def load_config(self, config_path):
        """加载配置"""
        import yaml
        with open(config_path, 'r', encoding='utf-8') as f:
            return yaml.safe_load(f)
    
    def load_state(self):
        """加载状态"""
        state_path = Path("state/model-switch.json")
        if state_path.exists():
            with open(state_path, 'r', encoding='utf-8') as f:
                return json.load(f)
        return {}
    
    def save_state(self):
        """保存状态"""
        state_path = Path("state/model-switch.json")
        state_path.parent.mkdir(parents=True, exist_ok=True)
        
        self.state = {
            "current_model": self.current_model,
            "token_usage": self.token_usage,
            "rate_limits": self.rate_limits,
            "last_update": datetime.now().isoformat()
        }
        
        with open(state_path, 'w', encoding='utf-8') as f:
            json.dump(self.state, f, indent=2, ensure_ascii=False)
    
    def get_current_model_config(self):
        """获取当前模型配置"""
        for model in self.config["models"]:
            if model["model"] == self.current_model:
                return model
        return None
    
    def check_token_usage(self):
        """检查token使用情况"""
        model_config = self.get_current_model_config()
        if not model_config or not model_config.get("daily_limit"):
            return None
        
        today = datetime.now().strftime("%Y-%m-%d")
        used = self.token_usage.get(today, {}).get(self.current_model, 0)
        limit = model_config["daily_limit"]
        
        return {
            "used": used,
            "limit": limit,
            "percentage": used / limit,
            "remaining": limit - used
        }
    
    def should_switch(self):
        """判断是否需要切换"""
        # 检查token使用
        usage = self.check_token_usage()
        if usage and usage["percentage"] >= self.config["auto_switch"]["critical_threshold"]:
            return True, "token_limit_exceeded"
        
        # 检查限流
        if self.current_model in self.rate_limits:
            limit_info = self.rate_limits[self.current_model]
            if time.time() - limit_info["timestamp"] < limit_info["retry_after"]:
                return True, "rate_limited"
        
        return False, None
    
    def get_next_model(self):
        """获取下一个可用模型"""
        models = sorted(self.config["models"], key=lambda x: x["priority"])
        
        for model in models:
            # 跳过当前模型
            if model["model"] == self.current_model:
                continue
            
            # 跳过限流中的模型
            if model["model"] in self.rate_limits:
                limit_info = self.rate_limits[model["model"]]
                if time.time() - limit_info["timestamp"] < limit_info["retry_after"]:
                    continue
            
            return model
        
        return None
    
    def switch_model(self, reason="manual"):
        """切换模型"""
        should, reason_code = self.should_switch()
        if not should and reason == "auto":
            return False
        
        next_model = self.get_next_model()
        if not next_model:
            self.notify("❌ 所有模型不可用，请等待")
            return False
        
        old_model = self.current_model
        self.current_model = next_model["model"]
        self.save_state()
        
        if self.config["notification"]["enabled"]:
            self.notify(f"✅ 模型切换：{old_model} → {next_model['name']}（{reason}）")
        
        return True
    
    def update_token_usage(self, model, tokens):
        """更新token使用"""
        today = datetime.now().strftime("%Y-%m-%d")
        if today not in self.token_usage:
            self.token_usage[today] = {}
        
        self.token_usage[today][model] = self.token_usage[today].get(model, 0) + tokens
        self.save_state()
        
        # 检查是否需要警告
        usage = self.check_token_usage()
        if usage and usage["percentage"] >= self.config["auto_switch"]["warning_threshold"]:
            self.notify(f"⚠️ Token使用已达 {usage['percentage']*100:.1f}%")
    
    def record_rate_limit(self, model, retry_after=60):
        """记录限流"""
        self.rate_limits[model] = {
            "timestamp": time.time(),
            "retry_after": retry_after
        }
        self.save_state()
        
        # 自动切换
        self.switch_model("rate_limited")
    
    def notify(self, message):
        """发送通知"""
        print(f"[AutoModelSwitch] {message}")
        # TODO: 集成OpenClaw消息系统
    
    def get_status(self):
        """获取状态"""
        usage = self.check_token_usage()
        model_config = self.get_current_model_config()
        
        status = {
            "current_model": self.current_model,
            "model_name": model_config["name"] if model_config else "Unknown",
            "token_usage": usage
        }
        
        return status


def main():
    """主函数"""
    switcher = AutoModelSwitch()
    
    # 检查状态
    status = switcher.get_status()
    print(f"当前模型: {status['model_name']}")
    
    if status["token_usage"]:
        print(f"Token使用: {status['token_usage']['used']}/{status['token_usage']['limit']}")
    
    # 检查是否需要切换
    should, reason = switcher.should_switch()
    if should:
        print(f"需要切换模型: {reason}")
        switcher.switch_model(reason)


if __name__ == "__main__":
    main()


path: config.yaml
content: # 自动切换模型配置

models:
  - id: primary
    model: custom-maas-coding-api-cn-huabei-1-xf-yun-com/astron-code-latest
    name: "Astron Code"
    daily_limit: 10000000
    priority: 1
  
  - id: backup-1
    model: zai/glm-5
    name: "GLM-4.5"
    daily_limit: null
    priority: 2
  
  - id: backup-2
    model: gl
    name: "Astron Code (备用)"
    daily_limit: null
    priority: 3

notification:
  enabled: true
  message: "🔄 模型已切换：{from} → {to}"

auto_switch:
  on_limit_exceeded: true
  on_rate_limit: true
  retry_delay: 60
  warning_threshold: 0.8
  critical_threshold: 0.95


path: heartbeat.js
content: #!/usr/bin/env node
/**
 * 心跳触发脚本 - 供 HEARTBEAT.md 调用
 * 
 * 用法: node heartbeat.js
 * 
 * 返回:
 * - OK: 一切正常
 * - WARN: Token即将用完
 * - SWITCH: 已切换模型
 * - ERROR: 所有模型不可用
 */

const AutoModelSwitch = require('./auto_model_switch.js');

async function main() {
  const ams = new AutoModelSwitch();
  
  try {
    const result = await ams.heartbeat();
    
    switch (result.action) {
      case 'ok':
        console.log('OK');
        break;
        
      case 'warn':
        console.log(`WARN: Token使用 ${result.percentage}%`);
        break;
        
      case 'switch':
        if (result.switched) {
          console.log(`SWITCH: ${result.from} → ${result.to}`);
        } else {
          console.log(`ERROR: 无法切换 - ${result.reason}`);
        }
        break;
    }
  } catch (e) {
    console.log('ERROR:', e.message);
  }
}

main();


path: package-lock.json
content: {
  "name": "auto-model-switch",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "auto-model-switch",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "js-yaml": "^4.1.0"
      }
    },
    "node_modules/argparse": {
      "version": "2.0.1",
      "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "Python-2.0"
    },
    "node_modules/js-yaml": {
      "version": "4.1.1",
      "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "argparse": "^2.0.1"
      },
      "bin": {
        "js-yaml": "bin/js-yaml.js"
      }
    }
  }
}


path: package.json
content: {
  "name": "auto-model-switch",
  "version": "1.0.0",
  "description": "自动切换模型 - 避免token限制和限流中断",
  "main": "auto_model_switch.js",
  "scripts": {
    "test": "node auto_model_switch.js status",
    "switch": "node auto_model_switch.js switch"
  },
  "keywords": [
    "openclaw",
    "model",
    "switch",
    "token",
    "rate-limit"
  ],
  "author": "龙虾小队",
  "license": "MIT",
  "dependencies": {
    "js-yaml": "^4.1.0"
  }
}


path: QUICKSTART.md
content: # 快速开始

## 安装依赖

```bash
cd auto-model-switch
npm install
```

## 配置

编辑 `config.yaml`，设置你的模型：

```yaml
models:
  - id: primary
    model: your-primary-model
    name: "主模型"
    daily_limit: 10000000  # 每日token限制
    priority: 1
  
  - id: backup-1
    model: your-backup-model
    name: "备用模型"
    daily_limit: null  # 无限制
    priority: 2
```

## 使用方法

### 1. 查看状态

```bash
node auto_model_switch.js status
```

输出：
```
📊 模型状态
当前：Astron Code
Token：8.5M / 10M (85%)

备用模型：
- GLM-4.5 (可用)
```

### 2. 手动切换

```bash
node auto_model_switch.js switch
```

### 3. 心跳检查（自动）

在 `HEARTBEAT.md` 中添加：

```markdown
- 运行: node ~/.openclaw/workspace/skills/auto-model-switch/heartbeat.js
```

或在心跳时调用：

```bash
node auto_model_switch.js heartbeat
```

### 4. 同步token使用量

```bash
node auto_model_switch.js sync
```

## 与OpenClaw集成

### 环境变量

```bash
export OPENCLAW_GATEWAY_URL="http://localhost:3000"
export OPENCLAW_GATEWAY_TOKEN="your-token"
```

### 自动切换

当token使用超过95%时，自动切换到备用模型。

## 测试

```bash
npm test
```


path: README.md
content: # Auto Model Switch

自动切换模型，避免token限制和限流中断。

## 快速开始

1. 复制到skills目录
2. 配置 `config.yaml`
3. 重启OpenClaw

## 功能

- ✅ 自动检测token消耗
- ✅ 自动检测API限流
- ✅ 智能切换备用模型
- ✅ 通知用户切换状态

## 配置

编辑 `config.yaml` 设置模型优先级和切换策略。

## 开源

将发布到ClawHub，供所有OpenClaw用户使用。

---

🦞 龙虾小队出品


path: state/model-switch.json
content: {"current_model":"zai/glm-5","token_usage":{"2026-03-24":{"zai/glm-5":5000000}}}


path: test.py
content: #!/usr/bin/env python3
"""测试自动切换模型"""

import sys
sys.path.insert(0, '.')

from auto_model_switch import AutoModelSwitch


def test_basic():
    """基础测试"""
    print("=" * 50)
    print("测试自动切换模型")
    print("=" * 50)
    
    switcher = AutoModelSwitch()
    
    # 1. 获取状态
    print("\n1. 当前状态:")
    status = switcher.get_status()
    print(f"   模型: {status['model_name']}")
    print(f"   ID: {status['current_model']}")
    
    # 2. 模拟token使用
    print("\n2. 模拟token使用:")
    switcher.update_token_usage(switcher.current_model, 1000000)
    usage = switcher.check_token_usage()
    if usage:
        print(f"   已用: {usage['used']:,}")
        print(f"   限制: {usage['limit']:,}")
        print(f"   百分比: {usage['percentage']*100:.1f}%")
    
    # 3. 测试切换
    print("\n3. 测试切换:")
    success = switcher.switch_model("manual")
    print(f"   切换结果: {'成功' if success else '失败'}")
    print(f"   新模型: {switcher.current_model}")
    
    # 4. 最终状态
    print("\n4. 最终状态:")
    status = switcher.get_status()
    print(f"   模型: {status['model_name']}")
    
    print("\n" + "=" * 50)
    print("测试完成")
    print("=" * 50)


if __name__ == "__main__":
    test_basic()


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