Skill: ventaozzz/arxiv-daily-pusher (version 1.0.0)

# arXiv Daily Paper Pusher ## Description Automatically fetch yesterday's arXiv papers, rank by keyword relevance, and push to Feishu (Lark) via webhook. ## Features - Multi-group support with independent keywords - Smart scoring: title matches weighted 2x, abstract 1x - Dual-mode API: arxiv library with automatic HTTP fallback - Per-group or single message push strategies ## Schedule ```yaml schedule: "30 2 * * *" ``` Runs daily at 10:30 AM Beijing Time (02:30 UTC). ## Requirements - Python 3.10+ - Dependencies: `arxiv`, `PyYAML`, `requests` - Feishu Incoming Webhook URL ## Quick Start 1. **Install:** ```bash pip install -r requirements.txt ``` 2. **Configure:** ```bash cp config.example.yaml config.yaml # Edit config.yaml with your webhook URL and keywords ``` 3. **Run:** ```bash python main.py ``` ## Configuration See `config.example.yaml` for full configuration options: - `groups`: Research groups with keywords - `feishu_webhook`: Your Feishu bot webhook URL - `top_k`: Papers per group (default: 6) - `timezone_offset`: Hours from UTC (default: 8 for Beijing) - `api_mode`: "auto" | "arxiv_only" | "http_only" - `push_strategy`: "per_group" | "single" ## Manual Test ```bash cd ~/.openclaw/skills/arxiv-daily-pusher python main.py ```

path: config.example.yaml
content: # 研究组配置
groups:
  - name: "Group A - Example Topic"
    keywords:
      - "machine learning"
      - "deep learning"

  - name: "Group B - Another Topic"
    keywords:
      - "computer vision"
      - "image processing"

# 飞书 Webhook URL（必填）
# 获取方式：飞书群设置 → 群机器人 → 添加自定义机器人 → 复制 Webhook 地址
feishu_webhook: "PASTE_YOUR_WEBHOOK_URL_HERE"

# 全局设置
top_k: 6                    # 每组推送论文数
timezone_offset: 8          # 时区偏移（北京时间=8，UTC=0）
api_mode: "auto"            # API 模式: auto | arxiv_only | http_only
push_strategy: "per_group"  # 推送策略: per_group (每组单独推送) | single (合并推送)


path: main.py
content: """
Phase 2 entry point: Scheduled skill that pushes to Feishu.
"""

import yaml
from datetime import timedelta, timezone

from fetch_papers import fetch_papers_for_group, get_yesterday_range_utc
from rank_papers import rank_and_filter
from push_feishu import push_to_feishu, format_date_range


def load_config(path: str = "config.yaml") -> dict:
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)


def main():
    config = load_config()
    tz_offset = config.get("timezone_offset", 8)
    top_k = config.get("top_k", 6)
    webhook_url = config.get("feishu_webhook", "")

    local_tz = timezone(timedelta(hours=tz_offset))
    start_utc, end_utc = get_yesterday_range_utc(tz_offset)
    date_range = format_date_range(start_utc, end_utc, local_tz)

    print(f"🚀 arXiv Daily Pusher started for {date_range}")

    group_results = []

    for group in config.get("groups", []):
        name = group["name"]
        keywords = group["keywords"]

        print(f"⏳ Fetching: {name}")
        api_mode = config.get("api_mode", "auto")
        candidates = fetch_papers_for_group(
            keywords=keywords,
            tz_offset_hours=tz_offset,
            api_mode=api_mode,
        )
        print(f"   Candidates: {len(candidates)}")

        top_papers = rank_and_filter(papers=candidates, keywords=keywords, top_k=top_k)

        group_results.append({
            "group_name": name,
            "keywords": keywords,
            "papers": top_papers,
        })

    # Push to Feishu
    push_strategy = config.get("push_strategy", "per_group")
    print(f"\n📤 Pushing to Feishu (strategy: {push_strategy})...")
    push_to_feishu(webhook_url, group_results, date_range, push_strategy)

    print("\n✅ Phase 2 complete.")


if __name__ == "__main__":
    main()


path: push_feishu.py
content: """
Pushes plain text paper recommendations to Feishu via Incoming Webhook.
Supports per-group messaging: each research group gets its own message.
"""

import requests
import time


