Skill: joewangup/trade-news-summary (version 2.3.3)

# 外贸资讯聚合器（多源 RSS + 翻译 + 飞书推送） **Version:** 2.3.3 ## 简介 自动抓取外贸相关 RSS 源（支持自定义关键词），调用百度翻译 API 生成中文标题，生成 Markdown 报告并可选推送到飞书群。 ## 功能 - 抓取多个 RSS 源（默认使用 Bing News 搜索外贸关键词） - 自动翻译标题（英 → 中，支持自动检测源语言） - 生成 Markdown 报告：`~/trade-news.md` - 飞书机器人推送（可选） - 自动分类新闻（帽子/面料/运费/关税/电商/运动服饰/汇率/国际关系/合规/行业动态） - 生成近7天趋势周报（各类别新闻数量及占比） ## 依赖 - `curl`, `jq`, `md5sum`, `xmlstarlet`（必须安装） - 百度翻译 API（免费，每月 100 万字符） - 飞书自定义机器人（可选） ## 环境变量 | 变量 | 必填 | 说明 | |------|------|------| | `BAIDU_APPID` | ✅ | 百度翻译 App ID | | `BAIDU_SECRET` | ✅ | 百度翻译密钥 | | `FEISHU_WEBHOOK` | ❌ | 飞书机器人 Webhook | ## 安全说明 本 Skill 不会自动加载任何外部 `.env` 文件。请通过环境变量或直接在 crontab 中设置密钥。 ## 网络要求 本 Skill 需要访问 Bing News 和百度翻译 API。如果您的运行环境无法直接访问外网，请配置代理（例如 `export http_proxy=http://your-proxy:port`）。 ## 安装与配置 ### 1. 获取百度翻译 API 密钥 - 访问 [百度翻译开放平台](https://fanyi-api.baidu.com/) - 注册/登录，申请通用翻译 API（免费版） - 记录 **App ID** 和 **密钥** ### 2. 创建飞书机器人（可选） - 在飞书群中添加“自定义机器人” - 复制 **Webhook URL** ### 3. 设置环境变量 将以下内容添加到 `~/.bashrc` 或执行脚本前 export： ```bash export BAIDU_APPID="你的AppID" export BAIDU_SECRET="你的密钥" export FEISHU_WEBHOOK="你的飞书Webhook地址"

path: classify_news.py
content: #!/usr/bin/env python3
import re
import requests

import os
WEBHOOK_URL = os.environ.get("FEISHU_WEBHOOK", "")
import os
NEWS_FILE = os.path.expanduser("~/trade-news.md")


