Skill: raydoomed/tavilysearch (version 1.0.4)

--- name: tavily-search description: Tavily 搜索引擎集成，支持 web 搜索、内容提取、实时新闻查询等功能。使用场景：需要搜索最新网络信息、查找实时新闻、获取专业资料、验证事实准确性等。 env: - name: TAVILY_API_KEY description: Tavily API 密钥，用于调用搜索、提取、爬取等功能 required: true --- # Tavily Search Skill Tavily 是专门为 AI 代理设计的搜索引擎，提供快速、准确的网络搜索能力，支持实时信息检索、内容提取、多维度过滤等功能。 ## 前置配置 1. 首先获取 Tavily API Key：https://tavily.com/ 2. 安装依赖：`pip install -r requirements.txt` 3. 新建 `.env` 文件，填入你的 API Key： ```env TAVILY_API_KEY=tvly-你的实际API密钥 ``` 4. 程序会自动读取技能目录下的 `.env` 文件，无需配置全局环境变量 ## 核心功能 ### 1. 网页搜索 Tavily 核心搜索能力，支持多维度过滤和优化： ```bash # 命令行调用 python scripts/search.py search "搜索关键词" [选项] # 最简调用 curl -X POST https://api.tavily.com/search \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAVILY_API_KEY" \ -d '{"query": "搜索关键词", "include_answer": true}' ``` **搜索选项：** | 参数 | 说明 | 默认值 | |------|------|--------| | `--depth <basic/advanced/fast/ultra-fast>` | 搜索深度，basic=1信用点，advanced=2信用点 | `basic` | | `--chunks <1-3>` | 每个来源返回的内容片段数量（仅advanced深度有效） | `3` | | `--max <0-20>` | 返回结果数量 | `5` | | `--topic <general/news/finance>` | 搜索主题，news适合实时新闻 | `general` | | `--time <day/week/month/year>` | 相对时间范围过滤 | 无 | | `--start-date <YYYY-MM-DD>` | 仅返回该日期之后的结果 | 无 | | `--end-date <YYYY-MM-DD>` | 仅返回该日期之前的结果 | 无 | | `--answer` | 包含LLM生成的直接答案 | `false` | | `--raw` | 包含网页原始内容 | `false` | | `--images` | 包含图片搜索结果 | `false` | | `--image-descriptions` | 包含图片描述文本 | `false` | | `--favicon` | 包含网站图标URL | `false` | | `--include <域名1,域名2>` | 限定搜索的域名列表（最多300个） | 无 | | `--exclude <域名1,域名2>` | 排除的域名列表（最多150个） | 无 | | `--country <国家/地区>` | 优先返回指定国家/地区的结果 | 无 | | `--auto-params` | 开启自动参数优化（自动调整搜索深度等） | `false` | | `--exact` | 精确匹配查询短语 | `false` | | `--usage` | 响应中包含用量信息 | `false` | | `--json` | 输出JSON格式 | `false` | **使用示例：** ```bash # 搜索2025年全年AI行业新闻 python scripts/search.py search "2025年AI行业大事件" \ --topic news \ --start-date 2025-01-01 \ --end-date 2025-12-31 \ --max 20 \ --answer # 高级技术搜索，限定学术/技术域名 python scripts/search.py search "大语言模型推理优化技术" \ --depth advanced \ --chunks 3 \ --include arxiv.org,github.com,stackoverflow.com \ --favicon ``` --- ### 2. 网页内容提取 批量提取指定URL的结构化内容，自动清理广告和无关元素： ```bash # 命令行调用 python scripts/search.py extract "url1,url2,..." [选项] ``` **提取选项：** | 参数 | 说明 | 默认值 | |------|------|--------| | `--query <关键词>` | 按关键词重排内容片段 | 无 | | `--chunks <1-5>` | 每个URL返回的片段数量 | `3` | | `--extract-depth <basic/advanced>` | 提取深度，advanced支持表格/嵌入内容 | `basic` | | `--markdown` | 输出Markdown格式 | `false` | | `--images` | 包含图片 | `false` | | `--favicon` | 包含网站图标 | `false` | | `--json` | 输出JSON格式 | `false` | **使用示例：** ```bash # 批量提取3个技术文档，按"API"关键词重排内容 python scripts/search.py extract "https://docs.tavily.com,https://example.com/docs,https://api.example.com" \ --query "API" \ --extract-depth advanced \ --chunks 5 \ --markdown > docs.md ``` --- ### 3. 整站爬取 自动遍历整个网站，智能发现并爬取所有相关页面： ```bash # 命令行调用 python scripts/search.py crawl "根URL" [选项] ``` **爬取选项：** | 参数 | 说明 | 默认值 | |------|------|--------| | `--instructions <文本>` | 自然语言爬取指令（如"查找所有API文档页面"） | 无 | | `--max-depth <1-5>` | 最大爬取深度 | `1` | | `--max-breadth <1-500>` | 每层页面的最大链接数 | `20` | | `--limit <数字>` | 总爬取页面上限 | `50` | | `--select-paths <正则1,正则2>` | 只爬取匹配路径的页面 | 无 | | `--select-domains <域名1,域名2>` | 只爬取指定域名的页面 | 无 | | `--exclude-paths <正则1,正则2>` | 排除匹配路径的页面 | 无 | | `--exclude-domains <域名1,域名2>` | 排除指定域名的页面 | 无 | | `--allow-external` | 允许爬取外部域名 | `true` | | `--extract-depth <basic/advanced>` | 内容提取深度 | `basic` | | `--markdown` | 输出Markdown格式 | `false` | | `--images` | 包含图片 | `false` | | `--favicon` | 包含网站图标 | `false` | | `--json` | 输出JSON格式 | `false` | **使用示例：** ```bash # 爬取Tavily文档站，只收集API相关页面 python scripts/search.py crawl "https://docs.tavily.com" \ --instructions "Find all API documentation pages" \ --max-depth 3 \ --limit 100 \ --select-paths "/api/.*,/documentation/.*" ``` --- ### 4. 深度研究 自动进行多轮搜索、信息整合，生成结构化研究报告： ```bash # 命令行调用 python scripts/search.py research "研究主题" [选项] ``` **研究选项：** | 参数 | 说明 | 默认值 | |------|------|--------| | `--model <mini/pro/auto>` | 研究模型，mini适合简单主题，pro适合复杂主题 | `auto` | | `--citation <numbered/mla/apa/chicago>` | 引用格式 | `numbered` | | `--json` | 输出JSON格式 | `false` | **使用示例：** ```bash # 生成行业研究报告 python scripts/search.py research "2026年AI Agent行业发展趋势" \ --model pro \ --citation apa # 查询已有研究任务结果 python scripts/search.py get-research "任务ID" ``` --- ### 5. 用量查询 > 官方文档：https://docs.tavily.com/documentation/api-reference/endpoint/usage 查看API信用点使用情况和剩余额度： ```bash # 脚本调用 python scripts/search.py usage ``` **返回结构说明：** ```json { "key": { "usage": 135, "limit": 1000, "search_usage": 81, "crawl_usage": 0, "extract_usage": 2, "map_usage": 0, "research_usage": 52 }, "account": { "current_plan": "Researcher", "plan_usage": 135, "plan_limit": 1000, "search_usage": 81, "crawl_usage": 0, "extract_usage": 2, "map_usage": 0, "research_usage": 52, "paygo_usage": 0, "paygo_limit": null } } ``` - `key` 字段：当前使用的API Key的用量统计 - `account` 字段：整个Tavily账户的总用量统计 - 用量查询结果有5-10分钟缓存，如需实时数据请直接调用官方API ### 2. 高级搜索参数 | 参数 | 类型 | 默认值 | 说明 | |------|------|--------|------| | `query` | string | 必填 | 搜索关键词 | | `search_depth` | enum | `basic` | 搜索深度：`basic`(1信用点, 平衡) / `advanced`(2信用点, 更高精度) / `fast`(快速) / `ultra-fast`(极速) | | `max_results` | integer | 5 | 返回结果数量，范围 0-20 | | `topic` | enum | `general` | 搜索主题：`general`(通用) / `news`(新闻) / `finance`(财经) | | `time_range` | enum | 可选 | 时间范围：`day`/`week`/`month`/`year` | | `include_answer` | boolean | false | 是否包含 LLM 生成的直接答案 | | `include_images` | boolean | false | 是否返回图片结果 | | `include_raw_content` | boolean | false | 是否返回网页原始内容 | | `include_domains` | array | 可选 | 限定搜索的域名列表 | | `exclude_domains` | array | 可选 | 排除的域名列表 | | `country` | string | 可选 | 按国家/地区优先返回结果 | ### 3. 响应格式 ```json { "query": "搜索关键词", "answer": "LLM生成的直接答案（当include_answer=true时返回）", "results": [ { "title": "结果标题", "url": "页面链接", "content": "内容摘要", "score": 0.85, "favicon": "网站图标链接" } ], "response_time": 1.23, "usage": { "credits": 1 } } ``` ## 使用示例 ### 搜索最新新闻 ```bash curl -X POST https://api.tavily.com/search \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAVILY_API_KEY" \ -d '{ "query": "2026年3月科技行业最新动态", "topic": "news", "time_range": "day", "include_answer": true, "max_results": 10 }' ``` ### 专业资料搜索 ```bash curl -X POST https://api.tavily.com/search \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAVILY_API_KEY" \ -d '{ "query": "大语言模型推理优化技术", "search_depth": "advanced", "include_raw_content": true, "include_domains": ["arxiv.org", "github.com", "openai.com"] }' ``` ## 注意事项 - API 调用信用点：基础搜索1点/次，高级搜索2点/次，内容提取1点/次，整站爬取2点/次，深度研究5点/次 - 免费 tier 提供 1000 次/月搜索额度 - 搜索结果默认按相关性排序，score 越高越相关 - 实时新闻查询请使用 `topic: "news"` 参数确保获取最新内容 - 深度研究任务耗时较长（通常30-60秒），请耐心等待 - 爬取大量页面时建议设置合理的 limit 参数，避免信用点过度消耗 ## 版本更新 ### v1.0.4 (2026-03-15) - ✅ 修复环境变量读取逻辑，只读取 TAVILY_API_KEY 避免暴露敏感信息 - ✅ 在 SKILL.md 中添加环境变量声明，确保注册数据与实际需求一致 - ✅ 移除未使用的 python-dotenv 依赖，优化依赖管理 - ✅ 添加 urllib3 依赖，确保脚本正常运行 - ✅ 所有核心功能测试通过

