Skill: leeshunee/searxng-search-cli (version 1.2.1)

--- name: searxng-search-cli displayName: "SearXNG Search CLI (Free, Self-hosted, Auto-deploy, Multi-Channel)" version: 1.2.1 description: | Use self-hosted SearXNG search engine (Free, Self-hosted, Auto-deploy, Multi-Channel). SearXNG is a free meta search engine that aggregates 200+ search engines (Google, Bing, Brave, GitHub, etc.), completely free and self-hostable. Trigger: User needs to search queries, research information, find resources, etc. --- # SearXNG-CLI (Free, Self-hosted, Search Engine Aggregator) | SearXNG CLI（免费、自托管、搜索引擎聚合器） - **Author**: [LeeShunEE](https://github.com/LeeShunEE) - **Organization**: [KinemaClawWorkspace](https://github.com/KinemaClawWorkspace) - **GitHub**: https://github.com/KinemaClawWorkspace/searxng-search-cli Use SearXNG self-hosted search API for fast, accurate searching. 使用 SearXNG 自托管搜索 API 进行快速、准确的搜索。 ## ⚠️ Before First Use | 首次使用必读 **首次使用此 skill 前，必须先读取 [ONBOARDING.md](ONBOARDING.md) 完成环境配置。** - **首次配置** → 读取 ONBOARDING.md 完成全部步骤 - **环境不可用**（命令不存在、依赖缺失、搜索失败）→ 读取 ONBOARDING.md Troubleshooting 排查修复 - **配置完成后** → 直接使用下方 Run Commands ## Run Commands | 使用命令 ```bash # Search | 搜索 searxng-search search "your query" # Specify engine | 指定引擎 searxng-search search "git clone" --engine github # Specify language | 指定语言 searxng-search search "AI News" --lang zh # Pagination | 分页 searxng-search search "llm" --page 2 # Time filter | 时间过滤 searxng-search search "python" --time-range month ``` ## Command List | 命令列表 | Command | Description | 说明 | |---------|-------------|------| | install | One-click install SearXNG | 一键安装 SearXNG | | start | Start service | 启动服务 | | stop | Stop service | 停止服务 | | restart | Restart service | 重启服务 | | status | Check service status | 查看服务状态 | | search \<query\> | Search | 搜索 | | enable | Enable auto-start | 开机自启 | | disable | Disable auto-start | 取消开机自启 | ## Configuration | 配置 - `SEARXNG_PORT` - Port (default 8888) | 端口 (默认 8888) - `SEARXNG_HOST` - Bind address (default 127.0.0.1) | 绑定地址 (默认 127.0.0.1) - `SEARXNG_SECRET` - Auth key (auto-generated if not set) | 认证密钥 (自动生成) ## Search Parameters | 搜索参数 | Parameter | Short | Description | Example | |-----------|-------|-------------|---------| | --engine | -e | Specify engine | github, google | | --lang | -l | Language | zh, en, auto | | --page | -p | Page number | 1, 2, 3 | | --time-range | -t | Time range | day, week, month, year | | --safe-search | -s | Safe search | 0, 1, 2 | | --limit | | Max results (default 5) | 10 | ### Available Engines | 可用引擎 General: google, bing, brave, duckduckgo, yandex, startpage, qwant Code/Dev: github, gitlab, stackoverflow, npm, pypi Academic: arxiv, pubmed, wikipedia, google-scholar Video/Image: youtube, vimeo, pexels, pixabay ## Known Limitations | 已知限制 - **部分引擎不稳定**：DuckDuckGo 可能触发 CAPTCHA、Brave 可能 403，属上游外部限制 - **首次搜索较慢**：可能需要 5-30 秒，后续请求会加速 - **推荐部署在有稳定网络出口的宿主机上** ## Related Documentation | 相关文档 - [SearXNG Official Docs](https://docs.searxng.org) | [SearXNG 官方文档](https://docs.searxng.org) - [GitHub](https://github.com/searxng/searx)

path: ONBOARDING.md
content: # SearXNG Search CLI Onboarding

> 本文档指导 AI Agent 完成首次环境配置。按顺序执行，遇到问题时参考 Troubleshooting。

## Prerequisites | 前置条件

