Skill: longjf25/baidu-nearby (version 1.0.4)

--- name: baidu-nearby description_en: | Baidu capability suite - provides Baidu web search and Baidu Map route planning. Includes: Baidu web search (baidu_search), route planning (baidu_direction), nearby place recommendation (baidu_nearby). Use when: Need to search the web, plan travel routes, or find nearby restaurants/attractions/hotels and other location services. description_zh: | 百度能力集合 - 提供百度搜索和百度地图路线规划功能。 包含：百度网页搜索(baidu_search)、路线规划(baidu_direction)、附近场所推荐(baidu_nearby)。 Use when: 需要搜索网页、规划出行路线、查找附近餐饮/景点/酒店等位置服务时。 version: 1.0.4 credentials: env: BAIDU_API_KEY: description: Baidu LBS Platform API Key (AK), used for map route planning and nearby place search / 百度 LBS 开放平台 API Key（AK），用于地图路线规划和附近场所搜索功能 required: true sensitive: true sources: - env: BAIDU_API_KEY - env: BAIDU_AK docs: https://lbsyun.baidu.com/apiconsole/key --- # Baidu Nearby / 百度能力集合 Baidu capability suite providing search and location services. / 百度能力集合，提供搜索和位置服务。 ## Features / 功能 | Command / 命令 | Description / 说明 | |------|------| | `baidu_search` | Baidu web search / 百度网页搜索 | | `baidu_direction` | Baidu Map route planning / 百度地图路线规划 | | `baidu_nearby` | Nearby place recommendation / 附近场所推荐 | ## Configuration / 配置 Set Baidu API Key (required for LBS location services) / 设置百度 API Key（LBS 位置服务需要）： ```bash export BAIDU_API_KEY="your_baidu_ak" ``` How to get AK / 获取 AK： 1. Visit https://lbsyun.baidu.com/ 2. Register a developer account / 注册开发者账号 3. Create an application to get AK / 创建应用获取 AK ## Usage / 使用方法 ### Baidu Search / 百度搜索 ```bash python scripts/baidu_search.py "search_keywords" [result_count] ``` ### Route Planning / 路线规划 ```bash python scripts/baidu_direction.py "origin_address" "destination_address" [driving|riding|walking|transit] ``` ### Nearby Place Recommendation / 附近场所推荐 ```bash python scripts/baidu_nearby.py "location" [category] [radius_meters] [count] ``` **Supported Categories / 支持的类别：** - 餐饮/美食/餐厅 — Food & Dining - 娱乐/休闲 — Entertainment & Leisure - 景点/旅游/景区 — Attractions & Tourism - 酒店/住宿 — Hotels & Accommodation - 购物/商场/超市 — Shopping & Malls - 交通/地铁/公交 — Transportation **Examples / 示例：** ```bash # Search for food near Sanlitun / 搜索三里屯附近美食 python scripts/baidu_nearby.py "Sanlitun, Chaoyang District, Beijing" 餐饮 1000 5 # Search for attractions near Tiananmen / 搜索天安门附近景点 python scripts/baidu_nearby.py "Tiananmen" 景点 5000 10 # Search using coordinates / 使用坐标搜索 python scripts/baidu_nearby.py "39.9,116.4" 娱乐 ``` ## Dependencies / 依赖 Pure Python standard library, no additional dependencies. / 纯 Python 标准库实现，无需额外依赖。 ## Security Notes / 安全说明 - Uses system default SSL certificate verification / 使用系统默认 SSL 证书验证 - Input parameters are validated and sanitized / 输入参数已做验证和清理 - Only HTTP/HTTPS protocols supported / 仅支持 HTTP/HTTPS 协议

path: scripts/baidu_direction.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
百度地图路线规划工具 - 安全修复版
使用百度地图 DirectionLite API

