Skill: jixsonwang/wechat-search (version 1.0.3)

--- name: wechat-search description: Search WeChat Official Account articles using OpenClaw's web search, Tavily API, and web fetch capabilities with compliance-focused design. --- # WeChat Search Skill Search for WeChat Official Account (微信公众号) articles using a compliant, three-layer approach that prioritizes legal search APIs and falls back to respectful web scraping when needed. ## Features - **Compliant Design**: Prioritizes legal search APIs, respects robots.txt and rate limits - **Three-Layer Strategy**: - Primary: OpenClaw web_search (Brave Search API) - Secondary: Tavily Search API (if Brave unavailable) - Fallback: Direct page fetching from WeChat search - **Recent Results**: Returns the 5 most recent articles by default (configurable) - **Time Filtering**: Support for date range and recency filters - **Multiple Output Formats**: Text, JSON, and markdown formats available ## Prerequisites - **OpenClaw Web Tools**: Requires `web_search`, `web_fetch` tools to be available - **API Keys** (optional but recommended): - Brave Search API Key (for primary search) - Tavily API Key (for secondary search, already configured in your environment) ## Usage ### Basic Search ```bash wechat-search "人工智能" ``` ### Advanced Options ```bash # Return 10 results instead of default 5 wechat-search "机器学习" --max-results 10 # Search within past week wechat-search "大模型" --past-week # Custom date range wechat-search "AI应用" --from 2026-01-01 --to 2026-02-01 # JSON output format wechat-search "开源AI" --output json # Force specific strategy wechat-search "最新技术" --strategy tavily_only ``` ## Configuration Create `~/.openclaw/wechat-search-config.json` to customize behavior: ```json { "defaultMaxResults": 5, "maxResultsLimit": 20, "requestDelayMs": 5000, "cacheDurationHours": 1, "userAgent": "OpenClaw-WeChat-Search-Bot/1.0 (+https://github.com/your-username/wechat-search-skill)" } ``` ## Search Strategy Details ### Layer 1: OpenClaw Web Search (Brave Search) - Uses Brave Search API with `site:mp.weixin.qq.com` filter - Fastest and most reliable when API key is configured - Respects search engine's indexing and ranking ### Layer 2: Tavily Search API - Activated when Brave Search is unavailable or fails - Uses Tavily's AI-powered search with WeChat site restriction - Provides high-quality, relevant results with good coverage ### Layer 3: Direct Web Fetch - Final fallback when both APIs are unavailable - Scrapes WeChat search results directly from搜狗微信搜索 - Implements proper delays and respects robots.txt - Parses HTML to extract article metadata ## Compliance & Ethics - **Respects robots.txt**: Checks and follows robots.txt directives - **Rate limiting**: Minimum 5-second delay between requests - **Transparent identification**: Clear User-Agent string identifying the bot - **Public content only**: Only accesses publicly available articles - **No data retention**: Does not store full article content, only metadata ## Error Handling - Automatic retry on network failures (up to 3 attempts) - Graceful fallback between all three search strategies - Clear error messages for debugging - Handles API key missing scenarios gracefully ## Future Enhancements - RSS feed integration support - Article content summarization - Author/subscription management - Enhanced filtering options This skill is designed to be both useful and responsible, providing access to valuable WeChat Official Account content while respecting platform rules and legal requirements.

path: README.md
content: # WeChat Search Skill

Search for WeChat Official Account (微信公众号) articles using a compliant, three-layer approach that prioritizes legal search APIs and falls back to respectful web scraping when needed.

## Features
- **Compliant Design**: Prioritizes legal search APIs, respects robots.txt and rate limits
- **Three-Layer Strategy**: 
  - Primary: OpenClaw web_search (Brave Search API)
  - Fallback 1: Tavily Search API (if Brave API unavailable)
  - Fallback 2: Direct web fetch from WeChat search results
- **Recent Results**: Returns the 5 most recent articles by default (configurable)
- **Time Filtering**: Support for date range and recency filters
- **Multiple Output Formats**: Text, JSON, and markdown formats available

## Prerequisites
- **OpenClaw Web Tools**: Requires `web_search` and `web_fetch` tools to be available
- **API Keys** (optional but recommended):
  - Brave Search API Key: Set via `openclaw configure --section web`
  - Tavily API Key: Set as `TAVILY_API_KEY` environment variable

## Usage

### Basic Search
```bash
wechat-search "人工智能"
```

