Skill: isongxw/hf-paper-skill (version 1.0.3)

--- name: HuggingFace Papers description: |- 获取 HuggingFace Papers 热门 AI 论文，自动翻译摘要为中文，生成带趋势分析的双语报告。 支持 DeepLX 和 OpenAI 兼容 LLM 两种翻译后端，自动降级。可保存为 Markdown 文件，方便导入 Obsidian 等笔记工具。 使用场景: 1. 用户想查看 HuggingFace 上的热门论文 2. 用户问"最近有什么 AI 论文推荐"或"有什么新论文" 3. 用户需要了解某个领域的最新研究动态 4. 用户想要 AI/ML 论文的摘要和趋势分析 **注意事项：** - 脚本默认通过 DeepLX API 将论文摘要翻译为中文 - 论文标题保留英文原文 - **必须保留完整摘要**，不得自行缩写或概括 - 中文摘要和英文原文摘要同时显示 - 使用 `--no-translate` 参数可输出纯英文报告 - upvotes 数值和论文链接保持原样 **输出规则：** - 运行脚本后，将报告同时 **保存为本地 .md 文件**（存入 skill 目录下的 `reports/` 文件夹） - 文件命名格式：`HuggingFace_本周热门论文_YYYY-MM-DD.md` - 保存完成后，告知用户文件路径，方便放入 Obsidian **工作流程:** 1. 运行 `scripts/get_papers.py` 获取论文数据 2. 运行 `scripts/generate_report.py --output reports/<文件名>.md` 生成报告并保存到文件 3. 将报告内容输出给用户，并告知文件路径方便放入 Obsidian required_environment_variables: - DEEPLX_URL — DeepLX 翻译 API 端点（使用 DeepLX 后端时必需） - OPENAI_API_KEY — OpenAI 兼容 API 密钥（使用 OpenAI 后端时必需） - OPENAI_BASE_URL — OpenAI 兼容 API 地址（可选，默认 https://api.openai.com/v1） - OPENAI_MODEL — 翻译用模型名（可选，默认 gpt-4o-mini） - TRANSLATE_BACKEND — 翻译后端选择（可选，默认 deeplx） --- # HuggingFace Papers Skill 获取 HuggingFace Papers (https://huggingface.co/papers) 热门 AI 论文并生成分析报告。 ## 使用场景 当用户询问以下问题时，使用本 skill： 1. "最近有什么热门论文？" 2. "有什么 AI 论文推荐？" 3. "帮我看看 HuggingFace 上的热门论文" 4. "本周/本月有什么新论文？" 5. "介绍一下最近的 AI 研究趋势" ## 依赖 - Python 3.8+ - `python-dotenv` — 加载 `.env` 文件 ```bash pip3 install python-dotenv ``` ## 使用方法 ### 前置条件：配置翻译后端 脚本支持 **DeepLX** 和 **OpenAI 兼容 API** 两种翻译后端，通过 `.env` 文件配置： ```bash cp .env.example .env # 然后编辑 .env ``` #### 方式一：DeepLX（默认） ```env TRANSLATE_BACKEND="deeplx" DEEPLX_URL="https://api.deeplx.org/你的token/translate" ``` #### 方式二：OpenAI 兼容 LLM 兼容任何 OpenAI 格式的 API（OpenAI、Azure、硅基流动、DeepSeek 等）： ```env TRANSLATE_BACKEND="openai" OPENAI_BASE_URL="https://api.openai.com/v1" OPENAI_API_KEY="sk-your-key-here" OPENAI_MODEL="gpt-4o-mini" ``` > **降级策略**：主后端失败时自动降级到另一后端，都失败则保留英文原文。 `.env` 已加入 `.gitignore`，不会提交到 GitHub。 ### 常用命令 ```bash # 获取论文列表（默认今日热门） python3 scripts/get_papers.py # 生成完整分析报告并保存到本地（推荐） mkdir -p reports && python3 scripts/generate_report.py --period weekly --output reports/HuggingFace_本周热门论文_$(date +%Y-%m-%d).md # 只查看不保存 python3 scripts/generate_report.py # 指定时间范围和数量 python3 scripts/generate_report.py --period weekly --limit 10 # 输出纯英文报告（不翻译） python3 scripts/generate_report.py --period weekly --no-translate # 保存到文件 python3 scripts/generate_report.py --period weekly --output report.md ``` 脚本在 skill 目录下的 `scripts/` 文件夹中，所有命令均从 skill 根目录执行。 ## 参数说明 ### generate_report.py | 参数 | 说明 | 默认值 | |:---|:---|:---| | `--period` | 时间范围: daily/weekly/monthly | weekly | | `--limit` | 论文数量 | 10 | | `--output` | 输出文件路径 | stdout | | `--no-translate` | 不翻译为中文，输出英文原文 | false | ### get_papers.py | 参数 | 说明 | 默认值 | |:---|:---|:---| | `--period` | 时间范围: daily/weekly/monthly | daily | | `--limit` | 返回论文数量 | 10 | | `--format` | 输出格式: md/json | md | | `--output` | 输出文件路径 | stdout | ## 输出语言 - **默认中文**：脚本通过 DeepLX API 自动将论文摘要翻译为中文，中文摘要与英文原文同时显示 - **论文标题**：保留英文原文 - **保留原始数据**：upvotes 数值和论文链接保持原样 - **英文模式**：使用 `--no-translate` 参数跳过翻译，输出纯英文原文 ## DeepLX 翻译 报告使用 DeepLX API 将摘要自动翻译为中文： - 批量翻译，逐条显示进度 - 单条翻译失败时自动降级，保留英文原文 - 无需手动翻译，开箱即用 ## 网络问题处理 如果获取失败： 1. 检查网络连接 2. 稍后重试