path: requirements.txt
content: requests>=2.31.0
urllib3>=2.0.0

path: scripts/search.py
content: #!/usr/bin/env python3
import warnings
warnings.filterwarnings("ignore", category=DeprecationWarning)
warnings.filterwarnings("ignore", message="urllib3 v2 only supports OpenSSL 1.1.1+")
import os
import sys
import json
import time
import requests
from typing import Optional, List, Dict, Any

# 只读取TAVILY_API_KEY环境变量
with open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env"), "r") as f:
    for line in f:
        line = line.strip()
        if line and not line.startswith("#") and "=" in line:
            key, value = line.split("=", 1)
            key = key.strip()
            if key == "TAVILY_API_KEY":
                os.environ[key] = value.strip()

class TavilySearch:
    def __init__(self, [REDACTED_SECRET] = None):
        self.[REDACTED_SECRET] or os.getenv("TAVILY_API_KEY")
        if not self.api_key:
            raise ValueError("TAVILY_API_KEY 未配置，请在 .env 文件中设置")
        self.base_url = "https://api.tavily.com"
    
    def search(
        self,
        query: str,
        search_depth: str = "basic",
        chunks_per_source: int = 3,
        max_results: int = 5,
        topic: str = "general",
        time_range: Optional[str] = None,
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
        include_answer: bool = False,
        include_raw_content: bool = False,
        include_images: bool = False,
        include_image_descriptions: bool = False,
        include_favicon: bool = False,
        include_domains: Optional[List[str]] = None,
        exclude_domains: Optional[List[str]] = None,
        country: Optional[str] = None,
        auto_parameters: bool = False,
        exact_match: bool = False,
        include_usage: bool = False
    ) -> Dict[str, Any]:
        """执行 Tavily 搜索"""
        payload = {
            "query": query,
            "search_depth": search_depth,
            "chunks_per_source": chunks_per_source,
            "max_results": max_results,
            "topic": topic,
            "include_answer": include_answer,
            "include_raw_content": include_raw_content,
            "include_images": include_images,
            "include_image_descriptions": include_image_descriptions,
            "include_favicon": include_favicon,
            "auto_parameters": auto_parameters,
            "exact_match": exact_match,
            "include_usage": include_usage
        }
        
        if time_range:
            payload["time_range"] = time_range
        if start_date:
            payload["start_date"] = start_date
        if end_date:
            payload["end_date"] = end_date
        if include_domains:
            payload["include_domains"] = include_domains
        if exclude_domains:
            payload["exclude_domains"] = exclude_domains
        if country:
            payload["country"] = country
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.api_key}"
        }
        
        response = requests.post(f"{self.base_url}/search", json=payload, headers=headers)
        response.raise_for_status()
        return response.json()
    
    def extract(
        self,
        urls: List[str],
        query: Optional[str] = None,
        chunks_per_source: int = 3,
        extract_depth: str = "basic",
        include_images: bool = False,
        include_favicon: bool = False,
        format: str = "markdown"
    ) -> Dict[str, Any]:
        """提取网页内容"""
        payload = {
            "urls": urls,
            "chunks_per_source": chunks_per_source,
            "extract_depth": extract_depth,
            "include_images": include_images,
            "include_favicon": include_favicon,
            "format": format
        }
        if query:
            payload["query"] = query
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.api_key}"
        }
        
        response = requests.post(f"{self.base_url}/extract", json=payload, headers=headers)
        response.raise_for_status()
        return response.json()
    
    def create_research_task(
        self,
        input: str,
        model: str = "auto",
        stream: bool = False,
        output_schema: Optional[Dict] = None,
        citation_format: str = "numbered"
    ) -> Dict[str, Any]:
        """创建深度研究任务"""
        payload = {
            "input": input,
            "model": model,
            "stream": stream,
            "citation_format": citation_format
        }
        if output_schema:
            payload["output_schema"] = output_schema
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.api_key}"
        }
        
        response = requests.post(f"{self.base_url}/research", json=payload, headers=headers)
        response.raise_for_status()
        return response.json()
    
    def get_research_result(self, request_id: str) -> Dict[str, Any]:
        """获取深度研究任务结果"""
        headers = {
            "Authorization": f"Bearer {self.api_key}"
        }
        
        response = requests.get(f"{self.base_url}/research/{request_id}", headers=headers)
        response.raise_for_status()
        return response.json()
    
    def crawl(
        self,
        url: str,
        instructions: Optional[str] = None,
        max_depth: int = 1,
        max_breadth: int = 20,
        limit: int = 50,
        select_paths: Optional[List[str]] = None,
        select_domains: Optional[List[str]] = None,
        exclude_paths: Optional[List[str]] = None,
        exclude_domains: Optional[List[str]] = None,
        allow_external: bool = True,
        include_images: bool = False,
        extract_depth: str = "basic",
        format: str = "markdown",
        include_favicon: bool = False
    ) -> Dict[str, Any]:
        """整站爬取"""
        payload = {
            "url": url,
            "max_depth": max_depth,
            "max_breadth": max_breadth,
            "limit": limit,
            "allow_external": allow_external,
            "include_images": include_images,
            "extract_depth": extract_depth,
            "format": format,
            "include_favicon": include_favicon
        }
        
        if instructions:
            payload["instructions"] = instructions
        if select_paths:
            payload["select_paths"] = select_paths
        if select_domains:
            payload["select_domains"] = select_domains
        if exclude_paths:
            payload["exclude_paths"] = exclude_paths
        if exclude_domains:
            payload["exclude_domains"] = exclude_domains
        
        headers = {
            "Content-Type": "application/json",
            "Authorization": f"Bearer {self.api_key}"
        }
        
        response = requests.post(f"{self.base_url}/crawl", json=payload, headers=headers)
        response.raise_for_status()
        return response.json()
    
    def format_crawl_results(self, results: Dict[str, Any]) -> str:
        """格式化爬取结果"""
        output = []
        output.append("🕷️ 整站爬取结果：")
        output.append("=" * 80)
        
        output.append(f"🌐 根URL: {results.get('base_url', '未知')}")
        output.append(f"⏱️  耗时: {results.get('response_time', 0)}s")
        output.append(f"📄 共爬取 {len(results.get('results', []))} 个页面")
        output.append("")
        
        for i, page in enumerate(results["results"][:10], 1):  # 只显示前10个页面
            output.append(f"{i}. {page['url']}")
            output.append(f"   📝 内容摘要: {page['raw_content'][:200]}..." if len(page['raw_content']) > 200 else f"   📝 内容: {page['raw_content']}")
            output.append("")
        
        if len(results.get('results', [])) > 10:
            output.append(f"... 还有 {len(results['results']) - 10} 个页面未显示")
        
        if results.get("usage"):
            output.append(f"\n💡 本次调用消耗 {results['usage']['credits']} 个信用点")
        
        return "\n".join(output)
    
    def research(
        self,
        input: str,
        model: str = "auto",
        output_schema: Optional[Dict] = None,
        citation_format: str = "numbered",
        poll_interval: int = 5,
        max_polls: int = 60
    ) -> Dict[str, Any]:
        """执行完整深度研究（自动轮询结果）"""
        # 创建任务
        task = self.create_research_task(
            input=input,
            model=model,
            output_schema=output_schema,
            citation_format=citation_format
        )
        request_id = task["request_id"]
        print(f"✅ 研究任务已创建，ID: {request_id}，正在生成报告...")
        
        # 轮询结果
        for _ in range(max_polls):
            result = self.get_research_result(request_id)
            if result["status"] == "completed":
                return result
            elif result["status"] == "failed":
                raise Exception(f"研究任务失败: {result.get('error', '未知错误')}")
            time.sleep(poll_interval)
        
        raise Exception(f"研究任务超时，已等待 {max_polls * poll_interval} 秒")
    
    def get_usage(self) -> Dict[str, Any]:
        """查询API用量（完全按照官方文档实现）"""
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Cache-Control": "no-cache, no-store, must-revalidate",
            "Pragma": "no-cache",
            "Expires": "0"
        }
        
        # 强制禁用所有缓存
        import socket
        import urllib3
        urllib3.disable_warnings(urllib3.exceptions.NotOpenSSLWarning)
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
        # 清空DNS缓存
        socket.gethostbyname.cache_clear() if hasattr(socket.gethostbyname, 'cache_clear') else None
        
        response = requests.get(
            f"{self.base_url}/usage", 
            headers=headers,
            allow_redirects=False
        )
        response.raise_for_status()
        return response.json()
    
    def format_results(self, results: Dict[str, Any], show_content: bool = True) -> str:
        """格式化搜索结果为易读文本"""
        output = []
        
        if results.get("answer"):
            output.append("📝 直接答案：")
            output.append(results["answer"])
            output.append("")
        
        output.append(f"🔍 搜索结果（共 {len(results['results'])} 条，耗时 {results['response_time']}s）：")
        output.append("=" * 80)
        
        for i, result in enumerate(results["results"], 1):
            output.append(f"{i}. {result['title']}")
            output.append(f"   🔗 {result['url']}")
            if show_content:
                output.append(f"   📄 {result['content'][:300]}..." if len(result['content']) > 300 else f"   📄 {result['content']}")
            output.append(f"   ⭐ 相关性：{result['score']:.2f}")
            output.append("")
        
        if results.get("usage"):
            output.append(f"💡 本次调用消耗 {results['usage']['credits']} 个信用点")
        
        return "\n".join(output)
    
    def format_extract_results(self, results: Dict[str, Any]) -> str:
        """格式化提取结果"""
        output = []
        output.append(f"📄 内容提取结果（共 {len(results['results'])} 个页面）：")
        output.append("=" * 80)
        
        for data in results["results"]:
            output.append(f"🌐 {data['url']}")
            output.append(f"📝 标题：{data['title']}")
            output.append(f"📄 内容摘要：{data['raw_content'][:500]}..." if len(data['raw_content']) > 500 else f"📄 内容：{data['raw_content']}")
            output.append("")
        
        if results.get("usage"):
            output.append(f"💡 本次调用消耗 {results['usage']['credits']} 个信用点")
        
        return "\n".join(output)
    
    def format_research_results(self, results: Dict[str, Any]) -> str:
        """格式化研究报告"""
        output = []
        output.append("📚 深度研究报告：")
        output.append("=" * 80)
        
        output.append(f"🆔 任务ID: {results.get('request_id', '未知')}")
        output.append(f"⚡ 状态: {results.get('status', '未知')}")
        output.append(f"⏱️  耗时: {results.get('response_time', 0)}s")
        output.append("")
        
        if results.get("content"):
            output.append("📝 报告内容：")
            output.append(results["content"])
            output.append("")
        
        if results.get("sources"):
            output.append(f"🔗 参考来源（共 {len(results['sources'])} 个）：")
            for i, source in enumerate(results["sources"], 1):
                output.append(f"{i}. {source['title']} - {source['url']}")
        
        return "\n".join(output)
    
    def format_usage(self, usage: Dict[str, Any]) -> str:
        """格式化用量信息"""
        output = []
        output.append("📊 API 用量统计：")
        output.append("=" * 80)
        
        # API Key 级别用量
        output.append("🔑 API Key 用量：")
        key_usage = usage.get("key", {})
        output.append(f"  总使用量：{key_usage.get('usage', 0)} / {key_usage.get('limit', '无限制')}")
        output.append(f"  搜索：{key_usage.get('search_usage', 0)} 次")
        output.append(f"  提取：{key_usage.get('extract_usage', 0)} 次")
        output.append(f"  爬取：{key_usage.get('crawl_usage', 0)} 次")
        output.append(f"  站点地图：{key_usage.get('map_usage', 0)} 次")
        output.append(f"  深度研究：{key_usage.get('research_usage', 0)} 次")
        output.append("")
        
        # 账户级别用量
        output.append("💳 账户套餐用量：")
        account = usage.get("account", {})
        output.append(f"  当前套餐：{account.get('current_plan', '未知')}")
        output.append(f"  套餐使用：{account.get('plan_usage', 0)} / {account.get('plan_limit', '无限制')}")
        if account.get('paygo_limit') is not None:
            output.append(f"  按量付费：{account.get('paygo_usage', 0)} / {account.get('paygo_limit', '无限制')}")
        output.append(f"  搜索：{account.get('search_usage', 0)} 次")
        output.append(f"  提取：{account.get('extract_usage', 0)} 次")
        output.append(f"  爬取：{account.get('crawl_usage', 0)} 次")
        output.append(f"  站点地图：{account.get('map_usage', 0)} 次")
        output.append(f"  深度研究：{account.get('research_usage', 0)} 次")
        
        return "\n".join(output)

