Skill: jiangzhiyu/auto-agent-router (version 1.0.0)

--- name: auto-agent-router description: "根据消息命令自动路由到子 Agent。**Agent 应该：收到消息时首先检查是否包含 /coder、/writer 等命令，如果是则调用 sessions_spawn 启动对应子 Agent。**" metadata: openclaw: emoji: "🤖" requires: bins: ["python3"] --- # Auto Agent Router - 自动子 Agent 路由 根据消息中的命令自动路由到合适的子 Agent，实现并发处理和专业化分工。 --- ## 🎯 When to Use ### Agent 行为准则 **收到每条消息时，首先检查是否包含命令**： ✅ **USE this skill when:** - 消息开头是 `/coder` → 启动 coder Agent (qwen3-coder-next) - 消息开头是 `/writer` → 启动 writer Agent (qwen3.5-plus) - 消息开头是 `/analyze` → 启动 analyst Agent (qwen3.5-plus) - 消息开头是 `/research` → 启动 researcher Agent (gemini-3.1-pro) - 消息开头是 `/review` → 启动 reviewer Agent (qwen3-max) - 消息开头是 `/devops` → 启动 devops Agent (qwen3.5-plus) - 消息开头是 `/auto` → 自动判断合适的 Agent **检查流程**： 1. 收到消息 → 首先检查开头是否包含 `/xxx` 命令 2. 如果有命令 → 调用 `sessions_spawn` 启动对应子 Agent 3. 如果没有命令 → 主 Session 直接处理 --- ## 🚫 When NOT to Use ❌ **DON'T use this skill when:** - 消息中没有命令 → 主 Session 直接处理 - 命令不在消息开头 → 如"请帮我 /coder"不触发 - 简单问答不需要子 Agent → 主 Session 处理即可 - 命令在消息中间或结尾 → 不触发路由 --- ## 📋 Trigger Rules ### 触发格式（严格模式） | 格式 | 示例 | 结果 | |------|------|------| | **命令开头** | `/coder 写代码` | ✅ 触发 | | **@名字 + 命令** | `@小牛马 /coder 写代码` | ✅ 触发 | | **消息中包含命令** | `请帮我 /coder 写代码` | ❌ 不触发 | ### 可用命令 | 命令 | Agent | 模型 | 用途 | |------|-------|------|------| | `/coder` | coder | qwen3-coder-next | 代码任务 | | `/writer` | writer | qwen3.5-plus | 写作任务 | | `/analyze` | analyst | qwen3.5-plus | 数据分析 | | `/research` | researcher | gemini-3.1-pro | 调研任务 | | `/review` | reviewer | qwen3-max | 审查优化 | | `/devops` | devops | qwen3.5-plus | 运维操作 | | `/auto` | 自动判断 | 根据内容 | 智能路由 | --- ## 🔧 How to Use ### 基本用法 **用户发送命令**： ``` /coder 写个 Hello World ``` **Agent 响应**： ``` ✅ 🧑‍💻 已启动 **coder** (代码专家) 处理您的任务 ───────────────────── 🤖 处理者：Agent: 🧑💻 coder 模型：dashscope/qwen3-coder-next ``` ### 带@的命令 **用户发送**： ``` @小牛马 /writer 写周报 ``` **处理流程**： 1. 去掉@名字 → `/writer 写周报` 2. 匹配命令 → `/writer` 3. 启动 Agent → writer 4. 返回结果 --- ## 📁 Implementation ### 核心组件 ``` ~/.openclaw/workspace/skills/auto-agent-router/ ├── SKILL.md # 本文档 ├── config.json # 路由配置 ├── auto-trigger.py # 触发检测 ├── dingtalk-command.py # 命令解析 ├── router.py # 路由匹配 └── logger.py # 日志记录 ``` ### 调用方式 **检测命令**： ```bash python3 ~/.openclaw/workspace/skills/auto-agent-router/auto-trigger.py "/coder 写代码" ``` **查看配置**： ```bash cat ~/.openclaw/workspace/skills/auto-agent-router/config.json ``` **查看日志**： ```bash tail -f /tmp/auto-route-handler.log ``` --- ## 🔄 Workflow ``` 用户消息：/coder 写代码 ↓ 1. 检测命令 (auto-trigger.py) - 去掉@名字 - 检查开头是否匹配命令 ↓ 2. 解析命令 (dingtalk-command.py) - 提取命令类型 - 提取任务内容 ↓ 3. 路由匹配 (router.py) - 查找 config.json - 确定目标 Agent ↓ 4. 启动 Agent (sessions_spawn) - 创建子 Agent Session - 分配任务 ↓ 5. 返回结果 - 子 Agent 完成任务 - 推送结果给用户 ``` --- ## 📊 Response Format ### 启动回复 ``` ✅ 🧑‍💻 已启动 **coder** (代码专家) 处理您的任务 ───────────────────── 🤖 处理者：Agent: 🧑💻 coder 模型：dashscope/qwen3-coder-next ``` ### 完成回复 ``` [子 Agent 的任务结果] ───────────────────── 🤖 处理者：Agent: 🧑💻 coder 运行时间：3s • Tokens: 20k ``` --- ## ⚙️ Configuration ### config.json ```json { "enabled": true, "autoRoute": true, "flexible": false, "bot_names": ["小牛马", "xiaoniuma", "AI 助手", "..."], "rules": [ { "type": "coding", "keywords": ["代码", "函数", "bug"], "agent": "coder", "model": "dashscope/qwen3-coder-next", "priority": "high" } ], "fallback": { "agent": null, "model": "dashscope/qwen3.5-plus" } } ``` ### 自定义 Agent 编辑 `config.json` 的 `rules` 数组添加新规则： ```json { "type": "custom", "keywords": ["关键词 1", "关键词 2"], "agent": "custom_agent", "model": "模型名称", "priority": "medium" } ``` --- ## 📝 Notes - **严格模式**：命令必须在消息开头，中间或结尾不触发 - **@名字可选**：支持带@或不带@的命令 - **自动学习**：支持自动学习新的机器人名字 - **并发处理**：多个子 Agent 可以并行运行 - **Session 隔离**：每个子 Agent 有独立的 Session - **日志位置**：`/tmp/auto-route-handler.log` --- ## 🧪 Testing ### 测试命令 ```bash # 测试触发 python3 ~/.openclaw/workspace/skills/auto-agent-router/auto-trigger.py "/coder 写代码" # 测试@命令 python3 ~/.openclaw/workspace/skills/auto-agent-router/auto-trigger.py "@小牛马 /coder 写代码" # 测试不触发 python3 ~/.openclaw/workspace/skills/auto-agent-router/auto-trigger.py "请帮我 /coder" ``` ### 预期结果 | 输入 | 输出 | |------|------| | `/coder 写代码` | ✅ 触发，路由到 coder | | `@小牛马 /coder 写代码` | ✅ 触发，路由到 coder | | `请帮我 /coder` | ❌ 不触发，主 Session 处理 | --- ## 🔗 Related - **MEMORY.md** - 包含配置和使用记录 - **sessions_spawn** - 启动子 Agent 的工具 --- **最后更新**: 2026-02-28 **版本**: 1.0.0

