返回 Skill 列表
extension
分类: 其它无需 API Key

qclaw-agent-audit

审核和检查 QClaw 的Agent配置、技能、权限和环境。当用户想要检查本地 QClaw 设置、列出代理、分析技能分配、验证权限,或生成 QClaw 环境的综合报告以进行规划或故障排除时,可以使用此功能。

person作者: user_5c543922hubcommunity

QClaw Agent Audit

Audit local QClaw configuration and generate a comprehensive report of agents, skills, permissions, and environment.

When to Use

  • Check how many agents are configured
  • List all available skills (built-in + user)
  • Analyze agent permissions and visibility
  • Verify model configuration
  • Generate reports for role/task planning
  • Share configuration with others for debugging

Quick Start

Run the audit by executing the Python code below with the exec tool:

#!/usr/bin/env python3
"""
QClaw Agent Audit - Scan and report QClaw configuration
"""

import json
import os
import platform
import sys
import argparse
from pathlib import Path
from datetime import datetime


def detect_os():
    """Detect operating system"""
    system = platform.system()
    if system == "Windows":
        return "Windows", os.environ.get("USERPROFILE", "")
    elif system == "Darwin":
        return "macOS", os.path.expanduser("~")
    else:
        return "Linux", os.path.expanduser("~")


def find_config_path(home_dir):
    """Find QClaw config file"""
    paths = [
        Path(home_dir) / ".qclaw" / "openclaw.json",
        Path(home_dir) / ".openclaw" / "openclaw.json",
        Path(home_dir) / ".config" / "qclaw" / "openclaw.json",
    ]
    
    # Windows: also check Program Files
    if platform.system() == "Windows":
        paths.append(Path("C:/Program Files/QClaw/resources/openclaw/config/openclaw.json"))
    
    for path in paths:
        if path.exists():
            return str(path)
    
    return None


def find_skill_dirs(home_dir):
    """Find skill directories"""
    dirs = []
    
    # Built-in skills
    builtin_paths = [
        Path("C:/Program Files/QClaw/resources/openclaw/config/skills"),
        Path("/usr/share/openclaw/skills"),
        Path("/opt/openclaw/skills"),
    ]
    
    for path in builtin_paths:
        if path.exists():
            dirs.append(("built-in", str(path)))
            break
    
    # User skills
    user_paths = [
        Path(home_dir) / ".qclaw" / "skills",
        Path(home_dir) / ".openclaw" / "workspace" / "skills",
    ]
    
    for path in user_paths:
        if path.exists():
            dirs.append(("user", str(path)))
    
    return dirs


def parse_skill_md(skill_path):
    """Parse SKILL.md frontmatter"""
    skill_file = Path(skill_path) / "SKILL.md"
    if not skill_file.exists():
        return None, None
    
    try:
        content = skill_file.read_text(encoding="utf-8")
        if content.startswith("---"):
            parts = content.split("---", 2)
            if len(parts) >= 3:
                frontmatter = parts[1].strip()
                name = None
                description = None
                for line in frontmatter.split("\n"):
                    if line.startswith("name:"):
                        name = line.split(":", 1)[1].strip()
                    elif line.startswith("description:"):
                        description = line.split(":", 1)[1].strip()
                return name, description
    except Exception:
        pass
    
    return None, None


def scan_skills(skill_dirs):
    """Scan all skills"""
    skills = {"built-in": [], "user": []}
    
    for skill_type, dir_path in skill_dirs:
        path = Path(dir_path)
        if not path.exists():
            continue
        
        for item in path.iterdir():
            if item.is_dir() and not item.name.startswith("."):
                name, description = parse_skill_md(item)
                skills[skill_type].append({
                    "id": item.name,
                    "name": name or item.name,
                    "description": description or "",
                    "path": str(item),
                })
    
    return skills


