Skill: freeter226/skill-radar (version 1.1.1)

--- name: skill-radar description: Scan, analyze, and optimize your AI skill ecosystem. Diagnose skill usage, discover capability gaps, and check version updates in one command. Trigger on "skill check", "skill radar", "skill management", "skill diagnostics", "skill optimization", "check skill", "skill usage". metadata: openclaw: emoji: "📡" requires: bins: ["python3", "openclaw"] --- # Skill Radar 📡 > Your AI Skill Manager — Make every installed Skill count ## Core Philosophy **Make Skill management simpler and more efficient.** - **Simpler** — See the full picture with one command, no more getting lost in 100+ Skills - **More Efficient** — Precisely identify idle skills and gaps, maximize every investment - **Smarter** — Data-driven decisions, not guesswork ## Trigger Conditions - "check skill", "skill radar", "skill diagnostics", "skill management", "skill optimization" - "skill usage", "skill insight" - "which skills am I not using", "should I install this skill", "do my skills need updating" ## Features | Command | Description | |---------|-------------| | `usage` | 📊 Usage Value Assessment (based on 5 data sources) | | `status` | 📋 Missing/Ready Status Check | | `recommend` | 💡 Smart Recommendations (discover capability gaps from conversation history) | | `versions` | 🔄 Version Check | | `all` | Full Report (all commands above) | ## Usage ```bash # Full report (recommended for first use) python3 <skill-path>/scripts/health_check.py all # Individual checks python3 <skill-path>/scripts/health_check.py usage # Usage value assessment python3 <skill-path>/scripts/health_check.py status # Status check python3 <skill-path>/scripts/health_check.py recommend # Smart recommendations python3 <skill-path>/scripts/health_check.py versions # Version check # Output as Markdown python3 <skill-path>/scripts/health_check.py all > report.md ``` ## Usage Value Assessment Integrates 5 data sources to infer each Skill's usage: | Data Source | Content | Weight | |-------------|---------|--------| | Daily Memory | Events, decisions, progress | ⭐⭐⭐ | | MEMORY.md | Core config, preferences | ⭐⭐⭐ | | HEARTBEAT.md | Scheduled task config | ⭐⭐ | | Session Logs | Raw conversation records | ⭐⭐ | | AGENTS.md | Work rules | ⭐⭐ | Scoring: 🔵 High (core tool) / 🟢 Medium / 🟡 Low / 🔴 Unused ## Dependencies - Python 3.8+ (pure standard library, no external dependencies) - OpenClaw CLI (`openclaw`) - ClawHub CLI (`npx clawhub`, only for version check and recommendations)

path: README.md
content: # Skill Radar 📡

> Your AI Skill Manager — Make every installed Skill count

## Core Philosophy

**Make Skill management simpler and more efficient.**

Too many Skills installed and don't know which ones are actually being used? Not sure what's missing? Wondering if versions are up to date?

Skill Radar scans everything and gives you the full picture in one command.

### Three Pillars

- **Simpler** — Lower cognitive load, see the full picture with one command
- **More Efficient** — Reduce wasted effort, precisely identify idle skills and gaps
- **Smarter** — Move from reactive management to proactive optimization with real data

## Features

| Command | Description |
|---------|-------------|
| `usage` | 📊 Usage Value Assessment — Which skills are active, idle, and how often |
| `status` | 📋 Status Check — Ready/Missing stats with cleanup suggestions |
| `recommend` | 💡 Smart Recommendations — Discover capability gaps from conversation history |
| `versions` | 🔄 Version Check — Compare against latest ClawHub versions |
| `all` | Full Report (all commands above) |

## Installation

```bash
# Install via ClawHub (recommended)
npx clawhub install skill-radar

# Or install manually
cp -r skill-radar ~/.openclaw/workspace/skills/
```

## Usage

```bash
SKILL_PATH=~/.openclaw/workspace/skills/skill-radar

# Full report (recommended for first use)
python3 $SKILL_PATH/scripts/health_check.py all

# Individual checks
python3 $SKILL_PATH/scripts/health_check.py usage       # Usage value assessment
python3 $SKILL_PATH/scripts/health_check.py status      # Status check
python3 $SKILL_PATH/scripts/health_check.py recommend   # Smart recommendations
python3 $SKILL_PATH/scripts/health_check.py versions    # Version check

# Output to file
python3 $SKILL_PATH/scripts/health_check.py all > skill-report.md
```

## Usage Value Assessment

Integrates 5 data sources to accurately determine whether each Skill is being used:

| Data Source | Content | Description |
|-------------|---------|-------------|
| 📝 Daily Memory | Events, decisions, progress | Clear timeline, shows trends |
| 📋 MEMORY.md | Core config, preferences | Long-term stable needs |
| 💓 HEARTBEAT.md | Scheduled task config | Indicates continuously used capabilities |
| 💬 Session Logs | Raw conversation records | Analyzed after filtering system messages |
| 🤖 AGENTS.md | Work rules | Reflects long-term usage patterns |

**Scoring System**:

| Score | Meaning | Criteria |
|-------|---------|----------|
| 🔵 High | Core tool | Frequent mentions across multiple data sources |
| 🟢 Medium | Regularly used | Appears in 2+ data sources |
| 🟡 Low | Occasionally used | Only keyword matches |
| 🔴 Unused | Never mentioned | No hits in any data source |

## Smart Recommendations

Based on conversation history and workspace config, proactively discovers capability gaps:

- Analyzes what the user frequently does
- Compares against installed Skills
- Recommends missing capabilities
- Distinguishes between high-frequency and low-frequency needs

## Dependencies

- Python 3.8+ (pure standard library, no external dependencies)
- OpenClaw CLI (`openclaw`, required for status check)
- ClawHub CLI (`npx clawhub`, required for version check and recommendations)

## Platform Support

- ✅ macOS (Homebrew / npm global)
- ✅ Linux (Linuxbrew / npm global)
- ⚠️ Windows (not tested, no guarantees)

## Security

Recommended Skills go through automatic security scanning (based on skill-vetter protocol):

- **Installed Skills**: Code-level scan (25+ red flag rules)
- **Uninstalled Skills**: Metadata-level check (author, update time, etc.)
- 🔴 High-risk Skills automatically removed from recommendation list
- Scan results cached for 7 days, recommendation results cached for 24 hours

## Zero-Config Principle

All external components are optional, with graceful degradation:

- No Session Logs? No problem, evaluate with workspace files
- Missing data sources? Use what's available, no errors, no ❌

## License

MIT


path: requirements.txt
content: # No external Python dependencies required
# This skill uses only Python standard library


path: scripts/__init__.py
content: # Skill Radar scripts package


path: scripts/checks.py
content: """
checks.py — 状态检查 + 版本检查

包含 Skill Missing/Ready 状态检查和 ClawHub 版本更新检查。
"""

import re
from pathlib import Path
from collections import defaultdict
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

from utils import run_cmd


# ─── 状态检查 ───────────────────────────────────────────────────────────────

def parse_skills_list() -> list[dict]:
    """解析 openclaw skills list 的输出"""
    output = run_cmd("openclaw skills list 2>/dev/null")
    if not output:
        return []

    skills = []
    sep = "\u2502"

    current_skill = None

    for line in output.split("\n"):
        stripped = line.strip()
        parts = [p.strip() for p in stripped.split(sep)]

        if len(parts) < 5:
            continue

        status_raw = parts[1].strip()
        if "ready" not in status_raw and "missing" not in status_raw:
            if current_skill and len(parts) >= 4:
                extra_desc = parts[2].strip()
                if extra_desc:
                    current_skill["description"] += " " + extra_desc
            continue

        if current_skill:
            skills.append(current_skill)

        status = "ready" if "ready" in status_raw else "missing"

        name_raw = parts[2].strip()
        name = re.sub(r'^[^\x00-\x7F]+', '', name_raw).strip()
        if not name:
            name = name_raw

        desc_part = parts[3].strip() if len(parts) > 3 else ""
        source = parts[4].strip() if len(parts) > 4 else ""

        current_skill = {
            "status": status,
            "name": name,
            "description": desc_part,
            "source": source,
        }

    if current_skill:
        skills.append(current_skill)

    return skills


def check_status() -> str:
    """检查 Skill Missing/Ready 状态"""
    lines = []
    lines.append(f"# 📊 Skill 状态检查")
    lines.append(f"\n> 生成时间：{datetime.now().strftime('%Y-%m-%d %H:%M')}")
    lines.append("")

    skills = parse_skills_list()
    if not skills:
        lines.append("⚠️ 无法获取 Skill 列表。请确保 `openclaw` CLI 可用。")
        return "\n".join(lines)

    ready = [s for s in skills if s["status"] == "ready"]
    missing = [s for s in skills if s["status"] == "missing"]

    ready_by_source = defaultdict(list)
    missing_by_source = defaultdict(list)
    for s in ready:
        ready_by_source[s["source"]].append(s)
    for s in missing:
        missing_by_source[s["source"]].append(s)

    lines.append("## 概览")
    lines.append("")
    lines.append(f"| 状态 | 数量 |")
    lines.append(f"|------|------|")
    lines.append(f"| ✅ Ready | **{len(ready)}** |")
    lines.append(f"| ❌ Missing | **{len(missing)}** |")
    lines.append(f"| 📦 总计 | **{len(skills)}** |")
    lines.append("")

    lines.append("## 按来源分布")
    lines.append("")
    lines.append("| 来源 | Ready | Missing |")
    lines.append("|------|-------|---------|")
    all_sources = sorted(set(s["source"] for s in skills))
    for src in all_sources:
        r = len(ready_by_source.get(src, []))
        m = len(missing_by_source.get(src, []))
        lines.append(f"| {src} | {r} | {m} |")
    lines.append("")

    if missing:
        lines.append(f"## ❌ Missing Skill 详情 ({len(missing)} 个)")
        lines.append("")

        bundled_missing = [s for s in missing if s["source"] == "openclaw-bundled"]
        other_missing = [s for s in missing if s["source"] != "openclaw-bundled"]

        if bundled_missing:
            lines.append(f"### Bundled Missing ({len(bundled_missing)} 个)")
            lines.append("")
            lines.append("> 💡 Bundled Skill 是 OpenClaw 自带的，missing 表示依赖未安装。")
            lines.append("> 如果确定不需要，可以通过更新 OpenClaw 来清理。")
            lines.append("")
            for s in bundled_missing:
                lines.append(f"- 🔐 `{s['name']}`")
                if s["description"]:
                    desc = s["description"][:80]
                    lines.append(f"  - {desc}")
            lines.append("")

        if other_missing:
            lines.append(f"### Other Missing ({len(other_missing)} 个)")
            lines.append("")
            for s in other_missing:
                lines.append(f"- ❌ `{s['name']}` [{s['source']}]")
                if s["description"]:
                    desc = s["description"][:80]
                    lines.append(f"  - {desc}")
            lines.append("")

        lines.append("### 💡 清理建议")
        lines.append("")
        lines.append(f"- 共 {len(bundled_missing)} 个 bundled Skill missing，建议等 OpenClaw 更新时自动清理")
        lines.append(f"- 共 {len(other_missing)} 个非 bundled Skill missing，检查是否需要安装依赖或卸载")
        lines.append("")
    else:
        lines.append("✅ 所有 Skill 均为 Ready 状态！")
        lines.append("")

    return "\n".join(lines)