def main():
    import sys
    import argparse
    
    if len(sys.argv) < 2:
        print("使用方式: python search.py <命令> [参数]")
        print("命令:")
        print("  search <关键词>   网页搜索")
        print("  extract <url1,url2>  提取指定网页内容")
        print("  crawl <根URL>  整站爬取，自动遍历所有相关页面")
        print("  research <主题>  生成深度研究报告（自动等待结果）")
        print("  get-research <任务ID>  查询已有研究任务结果")
        print("  usage  查询API用量")
        print("\n搜索选项:")
        print("  --depth <basic/advanced/fast/ultra-fast>  搜索深度")
        sys.exit(1)
    
    command = sys.argv[1]
    ts = TavilySearch()
    
    if command == "usage":
        usage_data = ts.get_usage()
        output = ts.format_usage(usage_data)
        print(output)
        sys.exit(0)
    elif command == "search":
        if len(sys.argv) < 3:
            print("错误：search 命令需要关键词参数")
            sys.exit(1)
        
        parser = argparse.ArgumentParser()
        parser.add_argument("command")
        parser.add_argument("query")
        parser.add_argument("--depth", default="basic", choices=["basic", "advanced", "fast", "ultra-fast"])
        parser.add_argument("--chunks", type=int, default=3)
        parser.add_argument("--max", type=int, default=5, dest="max_results")
        parser.add_argument("--topic", default="general", choices=["general", "news", "finance"])
        parser.add_argument("--time", default=None, choices=["day", "week", "month", "year"], dest="time_range")
        parser.add_argument("--start-date", default=None)
        parser.add_argument("--end-date", default=None)
        parser.add_argument("--answer", action="store_true", dest="include_answer")
        parser.add_argument("--raw", action="store_true", dest="include_raw_content")
        parser.add_argument("--images", action="store_true", dest="include_images")
        parser.add_argument("--image-descriptions", action="store_true", dest="include_image_descriptions")
        parser.add_argument("--favicon", action="store_true", dest="include_favicon")
        parser.add_argument("--include", default=None, dest="include_domains")
        parser.add_argument("--exclude", default=None, dest="exclude_domains")
        parser.add_argument("--country", default=None)
        parser.add_argument("--auto-params", action="store_true", dest="auto_parameters")
        parser.add_argument("--exact", action="store_true", dest="exact_match")
        parser.add_argument("--usage", action="store_true", dest="include_usage")
        parser.add_argument("--json", action="store_true")
        
        args = parser.parse_args()
        
        # 处理域名列表
        include_domains = args.include_domains.split(",") if args.include_domains else None
        exclude_domains = args.exclude_domains.split(",") if args.exclude_domains else None
        
        results = ts.search(
            query=args.query,
            search_depth=args.depth,
            chunks_per_source=args.chunks,
            max_results=args.max_results,
            topic=args.topic,
            time_range=args.time_range,
            start_date=args.start_date,
            end_date=args.end_date,
            include_answer=args.include_answer,
            include_raw_content=args.include_raw_content,
            include_images=args.include_images,
            include_image_descriptions=args.include_image_descriptions,
            include_favicon=args.include_favicon,
            include_domains=include_domains,
            exclude_domains=exclude_domains,
            country=args.country,
            auto_parameters=args.auto_parameters,
            exact_match=args.exact_match,
            include_usage=args.include_usage
        )
        
        if args.json:
            print(json.dumps(results, ensure_ascii=False, indent=2))
        else:
            print(ts.format_results(results))
        sys.exit(0)
    elif command == "extract":
        if len(sys.argv) < 3:
            print("错误：extract 命令需要URL参数")
            sys.exit(1)
        
        parser = argparse.ArgumentParser()
        parser.add_argument("command")
        parser.add_argument("urls")
        parser.add_argument("--query", default=None)
        parser.add_argument("--chunks", type=int, default=3)
        parser.add_argument("--extract-depth", default="basic", choices=["basic", "advanced"])
        parser.add_argument("--markdown", action="store_true")
        parser.add_argument("--images", action="store_true")
        parser.add_argument("--favicon", action="store_true")
        parser.add_argument("--json", action="store_true")
        
        args = parser.parse_args()
        urls = args.urls.split(",")
        
        results = ts.extract(
            urls=urls,
            query=args.query,
            chunks_per_source=args.chunks,
            extract_depth=args.extract_depth,
            include_images=args.images,
            include_favicon=args.favicon
        )
        
        if args.json:
            print(json.dumps(results, ensure_ascii=False, indent=2))
        else:
            print(ts.format_extract_results(results))
        sys.exit(0)
    elif command == "crawl":
        if len(sys.argv) < 3:
            print("错误：crawl 命令需要根URL参数")
            sys.exit(1)
        
        parser = argparse.ArgumentParser()
        parser.add_argument("command")
        parser.add_argument("root_url")
        parser.add_argument("--instructions", default=None)
        parser.add_argument("--max-depth", type=int, default=1)
        parser.add_argument("--max-breadth", type=int, default=20)
        parser.add_argument("--limit", type=int, default=50)
        parser.add_argument("--select-paths", default=None)
        parser.add_argument("--exclude-paths", default=None)
        parser.add_argument("--allow-external", action="store_true", default=True)
        parser.add_argument("--extract-depth", default="basic", choices=["basic", "advanced"])
        parser.add_argument("--markdown", action="store_true")
        parser.add_argument("--images", action="store_true")
        parser.add_argument("--favicon", action="store_true")
        parser.add_argument("--json", action="store_true")
        
        args = parser.parse_args()
        
        select_paths = args.select_paths.split(",") if args.select_paths else None
        exclude_paths = args.exclude_paths.split(",") if args.exclude_paths else None
        
        results = ts.crawl(
            url=args.root_url,
            instructions=args.instructions,
            max_depth=args.max_depth,
            max_breadth=args.max_breadth,
            limit=args.limit,
            select_paths=select_paths,
            exclude_paths=exclude_paths,
            allow_external=args.allow_external,
            extract_depth=args.extract_depth,
            include_images=args.images,
            include_favicon=args.favicon
        )
        
        if args.json:
            print(json.dumps(results, ensure_ascii=False, indent=2))
        else:
            print(ts.format_crawl_results(results))
        sys.exit(0)
    elif command == "research":
        if len(sys.argv) < 3:
            print("错误：research 命令需要研究主题参数")
            sys.exit(1)
        
        parser = argparse.ArgumentParser()
        parser.add_argument("command")
        parser.add_argument("topic")
        parser.add_argument("--model", default="auto", choices=["mini", "pro", "auto"])
        parser.add_argument("--citation", default="numbered", choices=["numbered", "mla", "apa", "chicago"])
        parser.add_argument("--json", action="store_true")
        
        args = parser.parse_args()
        
        results = ts.research(
            input=args.topic,
            model=args.model,
            citation_format=args.citation
        )
        
        if args.json:
            print(json.dumps(results, ensure_ascii=False, indent=2))
        else:
            print(ts.format_research_results(results))
        sys.exit(0)
    elif command == "get-research":
        if len(sys.argv) < 3:
            print("错误：get-research 命令需要任务ID参数")
            sys.exit(1)
        
        task_id = sys.argv[2]
        results = ts.get_research(task_id)
        
        if "--json" in sys.argv:
            print(json.dumps(results, ensure_ascii=False, indent=2))
        else:
            print(ts.format_research_results(results))
        sys.exit(0)
    else:
        print(f"错误：未知命令 {command}")
        sys.exit(1)