修复:
- [SECURITY] 移除 SSL 验证禁用代码
- [SECURITY] 添加输入验证和参数清理
- [SECURITY] 限制请求参数范围
- [SECURITY] API Key 不输出到日志
"""

import os
import sys
import json
import urllib.parse
import urllib.request
import ssl
import socket
import re

# ============== 安全配置 ==============

ssl_context = ssl.create_default_context()

GEOCODE_TIMEOUT = 10
DIRECTION_TIMEOUT = 15

MAX_ADDRESS_LENGTH = 200
VALID_MODES = ['driving', 'riding', 'walking', 'transit']

# 中国境内坐标范围
LAT_RANGE = (3, 54)
LNG_RANGE = (73, 136)


def validate_coordinates(coord_str):
    """
    验证坐标字符串格式 "lat,lng"
    
    Returns:
        (is_valid, result_or_error)
    """
    if not coord_str or not isinstance(coord_str, str):
        return False, "坐标为空"
    
    parts = coord_str.split(',')
    if len(parts) != 2:
        return False, "坐标格式应为 'lat,lng'"
    
    try:
        lat = float(parts[0].strip())
        lng = float(parts[1].strip())
    except ValueError:
        return False, "坐标必须为数字"
    
    if not (LAT_RANGE[0] <= lat <= LAT_RANGE[1]):
        return False, f"纬度超出范围 ({LAT_RANGE[0]}-{LAT_RANGE[1]})"
    
    if not (LNG_RANGE[0] <= lng <= LNG_RANGE[1]):
        return False, f"经度超出范围 ({LNG_RANGE[0]}-{LNG_RANGE[1]})"
    
    return True, f"{lat},{lng}"


def is_address(input_str):
    """判断输入是地址还是坐标"""
    if not input_str:
        return True
    
    # 检查是否为坐标格式
    coord_pattern = r'^-?\d+\.?\d*\s*,\s*-?\d+\.?\d*$'
    if re.match(coord_pattern, input_str.strip()):
        return False
    
    return True


def sanitize_address(address):
    """清理地址输入"""
    if not address or not isinstance(address, str):
        return None
    
    address = address.strip()
    if len(address) > MAX_ADDRESS_LENGTH:
        address = address[:MAX_ADDRESS_LENGTH]
    
    # 移除危险字符
    address = address.replace('\n', ' ').replace('\r', ' ')
    address = address.replace('<', '').replace('>', '')
    address = address.replace('"', '').replace("'", '')
    
    return address if address else None


def validate_mode(mode):
    """验证出行方式"""
    if not mode:
        return True, 'driving'
    
    mode = mode.strip().lower()
    if mode not in VALID_MODES:
        return False, f"不支持的出行方式: {mode}（支持: {', '.join(VALID_MODES)}）"
    
    return True, mode


def get_api_key():
    """从环境变量获取API Key"""
    [REDACTED_SECRET]'BAIDU_API_KEY') or os.environ.get('BAIDU_AK')
    if not [REDACTED_SECRET]"错误: 未设置 BAIDU_API_KEY 环境变量")
        print("请访问 https://lbsyun.baidu.com/ 申请AK")
        return None
    
    if len(api_key) < 10 or len(api_key) > 100:
        print("警告: API Key 格式可能不正确")
    
    return api_key


def safe_urlopen(url, timeout=10):
    """安全的 URL 请求"""
    try:
        req = urllib.request.Request(url)
        return urllib.request.urlopen(
            req,
            timeout=timeout,
            context=ssl_context
        )
    except urllib.error.HTTPError as e:
        print(f"HTTP错误: {e.code} {e.reason}")
        return None
    except urllib.error.URLError as e:
        print(f"URL错误: {e.reason}")
        return None
    except socket.timeout:
        print("请求超时")
        return None
    except Exception as e:
        print(f"请求失败: {type(e).__name__}")
        return None


def geocode(address, ak):
    """
    地理编码 - 地址转经纬度
    
    Args:
        address: 地址字符串
        ak: 百度API Key
    
    Returns:
        "lat,lng" 字符串 或 None
    """
    address = sanitize_address(address)
    if not address:
        print("地址无效")
        return None
    
    url = (
        f"https://api.map.baidu.com/geocoding/v3/?"
        f"address={urllib.parse.quote(address)}"
        f"&output=json&ak={ak}"
    )
    
    response = safe_urlopen(url, timeout=GEOCODE_TIMEOUT)
    if not response:
        return None
    
    try:
        data = json.loads(response.read().decode('utf-8'))
        if data.get('status') == 0:
            location = data['result']['location']
            coord = f"{location['lat']},{location['lng']}"
            # 验证返回的坐标
            is_valid, result = validate_coordinates(coord)
            if is_valid:
                return result
            else:
                print(f"返回坐标无效: {result}")
                return None
        else:
            print(f"地理编码失败: {data.get('message', '未知错误')}")
    except json.JSONDecodeError:
        print("响应解析失败")
    except Exception as e:
        print(f"处理失败: {type(e).__name__}")
    
    return None


def direction_lite(origin, destination, mode='driving', ak=None):
    """
    百度地图路线规划
    
    Args:
        origin: 起点 (地址或经纬度 "lat,lng")
        destination: 终点 (地址或经纬度 "lat,lng")
        mode: 出行方式 - driving|riding|walking|transit
        ak: 百度API Key
    
    Returns:
        dict: 路线规划结果 或 None
    """
    if not ak:
        ak = get_api_key()
        if not ak:
            return None
    
    # 验证出行方式
    is_valid, mode_result = validate_mode(mode)
    if not is_valid:
        print(mode_result)
        return None
    mode = mode_result
    
    # 解析起点
    if is_address(origin):
        origin_coord = geocode(origin, ak)
        if not origin_coord:
            print(f"无法解析起点: {sanitize_address(origin) or origin}")
            return None
    else:
        is_valid, origin_coord = validate_coordinates(origin)
        if not is_valid:
            print(f"起点坐标错误: {origin_coord}")
            return None
    
    # 解析终点
    if is_address(destination):
        dest_coord = geocode(destination, ak)
        if not dest_coord:
            print(f"无法解析终点: {sanitize_address(destination) or destination}")
            return None
    else:
        is_valid, dest_coord = validate_coordinates(destination)
        if not is_valid:
            print(f"终点坐标错误: {dest_coord}")
            return None
    
    # 构建请求
    base_url = "https://api.map.baidu.com/directionlite/v1/"
    endpoint = 'transit' if mode == 'transit' else mode
    
    params = {
        'origin': origin_coord,
        'destination': dest_coord,
        'ak': ak,
        'output': 'json'
    }
    
    if mode == 'driving':
        params['alternatives'] = '1'
    
    url = f"{base_url}{endpoint}?" + urllib.parse.urlencode(params)
    
    response = safe_urlopen(url, timeout=DIRECTION_TIMEOUT)
    if not response:
        return None
    
    try:
        data = json.loads(response.read().decode('utf-8'))
        return data
    except json.JSONDecodeError:
        print("响应解析失败")
        return None
    except Exception as e:
        print(f"处理失败: {type(e).__name__}")
        return None


def format_duration(seconds):
    """格式化时间"""
    try:
        seconds = int(seconds)
        hours = seconds // 3600
        minutes = (seconds % 3600) // 60
        if hours > 0:
            return f"{hours}小时{minutes}分钟"
        return f"{minutes}分钟"
    except (ValueError, TypeError):
        return "未知"


def format_distance(meters):
    """格式化距离"""
    try:
        meters = int(meters)
        if meters >= 1000:
            return f"{meters/1000:.1f}公里"
        return f"{meters}米"
    except (ValueError, TypeError):
        return "未知"


def print_route(result, mode):
    """打印路线结果"""
    if not result:
        print("未获取到路线信息")
        return
    
    status = result.get('status')
    if status != 0:
        print(f"请求失败，状态码: {status}")
        message = result.get('message', '未知错误')
        print(f"错误信息: {message}")
        return
    
    routes = result.get('result', {}).get('routes', [])
    if not routes:
        print("未找到可用路线")
        return
    
    route = routes[0]
    distance = route.get('distance', 0)
    duration = route.get('duration', 0)
    
    mode_names = {
        'driving': '🚗 驾车',
        'riding': '🚴 骑行',
        'walking': '🚶 步行',
        'transit': '🚌 公交'
    }
    
    print(f"\n{mode_names.get(mode, mode)}路线规划")
    print("=" * 40)
    print(f"总距离: {format_distance(distance)}")
    print(f"预计时间: {format_duration(duration)}")
    print()
    
    steps = route.get('steps', [])
    if steps:
        print("详细路线:")
        print("-" * 40)
        step_num = 1
        for step in steps:
            # 公交路线的 steps 是嵌套列表 [[{}, {}], [{}, {}]]
            # 驾车路线的 steps 是 ["指令", "指令"]
            if isinstance(step, list):
                # 公交路线：每个 step 包含多个子步骤
                for sub_step in step:
                    if isinstance(sub_step, dict):
                        instruction = sub_step.get('instruction', '')
                        step_distance = sub_step.get('distance', 0)
                        step_type = sub_step.get('type', 0)
                        vehicle = sub_step.get('vehicle', {})
                        
                        # 根据类型显示不同图标
                        icon = '🚶'
                        if step_type == 3:  # 公交
                            icon = '🚌'
                            vehicle_name = vehicle.get('name', '')
                            if vehicle_name:
                                instruction = f"乘坐{vehicle_name}: {instruction}"
                        elif step_type == 5:  # 步行
                            icon = '🚶'
                        
                        if instruction:
                            print(f"{step_num}. {icon} {instruction} ({format_distance(step_distance)})")
                            step_num += 1
            elif isinstance(step, dict):
                instruction = step.get('instruction', step.get('step_instruction', ''))
                if instruction:
                    step_distance = step.get('distance', 0)
                    print(f"{step_num}. {instruction} ({format_distance(step_distance)})")
                    step_num += 1
            elif isinstance(step, str):
                print(f"{step_num}. {step}")
                step_num += 1
    
    # 显示备选路线
    if len(routes) > 1:
        print(f"\n📋 共 {len(routes)} 条备选路线")
        for idx, alt_route in enumerate(routes[1:], 2):
            alt_dist = format_distance(alt_route.get('distance', 0))
            alt_dur = format_duration(alt_route.get('duration', 0))
            print(f"  路线{idx}: {alt_dist}, {alt_dur}")


def main():
    if len(sys.argv) < 3:
        print("使用方法: python baidu_direction.py <起点> <终点> [出行方式]")
        print("")
        print("起点/终点: 地址 或 经纬度坐标（如：39.9,116.3）")
        print("出行方式: driving(驾车), riding(骑行), walking(步行), transit(公交)")
        print("  默认: driving")
        print("")
        print("示例:")
        print("  python baidu_direction.py '北京市朝阳区' '北京市海淀区' driving")
        print("  python baidu_direction.py '39.9,116.3' '39.8,116.4' walking")
        print("  python baidu_direction.py '天安门' '故宫' transit")
        sys.exit(1)
    
    origin = sys.argv[1]
    destination = sys.argv[2]
    mode = sys.argv[3] if len(sys.argv) > 3 else 'driving'
    
    # 验证出行方式
    is_valid, mode_result = validate_mode(mode)
    if not is_valid:
        print(mode_result)
        sys.exit(1)
    mode = mode_result
    
    # 清理显示
    origin_display = sanitize_address(origin) if is_address(origin) else origin
    dest_display = sanitize_address(destination) if is_address(destination) else destination
    
    print(f"🗺️ 路线规划: {origin_display} → {dest_display}")
    
    result = direction_lite(origin, destination, mode)
    print_route(result, mode)


if __name__ == '__main__':
    main()


path: scripts/baidu_nearby.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
百度地图附近场所推荐工具 - 安全修复版
使用百度地图 Place API 进行周边POI搜索

修复:
- [SECURITY] 移除 SSL 验证禁用代码
- [SECURITY] 添加输入验证和参数清理
- [SECURITY] 限制搜索半径和结果数量
- [SECURITY] API Key 不输出到日志
"""