# ─── 版本检查 ───────────────────────────────────────────────────────────────

def parse_clawhub_list() -> dict[str, str]:
    """解析 `npx clawhub list` 的输出，返回 {skill_name: version}"""
    output = run_cmd("npx clawhub list 2>/dev/null", timeout=60)
    if not output:
        return {}

    versions = {}
    for line in output.split("\n"):
        line = line.strip()
        m = re.match(r'^(\S+)\s+(\d+\.\d+\.\d+)', line)
        if m:
            versions[m.group(1)] = m.group(2)

    return versions


def inspect_clawhub_version(skill_name: str) -> str | None:
    """通过 clawhub inspect 获取 Skill 的最新版本"""
    output = run_cmd(
        f"npx clawhub inspect {skill_name} 2>/dev/null", timeout=15
    )
    if not output:
        return None

    m = re.search(r'(?:Latest|latest)\s*[=:]\s*(\d+\.\d+\.\d+)', output)
    if m:
        return m.group(1)
    return None


def check_versions() -> str:
    """检查 ClawHub Skill 版本更新"""
    lines = []
    lines.append(f"# 🔄 版本更新检查")
    lines.append(f"\n> 生成时间：{datetime.now().strftime('%Y-%m-%d %H:%M')}")
    lines.append("")

    local_versions = parse_clawhub_list()
    if not local_versions:
        lines.append("⚠️ 无法获取本地 Skill 版本列表。请确保 `npx clawhub` 可用。")
        return "\n".join(lines)

    lines.append(f"共发现 **{len(local_versions)}** 个通过 ClawHub 安装的 Skill。")
    lines.append("")

    needs_update = []
    already_latest = []
    unknown = []

    skill_names = list(local_versions.keys())

    lines.append("> ⏳ 正在从 ClawHub 查询最新版本（并行）...")
    lines.append("")

    def check_one(name):
        local_ver = local_versions[name]
        remote_ver = inspect_clawhub_version(name)
        return name, local_ver, remote_ver

    with ThreadPoolExecutor(max_workers=5) as executor:
        futures = {executor.submit(check_one, name): name for name in skill_names}
        for future in as_completed(futures, timeout=300):
            try:
                name, local_ver, remote_ver = future.result()
                if remote_ver is None:
                    unknown.append((name, local_ver))
                elif remote_ver != local_ver:
                    needs_update.append((name, local_ver, remote_ver))
                else:
                    already_latest.append((name, local_ver))
            except Exception:
                name = futures[future]
                unknown.append((name, local_versions[name]))

    if needs_update:
        lines.append(f"## 🆕 需要更新 ({len(needs_update)} 个)")
        lines.append("")
        lines.append("| Skill | 本地版本 | 最新版本 |")
        lines.append("|-------|---------|---------|")
        for name, local, remote in sorted(needs_update):
            lines.append(f"| `{name}` | {local} | **{remote}** |")
        lines.append("")
        lines.append(
            "> 运行 `npx clawhub sync <skill-name>` 或 `npx clawhub sync` 全部更新"
        )
        lines.append("")

    if already_latest:
        lines.append(f"## ✅ 已是最新 ({len(already_latest)} 个)")
        lines.append("")
        for name, ver in sorted(already_latest):
            lines.append(f"- `{name}` v{ver}")
        lines.append("")

    if unknown:
        lines.append(f"## ❓ 无法查询 ({len(unknown)} 个)")
        lines.append("")
        for name, ver in sorted(unknown):
            lines.append(f"- `{name}` v{ver}（ClawHub 上未找到匹配项）")
        lines.append("")

    if not needs_update and not unknown:
        lines.append("🎉 所有 Skill 均为最新版本！")
        lines.append("")

    return "\n".join(lines)


path: scripts/health_check.py
content: #!/usr/bin/env python3
"""
Skill Radar — 扫描、洞察、优化你的 AI 技能生态

子命令:
  usage      使用价值评估（分析各 Skill 实际使用情况）
  status     Missing/Ready 状态检查
  versions   版本更新检查
  recommend  基于历史对话的智能推荐
  all        完整报告（以上四项）
"""

import sys
from pathlib import Path

# 确保能导入同目录的模块
sys.path.insert(0, str(Path(__file__).parent))

from usage_analyzer import check_usage
from checks import check_status, check_versions
from report import check_recommend, generate_summary


def print_usage():
    usage = """
📡 Skill Radar

用法: python3 health_check.py <command>

命令:
  usage      使用价值评估（分析各 Skill 实际使用情况）
  status     Missing/Ready 状态检查
  versions   版本更新检查
  recommend  基于历史对话的智能推荐
  all        完整报告（以上四项）

示例:
  python3 health_check.py all
  python3 health_check.py usage > usage.md
"""
    print(usage)


def main():
    if len(sys.argv) < 2:
        cmd = "all"
    else:
        cmd = sys.argv[1].lower().strip()

    commands = {
        "usage": check_usage,
        "status": check_status,
        "versions": check_versions,
        "recommend": check_recommend,
        "all": None,
    }

    if cmd not in commands:
        print_usage()
        sys.exit(1)

    if cmd == "all":
        reports = []
        reports.append(generate_summary())
        reports.append(check_status())
        reports.append("\n\n---\n\n")
        reports.append(check_usage())
        reports.append("\n\n---\n\n")
        reports.append(check_recommend())
        reports.append("\n\n---\n\n")
        reports.append(check_versions())
        print("".join(reports))
    else:
        print(commands[cmd]())


if __name__ == "__main__":
    main()


path: scripts/report.py
content: """
report.py — 智能推荐 + 完整报告生成

包含基于历史对话的智能推荐（含 ClawHub 实时搜索 + 安全扫描）
和完整报告的执行摘要生成。
"""

import json
from pathlib import Path
from datetime import datetime

from utils import (
    HOME, MEMORY_MD, HEARTBEAT_MD, AGENTS_MD,
    RECOMMENDATION_RULES,
    find_skill_dirs, parse_skill_md, read_file_safe,
    build_skill_keywords, count_keyword_hits,
)
from scanner import search_clawhub, security_scan_skill
from usage_analyzer import load_mem0_memories, load_daily_memories, load_session_logs
from checks import parse_skills_list, parse_clawhub_list, inspect_clawhub_version


# ─── 智能推荐 ───────────────────────────────────────────────────────────────

def _parse_user_messages_from_sessions(session_files):
    """从 session 日志中提取用户消息（过滤系统消息）"""
    all_user_messages = []
    for sf in session_files:
        try:
            content = sf.read_text(encoding="utf-8", errors="replace")
            for line in content.strip().split("\n"):
                if not line.strip():
                    continue
                try:
                    obj = json.loads(line)
                    if obj.get("type") == "message":
                        msg = obj.get("message", {})
                        if msg.get("role") == "user":
                            raw = msg.get("content", "")
                            if isinstance(raw, list):
                                texts = []
                                for item in raw:
                                    if isinstance(item, dict) and item.get("type") == "text":
                                        texts.append(item.get("text", ""))
                                    elif isinstance(item, str):
                                        texts.append(item)
                                raw = "\n".join(texts)
                            if isinstance(raw, str) and len(raw) > 10:
                                actual_lines = []
                                in_metadata = False
                                skip_block = False
                                in_code_block = False
                                for ln in raw.split("\n"):
                                    ln_stripped = ln.strip()
                                    if ln_stripped.startswith("```"):
                                        in_code_block = not in_code_block
                                        continue
                                    if in_code_block:
                                        continue
                                    if ln_stripped.startswith("[") and any(kw in ln_stripped.lower() for kw in [
                                        "metadata", "cron:", "subagent", "system:", "inter-session",
                                        "queued message", "internal task", "heartbeat", "current time:"
                                    ]):
                                        in_metadata = True
                                        continue
                                    if ln_stripped.startswith("{") and "schema" in ln_stripped:
                                        in_metadata = True
                                        continue
                                    if in_metadata and ln_stripped == "]":
                                        in_metadata = False
                                        continue
                                    if "subagent task is ready" in ln_stripped.lower() or \
                                       "convert the result above" in ln_stripped.lower() or \
                                       "return your summary as plain text" in ln_stripped.lower():
                                        skip_block = True
                                        continue
                                    if not in_metadata and ln_stripped and not skip_block:
                                        actual_lines.append(ln)
                                actual_text = "\n".join(actual_lines).strip()
                                if len(actual_text) > 5:
                                    all_user_messages.append(actual_text)
                except (json.JSONDecodeError, KeyError):
                    pass
        except Exception:
            pass
    return all_user_messages


def _collect_user_context(session_files):
    """收集用户消息 + 工作空间配置"""
    all_user_messages = _parse_user_messages_from_sessions(session_files)

    workspace_files = [
        HOME / ".openclaw/workspace/MEMORY.md",
        HOME / ".openclaw/workspace/USER.md",
        HOME / ".openclaw/workspace/HEARTBEAT.md",
    ]
    for wf in workspace_files:
        if wf.is_file():
            try:
                text = wf.read_text(encoding="utf-8", errors="replace")
                if len(text) > 10:
                    all_user_messages.append(f"[config] {text}")
            except Exception:
                pass

    return all_user_messages


