feat: 日常增量 - 小红书配图/舆情记录/日报/草稿归档

This commit is contained in:
小橙
2026-04-23 03:46:43 +00:00
parent 289878b05a
commit e0efcd0582
35 changed files with 1795 additions and 22 deletions

View File

@@ -0,0 +1,7 @@
{
"version": 1,
"registry": "https://clawhub.ai",
"slug": "tavily-web-search-for-openclaw",
"installedVersion": "1.0.0",
"installedAt": 1776838149303
}

View 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.

View 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>"

View File

@@ -0,0 +1,6 @@
{
"ownerId": "kn7dnrfjd81n2c2x98wy693sbs82nfgp",
"slug": "tavily-web-search-for-openclaw",
"version": "1.0.0",
"publishedAt": 1773269610000
}

View 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())