import os
import sys
import json
import urllib.parse
import urllib.request
import ssl
import socket

# ============== 安全配置 ==============

# 使用系统默认 SSL 上下文
ssl_context = ssl.create_default_context()

# 请求超时（秒）
GEOCODE_TIMEOUT = 10
SEARCH_TIMEOUT = 15

# 参数限制
MAX_RADIUS = 20000      # 最大搜索半径 20km
MAX_PAGE_SIZE = 20      # 最大返回数量
MAX_ADDRESS_LENGTH = 200  # 地址最大长度

# 场所分类映射
CATEGORY_MAP = {
    '餐饮': 'catering',
    '美食': 'catering',
    '餐厅': 'catering',
    '吃饭': 'catering',
    '娱乐': 'entertainment',
    '休闲': 'entertainment',
    '景点': 'scenic',
    '旅游': 'scenic',
    '景区': 'scenic',
    '酒店': 'hotel',
    '住宿': 'hotel',
    '购物': 'shopping',
    '商场': 'shopping',
    '超市': 'shopping',
    '交通': 'transportation',
    '地铁': 'transportation',
    '公交': 'transportation',
    '生活': 'life_service',
    '服务': 'life_service',
    '医疗': 'medical',
    '医院': 'medical',
    '教育': 'education',
    '学校': 'education',
    '金融': 'finance',
    '银行': 'finance',
}