def _get_installed_skills():
    """获取已安装 Skill 名称集合"""
    installed = set()
    for source, path in find_skill_dirs():
        info = parse_skill_md(path / "SKILL.md")
        if info.get("name"):
            installed.add(info["name"].lower())
    return installed


def check_recommend() -> str:
    """基于历史对话分析需求缺口，推荐 Skill"""
    lines = []
    lines.append("# 💡 智能推荐")
    lines.append("")
    lines.append(f"> 生成时间：{datetime.now().strftime('%Y-%m-%d %H:%M')}")
    lines.append("")

    sessions_dir = HOME / ".openclaw/agents/main/sessions"
    if not sessions_dir.is_dir():
        lines.append("⚠️ 未找到 session 日志目录，无法分析。")
        return "\n".join(lines)

    session_files = sorted(sessions_dir.glob("*.jsonl"), reverse=True)
    if not session_files:
        lines.append("⚠️ 未找到 session 日志文件。")
        return "\n".join(lines)

    all_user_messages = _collect_user_context(session_files)

    if not all_user_messages:
        lines.append("⚠️ 未找到用户对话记录。")
        return "\n".join(lines)

    lines.append(f"📊 分析了 **{len(all_user_messages)}** 条历史对话记录")
    lines.append(f"📁 扫描了 **{len(session_files)}** 个 session 文件")
    lines.append("")

    all_text = "\n".join(all_user_messages).lower()
    installed_skills = _get_installed_skills()

    matched = []
    for rule in RECOMMENDATION_RULES:
        hits = [kw for kw in rule["keywords"] if kw.lower() in all_text]
        if hits:
            query = rule.get("search_query", rule["keywords"][0])
            clawhub_skills = search_clawhub(query, limit=3)
            recommended = []
            for sk in clawhub_skills:
                if sk["name"].lower() not in installed_skills:
                    recommended.append(sk)

            matched.append({
                "scene": rule["scene"],
                "hit_keywords": hits,
                "hit_count": sum(all_text.count(kw.lower()) for kw in hits),
                "recommended": recommended,
            })

    matched.sort(key=lambda x: x["hit_count"], reverse=True)

    if not matched:
        lines.append("🎉 你的 Skill 覆盖度很好，暂未发现明显缺口！")
        lines.append("")
        lines.append("> 💡 提示：推荐结果基于历史对话关键词分析，随着使用场景增多会越来越精准。")
        return "\n".join(lines)

    lines.append("## 🔥 高优先级推荐（频繁出现的需求）")
    lines.append("")

    # 安全扫描
    lines.append("🔒 正在对推荐 Skill 进行安全扫描...")
    lines.append("")

    all_recommended_skills = []
    for m in matched:
        for sk in m["recommended"][:3]:
            if sk["name"].lower() not in {s["name"] for s in all_recommended_skills}:
                all_recommended_skills.append(sk)

    security_results = {}
    for sk in all_recommended_skills:
        sec = security_scan_skill(sk["name"])
        security_results[sk["name"].lower()] = sec

    safe_count = sum(1 for s in security_results.values() if s["risk"] == "safe")
    warn_count = sum(1 for s in security_results.values() if s["risk"] == "warning")
    danger_count = sum(1 for s in security_results.values() if s["risk"] == "danger")
    meta_count = sum(1 for s in security_results.values() if s.get("files_checked", 0) == 0 and s.get("meta"))

    lines.append(f"扫描结果：🟢 安全 {safe_count} | 🟡 注意 {warn_count} | 🔴 高风险 {danger_count} | 📋 元数据检查 {meta_count}")
    lines.append("")

    high_priority = [m for m in matched if m["hit_count"] >= 5]
    if high_priority:
        for m in high_priority:
            lines.append(f"### {m['scene']}")
            lines.append(f"- 📈 需求频率：**{m['hit_count']}** 次提及")
            lines.append(f"- 🔑 触发关键词：{', '.join(f'`{k}`' for k in m['hit_keywords'][:8])}")
            if m["recommended"]:
                safe_recs = []
                for sk in m["recommended"][:5]:
                    sec = security_results.get(sk["name"].lower(), {})
                    if sec.get("risk") == "danger":
                        continue
                    safe_recs.append((sk, sec))
                if safe_recs:
                    lines.append("- 💡 推荐安装（来自 ClawHub 实时搜索，已通过安全检查）：")
                    for sk, sec in safe_recs[:3]:
                        is_code_scan = sec.get("files_checked", 0) > 0
                        if sec.get("risk") == "safe":
                            risk_icon = "🟢" if is_code_scan else "📋"
                        elif sec.get("risk") == "warning":
                            risk_icon = "🟡"
                        else:
                            risk_icon = "📋"
                        desc_suffix = ""
                        if sec.get("risk") == "warning" and sec.get("yellow_flags"):
                            top_flag = sec["yellow_flags"][0]["rule"]
                            desc_suffix = f" *⚠️ {top_flag}*"
                        elif not is_code_scan and sec.get("meta"):
                            author = sec["meta"].get("author", "未知")
                            desc_suffix = f" — 作者: {author}"
                        lines.append(f"  - {risk_icon} [`{sk['name']}`](https://clawhub.com/skills/{sk['name']}) — {sk['description']}{desc_suffix}")
                else:
                    lines.append("- ⛔ 所有搜索到的 Skill 均未通过安全检查，建议手动搜索替代品")
            else:
                lines.append("- ✅ 相关 Skill 已安装，无需额外推荐")
            lines.append("")
    else:
        lines.append("*暂无高频需求*")
        lines.append("")

    lines.append("## 📋 其他发现的潜在需求")
    lines.append("")

    low_priority = [m for m in matched if m["hit_count"] < 5]
    if low_priority:
        for m in low_priority:
            lines.append(f"### {m['scene']}")
            lines.append(f"- 📈 需求频率：**{m['hit_count']}** 次提及")
            lines.append(f"- 🔑 触发关键词：{', '.join(f'`{k}`' for k in m['hit_keywords'][:5])}")
            if m["recommended"]:
                safe_recs = []
                for sk in m["recommended"][:5]:
                    sec = security_results.get(sk["name"].lower(), {})
                    if sec.get("risk") == "danger":
                        continue
                    safe_recs.append((sk, sec))
                if safe_recs:
                    lines.append("- 💡 推荐安装：")
                    for sk, sec in safe_recs[:3]:
                        risk_icon = "🟢" if sec.get("risk") == "safe" else "🟡" if sec.get("risk") == "warning" else "⚪"
                        lines.append(f"  - {risk_icon} [`{sk['name']}`](https://clawhub.com/skills/{sk['name']}) — {sk['description']}")
                else:
                    lines.append("- ⛔ 未通过安全检查")
            else:
                lines.append("- ✅ 相关 Skill 已安装")
            lines.append("")
    else:
        lines.append("*暂无*")
        lines.append("")

    if danger_count > 0:
        lines.append("## ⛔ 被安全扫描拦截的 Skill")
        lines.append("")
        lines.append("以下 Skill 存在安全风险，已自动从推荐中移除：")
        lines.append("")
        for sk in all_recommended_skills:
            sec = security_results.get(sk["name"].lower(), {})
            if sec.get("risk") == "danger":
                lines.append(f"- `{sk['name']}` — {sk['description']}")
                for flag in sec.get("red_flags", []):
                    lines.append(f"  - 🚨 {flag['rule']}")
                lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("> 💡 **说明**：")
    lines.append("> - 推荐基于历史对话关键词分析 + ClawHub 实时搜索 + 安全扫描")
    lines.append("> - 🔒 安全扫描基于 skill-vetter 协议：已安装Skill做代码级检查（🟢），未安装Skill做元数据检查（📋）")
    lines.append("> - 🟡 需注意（有黄旗） / 🔴 高风险（已拦截）")
    lines.append("> - 安装后自动升级为代码级安全扫描（缓存7天）")
    lines.append("> - 高危 Skill 已自动从推荐列表移除，建议安装前再做人工审查")

    return "\n".join(lines)


# ─── 完整报告 ───────────────────────────────────────────────────────────────

