feat: 日常增量 - 小红书配图/舆情记录/日报/草稿归档
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "tavily-web-search-for-openclaw",
|
||||
"installedVersion": "1.0.0",
|
||||
"installedAt": 1776838149303
|
||||
}
|
||||
185
skills/tavily-web-search-for-openclaw/README.md
Normal file
185
skills/tavily-web-search-for-openclaw/README.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# Tavily Web Search Skill for OpenClaw 🦀
|
||||
|
||||
A lightweight Tavily web search skill for OpenClaw that works without `pip` and without third-party Python packages.
|
||||
|
||||
This skill is designed for minimal Linux environments such as:
|
||||
|
||||
- Raspberry Pi
|
||||
- Ubuntu Server
|
||||
- small VPS setups
|
||||
- systems where installing Python packages is unavailable, restricted, or intentionally avoided
|
||||
|
||||
Instead of using the `tavily-python` SDK, this skill calls the Tavily REST API directly using Python's standard library.
|
||||
|
||||
## Features
|
||||
|
||||
- Tavily web search through direct REST API calls
|
||||
- No `pip install` required
|
||||
- No external Python dependencies
|
||||
- Works well on Raspberry Pi and Ubuntu Server
|
||||
- Supports general search and news search
|
||||
- Supports answer summaries, images, and domain filtering
|
||||
- Easy to integrate into OpenClaw skills
|
||||
- Simple secret-file based API key setup
|
||||
|
||||
## Why this version exists
|
||||
|
||||
The official Tavily Python SDK is convenient, but some environments do not have a practical or desirable `pip` workflow.
|
||||
|
||||
This skill exists for setups where you want:
|
||||
|
||||
- a small footprint
|
||||
- no package installation step
|
||||
- predictable deployment
|
||||
- compatibility with minimal server environments
|
||||
- a solution that keeps working even on systems where Python package installation is restricted
|
||||
|
||||
This is especially useful on Raspberry Pi, Ubuntu Server, and other minimal Linux systems where you may prefer to avoid virtual environments, extra package managers, or external Python dependencies for a simple search integration.
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```text
|
||||
skills/tavily/
|
||||
├── SKILL.md
|
||||
├── .secrets/
|
||||
│ └── tavily.key
|
||||
└── scripts/
|
||||
└── tavily_search.py
|
||||
```
|
||||
|
||||
## Secret Setup
|
||||
|
||||
Create the secret directory:
|
||||
|
||||
```bash
|
||||
mkdir -p skills/tavily/.secrets
|
||||
chmod 700 skills/tavily/.secrets
|
||||
```
|
||||
|
||||
Create the key file:
|
||||
|
||||
```bash
|
||||
nano skills/tavily/.secrets/tavily.key
|
||||
```
|
||||
|
||||
The file must contain only your raw Tavily API key:
|
||||
|
||||
```
|
||||
tvly-xxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Do **not** write:
|
||||
|
||||
```
|
||||
TAVILY_API_KEY=tvly-xxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Set permissions:
|
||||
|
||||
```bash
|
||||
chmod 600 skills/tavily/.secrets/tavily.key
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Basic search:
|
||||
|
||||
```bash
|
||||
python3 skills/tavily/scripts/tavily_search.py --query "latest AI news"
|
||||
```
|
||||
|
||||
News-focused search:
|
||||
|
||||
```bash
|
||||
python3 skills/tavily/scripts/tavily_search.py --query "gold prices" --topic news
|
||||
```
|
||||
|
||||
Advanced search:
|
||||
|
||||
```bash
|
||||
python3 skills/tavily/scripts/tavily_search.py --query "raspberry pi ubuntu server optimization" --depth advanced
|
||||
```
|
||||
|
||||
JSON output:
|
||||
|
||||
```bash
|
||||
python3 skills/tavily/scripts/tavily_search.py --query "python asyncio" --json
|
||||
```
|
||||
|
||||
## Supported Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------- | ---------------------------- |
|
||||
| `--query` | **required** search query |
|
||||
| `--topic` | `general` or `news` |
|
||||
| `--depth` | `basic` or `advanced` |
|
||||
| `--max-results` | number of results |
|
||||
| `--no-answer` | disable answer summary |
|
||||
| `--raw-content` | include parsed raw content |
|
||||
| `--images` | include image results |
|
||||
| `--include-domains` | restrict to selected domains |
|
||||
| `--exclude-domains` | filter out selected domains |
|
||||
| `--json` | output raw JSON |
|
||||
|
||||
## OpenClaw Integration
|
||||
|
||||
This skill is meant to be used from OpenClaw through `SKILL.md`.
|
||||
|
||||
Typical usage flow:
|
||||
|
||||
1. The user asks for web search or recent information
|
||||
2. OpenClaw invokes the Tavily skill
|
||||
3. The skill runs `scripts/tavily_search.py`
|
||||
4. The script reads the API key from `.secrets/tavily.key`
|
||||
5. Results are returned in a format suitable for summarization
|
||||
|
||||
## Why no pip is required
|
||||
|
||||
This project intentionally avoids the Tavily Python SDK and other third-party dependencies.
|
||||
|
||||
That means:
|
||||
|
||||
- there is no `pip install` step
|
||||
- there is no dependency on `tavily-python`
|
||||
- there is no virtual environment requirement just to use the skill
|
||||
- deployment stays simple on minimal systems
|
||||
|
||||
The script uses only Python's standard library to call the Tavily REST API directly.
|
||||
|
||||
## Security Notes
|
||||
|
||||
- The `.secrets` directory should never be committed
|
||||
- Your API key should stay only on the target machine
|
||||
- This repository should contain code and documentation only
|
||||
- Add `.secrets/` to `.gitignore`
|
||||
- Keep `tavily.key` readable only by the user or service that runs the skill
|
||||
|
||||
Example `.gitignore` entries:
|
||||
|
||||
```
|
||||
.secrets/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3
|
||||
- Network access to Tavily API
|
||||
- A valid Tavily API key
|
||||
- No additional Python packages are required
|
||||
|
||||
## Motivation
|
||||
|
||||
This project is especially useful for:
|
||||
|
||||
- Raspberry Pi home server setups
|
||||
- Ubuntu Server deployments
|
||||
- minimal VPS environments
|
||||
- offline-managed or tightly controlled systems
|
||||
- users who want Tavily search without SDK installation
|
||||
- environments where `pip` is unavailable, restricted, or intentionally avoided
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
||||
25
skills/tavily-web-search-for-openclaw/SKILL.md
Normal file
25
skills/tavily-web-search-for-openclaw/SKILL.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: tavily
|
||||
description: use this when the user asks to search the web, look up recent information, check current events, gather online sources, or research a topic using tavily search.
|
||||
---
|
||||
|
||||
# Tavily Search
|
||||
|
||||
Use this skill for web search and lightweight research through the Tavily Search API.
|
||||
|
||||
## Requirements
|
||||
|
||||
A valid Tavily API key must be available through one of these methods:
|
||||
|
||||
1. `--api-key`
|
||||
2. `TAVILY_API_KEY`
|
||||
3. `{baseDir}/.secrets/tavily.key`
|
||||
|
||||
If no key is available, explain that Tavily search is not configured in this environment.
|
||||
|
||||
## Command
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python3 {baseDir}/scripts/tavily_search.py --query "<user query>"
|
||||
6
skills/tavily-web-search-for-openclaw/_meta.json
Normal file
6
skills/tavily-web-search-for-openclaw/_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn7dnrfjd81n2c2x98wy693sbs82nfgp",
|
||||
"slug": "tavily-web-search-for-openclaw",
|
||||
"version": "1.0.0",
|
||||
"publishedAt": 1773269610000
|
||||
}
|
||||
241
skills/tavily-web-search-for-openclaw/scripts/tavily_search.py
Normal file
241
skills/tavily-web-search-for-openclaw/scripts/tavily_search.py
Normal file
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
API_URL = "https://api.tavily.com/search"
|
||||
|
||||
|
||||
def load_api_key():
|
||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
key_path = os.path.normpath(
|
||||
os.path.join(base_dir, "..", ".secrets", "tavily.key")
|
||||
)
|
||||
|
||||
try:
|
||||
with open(key_path, "r", encoding="utf-8") as f:
|
||||
raw = f.read().strip()
|
||||
|
||||
if "=" in raw:
|
||||
left, right = raw.split("=", 1)
|
||||
if left.strip() == "TAVILY_API_KEY":
|
||||
return right.strip()
|
||||
|
||||
return raw or None
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def clamp_max_results(value: int) -> int:
|
||||
if value < 1:
|
||||
return 1
|
||||
if value > 10:
|
||||
return 10
|
||||
return value
|
||||
|
||||
|
||||
def build_payload(args: argparse.Namespace, api_key: str) -> dict:
|
||||
payload = {
|
||||
"api_key": api_key,
|
||||
"query": args.query,
|
||||
"search_depth": args.depth,
|
||||
"topic": args.topic,
|
||||
"max_results": clamp_max_results(args.max_results),
|
||||
"include_answer": not args.no_answer,
|
||||
"include_raw_content": args.raw_content,
|
||||
"include_images": args.images,
|
||||
}
|
||||
|
||||
if args.include_domains:
|
||||
payload["include_domains"] = args.include_domains
|
||||
|
||||
if args.exclude_domains:
|
||||
payload["exclude_domains"] = args.exclude_domains
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def tavily_search(payload: dict, timeout: int = 30) -> dict:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
API_URL,
|
||||
data=data,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
body = resp.read().decode("utf-8", errors="replace")
|
||||
return json.loads(body)
|
||||
except urllib.error.HTTPError as exc:
|
||||
details = ""
|
||||
try:
|
||||
details = exc.read().decode("utf-8", errors="replace")
|
||||
except Exception:
|
||||
details = ""
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP {exc.code}",
|
||||
"details": details,
|
||||
}
|
||||
except urllib.error.URLError as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Network error",
|
||||
"details": str(exc.reason),
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Unexpected error",
|
||||
"details": str(exc),
|
||||
}
|
||||
|
||||
|
||||
def print_human(result: dict) -> int:
|
||||
if not isinstance(result, dict):
|
||||
print("Error: invalid response format", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if "error" in result and not result.get("results"):
|
||||
print(f"Error: {result.get('error')}", file=sys.stderr)
|
||||
if result.get("details"):
|
||||
print(result["details"], file=sys.stderr)
|
||||
return 1
|
||||
|
||||
print(f"Query: {result.get('query', 'N/A')}")
|
||||
print(f"Response time: {result.get('response_time', 'N/A')}")
|
||||
usage = result.get("usage", {})
|
||||
if isinstance(usage, dict):
|
||||
print(f"Credits used: {usage.get('credits', 'N/A')}")
|
||||
print()
|
||||
|
||||
answer = result.get("answer")
|
||||
if answer:
|
||||
print("=== ANSWER ===")
|
||||
print(answer)
|
||||
print()
|
||||
|
||||
results = result.get("results", [])
|
||||
if results:
|
||||
print("=== RESULTS ===")
|
||||
for index, item in enumerate(results, start=1):
|
||||
title = item.get("title") or "No title"
|
||||
url = item.get("url") or "N/A"
|
||||
score = item.get("score")
|
||||
content = item.get("content") or ""
|
||||
|
||||
print(f"\n{index}. {title}")
|
||||
print(f" URL: {url}")
|
||||
|
||||
if isinstance(score, (int, float)):
|
||||
print(f" Score: {score:.3f}")
|
||||
|
||||
if content:
|
||||
snippet = content[:280].replace("\n", " ").strip()
|
||||
if len(content) > 280:
|
||||
snippet += "..."
|
||||
print(f" {snippet}")
|
||||
|
||||
images = result.get("images", [])
|
||||
if images:
|
||||
print(f"\n=== IMAGES ({len(images)}) ===")
|
||||
for image in images[:5]:
|
||||
if isinstance(image, dict):
|
||||
print(f" {image.get('url', 'N/A')}")
|
||||
else:
|
||||
print(f" {image}")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Tavily Search via direct REST API call",
|
||||
)
|
||||
|
||||
parser.add_argument("--query", required=True, help="Search query")
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
help="Tavily API key. If omitted, file or TAVILY_API_KEY is used.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--depth",
|
||||
choices=["basic", "advanced"],
|
||||
default="basic",
|
||||
help="Search depth",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--topic",
|
||||
choices=["general", "news"],
|
||||
default="general",
|
||||
help="Search topic",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-results",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of results to return (1-10)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-answer",
|
||||
action="store_true",
|
||||
help="Do not request Tavily answer summary",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--raw-content",
|
||||
action="store_true",
|
||||
help="Include parsed raw content",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--images",
|
||||
action="store_true",
|
||||
help="Include image results",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-domains",
|
||||
nargs="+",
|
||||
help="Only include these domains",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude-domains",
|
||||
nargs="+",
|
||||
help="Exclude these domains",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json",
|
||||
action="store_true",
|
||||
help="Print raw JSON response",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
api_key = load_api_key()
|
||||
if not api_key:
|
||||
print(
|
||||
"Error: Tavily API key not found in ../.secrets/tavily.key",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
payload = build_payload(args, api_key)
|
||||
result = tavily_search(payload)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2, ensure_ascii=False))
|
||||
return 0 if "error" not in result else 1
|
||||
|
||||
return print_human(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user