Skill: champagne315/follow-arxiv (version 1.0.0)

--- name: Follow-arXiv-Paper description: 搜索过去一段用户指定的时间内arxiv上某题材的论文，并可以下载，阅读，深度分析特定论文。在用户请求处理arxiv平台论文，或想要得到最新的论文概要时使用 --- # Arxiv 论文处理 Skill 你是一个专业的 Arxiv 论文处理助手，具备论文搜索、内容提取和深度分析的能力。你可以帮助用户追踪特定领域的最新研究动态，生成每日论文摘要，以及对特定论文进行深度分析。 **这个 skill 完全在你的本地机器上运行。** 你对搜索关键词、时间范围和分析深度拥有完全控制权。 --- ## 首次运行 —— Onboarding 检查是否已完成 onboarding： ```bash python cli.py check-onboarding ``` 如果返回 `{"complete": false}`，运行 onboarding 流程： ### Step 1: 依赖检查 ```bash python cli.py check-dependencies ``` 如果 `ok: false`，告诉用户需要安装缺失的依赖并显示 `missing` 列表。 ### Step 2: 介绍 告诉用户： "我是你的 Arxiv 论文助手。我可以帮你： 1. **生成每日论文日报** —— 追踪指定领域的最新研究动态 2. **深度分析特定论文** —— 下载 PDF 并进行全方位分析 3. **自定义配置** —— 调整搜索关键词、时间范围、报告风格等 默认情况下，我会搜索过去 24 小时内发布的 AI Agent 相关论文（cs.AI, cs.CL, cs.LG 分类）。" ### Step 3: 语言设置 询问："你偏好什么语言的摘要？" 选项： - 中文（默认） - English - Bilingual（中英双语） ### Step 4: 时间窗口 询问："你想监控多长时间内的论文？默认是过去 24 小时。" 选项： - 24 小时（默认） - 48 小时 - 72 小时 - 1 周（168 小时） ### Step 5: 搜索关键词 询问："你想追踪哪个研究领域的论文？默认是 'AI Agent'。" 示例： - "AI Agent"（默认） - "Large Language Model" - "Machine Learning" - 或输入自定义关键词 ### Step 6: 完成配置 根据用户选择，保存配置： ```bash python cli.py set-config language "zh" python cli.py set-config time_window_hours 24 python cli.py set-config default_query "AI Agent" python cli.py set-config onboarding_complete true ``` ### Step 7: 欢迎运行 **不要跳过这一步。** 立即生成并向用户发送他们的第一个日报。 告诉用户："让我立即搜索最近的论文并生成一个示例日报。这大约需要 30 秒。" 然后运行"每日论文日报流程"。 交付日报后，询问反馈： "这是你的第一个 Arxiv 论文日报！几个问题： - 论文数量是否合适？ - 摘要风格是否满意？ - 你想调整时间窗口或搜索关键词吗？ 只需告诉我，我会调整。" --- ## 每日论文日报流程 ### Step 1: 准备日报数据 **这是唯一需要的数据获取步骤。** ```bash python cli.py prepare-daily ``` 该命令会： - 搜索论文（使用当前配置的查询、时间窗口、分类） - 加载日报提示词模板 - 填充论文数据到提示词 - **将数据保存为临时文件到项目的 `temp/` 目录** - 返回包含文件路径的简短 JSON，包含： - `file_path`: 临时文件完整路径 - `paper_count`: 论文数量 - `message`: 提示信息 **重要**：命令不再返回完整的论文数据 JSON，而是返回文件路径。你需要使用 Read 工具读取该文件来获取完整数据。 ### Step 2: 读取数据文件 命令返回的 JSON 包含 `file_path` 字段。**使用 Read 工具读取该文件**来获取完整数据。 命令返回的简短 JSON： ```json { "success": true, "file_path": "/path/to/project/temp/daily_data_1234567890.json", "paper_count": 10, "message": "Data saved to file. Use Read tool to read the file." } ``` 使用 Read 工具读取文件： ``` 读取 {file_path} 中的内容 ``` 文件中的完整数据结构： ```json { "success": true, "papers": [ { "title": "论文标题", "arxiv_id": "2604.01221v1", "authors": [...], "summary": "...", "published": "2026-04-01 17:58:33", "categories": ["cs.AI", "cs.CV"], ... } ], "paper_count": 10, "prompt": "填充数据后的完整提示词...", "instruction": "请使用上述提示词和数据生成论文日报", "config": { "language": "zh", "time_window_hours": 24, "default_query": "AI Agent", "max_results": 10, "search_categories": ["cs.AI", "cs.CL", "cs.LG"] } } ``` ### Step 3: 检查结果 检查读取到的数据： - 如果 `success: false`，显示 `error` 信息给用户 - 如果 `papers` 为空，告诉用户："在指定时间内未找到符合条件的论文，建议调整时间窗口或搜索关键词" - 如果有论文，继续 Step 4 ### Step 4: 生成日报 **直接使用返回的 `prompt` 字段生成日报**。不要修改或拼接它。 根据 `config.language` 应用语言设置： **如果是 "zh"**：使用中文生成日报 **如果是 "en"**：使用英文生成日报 **如果是 "bilingual"**：生成双语日报（先中文，后英文） 输出格式必须包含： 1. **概览统计**：论文数量、时间范围、主要领域 2. **重点推荐**（2-3 篇）：Arxiv ID、一句话总结、创新点、推荐理由 3. **完整论文列表**：标题、ID、作者、分类、摘要、关键词 4. **研究趋势观察**：基于论文列表总结热点和趋势 5. **建议关注**：推荐的阅读优先级 ### Step 4: 自省询问 ``` ✅ 日报已生成完毕（共 {count} 篇论文） 接下来您可以： 1. 调整搜索关键词（当前: "{current_query}"） 2. 修改时间窗口（（当前: {current_window}小时） 3. 调整报告风格或模板 4. 对某篇论文进行深度分析（输入序号，如"分析第 1 篇"） 5. 导出日报为文件 请告诉我您的需求。 ``` --- ## 特定论文深度分析流程 ### Step 1: 确认目标论文 根据用户输入获取论文 ID： **场景 A - 用户提供 Arxiv ID**： ``` 用户: "分析论文 2401.12345" target_arxiv_id = "2401.12345" ``` **场景 B - 用户提供关键词**： ``` 用户: "帮我找一篇关于 AI Agent 工具使用的论文" 使用搜索： python cli.py search --query "AI Agent tool use" --max-results 5 展示前 5 篇论文，让用户选择。 ``` **场景 C - 用户引用之前的搜索结果**： ``` 用户: "分析第 2 篇论文" 使用上下文中已存在的 papers 列表，获取 papers[1] 的 arxiv_id ``` ### Step 2: 准备分析数据 告诉用户："正在下载并处理论文 PDF，请稍候..." ```bash python cli.py prepare-analysis <arxiv_id> ``` 该命令会： - 查找论文元数据 - 下载 PDF - 提取文本内容（含公式） - 加载深度分析提示词模板 - 填充数据到提示词 - **将数据保存为临时文件到项目的 `temp/` 目录** - 返回包含文件路径的简短 JSON 命令返回的简短 JSON： ```json { "success": true, "file_path": "/path/to/project/temp/analysis_data_2604_01221v1_1234567890.json", "arxiv_id": "2604.01221v1", "message": "Data saved to file. Use Read tool to read the file." } ``` **使用 Read 工具读取该文件**来获取完整数据： 文件中的完整数据结构： ```json { "success": true, "paper": { "title": "论文标题", "arxiv_id": "2604.01221v1", "authors": [...], "summary": "...", "content": { "full_text": "完整PDF文本...", "equations": [...], "sections": [...] } }, "prompt": "填充数据后的完整提示词...", "config": { "language": "zh", "analysis_type": "deep" } } ``` ### Step 3: 检查结果 读取命令返回的 `file_path` 指向的文件，检查数据： - 如果 `success: false`，显示 `error` 信息，询问是否仅基于摘要分析 - 如果成功，继续 Step 4 ### Step 4: 生成分析报告 **直接使用返回的 `prompt` 字段生成报告**。不要修改或拼接它。 根据 `config.language` 和 `config.analysis_type` 生成报告。 输出格式必须包含： 1. **基本信息**：标题、作者、ID、日期、分类 2. **研究背景与动机**：问题陈述、研究背景、现有方法局限、研究动机 3. **核心贡献**（3-5 点）：主要贡献点、创新性分析、技术突破 4. **方法论**：整体框架、核心技术、数学建模、实现细节 5. **实验设计与结果**：实验设置、主要结果、消融研究、性能分析 6. **讨论与分析**：优势、局限性、适用场景 7. **未来工作展望**：改进方向、应用前景 8. **个人评价**：整体评价、亮点、不足、推荐指数（1-5 星） ### Step 5: 自省询问 ``` ✅ 论文分析报告已生成 接下来您可以： 1. 分析其他论文 2. 对比多篇论文 3. 调整分析深度或风格 4. 提取论文中的关键代码/数据 5. 导出报告为文件 请告诉我您的需求。 ``` --- ## 配置管理 所有配置操作通过 CLI 命令完成。 ### 显示当前设置 ```bash python cli.py get-config ``` 返回完整的配置 JSON，以友好格式展示给用户。 ### 修改时间窗口 ```bash python cli.py set-config time_window_hours 48 ``` ### 修改搜索关键词 ```bash python cli.py set-config default_query "Large Language Model" ``` ### 修改搜索分类 查看当前分类： ```bash python cli.py get-config | python -c "import sys,json; print(json.load(sys.stdin)['config']['search_categories'])" ``` 添加分类（需要获取当前配置，修改，然后保存）： ```bash python cli.py set-config search_categories '["cs.AI","cs.CL","cs.LG","cs.CV"]' ``` ### 修改语言设置 ```bash python cli.py set-config language "en" ``` ### 修改最大结果数 ```bash python cli.py set-config max_results 20 ``` ### 重置为默认配置 ```bash python cli.py reset-config ``` --- ## 提示词自定义 ### 查看所有提示词 ```bash python cli.py list-prompts ``` ### 获取提示词内容 ```bash python cli.py get-prompt daily_summary python cli.py get-prompt deep_analysis ``` ### 修改提示词 用户："让日报更简洁一点" ```bash echo "新的提示词内容" | python cli.py set-prompt daily_summary ``` 或使用文件： ```bash python cli.py set-prompt daily_summary --content "$(cat new_prompt.md)" ``` ### 重置提示词 ```bash python cli.py reset-prompt daily_summary python cli.py reset-prompt deep_analysis ``` --- ## 最佳实践 1. **使用单一命令**：`prepare-daily` 和 `prepare-analysis` 封装了所有步骤，不要拆分执行 2. **JSON 解析**：所有命令输出标准 JSON，易于解析和处理 3. **配置驱动**：通过修改配置改变行为，不要在命令行传递参数 4. **上下文保持**：记住之前的搜索结果，避免重复执行 5. **主动询问**：每次完成任务后，主动询问是否需要调整或继续 --- ## CLI 命令参考 | 命令 | 说明 | |------|------| | `check-onboarding` | 检查是否完成 onboarding | | `check-dependencies` | 检查依赖安装状态 | | `complete-onboarding` | 完成 onboarding（设置初始配置） | | `get-config` | 获取当前配置 | | `set-config <key> <value>` | 设置配置项 | | `reset-config` | 重置为默认配置 | | `search` | 搜索论文（使用当前配置或覆盖参数） | | `process-pdf <arxiv_id>` | 下载并处理 PDF | | `prepare-daily` | 准备日报数据（搜索+提示词填充） | | `prepare-analysis <arxiv_id>` | 准备分析数据（PDF+提示词填充） | | `get-prompt <name>` | 获取提示词内容 | | `set-prompt <name>` | 设置提示词内容 | | `reset-prompt <name>` | 重置提示词为默认 | | `list-prompts` | 列出所有可用提示词 | --- ## 技术说明 - **依赖库**: - `arxiv>=2.1.0`: Arxiv API 客户端 - `pymupdf>=1.23.0`: PDF 处理 - `requests>=2.31.0`: HTTP 请求 - **配置路径**: - 用户配置: `~/.arxiv-search/config.json` - 用户提示词: `~/.arxiv-search/prompts/` - PDF 缓存: `~/.arxiv-search/pdfs/` - 默认提示词: `prompts/` - **安全性**: - 所有操作通过 CLI 命令封装 - 无动态代码执行 - 输入验证在 CLI 内部完成 ---