- Python 3.x（运行 `python3 --version` 确认）
- 网络（需克隆 SearXNG 仓库，搜索时需访问外部搜索引擎）
- 可用端口（默认 8888）

## Step 1: 检查 Python 环境

### 检测
```bash
python3 --version
```
期望输出：`Python 3.x.x`

如果不存在，尝试：
```bash
python --version
which python3 || which python
```

如果完全没有 Python，告知用户需要安装 Python 3。

## Step 2: 安装 uv

### 检测
```bash
command -v uv && uv --version
```

如果已安装，跳到 Step 3。

### 安装
```bash
# 尝试 1: curl
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"

# 尝试 2: wget
wget -qO- https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"

# 尝试 3: 如果 PATH 未生效
export PATH="$HOME/.local/bin:$PATH"
```

### 验证
```bash
uv --version
```

## Step 3: 一键安装 SearXNG

### 检测
```bash
searxng-search status
```

如果显示 `✓ 服务运行中`，跳到 Step 5。

### 安装
```bash
# 确保脚本可执行
chmod +x <skill_dir>/scripts/searxng_cli.py

# 创建 symlink（如果不存在）
which searxng-search || sudo ln -sf <skill_dir>/scripts/searxng_cli.py /usr/local/bin/searxng-search

# 设置 secret（安装时会自动生成，也可预设置）
export SEARXNG_SECRET="your-secret-key"

# 一键安装（自动完成：克隆、venv、依赖、JSON启用、limiter配置、启动）
searxng-search install
```

> **install 自动执行以下操作：**
> 1. 克隆 SearXNG 到 `~/projects/searxng`
> 2. `uv venv --clear` 创建虚拟环境
> 3. `uv pip install -r requirements.txt` 安装依赖
> 4. 启用 JSON API（修改 `settings.yml`）
> 5. 复制 `limiter.toml` 到 `/etc/searxng/`（需要 sudo 或目录已存在）
> 6. 生成 secret 并启动服务

### 验证
```bash
searxng-search status
```
期望输出：`✓ 服务运行中: http://127.0.0.1:8888`

## Step 4: 验证搜索功能

### 验证
```bash
searxng-search search "test"
```
期望输出：搜索结果列表（标题 + URL + 摘要）

```bash
curl -s -H "X-Forwarded-For: 127.0.0.1" "http://127.0.0.1:8888/search?q=test&format=json" | python3 -m json.tool | head -10
```
期望输出：有效 JSON（`"query": "test"` + `"results": [...]`）

## Onboarding 完成

以上步骤全部通过后，onboarding 完成。后续可直接使用 `searxng-search` 命令。

## Known Limitations | 已知限制

- **部分引擎不稳定**：DuckDuckGo 可能触发 CAPTCHA、Brave 可能返回 403、Startpage 可能 JSON 解析失败。这是 SearXNG 上游和各搜索引擎的外部限制，非 CLI 问题
- **首次搜索较慢**：首次请求可能需要 5-30 秒（引擎初始化、DNS 解析），后续请求会加速
- **容器环境**：在资源受限的容器中可能启动超时，推荐部署在有稳定网络出口的宿主机上

## Troubleshooting | 故障排除

| 错误 | 原因 | 解决 |
|------|------|------|
| `command not found: searxng-search` | symlink 未创建 | `sudo ln -sf <skill_dir>/scripts/searxng_cli.py /usr/local/bin/searxng-search` |
| `command not found: uv` | uv 未安装或 PATH 未生效 | Step 2 重新安装，`export PATH="$HOME/.local/bin:$PATH"` |
| `TypeError: schema of limiter.toml is invalid` | `/etc/searxng/limiter.toml` 格式错误或为空 | `sudo cp ~/projects/searxng/searx/limiter.toml /etc/searxng/limiter.toml` |
| `403 Forbidden` (API 请求) | Bot detection 拦截（缺少 `X-Forwarded-For` header） | 确保 limiter.toml 存在且格式正确（使用源码默认文件）；CLI 已内置 header 处理 |
| JSON API 返回 HTML 而非 JSON | `settings.yml` 未启用 json format | 手动检查 `~/projects/searxng/searx/settings.yml` 的 `formats` 是否包含 `- json`，或重新运行 `searxng-search install` |
| `connection refused` | 服务未启动 | `searxng-search start` |
| `启动失败` (install/start 超时) | 服务启动慢或端口被占用 | 等待 20-30s 后重试 `searxng-search status`；检查端口：`ss -tlnp | grep 8888` |
| 搜索返回空结果 | 所有引擎被封/超时 | 部分引擎被封属正常现象，尝试换关键词或指定引擎 `--engine brave` |
| `No module named 'xxx'` | venv 依赖缺失 | `rm -rf ~/projects/searxng/.venv && searxng-search install` |
| `Permission denied: /home/node/projects` | 目录无写入权限 | `sudo mkdir -p ~/projects && sudo chown $(whoami) ~/projects` |
| `pip: not found` (旧版 install) | uv venv 不自带 pip | 确保使用最新版 CLI 脚本（v1.2.0+，已改用 `uv pip install`） |
| 端口 8888 被占用 | 其他服务占用 | `SEARXNG_PORT=9999 searxng-search start` |