path: README.md
content: # hf-paper-skill

HuggingFace Papers 热门论文获取与中文翻译

## 功能

- 获取 HuggingFace Papers 热门论文（日/周/月）
- **双翻译后端**：DeepLX 或 OpenAI 兼容 LLM（OpenAI、硅基流动、DeepSeek 等）
- 中英文摘要对照展示
- 趋势分析 + 推荐阅读排序
- **自动降级**：主后端失败时无缝切换到备用后端，都失败则保留英文原文

## 配置

### 依赖

```bash
pip3 install python-dotenv
```

### 翻译后端

翻译功能支持两种后端，通过 `.env` 文件配置（复制 `.env.example` 为 `.env` 并根据需要填写）：

**DeepLX（默认）：**
```env
TRANSLATE_BACKEND="deeplx"
DEEPLX_URL="https://api.deeplx.org/你的token/translate"
```

**OpenAI 兼容 LLM（OpenAI、硅基流动、DeepSeek 等）：**
```env
TRANSLATE_BACKEND="openai"
OPENAI_BASE_URL="https://api.openai.com/v1"
OPENAI_API_KEY="sk-your-key-here"
OPENAI_MODEL="gpt-4o-mini"
```

> 支持任意 OpenAI 兼容 API，只需修改 `OPENAI_BASE_URL` 和 `OPENAI_MODEL` 即可切换不同的 LLM 服务商。
> 主后端失败时自动降级到另一后端，都失败则保留英文原文。

## 使用方法

```bash
# 获取论文列表（今日热门）
python3 scripts/get_papers.py

# 生成分析报告（本周热门，自动翻译为中文）
mkdir -p reports
python3 scripts/generate_report.py --period weekly --output reports/HuggingFace_本周热门论文_$(date +%Y-%m-%d).md

# 指定数量和范围
python3 scripts/generate_report.py --period weekly --limit 10

# 纯英文输出
python3 scripts/generate_report.py --period weekly --no-translate
```

## 参数

| 参数 | 说明 | 默认值 |
|------|------|--------|
| `--period` | daily / weekly / monthly | weekly |
| `--limit` | 论文数量 | 10 |
| `--output` | 输出文件路径 | stdout |
| `--no-translate` | 不翻译为中文 | false |