def read_agent_personality(workspace_path):
    """Read agent personality from SOUL.md and IDENTITY.md"""
    personality = {
        "name": None,
        "emoji": None,
        "vibe": None,
        "background": None,
        "style": None,
    }
    
    # Read IDENTITY.md
    identity_file = Path(workspace_path) / "IDENTITY.md"
    if identity_file.exists():
        try:
            content = identity_file.read_text(encoding="utf-8")
            for line in content.split("\n"):
                line = line.strip()
                if line.startswith("- Name:"):
                    personality["name"] = line.split(":", 1)[1].strip()
                elif line.startswith("- Emoji:"):
                    personality["emoji"] = line.split(":", 1)[1].strip()
                elif line.startswith("- Vibe:"):
                    personality["vibe"] = line.split(":", 1)[1].strip()
        except Exception:
            pass
    
    # Read SOUL.md
    soul_file = Path(workspace_path) / "SOUL.md"
    if soul_file.exists():
        try:
            content = soul_file.read_text(encoding="utf-8")
            lines = content.split("\n")
            current_section = None
            section_content = []
            
            for line in lines:
                stripped = line.strip()
                if stripped.startswith("## "):
                    if current_section and section_content:
                        text = " ".join(section_content).strip()
                        if current_section == "经历":
                            personality["background"] = text
                        elif current_section == "风格":
                            personality["style"] = text
                    current_section = stripped[3:].strip()
                    section_content = []
                elif current_section and stripped:
                    section_content.append(stripped)
            
            # Don't forget the last section
            if current_section and section_content:
                text = " ".join(section_content).strip()
                if current_section == "经历":
                    personality["background"] = text
                elif current_section == "风格":
                    personality["style"] = text
        except Exception:
            pass
    
    return personality


def analyze_config(config_path):
    """Analyze QClaw configuration"""
    try:
        with open(config_path, "r", encoding="utf-8") as f:
            config = json.load(f)
    except Exception as e:
        return {"error": str(e)}
    
    result = {
        "meta": config.get("meta", {}),
        "agents": [],
        "permissions": {},
        "models": {},
        "skills_config": {},
    }
    
    # Agents
    agents_config = config.get("agents", {})
    defaults = agents_config.get("defaults", {})
    agent_list = agents_config.get("list", [])
    
    for agent in agent_list:
        workspace = agent.get("workspace", defaults.get("workspace", ""))
        personality = read_agent_personality(workspace) if workspace else {}
        
        # Use IDENTITY.md name if available, fallback to config name
        config_name = agent.get("name", "Unnamed")
        identity_name = personality.get("name")
        display_name = identity_name if identity_name else config_name
        
        # Track if names differ (for reporting)
        if identity_name and identity_name != config_name:
            personality["config_name"] = config_name
        
        agent_info = {
            "id": agent.get("id", "unknown"),
            "name": display_name,
            "type": "main" if agent.get("id") == "main" else "subagent",
            "workspace": workspace,
            "skills": agent.get("skills", []),
            "model": agent.get("model", defaults.get("model", {})),
            "personality": personality,
        }
        result["agents"].append(agent_info)
    
    # Permissions
    tools = config.get("tools", {})
    result["permissions"] = {
        "session_visibility": tools.get("sessions", {}).get("visibility", "unknown"),
        "agent_to_agent": tools.get("agentToAgent", {}).get("enabled", False),
        "subagent_allowlist": agents_config.get("defaults", {}).get("subagents", {}).get("allowAgents", []),
    }
    
    # Models
    models = config.get("models", {})
    providers = models.get("providers", {})
    result["models"] = {
        "mode": models.get("mode", "unknown"),
        "primary": defaults.get("model", {}).get("primary", "unknown"),
        "fallbacks": defaults.get("model", {}).get("fallbacks", []),
        "providers": list(providers.keys()),
    }
    
    # Skills config
    skills_entries = config.get("skills", {}).get("entries", {})
    result["skills_config"] = {
        k: v.get("enabled", False) if isinstance(v, dict) else bool(v)
        for k, v in skills_entries.items()
    }
    
    return result