CATEGORIES = {
    "帽子": ["hat", "cap", "beanie", "fedora", "beret", "sun hat", "hard hat", "gat", "headwear", "millinery", "snapback", "dad hat", "bucket hat", "visor", "cloche", "newsboy", "panama", "trilby", "cowboy", "outdoor hat"],
    
    "面料": ["fabric", "textile", "cloth", "material", "fibre", "yarn", "weave", "recycling", "spinning", "cotton", "polyester", "nylon", "wool", "linen", "hemp", "silk", "leather", "denim", "knit", "woven", "nonwoven", "technical fabric", "performance fabric", "breathable", "waterproof", "eco-friendly", "organic", "recycled", "sustainable"],
    
    "运动服饰": ["sportswear", "activewear", "athleisure", "gym", "yoga", "running", "training", "sports", "athletic", "fitness", "workout", "sports bra", "leggings", "joggers", "hoodie", "sweatshirt", "track", "jersey", "uniform", "team wear", "nike", "adidas", "under armour", "lululemon", "puma", "reebok", "new balance", "asics", "columbia", "the north face", "patagonia"],
    
    "运费": ["shipping", "freight", "logistics", "container", "port", "vessel", "cargo", "delivery", "supply chain", "transport", "haulage", "courier", "last mile", "fulfillment", "warehouse", "inventory", "stock", "distribution"],
    
    "关税": ["tariff", "duty", "tax", "customs", "levy", "surcharge", "electricity prices", "energy costs", "trade war", "trade barrier", "import", "export", "quota", "anti-dumping", "countervailing", "section 301", "trade agreement", "free trade", "fta", "gsp", "preferential", "rules of origin"],
    
    "电商": ["amazon", "ebay", "ecommerce", "e-commerce", "retail", "taobao", "shopify", "online", "marketplace", "d2c", "direct to consumer", "omni-channel", "cross-border", "aliexpress", "wish", "etsy", "walmart", "target", "costco", "shein", "temu", "tiktok shop", "instagram shop", "facebook marketplace"],
    
    "汇率": ["currency", "exchange", "forex", "yuan", "dollar", "euro", "yen", "rupee", "pound", "won", "real", "peso", "ringgit", "baht", "dong", "rupiah", "forex rate", "exchange rate", "appreciation", "depreciation", "devaluation", "revaluation", "currency war"],
    
    "国际关系": ["trade war", "trade dispute", "sanctions", "embargo", "tariff war", "diplomatic", "geopolitical", "bilateral", "multilateral", "wto", "imf", "world bank", "g20", "apec", "asean", "eu", "usmca", "cptpp", "rcep", "brexit", "china-us", "india-us", "europe", "middle east", "iran", "hormuz", "suez", "panama canal"],
    
    "合规": ["compliance", "regulation", "law", "act", "bill", "standard", "certification", "audit", "inspection", "testing", "safety", "environmental", "social", "labor", "human rights", "forced labor", "child labor", "modern slavery", "conflict mineral", "reach", "rohs", "cpsc", "astm", "iso", "oeko tex", "grs", "bc", "fair trade", "ethical", "sustainable"],
    
    "行业动态": ["brand", "launch", "release", "announce", "partnership", "acquisition", "merger", "investment", "funding", "ipo", "earnings", "revenue", "profit", "market share", "trend", "forecast", "outlook", "analysis", "report", "survey", "study", "research"]
}

def classify(title):
    for cat, kw in CATEGORIES.items():
        if any(k in title.lower() for k in kw):
            return cat
    return "其他"

try:
    with open(NEWS_FILE, 'r', encoding='utf-8') as f:
        content = f.read()
except FileNotFoundError:
    print("新闻文件不存在，请先运行 daily-news.sh")
    exit(1)

pattern = r'\*\*(\d+)\.\s*\[(.+?)\]\((.+?)\)\*\*.*?-\s*中文:\s*(.+?)(?:\n|$)'
matches = re.findall(pattern, content, re.DOTALL)

if not matches:
    print("未找到新闻，请检查文件格式")
    exit(1)

rows = []
for match in matches:
    num, title_en, link, title_zh = match
    cat = classify(title_en)
    summary = title_zh[:20] + ("..." if len(title_zh) > 20 else "")
    rows.append(f"| {cat} | [{title_en[:40]}]({link}) | {summary} |")

table = "| 类别 | 标题 | 摘要 |\n|------|------|------|\n" + "\n".join(rows)
card = {
    "msg_type": "interactive",
    "card": {
        "config": {"wide_screen_mode": True},
        "header": {"title": {"tag": "plain_text", "content": "📊 外贸资讯分析报告"}, "template": "blue"},
        "elements": [{"tag": "div", "text": {"tag": "lark_md", "content": table}}]
    }
}

# 打印发送的 JSON 用于调试
print("发送的 JSON:", card)

resp = requests.post(WEBHOOK_URL, json=card)
print(f"✅ 分析报告已推送，状态码: {resp.status_code}")
print("响应内容:", resp.text)


path: clawhub.json
content: {
  "dependencies": {
    "env": ["BAIDU_APPID", "BAIDU_SECRET", "FEISHU_WEBHOOK"],
    "bin": ["curl", "jq", "md5sum", "xmlstarlet"]
  }
}


path: daily-news.sh
content: #!/bin/bash

# ============================================================
# 外贸资讯聚合器（最终确定版）
# 版本：2.2.3
# ============================================================