path: prompts/daily_summary.md
content: # Arxiv 论文日报生成提示词

你是一名专业的学术研究助手，负责生成每日 Arxiv 论文摘要报告。

## 任务说明

你将收到一份最近 {{time_window}} 小时内发布的 Arxiv 论文列表（共 {{total_papers}} 篇）。请生成一份结构化的论文日报。

## 输出要求

### 1. 报告结构

```markdown
# 📚 Arxiv 论文日报 - [日期]

## 📊 概览
- 论文数量：X 篇
- 时间范围：[起始时间] - [结束时间]
- 主要领域：[主要研究类别]

## 🔥 重点推荐
[筛选 2-3 篇最有价值/创新性的论文进行重点介绍]

### 论文标题 1
- **Arxiv ID**: XXX
- **作者**: XXX 等
- **一句话总结**: [简明扼要的核心贡献]
- **创新点**: [主要创新点和价值]
- **推荐理由**: [为什么值得关注]

## 📋 论文列表

### 1. 论文标题
- **Arxiv ID**: XXX
- **作者**: XXX 等
- **发布日期**: XXX
- **分类**: XXX
- **摘要**: [摘要内容]
- **关键词**: [提取 3-5 个关键词]

[重复所有论文...]

## 💡 研究趋势观察
[基于本次论文列表，总结研究热点、技术趋势、方法论进展等]

## 📌 建议关注
[哪些论文值得深入阅读，建议的阅读优先级]
```

### 2. 内容要求

- **客观准确**：摘要和总结必须基于论文原文，不得虚构
- **简洁清晰**：避免冗长描述，突出核心信息
- **重点突出**：识别真正有价值的论文，不平均分配篇幅
- **可操作性**：为读者提供明确的阅读建议

### 3. 评价维度

在筛选重点论文时，考虑以下维度：
- **创新性**：是否提出了新方法、新视角或新发现
- **实用性**：是否有实际应用价值或工程意义
- **完整性**：研究是否完整、方法论是否严谨
- **时效性**：是否紧跟当前研究热点
- **影响力**：潜在的学术或产业影响力

## 论文数据

{{papers}}

---

请按照以上要求生成高质量的论文日报。


path: prompts/deep_analysis.md
content: # Arxiv 论文深度分析提示词

你是一名资深的学术研究助手，负责对 Arxiv 论文进行深度分析和解读。

## 任务说明

你将收到一篇论文的完整内容（包括元数据、全文、公式等）。请进行深度分析，生成一份全面的研究报告。

## 输出要求

### 1. 报告结构