if __name__ == "__main__":
    main()


path: references/api-docs.md
content: # Tavily API 完整文档
版本：v1.0.0
最后更新：2026-03-14

## 端点列表
| 端点 | 方法 | 功能 | 信用点消耗 |
|------|------|------|------------|
| `/search` | POST | 网页搜索 | 1-2 |
| `/extract` | POST | 网页内容提取 | 1/5个URL（基础）/ 2/5个URL（高级） |
| `/crawl` | POST | 整站爬取 | 1/10个页面（无指令）/ 2/10个页面（有指令） |
| `/research` | POST | 创建深度研究任务 | 10+ |
| `/research/{request_id}` | GET | 查询研究任务结果 | 0 |
| `/usage` | GET | 用量查询 | 0 |

## 更新日志
### v1.0.0 (2026-03-14)
- ✅ 修复 Crawl API 参数错误：`root_url` → `url`
- ✅ 修复 Research API 参数错误：`topic` → `input`，`citation_style` → `citation_format`
- ✅ 修复 Extract API 响应格式：结果从字典类型改为列表类型，字段名 `content` → `raw_content`
- ✅ 所有核心功能测试通过

## 错误码说明
| 错误码 | 含义 |
|--------|------|
| 400 | 请求参数错误 |
| 401 | API Key 无效 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 429 | 超出速率限制 |
| 432 | 内容政策违规 |
| 433 | 搜索查询违反服务条款 |
| 500 | 服务器内部错误 |