RSS_FEEDS=${RSS_FEEDS:-"https://www.bing.com/news/search?q=hat&format=rss,https://www.bing.com/news/search?q=textile&format=rss,https://www.bing.com/news/search?q=shipping&format=rss,https://www.bing.com/news/search?q=tariff&format=rss,https://www.bing.com/news/search?q=ecommerce&format=rss"}
KEYWORDS=${KEYWORDS:-}
MAX_ITEMS=${MAX_ITEMS:-30}
INCLUDE_HACKER_NEWS=${INCLUDE_HACKER_NEWS:-false}
BAIDU_APPID=${BAIDU_APPID:-}
BAIDU_SECRET=${BAIDU_SECRET:-}
FEISHU_WEBHOOK=${FEISHU_WEBHOOK:-}

if [ -z "$BAIDU_APPID" ] || [ -z "$BAIDU_SECRET" ]; then
    echo "错误：请设置环境变量 BAIDU_APPID 和 BAIDU_SECRET"
    exit 1
fi

retry() {
    local n=0 max=3 delay=2
    until [ $n -ge $max ]; do
        "$@" && return 0
        n=$((n+1))
        echo "命令失败，${delay}秒后重试 ($n/$max)..." >&2
        sleep $delay
    done
    echo "命令最终失败: $*" >&2
    return 1
}

translate() {
    local text="$1"
    local salt=$(date +%s)
    local sign=$(echo -n "$BAIDU_APPID$text$salt$BAIDU_SECRET" | md5sum | cut -d ' ' -f1)
    local encoded_text=$(echo -n "$text" | jq -sRr @uri)
    local result=$(retry curl -s --connect-timeout 10 --max-time 30 "https://fanyi-api.baidu.com/api/trans/vip/translate?q=$encoded_text&from=auto&to=zh&appid=$BAIDU_APPID&salt=$salt&sign=$sign")
    echo "$result" | jq -r '.trans_result[0].dst // ""'
}

fetch_rss() {
    local url="$1"
    local tmp=$(mktemp)
    retry curl -s --connect-timeout 10 --max-time 30 -A "Mozilla/5.0" "$url" > "$tmp"
    if command -v xmlstarlet >/dev/null 2>&1; then
        xmlstarlet sel -t -m "//item" -v "title" -o "||" -v "link" -o "||" -n "$tmp" 2>/dev/null
    else
        echo "错误：未安装 xmlstarlet，请运行 sudo apt install xmlstarlet -y" >&2
        return 1
    fi
    rm "$tmp"
}

fetch_hackernews() {
    local tmp=$(mktemp)
    retry curl -s https://hacker-news.firebaseio.com/v0/topstories.json | jq -r '.[0:20][]' > "$tmp"
    while read id; do
        local item=$(retry curl -s "https://hacker-news.firebaseio.com/v0/item/$id.json")
        local title=$(echo "$item" | jq -r '.title')
        local url=$(echo "$item" | jq -r '.url')
        [ "$url" = "null" ] && url="https://news.ycombinator.com/item?id=$id"
        echo "$title||$url"
    done < "$tmp"
    rm "$tmp"
}

match_keywords() {
    local title="$1"
    [ -z "$KEYWORDS" ] && return 0
    IFS=',' read -ra kw <<< "$KEYWORDS"
    for k in "${kw[@]}"; do
        echo "$title" | grep -iq "${k// /}" && return 0
    done
    return 1
}

notify_error() {
    local error_msg="$1"
    if [ -n "$FEISHU_WEBHOOK" ]; then
        local json=$(jq -n --arg msg "❌ 外贸资讯聚合器运行失败\n\n错误详情: $error_msg" '{"msg_type":"text","content":{"text":$msg}}')
        curl -X POST -H "Content-Type: application/json" -d "$json" "$FEISHU_WEBHOOK" > /dev/null 2>&1
    fi
}

OUTPUT="$HOME/trade-news.md"
DATE=$(date '+%Y-%m-%d')
declare -A seen
declare -a final_titles
declare -a final_urls
COUNT=0