### Advanced Options
```bash
# Return 10 results instead of default 5
wechat-search "机器学习" --max-results 10

# Search within past week
wechat-search "大模型" --past-week

# Custom date range
wechat-search "AI应用" --from 2026-01-01 --to 2026-02-01

# JSON output format
wechat-search "开源AI" --output json

# Force specific strategy
wechat-search "最新技术" --strategy tavily_only
```

## Configuration
Create `~/.openclaw/wechat-search-config.json` to customize behavior:

```json
{
  "defaultMaxResults": 5,
  "maxResultsLimit": 20,
  "requestDelayMs": 5000,
  "cacheDurationHours": 1,
  "userAgent": "OpenClaw-WeChat-Search-Bot/1.0 (+https://github.com/your-username/wechat-search-skill)"
}
```

## Search Strategy Details

### Layer 1: OpenClaw Web Search (Brave Search)
- Uses `web_search` tool with `site:mp.weixin.qq.com` filter
- Requires Brave Search API key
- Fastest and most reliable option

### Layer 2: Tavily Search API
- Uses Tavily API with `site:mp.weixin.qq.com` filter  
- Requires TAVILY_API_KEY environment variable
- Good alternative when Brave API is unavailable

### Layer 3: Direct Web Fetch
- Fetches results directly from WeChat search (sogou.com)
- Parses HTML to extract article information
- Slowest but always available as last resort

## Compliance & Ethics
- **Respects robots.txt**: Checks and follows robots.txt directives
- **Rate limiting**: Minimum 5-second delay between requests
- **Transparent identification**: Clear User-Agent string identifying the bot
- **Public content only**: Only accesses publicly available articles
- **No data retention**: Does not store full article content, only metadata

## Error Handling
- Automatic retry on network failures (up to 3 attempts)
- Graceful fallback between search strategies
- Clear error messages for debugging

## Future Enhancements
- RSS feed integration support
- Article content summarization
- Author/subscription management
- Enhanced filtering options

This skill is designed to be both useful and responsible, providing access to valuable WeChat Official Account content while respecting platform rules and legal requirements.

path: _meta.json
content: name: wechat-search
description: Search WeChat official account articles using OpenClaw's web search and web fetch capabilities with compliance-focused implementation.
version: 1.0.3

path: debug_test.py
content: #!/usr/bin/env python3

from wechat_search import WeChatSearch

searcher = WeChatSearch()
test_url = "https://mp.weixin.qq.com/s/abc123"
print(f"Testing URL: {test_url}")
result = searcher.is_valid_wechat_url(test_url)
print(f"Result: {result}")

# Test with the exact string from test
test_url2 = "https://mp.weixin.qq.com/s/abc123"
print(f"Testing URL2: {test_url2}")
result2 = searcher.is_valid_wechat_url(test_url2)
print(f"Result2: {result2}")

path: example-config.json
content: {
  "search": {
    "strategy": "web_search_first",
    "max_results": 5,
    "cache_duration_hours": 1,
    "request_delay_ms": 5000
  },
  "web_fetch": {
    "user_agent": "OpenClaw-WeChat-Search-Bot/1.0 (+https://github.com/jixsonwang/wechat-search-skill)",
    "timeout_seconds": 30,
    "respect_robots_txt": true
  }
}

path: requirements.txt
content: # Runtime dependencies
requests>=2.25.0

path: wechat_search.py
content: #!/usr/bin/env python3
"""
WeChat Search Skill - Search WeChat Official Account articles using Tavily Search API.
Returns the most recent 5 articles by default, with configurable options.

This skill uses Tavily Search API with site restriction to find WeChat articles.
It requires TAVILY_API_KEY environment variable to be set.
"""

import json
import sys
import os
import re
from urllib.parse import urlparse, parse_qs
import subprocess