path: auto-route-handler.py
content: #!/usr/bin/env python3
"""
Auto Route Handler - 自动消息路由处理器

集成到 OpenClaw 消息处理流程，实现收到消息时自动判断并分流
"""

import json
import sys
import os
from pathlib import Path
from typing import Dict, Optional
import subprocess

# 添加技能路径
SKILL_DIR = Path(__file__).parent
sys.path.insert(0, str(SKILL_DIR))

from router import route_message, spawn_agent, load_agents

class Colors:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    NC = '\033[0m'

class AutoRouteHandler:
    """自动路由处理器"""
    
    def __init__(self, config_path: Optional[Path] = None):
        self.config_path = config_path or SKILL_DIR / "config.json"
        self.config = self._load_config()
        self.log_file = Path("/tmp/auto-route-handler.log")
    
    def _load_config(self) -> dict:
        """加载配置"""
        if not self.config_path.exists():
            return {"enabled": False}
        
        with open(self.config_path, 'r', encoding='utf-8') as f:
            return json.load(f)
    
    def _log(self, message: str):
        """记录日志"""
        from datetime import datetime
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        log_entry = f"[{timestamp}] {message}\n"
        
        # 写入日志文件
        with open(self.log_file, 'a', encoding='utf-8') as f:
            f.write(log_entry)
        
        # 同时输出到控制台
        print(log_entry.strip())
    
    def should_auto_route(self, message: str, chat_type: str = "group") -> bool:
        """
        判断是否应该自动路由
        
        Args:
            message: 消息内容
            chat_type: 聊天类型 (group/direct)
        
        Returns:
            bool: 是否应该自动路由
        """
        # 检查是否启用
        if not self.config.get('enabled', False):
            return False
        
        # 检查是否启用自动路由
        if not self.config.get('autoRoute', False):
            return False
        
        # 群聊优先自动路由（并发需求高）
        if chat_type == "group":
            return True
        
        # 私聊需要更复杂的任务才路由
        if chat_type == "direct":
            analysis = self._analyze_message(message)
            return analysis['complexity'] in ['medium', 'complex']
        
        return False
    
    def _analyze_message(self, message: str) -> dict:
        """分析消息"""
        from router import analyze_message
        return analyze_message(message)
    
    def handle_message(self, message: str, chat_type: str = "group", 
                      from_user: str = "", verbose: bool = True) -> dict:
        """
        处理消息 - 主入口
        
        Args:
            message: 消息内容
            chat_type: 聊天类型 (group/direct)
            from_user: 发送者
            verbose: 是否输出详细信息
        
        Returns:
            dict: 处理结果
        """
        self._log(f"收到消息：{message[:50]}... (from: {from_user}, type: {chat_type})")
        
        # 路由决策
        result = route_message(message, verbose=verbose)
        
        # 记录决策
        self._log(f"路由决策：{'路由到子 Agent' if result['should_route'] else '主 Session 处理'}")
        self._log(f"  原因：{result['reason']}")
        
        if result['should_route']:
            # 启动子 Agent
            self._log(f"启动子 Agent: {result['agent']} (模型：{result['model']})")
            
            # 构建任务描述
            task_description = f"""
【来自 {chat_type} 的消息】
发送者：{from_user or '未知'}
原始消息：{message}

请以 {result['agent']} 的身份专业地处理这个请求。
"""
            
            # 启动子 Agent
            spawn_result = spawn_agent(
                agent_name=result['agent'],
                task=task_description,
                model=result['model']
            )
            
            self._log(f"Agent 启动结果：{spawn_result}")
            
            return {
                'action': 'spawned_agent',
                'agent': result['agent'],
                'model': result['model'],
                'spawn_result': spawn_result,
                'message': f"已启动 {result['agent']} 处理您的请求..."
            }
        else:
            # 主 Session 处理
            self._log("由主 Session 直接处理")
            
            return {
                'action': 'handle_directly',
                'agent': None,
                'model': result['model'],
                'message': '主 Session 处理'
            }
    
    def handle_with_timeout(self, message: str, chat_type: str = "group",
                           from_user: str = "", timeout_seconds: int = 300) -> dict:
        """
        带超时控制的消息处理
        
        Args:
            message: 消息内容
            chat_type: 聊天类型
            from_user: 发送者
            timeout_seconds: 超时时间（秒）
        
        Returns:
            dict: 处理结果
        """
        import signal
        
        def timeout_handler(signum, frame):
            raise TimeoutError(f"Agent 执行超时（{timeout_seconds}秒）")
        
        # 设置超时
        signal.signal(signal.SIGALRM, timeout_handler)
        signal.alarm(timeout_seconds)
        
        try:
            result = self.handle_message(message, chat_type, from_user)
            signal.alarm(0)  # 取消超时
            return result
        except TimeoutError as e:
            self._log(f"超时：{e}")
            return {
                'action': 'timeout',
                'error': str(e),
                'message': '任务执行超时，已切换到主 Session 处理'
            }