# 分类标签映射
TAG_MAP = {
    'catering': ['中餐厅', '西餐厅', '咖啡厅', '火锅', '烧烤', '小吃', '快餐', '酒吧'],
    'entertainment': ['KTV', '电影院', '网吧', '游乐场', '体育馆', '健身房', '公园'],
    'scenic': ['公园', '广场', '景区', '博物馆', '展览馆', '寺庙', '古迹'],
    'hotel': ['酒店', '宾馆', '旅馆', '招待所'],
    'shopping': ['商场', '购物中心', '超市', '便利店', '专卖店', '市场'],
    'transportation': ['地铁站', '公交站', '火车站', '机场', '汽车站'],
    'life_service': ['加油站', '停车场', '厕所', '快递', '洗衣', '理发'],
    'medical': ['医院', '诊所', '药店', '急救中心'],
    'education': ['大学', '中学', '小学', '幼儿园', '培训机构', '图书馆'],
    'finance': ['银行', 'ATM', '证券', '保险'],
}

DEFAULT_TAGS = {
    'catering': '美食',
    'entertainment': '休闲娱乐',
    'scenic': '旅游景点',
    'hotel': '酒店宾馆',
    'shopping': '购物',
    'transportation': '交通设施',
    'life_service': '生活服务',
    'medical': '医疗保健',
    'education': '教育培训',
    'finance': '金融银行',
}


def validate_coordinates(lat, lng):
    """验证坐标范围"""
    try:
        lat = float(lat)
        lng = float(lng)
        # 中国境内大致范围
        if not (3 <= lat <= 54 and 73 <= lng <= 136):
            return False, "坐标超出中国境内范围"
        return True, (lat, lng)
    except (ValueError, TypeError):
        return False, "坐标格式错误"


