NBA → WeChat Image Sender
A Hermes skill for reliably fetching a current image from NBA.com and sending it through the WeChat gateway.
This skill is designed for lightweight, repeatable media-delivery tasks. It avoids expensive model-driven browsing loops by delegating deterministic work—page fetch, image selection, download, conversion, and compression—to a local script. Hermes only reads the script result and performs the final message delivery.
Use Cases
Use this skill when the user asks for tasks such as:
- Fetch an NBA image and send it to WeChat
- Send today’s NBA game/arena/news image
- Test WeChat media delivery with an NBA image
- Run a bounded NBA image capture workflow from Hermes
Example prompts:
发送一张 NBA 图片到微信
抓取一张 NBA 图片并发送到微信
从 NBA.com 获取一张赛场图
测试微信图片通道
执行一次 NBA 图片抓取与微信发送流程
What This Skill Does
- Fetches NBA.com pages in a fixed priority order.
- Selects a usable image URL from Open Graph, Twitter card, or NBA CDN metadata.
- Downloads the image to a local temporary file.
- Converts and compresses it to a WeChat-friendly JPG.
- Returns a compact JSON result containing the local image path and source URL.
- Sends the local image through Hermes messaging using
MEDIA:/absolute/path. - Limits retries to avoid long-running loops and unnecessary token usage.
Requirements
- Hermes Agent with the following toolsets enabled:
terminalmessagingskills- A configured WeChat / Weixin gateway in Hermes.
- macOS is recommended for the bundled script because it uses
/usr/bin/sipsfor image conversion. On non-macOS systems, replace this step with ImageMagick, Pillow, or another image converter.
Installation
Place this skill under your Hermes skills directory, for example:
~/.hermes/skills/media/nba-wechat-image/
Expected structure:
nba-wechat-image/
├── SKILL.md
└── scripts/
└── fetch_nba_image.py
After adding or updating the skill, restart the Hermes gateway so messaging sessions can load the latest skill metadata:
hermes gateway restart
On macOS launchd installations, if the restart command hangs, use:
launchctl kickstart -k gui/$(id -u)/ai.hermes.gateway
Configuration
WeChat target
For private use, you can send to the WeChat home channel:
weixin
For explicit routing, replace the placeholder below with the actual Weixin DM target from:
hermes targets list
or by using Hermes messaging target discovery.
weixin:<YOUR_WEIXIN_DM_ID>@im.wechat
Do not publish personal chat IDs, account IDs, tokens, or API keys in shared versions of this skill.
Recommended safety settings
For this workflow, low-risk terminal commands should not require repeated manual approval. A practical setting is:
hermes config set approvals.mode smart
To prevent media tasks from running too long:
hermes config set agent.max_turns 20
hermes config set terminal.timeout 60
Restart the gateway after changing these settings.
Workflow
Step 1 — Run the image fetch script
python3 ~/.hermes/skills/media/nba-wechat-image/scripts/fetch_nba_image.py
The script returns JSON like:
{
"ok": true,
"image_path": "/tmp/nba_wechat_latest.jpg",
"image_url": "https://cdn.nba.com/...jpg",
"source_page": "https://www.nba.com/news",
"bytes": 277368
}
Step 2 — Send the image through WeChat
If ok=true, send the image as a media attachment:
MEDIA:/tmp/nba_wechat_latest.jpg
Use send_message with the configured WeChat target.
Recommended message body:
MEDIA:/tmp/nba_wechat_latest.jpg
For better delivery reliability, keep the media message short. Avoid Markdown image syntax for WeChat delivery.
Step 3 — Apply bounded retry
If the first send fails due to rate limiting:
- Wait about 10 seconds.
- Retry once.
- If it still fails, stop and report the failure reason.
Do not repeatedly resend the same image. This workflow is intentionally bounded to avoid excessive token and API usage.
Delivery Troubleshooting
Send API returns success but the image does not appear
Treat this as a delivery-layer issue rather than an image-fetching issue.
Recommended checks:
hermes gateway status
tail -120 ~/.hermes/logs/gateway.log ~/.hermes/logs/gateway.error.log
Look for:
rate limitedsession timeoutpoll errorServer disconnected- Weixin / iLink send errors
If needed, compress the image smaller once and retry once. If the image still does not appear, stop resending and investigate the WeChat gateway or iLink delivery channel.
WeChat replies become slow or stop
Common causes:
- The gateway is waiting for command approval.
- A previous task is still running.
- Weixin / iLink session has timed out.
- Proxy/VPN interrupted long polling.
- The gateway needs to be restarted after a skill/config update.
Recommended checks:
hermes gateway status
tail -120 ~/.hermes/logs/gateway.log ~/.hermes/logs/gateway.error.log
If the logs show repeated Weixin session errors, restart the gateway.
Operational Rules
- Use the bundled script for fetching and converting images.
- Do not ask the model to manually browse NBA.com unless the script fails.
- Use local absolute image paths.
- Convert or compress images to JPG before sending to WeChat.
- Prefer
MEDIA:/path/to/image.jpgfor media delivery. - Retry at most once after rate limiting.
- If delivery remains unreliable, switch to gateway diagnostics instead of repeating the fetch.
Verification Checklist
After each run, report:
- Source page
- Local image path
- Image size
- WeChat send status
- Whether a retry was used
Example concise report:
NBA image fetched and sent.
Source: https://www.nba.com/news
Image: /tmp/nba_wechat_latest.jpg
Size: 277 KB
WeChat send: success
Retry: no
Bundled Script
Save the following script as:
scripts/fetch_nba_image.py
#!/usr/bin/env python3
"""Fetch one current NBA.com image and normalize it for WeChat.
Prints JSON:
{ok, image_path, image_url, source_page, bytes}
Hermes should run this script, read the JSON, then send image_path via
MEDIA:/... to WeChat.
"""
from __future__ import annotations
import json
import re
import subprocess
import sys
import urllib.parse
import urllib.request
from pathlib import Path
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 Chrome/124 Safari/537.36"
)
}
PAGES = [
"https://www.nba.com/news",
"https://www.nba.com/games",
"https://www.nba.com/",
]
OUT = Path("/tmp/nba_wechat_latest.jpg")
RAW = Path("/tmp/nba_wechat_latest_raw")
PATTERNS = [
r"<meta[^>]+property=[\"']og:image[\"'][^>]+content=[\"']([^\"']+)",
r"<meta[^>]+content=[\"']([^\"']+)[\"'][^>]+property=[\"']og:image[\"']",
r"<meta[^>]+name=[\"']twitter:image[\"'][^>]+content=[\"']([^\"']+)",
r"https://cdn\.nba\.com/[^\"'<> ]+\.(?:jpg|jpeg|png|webp)(?:\?[^\"'<> ]*)?",
]
SKIP_WORDS = ["logo", "icon", "favicon", "apple-touch", "placeholder"]
def fetch_text(url: str) -> str:
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=20) as r:
return r.read().decode("utf-8", "ignore")
def pick_image() -> tuple[str | None, str | None]:
for page in PAGES:
try:
html = fetch_text(page)
except Exception:
continue
imgs: list[str] = []
for pattern in PATTERNS:
imgs.extend(re.findall(pattern, html, re.I))
seen: set[str] = set()
for img in imgs:
img = (img if img.startswith("http") else urllib.parse.urljoin(page, img)).replace("&", "&")
if img in seen:
continue
seen.add(img)
low = img.lower()
if any(word in low for word in SKIP_WORDS):
continue
return img, page
return None, None
def download(url: str) -> bytes:
req = urllib.request.Request(url, headers=HEADERS)
with urllib.request.urlopen(req, timeout=30) as r:
return r.read()
def convert_for_wechat(raw_path: Path, out_path: Path) -> None:
cmd = [
"/usr/bin/sips",
"-Z",
"1280",
"-s",
"format",
"jpeg",
"-s",
"formatOptions",
"70",
str(raw_path),
"--out",
str(out_path),
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=30)
except Exception:
out_path.write_bytes(raw_path.read_bytes())
def main() -> int:
img, page = pick_image()
if not img:
print(json.dumps({"ok": False, "error": "no usable NBA image found"}, ensure_ascii=False))
return 0
try:
RAW.write_bytes(download(img))
convert_for_wechat(RAW, OUT)
if not OUT.exists() or OUT.stat().st_size < 1000:
OUT.write_bytes(RAW.read_bytes())
print(
json.dumps(
{
"ok": True,
"image_path": str(OUT),
"image_url": img,
"source_page": page,
"bytes": OUT.stat().st_size,
},
ensure_ascii=False,
)
)
return 0
except Exception as exc:
print(json.dumps({"ok": False, "error": str(exc), "image_url": img}, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())