## 翻译

### DeepLX

专用翻译 API，速度快、成本低，适合大批量摘要翻译。通过 `DEEPLX_URL` 配置端点。

### OpenAI 兼容 LLM

通过 LLM 进行翻译，利用大模型的语义理解能力，翻译质量更高。支持任何兼容 OpenAI 格式的 API：

| 服务商 | BASE_URL | 推荐模型 |
|--------|----------|----------|
| OpenAI | `https://api.openai.com/v1` | `gpt-4o-mini` |
| 硅基流动 | `https://api.siliconflow.cn/v1` | `Qwen/Qwen2.5-7B-Instruct` |
| DeepSeek | `https://api.deepseek.com/v1` | `deepseek-chat` |
| 通义千问 | `https://dashscope.aliyuncs.com/compatible-mode/v1` | `qwen-turbo` |

## Agent 集成

将本 skill 接入不同的 AI Agent 框架，让 Agent 在用户问"最近有什么热门论文"时自动调起。

### Hermes Agent

```bash
# 方式一：克隆到 Hermes skills 目录
git clone https://github.com/isongxw/hf-paper-skill.git ~/.hermes/skills/research/hf-papers

# 方式二：如果已克隆到其他地方，创建软链接
ln -s /path/to/hf-paper-skill ~/.hermes/skills/research/hf-papers
```

完成后在 `.env` 中配置翻译 token，Hermes 下次启动时自动识别此 skill。

### Claude Code (Claude CLI)

将本 skill 目录添加到 Claude Code 的工作区中，或直接在项目中使用：

```bash
# 克隆到项目内
cd your-project
git clone https://github.com/isongxw/hf-paper-skill.git skills/hf-papers

# 在 CLAUDE.md 中添加配置说明
cat >> CLAUDE.md << 'EOF'

## hf-papers skill

路径：`skills/hf-papers/`
使用：`python3 skills/hf-papers/scripts/generate_report.py --period weekly --limit 10`
配置：复制 `skills/hf-papers/.env.example` 为 `skills/hf-papers/.env` 并填入 token
EOF
```

### OpenClaw

```bash
# 克隆到 OpenClaw workspace 的 skills 目录
git clone https://github.com/isongxw/hf-paper-skill.git /path/to/openclaw/workspace/skills/hf-papers
```

### 通用（任意框架）

本 skill 不依赖任何特定框架，纯 Python 脚本运行，通过 `.env` 文件配置。任何能执行 shell 命令的 Agent 都可以使用：

```bash
git clone https://github.com/isongxw/hf-paper-skill.git
cd hf-paper-skill
cp .env.example .env
# 编辑 .env 填入翻译 token
pip3 install python-dotenv
python3 scripts/generate_report.py --period weekly
```


path: references/date-override.md
content: # 指定日期获取论文（覆盖默认日期）

`get_papers.py` 和 `generate_report.py` 默认只支持 `daily`（今天）、`weekly`、`monthly` 三种周期。
如果需要获取**特定日期**（如昨天、上周某天）的论文，可按以下方式覆盖。

## 方法：Monkey-patch 日期函数

```python
from get_papers import get_papers, format_markdown
from datetime import datetime, timedelta
import get_papers as gp

# 设置目标日期
target = datetime.now() - timedelta(days=1)  # 昨天
date_str = target.strftime('%Y-%m-%d')

# 覆盖日期函数
orig = gp.get_hf_papers_url
def patched_url(period):
    if period == "daily":
        return f"https://huggingface.co/papers/date/{date_str}"
    return orig(period)
gp.get_hf_papers_url = patched_url

# 获取并格式化
papers = get_papers("daily", 10)
print(format_markdown(papers, "daily"))
```

## 用于完整报告（含翻译）

