Skill: triwinds/searxng-docker (version 1.0.0)

--- name: searxng description: Search the web using a local SearXNG instance (privacy-respecting metasearch engine). Use when user asks to search, look something up, "搜索", "搜一下", "查一下", or when web_search returns poor results. Supports categories (news, images, videos, IT, science), time filters, language, and multiple engines (Google, Bing, DuckDuckGo, etc.). No API key needed — fully self-hosted. --- # SearXNG Search Skill Search the web using a self-hosted SearXNG instance. ## When to Use - User asks to search, look something up: "搜索", "搜一下", "查一下", "search for", "look up" - Need results from multiple engines (Google, Bing, DuckDuckGo, etc.) - Need news, images, IT/science, or social media search - `web_search` unavailable or returns poor results ## Deployment ### Quick Start (Docker Compose) The `docker/` folder in this skill contains a ready-to-use Docker Compose setup. Run it directly — no need to copy files elsewhere. ```bash # 1. Generate a random secret key sed -i "s/CHANGE_ME_TO_A_RANDOM_STRING/$(openssl rand -hex 16)/" docker/settings.yml # 2. Start docker compose -f docker/docker-compose.yml up -d # 3. Verify curl -s "http://127.0.0.1:8888/search?q=test&format=json" | python3 -m json.tool | head -5 ``` ### Configuration Edit files in the `docker/` folder: - **Port**: Default `127.0.0.1:8888` → change in `docker-compose.yml` ports section - **Engines**: Edit `settings.yml` engines list (Google, Bing, DuckDuckGo, Wikipedia, GitHub enabled by default) - **Language**: `default_lang` in `settings.yml` (default: `auto`) ### Troubleshooting ```bash # Check if running docker ps | grep searxng # Restart docker compose -f docker/docker-compose.yml restart # View logs docker logs searxng --tail 50 ``` ## Usage All commands use the script at `scripts/searxng_search.py` (relative to this skill directory). The script defaults to `http://127.0.0.1:8888` but accepts `--base-url` to point elsewhere. ```bash # Basic search python3 scripts/searxng_search.py "your query" # Number of results python3 scripts/searxng_search.py "your query" -n 5 # Language python3 scripts/searxng_search.py "your query" -l zh # Chinese python3 scripts/searxng_search.py "your query" -l en # English # Category python3 scripts/searxng_search.py "your query" -c news python3 scripts/searxng_search.py "your query" -c images python3 scripts/searxng_search.py "your query" -c it python3 scripts/searxng_search.py "your query" -c science # Time filter python3 scripts/searxng_search.py "your query" -t day python3 scripts/searxng_search.py "your query" -t week python3 scripts/searxng_search.py "your query" -t month # Specific engines python3 scripts/searxng_search.py "your query" -e google,bing # JSON output python3 scripts/searxng_search.py "your query" --json # Custom SearXNG URL python3 scripts/searxng_search.py "your query" --base-url http://192.168.1.100:8888 # Combined python3 scripts/searxng_search.py "最新科技新闻" -c news -l zh -t week -n 5 ``` ## Categories | Category | Description | |----------|-------------| | `general` | Web search (default) | | `news` | News articles | | `images` | Image search | | `videos` | Video search | | `it` | IT / programming | | `science` | Scientific articles | | `files` | File search | | `social media` | Social media posts | ## Notes - SearXNG aggregates and deduplicates results from multiple engines - The `score` field indicates cross-engine ranking confidence - No API key needed — fully self-hosted and private

path: docker/docker-compose.yml
content: version: '3.8'

services:
  searxng:
    image: searxng/searxng:latest
    container_name: searxng
    restart: unless-stopped
    ports:
      - "127.0.0.1:8888:8080"
    volumes:
      - ./settings.yml:/etc/searxng/settings.yml:ro
    environment:
      - SEARXNG_BASE_URL=http://localhost:8888/
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETGID
      - SETUID


path: docker/settings.yml
content: use_default_settings: true

general:
  instance_name: "SearXNG Local"
  debug: false

search:
  safe_search: 0
  autocomplete: ""
  default_lang: "auto"
  formats:
    - html
    - json

server:
  secret_key: "CHANGE_ME_TO_A_RANDOM_STRING"
  bind_address: "0.0.0.0"
  port: 8080
  limiter: false
  image_proxy: true