def main():
    """命令行测试入口"""
    handler = AutoRouteHandler()
    
    # 测试消息
    test_cases = [
        ("写个 Python 脚本处理 Excel", "group", "用户 A"),
        ("今天天气怎么样", "group", "用户 B"),
        ("分析一下销售数据", "group", "用户 C"),
        ("帮我写周报", "direct", "主人"),
    ]
    
    print(f"{Colors.BLUE}{'='*60}{Colors.NC}")
    print(f"{Colors.BLUE}🧪 Auto Route Handler 测试{Colors.NC}")
    print(f"{Colors.BLUE}{'='*60}{Colors.NC}\n")
    
    for message, chat_type, from_user in test_cases:
        print(f"\n{Colors.CYAN}测试用例：{Colors.NC}")
        print(f"  消息：{message}")
        print(f"  类型：{chat_type}")
        print(f"  发送者：{from_user}")
        print(f"{Colors.BLUE}{'-'*60}{Colors.NC}")
        
        result = handler.handle_message(message, chat_type, from_user)
        
        print(f"\n{Colors.GREEN}处理结果：{Colors.NC}")
        print(f"  动作：{result['action']}")
        if result['agent']:
            print(f"  Agent: {result['agent']}")
        print(f"  回复：{result['message']}")
        print()

if __name__ == '__main__':
    main()


path: auto-trigger.py
content: #!/usr/bin/env python3
"""
钉钉消息自动触发器

集成到主 Session 消息处理流程，实现收到钉钉群@消息时自动路由到子 Agent
"""

import sys
import re
import json
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))

from dingtalk_command import handle_command

class Colors:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    NC = '\033[0m'

def check_command(message: str, chat_type: str = "", from_user: str = "") -> dict:
    """
    检查消息是否以命令开头，如果是则返回路由信息
    触发条件（严格模式）：
    1. 消息开头必须是 /coder 等命令
    2. 或 @名字 + 命令开头
    
    Args:
        message: 消息内容
        chat_type: 聊天类型 (dingtalk-group/dingtalk-direct/tui/other)
        from_user: 发送者
    
    Returns:
        dict: {
            'is_command': bool,
            'should_route': bool,
            'agent': str,
            'task': str,
            'message': str  # 回复消息
        }
    """
    # 可用命令列表
    COMMANDS = ['/coder', '/code', '/writer', '/write', '/analyze', '/analysis', 
                '/research', '/review', '/devops', '/ops', '/auto']
    
    # 清理消息：去掉开头的@名字
    clean_message = re.sub(r'@[^\s]+', '', message).strip()
    
    # 严格匹配：必须开头是命令
    has_command = any(clean_message.lower().startswith(cmd) for cmd in COMMANDS)
    
    if not has_command:
        return {
            'is_command': False,
            'should_route': False
        }
    
    # 调用命令处理器
    result = handle_command(message, from_user, verbose=False)
    
    if result.get('action') == 'spawned_agent':
        return {
            'is_command': True,
            'should_route': True,
            'agent': result.get('agent'),
            'task': result.get('message', ''),
            'handled_by': result.get('handled_by', ''),
            'message': f"{result.get('message', '')}\n\n─────────────────────\n🤖 处理者：{result.get('handled_by', '未知')}"
        }
    elif result.get('action') == 'handle_directly':
        return {
            'is_command': True,
            'should_route': False,
            'agent': None,
            'message': '简单任务，主 Session 处理'
        }
    else:
        return {
            'is_command': False,
            'should_route': False
        }

def main():
    """命令行测试"""
    if len(sys.argv) < 2:
        print("用法：python3 auto-trigger.py '<消息内容>' [聊天类型] [发送者]")
        print("\n示例：")
        print("  python3 auto-trigger.py '/coder 写代码'")
        print("  python3 auto-trigger.py '@小牛马 /coder 写代码'")
        print("  python3 auto-trigger.py '请帮我 /coder 写个脚本'")
        print("  python3 auto-trigger.py '/writer 写周报'")
        sys.exit(1)
    
    message = sys.argv[1]
    chat_type = sys.argv[2] if len(sys.argv) > 2 else "any"
    from_user = sys.argv[3] if len(sys.argv) > 3 else "未知用户"
    
    result = check_command(message, chat_type, from_user)
    
    print(f"\n{Colors.CYAN}检测结果：{Colors.NC}")
    print(f"  是否命令：{result['is_command']}")
    print(f"  是否路由：{result['should_route']}")
    if result.get('agent'):
        print(f"  Agent: {result['agent']}")
    if result.get('handled_by'):
        print(f"  处理者：{result['handled_by']}")
    print(f"  回复：{result.get('message', '')}")

if __name__ == '__main__':
    main()