def validate_radius(radius):
    """验证搜索半径"""
    try:
        radius = int(radius)
        if radius < 100:
            return False, "搜索半径不能小于100米"
        if radius > MAX_RADIUS:
            return False, f"搜索半径不能超过{MAX_RADIUS}米"
        return True, radius
    except (ValueError, TypeError):
        return False, "半径格式错误"


def validate_page_size(page_size):
    """验证返回数量"""
    try:
        page_size = int(page_size)
        if page_size < 1:
            return False, "返回数量不能小于1"
        if page_size > MAX_PAGE_SIZE:
            return False, f"返回数量不能超过{MAX_PAGE_SIZE}"
        return True, page_size
    except (ValueError, TypeError):
        return False, "数量格式错误"


def sanitize_address(address):
    """清理地址输入"""
    if not address or not isinstance(address, str):
        return None
    
    # 去除首尾空白
    address = address.strip()
    
    # 限制长度
    if len(address) > MAX_ADDRESS_LENGTH:
        address = address[:MAX_ADDRESS_LENGTH]
    
    # 移除潜在危险字符
    address = address.replace('\n', ' ').replace('\r', ' ')
    address = address.replace('<', '').replace('>', '')
    address = address.replace('"', '').replace("'", '')
    
    return address if address else None


def get_api_key():
    """从环境变量获取API Key（不输出到日志）"""
    [REDACTED_SECRET]'BAIDU_API_KEY') or os.environ.get('BAIDU_AK')
    if not [REDACTED_SECRET]"错误: 未设置 BAIDU_API_KEY 环境变量")
        print("请访问 https://lbsyun.baidu.com/ 申请AK")
        return None
    
    # 验证 API Key 格式（基本检查）
    if len(api_key) < 10 or len(api_key) > 100:
        print("警告: API Key 格式可能不正确")
    
    return api_key


def mask_api_key(ak):
    """掩码 API Key 用于日志输出"""
    if not ak or len(ak) < 8:
        return "***"
    return ak[:4] + "*" * (len(ak) - 8) + ak[-4:]


def safe_urlopen(url, timeout=10):
    """安全的 URL 请求"""
    try:
        req = urllib.request.Request(url)
        return urllib.request.urlopen(
            req,
            timeout=timeout,
            context=ssl_context
        )
    except urllib.error.HTTPError as e:
        print(f"HTTP错误: {e.code} {e.reason}")
        return None
    except urllib.error.URLError as e:
        print(f"URL错误: {e.reason}")
        return None
    except socket.timeout:
        print("请求超时")
        return None
    except Exception as e:
        print(f"请求失败: {type(e).__name__}")
        return None


def geocode(address, ak):
    """
    地理编码 - 地址转经纬度
    
    Args:
        address: 地址字符串
        ak: 百度API Key
    
    Returns:
        (lat, lng) 或 None
    """
    # 清理地址
    address = sanitize_address(address)
    if not address:
        print("地址无效")
        return None
    
    url = (
        f"https://api.map.baidu.com/geocoding/v3/?"
        f"address={urllib.parse.quote(address)}"
        f"&output=json&ak={ak}"
    )
    
    response = safe_urlopen(url, timeout=GEOCODE_TIMEOUT)
    if not response:
        return None
    
    try:
        data = json.loads(response.read().decode('utf-8'))
        if data.get('status') == 0:
            location = data['result']['location']
            lat, lng = location['lat'], location['lng']
            # 验证返回的坐标
            is_valid, result = validate_coordinates(lat, lng)
            if is_valid:
                return result
            else:
                print(f"返回坐标无效: {result}")
                return None
        else:
            print(f"地理编码失败: {data.get('message', '未知错误')}")
    except json.JSONDecodeError:
        print("响应解析失败")
    except Exception as e:
        print(f"处理响应失败: {type(e).__name__}")
    
    return None


def reverse_geocode(lat, lng, ak):
    """
    逆地理编码 - 经纬度转地址
    
    Args:
        lat: 纬度
        lng: 经度
        ak: 百度API Key
    
    Returns:
        地址字符串 或 None
    """
    url = (
        f"https://api.map.baidu.com/reverse_geocoding/v3/?"
        f"location={lat},{lng}&output=json&ak={ak}"
    )
    
    response = safe_urlopen(url, timeout=GEOCODE_TIMEOUT)
    if not response:
        return None
    
    try:
        data = json.loads(response.read().decode('utf-8'))
        if data.get('status') == 0:
            return data['result'].get('formatted_address')
    except Exception:
        pass
    
    return None