## 速率限制
| 套餐 | 速率限制 | 月度额度 |
|------|----------|----------|
| Free/Researcher | 10请求/分钟 | 1000信用点 |
| Project | 30请求/分钟 | 4000信用点 |
| Bootstrap | 100请求/分钟 | 15000信用点 |
| Startup | 300请求/分钟 | 38000信用点 |
| Growth | 1000请求/分钟 | 100000信用点 |
| Enterprise | 可定制 | 可定制 |

## 各API详细说明

### 1. Search API
**功能**：网页搜索，支持多维度过滤
**请求参数**：
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `query` | string | ✅ | - | 搜索关键词 |
| `search_depth` | enum | ❌ | `basic` | 搜索深度：`basic`/`advanced`/`fast`/`ultra-fast` |
| `chunks_per_source` | integer | ❌ | 3 | 每个来源返回的片段数量（1-3，仅advanced有效） |
| `max_results` | integer | ❌ | 5 | 返回结果数量（0-20） |
| `topic` | enum | ❌ | `general` | 搜索主题：`general`/`news`/`finance` |
| `time_range` | enum | ❌ | - | 相对时间范围：`day`/`week`/`month`/`year` |
| `start_date` | string | ❌ | - | 开始日期，格式YYYY-MM-DD |
| `end_date` | string | ❌ | - | 结束日期，格式YYYY-MM-DD |
| `include_answer` | boolean/string | ❌ | false | 包含直接答案：`true`/`basic`/`advanced` |
| `include_raw_content` | boolean/string | ❌ | false | 包含原始内容：`true`/`markdown`/`text` |
| `include_images` | boolean | ❌ | false | 包含图片结果 |
| `include_image_descriptions` | boolean | ❌ | false | 包含图片描述 |
| `include_favicon` | boolean | ❌ | false | 包含网站图标 |
| `include_domains` | array | ❌ | - | 限定搜索的域名列表（最多300个） |
| `exclude_domains` | array | ❌ | - | 排除的域名列表（最多150个） |
| `country` | string | ❌ | - | 优先返回的国家/地区 |
| `auto_parameters` | boolean | ❌ | false | 自动参数优化 |
| `exact_match` | boolean | ❌ | false | 精确匹配查询短语 |
| `include_usage` | boolean | ❌ | false | 包含用量信息 |