def push_single_group(
    webhook_url: str,
    group_name: str,
    keywords: list[str],
    papers: list[dict],
    date_range: str,
) -> bool:
    """Push a single research group's results as one message."""

    lines = [f"📚 [{group_name}] {date_range}\n"]
    lines.append("=" * 50)
    lines.append(f"🔑 关键词：{', '.join(keywords)}")
    lines.append("=" * 50)

    if not papers:
        lines.append("\n⚠️ 昨日无匹配论文")
    else:
        for i, paper in enumerate(papers, 1):
            lines.append(f"\n[{i}] {paper['title']}")
            lines.append(f"📅 {paper['published_local']}  |  Score: {paper['score']}")
            lines.append(f"🔗 {paper['alphaxiv_url']}")

    message_text = "\n".join(lines)

    payload = {"msg_type": "text", "content": {"text": message_text}}

    try:
        resp = requests.post(webhook_url, json=payload, timeout=10)
        resp.raise_for_status()
        return True
    except Exception as e:
        print(f"     ❌ Push failed: {e}")
        return False


def push_to_feishu(
    webhook_url: str,
    group_results: list[dict],
    date_range: str,
    push_strategy: str = "per_group",
) -> None:
    """Push paper recommendations to Feishu."""

    if (not webhook_url) or webhook_url == "" or webhook_url == "PASTE_YOUR_WEBHOOK_URL_HERE":
        print("❌ ERROR: feishu_webhook is empty in config.yaml")
        return

    if push_strategy == "per_group":
        success_count = 0
        fail_count = 0

        for i, group in enumerate(group_results, 1):
            print(f"\n📤 Pushing Group {i}/{len(group_results)}: {group['group_name']}...")

            success = push_single_group(
                webhook_url=webhook_url,
                group_name=group["group_name"],
                keywords=group["keywords"],
                papers=group["papers"],
                date_range=date_range,
            )

            if success:
                print(f"   ✅ Successfully pushed {group['group_name']}")
                success_count += 1
            else:
                print(f"   ❌ Failed to push {group['group_name']}")
                fail_count += 1

            if i < len(group_results):
                time.sleep(2)

        print(f"\n📊 Push summary: {success_count} succeeded, {fail_count} failed")

    else:
        lines = [f"📚 arXiv 每日论文推荐 · {date_range}\n"]

        for group in group_results:
            lines.append(f"\n{'='*50}")
            lines.append(f"📌 {group['group_name']}")
            lines.append(f"🔑 关键词：{', '.join(group['keywords'])}")
            lines.append("=" * 50)

            if not group["papers"]:
                lines.append("⚠️ 昨日无匹配论文")
            else:
                for i, paper in enumerate(group["papers"], 1):
                    lines.append(f"\n[{i}] {paper['title']}")
                    lines.append(f"📅 {paper['published_local']}  |  Score: {paper['score']}")
                    lines.append(f"🔗 {paper['alphaxiv_url']}")

        message_text = "\n".join(lines)
        payload = {"msg_type": "text", "content": {"text": message_text}}

        try:
            resp = requests.post(webhook_url, json=payload, timeout=10)
            resp.raise_for_status()
            print("✅ Successfully pushed to Feishu (single message)")
        except Exception as e:
            print(f"❌ Failed to push to Feishu: {e}")


def format_date_range(start_utc, end_utc, local_tz) -> str:
    """Helper to format date range for title."""

    start_local = start_utc.astimezone(local_tz)
    return start_local.strftime("%Y-%m-%d")


path: rank_papers.py
content: """
Scores each paper by keyword relevance (local, no external API needed).
Title match = 2pts per keyword, abstract match = 1pt per keyword.
Score is normalized to [0, 1].
"""


def compute_relevance_score(paper: dict, keywords: list[str]) -> float:
    title_lower = paper["title"].lower()
    abstract_lower = paper["abstract"].lower()
    max_possible = len(keywords) * 3
    score = 0.0
    for kw in keywords:
        kw_lower = kw.lower()
        if kw_lower in title_lower:
            score += 2
        if kw_lower in abstract_lower:
            score += 1
    return round(score / max_possible, 4) if max_possible > 0 else 0.0


def rank_and_filter(papers: list[dict], keywords: list[str], top_k: int = 6) -> list[dict]:
    for paper in papers:
        paper["score"] = compute_relevance_score(paper, keywords)
    ranked = sorted(papers, key=lambda p: (p["score"], p["published_local"]), reverse=True)
    return ranked[:top_k]


path: requirements.txt
content: arxiv>=2.1.0
PyYAML>=6.0
requests>=2.31.0


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