engines:
  - name: google
    engine: google
    shortcut: g
    disabled: false
  - name: bing
    engine: bing
    shortcut: b
    disabled: false
  - name: duckduckgo
    engine: duckduckgo
    shortcut: ddg
    disabled: false
  - name: wikipedia
    engine: wikipedia
    shortcut: wp
    disabled: false
  - name: github
    engine: github
    shortcut: gh
    disabled: false

outgoing:
  request_timeout: 10
  max_request_timeout: 15


path: scripts/searxng_search.py
content: #!/usr/bin/env python3
"""SearXNG search client - query a local SearXNG instance and return results."""

import argparse
import json
import sys
import urllib.request
import urllib.parse
import urllib.error

DEFAULT_BASE_URL = "http://127.0.0.1:8888"
DEFAULT_NUM_RESULTS = 10


def search(
    query: str,
    base_url: str = DEFAULT_BASE_URL,
    categories: str = "general",
    language: str = "auto",
    time_range: str = "",
    num_results: int = DEFAULT_NUM_RESULTS,
    engines: str = "",
    output_json: bool = False,
) -> str:
    """Perform a search via SearXNG JSON API."""
    params = {
        "q": query,
        "format": "json",
        "categories": categories,
        "language": language,
    }
    if time_range:
        params["time_range"] = time_range
    if engines:
        params["engines"] = engines

    url = f"{base_url.rstrip('/')}/search?{urllib.parse.urlencode(params)}"

    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
            data = json.loads(resp.read().decode())
    except urllib.error.HTTPError as e:
        body = e.read().decode() if e.fp else ""
        print(f"Error: HTTP {e.code} from SearXNG: {body[:500]}", file=sys.stderr)
        sys.exit(1)
    except urllib.error.URLError as e:
        print(f"Error: cannot reach SearXNG at {base_url}: {e.reason}", file=sys.stderr)
        sys.exit(1)

    results = data.get("results", [])[:num_results]

    if output_json:
        compact = []
        for r in results:
            compact.append({
                "title": r.get("title", ""),
                "url": r.get("url", ""),
                "content": r.get("content", ""),
                "engines": r.get("engines", []),
                "score": r.get("score", 0),
                "publishedDate": r.get("publishedDate"),
            })
        return json.dumps(compact, ensure_ascii=False, indent=2)

    # Human-readable output
    lines = []
    lines.append(f"搜索: {query}")
    lines.append(f"结果数: {data.get('number_of_results', len(results))}")
    lines.append("")

    for i, r in enumerate(results, 1):
        title = r.get("title", "(no title)")
        url = r.get("url", "")
        content = r.get("content", "")
        engines_list = ", ".join(r.get("engines", []))
        score = r.get("score", 0)

        lines.append(f"{i}. {title}")
        lines.append(f"   {url}")
        if content:
            # Truncate long snippets
            snippet = content[:300] + ("..." if len(content) > 300 else "")
            lines.append(f"   {snippet}")
        lines.append(f"   [来源: {engines_list} | 评分: {score}]")
        lines.append("")

    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="Search via local SearXNG instance")
    parser.add_argument("query", nargs="?", help="Search query")
    parser.add_argument("-q", "--query-flag", dest="query_flag", help="Search query (alternative)")
    parser.add_argument("--base-url", default=DEFAULT_BASE_URL, help=f"SearXNG base URL (default: {DEFAULT_BASE_URL})")
    parser.add_argument("-n", "--num", type=int, default=DEFAULT_NUM_RESULTS, help=f"Number of results (default: {DEFAULT_NUM_RESULTS})")
    parser.add_argument("-c", "--categories", default="general", help="Search categories: general, images, news, videos, it, science, files, social media (default: general)")
    parser.add_argument("-l", "--language", default="auto", help="Language code: auto, zh, en, ja, etc. (default: auto)")
    parser.add_argument("-t", "--time-range", default="", help="Time range: day, week, month, year (default: none)")
    parser.add_argument("-e", "--engines", default="", help="Comma-separated engine names: google, bing, duckduckgo, wikipedia, etc.")
    parser.add_argument("--json", action="store_true", help="Output as JSON")

    args = parser.parse_args()
    query = args.query or args.query_flag
    if not query:
        parser.error("Search query is required")

    result = search(
        query=query,
        base_url=args.base_url,
        categories=args.categories,
        language=args.language,
        time_range=args.time_range,
        num_results=args.num,
        engines=args.engines,
        output_json=args.json,
    )
    print(result)


if __name__ == "__main__":
    main()


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