---

### 2. Extract API
**功能**：批量提取指定URL的结构化内容
**请求参数**：
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `urls` | string/array | ✅ | - | 要提取的URL列表 |
| `query` | string | ❌ | - | 关键词，用于重排内容片段 |
| `chunks_per_source` | integer | ❌ | 3 | 每个URL返回的片段数量（1-5） |
| `extract_depth` | enum | ❌ | `basic` | 提取深度：`basic`/`advanced` |
| `include_images` | boolean | ❌ | false | 包含图片 |
| `include_favicon` | boolean | ❌ | false | 包含网站图标 |
| `format` | enum | ❌ | `markdown` | 输出格式：`markdown`/`text` |
| `timeout` | float | ❌ | - | 超时时间（1-60秒） |
| `include_usage` | boolean | ❌ | false | 包含用量信息 |

---

### 3. Crawl API
**功能**：整站爬取，自动遍历所有相关页面
**请求参数**：
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `url` | string | ✅ | - | 根URL |
| `instructions` | string | ❌ | - | 自然语言爬取指令 |
| `chunks_per_source` | integer | ❌ | 3 | 每个页面返回的片段数量（1-5，仅当有instructions时有效） |
| `max_depth` | integer | ❌ | 1 | 最大爬取深度（1-5） |
| `max_breadth` | integer | ❌ | 20 | 每层最大链接数（1-500） |
| `limit` | integer | ❌ | 50 | 总爬取页面上限 |
| `select_paths` | array | ❌ | - | 路径正则白名单 |
| `select_domains` | array | ❌ | - | 域名正则白名单 |
| `exclude_paths` | array | ❌ | - | 路径正则黑名单 |
| `exclude_domains` | array | ❌ | - | 域名正则黑名单 |
| `allow_external` | boolean | ❌ | true | 允许爬取外部域名 |
| `include_images` | boolean | ❌ | false | 包含图片 |
| `extract_depth` | enum | ❌ | `basic` | 提取深度：`basic`/`advanced` |
| `format` | enum | ❌ | `markdown` | 输出格式：`markdown`/`text` |
| `include_favicon` | boolean | ❌ | false | 包含网站图标 |
| `timeout` | float | ❌ | 150 | 超时时间（10-150秒） |
| `include_usage` | boolean | ❌ | false | 包含用量信息 |