path: README.md
content: # SearXNG Search CLI

> **ClawHub**: https://clawhub.ai/leeshunee/searxng-search-cli | `clawhub install searxng-search-cli`


使用 SearXNG 自托管搜索 API 进行快速、准确的搜索。

## 安装

```bash
# 1. 克隆仓库
git clone https://github.com/KinemaClawWorkspace/searxng-search-cli.git
cd searxng-search-cli

# 2. 安装依赖（脚本使用 Python 标准库，无需额外依赖）
```

## 配置

```bash
# 环境变量（可选，有默认值）
export SEARXNG_HOST="127.0.0.1"   # 绑定地址 (默认 127.0.0.1)
export SEARXNG_PORT="8888"          # 端口 (默认 8888)
export SEARXNG_SECRET="your-secret" # 认证密钥 (install 时自动生成)
```

## 一键安装 SearXNG

```bash
python scripts/searxng_cli.py install
```

自动完成：安装 uv → 克隆 SearXNG → 创建虚拟环境 → 安装依赖 → 启用 JSON API → 启动服务

## 使用

```bash
# 搜索
python scripts/searxng_cli.py search "Python Tutorial"
python scripts/searxng_cli.py search "git clone" --engine github

# 服务管理
python scripts/searxng_cli.py start
python scripts/searxng_cli.py stop
python scripts/searxng_cli.py status
```

## 命令列表

| Command | Description |
|---------|-------------|
| install | 一键安装 SearXNG |
| start / stop / restart | 服务管理 |
| status | 查看状态 |
| search \<query\> | 搜索 |
| enable / disable | 开机自启 |

## 参数说明

| 参数 | 说明 | 示例 |
|------|------|------|
| --engine, -e | 指定引擎 | github, google |
| --lang, -l | 语言 | zh, en |
| --page, -p | 分页 | 1, 2 |
| --time-range, -t | 时间过滤 | day, week, month |

## 项目结构

```
searxng-search-cli/
├── README.md
├── SKILL.md
├── LICENSE
└── scripts/
    └── searxng_cli.py
```

## 许可证

CC BY 4.0


path: scripts/searxng_cli.py
content: #!/usr/bin/env python3
"""
SearXNG CLI for OpenClaw
Usage: searxng-search <command> [options]
"""

import argparse
import json
import os
import re
import subprocess
import sys
import urllib.request
from pathlib import Path

# 配置
SEARXNG_DIR = Path.home() / "projects" / "searxng"
SEARXNG_PORT = os.environ.get("SEARXNG_PORT", "8888")
SEARXNG_HOST = os.environ.get("SEARXNG_HOST", "127.0.0.1")
SEARXNG_SECRET = os.environ.get("SEARXNG_SECRET", "")

# Bot detection bypass: 127.0.0.1 is in trusted_proxies by default
FORWARDED_FOR = {"X-Forwarded-For": "127.0.0.1"}


def log(msg):
    print(f"[searxng] {msg}", file=sys.stderr)


def run(cmd, check=True, cwd=None):
    result = subprocess.run(cmd, shell=True, capture_output=True, text=True, cwd=cwd)
    if check and result.returncode != 0:
        log(f"Error: {result.stderr}")
        sys.exit(1)
    return result


def is_installed():
    return SEARXNG_DIR.exists() and (SEARXNG_DIR / ".venv").exists()