def search_nearby(location, query=None, radius=1000, page_size=10, ak=None):
    """
    周边POI搜索
    
    Args:
        location: 中心点坐标 (lat,lng) 或地址
        query: 搜索关键词
        radius: 搜索半径（米），100-20000
        page_size: 返回结果数量，1-20
        ak: 百度API Key
    
    Returns:
        list: POI列表 或 None
    """
    if not ak:
        ak = get_api_key()
        if not ak:
            return None
    
    # 验证半径
    is_valid, radius = validate_radius(radius)
    if not is_valid:
        print(f"半径错误: {radius}")
        return None
    
    # 验证数量
    is_valid, page_size = validate_page_size(page_size)
    if not is_valid:
        print(f"数量错误: {page_size}")
        return None
    
    # 解析位置
    if isinstance(location, str) and (',' not in location or len(location.split(',')) != 2):
        # 是地址，需要地理编码
        coord = geocode(location, ak)
        if not coord:
            print(f"无法解析位置: {location}")
            return None
        lat, lng = coord
    elif isinstance(location, str):
        # 是坐标字符串
        parts = location.split(',')
        is_valid, result = validate_coordinates(parts[0], parts[1])
        if not is_valid:
            print(f"坐标错误: {result}")
            return None
        lat, lng = result
    else:
        lat, lng = location
    
    # 构建请求
    base_url = "https://api.map.baidu.com/place/v2/search"
    params = {
        'query': query or '美食',
        'location': f"{lat},{lng}",
        'radius': str(radius),
        'output': 'json',
        'ak': ak,
        'scope': '2',
        'page_size': str(page_size),
        'page_num': '0',
    }
    
    url = f"{base_url}?" + urllib.parse.urlencode(params)
    
    response = safe_urlopen(url, timeout=SEARCH_TIMEOUT)
    if not response:
        return None
    
    try:
        data = json.loads(response.read().decode('utf-8'))
        if data.get('status') == 0:
            return data.get('results', [])
        else:
            print(f"搜索失败: {data.get('message', '未知错误')}")
    except json.JSONDecodeError:
        print("响应解析失败")
    except Exception as e:
        print(f"处理失败: {type(e).__name__}")
    
    return None


def get_category_tag(category):
    """获取分类对应的搜索标签"""
    if not category:
        return None
    
    category_lower = category.lower()
    if category_lower in TAG_MAP:
        return DEFAULT_TAGS.get(category_lower, category)
    
    # 中文匹配
    for cn, en in CATEGORY_MAP.items():
        if cn in category or category in cn:
            return DEFAULT_TAGS.get(en, category)
    
    return category


def format_distance(meters):
    """格式化距离"""
    try:
        meters = int(meters)
        if meters >= 1000:
            return f"{meters/1000:.1f}km"
        return f"{meters}m"
    except (ValueError, TypeError):
        return "未知"


def print_poi_results(results, location_name, category=None):
    """格式化打印POI结果"""
    if not results:
        print(f"未找到{category or ''}相关场所")
        return
    
    category_display = category or "周边场所"
    print(f"\n📍 {location_name} 附近的{category_display}推荐")
    print("=" * 50)
    print(f"找到 {len(results)} 个推荐地点\n")
    
    for i, poi in enumerate(results, 1):
        name = poi.get('name', '未知')
        address = poi.get('address', '地址未知')
        distance = poi.get('distance', 0)
        detail = poi.get('detail_info', {})
        rating = detail.get('overall_rating', '')
        price = detail.get('price', '')
        tag = detail.get('tag', '')
        phone = poi.get('telephone', '')
        
        meta = []
        if rating:
            meta.append(f"⭐{rating}")
        if price:
            meta.append(f"💰{price}元")
        if tag:
            meta.append(f"🏷️{tag}")
        
        print(f"{i}. 🏪 {name}")
        print(f"   📍 {address}")
        if meta:
            print(f"   {' | '.join(meta)}")
        print(f"   📏 距离: {format_distance(distance)}")
        if phone:
            print(f"   📞 {phone}")
        print()


def recommend_nearby(location, category=None, radius=1000, limit=10):
    """
    推荐附近场所
    
    Args:
        location: 位置（地址或坐标）
        category: 类别
        radius: 搜索半径（米）
        limit: 返回数量
    
    Returns:
        list: POI列表 或 None
    """
    ak = get_api_key()
    if not ak:
        return None
    
    # 解析位置
    if isinstance(location, str) and (',' not in location or len(location.split(',')) != 2):
        coord = geocode(location, ak)
        if not coord:
            print(f"无法解析位置: {location}")
            return None
        lat, lng = coord
        location_name = sanitize_address(location) or f"{lat},{lng}"
    elif isinstance(location, str):
        parts = location.split(',')
        is_valid, result = validate_coordinates(parts[0], parts[1])
        if not is_valid:
            print(f"坐标错误: {result}")
            return None
        lat, lng = result
        location_name = reverse_geocode(lat, lng, ak) or f"{lat},{lng}"
    else:
        lat, lng = location
        location_name = reverse_geocode(lat, lng, ak) or f"{lat},{lng}"
    
    # 搜索关键词
    query = None
    if category:
        query = get_category_tag(category)
    
    # 执行搜索
    results = search_nearby((lat, lng), query=query, radius=radius, page_size=limit, ak=ak)
    
    if results:
        print_poi_results(results, location_name, category)
    
    return results