class WeChatSearch:
    def __init__(self, config_path=None):
        self.config = self.load_config(config_path)
        
    def load_config(self, config_path):
        """Load configuration from file or use defaults"""
        default_config = {
            "max_results": 5,
            "cache_duration_hours": 1,
            "user_agent": "OpenClaw-WeChat-Search-Bot/1.0"
        }
        
        if config_path and os.path.exists(config_path):
            try:
                with open(config_path, 'r') as f:
                    user_config = json.load(f)
                    default_config.update(user_config)
            except Exception as e:
                print(f"Warning: Failed to load config file: {e}", file=sys.stderr)
                
        return default_config

    def is_valid_wechat_url(self, url):
        """Check if URL is a valid WeChat Official Account article"""
        try:
            parsed = urlparse(url)
            if 'mp.weixin.qq.com' not in parsed.netloc:
                return False
            # Valid WeChat URLs have /s in the path
            return '/s' in parsed.path
        except:
            return False

    def extract_account_from_url(self, url):
        """Extract official account name from WeChat URL"""
        try:
            parsed = urlparse(url)
            if 'mp.weixin.qq.com' in parsed.netloc:
                return "Unknown Account"
        except:
            return "Unknown Account"

    def format_results(self, articles, brief=False):
        """Format search results for display"""
        if not articles:
            return "No WeChat Official Account articles found for your query."
            
        output = []
        for i, article in enumerate(articles, 1):
            if brief:
                output.append(f"{i}. {article['title']}")
                output.append(f"   {article['url']}")
            else:
                date_info = "Unknown date"
                output.append(f"{i}. [{article['official_account']}] {article['title']} - {date_info}")
                snippet_preview = article['snippet'][:100] + "..." if len(article['snippet']) > 100 else article['snippet']
                output.append(f"   {snippet_preview}")
                output.append(f"   {article['url']}")
                output.append("")
                
        return "\n".join(output).strip()

    def search(self, query, max_results=5):
        """Search using Tavily API with site restriction"""
        try:
            # Check if Tavily API key is available
            tavily_key = os.environ.get('TAVILY_API_KEY')
            if not tavily_key:
                print("Error: TAVILY_API_KEY environment variable not set", file=sys.stderr)
                return []
                
            # Construct search query with site restriction
            search_query = f'{query} site:mp.weixin.qq.com'
            
            # Use subprocess to call Tavily Node.js script
            result = subprocess.run([
                'node', '/root/.openclaw/workspace/skills/tavily-search/scripts/search.mjs',
                search_query, '-n', str(max_results)
            ], capture_output=True, text=True, timeout=30, 
            env={**os.environ, 'TAVILY_API_KEY': tavily_key})
            
            if result.returncode == 0:
                return self._parse_tavily_results(result.stdout, max_results)
            else:
                print(f"Tavily search failed: {result.stderr}", file=sys.stderr)
                return []
                
        except Exception as e:
            print(f"Tavily search error: {e}", file=sys.stderr)
            return []

    def _parse_tavily_results(self, tavily_output, max_results):
        """Parse Tavily search results into standardized format"""
        articles = []
        try:
            # Tavily returns markdown with "## Sources" section
            if "## Sources" in tavily_output:
                sources_section = tavily_output.split("## Sources")[1]
                lines = sources_section.strip().split('\n')
                
                for line in lines:
                    if line.strip().startswith('- **') and 'mp.weixin.qq.com' in line:
                        # Extract title and URL from Tavily format
                        parts = line.split('**')
                        if len(parts) >= 3:
                            title = parts[1].strip()
                            # Find URL in the line
                            url_match = re.search(r'https?://[^\s)]+', line)
                            if url_match:
                                url = url_match.group(0)
                                article = {
                                    'title': title,
                                    'url': url,
                                    'snippet': 'Content from Tavily search',
                                    'source': 'tavily',
                                    'published_at': None,
                                    'official_account': self.extract_account_from_url(url)
                                }
                                if self.is_valid_wechat_url(url):
                                    articles.append(article)
                                    if len(articles) >= max_results:
                                        break
        except Exception as e:
            print(f"Error parsing Tavily results: {e}", file=sys.stderr)
            
        return articles