path: config.json
content: {
  "enabled": true,
  "autoRoute": true,
  "bot_names": [
    "小牛马",
    "xiaoniuma",
    "小牛",
    "牛马",
    "AI 助手",
    "助手",
    "assistant",
    "bot",
    "智能机器人"
  ],
  "rules": [
    {
      "type": "coding",
      "keywords": [
        "代码",
        "函数",
        "bug",
        "调试",
        "编程",
        "写个",
        "脚本",
        "开发",
        "实现",
        "算法",
        "api",
        "接口"
      ],
      "agent": "coder",
      "model": "dashscope/qwen3-coder-next",
      "priority": "high"
    },
    {
      "type": "writing",
      "keywords": [
        "文章",
        "文档",
        "报告",
        "总结",
        "写作",
        "周报",
        "日报",
        "邮件",
        "文案"
      ],
      "agent": "writer",
      "model": "dashscope/qwen3.5-plus",
      "priority": "medium"
    },
    {
      "type": "analysis",
      "keywords": [
        "分析",
        "数据",
        "图表",
        "统计",
        "趋势",
        "对比",
        "可视化"
      ],
      "agent": "analyst",
      "model": "dashscope/qwen3.5-plus",
      "priority": "medium"
    },
    {
      "type": "research",
      "keywords": [
        "调研",
        "研究",
        "搜索",
        "查找",
        "资料",
        "背景",
        "竞品"
      ],
      "agent": "researcher",
      "model": "google/gemini-3.1-pro-preview",
      "priority": "medium"
    },
    {
      "type": "review",
      "keywords": [
        "审查",
        "review",
        "优化",
        "建议",
        "改进",
        "代码审查"
      ],
      "agent": "reviewer",
      "model": "dashscope/qwen3-max-2026-01-23",
      "priority": "high"
    },
    {
      "type": "devops",
      "keywords": [
        "部署",
        "服务器",
        "重启",
        "日志",
        "运维",
        "配置",
        "docker",
        "k8s"
      ],
      "agent": "devops",
      "model": "dashscope/qwen3.5-plus",
      "priority": "high"
    }
  ],
  "thresholds": {
    "maxLength": 500,
    "minSteps": 3,
    "maxWaitSeconds": 60
  },
  "fallback": {
    "agent": null,
    "model": "dashscope/qwen3.5-plus",
    "description": "默认由主 Session 处理"
  }
}

path: dingtalk-command.py
content: #!/usr/bin/env python3
"""
钉钉群命令处理器 - 解析斜杠命令并路由到子 Agent

用法：
    python3 dingtalk-command.py "@小牛马 /coder 写个脚本"
    python3 dingtalk-command.py --test
"""

import sys
import re
import json
import argparse
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))

from router import spawn_agent, route_message

class Colors:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    NC = '\033[0m'

# 命令映射
COMMAND_MAP = {
    '/coder': 'coder',
    '/code': 'coder',
    '/writer': 'writer',
    '/write': 'writer',
    '/analyze': 'analyst',
    '/analysis': 'analyst',
    '/research': 'researcher',
    '/review': 'reviewer',
    '/review-code': 'reviewer',
    '/devops': 'devops',
    '/ops': 'devops',
    '/auto': 'auto'  # 自动判断
}

# 命令帮助
COMMAND_HELP = {
    'coder': '🧑‍💻 代码专家 - 写代码、调试、重构',
    'writer': '✍️ 写作专家 - 文档、周报、文章',
    'analyst': '📊 数据专家 - 数据分析、图表、统计',
    'researcher': '🔍 调研专家 - 搜索、调研、竞品分析',
    'reviewer': '👀 审查专家 - 代码审查、优化建议',
    'devops': '🤖 运维专家 - 部署、服务器、日志',
    'auto': '🤖 自动判断 - 根据内容智能路由'
}

def get_bot_names() -> list:
    """获取机器人名字列表"""
    config_file = Path(__file__).parent / "config.json"
    if config_file.exists():
        with open(config_file, 'r', encoding='utf-8') as f:
            config = json.load(f)
            return config.get('bot_names', ['小牛马', 'xiaoniuma'])
    return ['小牛马', 'xiaoniuma']

def save_bot_name(new_name: str) -> bool:
    """
    自动保存新发现的机器人名字
    
    Args:
        new_name: 新发现的名字
    
    Returns:
        bool: 是否成功保存
    """
    config_file = Path(__file__).parent / "config.json"
    if not config_file.exists():
        return False
    
    try:
        with open(config_file, 'r', encoding='utf-8') as f:
            config = json.load(f)
        
        bot_names = config.get('bot_names', [])
        
        # 如果名字已存在，不重复添加
        if new_name in bot_names:
            return False
        
        # 添加新名字
        bot_names.append(new_name)
        config['bot_names'] = bot_names
        
        # 保存配置
        with open(config_file, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=2, ensure_ascii=False)
        
        print(f"✅ 自动保存新名字：{new_name}")
        return True
    
    except Exception as e:
        print(f"❌ 保存名字失败：{e}")
        return False

def extract_bot_name(message: str) -> Optional[str]:
    """
    从消息中提取@的机器人名字
    
    Args:
        message: 原始消息
    
    Returns:
        Optional[str]: 提取到的名字，如果没有@则返回 None
    """
    match = re.search(r'@([^\s,，]+)', message)
    if match:
        return match.group(1)
    return None