def is_running():
    # Try multiple times with short timeout (service may be slow to respond)
    for _ in range(3):
        try:
            req = urllib.request.Request(
                f"http://{SEARXNG_HOST}:{SEARXNG_PORT}",
                headers=FORWARDED_FOR
            )
            resp = urllib.request.urlopen(req, timeout=3)
            return resp.status == 200
        except:
            import time
            time.sleep(1)
    return False


def _ensure_settings():
    """Ensure JSON format is enabled and limiter.toml is configured."""
    settings_file = SEARXNG_DIR / "searx" / "settings.yml"
    limiter_src = SEARXNG_DIR / "searx" / "limiter.toml"
    limiter_dst = Path("/etc/searxng/limiter.toml")

    # Enable JSON format
    if settings_file.exists():
        with open(settings_file) as f:
            content = f.read()
        # Check if json is in formats list
        if "formats:" in content:
            formats_section = content.split("formats:")[1].split("\n")[0:5]
            formats_text = "\n".join(formats_section)
            if "- json" not in formats_text:
                # Replace formats section
                import re
                content = re.sub(
                    r"formats:\n(\s+-\s+\w+\n)*",
                    "formats:\n    - html\n    - json\n    - csv\n    - rss\n",
                    content
                )
                with open(settings_file, "w") as f:
                    f.write(content)
                log("已启用 JSON API")

    # Copy limiter.toml if missing
    if not limiter_dst.exists() and limiter_src.exists():
        limiter_dst.parent.mkdir(parents=True, exist_ok=True)
        try:
            import shutil
            shutil.copy(limiter_src, limiter_dst)
            log("已配置 limiter.toml")
        except PermissionError:
            log(f"⚠ 需要手动复制: sudo cp {limiter_src} {limiter_dst}")


def cmd_install(args):
    """一键安装 SearXNG"""
    log("开始安装 SearXNG...")

    # 1. 安装 uv
    if not subprocess.run("which uv", shell=True, capture_output=True).returncode == 0:
        log("安装 uv...")
        run('curl -LsSf https://astral.sh/uv/install.sh | sh')
        uv_path = Path.home() / ".local" / "bin" / "uv"
        if uv_path.exists():
            os.environ["PATH"] = f"{uv_path.parent}:{os.environ['PATH']}"

    # 2. 克隆 SearXNG
    if not SEARXNG_DIR.exists():
        log("克隆 SearXNG...")
        SEARXNG_DIR.parent.mkdir(parents=True, exist_ok=True)
        run(f"git clone --depth 1 https://github.com/searxng/searxng.git {SEARXNG_DIR}")

    # 3. 创建虚拟环境 + 安装依赖
    venv_dir = SEARXNG_DIR / ".venv"
    log("创建虚拟环境 + 安装依赖...")
    run(f"cd {SEARXNG_DIR} && uv venv .venv --clear")
    run(f"cd {SEARXNG_DIR} && uv pip install -r requirements.txt")

    # 4. 配置 settings.yml（启用 JSON）和 limiter.toml
    log("配置 SearXNG...")
    _ensure_settings()

    # 5. 生成 secret
    if not SEARXNG_SECRET:
        [REDACTED_SECRET]
            "python3 -c 'import secrets; print(secrets.token_hex(32))'",
            shell=True, capture_output=True, text=True
        ).stdout.strip()
        os.environ["SEARXNG_SECRET"] = secret
        log(f"生成的密钥: {secret[:16]}...")

    # 6. 启动服务
    cmd_start(args)

    log("安装完成！")