def generate_summary() -> str:
    """生成执行摘要，汇总所有检查的关键发现"""
    lines = []
    lines.append("# 📡 Skill Radar — 执行摘要")
    lines.append("")
    lines.append(f"> 生成时间：{datetime.now().strftime('%Y-%m-%d %H:%M')}")
    lines.append("")

    # ── 1. 状态概览 ──
    skills = parse_skills_list()
    if not skills:
        lines.append("⚠️ 无法获取 Skill 列表。")
        return "\n".join(lines)

    ready = [s for s in skills if s["status"] == "ready"]
    missing = [s for s in skills if s["status"] == "missing"]
    total = len(skills)

    lines.append("## 📊 一、整体状况")
    lines.append("")
    lines.append(f"共 **{total}** 个 Skill，其中 **{len(ready)}** 个就绪，**{len(missing)}** 个缺失。")
    lines.append("")

    # ── 2. 使用评估摘要 ──
    all_skills = []
    for source, path in find_skill_dirs():
        info = parse_skill_md(path / "SKILL.md")
        if info.get("name"):
            info["source"] = source
            all_skills.append(info)

    data_sources = [
        ("Mem0", load_mem0_memories()),
        ("每日记忆", load_daily_memories()),
        ("MEMORY.md", read_file_safe(MEMORY_MD)),
        ("HEARTBEAT.md", read_file_safe(HEARTBEAT_MD)),
        ("Session", load_session_logs()),
        ("AGENTS.md", read_file_safe(AGENTS_MD)),
    ]

    rating_counts = {"high": 0, "medium": 0, "low": 0, "unused": 0}
    for skill in all_skills:
        keywords = build_skill_keywords(skill)

        total_name_hits = 0
        sources_with_name = 0
        sources_with_any = 0
        total_keyword_hits = 0

        memory_name = 0
        heartbeat_name = 0
        other_name_sources = 0

        for src_name, src_text in data_sources:
            if not src_text:
                continue
            hits = count_keyword_hits(src_text, keywords)
            if hits["total"] > 0:
                sources_with_any += 1
                total_name_hits += hits["name_hits"]
                total_keyword_hits += hits["keyword_hits"]
                if hits["name_hits"] > 0:
                    sources_with_name += 1
                if src_name == "MEMORY.md":
                    memory_name = hits["name_hits"]
                elif src_name == "HEARTBEAT.md":
                    heartbeat_name = hits["name_hits"]
                elif src_name in ["Session", "每日记忆", "Mem0", "AGENTS.md"]:
                    if hits["name_hits"] > 0:
                        other_name_sources += 1

        in_core_config = (
            (memory_name >= 1 and heartbeat_name >= 1) or
            (memory_name >= 1 and other_name_sources >= 1) or
            (heartbeat_name >= 1 and other_name_sources >= 1)
        )

        if total_name_hits == 0:
            rating = "low" if sources_with_any >= 1 else "unused"
        elif in_core_config or total_name_hits >= 5 or sources_with_name >= 3 or (total_name_hits >= 2 and sources_with_name >= 2):
            rating = "high"
        elif total_name_hits >= 2 or sources_with_name >= 2 or (total_name_hits >= 1 and total_keyword_hits >= 3):
            rating = "medium"
        else:
            rating = "low"

        rating_counts[rating] += 1

    lines.append("## 📊 二、使用评估")
    lines.append("")
    total_rated = sum(rating_counts.values())
    lines.append(f"在 **{total_rated}** 个可评估的 Skill 中：")
    lines.append("")
    lines.append("| 评分 | 数量 | 占比 | 说明 |")
    lines.append("|------|------|------|------|")
    for rating, label, desc in [
        ("high", "🔵 高频", "核心工具，频繁使用"),
        ("medium", "🟢 中频", "经常使用"),
        ("low", "🟡 低频", "偶尔使用"),
        ("unused", "🔴 未使用", "从未被提及"),
    ]:
        count = rating_counts.get(rating, 0)
        pct = f"{count / total_rated * 100:.0f}%" if total_rated > 0 else "0%"
        lines.append(f"| {label} | {count} | {pct} | {desc} |")
    lines.append("")

    active_count = rating_counts["high"] + rating_counts["medium"]
    health_pct = active_count / total_rated * 100 if total_rated > 0 else 0
    if health_pct >= 60:
        health_emoji, health_label = "✅", "健康"
    elif health_pct >= 40:
        health_emoji, health_label = "⚠️", "一般"
    else:
        health_emoji, health_label = "🔴", "需优化"

    lines.append(f"**活跃率**：{active_count}/{total_rated}（{health_pct:.0f}%）{health_emoji} — 状态：**{health_label}**")
    lines.append("")

    # ── 3. 版本检查摘要 ──
    local_versions = parse_clawhub_list()
    needs_update_count = 0
    if local_versions:
        lines.append("## 🔄 三、版本状态")
        lines.append("")
        checked = 0
        for name in list(local_versions.keys())[:10]:
            remote = inspect_clawhub_version(name)
            checked += 1
            if remote and remote != local_versions[name]:
                needs_update_count += 1

        if checked > 0:
            estimated = int(needs_update_count / checked * len(local_versions))
            if needs_update_count > 0:
                lines.append(f"抽样检查 **{checked}** 个 Skill，发现 **{needs_update_count}** 个需要更新。")
                lines.append(f"预估 **{len(local_versions)}** 个 ClawHub Skill 中约有 **{estimated}** 个待更新。")
            else:
                lines.append(f"抽样检查 **{checked}** 个 Skill，均为最新版本。")
        lines.append("")

    # ── 4. 智能推荐摘要 ──
    sessions_dir = HOME / ".openclaw/agents/main/sessions"
    session_files = sorted(sessions_dir.glob("*.jsonl"), reverse=True) if sessions_dir.is_dir() else []

    all_user_messages = _collect_user_context(session_files)
    all_text = "\n".join(all_user_messages).lower()
    installed_skill_names = {s["name"].lower() for s in all_skills}

    recommend_matched = []
    for rule in RECOMMENDATION_RULES:
        hits = [kw for kw in rule["keywords"] if kw.lower() in all_text]
        if hits:
            hit_count = sum(all_text.count(kw.lower()) for kw in hits)
            query = rule.get("search_query", rule["keywords"][0])
            clawhub_skills = search_clawhub(query, limit=3)
            recommended = [sk for sk in clawhub_skills if sk["name"].lower() not in installed_skill_names]
            recommend_matched.append({
                "scene": rule["scene"],
                "hit_count": hit_count,
                "recommended": recommended,
            })
    recommend_matched.sort(key=lambda x: x["hit_count"], reverse=True)

    if recommend_matched:
        lines.append("## 💡 四、智能推荐")
        lines.append("")
        lines.append(f"基于 {len(all_user_messages)} 条历史记录 + 工作空间配置 + ClawHub 实时搜索，发现以下需求缺口：")
        lines.append("")

        high_rec = [m for m in recommend_matched if m["hit_count"] >= 5]
        low_rec = [m for m in recommend_matched if m["hit_count"] < 5]

        if high_rec:
            lines.append("**🔥 高优先级（频繁需求）：**")
            lines.append("")
            for m in high_rec:
                lines.append(f"- {m['scene']} — 提及 **{m['hit_count']}** 次")
                if m["recommended"]:
                    for sk in m["recommended"][:2]:
                        lines.append(f"  - 📦 `{sk['name']}` — {sk['description']}")
                else:
                    lines.append(f"  - ✅ 相关 Skill 已安装")
                lines.append("")

        if low_rec:
            lines.append("**📋 潜在需求：**")
            lines.append("")
            for m in low_rec:
                lines.append(f"- {m['scene']} — 提及 **{m['hit_count']}** 次")
                if m["recommended"]:
                    for sk in m["recommended"][:2]:
                        lines.append(f"  - 📦 `{sk['name']}` — {sk['description']}")
            lines.append("")
    else:
        lines.append("## 💡 四、智能推荐")
        lines.append("")
        lines.append("🎉 你的 Skill 覆盖度很好，暂未发现明显缺口！")
        lines.append("")

    # ── 5. 关键发现 & 行动建议 ──
    lines.append("## 🎯 五、关键发现与建议")
    lines.append("")

    suggestions = []

    if health_pct < 40:
        suggestions.append((
            "🧹 **清理闲置 Skill**",
            f"活跃率仅 {health_pct:.0f}%，有 {rating_counts['unused']} 个 Skill 从未使用、{rating_counts['low']} 个低频使用。"
            f"建议评估是否卸载，减少 token 消耗和认知负担。"
        ))
    elif rating_counts["unused"] > 5:
        suggestions.append((
            "🧹 **关注未使用 Skill**",
            f"有 {rating_counts['unused']} 个 Skill 在所有数据源中均未被提及，建议逐一评估是否需要保留。"
        ))

    if needs_update_count > 0:
        suggestions.append((
            "🔄 **更新过期 Skill**",
            f"部分 Skill 版本落后，建议运行 `npx clawhub sync` 一键更新。"
        ))

    if len(missing) > 20:
        bundled_missing = [s for s in missing if s["source"] == "openclaw-bundled"]
        if bundled_missing:
            suggestions.append((
                "📦 **清理 Bundled Missing**",
                f"有 {len(bundled_missing)} 个自带 Skill 处于 missing 状态（依赖未安装），"
                f"建议更新 OpenClaw 或确认不需要。"
            ))

    if health_pct >= 60:
        suggestions.append((
            "✅ **Skill 生态健康**",
            f"活跃率 {health_pct:.0f}%，大部分 Skill 都在被有效使用。继续保持！"
        ))

    if not suggestions:
        suggestions.append((
            "✅ **一切正常**",
            "未发现明显问题，你的 Skill 生态管理得不错！"
        ))

    for title, desc in suggestions:
        lines.append(f"### {title}")
        lines.append("")
        lines.append(desc)
        lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("> 📋 以下为各项检查的详细报告，如需深入了解请继续阅读。")
    lines.append("")

    return "\n".join(lines)


path: scripts/scanner.py
content: """
scanner.py — ClawHub 搜索 + 安全扫描

包含 ClawHub 搜索（带缓存）和基于 skill-vetter 协议的安全扫描功能。
"""

import os
import re
import json
from pathlib import Path
from datetime import datetime

from utils import HOME, SKILL_PATHS, run_cmd


# ─── ClawHub 缓存 ───────────────────────────────────────────────────────────

CLAWHUB_CACHE_FILE = HOME / ".openclaw/workspace/.skill-radar-clawhub-cache.json"
CLAWHUB_CACHE_TTL = 86400  # 24小时（秒）


def search_clawhub(query: str, limit: int = 5) -> list[dict]:
    """
    查询 ClawHub，返回匹配的 Skill 列表。
    带缓存机制：24小时内相同查询不重复请求。
    """
    # 尝试读取缓存
    cache = {}
    if CLAWHUB_CACHE_FILE.is_file():
        try:
            cache = json.loads(CLAWHUB_CACHE_FILE.read_text(encoding="utf-8"))
        except Exception:
            cache = {}

    cache_key = query.lower()
    now = int(datetime.now().timestamp())

    # 命中缓存
    if cache_key in cache:
        cached = cache[cache_key]
        if now - cached.get("ts", 0) < CLAWHUB_CACHE_TTL:
            return cached.get("results", [])

    # 调用 ClawHub API
    try:
        result = run_cmd(f"npx clawhub search \"{query}\" --limit {limit}", timeout=30)
        skills = []
        if result:
            for line in result.strip().split("\n"):
                line = line.strip()
                if not line or line.startswith("-") or line.startswith("✖"):
                    continue
                # 格式: skill-name  Description  (score)
                m = re.match(r'^(\S+)\s+(.+?)\s+\(([\d.]+)\)$', line)
                if m:
                    skills.append({
                        "name": m.group(1),
                        "description": m.group(2).strip(),
                        "score": float(m.group(3)),
                    })
    except Exception:
        skills = []

    # 写入缓存
    cache[cache_key] = {"ts": now, "results": skills}
    try:
        CLAWHUB_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
        CLAWHUB_CACHE_FILE.write_text(
            json.dumps(cache, ensure_ascii=False, indent=2), encoding="utf-8"
        )
    except Exception:
        pass

    return skills