def parse_command(message: str, auto_save: bool = True, flexible: bool = False) -> dict:
    """
    解析命令
    
    Args:
        message: 原始消息
        auto_save: 是否自动保存新发现的名字
        flexible: 是否灵活匹配（消息任意位置包含命令）
    
    Returns:
        dict: {
            'is_command': bool,
            'command': str,
            'agent': str,
            'task': str,
            'bot_name': str  # 提取到的机器人名字
        }
    """
    # 先提取@的机器人名字
    extracted_name = extract_bot_name(message)
    
    # 如果提取到名字，自动保存
    if extracted_name and auto_save:
        save_bot_name(extracted_name)
    
    # 获取机器人名字列表（包括刚保存的）
    bot_names = get_bot_names()
    
    # 构建正则表达式匹配所有机器人名字
    name_patterns = '|'.join([re.escape(name) for name in bot_names])
    
    # 去掉@部分（支持所有配置的名字）
    clean_msg = re.sub(rf'@({name_patterns})', '', message, flags=re.IGNORECASE).strip()
    
    # 灵活模式：匹配消息中任意位置的命令
    if flexible:
        # 查找第一个斜杠命令
        match = re.search(r'/(coder|code|writer|write|analyze|analysis|research|review|review-code|devops|ops|auto)\s+(.+?)(?=/|$)', 
                         clean_msg, re.IGNORECASE)
    else:
        # 严格模式：必须开头是命令
        match = re.match(r'^/(coder|code|writer|write|analyze|analysis|research|review|review-code|devops|ops|auto)\s+(.+)$', 
                        clean_msg, re.IGNORECASE)
    
    if not match:
        return {
            'is_command': False,
            'bot_name': extracted_name
        }
    
    cmd = '/' + match.group(1).lower()  # 保留斜杠
    task = match.group(2).strip()
    agent = COMMAND_MAP.get(cmd, None)
    
    if not agent:
        return {
            'is_command': False,
            'bot_name': extracted_name
        }
    
    return {
        'is_command': True,
        'command': cmd,
        'agent': agent,
        'task': task,
        'bot_name': extracted_name
    }

def handle_command(message: str, from_user: str = "", verbose: bool = True) -> dict:
    """
    处理命令
    
    Args:
        message: 消息内容
        from_user: 发送者
        verbose: 是否输出详细信息
    
    Returns:
        dict: 处理结果
    """
    parsed = parse_command(message, auto_save=True)
    
    if not parsed.get('is_command', False):
        bot_name = parsed.get('bot_name', '未知')
        return {
            'action': 'not_command',
            'message': f'未识别到命令（@的名字：{bot_name}）',
            'bot_name': bot_name
        }
    
    if verbose:
        print(f"\n{Colors.CYAN}📋 命令解析:{Colors.NC}")
        print(f"  机器人名字：@{parsed.get('bot_name', '未知')}")
        print(f"  命令：/{parsed['command']}")
        print(f"  目标 Agent: {parsed['agent']}")
        print(f"  任务：{parsed['task']}")
        print(f"  发送者：{from_user or '未知'}")
    
    if parsed['agent'] == 'auto':
        # 自动判断
        if verbose:
            print(f"\n{Colors.BLUE}🤖 自动路由模式{Colors.NC}")
        
        result = route_message(parsed['task'], verbose=verbose)
        
        if result['should_route']:
            agent_name = result['agent']
            spawn_result = spawn_agent(agent_name, parsed['task'], result['model'])
            
            # 构建带 Agent 信息的回复
            agent_display = spawn_result.get('agent_info', {}).get('emoji', '🤖') + ' ' + agent_name
            return {
                'action': 'spawned_agent',
                'agent': agent_name,
                'model': result['model'],
                'spawn_result': spawn_result,
                'message': f"🔄 {spawn_result.get('message', f'已启动 {agent_name}')} 处理您的请求...",
                'handled_by': f"Agent: {agent_display}"
            }
        else:
            return {
                'action': 'handle_directly',
                'agent': None,
                'model': result['model'],
                'message': '简单任务，主 Session 处理',
                'handled_by': '主 Session'
            }
    else:
        # 指定 Agent
        if verbose:
            print(f"\n{Colors.GREEN}🚀 启动 Agent: {parsed['agent']}{Colors.NC}")
        
        spawn_result = spawn_agent(parsed['agent'], parsed['task'])
        
        # 构建带 Agent 信息的回复
        agent_display = spawn_result.get('agent_info', {}).get('emoji', '🤖') + ' ' + parsed['agent']
        return {
            'action': 'spawned_agent',
            'agent': parsed['agent'],
            'spawn_result': spawn_result,
            'message': f"✅ {spawn_result.get('message', f'已启动 {parsed['agent']}')} 处理：{parsed['task']}",
            'handled_by': f"Agent: {agent_display}"
        }

def show_help():
    """显示帮助信息"""
    print(f"{Colors.BLUE}{'='*60}{Colors.NC}")
    print(f"{Colors.BLUE}🤖 钉钉群命令帮助{Colors.NC}")
    print(f"{Colors.BLUE}{'='*60}{Colors.NC}\n")
    
    print(f"{Colors.CYAN}用法：{Colors.NC}")
    print(f"  @小牛马 /命令 任务描述\n")
    
    print(f"{Colors.CYAN}可用命令：{Colors.NC}\n")
    
    for cmd, help_text in COMMAND_HELP.items():
        print(f"  /{cmd:<12} {help_text}")
    
    print(f"\n{Colors.CYAN}示例：{Colors.NC}\n")
    print(f"  @小牛马 /coder 写个 Python 脚本打印 hello world")
    print(f"  @小牛马 /writer 写一篇周报总结")
    print(f"  @小牛马 /analyze 分析销售数据趋势")
    print(f"  @小牛马 /auto 帮我处理这个任务...")
    print(f"  @小牛马 今天天气怎么样（简单问题，无需命令）")
    
    print(f"\n{Colors.BLUE}{'='*60}{Colors.NC}")