```markdown
# 📄 论文深度分析报告

## 基本信息
- **标题**: [论文标题]
- **作者**: [作者列表]
- **Arxiv ID**: [ID]
- **发布日期**: [日期]
- **分类**: [研究类别]
- **PDF 链接**: [链接]

## 一、研究背景与动机

### 1.1 问题陈述
[论文要解决的核心问题是什么？]

### 1.2 研究背景
[该领域的发展历程和现状]

### 1.3 现有方法的局限性
[现有方法的不足，引出本文动机]

### 1.4 研究动机
[作者为什么要做这个研究？痛点是什么？]

## 二、核心贡献

### 2.1 主要贡献点
[列出 3-5 个核心贡献]

1. **贡献点 1**: [详细说明]
2. **贡献点 2**: [详细说明]
3. **贡献点 3**: [详细说明]

### 2.2 创新性分析
[本文相比已有工作的创新之处]

### 2.3 技术突破
[关键技术突破点]

## 三、方法论

### 3.1 整体框架
[方法的整体架构和流程]

### 3.2 核心技术
[关键技术和算法细节]

#### 技术点 1
[详细描述]

#### 技术点 2
[详细描述]

### 3.3 数学建模
[数学公式和理论推导]

### 3.4 实现细节
[重要的实现细节和技巧]

## 四、实验设计与结果

### 4.1 实验设置
- **数据集**: [使用的数据集]
- **基线方法**: [对比的方法]
- **评价指标**: [评价标准]
- **实验环境**: [硬件、软件环境]

### 4.2 主要结果

#### 实验 1: [实验名称]
[实验设置和结果]

| 方法 | 指标 1 | 指标 2 | 指标 3 |
|------|--------|--------|--------|
| 本文方法 | XX | XX | XX |
| 基线 1 | XX | XX | XX |
| 基线 2 | XX | XX | XX |

[结果分析和讨论]

#### 实验 2: [实验名称]
[实验设置和结果]

### 4.3 消融研究
[消融实验的结果和结论]

### 4.4 性能分析
[性能、效率、可扩展性分析]

## 五、讨论与分析

### 5.1 优势分析
[方法的主要优势]

### 5.2 局限性
[方法的局限性和不足]

### 5.3 适用场景
[最适合的应用场景]

### 5.4 失败案例分析
[方法可能失败的情况]

## 六、相关研究对比

| 维度 | 本文 | 工作 A | 工作 B |
|------|------|--------|--------|
| 方法 | XXX | XXX | XXX |
| 创新点 | XXX | XXX | XXX |
| 性能 | XX | XX | XX |

## 七、未来工作展望

### 7.1 改进方向
[可能的改进方向]

### 7.2 应用前景
[潜在的应用场景和领域]

### 7.3 研究启发
[对后续研究的启发]

## 八、个人评价

### 8.1 整体评价
[对论文整体质量和价值的评价]

### 8.2 亮点
[最值得学习的亮点]

### 8.3 不足
[存在的不足和问题]

### 8.4 推荐指数
⭐⭐⭐⭐☆ [4/5]

### 8.5 推荐理由
[推荐或不推荐的理由]

## 九、关键引用

1. [重要参考文献 1]
2. [重要参考文献 2]
3. [重要参考文献 3]

## 十、代码与资源

- **官方代码**: [是否开源，GitHub 链接]
- **数据集**: [数据集获取方式]
- **其他资源**: [相关资源]

---

## 附录：关键公式解释

### 公式 1
[公式内容和详细解释]

### 公式 2
[公式内容和详细解释]
```

### 2. 分析要求

- **深度**：不仅要描述"是什么"，更要分析"为什么"和"怎么做"
- **系统性**：全面覆盖论文的各个方面，不遗漏重要内容
- **批判性**：客观评价论文的优缺点，不盲目赞美
- **可理解性**：用清晰的语言解释复杂概念，帮助读者理解
- **实用性**：提供实际的研究启发和应用建议

### 3. 评价标准

#### 整体评分（满分 5 星）
- ⭐⭐⭐⭐⭐ (5/5): 顶级工作，具有重大创新和影响力
- ⭐⭐⭐⭐☆ (4/5): 优秀工作，有明显创新和实用价值
- ⭐⭐⭐☆☆ (3/5): 合格工作，有一定贡献但创新有限
- ⭐⭐☆☆☆ (2/5): 一般工作，创新性或质量不足
- ⭐☆☆☆☆ (1/5): 存在明显问题，不推荐

#### 评价维度
1. **创新性** (30%): 方法、视角、应用等方面的创新程度
2. **严谨性** (25%): 理论基础、实验设计的严谨程度
3. **实用性** (20%): 实际应用价值和可操作性
4. **可复现性** (15%): 细节描述是否充分、代码是否开源
5. **写作质量** (10%): 论文写作的清晰度和逻辑性

## 论文数据

**元数据**:
{{metadata}}

**全文内容**:
{{full_text}}

**公式列表** (共 {{equation_count}} 个):
{{equations}}

---

请按照以上要求进行深度分析，生成高质量的论文分析报告。


path: src/analyzer.py
content: """论文深度分析模块"""

from typing import Dict, Any, List, Optional
import json


class PaperAnalyzer:
    """论文分析器"""

    def __init__(self, config: Dict[str, Any]):
        self.config = config

    def prepare_analysis_context(
        self,
        paper_data: Dict[str, Any],
        analysis_type: str = "deep"
    ) -> Dict[str, Any]:
        """
        准备分析上下文，返回给 Agent

        Args:
            paper_data: 论文数据（包含元数据和内容）
            analysis_type: 分析类型 ("deep" 或 "quick")

        Returns:
            分析上下文字典
        """
        metadata = {
            'title': paper_data.get('title', ''),
            'authors': paper_data.get('authors', []),
            'arxiv_id': paper_data.get('arxiv_id', ''),
            'published': paper_data.get('published', ''),
            'categories': paper_data.get('categories', []),
            'summary': paper_data.get('summary', '')
        }

        content = paper_data.get('content', {})
        full_text = content.get('full_text', '')
        equations = content.get('equations', [])

        # 构建分析上下文
        context = {
            'metadata': metadata,
            'full_text': full_text,
            'equations': equations,
            'analysis_type': analysis_type,
            'content_length': len(full_text),
            'equation_count': len(equations)
        }

        # 加载提示词模板并填充数据
        from utils import load_prompt
        prompt_template = load_prompt('deep_analysis')

        # 填充提示词模板
        filled_prompt = self._fill_prompt_template(prompt_template, context)

        # 返回完整信息给 Agent
        return {
            'context': context,
            'prompt': filled_prompt,
            'instruction': '请使用上述提示词和数据生成论文深度分析报告'
        }

    def prepare_summary_context(
        self,
        papers: List[Dict[str, Any]]
    ) -> Dict[str, Any]:
        """
        准备日报摘要上下文

        Args:
            papers: 论文列表

        Returns:
            摘要上下文字典
        """
        papers_metadata = []

        for paper in papers:
            metadata = {
                'title': paper.get('title', ''),
                'authors': paper.get('authors', []),
                'arxiv_id': paper.get('arxiv_id', ''),
                'published': paper.get('published', ''),
                'categories': paper.get('categories', []),
                'summary': paper.get('summary', ''),
                'pdf_url': paper.get('pdf_url', '')
            }
            papers_metadata.append(metadata)

        context = {
            'papers': papers_metadata,
            'total_papers': len(papers_metadata),
            'time_window': self.config.get('time_window_hours', 24)
        }

        # 加载提示词模板并填充数据
        from utils import load_prompt
        prompt_template = load_prompt('daily_summary')

        # 填充提示词模板
        filled_prompt = self._fill_prompt_template(prompt_template, context)

        # 返回完整信息给 Agent
        return {
            'context': context,
            'prompt': filled_prompt,
            'instruction': '请使用上述提示词和数据生成论文日报'
        }

    def _fill_prompt_template(self, template: str, context: Dict[str, Any]) -> str:
        """
        填充提示词模板

        Args:
            template: 提示词模板
            context: 上下文数据

        Returns:
            填充后的提示词
        """
        filled = template

        # 简单的模板变量替换
        for key, value in context.items():
            placeholder = '{{' + key + '}}'
            if isinstance(value, (list, dict)):
                # 格式化为 JSON 字符串
                filled = filled.replace(placeholder, json.dumps(value, ensure_ascii=False, indent=2))
            elif isinstance(value, (int, float, str)):
                filled = filled.replace(placeholder, str(value))

        return filled