def main():
    if len(sys.argv) < 2:
        print("使用方法: python baidu_nearby.py <位置> [类别] [半径(米)] [数量]")
        print("")
        print("位置: 具体地址或经纬度坐标（如：39.9,116.4）")
        print("类别: 餐饮/美食、娱乐/休闲、景点/旅游、酒店、购物等")
        print("半径: 100-20000 米")
        print("数量: 1-20 个")
        print("")
        print("示例:")
        print("  python baidu_nearby.py '北京市朝阳区三里屯' 餐饮 2000 5")
        print("  python baidu_nearby.py '39.9,116.4' 景点 5000 10")
        sys.exit(1)
    
    location = sys.argv[1]
    category = sys.argv[2] if len(sys.argv) > 2 else None
    
    # 验证参数
    try:
        radius = int(sys.argv[3]) if len(sys.argv) > 3 else 1000
    except ValueError:
        print("错误: 半径必须为整数")
        sys.exit(1)
    
    try:
        limit = int(sys.argv[4]) if len(sys.argv) > 4 else 10
    except ValueError:
        print("错误: 数量必须为整数")
        sys.exit(1)
    
    print(f"🔍 正在搜索: {sanitize_address(location) or location}")
    if category:
        print(f"   类别: {category}")
    print(f"   半径: {radius}米")
    print(f"   数量: {limit}个")
    print("-" * 40)
    
    recommend_nearby(location, category, radius, limit)


if __name__ == '__main__':
    main()


path: scripts/baidu_search.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
百度搜索工具 - 安全修复版
使用百度站内搜索API进行网页搜索