```python
import sys
sys.path.insert(0, 'scripts')

from get_papers import get_papers
from generate_report import format_report
from datetime import datetime, timedelta
import get_papers as gp

target = datetime.now() - timedelta(days=1)
date_str = target.strftime('%Y-%m-%d')

# Monkey-patch
orig = gp.get_hf_papers_url
def patched_url(period):
    if period == "daily":
        return f"https://huggingface.co/papers/date/{date_str}"
    return orig(period)
gp.get_hf_papers_url = patched_url

papers = get_papers("daily", 10)
report = format_report(papers, "daily")

with open(f"reports/HuggingFace_昨日热门论文_{date_str}.md", "w", encoding="utf-8") as f:
    f.write(report)
```

## 手动修正标题

`format_report("daily")` 输出标题为"今日热门"，如需改为"昨日热门"，保存后替换：

```bash
sed -i '' 's/今日/昨日/' reports/HuggingFace_昨日热门论文_*.md
```


path: scripts/generate_report.py
content: #!/usr/bin/env python3
"""
生成 HuggingFace Papers 分析报告
包含论文列表、趋势分析、阅读推荐（含 DeepLX 中文翻译）

Usage:
    python generate_report.py --period weekly --limit 10
    python generate_report.py --period weekly --output report.md
    python generate_report.py --period weekly --no-translate  # 输出英文原文
    python generate_report.py --period daily --date 2026-04-29  # 指定日期
"""

import argparse
import json
import sys
import urllib.request
import urllib.error
from datetime import datetime
from typing import List, Dict, Optional

from get_papers import get_papers

# DeepLX / OpenAI LLM 翻译配置 - 从环境变量读取，禁止硬编码 token
# 设置方式（优先级从高到低）：
#   1. export 环境变量（任意框架通用）
#   2. 在 skill 目录下创建 .env 文件（任意框架通用）
#   3. 在 agent 的默认 env 文件中配置（如 ~/.hermes/.env）
import os
from dotenv import load_dotenv

# 优先加载本地 .env（与脚本同目录或 skill 根目录）
_local_env = os.path.join(os.path.dirname(__file__), '..', '.env')
load_dotenv(_local_env)

# --- DeepLX 配置 ---
DEEPLX_URL = os.environ.get("DEEPLX_URL", "")
DEEPLX_TIMEOUT = 10

# --- OpenAI LLM 配置（备选翻译后端） ---
TRANSLATE_BACKEND = os.environ.get("TRANSLATE_BACKEND", "deeplx")  # deeplx / openai
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
OPENAI_TIMEOUT = 30

TRANSLATION_SYSTEM_PROMPT = (
    "You are a professional translator. Translate the following English academic paper "
    "abstract into Chinese. Keep all technical terms accurate. Output only the translation, "
    "no explanations, no notes."
)


def translate_deeplx(text: str, target_lang: str = "ZH", source_lang: str = "EN") -> Optional[str]:
    """使用 DeepLX API 翻译"""
    payload = json.dumps({
        "text": text,
        "source_lang": source_lang,
        "target_lang": target_lang
    }).encode("utf-8")

    req = urllib.request.Request(
        DEEPLX_URL,
        data=payload,
        headers={
            "Content-Type": "application/json",
            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
        },
        method="POST"
    )

    try:
        with urllib.request.urlopen(req, timeout=DEEPLX_TIMEOUT) as resp:
            result = json.loads(resp.read().decode("utf-8"))
            if result.get("code") == 200:
                return result.get("data", "").strip()
    except (urllib.error.URLError, TimeoutError, json.JSONDecodeError) as e:
        print(f"  ⚠️ DeepLX 翻译失败 ({type(e).__name__})", file=sys.stderr)

    return None