def cmd_start(args):
    """启动服务"""
    if is_running():
        log("服务已在运行")
        return

    log("启动服务...")
    env = os.environ.copy()
    env["SEARXNG_SECRET"] = SEARXNG_SECRET or env.get("SEARXNG_SECRET", "devsecret")

    cmd = (
        f'cd {SEARXNG_DIR} && '
        f'SEARXNG_SECRET={env["SEARXNG_SECRET"]} '
        f'{SEARXNG_DIR}/.venv/bin/python -m searx.webapp '
        f'--host {SEARXNG_HOST} --port {SEARXNG_PORT}'
    )
    subprocess.Popen(cmd, shell=True, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    import time
    for _ in range(15):
        time.sleep(1)
        if is_running():
            log(f"服务已启动: http://{SEARXNG_HOST}:{SEARXNG_PORT}")
            return

    log("启动失败，请检查日志")


def cmd_stop(args):
    """停止服务"""
    log("停止服务...")
    run("pkill -f 'searx.webapp'", check=False)
    log("服务已停止")


def cmd_restart(args):
    """重启服务"""
    cmd_stop(args)
    cmd_start(args)


def cmd_status(args):
    """查看状态"""
    if is_running():
        print(f"✓ 服务运行中: http://{SEARXNG_HOST}:{SEARXNG_PORT}")
    else:
        print("✗ 服务未运行")


def cmd_enable(args):
    """开机自启"""
    service_file = Path.home() / ".config" / "systemd" / "user" / "searxng.service"
    service_file.parent.mkdir(parents=True, exist_ok=True)

    [REDACTED_SECRET] or "devsecret"
    content = f"""[Unit]
Description=SearXNG Search Engine

[Service]
Type=simple
WorkingDirectory={SEARXNG_DIR}
ExecStart={SEARXNG_DIR}/.venv/bin/python -m searx.webapp --host {SEARXNG_HOST} --port {SEARXNG_PORT}
Environment=SEARXNG_SECRET={secret}
Restart=on-failure

[Install]
WantedBy=default.target
"""
    with open(service_file, "w") as f:
        f.write(content)

    run("systemctl --user daemon-reload", check=False)
    run("systemctl --user enable searxng", check=False)
    log("已启用开机自启")


def cmd_disable(args):
    """取消开机自启"""
    run("systemctl --user disable searxng", check=False)
    log("已取消开机自启")


def cmd_search(args):
    """搜索"""
    if not is_running():
        log("服务未运行，请先执行: searxng-search start")
        sys.exit(1)

    query = args.query
    params = {
        "q": query,
        "format": "json"
    }

    if args.engine:
        params["engines"] = args.engine
    if args.lang:
        params["lang"] = args.lang
    if args.page:
        params["pageno"] = args.page
    if args.time_range:
        params["time_range"] = args.time_range
    if args.safe_search:
        params["safesearch"] = args.safe_search

    from urllib.parse import urlencode
    url = f"http://{SEARXNG_HOST}:{SEARXNG_PORT}/search?{urlencode(params)}"

    try:
        req = urllib.request.Request(url, headers=FORWARDED_FOR)
        with urllib.request.urlopen(req, timeout=10) as resp:
            data = json.load(resp)

        results = data.get("results", [])
        if not results:
            print("未找到结果")
            return

        for r in results[:args.limit]:
            title = r.get("title", "")
            r_url = r.get("url", "")
            content = r.get("content", "")[:100]
            print(f"【{title}】")
            print(f"{r_url}")
            print(f"{content}...")
            print()
    except Exception as e:
        log(f"搜索失败: {e}")
        sys.exit(1)


def main():
    parser = argparse.ArgumentParser(prog="searxng-search")
    subparsers = parser.add_subparsers(dest="command", help="可用命令")

    subparsers.add_parser("install", help="一键安装 SearXNG")
    subparsers.add_parser("start", help="启动服务")
    subparsers.add_parser("stop", help="停止服务")
    subparsers.add_parser("restart", help="重启服务")
    subparsers.add_parser("status", help="查看服务状态")
    subparsers.add_parser("enable", help="开机自启")
    subparsers.add_parser("disable", help="取消开机自启")

    search_parser = subparsers.add_parser("search", help="搜索")
    search_parser.add_argument("query", help="搜索关键词")
    search_parser.add_argument("-e", "--engine", help="指定引擎")
    search_parser.add_argument("-l", "--lang", help="语言")
    search_parser.add_argument("-p", "--page", type=int, help="页码")
    search_parser.add_argument("-t", "--time-range", help="时间范围")
    search_parser.add_argument("-s", "--safe-search", help="安全搜索")
    search_parser.add_argument("--limit", type=int, default=5, help="结果数量")

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    {
        "install": cmd_install,
        "start": cmd_start,
        "stop": cmd_stop,
        "restart": cmd_restart,
        "status": cmd_status,
        "enable": cmd_enable,
        "disable": cmd_disable,
        "search": cmd_search,
    }[args.command](args)


if __name__ == "__main__":
    main()


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