def main():
    import argparse
    
    parser = argparse.ArgumentParser(description="Search WeChat Official Account articles")
    parser.add_argument('query', help="Search query")
    parser.add_argument('--max-results', type=int, default=5, 
                       help="Maximum number of results (default: 5, max: 20)")
    parser.add_argument('--brief', action='store_true', help="Brief output (title and URL only)")
    parser.add_argument('--config', help="Path to configuration file")
    
    args = parser.parse_args()
    
    # Validate max_results
    if args.max_results < 1 or args.max_results > 20:
        print("Error: --max-results must be between 1 and 20", file=sys.stderr)
        sys.exit(1)
    
    try:
        searcher = WeChatSearch(args.config)
        articles = searcher.search(args.query, args.max_results)
        output = searcher.format_results(articles, args.brief)
        print(output)
        
    except KeyboardInterrupt:
        print("\nSearch interrupted by user.", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

path: wechat_search_fixed.py
content: #!/usr/bin/env python3
"""
WeChat Search Skill - Fixed version with proper OpenClaw tool integration
Returns the most recent 5 articles by default, with configurable options.
"""

import json
import sys
import os
import re
from datetime import datetime
import subprocess
import urllib.parse


class WeChatSearch:
    def __init__(self, config_path=None):
        self.config = self.load_config(config_path)
        
    def load_config(self, config_path):
        """Load configuration from file or use defaults"""
        default_config = {
            "max_results": 5,
            "search_strategy": "auto",
            "cache_duration_hours": 1,
            "request_delay_ms": 5000,
            "user_agent": "OpenClaw-WeChat-Search-Bot/1.0"
        }
        
        if config_path and os.path.exists(config_path):
            try:
                with open(config_path, 'r') as f:
                    user_config = json.load(f)
                    default_config.update(user_config)
            except Exception as e:
                print(f"Warning: Failed to load config file: {e}", file=sys.stderr)
                
        return default_config

    def search_wechat_articles(self, query, max_results=5):
        """
        Main search function with three-tier fallback strategy:
        1. Tavily Search (if available)
        2. Web Fetch (direct scraping)
        Returns list of articles with title, url, snippet, source
        """
        articles = []
        
        # Tier 1: Try Tavily Search
        print("🔍 Trying Tavily Search...", file=sys.stderr)
        try:
            articles = self.tavily_search_wechat(query, max_results)
            if articles:
                print(f"✅ Tavily Search found {len(articles)} articles", file=sys.stderr)
                return articles
        except Exception as e:
            print(f"🔄 Tavily Search failed: {e}", file=sys.stderr)
        
        # Tier 2: Fallback to Web Fetch
        print("🔄 Tavily failed, trying direct web_fetch...", file=sys.stderr)
        try:
            articles = self.web_fetch_wechat(query, max_results)
            if articles:
                print(f"✅ Web fetch found {len(articles)} articles", file=sys.stderr)
                return articles
        except Exception as e:
            print(f"❌ Web fetch failed: {e}", file=sys.stderr)
        
        return []

    def tavily_search_wechat(self, query, max_results=5):
        """Use Tavily Search API to search WeChat articles"""
        try:
            # Construct search query with site restriction
            search_query = f'{query} site:mp.weixin.qq.com'
            
            # Call Tavily search script directly
            env = os.environ.copy()
            if 'TAVILY_API_KEY' not in env:
                # Try to get from common locations
                tavily_config_path = os.path.expanduser('~/.openclaw/tavily-config.json')
                if os.path.exists(tavily_config_path):
                    with open(tavily_config_path, 'r') as f:
                        config = json.load(f)
                        env['TAVILY_API_KEY'] = config.get('api_key', '')
            
            result = subprocess.run([
                'node',
                '/root/.openclaw/workspace/skills/tavily-search/scripts/search.mjs',
                search_query,
                '-n', str(max_results)
            ], capture_output=True, text=True, timeout=30, env=env)
            
            if result.returncode == 0:
                # Parse Tavily output
                return self.parse_tavily_results(result.stdout, max_results)
            else:
                raise Exception(f"Tavily search failed: {result.stderr}")
                
        except Exception as e:
            raise Exception(f"Tavily search error: {e}")

    def web_fetch_wechat(self, query, max_results=5):
        """Use OpenClaw's web_fetch tool to fetch WeChat search results"""
        try:
            # Construct WeChat search URL (using Sogou as proxy)
            encoded_query = urllib.parse.quote(query)
            search_url = f"https://weixin.sogou.com/weixin?type=2&query={encoded_query}"
            
            result = subprocess.run([
                'openclaw', 'tool', 'web_fetch',
                '--url', search_url,
                '--extract-mode', 'markdown'
            ], capture_output=True, text=True, timeout=30)
            
            if result.returncode == 0:
                content = result.stdout
                return self.parse_sogou_results(content, max_results)
            else:
                raise Exception(f"Web fetch failed: {result.stderr}")
                
        except Exception as e:
            raise Exception(f"Web fetch error: {e}")

    def parse_tavily_results(self, tavily_output, max_results):
        """Parse Tavily search results into standardized format"""
        articles = []
        try:
            # Tavily output contains markdown with sources section
            if '## Sources' in tavily_output:
                sources_section = tavily_output.split('## Sources')[1]
                lines = sources_section.strip().split('\n')
                
                current_title = ""
                current_url = ""
                current_snippet = ""
                
                for line in lines:
                    line = line.strip()
                    if line.startswith('- **') and '**' in line:
                        # Extract title
                        title_part = line.split('**')[1]
                        current_title = title_part
                    elif line.startswith('  ') and 'http' in line:
                        # Extract URL
                        url_start = line.find('http')
                        url_end = line.find(')', url_start)
                        if url_end == -1:
                            url_end = len(line)
                        current_url = line[url_start:url_end].strip()
                        
                        # Create article
                        if current_title and current_url:
                            article = {
                                'title': current_title.strip(),
                                'url': current_url.strip(),
                                'snippet': current_snippet.strip() if current_snippet else 'No snippet available',
                                'source': 'tavily',
                                'official_account': self.extract_account_from_title(current_title)
                            }
                            if self.is_valid_wechat_url(article['url']):
                                articles.append(article)
                                if len(articles) >= max_results:
                                    break
                        # Reset for next article
                        current_title = ""
                        current_url = ""
                        current_snippet = ""
                    elif line.startswith('  # ') and current_title:
                        # Extract snippet/content
                        current_snippet = line[4:].strip()
                        
        except Exception as e:
            print(f"Error parsing Tavily results: {e}", file=sys.stderr)
            
        return articles[:max_results]

    def parse_sogou_results(self, content, max_results):
        """Parse Sogou WeChat search results"""
        articles = []
        try:
            # Look for article patterns in the content
            lines = content.split('\n')
            for i, line in enumerate(lines):
                if '### [' in line and '](' in line:
                    # Extract title and URL
                    title_start = line.find('[') + 1
                    title_end = line.find(']')
                    url_start = line.find('(') + 1
                    url_end = line.find(')')
                    
                    if title_start > 0 and title_end > title_start and url_start > 0 and url_end > url_start:
                        title = line[title_start:title_end].strip()
                        url = line[url_start:url_end].strip()
                        
                        # Get snippet from next lines
                        snippet = ""
                        if i + 1 < len(lines):
                            snippet = lines[i + 1].strip()
                        
                        article = {
                            'title': title,
                            'url': url,
                            'snippet': snippet,
                            'source': 'sogou',
                            'official_account': self.extract_account_from_title(title)
                        }
                        if self.is_valid_wechat_url(article['url']):
                            articles.append(article)
                            if len(articles) >= max_results:
                                break
        except Exception as e:
            print(f"Error parsing Sogou results: {e}", file=sys.stderr)
            
        return articles[:max_results]

    def extract_account_from_title(self, title):
        """Extract official account name from title"""
        if ' - ' in title:
            return title.split(' - ')[-1]
        elif '｜' in title:
            return title.split('｜')[-1]
        elif '|' in title:
            return title.split('|')[-1]
        else:
            return "Unknown Account"

    def is_valid_wechat_url(self, url):
        """Check if URL is a valid WeChat Official Account article"""
        try:
            if not url.startswith('http'):
                return False
            if 'mp.weixin.qq.com' in url:
                return True
            if 'weixin.sogou.com/link' in url:
                return True
            return False
        except:
            return False

    def format_results(self, articles, brief=False):
        """Format search results for display"""
        if not articles:
            return "No WeChat Official Account articles found for your query."
            
        output = []
        for i, article in enumerate(articles, 1):
            if brief:
                output.append(f"{i}. {article['title']}")
                output.append(f"   {article['url']}")
            else:
                output.append(f"{i}. [{article['official_account']}] {article['title']}")
                snippet_preview = article['snippet'][:100] + "..." if len(article['snippet']) > 100 else article['snippet']
                output.append(f"   {snippet_preview}")
                output.append(f"   {article['url']}")
                output.append("")
                
        return "\n".join(output).strip()


def main():
    import argparse
    
    parser = argparse.ArgumentParser(description="Search WeChat Official Account articles")
    parser.add_argument('query', help="Search query")
    parser.add_argument('--max-results', type=int, default=5, 
                       help="Maximum number of results (default: 5, max: 20)")
    parser.add_argument('--brief', action='store_true', help="Brief output (title and URL only)")
    parser.add_argument('--config', help="Path to configuration file")
    
    args = parser.parse_args()
    
    # Validate max_results
    if args.max_results < 1 or args.max_results > 20:
        print("Error: --max-results must be between 1 and 20", file=sys.stderr)
        sys.exit(1)
    
    try:
        searcher = WeChatSearch(args.config)
        articles = searcher.search_wechat_articles(args.query, args.max_results)
        output = searcher.format_results(articles, args.brief)
        print(output)
        
    except KeyboardInterrupt:
        print("\nSearch interrupted by user.", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

path: wechat_search_simple.py
content: #!/usr/bin/env python3
"""
Simple WeChat Search Skill - Direct implementation using available tools
"""

import json
import sys
import os
import re
import subprocess
from urllib.parse import urlparse


def is_valid_wechat_url(url):
    """Check if URL is a valid WeChat Official Account article"""
    try:
        parsed = urlparse(url)
        if 'mp.weixin.qq.com' not in parsed.netloc:
            return False
        return '/s' in parsed.path
    except:
        return False


def extract_account_from_url(url):
    """Extract official account name from WeChat URL"""
    try:
        parsed = urlparse(url)
        if 'mp.weixin.qq.com' in parsed.netloc:
            return "WeChat Official Account"
    except:
        return "Unknown Account"


def parse_tavily_results(tavily_output, max_results):
    """Parse Tavily search results into standardized format"""
    articles = []
    try:
        if "## Sources" in tavily_output:
            sources_section = tavily_output.split("## Sources")[1]
            lines = sources_section.strip().split('\n')
            
            for line in lines:
                if line.strip().startswith('- **') and 'mp.weixin.qq.com' in line:
                    parts = line.split('**')
                    if len(parts) >= 3:
                        title = parts[1].strip()
                        url_match = re.search(r'https?://[^\s)]+', line)
                        if url_match:
                            url = url_match.group(0)
                            if is_valid_wechat_url(url):
                                article = {
                                    'title': title,
                                    'url': url,
                                    'snippet': 'Content from Tavily search',
                                    'official_account': extract_account_from_url(url)
                                }
                                articles.append(article)
                                if len(articles) >= max_results:
                                    break
    except Exception as e:
        print(f"Error parsing Tavily results: {e}", file=sys.stderr)
        
    return articles


def search_wechat_articles(query, max_results=5):
    """Search WeChat articles using Tavily with site restriction"""
    try:
        # Check if Tavily API key is available
        tavily_key = os.environ.get('TAVILY_API_KEY')
        if not tavily_key:
            print("Error: TAVILY_API_KEY environment variable not set", file=sys.stderr)
            return []
            
        # Construct search query with site restriction
        search_query = f'{query} site:mp.weixin.qq.com'
        
        # Call Tavily Node.js script
        result = subprocess.run([
            'node', '/root/.openclaw/workspace/skills/tavily-search/scripts/search.mjs',
            search_query, '-n', str(max_results)
        ], capture_output=True, text=True, timeout=30, 
        env={**os.environ, 'TAVILY_API_KEY': tavily_key})
        
        if result.returncode == 0:
            return parse_tavily_results(result.stdout, max_results)
        else:
            print(f"Tavily search failed: {result.stderr}", file=sys.stderr)
            return []
            
    except Exception as e:
        print(f"Search error: {e}", file=sys.stderr)
        return []


def format_results(articles, brief=False):
    """Format search results for display"""
    if not articles:
        return "No WeChat Official Account articles found for your query."
        
    output = []
    for i, article in enumerate(articles, 1):
        if brief:
            output.append(f"{i}. {article['title']}")
            output.append(f"   {article['url']}")
        else:
            output.append(f"{i}. [{article['official_account']}] {article['title']}")
            snippet_preview = article['snippet'][:100] + "..." if len(article['snippet']) > 100 else article['snippet']
            output.append(f"   {snippet_preview}")
            output.append(f"   {article['url']}")
            output.append("")
            
    return "\n".join(output).strip()


def main():
    import argparse
    
    parser = argparse.ArgumentParser(description="Search WeChat Official Account articles")
    parser.add_argument('query', help="Search query")
    parser.add_argument('--max-results', type=int, default=5, 
                       help="Maximum number of results (default: 5, max: 20)")
    parser.add_argument('--brief', action='store_true', help="Brief output (title and URL only)")
    
    args = parser.parse_args()
    
    if args.max_results < 1 or args.max_results > 20:
        print("Error: --max-results must be between 1 and 20", file=sys.stderr)
        sys.exit(1)
    
    try:
        articles = search_wechat_articles(args.query, args.max_results)
        output = format_results(articles, args.brief)
        print(output)
        
    except KeyboardInterrupt:
        print("\nSearch interrupted by user.", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

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