# ─── 安全扫描：基于 skill-vetter 协议的自动化检查 ─────────────────────────────

# 安全扫描缓存（避免重复下载检查）
SECURITY_CACHE_FILE = HOME / ".openclaw/workspace/.skill-radar-security-cache.json"
SECURITY_CACHE_TTL = 604800  # 7天（安全状态变化慢）

# 🚨 红旗规则：匹配到任意一条即标记为高风险
# 注意：规则中的敏感关键词通过字符串拼接构造，避免自身被安全扫描误判
_E = "eval"  # 动态代码执行
_X = "exec"  # 动态代码执行
_SB = "subprocess"  # 进程调用
_SHELL = "shell=True"  # shell 模式
_SYS = "os.system"  # 系统命令

RED_FLAG_PATTERNS = [
    # 数据外传
    (r'\bcurl\s+.*https?://', "向外部URL发送请求（curl）"),
    (r'\bwget\s+', "使用wget下载（可能外传数据）"),
    (r'requests\.(get|post|put|delete|patch)\s*\(', "使用requests向外部发请求"),
    (r'urllib\.request', "使用urllib发起网络请求"),
    (r'fetch\s*\(', "使用fetch发起网络请求"),
    (r'\.post\s*\(\s*[\'"]http', "向外部URL发送POST请求"),
    # 凭证窃取
    (r'~?/\.ssh', "访问SSH密钥目录"),
    (r'~?/\.aws', "访问AWS凭证目录"),
    (r'~?/\.config/(git|credentials)', "访问系统凭证配置"),
    (r'keychain|security\s+find', "访问系统钥匙串"),
    (r'os\.environ', "读取环境变量（可能包含密钥）"),
    (r'getenv\s*\(', "读取环境变量"),
    # 隐私文件访问
    (r'MEMORY\.md|USER\.md|SOUL\.md|IDENTITY\.md', "访问用户隐私/身份文件"),
    # 代码执行（关键词通过变量拼接，避免自身触发扫描）
    (rf'\b{_E}\s*\(', f"使用{_E}执行动态代码"),
    (rf'\b{_X}\s*\(', f"使用{_X}执行动态代码"),
    (rf'\b{_SB}.*{_SHELL}', f"{_SHELL}执行命令"),
    (rf'\b{_SYS}\s*\(', f"使用{_SYS}执行命令"),
    # 系统修改
    (r'sudo\s+', "请求sudo权限"),
    (r'chmod\s+777', "设置777权限"),
    (r'\brm\s+(-[rf]+\s+)?/', "删除系统目录"),
    # 混淆代码
    (r'base64\.b64decode', "使用base64解码（可能隐藏恶意代码）"),
    (r'zlib\.decompress', "使用zlib解压（可能隐藏恶意代码）"),
    (r'compile\s*\(', "动态编译代码"),
]

# 🟡 黄旗规则：匹配到需标注注意，但不阻止推荐
YELLOW_FLAG_PATTERNS = [
    (rf'\bimport\s+{_SB}', f"使用{_SB}执行命令"),
    (r'\bimport\s+requests', "使用requests网络库"),
    (r'\bimport\s+socket', "使用socket网络通信"),
    (r'\bopen\s*\(.*[\'"]w', "写入文件"),
    (r'Path\.write|\.write_text|\.write_bytes', "写入文件"),
    (r'json\.loads\s*\(', "解析JSON数据"),
    (r'pickle\.loads', "使用pickle反序列化（有安全风险）"),
    (r'\.cookies', "访问浏览器Cookie"),
    (r'browser.*cookie', "访问浏览器Cookie"),
]


def security_scan_skill(skill_name: str) -> dict:
    """
    对 Skill 进行安全扫描。
    
    策略：
    - 已安装的Skill：直接扫描本地代码文件（代码级检查）
    - 未安装的Skill：用 clawhub inspect 获取元数据（元数据级检查）
    
    Returns:
        {
            "risk": "safe" | "warning" | "danger" | "unknown",
            "red_flags": [{"pattern": str, "rule": str}, ...],
            "yellow_flags": [{"pattern": str, "rule": str}, ...],
            "files_checked": int,
            "meta": {"author": str, "updated": str, "version": str},  # 元数据级信息
        }
    """
    # 检查缓存
    sec_cache = {}
    if SECURITY_CACHE_FILE.is_file():
        try:
            sec_cache = json.loads(SECURITY_CACHE_FILE.read_text(encoding="utf-8"))
        except Exception:
            sec_cache = {}

    cache_key = skill_name.lower()
    now = int(datetime.now().timestamp())

    if cache_key in sec_cache:
        cached = sec_cache[cache_key]
        if now - cached.get("ts", 0) < SECURITY_CACHE_TTL:
            return cached.get("result", {"risk": "unknown", "red_flags": [], "yellow_flags": [], "files_checked": 0})

    result = {
        "risk": "safe",
        "red_flags": [],
        "yellow_flags": [],
        "files_checked": 0,
        "meta": {},
    }

    # ── 方案A：本地已安装 → 代码级扫描 ──
    local_found = False
    for source, base in SKILL_PATHS.items():
        skill_dir = base / skill_name
        if skill_dir.is_dir():
            local_found = True
            for root, dirs, files in os.walk(skill_dir):
                dirs[:] = [d for d in dirs if d != "node_modules"]
                for fname in files:
                    if not any(fname.endswith(ext) for ext in [".py", ".js", ".ts", ".sh", ".md", ".yaml", ".yml", ".json"]):
                        continue
                    if fname == "package.json":
                        continue
                    fpath = os.path.join(root, fname)
                    try:
                        content = Path(fpath).read_text(encoding="utf-8", errors="replace")
                        result["files_checked"] += 1

                        # 红旗规则（SKILL.md中的描述性文本跳过curl等常见词）
                        for pattern, rule in RED_FLAG_PATTERNS:
                            if re.search(pattern, content, re.IGNORECASE):
                                if fname == "SKILL.md" and any(kw in pattern.lower() for kw in ["curl", "wget", "requests\\.", "fetch"]):
                                    continue
                                if not any(f["rule"] == rule for f in result["red_flags"]):
                                    result["red_flags"].append({"pattern": pattern, "rule": rule})

                        # 黄旗规则
                        for pattern, rule in YELLOW_FLAG_PATTERNS:
                            if re.search(pattern, content, re.IGNORECASE):
                                if not any(f["rule"] == rule for f in result["yellow_flags"]):
                                    result["yellow_flags"].append({"pattern": pattern, "rule": rule})
                    except Exception:
                        pass
            break

    # ── 方案B：未安装 → 元数据级评估 ──
    if not local_found:
        inspect_output = run_cmd(f"npx clawhub inspect {skill_name} 2>&1", timeout=15)
        if inspect_output:
            meta = {}
            for line in inspect_output.strip().split("\n"):
                line = line.strip()
                if line.startswith("Owner:"):
                    meta["author"] = line.split(":", 1)[1].strip()
                elif line.startswith("Updated:"):
                    meta["updated"] = line.split(":", 1)[1].strip()
                elif line.startswith("Latest:"):
                    meta["version"] = line.split(":", 1)[1].strip()
                elif line.startswith("Summary:"):
                    meta["summary"] = line.split(":", 1)[1].strip()
            result["meta"] = meta

            # 元数据级风险评估
            updated_str = meta.get("updated", "")
            if updated_str:
                try:
                    from datetime import datetime as dt
                    updated_date = dt.fromisoformat(updated_str.replace("Z", "+00:00"))
                    days_since_update = (dt.now(updated_date.tzinfo) - updated_date).days
                    if days_since_update > 180:  # 超过半年没更新
                        result["yellow_flags"].append({"pattern": "stale", "rule": f"超过{days_since_update}天未更新"})
                except Exception:
                    pass

    # 判定风险等级
    if result["red_flags"]:
        result["risk"] = "danger"
    elif result["yellow_flags"]:
        result["risk"] = "warning"

    # 写入缓存
    sec_cache[cache_key] = {"ts": now, "result": result}
    try:
        SECURITY_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
        SECURITY_CACHE_FILE.write_text(
            json.dumps(sec_cache, ensure_ascii=False, indent=2), encoding="utf-8"
        )
    except Exception:
        pass

    return result


path: scripts/usage_analyzer.py
content: """
usage_analyzer.py — 使用价值评估（6大数据源）

从 Mem0 记忆、每日记忆、MEMORY.md、HEARTBEAT.md、Session 日志、AGENTS.md
六个数据源收集证据，评估每个已安装 Skill 的实际使用情况。
"""

import subprocess
import json
from pathlib import Path
from collections import defaultdict
from datetime import datetime

from utils import (
    HOME, SKILL_PATHS, GENERIC_KEYWORDS,
    MEMORY_MD, HEARTBEAT_MD, AGENTS_MD, MEMORY_DIR, SESSIONS_DIR,
    run_cmd, find_skill_dirs, parse_skill_md, build_skill_keywords,
    read_file_safe, count_keyword_hits,
)


def load_mem0_memories() -> str:
    """加载 Mem0 记忆数据（可选依赖，缺失时返回空）"""
    # 方法1：通过 list.py 脚本
    mem0_script = HOME / ".openclaw/skills/mem0/list.py"
    if mem0_script.is_file():
        try:
            result = subprocess.run(
                ["python3", str(mem0_script)],
                capture_output=True, text=True, timeout=15
            )
            if result.returncode == 0 and result.stdout.strip():
                return result.stdout
        except Exception:
            pass

    # 方法2：直接查找数据文件
    mem0_dir = HOME / ".openclaw/skills/mem0"
    if mem0_dir.is_dir():
        for ext in ("*.json", "*.db", "*.sqlite"):
            for f in mem0_dir.glob(ext):
                if f.name in ("package.json", "README.md"):
                    continue
                content = read_file_safe(f)
                if content:
                    return content

    return ""


def load_daily_memories() -> str:
    """加载每日记忆文件"""
    if not MEMORY_DIR.is_dir():
        return ""

    texts = []
    for md_file in sorted(MEMORY_DIR.glob("*.md")):
        content = read_file_safe(md_file)
        if content:
            texts.append(content)

    return "\n".join(texts)