def translate_openai(text: str, target_lang: str = "ZH", source_lang: str = "EN") -> Optional[str]:
    """使用 OpenAI 兼容 API 翻译"""
    if not OPENAI_API_KEY:
        print("  ⚠️ OPENAI_API_KEY 未设置，跳过 LLM 翻译", file=sys.stderr)
        return None

    payload = json.dumps({
        "model": OPENAI_MODEL,
        "messages": [
            {"role": "system", "content": TRANSLATION_SYSTEM_PROMPT},
            {"role": "user", "content": f"Translate from {source_lang} to {target_lang}:\n\n{text}"}
        ],
        "temperature": 0.1,
        "max_tokens": 4096
    }).encode("utf-8")

    url = OPENAI_BASE_URL.rstrip("/") + "/chat/completions"
    req = urllib.request.Request(
        url,
        data=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {OPENAI_API_KEY}",
            "User-Agent": "hf-papers-skill/1.0"
        },
        method="POST"
    )

    try:
        with urllib.request.urlopen(req, timeout=OPENAI_TIMEOUT) as resp:
            result = json.loads(resp.read().decode("utf-8"))
            content = result.get("choices", [{}])[0].get("message", {}).get("content", "").strip()
            if content:
                return content
    except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, IndexError) as e:
        print(f"  ⚠️ OpenAI 翻译失败 ({type(e).__name__})", file=sys.stderr)

    return None


def translate_text(text: str, target_lang: str = "ZH", source_lang: str = "EN") -> Optional[str]:
    """翻译文本（根据 TRANSLATE_BACKEND 选择后端）"""
    if not text or not text.strip():
        return text

    if TRANSLATE_BACKEND == "openai":
        result = translate_openai(text, target_lang, source_lang)
        if result:
            return result
        print("  ⚠️ LLM 翻译失败，降级到 DeepLX...", file=sys.stderr)
        return translate_deeplx(text, target_lang, source_lang) or text

    # 默认：DeepLX
    result = translate_deeplx(text, target_lang, source_lang)
    if result:
        return result
    print("  ⚠️ DeepLX 翻译失败，降级到 OpenAI LLM...", file=sys.stderr)
    return translate_openai(text, target_lang, source_lang) or text


def batch_translate(texts: List[str], target_lang: str = "ZH", source_lang: str = "EN") -> List[str]:
    """批量翻译（逐个调用，打印进度）"""
    results = []
    total = len(texts)
    for i, text in enumerate(texts, 1):
        if not text or not text.strip():
            results.append(text)
            continue
        print(f"  🌐 翻译中 ({i}/{total})...", file=sys.stderr)
        translated = translate_text(text, target_lang, source_lang)
        results.append(translated)
    return results


def analyze_categories(papers: List[Dict]) -> tuple[Dict[str, int], Dict[int, List[str]]]:
    """分析论文类别分布"""
    categories = {
        "Agentic AI": ["agent", "tool", "autonomous", "multi-agent", "llm agent", "agentic", "reasoning agent"],
        "多模态": ["multimodal", "vision", "video", "3d", "image", "visual", "vlm", "vision-language"],
        "生成模型": ["generation", "diffusion", "generative", "synthesis", "gans", "image generation"],
        "大语言模型": ["llm", "language model", "gpt", "claude", "gemini", "chatbot"],
        "推理优化": ["reasoning", "inference", "optimization", "efficient", "fast", "latency"],
        "架构创新": ["transformer", "architecture", "attention", "moe", "mixture", "mamba"],
        "训练方法": ["training", "fine-tuning", "rl", "reinforcement", "distillation", "sft"],
        "安全与对齐": ["safety", "alignment", "rlhf", "preference", "ethics", "bias"]
    }

    category_counts: Dict[str, int] = {cat: 0 for cat in categories}
    paper_categories: Dict[int, List[str]] = {}

    for i, paper in enumerate(papers):
        text = (paper.get("title", "") + " " + paper.get("abstract", "")).lower()
        matched: List[str] = []
        for cat, keywords in categories.items():
            if any(kw in text for kw in keywords):
                category_counts[cat] += 1
                matched.append(cat)
        paper_categories[i] = matched

    return category_counts, paper_categories