def prepare_analysis(
    paper_data: Dict[str, Any],
    analysis_type: str = "deep",
    config: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """
    准备分析的便捷函数

    Args:
        paper_data: 论文数据
        analysis_type: 分析类型
        config: 配置字典

    Returns:
        包含 context、prompt、instruction 的字典
    """
    if config is None:
        from utils import load_config
        config = load_config()

    analyzer = PaperAnalyzer(config)
    return analyzer.prepare_analysis_context(paper_data, analysis_type)


def prepare_summary(
    papers: List[Dict[str, Any]],
    config: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """
    准备日报摘要的便捷函数

    Args:
        papers: 论文列表
        config: 配置字典

    Returns:
        包含 context、prompt、instruction 的字典
    """
    if config is None:
        from utils import load_config
        config = load_config()

    analyzer = PaperAnalyzer(config)
    return analyzer.prepare_summary_context(papers)


path: src/pdf_processor.py
content: """PDF 处理与转换模块"""

import os
import requests
import fitz  # PyMuPDF
from typing import Dict, Any, Optional
from pathlib import Path
import re


class PDFProcessor:
    """PDF 处理器，支持 LaTeX 公式保留"""

    def __init__(self, config: Dict[str, Any]):
        self.pdf_dir = config.get('pdf_download_dir', './pdfs')
        self.max_retries = config.get('max_retries', 3)
        os.makedirs(self.pdf_dir, exist_ok=True)

    def download_pdf(self, pdf_url: str, arxiv_id: str) -> str:
        """
        下载 PDF 文件

        Args:
            pdf_url: PDF 的 URL
            arxiv_id: Arxiv ID

        Returns:
            下载后的文件路径
        """
        pdf_path = os.path.join(self.pdf_dir, f"{arxiv_id}.pdf")

        # 如果已存在，直接返回
        if os.path.exists(pdf_path):
            return pdf_path

        # 下载 PDF
        for attempt in range(self.max_retries):
            try:
                response = requests.get(pdf_url, timeout=30)
                response.raise_for_status()

                with open(pdf_path, 'wb') as f:
                    f.write(response.content)

                return pdf_path

            except Exception as e:
                if attempt == self.max_retries - 1:
                    raise Exception(f"下载 PDF 失败: {e}")
                continue

        raise Exception("下载 PDF 失败")

    def extract_text_with_latex(self, pdf_path: str) -> Dict[str, Any]:
        """
        提取 PDF 文本，尽可能保留 LaTeX 公式

        Args:
            pdf_path: PDF 文件路径

        Returns:
            包含结构化文本内容的字典
        """
        try:
            doc = fitz.open(pdf_path)

            sections = []
            full_text = []
            equations = []

            for page_num, page in enumerate(doc):
                # 提取文本块
                blocks = page.get_text("dict")["blocks"]

                for block in blocks:
                    if "lines" in block:
                        block_text = []

                        for line in block["lines"]:
                            line_text = ""
                            for span in line["spans"]:
                                text = span["text"]
                                font = span["font"]

                                # 检测是否为数学公式（基于字体）
                                if self._is_math_font(font):
                                    # 尝试保留 LaTeX 格式
                                    latex_text = self._extract_latex(text, font)
                                    line_text += latex_text
                                    if latex_text.strip():
                                        equations.append({
                                            'page': page_num + 1,
                                            'content': latex_text
                                        })
                                else:
                                    line_text += text

                            block_text.append(line_text)

                        section_text = '\n'.join(block_text)
                        if section_text.strip():
                            sections.append({
                                'page': page_num + 1,
                                'text': section_text,
                                'bbox': block.get("bbox", [])
                            })
                            full_text.append(section_text)

            # 在关闭文档之前获取页数
            total_pages = len(doc)
            doc.close()

            return {
                'full_text': '\n\n'.join(full_text),
                'sections': sections,
                'equations': equations,
                'metadata': {
                    'total_pages': total_pages,
                    'total_equations': len(equations)
                }
            }

        except Exception as e:
            raise Exception(f"提取 PDF 文本失败: {e}")

    def _is_math_font(self, font_name: str) -> bool:
        """判断是否为数学字体"""
        math_keywords = ['math', 'symbol', 'cal', 'cmr', 'cmsy', 'cmmi']
        font_lower = font_name.lower()
        return any(keyword in font_lower for keyword in math_keywords)

    def _extract_latex(self, text: str, font: str) -> str:
        """尝试提取 LaTeX 公式"""
        # 简单的 LaTeX 保留策略
        # 如果文本包含特殊数学符号，尝试用 LaTeX 表示

        # 常见符号映射
        symbol_map = {
            'α': r'\alpha',
            'β': r'\beta',
            'γ': r'\gamma',
            'δ': r'\delta',
            'ε': r'\epsilon',
            'θ': r'\theta',
            'λ': r'\lambda',
            'μ': r'\mu',
            'π': r'\pi',
            'σ': r'\sigma',
            'φ': r'\phi',
            'ω': r'\omega',
            '∞': r'\infty',
            '∑': r'\sum',
            '∏': r'\prod',
            '∫': r'\int',
            '∂': r'\partial',
            '∇': r'\nabla',
            '≤': r'\leq',
            '≥': r'\geq',
            '≠': r'\neq',
            '∈': r'\in',
            '⊂': r'\subset',
            '→': r'\rightarrow',
        }

        latex_text = text
        for symbol, latex in symbol_map.items():
            latex_text = latex_text.replace(symbol, latex)

        return latex_text

    def process_paper(self, paper_metadata: Dict[str, Any]) -> Dict[str, Any]:
        """
        处理论文：下载 PDF 并提取文本

        Args:
            paper_metadata: 论文元数据

        Returns:
            包含元数据和文本内容的字典
        """
        arxiv_id = paper_metadata['arxiv_id']
        pdf_url = paper_metadata['pdf_url']

        # 下载 PDF
        pdf_path = self.download_pdf(pdf_url, arxiv_id)

        # 提取文本
        text_data = self.extract_text_with_latex(pdf_path)

        # 合并元数据和文本
        result = {
            **paper_metadata,
            'content': text_data
        }

        return result


def process_pdf(
    paper_metadata: Dict[str, Any],
    config: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
    """
    处理 PDF 的便捷函数

    Args:
        paper_metadata: 论文元数据
        config: 配置字典

    Returns:
        处理后的论文数据
    """
    if config is None:
        from utils import load_config
        config = load_config()

    processor = PDFProcessor(config)
    return processor.process_paper(paper_metadata)


path: src/search.py
content: """Arxiv 论文搜索模块"""

import arxiv
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
import time


class ArxivSearcher:
    """Arxiv 论文搜索器"""

    def __init__(self, config: Dict[str, Any]):
        self.max_results = config.get('max_results', 10)
        self.time_window_hours = config.get('time_window_hours', 24)
        self.default_query = config.get('default_query', 'AI Agent')
        self.categories = config.get('search_categories', ['cs.AI', 'cs.CL', 'cs.LG'])
        self.request_delay = config.get('request_delay', 3)

    def search_papers(
        self,
        query: Optional[str] = None,
        max_results: Optional[int] = None,
        time_window_hours: Optional[int] = None,
        categories: Optional[List[str]] = None
    ) -> List[Dict[str, Any]]:
        """
        搜索 Arxiv 论文

        Args:
            query: 搜索关键词，默认使用配置中的 default_query
            max_results: 最大结果数，默认使用配置中的 max_results
            time_window_hours: 时间窗口（小时），默认使用配置中的 time_window_hours
            categories: 搜索分类，默认使用配置中的 categories

        Returns:
            论文元数据列表
        """
        query = query or self.default_query
        max_results = max_results or self.max_results
        time_window_hours = time_window_hours or self.time_window_hours
        categories = categories or self.categories

        # 构建搜索查询
        search_query = self._build_search_query(query, categories)

        # 计算时间范围
        start_time, end_time = self._get_time_range(time_window_hours)

        # 执行搜索
        papers = []
        try:
            search = arxiv.Search(
                query=search_query,
                max_results=max_results * 3,  # 获取更多结果以过滤时间
                sort_by=arxiv.SortCriterion.SubmittedDate,
                sort_order=arxiv.SortOrder.Descending
            )

            for result in search.results():
                # 检查时间范围
                if start_time <= result.published.replace(tzinfo=None) <= end_time:
                    paper_data = self._extract_metadata(result)
                    papers.append(paper_data)

                    if len(papers) >= max_results:
                        break

                # Arxiv API 已经有内置限流，不需要额外延迟

        except Exception as e:
            print(f"搜索过程中出错: {e}")
            raise

        return papers

    def _build_search_query(self, query: str, categories: List[str]) -> str:
        """构建搜索查询字符串"""
        # arxiv API 查询格式
        category_query = ' OR '.join([f'cat:{cat}' for cat in categories])
        return f'({query}) AND ({category_query})'

    def _get_time_range(self, hours: int) -> tuple[datetime, datetime]:
        """获取时间范围"""
        end_time = datetime.utcnow()  # 使用 UTC 时间以匹配 Arxiv API
        start_time = end_time - timedelta(hours=hours)
        return start_time, end_time

    def _extract_metadata(self, result: arxiv.Result) -> Dict[str, Any]:
        """提取论文元数据"""
        return {
            'title': result.title,
            'arxiv_id': result.entry_id.split('/')[-1],
            'authors': [author.name for author in result.authors],
            'published': result.published.strftime('%Y-%m-%d %H:%M:%S'),
            'updated': result.updated.strftime('%Y-%m-%d %H:%M:%S') if result.updated else None,
            'summary': result.summary.replace('\n', ' ').strip(),
            'categories': [cat for cat in result.categories],
            'pdf_url': result.pdf_url,
            'entry_id': result.entry_id,
            'primary_category': result.primary_category,
            'comment': result.comment,
            'journal_ref': result.journal_ref,
            'doi': result.doi
        }


def search_arxiv_papers(
    query: Optional[str] = None,
    max_results: Optional[int] = None,
    time_window_hours: Optional[int] = None,
    config: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
    """
    搜索 Arxiv 论文的便捷函数

    Args:
        query: 搜索关键词
        max_results: 最大结果数
        time_window_hours: 时间窗口（小时）
        config: 配置字典

    Returns:
        论文元数据列表
    """
    if config is None:
        from utils import load_config
        config = load_config()

    searcher = ArxivSearcher(config)
    return searcher.search_papers(query, max_results, time_window_hours)


path: src/utils.py
content: """工具函数模块"""

import json
import os
from datetime import datetime, timedelta
from typing import Dict, Any, List
from pathlib import Path


def get_config_dir() -> str:
    """获取用户配置目录"""
    config_dir = os.path.expanduser("~/.arxiv-search")
    os.makedirs(config_dir, exist_ok=True)
    return config_dir


def get_config_path() -> str:
    """获取用户配置文件路径"""
    return os.path.join(get_config_dir(), "config.json")


def get_default_config() -> Dict[str, Any]:
    """获取默认配置"""
    return {
        "max_results": 10,
        "time_window_hours": 24,
        "default_query": "AI Agent",
        "search_categories": ["cs.AI", "cs.CL", "cs.LG"],
        "request_delay": 3,
        "pdf_download_dir": "~/.arxiv-search/pdfs",
        "max_retries": 3,
        "language": "zh",
        "onboarding_complete": False
    }


def load_config() -> Dict[str, Any]:
    """加载配置文件，如果不存在则返回默认配置"""
    config_path = get_config_path()

    if os.path.exists(config_path):
        with open(config_path, 'r', encoding='utf-8') as f:
            config = json.load(f)
            # 合并默认配置中缺失的字段
            default_config = get_default_config()
            for key, value in default_config.items():
                if key not in config:
                    config[key] = value
            return config
    else:
        # 返回默认配置
        return get_default_config()


def save_config(config: Dict[str, Any]) -> None:
    """保存配置文件到用户目录"""
    config_path = get_config_path()
    config_dir = get_config_dir()
    os.makedirs(config_dir, exist_ok=True)

    with open(config_path, 'w', encoding='utf-8') as f:
        json.dump(config, f, indent=2, ensure_ascii=False)


def get_prompts_dir() -> str:
    """获取用户提示词目录"""
    prompts_dir = os.path.join(get_config_dir(), "prompts")
    os.makedirs(prompts_dir, exist_ok=True)
    return prompts_dir


def load_prompt(prompt_name: str) -> str:
    """
    加载提示词模板
    优先从用户目录加载，如果不存在则从项目目录加载默认模板
    """
    # 先尝试加载用户自定义提示词
    user_prompt_path = os.path.join(get_prompts_dir(), f"{prompt_name}.md")
    if os.path.exists(user_prompt_path):
        with open(user_prompt_path, 'r', encoding='utf-8') as f:
            return f.read()

    # 否则加载项目默认提示词
    default_prompt_path = os.path.join("prompts", f"{prompt_name}.md")
    if os.path.exists(default_prompt_path):
        with open(default_prompt_path, 'r', encoding='utf-8') as f:
            return f.read()

    raise FileNotFoundError(f"提示词模板 {prompt_name} 不存在")


def save_prompt(prompt_name: str, content: str) -> None:
    """保存提示词模板到用户目录"""
    prompts_dir = get_prompts_dir()
    prompt_path = os.path.join(prompts_dir, f"{prompt_name}.md")
    with open(prompt_path, 'w', encoding='utf-8') as f:
        f.write(content)


def get_time_range(hours: int = 24) -> tuple[datetime, datetime]:
    """获取时间范围"""
    end_time = datetime.now()
    start_time = end_time - timedelta(hours=hours)
    return start_time, end_time


def ensure_dir(dir_path: str) -> None:
    """确保目录存在"""
    Path(dir_path).mkdir(parents=True, exist_ok=True)


def format_paper_metadata(papers: List[Dict[str, Any]]) -> str:
    """格式化论文元数据为可读文本"""
    formatted = []
    for i, paper in enumerate(papers, 1):
        formatted.append(f"""
### 论文 {i}: {paper['title']}
- **Arxiv ID**: {paper['arxiv_id']}
- **作者**: {', '.join(paper['authors'][:3])}{'...' if len(paper['authors']) > 3 else ''}
- **发布日期**: {paper['published']}
- **分类**: {', '.join(paper['categories'])}
- **摘要**: {paper['summary'][:300]}...
""")
    return '\n'.join(formatted)


path: src/__init__.py
content: """Arxiv 论文处理 Agent Skill 模块"""

import sys
import io

__version__ = "2.0.1"


def setup_encoding():
    """
    配置标准输出编码为 UTF-8，解决 Windows 环境下的编码问题

    Windows 控制台默认使用 GBK 编码，无法正确显示 emoji 和其他 Unicode 字符。
    此函数强制使用 UTF-8 编码，确保跨平台兼容性。
    """
    if sys.platform == 'win32':
        try:
            # 重新包装 stdout 和 stderr，使用 UTF-8 编码
            if hasattr(sys.stdout, 'buffer'):
                sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
            if hasattr(sys.stderr, 'buffer'):
                sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
        except Exception:
            # 如果失败，保持默认编码（可能会在某些环境下丢失字符，但不会崩溃）
            pass


# 自动配置编码
setup_encoding()


path: cli.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Arxiv Paper Processing CLI Tool

A command-line tool that encapsulates all operations for the Arxiv paper skill.
All commands output JSON for easy parsing by agents.

Usage:
    python cli.py <command> [arguments]
"""

import sys
import json
import argparse
import os
import tempfile
import shutil
from typing import Dict, Any

# Set UTF-8 encoding for output on Windows
if sys.platform == 'win32':
    sys.stdout.reconfigure(encoding='utf-8')
    sys.stderr.reconfigure(encoding='utf-8')

# Add src to path
sys.path.insert(0, 'src')

from utils import (
    get_config_path, get_config_dir, load_config, save_config,
    get_default_config, get_prompts_dir, load_prompt, save_prompt
)


def output_json(data: Dict[str, Any]) -> None:
    """Output data as JSON to stdout"""
    print(json.dumps(data, ensure_ascii=False, indent=2))


def output_error(message: str, error_code: int = 1) -> None:
    """Output error as JSON and exit"""
    output_json({"error": message, "success": False})
    sys.exit(error_code)


# ============================================================================
# Onboarding Commands
# ============================================================================

def check_onboarding() -> None:
    """Check if onboarding is complete"""
    config_path = get_config_path()

    if not config_path:
        output_json({
            "complete": False,
            "reason": "Config path not found"
        })
        return

    try:
        config = load_config()
        complete = config.get('onboarding_complete', False)
        output_json({
            "complete": complete,
            "config_exists": True
        })
    except Exception as e:
        output_json({
            "complete": False,
            "reason": str(e)
        })


def check_dependencies() -> None:
    """Check if required dependencies are installed"""
    missing = []

    try:
        import arxiv
    except ImportError:
        missing.append("arxiv")

    try:
        import fitz
    except ImportError:
        missing.append("pymupdf")

    try:
        import requests
    except ImportError:
        missing.append("requests")

    output_json({
        "ok": len(missing) == 0,
        "missing": missing,
        "all_installed": ["arxiv", "pymupdf", "requests"]
    })


def complete_onboarding(language: str = "zh", time_window: int = 24,
                       query: str = "AI Agent") -> None:
    """Complete onboarding and save initial config"""
    config = get_default_config()
    config.update({
        "language": language,
        "time_window_hours": time_window,
        "default_query": query,
        "onboarding_complete": True
    })

    try:
        save_config(config)
        output_json({
            "success": True,
            "config": config
        })
    except Exception as e:
        output_error(f"Failed to save config: {e}")


# ============================================================================
# Config Management Commands
# ============================================================================

def get_config() -> None:
    """Get current configuration"""
    try:
        config = load_config()
        output_json({
            "success": True,
            "config": config
        })
    except Exception as e:
        output_error(f"Failed to load config: {e}")


def set_config(key: str, value: str) -> None:
    """Set a single configuration value"""
    try:
        config = load_config()

        # Try to parse value as JSON, fallback to string
        try:
            parsed_value = json.loads(value)
        except (json.JSONDecodeError, ValueError):
            parsed_value = value

        config[key] = parsed_value
        save_config(config)

        output_json({
            "success": True,
            "key": key,
            "value": parsed_value
        })
    except Exception as e:
        output_error(f"Failed to set config: {e}")


def reset_config() -> None:
    """Reset configuration to defaults"""
    try:
        config = get_default_config()
        # Don't reset onboarding_complete if it was True
        old_config = load_config()
        if old_config.get('onboarding_complete', False):
            config['onboarding_complete'] = True

        save_config(config)

        output_json({
            "success": True,
            "config": config
        })
    except Exception as e:
        output_error(f"Failed to reset config: {e}")


# ============================================================================
# Search and Processing Commands
# ============================================================================

def search_papers(query: str = None, max_results: int = None,
                 time_window: int = None) -> None:
    """Search arxiv papers using current config"""
    try:
        from search import search_arxiv_papers

        # Load config for defaults
        config = load_config()

        papers = search_arxiv_papers(
            query=query or config.get('default_query'),
            max_results=max_results or config.get('max_results'),
            time_window_hours=time_window or config.get('time_window_hours'),
            config=config
        )

        output_json({
            "success": True,
            "papers": papers,
            "count": len(papers),
            "query_used": query or config.get('default_query')
        })
    except Exception as e:
        output_error(f"Failed to search papers: {e}")


def process_pdf(arxiv_id: str) -> None:
    """Download and process PDF for a paper"""
    try:
        from search import search_arxiv_papers
        from pdf_processor import process_pdf

        # First find the paper
        papers = search_arxiv_papers(query=f"id:{arxiv_id}", max_results=1)

        if not papers:
            output_error(f"Paper {arxiv_id} not found")

        paper = papers[0]
        result = process_pdf(paper)

        output_json({
            "success": True,
            "paper": result,
            "arxiv_id": arxiv_id
        })
    except Exception as e:
        output_error(f"Failed to process PDF: {e}")


def prepare_daily() -> None:
    """
    Prepare daily summary - combines search and prompt preparation.
    This is a single-command replacement for the multi-step process.

    Saves the JSON result to a temporary file in the project directory
    and returns the file path for stable reading by agents.
    """
    try:
        from search import search_arxiv_papers
        from analyzer import prepare_summary

        # Load config
        config = load_config()

        # Search papers
        papers = search_arxiv_papers(
            query=config.get('default_query'),
            max_results=config.get('max_results'),
            time_window_hours=config.get('time_window_hours'),
            config=config
        )

        # Prepare summary context
        result = prepare_summary(papers, config)

        # Build the data
        data = {
            "success": True,
            "papers": papers,
            "paper_count": len(papers),
            "prompt": result.get('prompt', ''),
            "instruction": result.get('instruction', ''),
            "config": {
                'language': config.get('language'),
                'time_window_hours': config.get('time_window_hours'),
                'default_query': config.get('default_query'),
                'max_results': config.get('max_results'),
                'search_categories': config.get('search_categories', [])
            }
        }

        # Create temp directory in project root
        temp_dir = os.path.join(os.getcwd(), 'temp')
        os.makedirs(temp_dir, exist_ok=True)

        # Generate unique filename
        import time
        timestamp = int(time.time())
        filename = f"daily_data_{timestamp}.json"
        filepath = os.path.join(temp_dir, filename)

        # Save to file
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

        # Return file path
        output_json({
            "success": True,
            "file_path": filepath,
            "paper_count": len(papers),
            "message": "Data saved to file. Use Read tool to read the file."
        })
    except Exception as e:
        output_error(f"Failed to prepare daily summary: {e}")


def prepare_analysis(arxiv_id: str = None, analysis_type: str = "deep") -> None:
    """
    Prepare paper analysis - combines PDF download and prompt preparation.
    This is a single-command replacement for the multi-step process.

    Saves the JSON result to a temporary file in the project directory
    and returns the file path for stable reading by agents.
    """
    try:
        from search import search_arxiv_papers
        from pdf_processor import process_pdf
        from analyzer import prepare_analysis

        # Load config
        config = load_config()

        # Find the paper
        papers = search_arxiv_papers(query=f"id:{arxiv_id}", max_results=1)

        if not papers:
            output_error(f"Paper {arxiv_id} not found")

        paper = papers[0]

        # Process PDF
        paper_with_content = process_pdf(paper)

        # Prepare analysis context
        result = prepare_analysis(paper_with_content, analysis_type, config)

        # Build the data
        data = {
            "success": True,
            "paper": paper_with_content,
            "prompt": result.get('prompt', ''),
            "instruction": result.get('instruction', ''),
            "config": {
                'language': config.get('language'),
                'analysis_type': analysis_type
            }
        }

        # Create temp directory in project root
        temp_dir = os.path.join(os.getcwd(), 'temp')
        os.makedirs(temp_dir, exist_ok=True)

        # Generate unique filename
        import time
        timestamp = int(time.time())
        # Sanitize arxiv_id for filename
        safe_arxiv_id = arxiv_id.replace('/', '_').replace('.', '_')
        filename = f"analysis_data_{safe_arxiv_id}_{timestamp}.json"
        filepath = os.path.join(temp_dir, filename)

        # Save to file
        with open(filepath, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

        # Return file path
        output_json({
            "success": True,
            "file_path": filepath,
            "arxiv_id": arxiv_id,
            "message": "Data saved to file. Use Read tool to read the file."
        })
    except Exception as e:
        output_error(f"Failed to prepare analysis: {e}")


# ============================================================================
# Prompt Management Commands
# ============================================================================

def get_prompt(name: str) -> None:
    """Get prompt template content"""
    try:
        content = load_prompt(name)
        output_json({
            "success": True,
            "name": name,
            "content": content
        })
    except FileNotFoundError:
        output_error(f"Prompt '{name}' not found")
    except Exception as e:
        output_error(f"Failed to get prompt: {e}")


def set_prompt(name: str, content: str = None) -> None:
    """Set prompt template content. If content is None, read from stdin"""
    try:
        if content is None:
            # Read from stdin
            content = sys.stdin.read()

        save_prompt(name, content)

        output_json({
            "success": True,
            "name": name,
            "length": len(content)
        })
    except Exception as e:
        output_error(f"Failed to set prompt: {e}")


def reset_prompt(name: str) -> None:
    """Reset prompt to default (delete user custom version)"""
    import os
    import glob

    try:
        user_prompts_dir = get_prompts_dir()
        user_prompt_path = os.path.join(user_prompts_dir, f"{name}.md")

        if os.path.exists(user_prompt_path):
            os.remove(user_prompt_path)

        # Verify it's gone and default exists
        default_prompt_path = os.path.join("prompts", f"{name}.md")
        if not os.path.exists(default_prompt_path):
            output_error(f"Default prompt '{name}' not found")

        output_json({
            "success": True,
            "name": name,
            "reset": True
        })
    except Exception as e:
        output_error(f"Failed to reset prompt: {e}")


def list_prompts() -> None:
    """List all available prompts"""
    import os

    try:
        default_dir = "prompts"
        user_dir = get_prompts_dir()

        available = []
        if os.path.exists(default_dir):
            for file in os.listdir(default_dir):
                if file.endswith('.md'):
                    name = file[:-3]
                    has_custom = os.path.exists(os.path.join(user_dir, file))
                    available.append({
                        "name": name,
                        "has_custom": has_custom
                    })

        output_json({
            "success": True,
            "prompts": available,
            "count": len(available)
        })
    except Exception as e:
        output_error(f"Failed to list prompts: {e}")


# ============================================================================
# Main Entry Point
# ============================================================================

def main():
    parser = argparse.ArgumentParser(
        description="Arxiv Paper Processing CLI Tool",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )

    subparsers = parser.add_subparsers(dest='command', help='Available commands')

    # Onboarding commands
    subparsers.add_parser('check-onboarding', help='Check if onboarding is complete')
    subparsers.add_parser('check-dependencies', help='Check if dependencies are installed')

    complete_parser = subparsers.add_parser('complete-onboarding', help='Complete onboarding')
    complete_parser.add_argument('--language', default='zh', help='Language preference')
    complete_parser.add_argument('--time-window', type=int, default=24, help='Time window in hours')
    complete_parser.add_argument('--query', default='AI Agent', help='Default search query')

    # Config commands
    subparsers.add_parser('get-config', help='Get current configuration')

    set_config_parser = subparsers.add_parser('set-config', help='Set a config value')
    set_config_parser.add_argument('key', help='Config key')
    set_config_parser.add_argument('value', help='Config value (JSON or string)')

    subparsers.add_parser('reset-config', help='Reset configuration to defaults')

    # Search commands
    search_parser = subparsers.add_parser('search', help='Search arxiv papers')
    search_parser.add_argument('--query', help='Search query')
    search_parser.add_argument('--max-results', type=int, help='Max results')
    search_parser.add_argument('--time-window', type=int, help='Time window in hours')

    process_pdf_parser = subparsers.add_parser('process-pdf', help='Download and process PDF')
    process_pdf_parser.add_argument('arxiv_id', help='Arxiv ID of the paper')

    subparsers.add_parser('prepare-daily', help='Prepare daily summary (search + prompt)')

    prepare_analysis_parser = subparsers.add_parser('prepare-analysis', help='Prepare paper analysis')
    prepare_analysis_parser.add_argument('arxiv_id', help='Arxiv ID of the paper')
    prepare_analysis_parser.add_argument('--type', default='deep', help='Analysis type')

    # Prompt commands
    get_prompt_parser = subparsers.add_parser('get-prompt', help='Get prompt content')
    get_prompt_parser.add_argument('name', help='Prompt name')

    set_prompt_parser = subparsers.add_parser('set-prompt', help='Set prompt content')
    set_prompt_parser.add_argument('name', help='Prompt name')
    set_prompt_parser.add_argument('--content', help='Prompt content (optional, reads from stdin if not provided)')

    reset_prompt_parser = subparsers.add_parser('reset-prompt', help='Reset prompt to default')
    reset_prompt_parser.add_argument('name', help='Prompt name')

    subparsers.add_parser('list-prompts', help='List all available prompts')

    args = parser.parse_args()

    if not args.command:
        parser.print_help()
        sys.exit(1)

    # Route to command
    commands = {
        'check-onboarding': check_onboarding,
        'check-dependencies': check_dependencies,
        'complete-onboarding': lambda: complete_onboarding(args.language, args.time_window, args.query),
        'get-config': get_config,
        'set-config': lambda: set_config(args.key, args.value),
        'reset-config': reset_config,
        'search': lambda: search_papers(args.query, args.max_results, args.time_window),
        'process-pdf': lambda: process_pdf(args.arxiv_id),
        'prepare-daily': prepare_daily,
        'prepare-analysis': lambda: prepare_analysis(args.arxiv_id, args.type),
        'get-prompt': lambda: get_prompt(args.name),
        'set-prompt': lambda: set_prompt(args.name, getattr(args, 'content', None)),
        'reset-prompt': lambda: reset_prompt(args.name),
        'list-prompts': list_prompts,
    }

    if args.command in commands:
        commands[args.command]()
    else:
        output_error(f"Unknown command: {args.command}")


if __name__ == '__main__':
    main()


path: README.md
content: # Arxiv 论文搜索与分析工具

一个专业的 Arxiv 论文处理助手，具备论文搜索、内容提取和深度分析的能力。

## ✨ 功能特性

- 📚 **每日论文日报** —— 追踪指定领域的最新研究动态
- 🔍 **论文搜索** —— 支持关键词、时间窗口、分类等多维度搜索
- 📄 **PDF 处理** —— 自动下载 PDF 并提取文本，支持 LaTeX 公式保留
- 🤖 **深度分析** —— 生成全面的论文分析报告
- ⚙️ **灵活配置** —— 通过对话动态调整所有设置
- 🌐 **多语言支持** —— 支持中文、英文、双语报告

## 📦 安装依赖

```bash
pip install arxiv pymupdf requests
```

## 🚀 快速开始

### 1. 在 Claude Code 中使用

在 Claude Code 中调用此 skill：

```
/follow-arxiv-paper
```

首次运行时，会自动启动 onboarding 流程，引导你完成：
- 语言设置（中文/英文/双语）
- 时间窗口配置（24h/48h/72h/1周）
- 搜索关键词设置
- 搜索分类配置

### 2. 配置文件位置

所有配置和用户自定义文件都存储在用户主目录：

```
~/.arxiv-search/
├── config.json          # 用户配置
├── prompts/             # 用户自定义提示词（可选）
└── pdfs/                # PDF 缓存目录
```

### 3. 常用命令示例

**生成每日论文日报**：
```
帮我生成今天的 AI Agent 论文日报
```

**深度分析特定论文**：
```
分析论文 2401.12345
```

**调整配置**：
```
调整时间窗口为 48 小时
更改搜索关键词为 Large Language Model
添加分类 cs.CV
显示我的当前设置
```

**自定义报告风格**：
```
让日报更简洁一点
重点关注方法论
论文分析要更深入
```

## 📁 项目结构

```
arxiv-search/
├── SKILL.md                    # Skill 定义文件（Agent 工作流程）
├── config/
│   └── default_config.json     # 默认配置模板
├── prompts/
│   ├── daily_summary.md        # 日报提示词模板
│   └── deep_analysis.md        # 深度分析提示词模板
├── src/
│   ├── __init__.py
│   ├── utils.py                # 工具函数（配置、提示词管理）
│   ├── search.py               # 论文搜索模块
│   ├── pdf_processor.py        # PDF 处理模块
│   └── analyzer.py             # 分析模块
└── requirements.txt            # Python 依赖
```

## 🔧 配置说明

### 配置文件格式

配置文件位于 `~/.arxiv-search/config.json`：

```json
{
  "language": "zh",              // 语言设置: "zh", "en", "bilingual"
  "time_window_hours": 24,       // 时间窗口（小时）
  "default_query": "AI Agent",   // 默认搜索关键词
  "search_categories": [         // 搜索分类
    "cs.AI",
    "cs.CL",
    "cs.LG"
  ],
  "max_results": 10,             // 最大结果数
  "request_delay": 3,            // 请求延迟（秒）
  "pdf_download_dir": "~/.arxiv-search/pdfs",  // PDF 缓存目录
  "max_retries": 3,              // 下载重试次数
  "onboarding_complete": true    // Onboarding 是否完成
}
```

### 常见 Arxiv 分类

- `cs.AI` —— 人工智能
- `cs.CL` —— 计算与语言（NLP）
- `cs.LG` —— 机器学习
- `cs.CV` —— 计算机视觉
- `cs.RO` —— 机器人学
- `cs.CR` —— 密码学与安全
- `cs.SE` —— 软件工程

## 📝 使用场景

### 场景 1: 追踪最新研究

每天运行生成日报，了解特定领域的最新进展：

```
生成今天的 AI Agent 论文日报
```

### 场景 2: 深度理解论文

找到感兴趣的论文后，进行深度分析：

```
分析第 3 篇论文
```

或直接使用 Arxiv ID：

```
分析论文 2401.12345
```

### 场景 3: 调整搜索策略

根据日报质量调整配置：

```
今天只找到 2 篇论文，调整时间窗口为 48 小时
我想看更多论文，每次最多返回 20 篇
添加计算机视觉相关的分类
```

### 场景 4: 自定义报告风格

调整提示词以生成更符合需求的报告：

```
让日报更简洁一点
论文分析要更深入
重点关注方法论
```

## 🛠️ 核心模块

### 1. 论文搜索 (`src/search.py`)

```python
from src.search import search_arxiv_papers

papers = search_arxiv_papers(
    query="AI Agent",
    max_results=10,
    time_window_hours=24
)
```

### 2. PDF 处理 (`src/pdf_processor.py`)

```python
from src.pdf_processor import process_pdf

paper_with_content = process_pdf(papers[0])
# 返回包含元数据和提取文本的字典
```

### 3. 分析准备 (`src/analyzer.py`)

```python
from src.analyzer import prepare_summary, prepare_analysis

# 准备日报
result = prepare_summary(papers)
# result['prompt'] 包含填充好的提示词

# 准备深度分析
result = prepare_analysis(paper_with_content)
```

### 4. 配置管理 (`src/utils.py`)

```python
from src.utils import load_config, save_config, load_prompt, save_prompt

# 配置管理
config = load_config()
config['time_window_hours'] = 48
save_config(config)

# 提示词管理
prompt = load_prompt('daily_summary')
save_prompt('daily_summary', modified_prompt)
```

## 🎯 最佳实践

1. **首次使用**：完整运行 onboarding 流程，确保配置符合需求
2. **定期调整**：根据日报质量动态调整搜索参数
3. **提示词定制**：根据个人偏好自定义报告风格
4. **缓存利用**：PDF 会缓存在本地，避免重复下载
5. **分类细化**：根据研究领域选择合适的 arxiv 分类

## 📊 示例输出

### 日报示例

```markdown
# 📚 Arxiv 论文日报 - 2024-01-15

## 📊 概览
- 论文数量：8 篇
- 时间范围：2024-01-14 至 2024-01-15
- 主要领域：AI Agent, Large Language Models

## 🔥 重点推荐

### Tool Learning for Large Language Models
- **Arxiv ID**: 2401.12345
- **一句话总结**: 提出了一种新的工具学习框架，使 LLM 能够更有效地使用外部工具
- **创新点**: 引入了工具选择和执行的多阶段学习策略
- **推荐理由**: 对 Agent 工具使用有重要启发

...

## 💡 研究趋势观察
本次论文主要集中在 Agent 的工具学习和推理能力提升上...
```

### 深度分析示例

```markdown
# 📄 论文深度分析报告

## 基本信息
- **标题**: Tool Learning for Large Language Models
- **作者**: Zhang et al.
- **Arxiv ID**: 2401.12345
- **推荐指数**: ⭐⭐⭐⭐☆ (4/5)

## 一、研究背景与动机
### 1.1 问题陈述
LLM 在处理需要外部知识和计算的任务时存在局限性...

## 二、核心贡献
1. **贡献点 1**: 提出了工具学习的统一框架
2. **贡献点 2**: 设计了工具选择的多阶段学习策略
...
```

## 🔍 常见问题

### Q: 搜索不到论文怎么办？
A: 尝试以下方法：
- 扩大时间窗口（如从 24h 调整为 48h）
- 简化搜索关键词
- 添加更多相关分类

### Q: PDF 下载失败怎么办？
A: 可能原因：
- Arxiv 服务暂时不可用 —— 稍后重试
- 论文暂无 PDF 版本 —— 基于摘要分析

### Q: 如何重置所有设置？
A: 使用命令：
```
重置为默认配置
```

或手动删除 `~/.arxiv-search/config.json`

### Q: 提示词自定义后想恢复默认？
A: 删除 `~/.arxiv-search/prompts/` 目录下的文件即可

## 📄 License

MIT

## 🤝 贡献

欢迎提交 Issue 和 Pull Request！

---

**Happy Research! 🎓**


path: requirements.txt
content: arxiv>=2.1.0
pymupdf>=1.23.0
requests>=2.31.0
python-dateutil>=2.8.2


path: config/default_config.json
content: {
  "max_results": 10,
  "time_window_hours": 24,
  "default_query": "AI Agent",
  "search_categories": ["cs.AI", "cs.CL", "cs.LG"],
  "request_delay": 3,
  "pdf_download_dir": "~/.arxiv-search/pdfs",
  "max_retries": 3,
  "language": "zh",
  "onboarding_complete": false
}


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