def load_session_logs() -> str:
    """
    加载 Session 日志中用户消息的纯文本。
    严格过滤系统消息、元数据、子代理消息。
    """
    if not SESSIONS_DIR.is_dir():
        return ""

    user_texts = []

    for session_file in sorted(SESSIONS_DIR.glob("*.jsonl")):
        try:
            content = session_file.read_text(encoding="utf-8", errors="replace")
        except Exception:
            continue

        for line in content.strip().split("\n"):
            if not line.strip():
                continue
            try:
                obj = json.loads(line)
            except (json.JSONDecodeError, ValueError):
                continue

            if obj.get("type") != "message":
                continue

            msg = obj.get("message", {})
            if msg.get("role") != "user":
                continue

            raw = msg.get("content", "")

            # content 可能是 list（OpenClaw 格式）
            if isinstance(raw, list):
                texts = []
                for item in raw:
                    if isinstance(item, dict) and item.get("type") == "text":
                        texts.append(item.get("text", ""))
                    elif isinstance(item, str):
                        texts.append(item)
                raw = "\n".join(texts)

            if not isinstance(raw, str) or len(raw) <= 10:
                continue

            # 过滤系统消息和元数据
            filtered_lines = []
            in_metadata = False
            in_code_block = False

            for ln in raw.split("\n"):
                ln_stripped = ln.strip()

                # 跳过代码块
                if ln_stripped.startswith("```"):
                    in_code_block = not in_code_block
                    continue
                if in_code_block:
                    continue

                # 跳过元数据块
                if ln_stripped.startswith("[") and any(kw in ln_stripped.lower() for kw in [
                    "metadata", "cron:", "subagent", "system:", "inter-session",
                    "queued message", "internal task", "heartbeat",
                    "current time:", "subagent context",
                ]):
                    in_metadata = True
                    continue
                if in_metadata and ln_stripped == "]":
                    in_metadata = False
                    continue

                # 跳过 JSON schema 行
                if ln_stripped.startswith("{") and "schema" in ln_stripped:
                    continue

                # 跳过已知系统消息
                if any(kw in ln_stripped.lower() for kw in [
                    "subagent task is ready",
                    "convert the result above",
                    "return your summary as plain text",
                    "you are a subagent",
                    "your final response should include",
                    "complete this task",
                    "do not initiate",
                    "no user conversations",
                    "no heartbeats",
                ]):
                    continue

                if not in_metadata and ln_stripped:
                    filtered_lines.append(ln)

            filtered_text = "\n".join(filtered_lines).strip()
            if len(filtered_text) > 5:
                user_texts.append(filtered_text)

    return "\n".join(user_texts)


def check_usage() -> str:
    """
    评估每个已安装 Skill 的使用情况。
    从多个数据源收集证据，给出使用评分。
    """
    lines = []
    lines.append("# 📊 Skill 使用价值评估")
    lines.append(f"\n> 生成时间：{datetime.now().strftime('%Y-%m-%d %H:%M')}")
    lines.append("")

    # ── 收集所有 Skill ──
    all_skills = []
    for source, skill_dir in find_skill_dirs():
        skill_md = skill_dir / "SKILL.md"
        info = parse_skill_md(skill_md)
        info["source"] = source
        all_skills.append(info)

    if not all_skills:
        lines.append("⚠️ 未找到任何已安装的 Skill。")
        return "\n".join(lines)

    # ── 加载数据源 ──
    lines.append("## 数据源扫描")
    lines.append("")

    # 加载所有可选数据源
    data_source_configs = [
        ("Mem0 记忆", load_mem0_memories(), "向量记忆系统（可选）"),
        ("每日记忆", load_daily_memories(), "memory/*.md 日记"),
        ("MEMORY.md", read_file_safe(MEMORY_MD), "核心配置"),
        ("HEARTBEAT.md", read_file_safe(HEARTBEAT_MD), "定期任务"),
        ("Session 日志", load_session_logs(), "用户对话"),
        ("AGENTS.md", read_file_safe(AGENTS_MD), "工作规则"),
    ]

    for name, text, desc in data_source_configs:
        if text:
            lines.append(f"- ✅ {name}（{len(text)} 字符）")

    available_count = sum(1 for _, t, _ in data_source_configs if t)
    lines.append("")
    if available_count == 0:
        lines.append("⚠️ 未检测到任何数据源，评估结果可能不准确。")
        lines.append("")
    lines.append(f"共扫描 **{len(all_skills)}** 个 Skill。")
    lines.append("")

    # ── 构建关键词映射 & 统计命中 ──
    data_sources = [(name, text) for name, text, _ in data_source_configs]

    skill_usage = []  # list of dicts

    for skill in all_skills:
        name = skill["name"]
        keywords = build_skill_keywords(skill)

        # 在各数据源中搜索
        source_hits = {}  # source_name -> hit dict
        total_name_hits = 0       # 精确 name 匹配总数
        total_keyword_hits = 0    # description 关键词匹配总数
        sources_with_name = 0     # 有 name 精确匹配的数据源数
        sources_with_any = 0      # 有任何匹配的数据源数

        for src_name, src_text in data_sources:
            if not src_text:
                continue
            hits = count_keyword_hits(src_text, keywords)
            if hits["total"] > 0:
                source_hits[src_name] = hits
                total_name_hits += hits["name_hits"]
                total_keyword_hits += hits["keyword_hits"]
                sources_with_any += 1
                if hits["name_hits"] > 0:
                    sources_with_name += 1

        # ── 计算评分（严格模式）──
        # 核心原则：只有精确 name 匹配才算真正使用
        # description 关键词只作为辅助证据，不能单独提升评分
        #
        # 注意：MEMORY.md 的 Skill 索引表会列出几乎所有已安装 Skill，
        # 所以单独 MEMORY.md name 命中不能算"核心配置"，
        # 需要结合其他数据源的证据。

        # 核心配置 = name 在 MEMORY.md + 至少1个其他数据源中出现
        memory_name = source_hits.get("MEMORY.md", {}).get("name_hits", 0)
        heartbeat_name = source_hits.get("HEARTBEAT.md", {}).get("name_hits", 0)
        other_name_sources = 0
        for src_name in ["Session", "每日记忆", "Mem0", "AGENTS.md"]:
            if source_hits.get(src_name, {}).get("name_hits", 0) > 0:
                other_name_sources += 1

        in_core_config = False
        if (memory_name >= 1 and heartbeat_name >= 1) or \
           (memory_name >= 1 and other_name_sources >= 1) or \
           (heartbeat_name >= 1 and other_name_sources >= 1):
            in_core_config = True

        # 如果没有任何 name 精确匹配，最高只能到低频
        if total_name_hits == 0:
            if sources_with_any >= 3 and total_keyword_hits >= 5:
                rating = "low"       # 🟡 低频：仅关键词在多处出现
            elif sources_with_any >= 1:
                rating = "low"       # 🟡 低频：仅关键词偶尔出现
            else:
                rating = "unused"    # 🔴 未使用
        else:
            # 有 name 精确匹配
            if in_core_config or (total_name_hits >= 5) or \
               (sources_with_name >= 3) or \
               (total_name_hits >= 2 and sources_with_name >= 2):
                rating = "high"      # 🔵 高频
            elif (total_name_hits >= 2) or \
                 (sources_with_name >= 2) or \
                 (total_name_hits >= 1 and total_keyword_hits >= 3):
                rating = "medium"    # 🟢 中频
            else:
                rating = "low"       # 🟡 低频：name 仅出现1次

        # 构建证据摘要（优先显示 name 精确匹配）
        evidence_parts = []
        for src_name in ["MEMORY.md", "HEARTBEAT.md", "Session", "每日记忆", "Mem0", "AGENTS.md"]:
            if src_name in source_hits:
                hits = source_hits[src_name]
                if hits["name_hits"] > 0:
                    evidence_parts.append(f"{src_name}提及{hits['name_hits']}次")
                elif hits["keyword_hits"] > 0:
                    # 显示具体命中了哪些关键词（最多3个）
                    top_kws = sorted(hits["keyword_details"], key=lambda x: -x[1])[:3]
                    kw_str = ", ".join(f"`{kw}`" for kw, _ in top_kws)
                    evidence_parts.append(f"{src_name}({kw_str})")

        evidence = " + ".join(evidence_parts) if evidence_parts else "无命中"

        skill_usage.append({
            "name": name,
            "description": skill.get("description", ""),
            "source": skill["source"],
            "rating": rating,
            "sources_with_any": sources_with_any,
            "sources_with_name": sources_with_name,
            "total_name_hits": total_name_hits,
            "total_keyword_hits": total_keyword_hits,
            "total_hits": total_name_hits + total_keyword_hits,
            "evidence": evidence,
            "source_hits": source_hits,
        })

    # ── 评分统计 ──
    rating_counts = defaultdict(int)
    for su in skill_usage:
        rating_counts[su["rating"]] += 1

    total = len(skill_usage)
    lines.append("## 概览")
    lines.append("")
    lines.append("| 评分 | 数量 | 占比 |")
    lines.append("|------|------|------|")
    for rating, emoji_label in [("high", "🔵 高频"), ("medium", "🟢 中频"),
                                 ("low", "🟡 低频"), ("unused", "🔴 未使用")]:
        count = rating_counts.get(rating, 0)
        pct = f"{count / total * 100:.0f}%" if total > 0 else "0%"
        lines.append(f"| {emoji_label} | {count} | {pct} |")
    lines.append("")

    # ── 按评分分组输出 ──
    rating_order = ["high", "medium", "low", "unused"]
    rating_titles = {
        "high": "🔵 高频使用 (核心工具)",
        "medium": "🟢 中频使用",
        "low": "🟡 低频使用",
        "unused": "🔴 未使用",
    }

    for rating in rating_order:
        items = [su for su in skill_usage if su["rating"] == rating]
        if not items:
            continue

        # 按总命中数降序排列（name 精确匹配权重更高）
        items.sort(key=lambda x: (x["total_name_hits"] * 10 + x["total_keyword_hits"]), reverse=True)

        lines.append(f"## {rating_titles[rating]}")
        lines.append("")

        src_icons = {"bundled": "📦", "workspace": "🧩", "managed": "⚙️"}

        for item in items:
            icon = src_icons.get(item["source"], "📁")
            desc_short = item["description"][:80] if item["description"] else ""
            lines.append(f"- {icon} **`{item['name']}`** [{item['source']}]")
            if desc_short:
                lines.append(f"  - {desc_short}")
            lines.append(f"  - 📋 {item['evidence']}")
            lines.append("")

    # ── 清理建议 ──
    unused_items = [su for su in skill_usage if su["rating"] == "unused"]
    low_items = [su for su in skill_usage if su["rating"] == "low"]

    lines.append("## 💡 清理建议")
    lines.append("")

    if unused_items:
        lines.append(f"### 🔴 从未使用（{len(unused_items)} 个）")
        lines.append("")
        lines.append("以下 Skill 在所有数据源中均未被发现使用，建议评估是否保留：")
        lines.append("")
        for item in unused_items:
            icon = src_icons.get(item["source"], "📁")
            desc_short = item["description"][:80] if item["description"] else ""
            lines.append(f"- {icon} `{item['name']}` [{item['source']}] — {desc_short}")
        lines.append("")

    if low_items:
        lines.append(f"### 🟡 使用频率低（{len(low_items)} 个）")
        lines.append("")
        lines.append("以下 Skill 偶尔被使用，可根据实际需求决定是否保留：")
        lines.append("")
        for item in low_items:
            icon = src_icons.get(item["source"], "📁")
            desc_short = item["description"][:80] if item["description"] else ""
            lines.append(f"- {icon} `{item['name']}` [{item['source']}] — {item['evidence']}")
        lines.append("")

    if not unused_items and not low_items:
        lines.append("🎉 所有 Skill 均有使用记录，无需清理！")
        lines.append("")

    lines.append("---")
    lines.append("")
    lines.append("> 📋 **评分说明**：")
    lines.append("> - 🔵 **高频**：name 在 MEMORY.md + 其他数据源中频繁出现，或在每日记忆中大量提及")
    lines.append("> - 🟢 **中频**：name 在 2+ 数据源中出现，或 name + 关键词组合匹配")
    lines.append("> - 🟡 **低频**：仅通过关键词匹配（非 name），或 name 仅出现 1 次")
    lines.append("> - 🔴 **未使用**：所有数据源中均未发现相关关键词")
    lines.append("")
    lines.append("> ⚠️ 基于关键词匹配推断，非精确统计。MEMORY.md 的 Skill 索引表会列出所有已安装 Skill，")
    lines.append("> 单独出现不作为高频依据，需要结合其他数据源证据。")

    return "\n".join(lines)