def generate_trend_analysis(papers: List[Dict]) -> str:
    """生成趋势分析"""
    category_counts, _ = analyze_categories(papers)
    sorted_cats = sorted(list(category_counts.items()), key=lambda x: x[1], reverse=True)

    lines = ["### 热门方向\n"]
    for cat, count in sorted_cats:
        if count > 0:
            bar = "█" * count + "░" * max(0, 5 - count)
            lines.append(f"- **{cat}**: {bar} ({count}篇)")

    lines.append("\n### 技术趋势\n")

    top_cats = [c for c, n in sorted_cats if n > 0][:3]
    if top_cats:
        lines.append(f"1. **{top_cats[0]}** 是当前最热门方向")
        if len(top_cats) > 1:
            lines.append(f"2. **{top_cats[1]}** 紧随其后")

    return "\n".join(lines)


def generate_recommendations(papers: List[Dict]) -> str:
    """生成阅读推荐"""
    if not papers:
        return ""

    sorted_papers = sorted(papers, key=lambda x: x.get("upvotes", 0), reverse=True)

    lines = ["### 推荐阅读\n"]

    must_read = sorted_papers[:3]
    lines.append("**⭐⭐⭐ 必读 (高热度):**")
    for i, p in enumerate(must_read, 1):
        title = p.get("title", "Unknown")[:70]
        upvotes = p.get("upvotes", 0)
        lines.append(f"{i}. [{title}]({p.get('url', '')}) (👍 {upvotes})")

    if len(sorted_papers) > 3:
        important = sorted_papers[3:7]
        lines.append("\n**⭐⭐ 值得关注:**")
        for p in important:
            title = p.get("title", "Unknown")[:60]
            lines.append(f"- [{title}]({p.get('url', '')})")

    return "\n".join(lines)


def format_report(papers: List[Dict], period: str, no_translate: bool = False) -> str:
    """格式化完整报告"""
    period_names = {
        "daily": "今日",
        "weekly": "本周",
        "monthly": "本月"
    }

    lines = [
        f"# 📄 HuggingFace Papers {period_names.get(period, period)}热门 TOP{len(papers)}",
        "",
        f"**获取时间:** {datetime.now().strftime('%Y年%m月%d日 %H:%M')} (GMT+8)",
        "",
        "---",
        ""
    ]

    sorted_papers = sorted(papers, key=lambda x: x.get("upvotes", 0), reverse=True)

    # 批量翻译摘要（除非指定 --no-translate）
    if not no_translate:
        abstracts = [p.get("abstract", "") for p in sorted_papers]
        translated_abstracts = batch_translate(abstracts)
    else:
        translated_abstracts = [p.get("abstract", "") for p in sorted_papers]

    for i, (paper, cn_abstract) in enumerate(zip(sorted_papers, translated_abstracts), 1):
        title = paper.get("title", "Unknown")
        paper_id = paper.get("paper_id", "")
        url = paper.get("url", f"https://huggingface.co/papers/{paper_id}")
        upvotes = paper.get("upvotes", 0)
        abstract = paper.get("abstract", "")

        lines.append(f"## {i}. {title}")
        lines.append(f"- 👍 **{upvotes}** upvotes")
        lines.append(f"- 🔗 {url}")

        # 如果有中文摘要，显示中英文
        if cn_abstract and cn_abstract != abstract:
            lines.append(f"- 📝 **中文摘要：** {cn_abstract}")
            if abstract:
                lines.append(f"- 📄 **原文摘要：** {abstract}")
        elif abstract:
            lines.append(f"- 📝 {abstract}")

        lines.append("")

    lines.extend([
        "---",
        "",
        "## 📊 趋势分析",
        "",
        generate_trend_analysis(papers),
        "",
        "---",
        "",
        generate_recommendations(papers),
        "",
        "---",
        "",
        "## 🔗 相关资源",
        "",
        "- [HuggingFace Papers](https://huggingface.co/papers)",
        "- [arXiv CS.AI](https://arxiv.org/list/cs.AI/recent)",
        "",
        f"*由 🤖 生成于 {datetime.now().strftime('%Y-%m-%d')}*"
    ])

    return "\n".join(lines)