def main():
    parser = argparse.ArgumentParser(
        description='钉钉群命令处理器',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
示例:
  %(prog)s "@小牛马 /coder 写个脚本"
  %(prog)s --test
  %(prog)s --help
        """
    )
    
    parser.add_argument('message', nargs='?', help='消息内容')
    parser.add_argument('--test', action='store_true', help='运行测试')
    parser.add_argument('--help-commands', action='store_true', help='显示命令帮助')
    parser.add_argument('--user', default='', help='发送者名称')
    parser.add_argument('--json', action='store_true', help='输出 JSON 格式')
    
    args = parser.parse_args()
    
    if args.help_commands:
        show_help()
        return
    
    if args.test:
        # 测试模式
        test_messages = [
            "@小牛马 /coder 写个 hello world",
            "@小牛马 /writer 写周报",
            "@小牛马 /analyze 分析数据",
            "@小牛马 /auto 帮我处理这个",
            "@小牛马 今天天气",  # 无命令
        ]
        
        print(f"{Colors.BLUE}{'='*60}{Colors.NC}")
        print(f"{Colors.BLUE}🧪 钉钉命令测试{Colors.NC}")
        print(f"{Colors.BLUE}{'='*60}{Colors.NC}\n")
        
        for msg in test_messages:
            print(f"\n{Colors.CYAN}消息：{Colors.NC}{msg}")
            print(f"{Colors.BLUE}{'-'*60}{Colors.NC}")
            
            result = handle_command(msg, "测试用户", verbose=True)
            
            print(f"\n{Colors.GREEN}处理结果：{Colors.NC}")
            print(f"  动作：{result['action']}")
            if result.get('agent'):
                print(f"  Agent: {result['agent']}")
            print(f"  回复：{result['message']}")
        
        return
    
    if not args.message:
        parser.print_help()
        return
    
    # 处理实际消息
    result = handle_command(args.message, args.user, verbose=not args.json)
    
    if args.json:
        # JSON 输出（便于程序调用）
        print(json.dumps(result, indent=2, ensure_ascii=False))
    else:
        # 人类可读输出
        print(f"\n{Colors.GREEN}✅ 处理结果：{Colors.NC}")
        print(f"  {result['message']}")

if __name__ == '__main__':
    main()


path: logger.py
content: #!/usr/bin/env python3
"""
Auto Agent Router 日志记录器
"""

from datetime import datetime
from pathlib import Path

LOG_FILE = Path("/tmp/auto-route-handler.log")

class Colors:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    NC = '\033[0m'

def log(message: str, level: str = "INFO"):
    """
    记录日志
    
    Args:
        message: 日志内容
        level: 日志级别 (INFO/DEBUG/ERROR/SUCCESS)
    """
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_entry = f"[{timestamp}] [{level}] {message}\n"
    
    # 写入日志文件
    try:
        with open(LOG_FILE, 'a', encoding='utf-8') as f:
            f.write(log_entry)
    except Exception as e:
        pass  # 忽略写入错误
    
    # 输出到控制台（带颜色）
    color_map = {
        "INFO": Colors.BLUE,
        "DEBUG": Colors.CYAN,
        "ERROR": Colors.RED,
        "SUCCESS": Colors.GREEN,
        "WARNING": Colors.YELLOW
    }
    color = color_map.get(level, Colors.NC)
    
    print(f"{color}[{timestamp}] [{level}] {message}{Colors.NC}")

def log_command(message: str, command: str, agent: str, chat_type: str = ""):
    """记录命令检测"""
    log(f"收到消息：{message[:50]}... (类型：{chat_type or '未知'})")
    log(f"检测到命令：{command} → 路由到：{agent}", "SUCCESS")

def log_agent_start(agent: str, model: str, task: str):
    """记录 Agent 启动"""
    log(f"启动 Agent: {agent} (模型：{model})", "SUCCESS")
    log(f"任务：{task[:100]}...")

def log_agent_complete(agent: str, result: str):
    """记录 Agent 完成"""
    log(f"Agent {agent} 完成：{result[:100]}...", "SUCCESS")

def log_error(message: str):
    """记录错误"""
    log(f"错误：{message}", "ERROR")

def get_recent_logs(count: int = 20):
    """获取最近的日志"""
    if not LOG_FILE.exists():
        return ["日志文件不存在"]
    
    with open(LOG_FILE, 'r', encoding='utf-8') as f:
        lines = f.readlines()
        return lines[-count:]

def clear_logs():
    """清空日志"""
    if LOG_FILE.exists():
        LOG_FILE.unlink()
        log("日志已清空", "SUCCESS")
    else:
        log("日志文件不存在", "WARNING")

if __name__ == '__main__':
    # 测试
    print("测试日志记录器：\n")
    log("系统启动", "INFO")
    log_command("/coder 写代码", "/coder", "coder", "test")
    log_agent_start("coder", "qwen3-coder-next", "写个 Hello World")
    log_agent_complete("coder", "代码已生成")
    log_error("测试错误")
    
    print(f"\n日志文件位置：{LOG_FILE}")
    print(f"\n最近日志：")
    for line in get_recent_logs(10):
        print(line.strip())


path: message-handler.py
content: #!/usr/bin/env python3
"""
钉钉群消息自动处理器

集成到 OpenClaw 消息处理流程，实现收到@消息时自动路由到子 Agent
"""

import sys
import json
import re
import subprocess
from pathlib import Path
from datetime import datetime

sys.path.insert(0, str(Path(__file__).parent))

from dingtalk_command import parse_command, handle_command

class Colors:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    NC = '\033[0m'

LOG_FILE = Path("/tmp/auto-route-handler.log")

def log(message: str):
    """记录日志"""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    log_entry = f"[{timestamp}] {message}\n"
    
    # 写入日志
    with open(LOG_FILE, 'a', encoding='utf-8') as f:
        f.write(log_entry)
    
    # 输出到控制台
    print(log_entry.strip())

def handle_dingtalk_message(message: str, chat_type: str = "group", 
                           from_user: str = "", conversation_id: str = "") -> dict:
    """
    处理钉钉群消息
    
    Args:
        message: 消息内容
        chat_type: 聊天类型 (group/direct)
        from_user: 发送者
        conversation_id: 会话 ID
    
    Returns:
        dict: 处理结果
    """
    log(f"收到钉钉消息：{message[:50]}... (from: {from_user}, conv: {conversation_id})")
    
    # 检查是否包含@和命令
    if '@' not in message or '/' not in message:
        log("不是命令消息，跳过")
        return {'action': 'skip', 'reason': 'not_a_command'}
    
    # 解析命令
    result = handle_command(message, from_user, verbose=False)
    
    log(f"处理结果：{result.get('action', 'unknown')}")
    
    if result.get('action') == 'spawned_agent':
        log(f"✅ 已启动 Agent: {result.get('agent')}")
        return {
            'action': 'agent_spawned',
            'agent': result.get('agent'),
            'message': result.get('message', '')
        }
    elif result.get('action') == 'handle_directly':
        log("ℹ️  主 Session 处理")
        return {
            'action': 'handle_directly',
            'message': '简单任务，主 Session 处理'
        }
    else:
        log("❌ 未识别到命令")
        return {
            'action': 'not_command',
            'message': result.get('message', '未识别到命令')
        }

def main():
    """命令行入口 - 用于测试"""
    if len(sys.argv) < 2:
        print("用法：python3 message-handler.py '<消息内容>' [发送者] [会话 ID]")
        sys.exit(1)
    
    message = sys.argv[1]
    from_user = sys.argv[2] if len(sys.argv) > 2 else "未知用户"
    conversation_id = sys.argv[3] if len(sys.argv) > 3 else ""
    
    result = handle_dingtalk_message(message, "group", from_user, conversation_id)
    
    print(f"\n{Colors.GREEN}处理结果：{Colors.NC}")
    print(f"  动作：{result['action']}")
    if result.get('agent'):
        print(f"  Agent: {result['agent']}")
    print(f"  回复：{result.get('message', '')}")

if __name__ == '__main__':
    main()


path: router.py
content: #!/usr/bin/env python3
"""
Auto Agent Router - 自动子 Agent 分流

根据消息内容自动判断并分发给合适的子 Agent
"""

import json
import re
from pathlib import Path
from typing import Dict, List, Optional, Tuple

# 配置
CONFIG_FILE = Path(__file__).parent / "config.json"
WORKSPACE_DIR = Path.home() / ".openclaw" / "workspace"

class Colors:
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    NC = '\033[0m'

def load_config() -> dict:
    """加载路由配置"""
    if not CONFIG_FILE.exists():
        return {"enabled": False, "rules": []}
    
    with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
        return json.load(f)

def load_agents() -> Dict[str, dict]:
    """加载所有可用 Agent"""
    agents = {}
    agents_dir = WORKSPACE_DIR / "agents"
    
    if not agents_dir.exists():
        return agents
    
    for agent_dir in agents_dir.iterdir():
        if agent_dir.is_dir():
            config_file = agent_dir / "config.json"
            if config_file.exists():
                with open(config_file, 'r', encoding='utf-8') as f:
                    config = json.load(f)
                    config['_path'] = str(agent_dir)
                    agents[agent_dir.name] = config
    
    return agents

def analyze_message(message: str) -> dict:
    """
    分析消息内容，返回分析结果
    
    Returns:
        dict: {
            'length': int,          # 消息长度
            'steps': int,           # 步骤数量
            'is_question': bool,    # 是否是问题
            'has_attachment': bool, # 是否有附件
            'complexity': str       # 复杂度：simple/medium/complex
        }
    """
    # 计算长度
    length = len(message)
    
    # 检测步骤数量（通过序号或连接词）
    steps = len(re.findall(r'(?:(?:首先 | 然后 | 接着 | 最后)|\d+[,.]\s*\n)', message))
    
    # 检测是否是问题
    is_question = bool(re.search(r'[?？]$', message.strip()))
    
    # 检测是否有附件引用
    has_attachment = bool(re.search(r'(文件 | 图片 | 截图 | 附件|upload|file)', message, re.IGNORECASE))
    
    # 综合复杂度判断
    if length > 500 or steps >= 3 or has_attachment:
        complexity = "complex"
    elif length > 200 or steps >= 1:
        complexity = "medium"
    else:
        complexity = "simple"
    
    return {
        'length': length,
        'steps': steps,
        'is_question': is_question,
        'has_attachment': has_attachment,
        'complexity': complexity
    }

def match_keywords(message: str, keywords: List[str]) -> Tuple[bool, List[str]]:
    """
    匹配关键词
    
    Returns:
        Tuple[bool, List[str]]: (是否匹配，匹配到的关键词列表)
    """
    message_lower = message.lower()
    matched = [kw for kw in keywords if kw.lower() in message_lower]
    return len(matched) > 0, matched

def route_message(message: str, verbose: bool = True) -> dict:
    """
    路由决策主函数
    
    Args:
        message: 消息内容
        verbose: 是否输出详细信息
    
    Returns:
        dict: {
            'should_route': bool,       # 是否应该路由到子 Agent
            'agent': Optional[str],     # 目标 Agent 名称
            'model': str,               # 使用的模型
            'reason': str,              # 路由原因
            'matched_keywords': list,   # 匹配到的关键词
            'analysis': dict            # 消息分析结果
        }
    """
    config = load_config()
    
    # 检查是否启用自动路由
    if not config.get('enabled', False):
        return {
            'should_route': False,
            'agent': None,
            'model': config.get('fallback', {}).get('model', 'dashscope/qwen3.5-plus'),
            'reason': '自动路由未启用',
            'matched_keywords': [],
            'analysis': analyze_message(message)
        }
    
    # 分析消息
    analysis = analyze_message(message)
    
    if verbose:
        print(f"\n{Colors.CYAN}📊 消息分析:{Colors.NC}")
        print(f"  长度：{analysis['length']} 字")
        print(f"  步骤数：{analysis['steps']}")
        print(f"  复杂度：{analysis['complexity']}")
        print(f"  是问题：{analysis['is_question']}")
    
    # 规则匹配
    rules = config.get('rules', [])
    for rule in rules:
        matched, keywords = match_keywords(message, rule.get('keywords', []))
        if matched:
            agent_name = rule.get('agent')
            model = rule.get('model', 'dashscope/qwen3.5-plus')
            
            # 检查 Agent 是否存在
            agents = load_agents()
            agent_exists = agent_name in agents
            
            if verbose:
                print(f"\n{Colors.GREEN}✅ 匹配到规则:{Colors.NC}")
                print(f"  类型：{rule.get('type', 'unknown')}")
                print(f"  关键词：{', '.join(keywords)}")
                print(f"  目标 Agent: {agent_name} {'✅' if agent_exists else '⚠️ 未找到'}")
                print(f"  模型：{model}")
            
            return {
                'should_route': agent_exists,  # 只有 Agent 存在时才路由
                'agent': agent_name if agent_exists else None,
                'model': model,
                'reason': f"匹配到 {rule.get('type', 'unknown')} 类型任务",
                'matched_keywords': keywords,
                'analysis': analysis
            }
    
    # 复杂度判断 - 长消息可能需要研究专家
    thresholds = config.get('thresholds', {})
    if analysis['length'] > thresholds.get('maxLength', 500):
        if verbose:
            print(f"\n{Colors.YELLOW}⚠️  消息较长，建议由 researcher 处理{Colors.NC}")
        return {
            'should_route': True,
            'agent': 'researcher',
            'model': 'google/gemini-3.1-pro-preview',
            'reason': '消息过长，需要深度分析',
            'matched_keywords': [],
            'analysis': analysis
        }
    
    # 默认由主 Session 处理
    if verbose:
        print(f"\n{Colors.BLUE}ℹ️  简单消息，主 Session 处理{Colors.NC}")
    
    return {
        'should_route': False,
        'agent': None,
        'model': config.get('fallback', {}).get('model', 'dashscope/qwen3.5-plus'),
        'reason': '简单消息，主 Session 处理',
        'matched_keywords': [],
        'analysis': analysis
    }

def spawn_agent(agent_name: str, task: str, model: str = None) -> dict:
    """
    启动子 Agent 执行任务
    
    Args:
        agent_name: Agent 名称
        task: 任务描述
        model: 模型名称（可选）
    
    Returns:
        dict: {
            'success': bool,
            'agent': str,
            'agent_info': dict,
            'session_key': str,
            'message': str
        }
    """
    agents = load_agents()
    if agent_name not in agents:
        return {
            'success': False,
            'agent': agent_name,
            'message': f"❌ Agent 不存在：{agent_name}"
        }
    
    agent_config = agents[agent_name]
    model = model or agent_config.get('model', {}).get('primary', 'dashscope/qwen3.5-plus')
    emoji = agent_config.get('emoji', '🤖')
    role = agent_config.get('role', '专家')
    
    # 读取 SOUL.md
    soul_file = Path(agent_config['_path']) / "SOUL.md"
    soul_content = ""
    if soul_file.exists():
        with open(soul_file, 'r', encoding='utf-8') as f:
            soul_content = f.read()
    
    # 构建系统提示
    system_prompt = f"""你现在的身份是 {agent_name} ({role})。

{soul_content}

请完全代入这个角色，按照 SOUL.md 中定义的性格、价值观和沟通风格与用户交流。
"""
    
    # 返回启动信息（实际启动由主 Session 调用 sessions_spawn 工具）
    return {
        'success': True,
        'agent': agent_name,
        'agent_info': {
            'name': agent_name,
            'role': role,
            'emoji': emoji,
            'model': model
        },
        'system_prompt': system_prompt,
        'task': task,
        'model': model,
        'message': f"{emoji} 已启动 **{agent_name}** ({role}) 处理您的任务"
    }
    
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
        if result.returncode == 0:
            return f"✅ Agent 已启动：{agent_name}"
        else:
            return f"❌ 启动失败：{result.stderr}"
    except subprocess.TimeoutExpired:
        return f"⚠️  超时，但 Agent 可能已在后台运行"
    except Exception as e:
        return f"❌ 错误：{e}"

def main():
    """命令行入口"""
    import sys
    
    if len(sys.argv) < 2:
        print("用法：python router.py <消息内容>")
        print("   或：python router.py --test")
        sys.exit(1)
    
    if sys.argv[1] == '--test':
        # 测试模式
        test_messages = [
            "写个 Python 脚本处理 Excel 文件",
            "分析一下销售数据趋势",
            "今天天气怎么样",
            "帮我写一篇周报总结",
            "调研一下竞品情况",
            "审查这段代码有什么问题"
        ]
        
        print(f"{Colors.BLUE}{'='*60}{Colors.NC}")
        print(f"{Colors.BLUE}🧪 路由测试{Colors.NC}")
        print(f"{Colors.BLUE}{'='*60}{Colors.NC}\n")
        
        for msg in test_messages:
            print(f"\n{Colors.CYAN}消息：{Colors.NC}{msg}")
            print(f"{Colors.BLUE}{'-'*60}{Colors.NC}")
            result = route_message(msg, verbose=True)
            print(f"\n{Colors.GREEN}决策：{Colors.NC}")
            print(f"  路由：{'是' if result['should_route'] else '否'}")
            print(f"  Agent: {result['agent'] or '主 Session'}")
            print(f"  模型：{result['model']}")
            print(f"  原因：{result['reason']}")
    else:
        # 实际路由
        message = ' '.join(sys.argv[1:])
        result = route_message(message, verbose=True)
        
        print(f"\n{Colors.GREEN}最终决策：{Colors.NC}")
        print(f"  路由：{'是' if result['should_route'] else '否'}")
        if result['should_route']:
            print(f"  目标 Agent: {result['agent']}")
            print(f"  模型：{result['model']}")
        print(f"  原因：{result['reason']}")

if __name__ == '__main__':
    main()


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