def generate_markdown_report(data, skills, output_path):
    """Generate Markdown report"""
    lines = []
    lines.append("# QClaw Agent Audit Report")
    lines.append(f"\n**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    lines.append(f"**OS:** {data.get('os', 'Unknown')}")
    lines.append(f"**QClaw Version:** {data.get('meta', {}).get('lastTouchedVersion', 'Unknown')}")
    lines.append("")
    
    # Agents
    lines.append("## Agents")
    lines.append("")
    lines.append("| ID | Name | Type | Skills |")
    lines.append("|----|------|------|--------|")
    for agent in data.get("agents", []):
        skill_count = len(agent.get("skills", []))
        skills_str = f"{skill_count} explicit" if skill_count > 0 else "inherits defaults"
        lines.append(f"| {agent['id']} | {agent['name']} | {agent['type']} | {skills_str} |")
    lines.append("")
    
    # Agent details
    for agent in data.get("agents", []):
        personality = agent.get("personality", {})
        lines.append(f"### {agent['name']} ({agent['id']})")
        lines.append(f"- **Type:** {agent['type']}")
        lines.append(f"- **Workspace:** {agent.get('workspace', 'N/A')}")
        
        # Show config name if different from identity name
        if personality.get("config_name"):
            lines.append(f"- **Config Name:** {personality['config_name']} *(IDENTITY.md overrides this)*")
        
        # Personality
        if personality.get("emoji"):
            lines.append(f"- **Emoji:** {personality['emoji']}")
        if personality.get("vibe"):
            lines.append(f"- **Vibe:** {personality['vibe']}")
        if personality.get("background"):
            lines.append(f"- **Background:** {personality['background']}")
        if personality.get("style"):
            lines.append(f"- **Style:** {personality['style']}")
        
        if agent.get("skills"):
            lines.append(f"- **Explicit Skills:** {', '.join(agent['skills'])}")
        lines.append("")
    
    # Permissions
    perms = data.get("permissions", {})
    lines.append("## Permissions")
    lines.append(f"- **Session Visibility:** {perms.get('session_visibility', 'unknown')}")
    lines.append(f"- **Agent-to-Agent Communication:** {'Enabled' if perms.get('agent_to_agent') else 'Disabled'}")
    lines.append(f"- **Subagent Allowlist:** {', '.join(perms.get('subagent_allowlist', []))}")
    lines.append("")
    
    # Models
    models = data.get("models", {})
    lines.append("## Models")
    lines.append(f"- **Primary:** {models.get('primary', 'unknown')}")
    lines.append(f"- **Fallbacks:** {', '.join(models.get('fallbacks', []))}")
    lines.append(f"- **Providers:** {', '.join(models.get('providers', []))}")
    lines.append("")
    
    # Skills
    lines.append("## Skills")
    lines.append("")
    
    # Built-in
    lines.append(f"### Built-in Skills ({len(skills.get('built-in', []))})")
    lines.append("")
    for skill in sorted(skills.get("built-in", []), key=lambda x: x["id"]):
        desc = skill.get("description", "")[:80] + "..." if len(skill.get("description", "")) > 80 else skill.get("description", "")
        lines.append(f"- **{skill['id']}**: {skill['name']}" + (f" - {desc}" if desc else ""))
    lines.append("")
    
    # User
    lines.append(f"### User Skills ({len(skills.get('user', []))})")
    lines.append("")
    lines.append("| Skill | Name | Purpose |")
    lines.append("|-------|------|---------|")
    for skill in sorted(skills.get("user", []), key=lambda x: x["id"]):
        desc = skill.get("description", "")
        # Clean up description for table
        desc = desc.replace("|", "/").replace("\n", " ")
        if len(desc) > 100:
            desc = desc[:97] + "..."
        lines.append(f"| {skill['id']} | {skill['name']} | {desc} |")
    lines.append("")
    
    # Warnings
    lines.append("## Notes")
    lines.append("- Subagents cannot modify system configuration (no gateway access)")
    lines.append("- All agents share built-in skills automatically")
    lines.append("- User skills must be explicitly assigned per agent")
    lines.append("")
    
    with open(output_path, "w", encoding="utf-8") as f:
        f.write("\n".join(lines))
    
    return output_path


def generate_json_report(data, skills, output_path):
    """Generate JSON report"""
    report = {
        "generated_at": datetime.now().isoformat(),
        "environment": {
            "os": data.get("os"),
            "python_version": platform.python_version(),
            "qclaw_version": data.get("meta", {}).get("lastTouchedVersion"),
        },
        "agents": data.get("agents", []),
        "permissions": data.get("permissions", {}),
        "models": data.get("models", {}),
        "skills": skills,
    }
    
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(report, f, indent=2, ensure_ascii=False)
    
    return output_path


def main():
    parser = argparse.ArgumentParser(description="QClaw Agent Audit")
    parser.add_argument("--format", choices=["md", "json"], default="md", help="Output format")
    parser.add_argument("--config", help="Path to openclaw.json")
    parser.add_argument("--output", help="Output file path")
    args = parser.parse_args()
    
    # Detect environment
    os_name, home_dir = detect_os()
    print(f"OS: {os_name}")
    print(f"Home: {home_dir}")
    
    # Find config
    config_path = args.config or find_config_path(home_dir)
    if not config_path:
        print("Error: Could not find openclaw.json")
        sys.exit(1)
    
    print(f"Config: {config_path}")
    
    # Analyze config
    data = analyze_config(config_path)
    data["os"] = os_name
    
    if "error" in data:
        print(f"Error reading config: {data['error']}")
        sys.exit(1)
    
    # Scan skills
    skill_dirs = find_skill_dirs(home_dir)
    skills = scan_skills(skill_dirs)
    
    print(f"Built-in skills: {len(skills['built-in'])}")
    print(f"User skills: {len(skills['user'])}")
    
    # Generate report
    if args.format == "json":
        output = args.output or "qclaw-agent-audit-report.json"
        generate_json_report(data, skills, output)
    else:
        output = args.output or "qclaw-agent-audit-report.md"
        generate_markdown_report(data, skills, output)
    
    print(f"Report saved: {output}")


if __name__ == "__main__":
    main()

执行方式:将上述代码保存为临时文件后用 Python 运行,或直接用 exec 工具执行 Python 代码。

输出:Markdown 报告保存到 qclaw-agent-audit-report.md

What It Checks

Environment

  • Operating system
  • QClaw installation path
  • Python version
  • Config file location
  • Disk space

Agents

  • Agent ID and name
  • Type (main/subagent)
  • Workspace path
  • Explicit skill assignments

Skills

  • Built-in skills (from bundled directory)
  • User skills (from user directories)
  • Skill descriptions (from SKILL.md frontmatter)
  • Per-agent skill assignments

Permissions

  • Session visibility (all/self/none)
  • Agent-to-agent communication
  • Subagent delegation allowlist
  • Gateway access (config modification rights)

Models

  • Primary model
  • Fallback models
  • Available providers
  • Model capabilities (text/image)

Cross-Platform Support

| OS | Config Path Detection | |----|----------------------| | Windows | %USERPROFILE%\.qclaw\openclaw.json | | macOS | ~/.qclaw/openclaw.json | | Linux | ~/.qclaw/openclaw.json or $XDG_CONFIG_HOME/qclaw/openclaw.json |

Output Formats

Default: Markdown (--format md) Optional: JSON (--format json)

Notes

  • Read-only: Does not modify any configuration
  • Safe to run: Only reads config files and scans directories
  • Requires Python 3.7+