ALL_NEWS=""
[ "$INCLUDE_HACKER_NEWS" = "true" ] && ALL_NEWS+=$'\n'"$(fetch_hackernews)"
if [ -n "$RSS_FEEDS" ]; then
    IFS=',' read -ra feeds <<< "$RSS_FEEDS"
    for feed in "${feeds[@]}"; do
        feed=$(echo "$feed" | tr -d ' ')
        ALL_NEWS+=$'\n'"$(fetch_rss "$feed")"
    done
fi

while IFS='||' read -r title url; do
    [ -z "$title" ] && continue
    title=$(echo "$title" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    url=$(echo "$url" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    [ -n "${seen["$title"]}" ] && continue
    seen["$title"]=1
    if match_keywords "$title"; then
        final_titles[$COUNT]="$title"
        final_urls[$COUNT]="$url"
        COUNT=$((COUNT+1))
        [ $COUNT -ge $MAX_ITEMS ] && break
    fi
done <<< "$ALL_NEWS"

{
    echo "# 每日外贸资讯汇总 - $DATE"
    echo ""
    echo "## 相关资讯（关键词: ${KEYWORDS:-无}）"
    echo ""
    for ((i=0; i<COUNT; i++)); do
    title="${final_titles[$i]}"
    url="${final_urls[$i]}"
    num=$((i+1))
    zh=$(translate "$title")
    # 清理链接
    clean_url=$(echo "$url" | sed 's/^[| ]*//;s/[| ]*$//;s/&amp;/\&/g')
    # 标题变成可点击链接
    echo "**$num. [$title]($clean_url)**"
    [ -n "$zh" ] && [ "$zh" != "null" ] && echo "   - 中文: $zh"
    echo ""
    sleep 1
done
    echo "✅ 共抓取 $COUNT 条新闻"
} > "$OUTPUT"

if [ $COUNT -eq 0 ]; then
    notify_error "未抓取到任何新闻，请检查网络连接、代理或 RSS 源配置。"
    exit 1
fi

if [ -n "$FEISHU_WEBHOOK" ]; then
    CARD_JSON=$(jq -n \
        --arg title "📰 每日外贸资讯 - $DATE" \
        --arg content "$(cat "$OUTPUT")" \
        '{
            "msg_type": "interactive",
            "card": {
                "config": {"wide_screen_mode": true},
                "header": {"title": {"tag": "plain_text", "content": $title}, "template": "blue"},
                "elements": [
                    {"tag": "div", "text": {"tag": "lark_md", "content": $content}},
                    {"tag": "hr"},
                    {"tag": "note", "elements": [{"tag": "plain_text", "content": "Generated by OpenClaw | 外贸资讯聚合器 v2.2.3"}]}
                ]
            }
        }')
    curl -X POST -H "Content-Type: application/json" -d "$CARD_JSON" "$FEISHU_WEBHOOK" > /dev/null 2>&1
    echo "✅ 已推送到飞书（卡片消息）"
fi

# 调用分类推送脚本
python3 "$(dirname "$0")/classify_news.py"

# 趋势分析（每日保存数据，积累7天后自动推送周报）
python3 "$(dirname "$0")/trend_analysis.py"

echo "资讯汇总已完成！文件位置：$OUTPUT"


path: trend_analysis.py
content: #!/usr/bin/env python3
import re
import json
import requests
from collections import Counter
from datetime import datetime, timedelta
import os
WEBHOOK_URL = os.environ.get("FEISHU_WEBHOOK", "")
if not WEBHOOK_URL:
    print("错误：请设置环境变量 FEISHU_WEBHOOK")
    exit(1)


NEWS_FILE = os.path.expanduser("~/trade-news.md")
HISTORY_DIR = os.path.expanduser("~/.openclaw/workspace/history")


os.makedirs(HISTORY_DIR, exist_ok=True)

CATEGORIES = {
    "帽子": ["hat", "cap", "beanie", "fedora", "beret", "sun hat", "hard hat", "gat", "headwear", "millinery"],
    "面料": ["fabric", "textile", "cloth", "material", "fibre", "yarn", "weave", "recycling", "spinning", "cotton", "polyester", "nylon", "wool", "linen"],
    "运动服饰": ["sportswear", "activewear", "athleisure", "gym", "yoga", "running", "training", "sports", "athletic", "fitness", "nike", "adidas", "under armour", "lululemon"],
    "运费": ["shipping", "freight", "logistics", "container", "port", "vessel", "cargo", "delivery", "supply chain", "warehouse"],
    "关税": ["tariff", "duty", "tax", "customs", "levy", "surcharge", "electricity prices", "energy costs", "trade war", "import", "export"],
    "电商": ["amazon", "ebay", "ecommerce", "e-commerce", "retail", "taobao", "shopify", "online", "marketplace", "shein", "temu"],
    "汇率": ["currency", "exchange", "forex", "yuan", "dollar", "euro", "yen", "rupee", "exchange rate"],
    "国际关系": ["trade war", "sanctions", "embargo", "geopolitical", "wto", "eu", "brexit", "middle east", "iran", "hormuz"],
    "合规": ["compliance", "regulation", "law", "standard", "certification", "safety", "environmental", "labor", "forced labor", "reach", "rohs"],
    "行业动态": ["brand", "launch", "release", "announce", "partnership", "acquisition", "investment", "funding", "earnings", "trend", "forecast"]
}

def classify(title):
    for cat, kw in CATEGORIES.items():
        if any(k in title.lower() for k in kw):
            return cat
    return "其他"

def get_today_news():
    try:
        with open(NEWS_FILE, 'r', encoding='utf-8') as f:
            content = f.read()
    except FileNotFoundError:
        return []
    pattern = r'\*\*(\d+)\.\s*(.+?)\*\*'
    matches = re.findall(pattern, content, re.DOTALL)
    categories = []
    for match in matches:
        title_en = match[1]
        cat = classify(title_en)
        categories.append(cat)
    return categories

def save_history(categories):
    today = datetime.now().strftime("%Y-%m-%d")
    history_file = os.path.join(HISTORY_DIR, f"{today}.json")
    with open(history_file, 'w', encoding='utf-8') as f:
        json.dump(categories, f)

def load_week_history():
    week_ago = datetime.now() - timedelta(days=7)
    all_cats = []
    for fname in os.listdir(HISTORY_DIR):
        if fname.endswith('.json'):
            date_str = fname.replace('.json', '')
            try:
                date = datetime.strptime(date_str, "%Y-%m-%d")
                if date >= week_ago:
                    with open(os.path.join(HISTORY_DIR, fname), 'r') as f:
                        all_cats.extend(json.load(f))
            except:
                pass
    return all_cats

def send_trend_report(counter, total):
    rows = [f"| {cat} | {count} | {count/total*100:.1f}% |" for cat, count in counter.most_common(10)]
    table = "| 类别 | 新闻数量 | 占比 |\n|------|----------|------|\n" + "\n".join(rows)
    card = {
        "msg_type": "interactive",
        "card": {
            "config": {"wide_screen_mode": True},
            "header": {"title": {"tag": "plain_text", "content": "📈 外贸资讯周报（近7天）"}, "template": "green"},
            "elements": [{"tag": "div", "text": {"tag": "lark_md", "content": table}}]
        }
    }
    try:
        resp = requests.post(WEBHOOK_URL, json=card)
        print(f"趋势报告已推送，状态码: {resp.status_code}")
    except Exception as e:
        print(f"推送失败: {e}")

if __name__ == "__main__":
    today_cats = get_today_news()
    if today_cats:
        save_history(today_cats)
        print(f"已保存今日 {len(today_cats)} 条分类数据")
    
    week_cats = load_week_history()
    if week_cats:
        counter = Counter(week_cats)
        send_trend_report(counter, len(week_cats))
    else:
        print("暂无历史数据，请等待几天后再查看趋势")


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