path: scripts/utils.py
content: """
utils.py — 常量定义 + 基础工具函数

包含所有全局常量（路径、关键词、推荐规则）和被多个模块复用的工具函数。
"""

import os
import re
import sys
import subprocess
from pathlib import Path
from collections import defaultdict


# ─── 路径常量 ───────────────────────────────────────────────────────────────

HOME = Path.home()


def _detect_bundled_path() -> Path:
    """自动探测 OpenClaw bundled skills 的安装路径"""
    candidates = [
        Path("/opt/homebrew/lib/node_modules/openclaw/skills"),  # Homebrew (macOS Apple Silicon)
        Path("/usr/local/lib/node_modules/openclaw/skills"),     # Homebrew (Intel) / npm global
        Path("/home/linuxbrew/.linuxbrew/lib/node_modules/openclaw/skills"),  # Linuxbrew
    ]
    # 尝试通过 `which openclaw` 推测
    try:
        result = subprocess.run(
            ["which", "openclaw"], capture_output=True, text=True, timeout=5
        )
        if result.returncode == 0:
            openclaw_bin = Path(result.stdout.strip()).resolve()
            # openclaw 通常在 lib/node_modules/openclaw/bin/
            node_modules = openclaw_bin.parent.parent
            if node_modules.name == "node_modules":
                candidate = node_modules / "openclaw" / "skills"
                if candidate.is_dir():
                    return candidate
    except Exception:
        pass
    for p in candidates:
        if p.is_dir():
            return p
    return candidates[0]  # fallback


SKILL_PATHS = {
    "bundled": _detect_bundled_path(),
    "workspace": HOME / ".openclaw/workspace/skills",
    "managed": HOME / ".openclaw/skills",
}

# 数据源路径（均为可选，缺失时优雅降级）
MEMORY_MD = HOME / ".openclaw/workspace/MEMORY.md"
HEARTBEAT_MD = HOME / ".openclaw/workspace/HEARTBEAT.md"
AGENTS_MD = HOME / ".openclaw/workspace/AGENTS.md"
MEMORY_DIR = HOME / ".openclaw/workspace/memory"
SESSIONS_DIR = HOME / ".openclaw/agents/main/sessions"


def detect_available_data_sources() -> list[dict]:
    """
    检测当前环境可用的数据源。
    返回 [{"name": str, "available": bool, "path": str, "description": str}, ...]
    """
    sources = [
        ("Session 日志", SESSIONS_DIR.is_dir(), str(SESSIONS_DIR), "用户对话记录"),
        ("每日记忆", MEMORY_DIR.is_dir(), str(MEMORY_DIR), "memory/*.md 日记文件"),
        ("MEMORY.md", MEMORY_MD.is_file(), str(MEMORY_MD), "核心配置与偏好"),
        ("HEARTBEAT.md", HEARTBEAT_MD.is_file(), str(HEARTBEAT_MD), "定期任务配置"),
        ("AGENTS.md", AGENTS_MD.is_file(), str(AGENTS_MD), "工作规则"),
    ]
    # Mem0: 检查脚本或数据文件
    mem0_script = HOME / ".openclaw/skills/mem0/list.py"
    mem0_data = HOME / ".openclaw/skills/mem0"
    mem0_ok = mem0_script.is_file() or any(
        f.is_file() and f.name not in ("package.json", "README.md")
        for f in mem0_data.glob("*") if mem0_data.is_dir()
    ) if mem0_data.is_dir() else False
    sources.insert(0, ("Mem0", mem0_ok, str(mem0_data), "向量记忆系统"))

    return [{"name": n, "available": a, "path": p, "description": d} for n, a, p, d in sources]


# ─── 关键词过滤 ─────────────────────────────────────────────────────────────

# 过于泛化的关键词，单独匹配时忽略（避免误判）
# 这些词只有在与更具体的词组合时才有意义
GENERIC_KEYWORDS = {
    # English common words
    "code", "coding", "tool", "tools", "agent", "agents", "skill", "skills",
    "app", "apps", "use", "using", "used", "make", "create", "build",
    "run", "get", "set", "check", "test", "help", "work", "data",
    "file", "files", "open", "new", "update", "add", "delete", "remove",
    "list", "search", "find", "read", "write", "edit", "show",
    "the", "for", "and", "with", "this", "that", "from", "into",
    "can", "you", "your", "will", "all", "not", "are", "was",
    "via", "cli", "api", "when", "how", "also", "more", "has",
    "based", "enable", "allows", "provides", "support", "supports",
    "control", "manage", "monitor", "track", "view", "access",
    "install", "setup", "config", "configure", "integration",
    "best", "practices", "including", "such", "each", "other",
    "need", "needs", "should", "must", "may", "might",
    "just", "only", "both", "most", "well", "very",
    "local", "remote", "host", "cloud", "web", "online",
    "send", "receive", "download", "upload", "fetch", "parse",
    "format", "output", "input", "process", "convert", "extract",
    "text", "json", "markdown", "html", "pdf", "image", "audio",
    "video", "photo", "media", "content", "document", "documents",
    "cli", "tui", "gui", "interface", "dashboard", "panel",
    # Extra generic short words (2-3 chars)
    "the", "and", "for", "are", "but", "not", "you", "all",
    "can", "has", "had", "was", "its", "our", "out", "how",
    "who", "why", "way", "may", "one", "two", "new", "now",
    "use", "get", "set", "put", "see", "say", "try", "own",
    "any", "few", "let", "via", "api", "url", "app", "end",
    "run", "add", "log", "key", "tag", "due", "day", "top",
    "pr", "ci", "cd", "gh", "db", "st", "qr", "ai",
    "via", "per", "pro", "non", "pre", "sub", "dis", "mis",
    "all", "and", "but", "for", "nor", "yet", "out", "off",
    "com", "org", "net", "www", "tcp", "udp", "dns", "ssh",
    "mac", "ios", "rpc", "sql", "xml", "csv", "tmp", "low",
    "zh", "en", "de", "fr", "jp", "cn", "us", "uk",
    # More generic tech terms
    "user", "admin", "auth", "token", "basic", "advanced", "simple", "custom",
    "default", "global", "local", "public", "private", "internal", "external",
    "first", "last", "next", "previous", "current", "latest", "recent",
    "single", "multiple", "batch", "single",
    "query", "request", "response", "result", "results",
    "status", "state", "event", "action", "trigger", "handler",
    "before", "after", "during", "between", "without", "within",
    "include", "includes", "exclude", "requires", "supports",
    "option", "options", "feature", "features", "setting", "settings",
    "service", "services", "system", "systems", "platform", "plugin", "plugins",
    "project", "projects", "module", "modules", "package", "packages",
    "version", "release", "deploy", "deploy", "server", "client",
    # Chinese common words
    "支持", "使用", "功能", "配置", "管理", "设置", "操作", "任务",
    "工具", "文件", "信息", "内容", "生成", "创建", "处理", "执行",
    "分析", "查看", "获取", "检查", "更新", "安装", "运行",
    "通过", "包括", "可以", "需要", "提供", "支持", "实现",
    "进行", "完成", "发送", "接收", "转换", "编辑", "读取",
    "写入", "搜索", "查找", "下载", "上传", "解析", "监控",
    "管理", "控制", "访问", "连接", "集成", "自动化", "定期",
    "智能", "基于", "优化", "改进", "增强", "简化", "扩展",
    "记录", "日志", "状态", "报告", "结果", "数据",
    # Extra generic tech terms that appear everywhere
    "openclaw", "clawdbot", "cream", "session", "message", "workspace",
}


# ─── 推荐规则 ───────────────────────────────────────────────────────────────