---

### 4. Research API
**功能**：生成深度研究报告（异步）
#### 4.1 创建研究任务
**端点**：`POST /research`
**请求参数**：
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| `input` | string | ✅ | - | 研究主题 |
| `model` | enum | ❌ | `auto` | 研究模型：`mini`/`pro`/`auto` |
| `stream` | boolean | ❌ | false | 流式返回结果 |
| `output_schema` | object | ❌ | - | 自定义输出JSON Schema |
| `citation_format` | enum | ❌ | `numbered` | 引用格式：`numbered`/`mla`/`apa`/`chicago` |

#### 4.2 查询研究结果
**端点**：`GET /research/{request_id}`
**路径参数**：
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `request_id` | string | ✅ | 研究任务ID |

**任务状态**：
- `pending`：任务排队中
- `running`：研究进行中
- `completed`：完成
- `failed`：失败

---

### 5. Usage API
**功能**：查询用量统计
**端点**：`GET /usage`
**响应字段**：
```json
{
  "key": {
    "usage": 150,
    "limit": 1000,
    "search_usage": 100,
    "extract_usage": 25,
    "crawl_usage": 15,
    "map_usage": 7,
    "research_usage": 3
  },
  "account": {
    "current_plan": "Researcher",
    "plan_usage": 500,
    "plan_limit": 15000,
    "paygo_usage": 25,
    "paygo_limit": 100,
    "search_usage": 350,
    "extract_usage": 75,
    "crawl_usage": 50,
    "map_usage": 15,
    "research_usage": 10
  }
}
```

## 最佳实践
### 搜索优化
1. **深度选择**：普通查询用`basic`，高精度需求用`advanced`，实时查询用`fast`/`ultra-fast`
2. **新闻查询**：设置`topic: "news"` + `time_range: "day"`获取最新内容
3. **成本优化**：限制`max_results`到实际需要的数量，关闭不需要的`include_*`参数
4. **中文搜索**：设置`country: "china"`提升结果相关性

### 提取优化
1. 批量提取多个URL时优先使用`extract` API，比单独爬取成本低5倍
2. 需要精准内容时使用`query`参数，自动重排最相关的片段
3. 复杂页面使用`extract_depth: "advanced"`提取表格和嵌入内容

### 爬取优化
1. 明确爬取范围时使用`select_paths`/`exclude_paths`减少无效爬取
2. 整站文档爬取设置`max_depth: 3`+`limit: 100`平衡覆盖度和成本
3. 自然语言指令可以精准筛选需要的内容，如"只爬取API文档页面"

### 研究优化
1. 简单主题用`model: "mini"`，复杂多领域主题用`model: "pro"`
2. 自定义`output_schema`可以直接得到结构化的研究结果，无需额外解析
3. 研究任务平均耗时2-5分钟，建议异步查询结果

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