def main():
    parser = argparse.ArgumentParser(description="生成 HuggingFace Papers 分析报告")
    parser.add_argument("--period", choices=["daily", "weekly", "monthly"], default="weekly",
                        help="时间范围: daily/weekly/monthly")
    parser.add_argument("--limit", type=int, default=10,
                        help="论文数量 (默认: 10)")
    parser.add_argument("--output", type=str, default=None,
                        help="输出文件路径")
    parser.add_argument("--no-translate", action="store_true",
                        help="不翻译为中文，输出英文原文")

    args = parser.parse_args()

    print(f"🔍 正在获取 HuggingFace Papers {args.period} 数据...")

    try:
        papers = get_papers(args.period, args.limit)
    except Exception as e:
        print(f"❌ 获取失败: {e}", file=sys.stderr)
        sys.exit(1)

    if not papers:
        print("❌ 未能获取论文数据", file=sys.stderr)
        sys.exit(1)

    print(f"✅ 成功获取 {len(papers)} 篇论文")

    if not args.no_translate:
        print(f"🌐 正在通过 DeepLX 翻译摘要为中文...")

    report = format_report(papers, args.period, no_translate=args.no_translate)

    if args.output:
        with open(args.output, "w", encoding="utf-8") as f:
            f.write(report)
        print(f"✅ 报告已保存到: {args.output}")
    else:
        print("\n" + report)


if __name__ == "__main__":
    main()


path: scripts/get_papers.py
content: #!/usr/bin/env python3
"""
HuggingFace Papers Fetcher
获取 HuggingFace Papers 热门论文

Usage:
    python get_papers.py --period daily          # 今日热门
    python get_papers.py --period weekly         # 本周热门
    python get_papers.py --period monthly        # 本月热门
    python get_papers.py --period daily --date 2026-04-29   # 指定日期
    python get_papers.py --period weekly --limit 10
"""

import argparse
import json
import re
import sys
import urllib.request
import urllib.error
from datetime import datetime
from typing import List, Dict, Optional


def get_hf_papers_url(period: str, custom_date: Optional[str] = None) -> str:
    """根据时间范围和可选的自定义日期生成 HuggingFace Papers URL"""
    if custom_date:
        # 使用自定义日期
        if period == "daily":
            return f"https://huggingface.co/papers/date/{custom_date}"
        elif period == "weekly":
            d = datetime.strptime(custom_date, "%Y-%m-%d")
            week_number = d.isocalendar()[1]
            return f"https://huggingface.co/papers/week/{d.strftime('%Y')}-W{week_number:02d}"
        elif period == "monthly":
            d = datetime.strptime(custom_date, "%Y-%m-%d")
            return f"https://huggingface.co/papers/month/{d.strftime('%Y-%m')}"
    
    today = datetime.now()
    if period == "daily":
        return f"https://huggingface.co/papers/date/{today.strftime('%Y-%m-%d')}" 
    elif period == "weekly":
        week_number = today.isocalendar()[1]
        return f"https://huggingface.co/papers/week/{today.strftime('%Y')}-W{week_number:02d}"
    elif period == "monthly":
        return f"https://huggingface.co/papers/month/{today.strftime('%Y-%m')}"
    else:
        return f"https://huggingface.co/papers/date/{today.strftime('%Y-%m-%d')}"