RECOMMENDATION_RULES = [
    {
        "scene": "💰 金融/理财/基金/股票",
        "keywords": ["基金", "股票", "理财", "投资", "汇率", "黄金", "加密", "比特币",
                     "BTC", "ETF", "定投", "净值", "A股", "港股", "美股", "财经",
                     "fund", "stock", "crypto", "forex", "finance"],
        "search_query": "finance",
    },
    {
        "scene": "🌐 翻译/多语言",
        "keywords": ["翻译", "英文", "日文", "韩文", "translate", "translation",
                     "中翻英", "英翻中", "多语言", "localization", "i18n"],
        "search_query": "translate",
    },
    {
        "scene": "📅 日历/日程/提醒",
        "keywords": ["日程", "会议", "日历", "提醒", "日程表", "calendar", "schedule",
                     "meeting", "reminder", "提醒我", "几点"],
        "search_query": "calendar",
    },
    {
        "scene": "📊 数据分析/可视化",
        "keywords": ["数据分析", "图表", "可视化", "统计", "报表", "data analysis",
                     "visualization", "chart", "dashboard", "数据透视", "CSV分析"],
        "search_query": "data analysis",
    },
    {
        "scene": "🔐 密码/凭证管理",
        "keywords": ["密码", "密码管理", "凭证", "credential", "password", "API key",
                     "密钥", "token管理"],
        "search_query": "password",
    },
    {
        "scene": "📝 任务管理/待办",
        "keywords": ["待办", "TODO", "todo list", "任务列表",
                     "提醒我做什么", "清单", "checklist"],
        "search_query": "todo task",
    },
    {
        "scene": "🔗 API/接口测试",
        "keywords": ["API测试", "接口测试", "curl", "HTTP请求", "接口调试",
                     "API debug", "postman", "rest api", "endpoint"],
        "search_query": "api test",
    },
    {
        "scene": "📁 文件管理/整理",
        "keywords": ["文件整理", "文件管理", "重命名", "批量", "归类", "文件分类",
                     "organize", "file management", "批量处理"],
        "search_query": "file organize",
    },
    {
        "scene": "🤖 AI提示词优化",
        "keywords": ["提示词", "prompt", "提示工程", "提示优化", "system prompt",
                     "prompt engineering", "提示词模板"],
        "search_query": "prompt engineering",
    },
    {
        "scene": "📰 RSS/信息订阅",
        "keywords": ["RSS", "订阅", "信息源", "feed", "博客更新", "newsletter"],
        "search_query": "rss",
    },
    {
        "scene": "🖼️ 截图/录屏",
        "keywords": ["截图", "录屏", "屏幕录制", "screenshot", "screen recording",
                     "屏幕截图", "屏幕抓取"],
        "search_query": "screenshot",
    },
    {
        "scene": "🧹 系统/环境清理",
        "keywords": ["清理", "磁盘", "缓存", "垃圾文件", "内存", "系统优化",
                     "cleanup", "disk", "cache", "storage"],
        "search_query": "system cleanup",
    },
]


# ─── 工具函数 ───────────────────────────────────────────────────────────────

def run_cmd(cmd: str, timeout: int = 30) -> str:
    """运行 shell 命令，返回 stdout"""
    try:
        result = subprocess.run(
            ["/bin/sh", "-c", cmd],
            capture_output=True, text=True, timeout=timeout
        )
        return result.stdout.strip()
    except subprocess.TimeoutExpired:
        return ""
    except Exception:
        return ""


def find_skill_dirs() -> list[tuple[str, Path]]:
    """发现所有 Skill 目录，返回 [(source, path), ...]"""
    dirs = []
    for source, base in SKILL_PATHS.items():
        if not base.is_dir():
            continue
        for entry in sorted(base.iterdir()):
            skill_md = entry / "SKILL.md"
            if skill_md.is_file():
                dirs.append((source, entry))
    return dirs


def parse_skill_md(path: Path) -> dict:
    """解析 SKILL.md，提取 name、description 等元信息"""
    try:
        text = path.read_text(encoding="utf-8", errors="replace")
    except Exception:
        return {}

    info = {"name": path.parent.name, "description": "", "path": str(path)}

    # 解析 YAML frontmatter
    if text.startswith("---"):
        parts = text.split("---", 2)
        if len(parts) >= 3:
            frontmatter = parts[1].strip()
            m = re.search(r'^name:\s*(.+)$', frontmatter, re.MULTILINE)
            if m:
                info["name"] = m.group(1).strip().strip('"\'')
            m = re.search(r'^description:\s*(.+)$', frontmatter, re.MULTILINE)
            if m:
                info["description"] = m.group(1).strip().strip('"\'')

    # 如果 frontmatter 没有提取到 description，从第一段非空内容取
    if not info["description"]:
        for line in text.split("\n"):
            line = line.strip()
            if line and not line.startswith("#") and not line.startswith("---"):
                info["description"] = line[:200]
                break

    return info


def tokenize(text: str) -> set[str]:
    """文本分词，返回小写词集合"""
    text = text.lower()
    words = re.split(r'[\s\-_/|,.:;!?()\[\]{}]+', text)
    return {w for w in words if len(w) >= 2}


def build_skill_keywords(skill_info: dict) -> list[str]:
    """
    为单个 Skill 构建搜索关键词列表。

    优先级：精确 name > name 分词 > description 关键词
    """
    keywords = []
    name = skill_info.get("name", "").lower()
    desc = skill_info.get("description", "").lower()

    # 1. 精确 name（最高优先级）
    if name:
        keywords.append(name)

    # 2. name 的各部分（按 - _ 分割）
    name_parts = re.split(r'[-_]+', name)
    for part in name_parts:
        if len(part) >= 2 and part not in GENERIC_KEYWORDS:
            keywords.append(part)

    # 3. 从 description 提取关键词
    # 移除标点，分词
    desc_words = re.split(r'[\s\-_/|,.:;!?()\[\]{}·、，。；：！？（）【】]+', desc)
    for w in desc_words:
        w = w.strip().lower()
        # 过滤：至少3个字符，不在通用词列表中，不是纯数字
        if len(w) >= 3 and w not in GENERIC_KEYWORDS and not w.isdigit():
            keywords.append(w)

    # 4. 中文变体 / 常见别名（手动映射部分高频 Skill）
    name_aliases = {
        "tavily-search": ["tavily", "tavily search"],
        "baidu-search": ["百度", "百度搜索", "baidu"],
        "edge-tts": ["edge-tts", "edge tts", "微软tts"],
        "feishu-edge-tts-voice": ["语音消息", "语音回复", "语音发送", "晓晓", "云希", "飞书语音"],
        "feishu-voice": ["语音消息", "语音回复", "飞书语音"],
        "openai-whisper": ["whisper", "语音识别", "stt", "语音转文字"],
        "apple-notes": ["apple notes", "苹果笔记", "备忘录"],
        "feishu-doc": ["飞书文档", "飞书doc"],
        "feishu-wiki": ["飞书知识库", "知识库"],
        "feishu-drive": ["飞书云空间", "云空间", "飞盘"],
        "nano-pdf": ["pdf编辑", "编辑pdf"],
        "qwen-image": ["通义万相", "图片生成", "qwen image"],
        "alicloud-ai-image-qwen-image-edit": ["图片编辑", "图像编辑", "inpaint"],
        "weather": ["天气", "weather"],
        "summarize": ["总结", "摘要", "summarize", "总结工具"],
        "video-frames": ["视频帧", "视频截图", "ffmpeg"],
        "image-text-editor": ["图片文字", "修复错别字", "图片修字"],
        "markdown-converter": ["markitdown", "格式转换", "文档转换"],
        "find-skills": ["查找skill", "发现skill", "skill搜索"],
        "clawhub": ["clawhub", "skill安装", "skill管理"],
        "coding-agent": ["codex", "claude code", "编程代理"],
        "gh-issues": ["github issue", "issue处理"],
        "session-logs": ["会话日志", "session日志"],
        "tmux": ["tmux", "终端管理"],
        "humanize-ai-text": ["降ai率", "人性化", "ai检测"],
        "ai-news-zh": ["ai日报", "ai新闻", "科技日报"],
        "proactive-agent-lite": ["主动代理", "proactive"],
        "mem0": ["mem0", "记忆系统", "向量记忆"],
        "healthcheck": ["健康检查", "安全审计"],
        "node-connect": ["节点连接", "设备配对", "node连接"],
        "auto-updater": ["自动更新", "skill更新"],
        "agentic-coding": ["agentic coding", "代理编程"],
        "gog": ["google workspace", "g suite", "gmail cli"],
        "skill-creator": ["skill创建", "skill开发", "创建skill"],
        "code-review": ["代码审查", "code review", "pr审查"],
    }

    if name in name_aliases:
        for alias in name_aliases[name]:
            if alias not in keywords:
                keywords.append(alias)

    # 去重但保持顺序（靠前的优先级更高）
    seen = set()
    result = []
    for kw in keywords:
        if kw not in seen:
            seen.add(kw)
            result.append(kw)

    return result


def read_file_safe(path: Path) -> str:
    """安全读取文件内容"""
    try:
        if path.is_file():
            return path.read_text(encoding="utf-8", errors="replace")
    except Exception:
        pass
    return ""


def count_keyword_hits(text: str, keywords: list[str]) -> dict:
    """
    在文本中统计关键词命中次数。

    keywords[0] 是 skill name（精确匹配），其余是 description 关键词。
    返回 {"name_hits": int, "keyword_hits": int, "name_details": [...], "keyword_details": [...]}
    """
    text_lower = text.lower()
    name_hits = 0
    keyword_hits = 0
    name_details = []
    keyword_details = []

    for i, kw in enumerate(keywords):
        kw_lower = kw.lower()
        if not kw_lower or len(kw_lower) < 2:
            continue
        count = text_lower.count(kw_lower)
        if count > 0:
            if i == 0:
                # 精确 name 匹配
                name_hits += count
                name_details.append((kw, count))
            else:
                # description 关键词匹配
                keyword_hits += count
                keyword_details.append((kw, count))

    return {
        "name_hits": name_hits,
        "keyword_hits": keyword_hits,
        "total": name_hits + keyword_hits,
        "name_details": name_details,
        "keyword_details": keyword_details,
    }


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