修复:
- [SECURITY] 移除 SSL 验证禁用代码
- [SECURITY] 添加输入验证
- [SECURITY] 添加请求超时和重试限制
- [IMPROVE] 改进错误处理
"""

import os
import sys
import json
import urllib.parse
import urllib.request
import re
import ssl
import socket

# ============== 安全配置 ==============

# 使用系统默认 SSL 上下文（不禁用证书验证）
# 如果需要自定义 CA 证书，可以使用:
# import certifi
# ssl_context = ssl.create_default_context(cafile=certifi.where())
ssl_context = ssl.create_default_context()

# 请求超时设置（秒）
REQUEST_TIMEOUT = 10

# 最大重试次数
MAX_RETRIES = 2

# 最大结果数量限制
MAX_RESULTS = 50


def validate_input(query, num=10, page=1):
    """
    验证输入参数
    
    Returns:
        (is_valid, error_message)
    """
    # 验证查询词
    if not query or not isinstance(query, str):
        return False, "搜索词不能为空"
    
    # 限制查询长度
    if len(query) > 200:
        return False, "搜索词过长（最大200字符）"
    
    # 清理查询词（移除危险字符）
    query = query.strip()
    if not query:
        return False, "搜索词不能为空"
    
    # 验证数量
    if not isinstance(num, int) or num < 1:
        return False, "结果数量必须为正整数"
    
    if num > MAX_RESULTS:
        return False, f"结果数量超过最大限制（{MAX_RESULTS}）"
    
    # 验证页码
    if not isinstance(page, int) or page < 1:
        return False, "页码必须为正整数"
    
    return True, None


def sanitize_html(text):
    """
    清理 HTML 内容，移除潜在危险标签
    """
    if not text:
        return ""
    
    # 移除 HTML 标签
    text = re.sub(r'<[^>]+>', '', text)
    
    # 解码 HTML 实体
    text = text.replace('&amp;', '&')
    text = text.replace('&lt;', '<')
    text = text.replace('&gt;', '>')
    text = text.replace('&quot;', '"')
    text = text.replace('&#39;', "'")
    text = text.replace('&nbsp;', ' ')
    
    # 清理多余空白
    text = re.sub(r'\s+', ' ', text).strip()
    
    return text


def baidu_search(query, num=10, page=1):
    """
    执行百度搜索
    
    Args:
        query: 搜索关键词（最大200字符）
        num: 返回结果数量 (1-50)
        page: 页码
    
    Returns:
        dict: 搜索结果，格式:
        {
            'query': '搜索词',
            'total': 结果数量,
            'results': [
                {'title': '标题', 'link': '链接', 'abstract': '摘要'},
                ...
            ]
        }
        None: 搜索失败
    """
    # 输入验证
    is_valid, error = validate_input(query, num, page)
    if not is_valid:
        print(f"参数错误: {error}")
        return None
    
    # 清理查询词
    query = query.strip()
    
    headers = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
        'Accept-Encoding': 'identity',  # 不压缩，简化处理
        'Referer': 'https://www.baidu.com/',
        'Connection': 'keep-alive'
    }
    
    params = {
        'wd': query,
        'pn': (page - 1) * 10,
        'rn': min(num, MAX_RESULTS),
        'tn': 'baidu',
        'ie': 'utf-8'
    }
    
    url = 'https://www.baidu.com/s?' + urllib.parse.urlencode(params)
    
    # 请求重试
    html = None
    last_error = None
    
    for attempt in range(MAX_RETRIES):
        try:
            req = urllib.request.Request(url, headers=headers)
            
            # 使用安全的 SSL 上下文
            with urllib.request.urlopen(
                req, 
                timeout=REQUEST_TIMEOUT,
                context=ssl_context
            ) as response:
                html = response.read().decode('utf-8', errors='ignore')
            break  # 成功，退出重试
            
        except urllib.error.HTTPError as e:
            last_error = f"HTTP错误: {e.code} {e.reason}"
            if e.code == 429:  # 限流
                break
        except urllib.error.URLError as e:
            last_error = f"URL错误: {e.reason}"
        except socket.timeout:
            last_error = "请求超时"
        except Exception as e:
            last_error = f"未知错误: {str(e)}"
    
    if html is None:
        print(f"搜索失败: {last_error}")
        return None
    
    try:
        results = []
        
        # 查找所有结果容器
        result_pattern = r'<div[^>]*class="[^"]*result[^"]*"[^>]*>(.*?)</div>\s*</div>'
        result_blocks = re.findall(result_pattern, html, re.DOTALL)
        
        if not result_blocks:
            # 尝试另一种模式
            result_pattern = r'<div[^>]*class="c-container"[^>]*>(.*?)</div>\s*</div>'
            result_blocks = re.findall(result_pattern, html, re.DOTALL)
        
        for block in result_blocks[:num]:
            try:
                # 提取标题和链接
                title_match = re.search(
                    r'<h3[^>]*>.*?<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>.*?</h3>',
                    block,
                    re.DOTALL
                )
                if not title_match:
                    continue
                
                link = title_match.group(1)
                title = sanitize_html(title_match.group(2))
                
                # 验证链接格式（只允许 http/https）
                if not link.startswith(('http://', 'https://')):
                    continue
                
                # 提取摘要
                abstract = ""
                abstract_patterns = [
                    r'<span class="content-right[^"]*">(.*?)</span>',
                    r'<div class="content-right[^"]*">(.*?)</div>',
                    r'<span class="c-abstract">(.*?)</span>',
                    r'<div class="c-abstract">(.*?)</div>',
                ]
                
                for pattern in abstract_patterns:
                    abs_match = re.search(pattern, block, re.DOTALL)
                    if abs_match:
                        abstract = sanitize_html(abs_match.group(1))
                        # 限制摘要长度
                        if len(abstract) > 200:
                            abstract = abstract[:200] + '...'
                        break
                
                if title:
                    results.append({
                        'title': title,
                        'link': link,
                        'abstract': abstract
                    })
            except Exception:
                continue
        
        # 备用方案：直接匹配所有标题和链接
        if not results:
            h3_pattern = r'<h3[^>]*>.*?<a[^>]*href="([^"]*)"[^>]*>(.*?)</a>.*?</h3>'
            matches = re.findall(h3_pattern, html, re.DOTALL)
            
            for link, title_html in matches[:num]:
                title = sanitize_html(title_html)
                # 验证链接
                if title and link and link.startswith(('http://', 'https://')):
                    results.append({
                        'title': title,
                        'link': link,
                        'abstract': ''
                    })
        
        return {
            'query': query,
            'total': len(results),
            'results': results
        }
        
    except Exception as e:
        print(f"解析结果出错: {e}")
        return None


def main():
    if len(sys.argv) < 2:
        print("使用方法: python baidu_search.py <搜索词> [结果数量]")
        print("示例: python baidu_search.py 'Python教程' 10")
        sys.exit(1)
    
    query = sys.argv[1]
    
    # 验证数量参数
    try:
        num = int(sys.argv[2]) if len(sys.argv) > 2 else 10
    except ValueError:
        print("错误: 结果数量必须为整数")
        sys.exit(1)
    
    print(f"🔍 百度搜索: {query}\n")
    
    result = baidu_search(query, num)
    
    if result and result['results']:
        for i, item in enumerate(result['results'], 1):
            print(f"{i}. {item['title']}")
            print(f"   链接: {item['link']}")
            if item['abstract']:
                print(f"   摘要: {item['abstract']}")
            print()
    else:
        print("未找到搜索结果")


if __name__ == '__main__':
    main()


path: _meta.json
content: ownerId: kn747z60pfarj1jd6chhcttzan81yj7n
slug: baidu-nearby
version: 1.0.3
publishedAt: 1774018445106

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