Markdown 文档

NBA → WeChat Image Sender

Fetch one current NBA.com image, normalize it into a WeChat-friendly JPG, and send it through Hermes WeChat gateway with bounded retry and delivery diagnostics. Use when a user asks to抓取/下载/发送 NBA 图片、赛场图、比赛图、今日 NBA 图片,or requests an NBA image delivery test from WeChat or CLI.

查看原始 MarkdownNBA / WeChat / Hermes Skill

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:

Example prompts:

发送一张 NBA 图片到微信
抓取一张 NBA 图片并发送到微信
从 NBA.com 获取一张赛场图
测试微信图片通道
执行一次 NBA 图片抓取与微信发送流程

What This Skill Does

Requirements

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:

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:

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:

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

Verification Checklist

After each run, report:

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("&amp;", "&")
            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())