def fetch_html(url: str, timeout: int = 15) -> str:
    """获取 HTML 内容"""
    req = urllib.request.Request(
        url,
        headers={
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.5",
        }
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as response:
            return response.read().decode("utf-8")
    except urllib.error.URLError as e:
        raise Exception(f"网络错误：无法访问 {url} - {e.reason}")
    except TimeoutError:
        raise Exception(f"请求超时：{timeout}秒内未响应")


def decode_html_entities(html: str) -> str:
    """解码 HTML 实体和转义字符"""
    result = html
    result = result.replace('&quot;', '"')
    result = result.replace('&amp;', '&')
    result = result.replace('\\&quot;', '"')
    result = result.replace('\\n', ' ')
    return result


def parse_papers(html: str, limit: int = 10) -> List[Dict]:
    """解析页面获取论文列表"""
    decoded = decode_html_entities(html)
    papers = []
    seen_ids = set()
    
    # 提取所有 paper id
    id_pattern = r'"id":"(\d{4}\.\d{4,5})","authors"'
    ids = re.findall(id_pattern, decoded)
    
    for pid in ids:
        if pid in seen_ids:
            continue
        seen_ids.add(pid)
        
        # 找到这个 id 的位置
        id_pos = decoded.find(f'"id":"{pid}"')
        if id_pos < 0:
            continue
        
        # 扩大搜索范围以包含所有字段
        search_range = 15000
        search_block = decoded[id_pos:id_pos+search_range]
        
        # 提取 title
        title_match = re.search(r'"title":"(.+?)"', search_block)
        title = title_match.group(1) if title_match else f"arXiv:{pid}"
        
        # 提取 summary
        summary_match = re.search(r'"summary":"(.+?)(?:","|"\})', search_block)
        summary = summary_match.group(1) if summary_match else ""
        
        # 提取 upvotes
        upvotes_match = re.search(r'"upvotes":(\d+)', search_block)
        upvotes = int(upvotes_match.group(1)) if upvotes_match else 0
        
        papers.append({
            "paper_id": pid,
            "title": title,
            "abstract": summary,
            "upvotes": upvotes,
            "url": f"https://huggingface.co/papers/{pid}"
        })
        
        if len(papers) >= limit:
            break
    
    return papers


def get_papers(period: str = "weekly", limit: int = 10, custom_date: Optional[str] = None) -> List[Dict]:
    """获取热门论文列表"""
    url = get_hf_papers_url(period, custom_date)
    html = fetch_html(url)
    return parse_papers(html, limit)


def format_markdown(papers: List[Dict], period: str) -> str:
    """格式化为 Markdown（英文原文）"""
    period_names = {
        "daily": "今日热门",
        "weekly": "本周热门",
        "monthly": "本月热门"
    }
    
    lines = [
        f"# 📄 HuggingFace Papers {period_names.get(period, period)}",
        "",
        f"**获取时间:** {datetime.now().strftime('%Y/%m/%d %H:%M:%S')} (GMT+8)",
        "",
        "---",
        ""
    ]
    
    for i, paper in enumerate(papers, 1):
        fire = " 🔥" if paper["upvotes"] >= 50 else ""
        lines.append(f'{i}. **{paper["title"]}**{fire}')
        lines.append(f'   - 👍 {paper["upvotes"]:,} upvotes')
        if paper.get("abstract"):
            lines.append(f'   - 📝 {paper["abstract"]}')
        lines.append(f'   - 🔗 {paper["url"]}')
        lines.append("")
    
    lines.extend([
        "---",
        "",
        "*由 🤖 生成*"
    ])
    
    return "\n".join(lines)


def format_json(papers: List[Dict]) -> str:
    """格式化为 JSON"""
    return json.dumps(papers, ensure_ascii=False, indent=2)


def main():
    parser = argparse.ArgumentParser(description="获取 HuggingFace Papers 热门论文")
    parser.add_argument("--period", choices=["daily", "weekly", "monthly"], default="daily",
                        help="时间范围: daily/weekly/monthly")
    parser.add_argument("--limit", type=int, default=10,
                        help="返回论文数量 (默认: 10)")
    parser.add_argument("--format", choices=["md", "json"], default="md",
                        help="输出格式: md/json")
    parser.add_argument("--output", type=str, default=None,
                        help="输出文件路径 (可选)")
    parser.add_argument("--date", type=str, default=None,
                        help="自定义日期 (YYYY-MM-DD)，覆盖自动计算的日期")
    
    args = parser.parse_args()
    
    try:
        papers = get_papers(args.period, args.limit, args.date)
        
        if args.format == "json":
            output = format_json(papers)
        else:
            output = format_markdown(papers, args.period)
        
        if args.output:
            with open(args.output, "w", encoding="utf-8") as f:
                f.write(output)
            print(f"✅ 已保存到: {args.output}")
        else:
            print(output)
            
    except Exception as e:
        print(f"❌ 错误: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


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