Skill: xiaopengs/ai-sentinel-2 (version 0.1.0)

--- slug: ai-sentinel name: AI前沿哨兵 version: 1.0.0 description: AI情报哨兵 - 自动采集、分析与报告AI领域最新动态的多源情报系统 trigger: "AI情报|AI哨兵|AI资讯|AI动态|科技新闻|论文追踪|开源项目追踪|晨报|晚报" tools: [shell, filesystem, http] author: xiaopengs --- # AI前沿哨兵 (AI Sentinel) > 你的AI情报指挥中心，自动采集、分析与报告AI领域最新动态 ## 技能概述 AI前沿哨兵是一个开源的AI情报采集系统，帮助用户自动追踪AI领域的最新动态，包括开源项目、学术论文、技术博客、行业新闻等，并生成结构化的晨报和晚报。 **核心能力：** - 🔍 多源信息采集（GitHub、arXiv、HackerNews、Twitter/X、RSS等） - ⏰ 智能定时调度（晨报08:00、晚报20:00） - 📊 智能分析评分（热度+新鲜度+来源可信度） - 📄 自动化报告生成（Markdown格式） - 🎛️ WebUI管理面板 ## 适用场景 - AI从业者追踪最新技术动态 - 研究人员关注前沿论文 - 开发者发现优质开源项目 - 产品经理了解行业趋势 - 内容创作者获取素材 ## 使用方法 ### 1. 基本采集 当用户说"采集AI情报"、"生成AI晨报"、"追踪AI动态"时，执行以下步骤： ```bash # 采集所有信息源 python scripts/collect.py --all # 生成晨报 python scripts/reporter.py --type morning # 生成晚报 python scripts/reporter.py --type evening ``` ### 2. 单源采集 ```bash # 仅采集GitHub Trending python scripts/collect.py --source github # 仅采集arXiv论文 python scripts/collect.py --source arxiv # 仅采集HackerNews python scripts/collect.py --source hackernews ``` ### 3. 查看报告 生成的报告保存在 `reports/YYYY-MM-DD/` 目录： - `morning_report.md` - 晨报 - `evening_report.md` - 晚报 - `xiaohongshu_report.md` - 小红书风格报告 ## 配置说明 ### 信息源配置 (`config/sources.yaml`) 系统支持以下信息源： 1. **GitHub Trending** - AI开源项目热度排行 2. **arXiv** - 最新AI论文（cs.AI, cs.LG, cs.CL） 3. **HackerNews** - 开发者社区热点讨论 4. **Twitter/X** - AI大咖动态（需API Token） 5. **RSS博客** - 官方博客和技术媒体 - Anthropic, OpenAI, Google AI, DeepMind - Hugging Face, xAI, Cursor - TechCrunch AI, VentureBeat AI - InfoQ全球/中文 - MMChat AI资讯 6. **中国AI公司** - 官网新闻解析 - 智谱AI、MiniMax、扣子Coze ### 调度配置 (`config/schedule.yaml`) ```yaml schedule: morning: enabled: true time: "08:00" evening: enabled: true time: "20:00" ``` ## 工作流程 1. **采集阶段** - 从配置的信息源拉取最新数据 2. **分析阶段** - 计算热度、新鲜度、质量评分 3. **筛选阶段** - 按优先级排序，过滤低质量内容 4. **生成阶段** - 使用Jinja2模板生成Markdown报告 5. **存档阶段** - 按日期保存到 `reports/` 目录 ## 项目结构 ``` ai-sentinel/ ├── SKILL.md # 本文档 ├── config/ # 配置文件 │ ├── sources.yaml # 信息源配置 │ ├── settings.yaml # API配置 │ └── schedule.yaml # 调度配置 ├── scripts/ # 核心脚本 │ ├── collect.py # 采集入口 │ ├── analyzer.py # 分析评分 │ ├── reporter.py # 报告生成 │ ├── quality_scorer.py # 质量评分系统 │ └── parsers/ # 平台解析器 │ ├── github_trending.py │ ├── arxiv.py │ ├── hackernews.py │ ├── blog_rss.py │ └── web_news.py ├── templates/ # 报告模板 │ ├── morning_report.md │ ├── evening_report.md │ └── xiaohongshu_report.md ├── reports/ # 报告存档 │ └── YYYY-MM-DD/ │ ├── morning_report.md │ ├── evening_report.md │ └── xiaohongshu_report.md ├── webui/ # Web界面 │ ├── index.html │ ├── style.css │ └── app.js └── references/ # 参考文档 ├── sources_guide.md └── api_setup.md ``` ## 安装依赖 ```bash pip install requests feedparser pyyaml jinja2 schedule ``` ## 扩展指南 ### 添加新信息源 1. 在 `scripts/parsers/` 创建新解析器 2. 实现 `fetch()` 方法返回标准格式数据 3. 在 `config/sources.yaml` 添加配置 4. 在 `scripts/collect.py` 注册解析器 ### 自定义报告模板 编辑 `templates/` 目录下的模板文件，使用Jinja2语法： - `{{ title }}` - 标题 - `{{ items }}` - 条目列表 - `{{ analysis }}` - 分析内容 ## 常见问题 **Q: Twitter采集失败？** A: 需要Twitter Developer账号和Bearer Token，未配置时自动跳过。 **Q: 如何添加自定义关键词？** A: 编辑 `config/sources.yaml` 中各源的 `keywords` 字段。 **Q: 报告保存位置？** A: 默认 `reports/YYYY-MM-DD/`，可在配置中修改。 ## 仓库地址 https://github.com/xiaopengs/ai-sentinel ## License MIT License - 详见 LICENSE 文件

path: README.md
content: # 🔭 AI前沿哨兵 (AI Sentinel)

> 你的AI情报指挥中心，自动采集、分析与报告AI领域最新动态

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Python](https://img.shields.io/badge/Python-3.9+-blue.svg)](https://www.python.org/)
[![Stars](https://img.shields.io/github/stars/xiaopengs/ai-sentinel?style=social)](https://github.com/xiaopengs/ai-sentinel)

---

## 🚀 一分钟快速开始

```bash
# 1. 克隆项目
git clone https://github.com/xiaopengs/ai-sentinel.git
cd ai-sentinel

# 2. 安装依赖
pip install requests feedparser pyyaml jinja2

# 3. 创建输出目录
mkdir -p output

# 4. 运行采集（采集所有信息源）
python scripts/collect.py --all

# 5. 生成报告
python scripts/reporter.py --type morning
```

> 💡 **提示**：使用 WebUI 可以更直观地管理信息源和查看报告

```bash
cd webui && python -m http.server 8080
# 然后浏览器打开 http://localhost:8080
```

---

## 👤 适合谁用？

| 人群 | 使用场景 |
|------|----------|
| **AI 研究者** | 追踪最新论文和开源项目，不错过重要研究进展 |
| **开发者** | 发现热门AI项目和最佳实践，紧跟技术趋势 |
| **投资人** | 了解AI行业最新动态，发现潜在投资机会 |
| **产品经理** | 关注AI产品更新和行业新闻，启发产品灵感 |
| **技术爱好者** | 保持对AI领域的了解，学习新技术 |

---

## ✨ 功能特性

### 🔍 多源信息采集

| 信息源 | 说明 | 内置 |
|--------|------|------|
| **GitHub Trending** | 追踪AI开源项目热度，发现新兴项目 | ✅ |
| **arXiv 论文** | 获取最新学术研究成果 | ✅ |
| **HackerNews** | 捕捉开发者社区热点讨论 | ✅ |
| **Twitter/X** | 追踪AI大咖和机构动态 (需配置API) | ✅ |
| **RSS 订阅** | 支持任意博客/新闻源 | ✅ |
| **官方博客** | Anthropic、OpenAI、Google AI、DeepMind等 | ✅ (快速模板) |

### ⏰ 智能定时调度

- **晨报** - 每天早8点推送，隔夜重要动态
- **晚报** - 每天晚8点推送，全天热点汇总
- **即时采集** - 手动触发，按需获取

### 📊 智能分析评分

- **热度评分** - 基于star、fork、评论数
- **新鲜度权重** - 越新权重越高
- **来源可信度** - 不同来源不同权重
- **关键词匹配** - 自定义关注领域

### 📄 自动化报告

- 结构化 Markdown 格式
- 分类清晰：论文、项目、讨论、新闻
- 摘要+链接，可直接跳转阅读

### 🎛️ WebUI 管理面板

- 信息源配置与管理
- 实时采集状态监控
- 报告预览与导出
- 设置调整即时生效

---

## 📖 快速开始

### 环境要求

- Python 3.9+
- 网络连接

### 安装

```bash
# 克隆项目
git clone https://github.com/xiaopengs/ai-sentinel.git
cd ai-sentinel

# 安装依赖
pip install requests feedparser pyyaml jinja2

# 创建输出目录
mkdir -p output
```

### 基本使用

#### 命令行采集

```bash
# 采集所有信息源
python scripts/collect.py --all

# 仅采集 GitHub
python scripts/collect.py --source github

# 仅采集 arXiv 论文
python scripts/collect.py --source arxiv

# 查看采集状态
python scripts/collect.py --status
```

#### 生成报告

```bash
# 生成晨报
python scripts/reporter.py --type morning

# 生成晚报
python scripts/reporter.py --type evening
```

#### 启动 WebUI

```bash
cd webui && python -m http.server 8080
```

然后在浏览器打开 `http://localhost:8080`

#### 定时任务 (Linux/Mac)

```bash
# 编辑 crontab
crontab -e

# 添加以下内容
0 8 * * * cd /path/to/ai-sentinel && python scripts/collect.py --all && python scripts/reporter.py --type morning
0 20 * * * cd /path/to/ai-sentinel && python scripts/collect.py --all && python scripts/reporter.py --type evening
```

---

## 🔗 如何添加信息源

### 使用 WebUI 添加（推荐）

1. 打开 WebUI，点击左侧菜单的 **「信息源」**
2. 点击右上角的 **「添加信息源」** 按钮
3. 你可以使用两种方式添加：

#### 方式一：快速添加（一键添加常用博客）

在弹窗中的「快速添加」区域，我们提供了常用的AI博客模板：

| 博客 | 说明 |
|------|------|
| OpenAI 官方博客 | GPT模型更新、研究论文 |
| Anthropic 官方博客 | Claude、 Constitutional AI |
| Google AI Blog | Google在AI领域的研究 |
| DeepMind 研究 | 前沿AI研究成果 |
| Hugging Face Blog | 开源LLM和模型库动态 |
| Machine Learning Mastery | 实用的机器学习教程 |

点击「添加」按钮即可一键订阅！

#### 方式二：手动添加自定义源

如果想添加其他博客，需要获取该博客的 **RSS 订阅地址**：

1. 打开目标博客网站
2. 找页面底部的 **RSS** 或 **Feed** 图标
3. 点击后复制浏览器地址栏的URL
4. 回到 WebUI，填写：
   - **名称**：博客的名字（如「机器之心」）
   - **RSS订阅地址**：刚才复制的URL
   - **描述**：简短描述（可选）

### 命令行添加 RSS 源

编辑 `config/sources.yaml`：

```yaml
custom_rss:
  - name: "机器之心"
    url: "https://feed.jiqizhixin.com/rss"
    enabled: true
  - name: "量子位"
    url: "https://www.qbitai.com/feed"
    enabled: true
```

### 常见博客 RSS 地址

| 博客 | RSS 地址 |
|------|----------|
| 机器之心 | `https://feed.jiqizhixin.com/rss` |
| 量子位 | `https://www.qbitai.com/feed` |
| 知乎专栏-AI | `https://zhuanlan.zhihu.com/feed/` |
| Medium-AI | `https://medium.com/feed/tag/artificial-intelligence` |
| OpenAI Blog | `https://openai.com/blog/rss.xml` |
| Anthropic Blog | `https://www.anthropic.com/news/rss` |

---

## ⚙️ 信息源配置说明

### GitHub

```yaml
github:
  enabled: true
  language: python        # 编程语言
  date_range: weekly      # daily/weekly/monthly
  limit: 20
  keywords:              # 可选关键词过滤
    - AI
    - machine-learning
    - LLM
```

### arXiv

```yaml
arxiv:
  enabled: true
  categories:            # 论文分类
    - cs.AI              # Artificial Intelligence
    - cs.LG              # Machine Learning
    - cs.CL              # Computation and Language
  max_results: 20
```

### HackerNews

```yaml
hackernews:
  enabled: true
  item_type: top         # top/new/best/ask/show
  limit: 20
  keywords:              # AI相关关键词过滤
    - AI
    - machine learning
    - LLM
```

### Twitter/X

```yaml
twitter:
  enabled: true
  bearer_token: "your-twitter-bearer-token"  # 必需
  keywords:              # 关注关键词
    - AI
    - GPT
    - LLM
```

**获取 Bearer Token 步骤：**

1. 访问 [Twitter Developer Portal](https://developer.twitter.com)
2. 注册开发者账号（需要审核，通常1-2天）
3. 创建项目和应用
4. 在应用设置中生成 Bearer Token
5. 复制 Token 到 `config/settings.yaml` 或 WebUI 配置页

**推荐的 AI 大咖账号：**
- `@sama` - Sam Altman (OpenAI CEO)
- `@ylecun` - Yann LeCun (Meta AI 首席科学家)
- `@karpathy` - Andrej Karpathy (前OpenAI)
- `@AndrewYNg` - Andrew Ng (DeepLearning.AI)

### RSS 自定义源

```yaml
rss:
  enabled: true
  sources:
    - name: "OpenAI Blog"
      url: "https://openai.com/blog/rss.xml"
      update_interval: 3600    # 更新间隔（秒）
```

---

## 📁 项目结构

```
ai-sentinel/
├── config/                 # 配置文件目录
│   ├── sources.yaml        # 信息源配置
│   ├── settings.yaml       # 系统设置
│   └── schedule.yaml       # 定时任务配置
├── scripts/                # 脚本目录
│   ├── collect.py          # 主采集脚本
│   ├── reporter.py         # 报告生成脚本
│   └── parsers/            # 各信息源解析器
│       ├── github_trending.py
│       ├── arxiv.py
│       ├── hackernews.py
│       ├── twitter_x.py
│       └── blog_rss.py
├── templates/              # 报告模板
├── webui/                  # Web 管理界面
│   ├── index.html         # 主页面
│   ├── app.js             # 前端逻辑
│   └── style.css          # 样式文件
├── output/                 # 采集结果输出目录
├── SKILL.md                # 详细技术文档
├── LICENSE                 # MIT License
└── README.md               # 本文档
```

---

## ❓ 常见问题 (FAQ)

### Q: 如何添加 Twitter 信息源？

1. 访问 [Twitter Developer Portal](https://developer.twitter.com)
2. 创建开发者账号（需要审核，通常1-2天）
3. 创建 App 并生成 Bearer Token
4. 在 WebUI 的「配置」页面或 `config/settings.yaml` 填入 Token

> ⚠️ 需要Twitter开发者账号，审核通过后即可使用

### Q: 如何获取 Twitter Bearer Token？

1. 访问 [Twitter Developer Portal](https://developer.twitter.com)
2. 创建一个开发者账号（需要审核）
3. 创建 App 并申请 Bearer Token
4. 在 WebUI 的「配置」页面填入 Token

> ⚠️ 注意：Twitter API 有访问限制和配额，请合理使用

### Q: RSS 订阅不生效怎么办？

1. 确认 RSS 地址正确（可以在浏览器直接访问测试）
2. 有些网站使用 Atom 格式，尝试 `/atom.xml` 或 `/feed/atom`
3. 检查是否需要认证

### Q: 采集的数据在哪里？

- **原始数据**：`output/raw_data_YYYYMMDD_HHMMSS.json`
- **处理后数据**：`output/processed_data.json`
- **报告文件**：`output/reports/morning_YYYYMMDD.md` 或 `evening_`

### Q: 如何自定义报告模板？

编辑 `templates/report_template.md`，使用 Jinja2 语法。

### Q: 采集频率怎么调整？

修改定时任务或使用 crontab：

```bash
# 每6小时采集一次
0 */6 * * * cd /path/to/ai-sentinel && python scripts/collect.py --all
```

### Q: 支持哪些 RSS 格式？

支持标准的 RSS 2.0 和 Atom 格式。

---

## 📝 示例输出

采集结果示例 (`output/raw_data_*.json`)：

```json
{
  "timestamp": "2024-01-15T10:30:00",
  "sources": {
    "github": {
      "status": "success",
      "count": 15,
      "items": [
        {
          "title": "user/repo-name",
          "description": "项目描述",
          "url": "https://github.com/user/repo-name",
          "stars": 12500,
          "forks": 1200,
          "language": "Python",
          "source": "github"
        }
      ]
    }
  }
}
```

---

## 🤝 贡献

欢迎提交 Issue 和 Pull Request！

## 📄 License

本项目采用 [MIT License](LICENSE) 开源。

---

⭐ **如果这个项目对你有帮助，请给它一个星！**


path: config/schedule.yaml
content: # AI前沿哨兵 - 调度配置
# 配置定时采集任务

schedule:
  # 晨报设置
  morning:
    enabled: true
    time: "08:00"  # 24小时制
    # 选择包含的信息源
    include_sources:
      - github
      - arxiv
      - hackernews
    # 生成报告
    generate_report: true
    report_type: "morning"
    # 通知设置
    notify: false
    
  # 晚报设置
  evening:
    enabled: true
    time: "20:00"  # 24小时制
    # 晚报包含所有信息源
    include_sources:
      - github
      - arxiv
      - hackernews
      - twitter
      - blogs
    generate_report: true
    report_type: "evening"
    notify: false

  # 自定义定时任务
  custom:
    - name: "每周AI论文精选"
      enabled: false
      cron: "0 9 * * 1"  # 每周一早上9点
      sources:
        - arxiv
      generate_report: true
      report_type: "weekly"

# 采集频率限制
# 避免过度请求
limits:
  # 同一信息源最小采集间隔(分钟)
  min_interval: 30
  # 每日最大采集次数
  max_daily_runs: 10


path: config/sources.yaml
content: # AI前沿哨兵 - 信息源配置
# 配置需要采集的信息源

# GitHub Trending配置
github:
  enabled: true
  language: python
  date_range: daily  # daily / weekly / monthly
  limit: 20
  # 可添加关键词过滤
  keywords:
    - AI
    - machine-learning
    - deep-learning
    - LLM
    - GPT

# arXiv论文配置
arxiv:
  enabled: true
  # 论文类别，常见AI类别:
  # cs.AI - Artificial Intelligence
  # cs.LG - Machine Learning
  # cs.CL - Computation and Language
  # cs.CV - Computer Vision
  # cs.NE - Neural and Evolutionary Computing
  categories:
    - cs.AI
    - cs.LG
    - cs.CL
  max_results: 20
  # 自定义搜索词 (可选)
  search_query: "GPT OR transformer OR diffusion"

# HackerNews配置
hackernews:
  enabled: true
  # 获取类型: top / new / best / ask / show
  item_type: top
  limit: 20
  # 关键词过滤，用于筛选AI相关内容
  keywords:
    - AI
    - ML
    - machine learning
    - neural
    - LLM
    - GPT
    - Claude
    - deep learning
    - language model
    - open source AI

# RSSHub实例配置
# 自部署的RSSHub服务，支持多种信息源
rsshub:
  instance_url: https://brain.zeabur.app
  enabled: true
  
  # Twitter/X配置 (注意: Twitter反爬严格，可能需要配置代理)
  twitter:
    enabled: false  # 当前Twitter路由受限，暂不启用
    # 关注的AI领袖账号
    accounts:
      - name: Yann LeCun
        handle: ylecun
        category: Research
      - name: Sam Altman
        handle: sama
        category: Industry Leader
      - name: Andrew Ng
        handle: AndrewYNg
        category: Education
      - name: Dario Amodei
        handle: DarioAmodei
        category: AI Company
      - name: Jensen Huang
        handle: nvidia
        category: Hardware
      - name: Andrej Karpathy
        handle: karpathy
        category: Research
      - name: Lilian Weng
        handle: lilianweng
        category: Research
      - name: Jason Wei
        handle: _jasonwei
        category: Research
      - name: Ilya Sutskever
        handle: IlyaSutskever
        category: Research
      - name: Fei-Fei Li
        handle: drfeifei
        category: Research
    # 搜索关键词
    search_queries:
      - "AI breakthrough"
      - "LLM news"
      - "machine learning"
    limit: 20

# 原始Twitter配置 (需要API Key)
twitter:
  enabled: false
  accounts:
    - ylecun
    - sama
    - AndrewYNg
    - binduredy
  keywords:
    - AI
    - machine learning
    - LLM
    - GPT-4
    - Claude
  limit: 20

# RSS博客订阅配置
blogs:
  enabled: true
  limit_per_feed: 5
  feeds:
    # Anthropic (社区维护源)
    - name: Anthropic Engineering
      url: https://raw.githubusercontent.com/Olshansk/rss-feeds/main/feeds/feed_anthropic_engineering.xml
      category: AI Company
      
    - name: Anthropic News
      url: https://raw.githubusercontent.com/Olshansk/rss-feeds/main/feeds/feed_anthropic_news.xml
      category: AI Company
      
    - name: Claude Blog
      url: https://raw.githubusercontent.com/Olshansk/rss-feeds/main/feeds/feed_claude.xml
      category: AI Company
      
    # OpenAI
    - name: OpenAI Blog
      url: https://openai.com/blog/rss.xml
      category: AI Company
      
    - name: OpenAI Research
      url: https://raw.githubusercontent.com/Olshansk/rss-feeds/main/feeds/feed_openai_research.xml
      category: AI Company
      
    # Google/DeepMind
    - name: Google AI Blog
      url: https://research.google/blog/rss
      category: AI Company
      
    - name: DeepMind Blog
      url: https://deepmind.google/blog/rss.xml
      category: AI Company
      
    - name: Hugging Face Blog
      url: https://huggingface.co/blog/feed.xml
      category: AI Company
      
    # xAI
    - name: xAI News
      url: https://raw.githubusercontent.com/Olshansk/rss-feeds/main/feeds/feed_xainews.xml
      category: AI Company
      
    # Cursor
    - name: Cursor Blog
      url: https://raw.githubusercontent.com/Olshansk/rss-feeds/main/feeds/feed_cursor.xml
      category: AI Tool
      
    # DeepLearning.AI
    - name: The Batch
      url: https://raw.githubusercontent.com/Olshansk/rss-feeds/main/feeds/feed_the_batch.xml
      category: AI Education
      
    # 学术博客
    - name: BAIR Blog
      url: https://bair.berkeley.edu/blog/feed.xml
      category: Research
      
    # 科技媒体
    - name: TechCrunch AI
      url: https://techcrunch.com/category/artificial-intelligence/feed/
      category: Tech Media
      
    - name: VentureBeat AI
      url: https://venturebeat.com/category/ai/feed/
      category: Tech Media

# 中国AI公司官网新闻监控 (无RSS，使用网页解析)
# 注意: 这些源需要使用topic_tracking技能或自定义解析器
chinese_ai_companies:
  - name: 智谱AI新品发布
    enabled: true
    url: https://docs.bigmodel.cn/cn/update/new-releases
    company: ZhipuAI (智谱)
    category: AI Company
    parser: web_news  # 使用网页新闻解析器
    update_frequency: daily  # 每日检查更新
    
  - name: MiniMax新闻动态
    enabled: true
    url: https://www.minimaxi.com/news
    company: MiniMax
    category: AI Company
    parser: web_news
    update_frequency: daily
    
  - name: 扣子Coze文档更新
    enabled: true
    url: https://docs.coze.cn
    company: Coze (扣子)
    category: AI Tool
    parser: web_news
    update_frequency: daily

# 自定义RSS源 (可添加更多)
custom_feeds:
  - name: MMChat AI资讯
    url: https://mmchat.zeabur.app/feeds/all.atom
    category: AI News Aggregator
    enabled: true
    
  # InfoQ技术媒体
  - name: InfoQ全球
    url: https://feed.infoq.com
    category: Tech Media
    enabled: true
    
  - name: InfoQ中文
    url: https://www.infoq.cn/rss/
    category: Tech Media (Chinese)
    enabled: true


path: references/api_setup.md
content: # API配置指南

本文档说明如何配置各个平台的API以启用完整功能。

## Twitter/X API

### 为什么需要配置？

Twitter/X API用于采集：
- AI大咖的最新推文
- 关键词相关的讨论
- 热门话题

**不配置此API**：Twitter采集功能将自动跳过，不影响其他功能。

### 申请步骤

1. **申请开发者账号**
   - 访问 [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard)
   - 使用Twitter账号登录
   - 申请开发者访问（可能需要审核）

2. **创建项目和应用**
   - 创建新项目
   - 创建应用（选择"Create App"）
   - 获取 API Key 和 API Secret

3. **获取Bearer Token**
   - 在 Developer Portal 找到 "Authentication Tokens" 部分
   - 生成 Bearer Token

### 配置方法

编辑 `config/settings.yaml`：

```yaml
twitter:
  bearer_token: "YOUR_BEARER_TOKEN_HERE"
```

或在WebUI的"配置"页面输入。

### API限制说明

| 账号类型 | 搜索API | 用户推文 |
|---------|--------|---------|
| Free | 500/15min | 1000/15min |
| Basic | 10K/15min | 10K/15min |
| Pro | 100K/15min | 1M/15min |

默认配置使用免费额度，已做节流处理。

---

## GitHub API

### 无需配置

GitHub Trending功能使用公开的GitHub REST API，无需认证即可使用。

### API限制

- 未认证：60次/小时
- 已认证：5000次/小时

如需更高配额：
1. 在 [GitHub Settings](https://github.com/settings/tokens) 生成 Personal Access Token
2. 在请求时添加 Header：

```python
headers = {
    "Authorization": f"token {YOUR_TOKEN}",
    "Accept": "application/vnd.github.v3+json"
}
```

---

## arXiv API

### 无需配置

arXiv API是完全公开的学术论文接口：
- 地址：`http://export.arxiv.org/api/query`
- 无需认证
- 无严格限制（建议遵守服务条款）

---

## HackerNews API

### 无需配置

HackerNews Firebase API是公开接口：
- 地址：`https://hacker-news.firebaseio.com/v0/`
- 无需认证
- 无速率限制

---

## RSS订阅

### 无需配置

RSS/Atom订阅解析使用通用标准协议，支持：
- RSS 2.0
- Atom 1.0
- 大部分博客平台的订阅源

只需在 `config/sources.yaml` 中添加RSS地址即可。

### 测试RSS地址

在 `scripts/parsers/blog_rss.py` 中有测试函数：

```python
from blog_rss import validate_rss_url

result = validate_rss_url("https://example.com/feed.xml")
print(result)
```

---

## 自定义API

### 添加新的认证信息

在 `config/settings.yaml` 中添加：

```yaml
# 自定义API配置
custom_apis:
  my_api:
    [REDACTED_SECRET]"
    api_secret: "YOUR_API_SECRET"
    base_url: "https://api.example.com"
```

### 在解析器中使用

```python
def fetch_custom_api(config, api_settings):
    [REDACTED_SECRET]"my_api", {}).get("api_key", "")
    
    headers = {
        "Authorization": f"Bearer {api_key}"
    }
    
    response = requests.get(
        config["url"],
        headers=headers
    )
    return response.json()
```

---

## 安全建议

### 1. 保护API密钥

- **不要**将真实的API密钥提交到代码仓库
- 使用环境变量：

```python
import os

bearer_token = os.environ.get("TWITTER_BEARER_TOKEN", "")
```

- 或创建 `config/secrets.yaml`（已加入.gitignore）：

```yaml
twitter:
  bearer_token: "REAL_TOKEN"
```

### 2. 本地配置文件

项目包含 `.gitignore` 文件，会忽略：

```
config/settings.yaml
config/secrets.yaml
output/
```

首次使用时，复制模板配置：

```bash
cp config/settings.yaml.example config/settings.yaml
```

### 3. 定期轮换密钥

定期更新API密钥，特别是生产环境。

---

## 故障排除

### Twitter采集失败

1. 检查Bearer Token是否正确
2. 确认开发者账号状态正常
3. 检查API配额是否用尽
4. 查看 [Twitter API Status](https://api.twitterstat.us/)

### GitHub API 403错误

通常是被限流，解决方案：
- 添加认证Token
- 减少采集频率
- 使用备用解析器（直接爬取Trending页面）

### RSS解析失败

常见原因：
- RSS地址已失效
- 需要认证
- 非标准格式

解决方案：
- 在浏览器中直接打开RSS地址验证
- 使用在线RSS验证工具
- 联系博客管理员获取正确的订阅地址

---

## 资源链接

| 平台 | 开发者门户 | 文档 |
|------|----------|------|
| Twitter/X | [Developer Portal](https://developer.twitter.com/en/portal/dashboard) | [API v2 Docs](https://developer.twitter.com/en/docs/twitter-api) |
| GitHub | [Settings](https://github.com/settings/tokens) | [REST API](https://docs.github.com/en/rest) |
| arXiv | - | [API Guide](https://info.arxiv.org/help/api/basics.html) |
| HackerNews | - | [Firebase API](https://github.com/HackerNews/API) |


path: references/sources_guide.md
content: # 信息源添加指南

本文档说明如何为AI前沿哨兵添加新的信息源。

## 添加新的解析器

### 步骤1：创建解析器文件

在 `scripts/parsers/` 目录下创建新的Python文件，例如 `custom_source.py`：

```python
"""
自定义信息源解析器
"""
import requests
from datetime import datetime


def fetch_custom_source(config):
    """
    获取自定义信息源内容
    
    Args:
        config: 信息源配置，来自 sources.yaml
    
    Returns:
        list: 内容列表，每项包含:
            - title: 标题
            - url: 链接
            - description: 描述
            - published: 发布时间
            - source: 源名称
            - source_type: 内容类型 (paper/project/discussion/article/tweet)
    """
    url = config.get("url", "")
    limit = config.get("limit", 20)
    
    items = []
    
    try:
        response = requests.get(url, timeout=30)
        response.raise_for_status()
        
        # 根据返回的数据格式解析
        data = response.json()  # 或 response.text 用 re/BeautifulSoup 解析
        
        for item in data[:limit]:
            items.append({
                "title": item.get("title", "无标题"),
                "url": item.get("url", ""),
                "description": item.get("description", ""),
                "published": item.get("published", ""),
                "author": item.get("author", ""),
                "source": "custom",
                "source_type": "article"
            })
            
    except Exception as e:
        print(f"采集失败: {e}")
    
    return items


# 如果需要导出搜索功能
def search_custom_source(query, limit=10):
    """搜索自定义源"""
    # 实现搜索逻辑
    pass


if __name__ == "__main__":
    # 测试代码
    config = {"url": "https://example.com/api", "limit": 10}
    results = fetch_custom_source(config)
    print(f"获取到 {len(results)} 条内容")
```

### 步骤2：在 sources.yaml 中注册

```yaml
# config/sources.yaml

custom_source:
  enabled: true
  url: "https://example.com/api"
  limit: 20
  # 其他自定义配置...
```

### 步骤3：在 collect.py 中注册

编辑 `scripts/collect.py`，在 `_collect_source` 方法中添加：

```python
elif source_name == "custom_source":
    items = fetch_custom_source(source_config)
```

### 步骤4：更新 __init__.py

在 `scripts/parsers/__init__.py` 中添加导出：

```python
from .custom_source import fetch_custom_source, search_custom_source

__all__ = [
    # ... 其他
    'fetch_custom_source',
    'search_custom_source'
]
```

## 常见数据格式处理

### JSON API

```python
def fetch_json_api(config):
    response = requests.get(config["url"])
    data = response.json()
    
    items = []
    for item in data.get("items", []):
        items.append({
            "title": item["title"],
            "url": item["url"],
            "source_type": "article"
        })
    return items
```

### HTML网页解析

需要安装 BeautifulSoup：

```bash
pip install beautifulsoup4 lxml
```

```python
from bs4 import BeautifulSoup

def fetch_html_page(config):
    response = requests.get(config["url"])
    soup = BeautifulSoup(response.text, "lxml")
    
    items = []
    for element in soup.select(".article-list .item"):
        items.append({
            "title": element.select_one(".title").text.strip(),
            "url": element.select_one("a")["href"],
            "source_type": "article"
        })
    return items
```

### RSS/Atom订阅

使用已有的 `blog_rss.py` 解析器，只需在配置中添加：

```yaml
blogs:
  enabled: true
  feeds:
    - name: "自定义源"
      url: "https://example.com/feed.xml"
      category: "自定义分类"
```

## 高级：实现认证

如果API需要认证：

```python
def fetch_authenticated_api(config, auth_settings):
    [REDACTED_SECRET]"api_key", "")
    
    headers = {
        "Authorization": f"Bearer {api_key}",
        "User-Agent": "AI-Sentinel/1.0"
    }
    
    response = requests.get(
        config["url"],
        headers=headers,
        timeout=30
    )
    response.raise_for_status()
    
    return response.json()
```

然后在 `collect.py` 中调用时传入认证信息：

```python
auth_settings = self.settings.get("custom_source_auth", {})
items = fetch_authenticated_api(source_config, auth_settings)
```

## 数据标准化

解析后的数据应该遵循以下格式：

| 字段 | 类型 | 必需 | 说明 |
|------|------|------|------|
| title | string | ✅ | 内容标题 |
| url | string | ✅ | 原文链接 |
| description | string | ❌ | 简短描述/摘要 |
| published | string | ❌ | 发布时间 |
| author | string | ❌ | 作者 |
| source | string | ✅ | 来源名称 |
| source_type | string | ✅ | 内容类型 |

**source_type 可选值**：
- `paper` - 学术论文
- `repository` - 开源项目
- `discussion` - 社区讨论
- `article` - 博客文章
- `tweet` - 社交动态
- `news` - 新闻

## 测试解析器

```bash
cd scripts/parsers
python -c "
from custom_source import fetch_custom_source
config = {'url': 'https://example.com/api', 'limit': 5}
results = fetch_custom_source(config)
print(f'获取到 {len(results)} 条内容')
for item in results:
    print(f'  - {item[\"title\"]}')"
```

## 常见问题

### Q: API返回429 Too Many Requests怎么办？

A: 添加请求间隔和重试机制：

```python
import time

def fetch_with_retry(url, max_retries=3):
    for i in range(max_retries):
        try:
            response = requests.get(url, timeout=30)
            if response.status_code == 429:
                time.sleep(60 * (i + 1))  # 递增等待
                continue
            response.raise_for_status()
            return response
        except Exception as e:
            if i == max_retries - 1:
                raise
            time.sleep(2 ** i)
```

### Q: 数据量很大怎么处理？

A: 使用分页和流式处理：

```python
def fetch_with_pagination(config):
    items = []
    page = 1
    per_page = 100
    
    while True:
        response = requests.get(
            f"{config['url']}?page={page}&per_page={per_page}"
        )
        data = response.json()
        
        if not data.get("items"):
            break
            
        items.extend(data["items"])
        
        if len(data["items"]) < per_page:
            break
            
        page += 1
        time.sleep(1)  # 避免过快
        
    return items
```


path: reports/2026-04-10/evening_report.md
content: # AI哨兵晚报 | 2026年4月10日

> 本报告追踪AI领域最新动态，为极客Jerry提供决策参考

---

## 📰 今日头条

### 🔥 DeepSeek V4定档4月下旬发布——万亿参数、推理提速35倍

**来源**：[DeepSeek V4定档四月下旬：万亿参数落地，国产算力深度适配](https://www.coze.cn/share-article/201775802144476864)

**摘要**：DeepSeek创始人梁文锋确认，新一代旗舰模型V4将于4月下旬正式发布。V4采用1万亿参数的MoE架构，推理时仅激活370亿参数，推理速度提升35倍、能耗降低40%。上下文窗口扩展至100万token。作为原生多模态模型，支持文本、图像、视频和音频。编程能力备受关注，SWE-Bench测试得分83.7%，超过GPT-5.2和Claude Opus。

**关键技术点**：
- 首次实现与华为昇腾等国产芯片的深度适配
- 支持338种编程语言
- 华为已下达数十万片芯片批量订单

**影响评估**：DeepSeek V4的发布将是国产大模型的重要里程碑，"去CUDA化"迈出实质性一步。受此影响，相关AI芯片价格上涨约20%。

**优先级**：⭐ P0

---

### 🔥 阿里HappyHorse视频模型空降榜首——4月17日正式开源

**来源**：[AI视频生成模型HappyHorse将一周后对外发布](https://www.coze.cn/share-article/201775792467061824)

**摘要**：阿里神秘视频模型HappyHorse已完成内部上架阿里百炼平台，预计4月17日正式对外发布。这是全球首个原生支持音视频联合生成的开源视频大模型，搭载150亿参数。在Artificial Analysis视频竞技场排行榜中，文本生成视频（不含音频）Elo得分1383，图片生成视频（不含音频）得分1413，双双刷新榜单纪录。

**技术亮点**：
- 单张H100生成5秒1080p视频仅需约38秒
- 原生支持7种语言的唇形同步
- 曾以匿名身份与字节Seedance、Kling等闭源产品盲测对战

**影响评估**：阿里股价盘中涨幅一度接近4%，触及127.5港元。视频生成赛道格局将迎来变局。

**优先级**：⭐ P0

---

## 🚀 重磅发布

### 字节跳动Seeduplex全双工语音大模型——AI迈入"真对话"时代

**来源**：[字节跳动全双工语音大模型Seeduplex发布，AI迈入"真对话"时代](http://m.toutiao.com/group/7626983621626167842/)

**摘要**：4月9日，字节跳动豆包团队发布原生全双工语音大模型Seeduplex，已全量上线豆包App。实现"边听边说"的类人交互，端到端延迟压缩至300ms以内，与人类对话间隔几乎一致。

**核心能力**：
- 实时打断：80ms内感知切换
- 超低延迟：端到端320ms
- 超强抗干扰：误回复率和误打断率降低50%

**影响评估**：标志着AI语音正式从"命令式"迈向"自然对话式"，将重塑车载、IoT、客服、教育等多行业体验。

**优先级**：⭐ P1

---

### 智谱GLM-5.1正式开源——国产模型首次价格对齐国际头部

**来源**：[AI大模型周报：阿里智谱密集发新品，月之暗面传上市](http://m.toutiao.com/group/7627066659551625771/)

**摘要**：4月8日，智谱发布GLM-5.1，是全球首个在真实工程任务中验证8小时持续工作能力的开源模型。代码能力全球第三、SWE-bench Pro国产首次超越Opus 4.6。调价后Coding场景价格已接近Claude Sonnet 4.6水平，国产大模型首次在核心场景实现价格对齐。

**关键数据**：
- 智谱2025年总收入7.24亿元，同比增长131.9%
- 2026年Q1 API定价提升83%，调用量增长400%
- 港股智谱股价一度涨近18%

**优先级**：⭐ P1

---

## 🛡️ 安全与研究

### 蚂蚁集团获CVPR 2026 AIGC图像检测挑战赛冠军

**来源**：[蚂蚁AI安全实验室拿下CVPR 2026 AIGC图像检测挑战赛冠军](https://www.coze.cn/share-article/201775817494649504)

**摘要**：蚂蚁集团AI安全实验室MICV队伍在CVPR 2026 NTIRE鲁棒性AIGC图像检测挑战赛中，以ROC AUC 0.9723的成绩获「复杂真实场景鲁棒性样本测试」赛道冠军。同时发布Awesome-AIGC-Image-Video-Detection资源仓库，整合前沿论文、基准数据集及实用工具。

**技术方案**：
- 基于DINOv3视觉基础模型的鲁棒检测框架
- 百万级多源训练语料库
- 双流并行架构 + 阶梯式鲁棒数据增强

**优先级**：⭐ P1

---

### 脑机接口AI算法云平台"脑电云枢"发布

**来源**：[脑机接口与AI论坛举办，全球首个多尺度脑机接口AI算法云平台发布](https://www.coze.cn/share-article/201775795994584128)

**摘要**：4月8日，香港中文大学（深圳）发布全球首个多尺度脑机接口AI算法云平台"脑电云枢"。融合分子动力学模拟与脑机接口技术，无创脑电信号采集精度提升至0.2μV级别，解码准确率可达89%以上。

**优先级**：⭐ P2

---

## 💰 投融资动态

### 生数科技完成3亿美元B轮融资——阿里云领投

**来源**：[生数科技完成3亿美元融资，阿里云领投](https://www.coze.cn/share-article/201775779677993760)

**摘要**：AI视频生成公司生数科技完成3亿美元B轮融资，由阿里云领投，百度Ventures等跟投。资金将用于通用世界模型开发，推进物理世界通用人工智能。生数科技由清华大学朱军创立，2024年推出国内首款视频生成模型Vidu。

**优先级**：⭐ P1

---

### 月之暗面被传赴港IPO——Kimi K2.5二十天收入2.4亿美元

**来源**：[AI大模型周报：阿里智谱密集发新品，月之暗面传上市](http://m.toutiao.com/group/7627066659551625771/)

**摘要**：月之暗面被传正处于赴港上市早期考量阶段。Kimi K2.5上线20天内收入达2.4亿美元，超过2025年全年总和。但月活从2165万腰斩至902万，估值约180亿美元，存在潜在倒挂风险。

**优先级**：⭐ P2

---

## 🛠️ 开源项目推荐

### Hermes Agent v0.8.0发布——两个月狂揽4.7万GitHub星标

**来源**：[开源AI智能体Hermes Agent发v0.8.0版，两个月狂揽4.7万GitHub星标](https://www.coze.cn/share-article/201775817494686400)

**摘要**：Nous Research开发的MIT许可开源自主AI框架Hermes Agent发布v0.8.0版本，4月8日单日新增超6400个GitHub星标。自2月发布以来不到两个月，总星标突破4.7万，持续霸榜全球开源榜单第一。

**与OpenClaw对比**：
| 维度 | Hermes Agent | OpenClaw |
|------|--------------|----------|
| 技能获取 | 自主从任务经验生成 | 依赖人类编写 |
| 记忆机制 | 分层记忆系统 | 显式检索式记忆 |
| 擅长场景 | 探索性编程、复杂工程 | 步骤明确、容错率低任务 |

**优先级**：⭐ P1

---

### 高德开源具身操作基座模型ABot-M0

**来源**：[AI大模型周报](http://m.toutiao.com/group/7627066659551625771/)

**摘要**：高德全量开源具身操作基座模型ABot-M0，全球首个基于统一架构的机器人具身操作基座模型。Libero-Plus基准任务成功率达80.5%，较业界标杆Pi0提升近30%。开源涵盖数据、算法与模型三大维度。

**优先级**：⭐ P2

---

## 🚗 具身智能

### 小马智行发布PonyWorld世界模型2.0

**来源**：[小马智行发布PonyWorld世界模型2.0](https://www.coze.cn/share-article/201775794134893408)

**摘要**：4月10日，小马智行发布PonyWorld世界模型2.0，具备自我诊断与定向进化能力，已应用于L4级无人驾驶车队和研发体系，标志着自动驾驶技术进入全新研发训练范式。

**优先级**：⭐ P2

---

### 智元机器人发布具身基座大模型GO-2

**来源**：[AI大模型周报](http://m.toutiao.com/group/7627066659551625771/)

**摘要**：4月9日，智元机器人发布具身基座大模型GO-2，首次在统一架构中打通从逻辑推理到精准动作执行，让机器人从"黑盒摸索"迈向"知行合一"。

**优先级**：⭐ P2

---

### 博泰车联与上海交大签署开源鸿蒙具身智能战略合作

**来源**：[博泰车联联手上海交大，锚定开源鸿蒙具身智能赛道](https://www.coze.cn/share-article/201775795118752000)

**摘要**：4月10日，博泰车联与上海交通大学人工智能操作系统研究院签署战略合作框架协议，合作覆盖学术研究、技术研发、产品应用及知识产权共建，重点推进"大脑-小脑"协同的具身智能系统架构研发。

**优先级**：⭐ P2

---

## 📊 数据洞察

### 本周行业关键数据

| 指标 | 数据 | 趋势 |
|------|------|------|
| 中国AI大模型日均Token调用量 | 140万亿+ | 📈 较2024年初增长超千倍 |
| OpenRouter周Token调用量 | 27T | 📈 环比增长18.94% |
| 豆包大模型日均Token使用量 | 120万亿 | 稳定增长 |
| 广东省备案大模型数量 | 132款 | — |
| Qwen3.6 Plus单日调用量 | 1.4万亿Token | 📈 登顶全球 |

---

## 🔮 技术趋势洞察

### 1. 从"参数军备竞赛"到"工程化落地"
本周阿里一周三连发、智谱GLM-5.1重新定义"持续工作"标准，竞争的焦点已从参数规模转向：
- 长程任务执行能力（8小时持续工作）
- Agent工程化水平
- 定价逻辑重构（从降价换市场到性能锚定价格）

### 2. 全双工语音交互成为新战场
字节Seeduplex的发布标志着语音交互从"回合制"进入"边听边说"时代，毫秒级响应、实时打断成为标配能力。

### 3. 视频生成赛道格局动荡
即梦涨价8倍与阿里HappyHorse空降榜首形成鲜明对比，算力成本压力与技术创新之间的博弈正在重塑市场格局。

### 4. 国产算力适配加速
DeepSeek V4首次实现与华为昇腾深度适配，"去CUDA化"迈出实质性一步，全球AI技术竞争格局正在发生变化。

---

## 💡 行动建议

### 对Jerry工作的启发

1. **关注全双工语音技术**：字节Seeduplex的实现思路（端到端统一建模、超强抗干扰）值得借鉴，可能对vibe coding中的语音交互设计有参考价值

2. **视频生成模型动态**：HappyHorse的音视频联合生成能力值得关注，可能影响AI哨兵项目的多模态内容处理策略

3. **Agent能力构建**：Hermes Agent的"自进化"特性与GLM-5.1的8小时持续工作能力，代表了Agent发展的新方向

4. **AIGC检测需求**：蚂蚁的AIGC图像检测方案对于AI哨兵项目的质量评价体系有直接参考价值

---

> 📅 本报告生成时间：2026-04-10 20:09
> 
> 🛡️ AI哨兵 | 守护你的AI信息边界


path: reports/2026-04-10/morning_report.md
content: # 🌅 AI前沿哨兵 - 2026年04月10日晨报

> **采集时间**: 2026-04-10T16:47:05.899055
> **数据来源**: GitHub Trending、arXiv、AI博客、MMChat

---

## ⭐ GitHub Trending (9个热门项目)

1. **aWiseKing/awiselang** ⭐1
   A toy language with Python syntax and Rust features
   链接: https://github.com/aWiseKing/awiselang

2. **kpjadhav94/python-for-embedded** ⭐0
   Python Language for Embedded Engineer
   链接: https://github.com/kpjadhav94/python-for-embedded

3. **modern-python/spellchecks** ⭐0
   Python library for spell-checking text in multiple languages, backed by pyenchan
   链接: https://github.com/modern-python/spellchecks

4. **vishwaksenareddykatta/AutoMailer** ⭐0
   AutoMailer is a Python-based intelligent email management system that combines t
   链接: https://github.com/vishwaksenareddykatta/AutoMailer

5. **kaitlynnFelty/Core-Language-Interpreter** ⭐0
   Python implementation of a custom programming language interpreter, including a 
   链接: https://github.com/kaitlynnFelty/Core-Language-Interpreter

---

## 📚 arXiv 最新论文 (20篇)

1. **When Numbers Speak: Aligning Textual Numerals and Visual Instances in Text-to-Video Diffusion Models**
   作者: Zhengyang Sun, Yu Chen
   摘要: ...

2. **SIM1: Physics-Aligned Simulator as Zero-Shot Data Scaler in Deformable Worlds**
   作者: Yunsong Zhou, Hangxu Liu
   摘要: ...

3. **RewardFlow: Generate Images by Optimizing What You Reward**
   作者: Onkar Susladkar, Dong-Hwan Jang
   摘要: ...

4. **PSI: Shared State as the Missing Layer for Coherent AI-Generated Instruments in Personal AI Agents**
   作者: Zhiyuan Wang, Erzhen Hu
   摘要: ...

5. **Ads in AI Chatbots? An Analysis of How Large Language Models Navigate Conflicts of Interest**
   作者: Addison J. Wu, Ryan Liu
   摘要: ...

---

## 📰 AI博客动态 (45条)

### TechCrunch AI (5条)

- [ChatGPT finally offers $100/month Pro plan]()
- [Florida AG announces investigation into OpenAI over shooting that allegedly involved ChatGPT]()

### Claude Blog (5条)

- [Making Claude Cowork ready for enterprise]()
- [The advisor strategy: Give agents an intelligence boost]()

### OpenAI Blog (5条)

- [CyberAgent moves faster with ChatGPT Enterprise and Codex]()
- [OpenAI Full Fan Mode Contest: Terms & Conditions]()

### Cursor Blog (5条)

- [Bugbot now self-improves with learned rules]()
- [Better MoE model inference with warp decode]()

### Anthropic News (5条)

- [Anthropic expands partnership with Google and Broadcom for multiple gigawatts of next-generation compute]()
- [Australian government and Anthropic sign MOU for AI safety and research]()

---

## 🔥 MMChat 热门资讯 (10条)

1. **Anthropic的疯狂一周：每天一个新产品，我已经跟不上了**
2. **2亿多人开始和AI打电话，他们在聊什么？**
3. **Sam Altman被纽约客指控撒谎成性**
4. **Karpathy又双叒叕发新概念了，这次我替你找到了那个产品**
5. **蒸馏出「乔布斯」之后，他说Apple AI完全是shit**
6. **开源「女娲.skill」，你现在可以去蒸馏任何人！**
7. **把同事作为Skill**
8. **丢张图就能复刻：GLM-5V-Turbo视觉Coding实测**
9. **13万人排队的Seedance 2.0，我2分钟就用上了**
10. **真开源！Claude Code75页橙皮书，带你从入门到精通**

---

## 📊 今日数据统计

| 类型 | 数量 |
|------|------|
| GitHub热门项目 | 9 个 |
| arXiv论文 | 20 篇 |
| AI博客文章 | 45 条 |
| MMChat资讯 | 10 条 |
| **总计** | **84 条** |

---

**报告生成时间**: 2026-04-10 16:57:21
**由 AI前沿哨兵 自动生成**


path: reports/2026-04-10/xiaohongshu_report.md
content: # 🔥AI前沿情报站｜04月10日必看清单

姐妹们！今天的AI圈又炸了💥 一口气整理了84条干货，建议先码再看⭐

---

## 💎 今日重磅（必看TOP5）

**1. aWiseKing/awiselang**
⭐1 Stars | 热门开源项目

**2. kpjadhav94/python-for-embedded**
⭐0 Stars | 热门开源项目

**3. When Numbers Speak: Aligning Textual Num**
📄 最新论文 | 学术前沿

**4. SIM1: Physics-Aligned Simulator as Zero-**
📄 最新论文 | 学术前沿

**5. ChatGPT finally offers $100/month Pro pl**
📰 TechCrunch AI | 业内动态

---

## 📚 论文党的福音（arXiv精选）

📄 **When Numbers Speak: Aligning Textua...**
💡一句话：前沿AI研究...
🏷️ 作者：Zhengyang Sun

📄 **SIM1: Physics-Aligned Simulator as ...**
💡一句话：前沿AI研究...
🏷️ 作者：Yunsong Zhou

📄 **RewardFlow: Generate Images by Opti...**
💡一句话：前沿AI研究...
🏷️ 作者：Onkar Susladkar

---

## 💻 开源真香现场（GitHub）

🔧 **aWiseKing/awiselang**
A toy language with Python syntax and Ru...
⭐1 Stars | Rust

🔧 **kpjadhav94/python-for-embedded**
Python Language for Embedded Engineer...
⭐0 Stars | Python

🔧 **modern-python/spellchecks**
Python library for spell-checking text i...
⭐0 Stars | Python

---

## 📰 业内动态（不容错过）

**TechCrunch AI**（5条）
• ChatGPT finally offers $100/mo...
• Florida AG announces investiga...

**Claude Blog**（5条）
• Making Claude Cowork ready for...
• The advisor strategy: Give age...

**OpenAI Blog**（5条）
• CyberAgent moves faster with C...
• OpenAI Full Fan Mode Contest: ...

**Cursor Blog**（5条）
• Bugbot now self-improves with ...
• Better MoE model inference wit...

---

## 🔥 热门话题（MMChat精选）

1️⃣ Anthropic的疯狂一周：每天一个新产品
2️⃣ 2亿多人开始和AI打电话
3️⃣ Sam Altman被纽约客指控撒谎成性
4️⃣ Karpathy新概念产品来了
5️⃣ 蒸馏出「乔布斯」！开源女娲.skill


---

## 📊 今日数据

| 类型 | 数量 |
|------|------|
| 📄 论文 | 20 篇 |
| 💻 项目 | 9 个 |
| 📰 资讯 | 45 条 |
| 🔥 热点 | 10 个 |

---

💬 **今日份AI情报已送达！觉得有用就点个收藏吧～**
🔖 明天08:30准时更新，记得蹲！

#AI #人工智能 #科技资讯 #每日打卡 #干货分享


path: reports/案例分析/Elseland案例研究.md
content: # Elseland 案例研究：AI 开放世界平台的技术架构与 Vibe Coding 协作模式

> **案例时间**：2026年2月20日 - 4月10日（49天）  
> **核心人物**：刘耕（北京大学美学博士）  
> **项目规模**：30万行代码，成本仅5000元  
> **Agent规模**：17个AI Agent协同开发

---

## 一、项目概述

### 1.1 什么是 Elseland

Elseland 是一个**AI 开放世界平台**，由北大美学博士刘耕携17个AI Agent在49天内开发完成。这个项目的核心目标是构建一个「人和AI生命体共存的世界」，让每个用户都能在其中创造并经历新的体验。

Elseland 的开放性体现在三个层次：

| 层次 | 描述 |
|------|------|
| **玩法的开放** | 丰富的社区玩法，包括各种互动娱乐形式 |
| **世界的开放** | 横跨5个纪元、10大种族的宏大世界观（第三纪元已上线） |
| **创造的开放** | 提供多种编辑器，赋予用户创造能力 |

### 1.2 主剧情世界设定

Elseland 首先推出的是「第三纪元」末期的故事：

- **时代背景**：魔法、炼金与偃甲并存的时代
- **核心冲突**：阿卡迪亚帝国走向分崩离析——皇统断裂、寡头夺权、魔潮复苏
- **玩家角色**：来自地球的穿越者，被流浪皇子 Rowan 收留于前线重镇安帕
- **叙事目标**：卷入帝国覆灭与重生的洪流，结识命运各异的伙伴

### 1.3 编辑器生态

Elseland 提供了丰富的用户创作工具：

- **角色编辑器**：自定义游戏角色
- **世界模拟器**：构建游戏世界观
- **互动剧场编辑器**：节点制剧情编排
- **2D小游戏编辑器**：创建轻量游戏
- **3D Studio**：三维内容创作
- **策略地图编辑器**：战略游戏地图设计
- **多Agent协作工坊**：复杂创作的团队协作

---

## 二、技术架构

### 2.1 Agent 分工体系（17个Agent）

刘耕将17个Agent分为两大类别，形成清晰的专业分工：

#### 程序开发与测试 Agent（9个）

| Agent | 角色定位 | 核心职责 |
|-------|----------|----------|
| Claude Code × 6 | 主力开发 | 覆盖架构、游戏引擎、智脑、小游戏、编辑器、测试 |
| Codex × 1 | 顾问角色 | 世界观讨论、项目建议 |
| Gemini Cli × 1 | UI优化 | 界面优化与打磨 |
| Kimi Code × 1 | 机动配合 | 临时任务响应 |

**开发Agent详细分工：**
1. **主架构师**：负责整体技术架构设计
2. **游戏引擎工程师**：核心游戏逻辑开发
3. **智脑工程师**：AI对话与NPC智能系统
4. **小游戏开发工程师**：轻量级游戏模块
5. **编辑器工程师**：用户创作工具开发
6. **主测试**：质量保障与Bug修复

#### 资产与内容生成 Agent（8个）

| Agent | 角色定位 | 核心职责 |
|-------|----------|----------|
| Elser | 角色设计师 | 角色创建与设定 |
| 虾酱 | 图像生成 | 2D图片素材生成 |
| 皮格马利翁 | 3D资产生成 | 3D模型、骨骼绑定与驱动 |
| 莎士比亚 | 剧本创作 | 游戏剧本编写 |
| 魔术师 | 特效制作 | 精灵资产与视觉特效 |
| 调度师 | 协作枢纽 | 多Agent调度管理 |
| 小说家 | 叙事创作 | 故事与小说创作 |
| 诺兰 | 导演职能 | 编导与视频生成 |

### 2.2 Agent 调度机制

刘耕在开发过程中积累了大量 **Skill、Tool 和 Workflow**，并将这些技能配置给各个Agent。调度方式包括：

1. **主Claude Code直接调度**：让Claude Code直接管理和协调其他Agent
2. **调度师Agent管理**：通过专门的调度师Agent协助管理

这种配置使得Elseland的开发效率持续加速。刘耕预期：**当成熟专业技能的Agent越来越多时，效率还可能有数倍到数十倍的提升**。

### 2.3 技术栈推测

虽然官方未详细披露技术栈，但从项目特性推测：

| 层级 | 可能技术 |
|------|----------|
| 前端框架 | React/Vue（现代Web框架） |
| 游戏引擎 | Unity/Godot/Unreal（3D支持） |
| 后端服务 | Node.js/Python（API服务） |
| AI模型 | Claude Code、Codex、Gemini、Kimi（多模型协同） |
| 数据库 | PostgreSQL/MySQL + Redis |
| 部署 | Docker + 云服务 |

---

## 三、Agent 协作模式

### 3.1 协作架构设计

Elseland 的Agent协作采用**专业化分工 + 中心化调度**的模式：

```
                    ┌─────────────────┐
                    │   人类（刘耕）   │
                    │   产品决策者     │
                    └────────┬────────┘
                             │
                    ┌────────▼────────┐
                    │   调度师Agent   │
                    │   任务分配与协调 │
                    └────────┬────────┘
                             │
        ┌────────────────────┼────────────────────┐
        │                    │                    │
┌───────▼───────┐    ┌──────▼──────┐    ┌──────▼──────┐
│  开发Agent群  │    │  资产Agent群 │    │  内容Agent群 │
│     (9个)     │    │    (6个)     │    │    (2个)    │
└───────────────┘    └─────────────┘    └─────────────┘
```

### 3.2 协作流程

**典型开发流程：**

1. **需求定义** → 刘耕用自然语言描述功能和目标
2. **任务分解** → 调度师Agent将需求拆解为具体子任务
3. **并行执行** → 各专业Agent同时处理分配的任务
4. **结果整合** → 主架构师Claude Code整合各模块产出
5. **质量验证** → 主测试Agent进行测试验证
6. **迭代优化** → 根据运行结果进行反馈调整

### 3.3 效率对比数据

| 指标 | 传统模式 | Elseland模式 | 效率提升 |
|------|----------|--------------|----------|
| 小说Agent开发 | 50天（4人团队） | 0.5天（单人+Agent） | **100倍** |
| 日均产出 | 一个功能模块 | 一个独立小产品 | - |
| 周产出 | 有限 | 高难度创新产品 | - |

---

## 四、Vibe Coding 实践亮点

### 4.1 Vibe Coding 定义

Vibe Coding（氛围编程）由 Andrej Karpathy 于2025年提出，核心理念是：

> **从"手写每一行代码" → 变成"聊天指挥AI造App"**

### 4.2 刘耕的 Vibe Coding 工作流

#### 每日工作节奏
- **日均时长**：14小时以上
- **工作周期**：49天（除出差外几乎无休）
- **核心模式**：「言出法随」——念头一起，Agent便将其化作现实

#### 四层工作流结构

```
意图层（人） → 生成层（AI） → 反馈层（人+AI） → 交付层（AI）
```

1. **意图层**：刘耕描述"想要的感觉"和高层次目标
2. **生成层**：Claude Code等Agent自动生成代码、文件结构、部署脚本
3. **反馈层**：运行代码→发现问题→复制错误给AI→AI自动修复
4. **交付层**：测试→部署→持续迭代

### 4.3 实践技巧

#### "氛围调试"法
- **传统调试**：说具体的代码问题
- **氛围调试**：只描述感觉，如"按钮位置不对"、"动画不够流畅"
- **优势**：保持创意流不中断，让AI自主定位问题

#### 多Agent并行策略
- 前端、后端、测试可同时启动
- 不同专业Agent处理各自领域任务
- 减少等待时间，最大化并发效率

### 4.4 传统 vs Vibe Coding 对比

| 维度 | 传统开发 | Vibe Coding |
|------|----------|-------------|
| 代码来源 | 开发者手写 | AI自动生成 |
| 调试方式 | 人工定位 | "氛围调试" |
| 并行度 | 低（顺序执行） | 高（多Agent并行） |
| 学习门槛 | 高（需编程基础） | 低（自然语言即可） |
| 创新速度 | 慢 | 快 |
| 适用场景 | 生产级项目 | 快速原型、创新实验 |

---

## 五、成本与效率分析

### 5.1 成本结构

| 成本项 | 金额 | 说明 |
|--------|------|------|
| API调用费用 | ~5000元 | 主要为Claude Code等Agent服务的Token消耗 |
| 人力成本 | 0元 | 刘耕本人+17个Agent |
| 开发周期 | 49天 | 有效工作日 |

### 5.2 效率对比

#### 与传统开发对比

| 项目 | 传统模式（估算） | Elseland模式 | 提升倍数 |
|------|-----------------|---------------|----------|
| 开放世界项目 | 50人×4-6个月 | 1人+17Agent×49天 | **100倍以上** |
| 决策链长度 | 长（多层级审批） | 短（一人决策） | - |
| 迭代速度 | 周级/天级 | 实时 | - |

#### 与团队历史对比

**案例：小说Agent重构**
- **2024年7月**：4人团队开发，花费50天
- **2026年**：Elseland架构下重构，仅需半天
- **效率提升**：100倍

### 5.3 开发产出时间线

| 复杂度 | 预计开发时间 |
|--------|--------------|
| 独立小产品 | 1天 |
| 高难度创新产品 | 1周 |
| 互动剧本编辑器 | >1周 |
| 3D Studio | >1周 |

---

## 六、对 Vibe Coding 方法论的启发

### 6.1 核心理念

刘耕的实践验证了Vibe Coding的核心价值：

> **当具体技能的门槛被踏平，「理解事物的本质」反而成了真正的护城河。**

### 6.2 关键启示

#### 启示1：专业分工的重新定义

**传统观念**：程序员写代码，设计师做设计，编剧写剧本
**Vibe Coding**：人做导演，AI做执行，专业边界被打破

刘耕的案例证明：**一个不懂编程的文科博士，通过17个Agent协作，可以完成需要50人产研团队4-6个月工作的项目**。

#### 启示2：决策效率的指数提升

当一个人必须为最终结果负责、同时兼顾产品价值、审美和工程实现时：
- 决策速度更快
- 迭代周期更短
- 沟通成本更低

#### 启示3：超级个体的诞生

Vibe Coding 的终极目标是打造「超级个体」：

```
传统模式：PRD → 设计 → 开发 → 测试 → 部署（各由不同角色完成）
Vibe模式：人（全程主导） + Agent（全流程执行）
```

一个人可以跑通全流程：**PRD、UI、世界观、故事、视频、配乐、编程——从脑子到手指，全栈贯通。**

### 6.3 最佳实践建议

#### 团队层面

| 实践 | 说明 |
|------|------|
| **建立Agent技能库** | 积累Skill、Tool、Workflow配置 |
| **设计调度机制** | 建立中心化或分布式调度体系 |
| **配置专业Agent** | 按领域配置专业化Agent |
| **人机协作审核** | 高风险任务需人工审批 |

#### 个人层面

| 实践 | 说明 |
|------|------|
| **明确问题定义** | 先框定问题，再让AI执行 |
| **结构化上下文** | 给AI清晰的背景信息 |
| **持续迭代反馈** | 快速验证，快速调整 |
| **聚焦价值判断** | 让AI负责效率，人负责决策 |

### 6.4 未来展望

刘耕的预测：

> **在可预见的将来，当有成熟专业技能的Agent越来越多时，效率还可能有数倍到数十倍的提升。**

这预示着：
1. **工具门槛持续降低**：更多非技术背景的人可以参与创作
2. **单人产能持续提升**：一个人可以完成越来越复杂的项目
3. **专业化分工转变**：从"人的专业分工"转向"Agent的专业分工"

---

## 七、创始人背景

### 7.1 刘耕（CEO）

| 维度 | 信息 |
|------|------|
| 教育背景 | 北京大学哲学系（本硕博） |
| 研究方向 | 美学、中国文人艺术 |
| 职业生涯 | 武汉大学副教授→字节跳动→创业 |
| 核心能力 | 美学鉴赏、产品设计、跨学科整合 |
| 编程起点 | 30岁开始自学编程 |

### 7.2 张国栋（CTO）

| 维度 | 信息 |
|------|------|
| 教育背景 | 北京科技大学（本科）+ 北京大学计算机系（硕士） |
| 职业生涯 | 阿里巴巴→创业 |
| 核心能力 | 技术架构、系统工程 |

### 7.3 公司融资历程

| 时间 | 轮次 | 金额 | 投资方 |
|------|------|------|--------|
| 2025年6月 | 种子轮 | 未披露 | 奇绩创坛 |
| 2025年11月 | 天使轮 | 数百万美元 | 正轩投资（领投）、云沐资本、后浪资本 |

---

## 八、参考资料

1. **新智元** - 《裸辞北大博士带17个Agent，49天造出全球首个AI开放世界！成本仅5000》
2. **北京大学校友网** - 《文科生跨界AI！北大校友做了一款有"灵魂"的AI漫剧应用》
3. **什么值得买** - 《【AI短剧制作】工具"登顶"不是目标，优质内容才是毕生所求》
4. **51CTO** - 《北大博士带17个Agent，49天造出全球首个AI开放世界》
5. **CSDN** - 《Vibe Coding详解：Karpathy氛围编程的概念、原理、5层工作流结构》
6. **启信宝** - 上海心影次元人工智能科技有限公司工商信息

---

## 九、总结

Elseland 案例是 **Vibe Coding 方法论**的绝佳实践样本：

| 维度 | 成果 |
|------|------|
| **开发效率** | 传统管线的**百倍**提升 |
| **成本控制** | 仅**5000元**API费用 |
| **产出规模** | **30万行代码**的完整产品 |
| **时间周期** | **49天**完成通常需要团队数月的工作 |
| **协作规模** | **17个Agent**专业化分工协作 |

这个案例证明：**在AI时代，「理解事物的本质」和「清晰表达意图」比「掌握具体技能」更重要**。Vibe Coding正在重新定义软件开发的范式，让更多人有机会成为「超级个体」。

---

*报告生成时间：2026年4月*  
*数据来源：公开报道与网络搜索*


path: scripts/analyzer.py
content: """
AI前沿哨兵 - 信息分析评分模块
对采集的信息进行智能分析和评分
"""
import json
from datetime import datetime
from pathlib import Path


class InfoAnalyzer:
    """信息分析器"""
    
    # 来源权重配置
    SOURCE_WEIGHTS = {
        "arxiv": 1.5,       # 学术论文权重高
        "github": 1.3,      # 开源项目
        "hackernews": 1.2,  # 技术社区讨论
        "twitter": 1.0,     # 社交媒体
        "blogs": 1.4        # 官方博客
    }
    
    # 热度关键词
    HOT_KEYWORDS = [
        "breaking", "revolutionary", "breakthrough", "new model",
        "launch", "release", "announce", "official",
        "GPT-4", "Claude", "Gemini", "Llama", "Mistral",
        "Sora", "DALL-E", "Stable Diffusion"
    ]
    
    def __init__(self, settings=None):
        self.settings = settings or {}
        self.keywords = self.settings.get("keywords", [])
    
    def analyze(self, raw_data):
        """
        分析采集的原始数据
        
        Args:
            raw_data: 采集的原始数据，包含 sources 字段
        
        Returns:
            dict: 分析后的结构化数据
        """
        sources = raw_data.get("sources", {})
        
        # 合并所有项目
        all_items = []
        for source_name, source_data in sources.items():
            if source_data.get("status") != "success":
                continue
            
            items = source_data.get("items", [])
            for item in items:
                item["_source_name"] = source_name
                item["_weight"] = self.SOURCE_WEIGHTS.get(source_name, 1.0)
                all_items.append(item)
        
        # 计算综合评分
        scored_items = self._calculate_scores(all_items)
        
        # 按类型分类
        categorized = self._categorize(scored_items)
        
        # 提取关键发现
        insights = self._extract_insights(scored_items)
        
        return {
            "timestamp": raw_data.get("timestamp", datetime.now().isoformat()),
            "total_items": len(all_items),
            "scored_items": scored_items,
            "categorized": categorized,
            "insights": insights,
            "summary": self._generate_summary(categorized, insights)
        }
    
    def _calculate_scores(self, items):
        """
        计算每个项目的综合评分
        
        评分维度:
        1. 原始热度 (stars, score, likes等)
        2. 新鲜度 (越新分数越高)
        3. 来源权重
        4. 关键词匹配
        """
        from datetime import datetime, timedelta
        
        now = datetime.now()
        scored_items = []
        
        for item in items:
            score = 0
            factors = {}
            
            # 1. 热度分数
            if "stars" in item:
                score += min(item["stars"] / 100, 50)  # 最多50分
                factors["stars"] = item["stars"]
            if "score" in item:
                score += min(item["score"] / 10, 30)  # 最多30分
                factors["score"] = item["score"]
            if "likes" in item:
                score += min(item["likes"] / 50, 20)  # 最多20分
                factors["likes"] = item["likes"]
            
            # 2. 新鲜度分数
            time_score = self._calculate_freshness(item, now)
            score += time_score
            factors["freshness"] = time_score
            
            # 3. 来源权重
            weight = item.get("_weight", 1.0)
            score *= weight
            factors["weight"] = weight
            
            # 4. 关键词匹配加分
            keyword_bonus = self._calculate_keyword_bonus(item)
            score += keyword_bonus
            factors["keyword_bonus"] = keyword_bonus
            
            # 5. 热度关键词检测
            if self._contains_hot_keyword(item):
                score *= 1.5
                factors["hot_keyword"] = True
            
            # 存储评分结果
            item["_score"] = round(score, 2)
            item["_factors"] = factors
            scored_items.append(item)
        
        # 按分数排序
        scored_items.sort(key=lambda x: x["_score"], reverse=True)
        
        return scored_items
    
    def _calculate_freshness(self, item, now):
        """计算新鲜度分数"""
        from datetime import datetime
        
        # 尝试从各种字段获取时间
        time_fields = ["created_at", "published", "updated_at", "created", "time"]
        
        for field in time_fields:
            if field in item and item[field]:
                try:
                    time_str = item[field]
                    # 处理ISO格式
                    if "T" in time_str:
                        item_time = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
                    else:
                        item_time = datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S")
                    
                    # 计算时间差
                    diff = now - item_time
                    hours = diff.total_seconds() / 3600
                    
                    # 24小时内满分，48小时60%分，7天外10%分
                    if hours < 24:
                        return 30
                    elif hours < 48:
                        return 20
                    elif hours < 72:
                        return 15
                    elif hours < 168:  # 7天
                        return 10
                    else:
                        return 5
                        
                except:
                    continue
        
        return 10  # 默认中等分数
    
    def _calculate_keyword_bonus(self, item):
        """计算关键词匹配加分"""
        if not self.keywords:
            return 0
        
        text = f"{item.get('title', '')} {item.get('description', '')} {item.get('text', '')} {item.get('abstract', '')}"
        text = text.lower()
        
        matches = sum(1 for kw in self.keywords if kw.lower() in text)
        return matches * 5  # 每个匹配加5分
    
    def _contains_hot_keyword(self, item):
        """检测是否包含热度关键词"""
        text = f"{item.get('title', '')} {item.get('description', '')}"
        text = text.lower()
        
        for keyword in self.HOT_KEYWORDS:
            if keyword.lower() in text:
                return True
        
        return False
    
    def _categorize(self, items):
        """按类型分类（自动去重）"""
        categories = {
            "papers": [],      # 学术论文
            "projects": [],    # 开源项目
            "discussions": [], # 社区讨论
            "news": [],        # 新闻动态
            "tweets": []       # 社交动态
        }
        
        # 去重跟踪：使用URL作为唯一标识
        seen_urls = set()
        
        type_mapping = {
            "paper": "papers",
            "repository": "projects",
            "discussion": "discussions",
            "article": "news",
            "tweet": "tweets"
        }
        
        for item in items:
            source_type = item.get("source_type", "")
            category = type_mapping.get(source_type, "news")
            
            # 使用URL去重，如果没有URL则用title+source_type组合
            url = item.get("url") or item.get("hn_url") or item.get("pdf_url") or ""
            dedup_key = f"{url}:{item.get('title', '')}"
            
            if url and dedup_key in seen_urls:
                continue
            
            if url:
                seen_urls.add(dedup_key)
            
            categories[category].append(item)
        
        return categories
    
    def _extract_insights(self, items):
        """提取关键发现"""
        insights = {
            "hot_topics": [],
            "trending_projects": [],
            "important_papers": [],
            "must_read": []
        }
        
        # 热度话题
        title_words = {}
        for item in items[:30]:
            title = item.get("title", "")
            for word in title.split():
                if len(word) > 4:
                    title_words[word] = title_words.get(word, 0) + 1
        
        top_words = sorted(title_words.items(), key=lambda x: x[1], reverse=True)[:5]
        insights["hot_topics"] = [w[0] for w in top_words]
        
        # 热门项目 (Top 5 by score)
        insights["trending_projects"] = [
            {
                "title": item.get("title", ""),
                "url": item.get("url", ""),
                "score": item.get("_score", 0),
                "stars": item.get("stars", 0)
            }
            for item in items
            if item.get("source_type") == "repository"
        ][:5]
        
        # 重要论文
        insights["important_papers"] = [
            {
                "title": item.get("title", ""),
                "authors": item.get("author_str", ""),
                "url": item.get("url", ""),
                "score": item.get("_score", 0)
            }
            for item in items
            if item.get("source_type") == "paper"
        ][:5]
        
        # 必读内容 (Top 10)
        insights["must_read"] = items[:10]
        
        return insights
    
    def _generate_summary(self, categorized, insights):
        """生成摘要"""
        return {
            "papers_count": len(categorized["papers"]),
            "projects_count": len(categorized["projects"]),
            "discussions_count": len(categorized["discussions"]),
            "news_count": len(categorized["news"]),
            "tweets_count": len(categorized["tweets"]),
            "hot_topics": insights["hot_topics"],
            "top_score": insights["must_read"][0]["_score"] if insights["must_read"] else 0
        }


def load_raw_data(data_dir):
    """加载最新的原始数据"""
    data_dir = Path(data_dir)
    if not data_dir.exists():
        return None
    
    files = list(data_dir.glob("raw_data_*.json"))
    if not files:
        return None
    
    # 获取最新的文件
    latest = max(files, key=lambda f: f.stat().st_mtime)
    
    with open(latest, 'r', encoding='utf-8') as f:
        return json.load(f)


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="AI前沿哨兵 - 信息分析工具")
    parser.add_argument("--input", type=str, help="原始数据JSON文件路径")
    parser.add_argument("--output", type=str, help="分析结果输出路径")
    args = parser.parse_args()
    
    data_dir = Path(__file__).parent.parent / "output"
    
    # 加载原始数据
    if args.input:
        with open(args.input, 'r', encoding='utf-8') as f:
            raw_data = json.load(f)
    else:
        raw_data = load_raw_data(data_dir)
    
    if raw_data:
        analyzer = InfoAnalyzer({"keywords": ["AI", "LLM", "GPT"]})
        result = analyzer.analyze(raw_data)
        
        # 保存结果
        if args.output:
            output_path = Path(args.output)
        else:
            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
            output_path = data_dir / f"analyzed_{timestamp}.json"
        
        with open(output_path, 'w', encoding='utf-8') as f:
            json.dump(result, f, ensure_ascii=False, indent=2)
        
        print(f"✅ 分析完成，结果已保存到: {output_path}")
    else:
        print("❌ 未找到原始数据文件")


path: scripts/collect.py
content: """
AI前沿哨兵 - 主采集脚本
负责协调各信息源采集任务
"""
import argparse
import json
import os
import sys
import time
from datetime import datetime
from pathlib import Path

import yaml

# 添加scripts目录到路径
sys.path.insert(0, str(Path(__file__).parent))

from parsers.github_trending import fetch_github_trending
from parsers.arxiv import fetch_arxiv_papers
from parsers.hackernews import fetch_hackernews
from parsers.twitter_x import fetch_twitter
from parsers.blog_rss import fetch_blog_rss
from parsers.web_news import parse_web_news


class DataCollector:
    """数据采集器主类"""
    
    def __init__(self, config_dir=None):
        self.config_dir = config_dir or Path(__file__).parent.parent / "config"
        self.output_dir = Path(__file__).parent.parent / "output"
        self.output_dir.mkdir(exist_ok=True)
        
        # 加载配置
        self.sources = self._load_config("sources.yaml")
        self.settings = self._load_config("settings.yaml")
        self.schedule = self._load_config("schedule.yaml")
        
        # 采集结果存储
        self.results = {
            "timestamp": datetime.now().isoformat(),
            "sources": {}
        }
    
    def _load_config(self, filename):
        """加载配置文件"""
        config_path = self.config_dir / filename
        if config_path.exists():
            with open(config_path, 'r', encoding='utf-8') as f:
                return yaml.safe_load(f)
        return {}
    
    def _save_results(self):
        """保存采集结果"""
        output_file = self.output_dir / f"raw_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
        with open(output_file, 'w', encoding='utf-8') as f:
            json.dump(self.results, f, ensure_ascii=False, indent=2)
        return output_file
    
    def collect_all(self):
        """采集所有启用的信息源"""
        print(f"[{datetime.now().strftime('%H:%M:%S')}] 开始采集...")
        
        for source_name, source_config in self.sources.items():
            # 跳过列表类型的配置（由其他源处理）
            if isinstance(source_config, list):
                continue
            if not source_config.get("enabled", True):
                print(f"  ⏭️  跳过 {source_name} (已禁用)")
                continue
            
            try:
                self._collect_source(source_name, source_config)
            except Exception as e:
                print(f"  ❌ {source_name} 采集失败: {e}")
                self.results["sources"][source_name] = {
                    "status": "error",
                    "error": str(e),
                    "items": []
                }
        
        # 保存结果
        output_file = self._save_results()
        print(f"\n✅ 采集完成，结果已保存到: {output_file}")
        
        return self.results
    
    def collect_source(self, source_name):
        """采集指定信息源"""
        source_config = self.sources.get(source_name)
        if not source_config:
            print(f"❌ 未找到信息源: {source_name}")
            return None
        
        # 跳过列表类型的配置
        if isinstance(source_config, list):
            print(f"❌ {source_name} 是列表类型，无法单独采集")
            return None
        
        if not source_config.get("enabled", True):
            print(f"⏭️  信息源 {source_name} 已禁用")
            return None
        
        try:
            result = self._collect_source(source_name, source_config)
            output_file = self._save_results()
            print(f"✅ {source_name} 采集完成，结果已保存到: {output_file}")
            return result
        except Exception as e:
            print(f"❌ {source_name} 采集失败: {e}")
            return None
    
    def _collect_source(self, source_name, source_config):
        """内部方法：采集单个信息源"""
        print(f"  📡 正在采集 {source_name}...")
        
        start_time = time.time()
        items = []
        
        # 根据源类型调用对应的解析器
        if source_name == "github":
            items = fetch_github_trending(source_config)
        elif source_name == "arxiv":
            items = fetch_arxiv_papers(source_config)
        elif source_name == "hackernews":
            items = fetch_hackernews(source_config)
        elif source_name == "twitter":
            items = fetch_twitter(source_config, self.settings.get("twitter", {}))
        elif source_name == "blogs":
            items = fetch_blog_rss(source_config)
        elif source_name == "chinese_ai_companies":
            items = parse_web_news({"chinese_ai_companies": source_config})
        elif source_name == "custom_feeds":
            # 自定义RSS源，复用blog_rss解析器
            items = fetch_blog_rss({"enabled": True, "limit_per_feed": 10, "feeds": source_config if isinstance(source_config, list) else [source_config]})
        elif source_name == "rsshub":
            # RSSHub实例，暂不处理（需要特殊配置）
            print(f"    ⏭️  RSSHub实例需要单独配置代理")
            return []
        else:
            print(f"    ⚠️  未知的源类型: {source_name}")
            return []
        
        elapsed = time.time() - start_time
        print(f"    ✅ 获取 {len(items)} 条记录，耗时 {elapsed:.2f}s")
        
        self.results["sources"][source_name] = {
            "status": "success",
            "count": len(items),
            "items": items
        }
        
        return items
    
    def get_status(self):
        """获取采集状态"""
        status = {
            "timestamp": datetime.now().isoformat(),
            "sources": {}
        }
        
        for source_name, source_config in self.sources.items():
            last_result = self.results.get("sources", {}).get(source_name, {})
            status["sources"][source_name] = {
                "enabled": source_config.get("enabled", True),
                "status": last_result.get("status", "unknown"),
                "count": last_result.get("count", 0)
            }
        
        return status


def main():
    parser = argparse.ArgumentParser(description="AI前沿哨兵 - 信息采集工具")
    parser.add_argument("--all", action="store_true", help="采集所有信息源")
    parser.add_argument("--source", type=str, help="指定采集的信息源")
    parser.add_argument("--status", action="store_true", help="显示采集状态")
    
    args = parser.parse_args()
    
    collector = DataCollector()
    
    if args.all:
        collector.collect_all()
    elif args.source:
        collector.collect_source(args.source)
    elif args.status:
        status = collector.get_status()
        print(json.dumps(status, ensure_ascii=False, indent=2))
    else:
        parser.print_help()
        print("\n示例:")
        print("  python collect.py --all          # 采集所有信息源")
        print("  python collect.py --source github  # 仅采集GitHub")
        print("  python collect.py --status      # 查看采集状态")


if __name__ == "__main__":
    main()


path: scripts/parsers/__init__.py
content: # Parsers模块初始化
from .github_trending import fetch_github_trending
from .arxiv import fetch_arxiv_papers
from .hackernews import fetch_hackernews
from .twitter_x import fetch_twitter
from .blog_rss import fetch_blog_rss

__all__ = [
    'fetch_github_trending',
    'fetch_arxiv_papers', 
    'fetch_hackernews',
    'fetch_twitter',
    'fetch_blog_rss'
]


path: scripts/parsers/arxiv.py
content: """
arXiv 论文解析器
获取AI/ML领域最新学术论文
"""
import requests
import xml.etree.ElementTree as ET
from datetime import datetime
from urllib.parse import urlencode


def fetch_arxiv_papers(config):
    """
    获取arXiv最新论文
    
    Args:
        config: 信息源配置，包含:
            - categories: 论文类别列表 (默认: cs.AI, cs.LG, cs.CL)
            - max_results: 返回数量 (默认: 20)
            - search_query: 自定义搜索词
    
    Returns:
        list: 论文列表，每项包含 title, authors, abstract, url, published
    """
    categories = config.get("categories", ["cs.AI", "cs.LG", "cs.CL"])
    max_results = config.get("max_results", 20)
    search_query = config.get("search_query", "")
    
    # 构建搜索查询
    # 基础查询：AI相关类别
    base_query = "+OR+".join([f"cat:{cat}" for cat in categories])
    
    # 添加自定义搜索词
    if search_query:
        full_query = f"({base_query})+AND+({search_query})"
    else:
        full_query = base_query
    
    # arXiv API URL
    base_url = "http://export.arxiv.org/api/query"
    params = {
        "search_query": full_query,
        "sortBy": "submittedDate",
        "sortOrder": "descending",
        "start": 0,
        "max_results": min(max_results, 100)
    }
    
    url = f"{base_url}?{urlencode(params)}"
    
    try:
        response = requests.get(url, timeout=60)
        response.raise_for_status()
        
        return _parse_arxiv_response(response.text)
        
    except requests.exceptions.RequestException as e:
        print(f"arXiv API请求失败: {e}")
        return []


def _parse_arxiv_response(xml_content):
    """
    解析arXiv API返回的XML内容
    
    Args:
        xml_content: XML格式的API响应
    
    Returns:
        list: 解析后的论文列表
    """
    items = []
    
    try:
        root = ET.fromstring(xml_content)
        namespaces = {
            "atom": "http://www.w3.org/2005/Atom",
            "arxiv": "http://arxiv.org/schemas/atom"
        }
        
        for entry in root.findall("atom:entry", namespaces):
            # 提取基本信息
            title = entry.find("atom:title", namespaces)
            title = title.text.strip().replace("\n", " ") if title is not None else "无标题"
            
            summary = entry.find("atom:summary", namespaces)
            abstract = summary.text.strip().replace("\n", " ") if summary is not None else ""
            
            # 截取摘要前300字符
            abstract_short = abstract[:300] + "..." if len(abstract) > 300 else abstract
            
            authors = []
            for author in entry.findall("atom:author", namespaces):
                name = author.find("atom:name", namespaces)
                if name is not None:
                    authors.append(name.text)
            
            # 获取链接
            links = {}
            for link in entry.findall("atom:link", namespaces):
                href = link.get("href", "")
                link_type = link.get("type", "")
                if "pdf" in link_type:
                    links["pdf"] = href
                elif link.get("rel") == "alternate":
                    links["html"] = href
            
            # 获取发布日期
            published = entry.find("atom:published", namespaces)
            published = published.text if published is not None else ""
            
            # 获取类别
            categories = []
            for cat in entry.findall("atom:category", namespaces):
                term = cat.get("term", "")
                if term:
                    categories.append(term)
            
            items.append({
                "title": title,
                "authors": authors,
                "author_str": ", ".join(authors[:3]) + (" et al." if len(authors) > 3 else ""),
                "abstract": abstract_short,
                "abstract_full": abstract,
                "url": links.get("html", links.get("pdf", "")),
                "pdf_url": links.get("pdf", ""),
                "published": published,
                "categories": categories,
                "primary_category": categories[0] if categories else "Unknown",
                "source": "arxiv",
                "source_type": "paper"
            })
            
    except ET.ParseError as e:
        print(f"XML解析失败: {e}")
    
    return items


def search_arxiv(query, categories=None, max_results=10):
    """
    搜索arXiv论文
    
    Args:
        query: 搜索关键词
        categories: 论文类别列表
        max_results: 返回数量
    
    Returns:
        list: 论文列表
    """
    if categories:
        cat_query = "+OR+".join([f"cat:{cat}" for cat in categories])
        full_query = f"({query})+AND+({cat_query})"
    else:
        full_query = f"all:{query}"
    
    base_url = "http://export.arxiv.org/api/query"
    params = {
        "search_query": full_query,
        "sortBy": "relevance",
        "sortOrder": "descending",
        "start": 0,
        "max_results": min(max_results, 100)
    }
    
    url = f"{base_url}?{urlencode(params)}"
    
    try:
        response = requests.get(url, timeout=60)
        response.raise_for_status()
        return _parse_arxiv_response(response.text)
    except requests.exceptions.RequestException:
        return []


if __name__ == "__main__":
    # 测试代码
    config = {
        "categories": ["cs.AI", "cs.LG"],
        "max_results": 5
    }
    results = fetch_arxiv_papers(config)
    print(f"获取到 {len(results)} 篇论文:")
    for item in results:
        print(f"  - {item['title'][:50]}...")


path: scripts/parsers/blog_rss.py
content: """
博客RSS解析器
支持任意RSS/Atom订阅源
"""
import feedparser
import requests
from datetime import datetime
from urllib.parse import urlparse


def fetch_blog_rss(config):
    """
    获取RSS订阅源的最新内容
    
    Args:
        config: 信息源配置，包含:
            - feeds: RSS源列表，每个源包含:
                - name: 源名称
                - url: RSS地址
                - type: rss/atom (可选，自动检测)
            - limit: 每个源返回数量 (默认: 5)
    
    Returns:
        list: 文章列表
    """
    feeds = config.get("feeds", [])
    limit_per_feed = config.get("limit_per_feed", 5)
    
    items = []
    
    for feed_config in feeds:
        name = feed_config.get("name", "Unknown")
        url = feed_config.get("url", "")
        
        if not url:
            continue
        
        feed_items = _fetch_single_feed(name, url, limit_per_feed)
        items.extend(feed_items)
    
    # 按发布日期排序
    items.sort(key=lambda x: x.get("published_parsed") or (0,) * 9, reverse=True)
    
    return items


def _fetch_single_feed(name, url, limit=5, max_retries=2):
    """
    获取单个RSS源的内容
    
    Args:
        name: 源名称
        url: RSS地址
        limit: 返回数量
        max_retries: 最大重试次数
    
    Returns:
        list: 文章列表
    """
    # 设置请求头，模拟浏览器访问
    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": "application/rss+xml, application/xml, text/xml, application/atom+xml, */*"
    }
    
    last_error = None
    
    for attempt in range(max_retries + 1):
        try:
            # 先尝试直接请求，增加超时设置
            response = requests.get(url, headers=headers, timeout=10)
            response.raise_for_status()
            
            # 使用feedparser解析
            feed = feedparser.parse(response.text)
            
            items = []
            for entry in feed.entries[:limit]:
                item = _parse_feed_entry(entry, name, url)
                if item:
                    items.append(item)
            
            return items
            
        except requests.exceptions.Timeout:
            last_error = f"超时 (10秒)"
            if attempt < max_retries:
                print(f"RSS请求超时 ({name}), 正在重试 ({attempt + 1}/{max_retries})...")
        except requests.exceptions.ConnectionError as e:
            last_error = f"连接失败"
            if attempt < max_retries:
                print(f"RSS连接失败 ({name}), 正在重试 ({attempt + 1}/{max_retries})...")
        except requests.exceptions.RequestException as e:
            last_error = str(e)
            if attempt < max_retries:
                print(f"RSS请求失败 ({name}): {e}, 正在重试 ({attempt + 1}/{max_retries})...")
            break  # 其他请求错误不重试
        except Exception as e:
            print(f"RSS解析失败 ({name}): {e}")
            return []
    
    # 所有重试都失败后
    print(f"RSS请求最终失败 ({name}): {last_error}")
    return []


def _parse_feed_entry(entry, source_name, source_url):
    """
    解析单个RSS条目
    
    Args:
        entry: feedparser的条目对象
        source_name: 源名称
        source_url: 源URL
    
    Returns:
        dict: 解析后的文章信息
    """
    # 获取标题
    title = getattr(entry, "title", "无标题") or "无标题"
    
    # 获取链接
    link = getattr(entry, "link", "") or ""
    
    # 获取摘要/内容
    summary = getattr(entry, "summary", "") or getattr(entry, "description", "") or ""
    # 清理HTML标签
    summary = _strip_html(summary)
    summary = summary[:300] + "..." if len(summary) > 300 else summary
    
    # 获取发布日期
    published = getattr(entry, "published", "") or getattr(entry, "updated", "") or ""
    published_parsed = getattr(entry, "published_parsed", None)
    
    # 格式化日期
    if published_parsed:
        try:
            pub_date = datetime(*published_parsed[:6])
            published = pub_date.strftime("%Y-%m-%d %H:%M")
        except:
            pass
    
    # 获取作者
    author = getattr(entry, "author", "") or ""
    if not author and hasattr(entry, "authors") and entry.authors:
        author = entry.authors[0].get("name", "")
    
    # 获取分类
    categories = getattr(entry, "tags", [])
    if categories:
        categories = [tag.term for tag in categories]
    
    return {
        "title": title.strip(),
        "url": link,
        "summary": summary,
        "published": published,
        "published_parsed": published_parsed,
        "author": author,
        "categories": categories,
        "source": source_name,
        "source_url": source_url,
        "source_type": "article"
    }


def _strip_html(text):
    """移除HTML标签"""
    import re
    # 移除script和style标签及其内容
    text = re.sub(r'<script[^>]*>.*?</script>', '', text, flags=re.DOTALL | re.IGNORECASE)
    text = re.sub(r'<style[^>]*>.*?</style>', '', text, flags=re.DOTALL | re.IGNORECASE)
    # 移除所有HTML标签
    text = re.sub(r'<[^>]+>', '', text)
    # 清理多余空白
    text = re.sub(r'\s+', ' ', text)
    return text.strip()


# 预设的AI博客RSS源
DEFAULT_BLOGS = [
    {
        "name": "Anthropic News",
        "url": "https://www.anthropic.com/news/rss",
        "category": "AI Company"
    },
    {
        "name": "OpenAI Blog",
        "url": "https://openai.com/blog/rss.xml",
        "category": "AI Company"
    },
    {
        "name": "Google AI Blog",
        "url": "https://blog.google/technology/ai/rss/",
        "category": "AI Company"
    },
    {
        "name": "DeepMind Blog",
        "url": "https://deepmind.com/blog/feed/basic/",
        "category": "AI Company"
    },
    {
        "name": "Meta AI Blog",
        "url": "https://ai.meta.com/blog/rss/",
        "category": "AI Company"
    },
    {
        "name": "Microsoft AI Blog",
        "url": "https://blogs.microsoft.com/ai/feed/",
        "category": "AI Company"
    },
    {
        "name": "Hugging Face Blog",
        "url": "https://huggingface.co/blog/feed.xml",
        "category": "AI Company"
    },
]


def validate_rss_url(url):
    """
    验证RSS URL是否有效
    
    Args:
        url: RSS地址
    
    Returns:
        dict: 验证结果，包含 status, title, item_count
    """
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    }
    
    try:
        response = requests.get(url, headers=headers, timeout=30)
        response.raise_for_status()
        
        feed = feedparser.parse(response.text)
        
        return {
            "status": "valid",
            "title": feed.feed.get("title", "Unknown"),
            "item_count": len(feed.entries),
            "description": feed.feed.get("description", "")
        }
        
    except Exception as e:
        return {
            "status": "invalid",
            "error": str(e)
        }


if __name__ == "__main__":
    # 测试代码
    test_feeds = [
        {"name": "Test", "url": "https://www.anthropic.com/news/rss"}
    ]
    
    config = {"feeds": test_feeds, "limit_per_feed": 3}
    results = fetch_blog_rss(config)
    
    print(f"获取到 {len(results)} 篇文章:")
    for item in results:
        print(f"  - [{item['source']}] {item['title'][:40]}...")


path: scripts/parsers/github_trending.py
content: """
GitHub Trending 解析器
获取AI领域热门开源项目
"""
import requests
from datetime import datetime, timedelta


def fetch_github_trending(config):
    """
    获取GitHub Trending项目
    
    Args:
        config: 信息源配置，包含:
            - language: 编程语言 (默认: python)
            - date_range: 时间范围 (daily/weekly/monthly)，用于筛选新项目
            - limit: 返回数量 (默认: 20)
            - keywords: 关键词过滤列表
    
    Returns:
        list: 项目列表，每项包含 title, description, url, stars, forks, language
    """
    language = config.get("language", "python")
    date_range = config.get("date_range", "weekly")
    limit = config.get("limit", 20)
    keywords = config.get("keywords", [])
    
    # GitHub API请求
    # 使用搜索API按star排序获取热门项目
    # 构建查询：按语言获取热门项目
    query = f"language:{language}"
    
    # 如果设置了关键词，添加关键词过滤
    if keywords:
        keyword_query = " OR ".join(keywords)
        query = f"({query}) AND ({keyword_query})"
    
    # 设置时间参数（只在需要筛选新项目时使用）
    date_filter = ""
    if date_range:
        if date_range == "daily":
            since = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
        elif date_range == "weekly":
            since = (datetime.now() - timedelta(days=7)).strftime("%Y-%m-%d")
        else:
            since = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
        date_filter = f" created:>{since}"
    
    url = "https://api.github.com/search/repositories"
    params = {
        "q": query + date_filter,
        "sort": "stars",
        "order": "desc",
        "per_page": min(limit, 100)
    }
    
    headers = {
        "Accept": "application/vnd.github.v3+json",
        "User-Agent": "AI-Sentinel/1.0"
    }
    
    try:
        response = requests.get(url, params=params, headers=headers, timeout=30)
        response.raise_for_status()
        data = response.json()
        
        items = []
        for repo in data.get("items", [])[:limit]:
            # 如果没有设置关键词过滤但有结果，返回
            items.append({
                "title": repo.get("full_name", ""),
                "description": repo.get("description", "") or "无描述",
                "url": repo.get("html_url", ""),
                "stars": repo.get("stargazers_count", 0),
                "forks": repo.get("forks_count", 0),
                "language": repo.get("language", "Unknown"),
                "author": repo.get("owner", {}).get("login", ""),
                "author_url": repo.get("owner", {}).get("html_url", ""),
                "created_at": repo.get("created_at", ""),
                "updated_at": repo.get("updated_at", ""),
                "source": "github",
                "source_type": "repository"
            })
        
        # 按URL去重，确保每个项目只出现一次
        seen_urls = set()
        unique_items = []
        for item in items:
            if item["url"] not in seen_urls:
                seen_urls.add(item["url"])
                unique_items.append(item)
        
        return unique_items
        
    except requests.exceptions.RequestException as e:
        print(f"GitHub API请求失败: {e}")
        # 尝试使用备用方案：直接爬取Trending页面
        return _fetch_github_trending_fallback(limit)


def _fetch_github_trending_fallback(limit=20):
    """
    备用方案：直接爬取GitHub Trending页面
    当API受限或失败时使用
    """
    url = "https://github.com/trending/python"
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    }
    
    try:
        response = requests.get(url, headers=headers, timeout=30)
        response.raise_for_status()
        
        # 简单的HTML解析（实际项目中建议使用BeautifulSoup）
        items = []
        # 这里简化处理，实际需要解析HTML
        return items
        
    except requests.exceptions.RequestException:
        return []


def search_github_repos(query, language=None, sort="stars", limit=10):
    """
    搜索GitHub仓库
    
    Args:
        query: 搜索关键词
        language: 编程语言过滤
        sort: 排序方式 (stars/forks/updated)
        limit: 返回数量
    
    Returns:
        list: 仓库列表
    """
    url = "https://api.github.com/search/repositories"
    q = query
    if language:
        q += f"+language:{language}"
    
    params = {
        "q": q,
        "sort": sort,
        "order": "desc",
        "per_page": min(limit, 100)
    }
    
    headers = {
        "Accept": "application/vnd.github.v3+json",
        "User-Agent": "AI-Sentinel/1.0"
    }
    
    try:
        response = requests.get(url, params=params, headers=headers, timeout=30)
        response.raise_for_status()
        data = response.json()
        
        return [{
            "title": repo.get("full_name"),
            "description": repo.get("description"),
            "url": repo.get("html_url"),
            "stars": repo.get("stargazers_count"),
            "language": repo.get("language"),
            "source": "github",
            "source_type": "search"
        } for repo in data.get("items", [])[:limit]]
        
    except requests.exceptions.RequestException:
        return []


if __name__ == "__main__":
    # 测试代码
    config = {
        "language": "python",
        "date_range": "daily",
        "limit": 10
    }
    results = fetch_github_trending(config)
    print(f"获取到 {len(results)} 个项目:")
    for item in results:
        print(f"  - {item['title']} ⭐{item['stars']}")


path: scripts/parsers/hackernews.py
content: """
HackerNews 解析器
获取热门讨论和链接
"""
import requests
from datetime import datetime


# HackerNews Firebase API基础URL
HN_API_BASE = "https://hacker-news.firebaseio.com/v0"


def fetch_hackernews(config):
    """
    获取HackerNews热门内容
    
    Args:
        config: 信息源配置，包含:
            - item_type: 获取类型 (top/new/best/ask/show)
            - limit: 返回数量 (默认: 20)
            - keywords: 关键词过滤，用于筛选AI相关内容
    
    Returns:
        list: 项目列表，每项包含 title, url, score, comments, author, time
    """
    item_type = config.get("item_type", "top")
    limit = config.get("limit", 20)
    keywords = config.get("keywords", ["AI", "ML", "machine learning", "neural", "LLM", "GPT", "deep learning"])
    
    # 获取对应的故事ID列表
    endpoint_map = {
        "top": "topstories",
        "new": "newstories",
        "best": "beststories",
        "ask": "askstories",
        "show": "showstories"
    }
    
    endpoint = endpoint_map.get(item_type, "topstories")
    url = f"{HN_API_BASE}/{endpoint}.json"
    
    try:
        response = requests.get(url, timeout=30)
        response.raise_for_status()
        story_ids = response.json()
        
        # 获取每个故事的详细信息
        items = []
        count = 0
        for story_id in story_ids[:limit * 3]:  # 预取更多以便过滤
            if count >= limit:
                break
            
            item = _fetch_hn_item(story_id)
            if item and _filter_by_keywords(item, keywords):
                items.append(item)
                count += 1
        
        return items
        
    except requests.exceptions.RequestException as e:
        print(f"HackerNews API请求失败: {e}")
        return []


def _fetch_hn_item(item_id):
    """
    获取单个HackerNews项目详情
    
    Args:
        item_id: 项目ID
    
    Returns:
        dict: 项目信息
    """
    url = f"{HN_API_BASE}/item/{item_id}.json"
    
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()
        item = response.json()
        
        if not item or item.get("type") != "story":
            return None
        
        # 格式化时间
        timestamp = item.get("time", 0)
        time_str = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M") if timestamp else ""
        
        return {
            "title": item.get("title", "无标题"),
            "url": item.get("url", ""),
            "hn_url": f"https://news.ycombinator.com/item?id={item_id}",
            "score": item.get("score", 0),
            "comments": item.get("descendants", 0),
            "author": item.get("by", "anonymous"),
            "author_url": f"https://news.ycombinator.com/user?id={item.get('by', 'anonymous')}",
            "time": time_str,
            "timestamp": timestamp,
            "type": item.get("type", "story"),
            "text": item.get("text", ""),  # 用于Ask HN等
            "source": "hackernews",
            "source_type": "discussion"
        }
        
    except requests.exceptions.RequestException:
        return None


def _filter_by_keywords(item, keywords):
    """
    根据关键词过滤项目
    
    Args:
        item: 项目信息
        keywords: 关键词列表
    
    Returns:
        bool: 是否匹配
    """
    if not keywords:
        return True
    
    title = item.get("title", "").lower()
    url = item.get("url", "").lower()
    text = item.get("text", "").lower()
    
    content = f"{title} {url} {text}"
    
    for keyword in keywords:
        if keyword.lower() in content:
            return True
    
    return False


def fetch_hn_comments(story_id, limit=10):
    """
    获取故事的评论
    
    Args:
        story_id: 故事ID
        limit: 评论数量限制
    
    Returns:
        list: 评论列表
    """
    url = f"{HN_API_BASE}/item/{story_id}.json"
    
    try:
        response = requests.get(url, timeout=30)
        response.raise_for_status()
        story = response.json()
        
        comments = []
        for kid in story.get("kids", [])[:limit]:
            comment = _fetch_hn_item(kid)
            if comment:
                comments.append(comment)
        
        return comments
        
    except requests.exceptions.RequestException:
        return []


if __name__ == "__main__":
    # 测试代码
    config = {
        "item_type": "top",
        "limit": 5,
        "keywords": ["AI", "ML", "GPT"]
    }
    results = fetch_hackernews(config)
    print(f"获取到 {len(results)} 条讨论:")
    for item in results:
        print(f"  - {item['title'][:50]}... ({item['score']} points)")


path: scripts/parsers/twitter_x.py
content: """
Twitter/X 解析器
获取AI大咖和机构的最新动态

注意: Twitter API v2 需要 Bearer Token
如果没有配置API，将返回空列表
"""
import requests
from datetime import datetime, timedelta


def fetch_twitter(config, api_settings):
    """
    获取Twitter/X动态
    
    Args:
        config: 信息源配置，包含:
            - accounts: 关注的账号列表
            - keywords: 关键词搜索
            - limit: 返回数量 (默认: 20)
        api_settings: API设置，包含:
            - bearer_token: Twitter API Bearer Token
    
    Returns:
        list: 推文列表
    """
    bearer_token = api_settings.get("bearer_token", "")
    
    if not bearer_token:
        print("⚠️  未配置Twitter Bearer Token，跳过Twitter采集")
        print("   请参考 references/api_setup.md 配置Twitter API")
        return []
    
    accounts = config.get("accounts", [])
    keywords = config.get("keywords", ["AI", "machine learning", "LLM", "GPT", "Claude"])
    limit = config.get("limit", 20)
    
    items = []
    
    # 1. 获取指定账号的推文
    if accounts:
        for account in accounts[:5]:  # 限制账号数量避免API限制
            tweets = _fetch_user_tweets(account, bearer_token, limit // len(accounts) if accounts else 10)
            items.extend(tweets)
    
    # 2. 搜索关键词
    if keywords:
        for keyword in keywords[:3]:  # 限制关键词数量
            tweets = _search_tweets(keyword, bearer_token, limit // 3)
            items.extend(tweets)
    
    # 去重
    seen_ids = set()
    unique_items = []
    for item in items:
        if item.get("id") not in seen_ids:
            seen_ids.add(item.get("id"))
            unique_items.append(item)
    
    # 按时间排序
    unique_items.sort(key=lambda x: x.get("created_at", ""), reverse=True)
    
    return unique_items[:limit]


def _fetch_user_tweets(username, bearer_token, limit=10):
    """
    获取指定用户的推文
    
    Args:
        username: 用户名
        bearer_token: API密钥
        limit: 数量限制
    
    Returns:
        list: 推文列表
    """
    # Twitter API v2 endpoint
    url = f"https://api.twitter.com/2/users/by/username/{username}"
    headers = {
        "Authorization": f"Bearer {bearer_token}",
        "User-Agent": "AI-Sentinel/1.0"
    }
    
    try:
        # 先获取用户ID
        response = requests.get(url, headers=headers, timeout=30)
        response.raise_for_status()
        user_data = response.json()
        
        user_id = user_data.get("data", {}).get("id")
        if not user_id:
            return []
        
        # 获取用户推文
        tweets_url = f"https://api.twitter.com/2/users/{user_id}/tweets"
        params = {
            "max_results": min(limit, 100),
            "tweet.fields": "created_at,public_metrics,lang",
            "expansions": "author_id",
            "user.fields": "name,username,profile_image_url"
        }
        
        tweets_response = requests.get(tweets_url, params=params, headers=headers, timeout=30)
        tweets_response.raise_for_status()
        tweets_data = tweets_response.json()
        
        items = []
        includes = tweets_data.get("includes", {})
        users = {u["id"]: u for u in include.get("users", [])}
        
        for tweet in tweets_data.get("data", []):
            author = users.get(tweet.get("author_id", {}), {})
            metrics = tweet.get("public_metrics", {})
            
            items.append({
                "id": tweet.get("id"),
                "text": tweet.get("text", ""),
                "author_name": author.get("name", username),
                "author_username": author.get("username", username),
                "author_url": f"https://twitter.com/{author.get('username', username)}",
                "author_image": author.get("profile_image_url", ""),
                "created_at": tweet.get("created_at", ""),
                "likes": metrics.get("like_count", 0),
                "retweets": metrics.get("retweet_count", 0),
                "replies": metrics.get("reply_count", 0),
                "url": f"https://twitter.com/{author.get('username', username)}/status/{tweet.get('id')}",
                "source": "twitter",
                "source_type": "tweet"
            })
        
        return items
        
    except requests.exceptions.RequestException as e:
        print(f"Twitter API请求失败 ({username}): {e}")
        return []


def _search_tweets(query, bearer_token, limit=10):
    """
    搜索推文
    
    Args:
        query: 搜索关键词
        bearer_token: API密钥
        limit: 数量限制
    
    Returns:
        list: 推文列表
    """
    url = "https://api.twitter.com/2/tweets/search/recent"
    headers = {
        "Authorization": f"Bearer {bearer_token}",
        "User-Agent": "AI-Sentinel/1.0"
    }
    params = {
        "query": query,
        "max_results": min(limit, 100),
        "tweet.fields": "created_at,public_metrics,lang,author_id",
        "expansions": "author_id",
        "user.fields": "name,username,profile_image_url"
    }
    
    try:
        response = requests.get(url, params=params, headers=headers, timeout=30)
        response.raise_for_status()
        data = response.json()
        
        items = []
        includes = data.get("includes", {})
        users = {u["id"]: u for u in includes.get("users", [])}
        
        for tweet in data.get("data", []):
            author = users.get(tweet.get("author_id", {}), {})
            metrics = tweet.get("public_metrics", {})
            
            items.append({
                "id": tweet.get("id"),
                "text": tweet.get("text", ""),
                "author_name": author.get("name", "Unknown"),
                "author_username": author.get("username", "unknown"),
                "author_url": f"https://twitter.com/{author.get('username', 'unknown')}",
                "author_image": author.get("profile_image_url", ""),
                "created_at": tweet.get("created_at", ""),
                "likes": metrics.get("like_count", 0),
                "retweets": metrics.get("retweet_count", 0),
                "replies": metrics.get("reply_count", 0),
                "url": f"https://twitter.com/{author.get('username', 'unknown')}/status/{tweet.get('id')}",
                "query": query,  # 记录匹配的搜索词
                "source": "twitter",
                "source_type": "tweet"
            })
        
        return items
        
    except requests.exceptions.RequestException as e:
        print(f"Twitter搜索失败 ({query}): {e}")
        return []


# 推荐的AI领域大咖账号
DEFAULT_AI_ACCOUNTS = [
    "ylecun",        # Yann LeCun (Meta AI)
    "AndrewYNg",     # Andrew Ng
    "sama",          # Sam Altman (OpenAI CEO)
    "gaborcselle",   # 每日AI新闻
    "binduredy",     # Google AI
    "JeffDrewTech",  # Tech新闻
    "emaboros",      # AI研究
    "ylecun",        # Meta AI
    "kaborl",        # AI新闻
]


if __name__ == "__main__":
    # 测试代码 - 需要有效的Bearer Token
    print("Twitter解析器需要配置Bearer Token")
    print("请参考 references/api_setup.md 进行配置")


path: scripts/parsers/web_news.py
content: #!/usr/bin/env python3
"""
网页新闻解析器
用于解析中国AI公司官网的新闻页面（无RSS的情况）
支持：智谱AI、MiniMax、扣子Coze等
"""

import requests
from bs4 import BeautifulSoup
from datetime import datetime
from typing import List, Dict, Optional
from dataclasses import dataclass
import logging
import time
import re

logger = logging.getLogger(__name__)


@dataclass
class WebNewsItem:
    """网页新闻条目"""
    title: str
    url: str
    source: str
    company: str
    summary: str = ""
    published_date: Optional[datetime] = None
    category: str = ""
    
    def to_dict(self) -> Dict:
        """转换为字典格式"""
        return {
            'title': self.title,
            'url': self.url,
            'source': self.source,
            'company': self.company,
            'summary': self.summary,
            'published_date': self.published_date.isoformat() if self.published_date else None,
            'category': self.category,
            'source_type': 'web_news'
        }


class WebNewsParser:
    """网页新闻解析器"""
    
    # 请求配置
    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,image/webp,*/*;q=0.8',
        'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
    }
    TIMEOUT = 15
    RETRY_COUNT = 2
    
    def __init__(self):
        self.session = requests.Session()
        self.session.headers.update(self.HEADERS)
    
    def fetch_page(self, url: str) -> Optional[str]:
        """获取网页内容"""
        for attempt in range(self.RETRY_COUNT):
            try:
                response = self.session.get(url, timeout=self.TIMEOUT)
                response.raise_for_status()
                response.encoding = response.apparent_encoding or 'utf-8'
                return response.text
            except Exception as e:
                logger.warning(f"获取页面失败 (尝试 {attempt+1}/{self.RETRY_COUNT}): {url}, 错误: {e}")
                if attempt < self.RETRY_COUNT - 1:
                    time.sleep(2)
        return None
    
    def parse_zhipu(self, html: str, base_url: str) -> List[WebNewsItem]:
        """解析智谱AI新品发布页面"""
        items = []
        soup = BeautifulSoup(html, 'html.parser')
        
        # 智谱文档页面结构：查找文章列表
        # 通常包含标题、日期等信息
        articles = soup.find_all(['article', 'div'], class_=re.compile(r'(article|post|news|item|release)', re.I))
        
        for article in articles[:10]:  # 限制10条
            try:
                # 尝试提取标题
                title_elem = article.find(['h1', 'h2', 'h3', 'h4', 'a'], class_=re.compile(r'(title|heading)', re.I))
                if not title_elem:
                    title_elem = article.find(['h1', 'h2', 'h3', 'h4'])
                
                if not title_elem:
                    continue
                    
                title = title_elem.get_text(strip=True)
                if not title or len(title) < 5:
                    continue
                
                # 提取链接
                link = article.find('a', href=True)
                url = link['href'] if link else ''
                if url and not url.startswith('http'):
                    url = base_url.rstrip('/') + '/' + url.lstrip('/')
                
                # 提取日期
                date_elem = article.find(['time', 'span', 'div'], class_=re.compile(r'(date|time)', re.I))
                published_date = None
                if date_elem:
                    date_str = date_elem.get_text(strip=True)
                    published_date = self._parse_date(date_str)
                
                # 提取摘要
                summary_elem = article.find(['p', 'div'], class_=re.compile(r'(summary|description|excerpt)', re.I))
                summary = summary_elem.get_text(strip=True)[:200] if summary_elem else ""
                
                items.append(WebNewsItem(
                    title=title,
                    url=url,
                    source="智谱AI新品发布",
                    company="ZhipuAI (智谱)",
                    summary=summary,
                    published_date=published_date,
                    category="AI Company"
                ))
                
            except Exception as e:
                logger.debug(f"解析智谱文章失败: {e}")
                continue
        
        # 如果没找到标准结构，尝试通用解析
        if not items:
            items = self._generic_parse(html, base_url, "智谱AI新品发布", "ZhipuAI (智谱)")
        
        return items
    
    def parse_minimax(self, html: str, base_url: str) -> List[WebNewsItem]:
        """解析MiniMax新闻页面"""
        items = []
        soup = BeautifulSoup(html, 'html.parser')
        
        # MiniMax新闻页面可能是动态渲染的，这里尝试解析HTML结构
        # 查找新闻卡片或列表项
        news_cards = soup.find_all(['div', 'article', 'li'], 
                                    class_=re.compile(r'(news|article|card|item|post)', re.I))
        
        for card in news_cards[:10]:
            try:
                # 提取标题
                title_elem = card.find(['h1', 'h2', 'h3', 'h4', 'a', 'span'])
                if not title_elem:
                    continue
                    
                title = title_elem.get_text(strip=True)
                if not title or len(title) < 5:
                    continue
                
                # 提取链接
                link = card.find('a', href=True)
                url = link['href'] if link else ''
                if url and not url.startswith('http'):
                    url = base_url.rstrip('/') + '/' + url.lstrip('/')
                
                # 提取日期
                date_elem = card.find(['time', 'span'], class_=re.compile(r'(date|time)', re.I))
                published_date = None
                if date_elem:
                    date_str = date_elem.get_text(strip=True)
                    published_date = self._parse_date(date_str)
                
                items.append(WebNewsItem(
                    title=title,
                    url=url or base_url,
                    source="MiniMax新闻动态",
                    company="MiniMax",
                    summary="",
                    published_date=published_date,
                    category="AI Company"
                ))
                
            except Exception as e:
                logger.debug(f"解析MiniMax文章失败: {e}")
                continue
        
        # 如果没找到标准结构，尝试通用解析
        if not items:
            items = self._generic_parse(html, base_url, "MiniMax新闻动态", "MiniMax")
        
        return items
    
    def parse_coze(self, html: str, base_url: str) -> List[WebNewsItem]:
        """解析扣子Coze文档页面"""
        items = []
        soup = BeautifulSoup(html, 'html.parser')
        
        # Coze文档页面结构
        # 查找更新日志、公告等
        updates = soup.find_all(['div', 'section', 'article'], 
                                class_=re.compile(r'(update|release|changelog|announcement)', re.I))
        
        for update in updates[:10]:
            try:
                # 提取标题
                title_elem = update.find(['h1', 'h2', 'h3', 'h4', 'strong', 'a'])
                if not title_elem:
                    continue
                    
                title = title_elem.get_text(strip=True)
                if not title or len(title) < 5:
                    continue
                
                # 提取链接
                link = update.find('a', href=True)
                url = link['href'] if link else ''
                if url and not url.startswith('http'):
                    url = base_url.rstrip('/') + '/' + url.lstrip('/')
                
                # 提取日期
                date_elem = update.find(['time', 'span'], class_=re.compile(r'(date|time)', re.I))
                published_date = None
                if date_elem:
                    date_str = date_elem.get_text(strip=True)
                    published_date = self._parse_date(date_str)
                
                # 提取摘要
                summary_elem = update.find(['p', 'div'], class_=re.compile(r'(summary|description|content)', re.I))
                summary = summary_elem.get_text(strip=True)[:200] if summary_elem else ""
                
                items.append(WebNewsItem(
                    title=title,
                    url=url or base_url,
                    source="扣子Coze文档更新",
                    company="Coze (扣子)",
                    summary=summary,
                    published_date=published_date,
                    category="AI Tool"
                ))
                
            except Exception as e:
                logger.debug(f"解析Coze文章失败: {e}")
                continue
        
        # 如果没找到标准结构，尝试通用解析
        if not items:
            items = self._generic_parse(html, base_url, "扣子Coze文档更新", "Coze (扣子)")
        
        return items
    
    def _generic_parse(self, html: str, base_url: str, source_name: str, company: str) -> List[WebNewsItem]:
        """通用HTML解析"""
        items = []
        soup = BeautifulSoup(html, 'html.parser')
        
        # 移除脚本和样式
        for tag in soup(['script', 'style', 'nav', 'footer', 'header']):
            tag.decompose()
        
        # 查找所有链接
        links = soup.find_all('a', href=True)
        
        seen_urls = set()
        for link in links[:30]:  # 限制处理数量
            url = link['href']
            if url in seen_urls:
                continue
            seen_urls.add(url)
            
            # 跳过导航链接、空链接等
            if url in ['#', '/', ''] or url.startswith('javascript:'):
                continue
            
            title = link.get_text(strip=True)
            
            # 过滤掉太短或太长的标题
            if not title or len(title) < 10 or len(title) > 200:
                continue
            
            # 过滤掉导航类标题
            nav_keywords = ['登录', '注册', '首页', '关于', '联系', '帮助', 'login', 'home', 'about']
            if any(kw in title.lower() for kw in nav_keywords):
                continue
            
            # 补全URL
            if not url.startswith('http'):
                url = base_url.rstrip('/') + '/' + url.lstrip('/')
            
            items.append(WebNewsItem(
                title=title,
                url=url,
                source=source_name,
                company=company,
                summary="",
                published_date=datetime.now(),
                category="AI Company" if "AI" in company or "智谱" in company else "AI Tool"
            ))
        
        return items[:10]  # 最多返回10条
    
    def _parse_date(self, date_str: str) -> Optional[datetime]:
        """解析日期字符串"""
        # 常见日期格式
        patterns = [
            r'(\d{4})[/-](\d{1,2})[/-](\d{1,2})',  # 2024-01-15, 2024/01/15
            r'(\d{1,2})[/-](\d{1,2})[/-](\d{4})',  # 01-15-2024
            r'(\d{4})年(\d{1,2})月(\d{1,2})日',    # 2024年1月15日
        ]
        
        for pattern in patterns:
            match = re.search(pattern, date_str)
            if match:
                groups = match.groups()
                try:
                    if len(groups[0]) == 4:  # 年份在前
                        year, month, day = int(groups[0]), int(groups[1]), int(groups[2])
                    else:  # 年份在后
                        month, day, year = int(groups[0]), int(groups[1]), int(groups[2])
                    return datetime(year, month, day)
                except:
                    pass
        
        return None
    
    def parse(self, url: str, source_type: str) -> List[Dict]:
        """
        主解析入口
        
        Args:
            url: 页面URL
            source_type: 源类型 ('zhipu', 'minimax', 'coze')
        
        Returns:
            新闻列表（字典格式）
        """
        logger.info(f"正在解析: {url} ({source_type})")
        
        html = self.fetch_page(url)
        if not html:
            logger.error(f"无法获取页面: {url}")
            return []
        
        # 根据源类型选择解析方法
        if 'bigmodel' in url or 'zhipu' in url:
            items = self.parse_zhipu(html, url)
        elif 'minimax' in url:
            items = self.parse_minimax(html, url)
        elif 'coze' in url:
            items = self.parse_coze(html, url)
        else:
            # 通用解析
            items = self._generic_parse(html, url, "网页新闻", "未知")
        
        logger.info(f"解析完成: 获得 {len(items)} 条新闻")
        return [item.to_dict() for item in items]


# 便捷函数
def parse_web_news(config: Dict) -> List[Dict]:
    """
    解析网页新闻配置
    
    Args:
        config: sources.yaml中的chinese_ai_companies配置
    
    Returns:
        所有新闻列表
    """
    parser = WebNewsParser()
    all_items = []
    
    sources = config.get('chinese_ai_companies', [])
    if isinstance(sources, dict):
        # 处理enabled字段
        sources = [s for s in sources.values() if isinstance(s, dict)]
    
    for source in sources:
        if not source.get('enabled', True):
            continue
            
        url = source.get('url')
        if not url:
            continue
        
        items = parser.parse(url, source.get('name', ''))
        for item in items:
            item['source_config'] = source
        all_items.extend(items)
    
    return all_items


if __name__ == '__main__':
    # 测试
    logging.basicConfig(level=logging.INFO)
    
    test_sources = [
        ('https://docs.bigmodel.cn/cn/update/new-releases', 'zhipu'),
        ('https://www.minimaxi.com/news', 'minimax'),
        ('https://docs.coze.cn', 'coze'),
    ]
    
    parser = WebNewsParser()
    for url, source_type in test_sources:
        print(f"\n{'='*60}")
        print(f"测试: {url}")
        print('='*60)
        items = parser.parse(url, source_type)
        for item in items[:5]:
            print(f"\n标题: {item['title']}")
            print(f"链接: {item['url']}")
            print(f"来源: {item['source']}")
            if item.get('published_date'):
                print(f"日期: {item['published_date']}")


path: scripts/quality_scorer.py
content: """
AI前沿哨兵 - 报告质量评分系统
自动评估生成报告的质量
"""
from datetime import datetime, timedelta
from typing import Dict, List, Any


class QualityScorer:
    """报告质量评分器"""
    
    def __init__(self):
        self.weights = {
            "information_density": 25,   # 信息密度
            "timeliness": 25,            # 时效性
            "diversity": 20,             # 多样性
            "readability": 15,           # 可读性
            "practicality": 15           # 实用性
        }
    
    def score_report(self, data: Dict[str, Any], report_content: str = "") -> Dict[str, Any]:
        """
        对报告进行全面评分
        
        Args:
            data: 报告数据（包含采集和分析结果）
            report_content: 报告原文（用于分析可读性）
        
        Returns:
            dict: 评分结果
        """
        scores = {}
        details = {}
        
        # 1. 信息密度评分（25分）
        info_score, info_detail = self._score_information_density(data)
        scores["information_density"] = info_score
        details["information_density"] = info_detail
        
        # 2. 时效性评分（25分）
        time_score, time_detail = self._score_timeliness(data)
        scores["timeliness"] = time_score
        details["timeliness"] = time_detail
        
        # 3. 多样性评分（20分）
        div_score, div_detail = self._score_diversity(data)
        scores["diversity"] = div_score
        details["diversity"] = div_detail
        
        # 4. 可读性评分（15分）
        read_score, read_detail = self._score_readability(data, report_content)
        scores["readability"] = read_score
        details["readability"] = read_detail
        
        # 5. 实用性评分（15分）
        prac_score, prac_detail = self._score_practicality(data)
        scores["practicality"] = prac_score
        details["practicality"] = prac_detail
        
        # 计算总分
        total = sum(scores.values())
        max_total = sum(self.weights.values())
        percentage = (total / max_total) * 100
        
        # 星级评分
        stars = self._get_stars(percentage)
        
        return {
            "scores": scores,
            "details": details,
            "total": total,
            "max_total": max_total,
            "percentage": round(percentage, 1),
            "stars": stars,
            "grade": self._get_grade(percentage)
        }
    
    def _score_information_density(self, data: Dict) -> tuple:
        """信息密度评分（25分）"""
        categorized = data.get("categorized", {})
        insights = data.get("insights", {})
        
        total_items = 0
        total_content_length = 0
        meaningful_items = 0
        
        # 统计各类内容
        for key, items in categorized.items():
            if isinstance(items, list):
                total_items += len(items)
                for item in items:
                    # 检查是否有有效内容
                    content = ""
                    if isinstance(item, dict):
                        content = (item.get("description", "") or 
                                  item.get("abstract", "") or 
                                  item.get("summary", "") or "")
                        if len(content) > 20:  # 有实质内容
                            meaningful_items += 1
                    total_content_length += len(str(content))
        
        # 统计编辑精选
        must_read = insights.get("must_read", [])
        total_items += len(must_read)
        meaningful_items += len([m for m in must_read if m.get("description") or m.get("abstract")])
        
        # 计算评分
        # 基础分：内容数量（10分）
        item_score = min(10, total_items * 0.5)
        
        # 内容质量：平均内容长度（10分）
        if total_items > 0:
            avg_length = total_content_length / total_items
            quality_score = min(10, avg_length / 30)  # 平均30字符/项为满分
        else:
            quality_score = 0
        
        # 有意义内容占比（5分）
        if total_items > 0:
            ratio_score = (meaningful_items / total_items) * 5
        else:
            ratio_score = 0
        
        total_score = round(item_score + quality_score + ratio_score)
        
        # 生成评价
        if total_items >= 20:
            detail = f"内容丰富，共{total_items}条有效信息"
        elif total_items >= 10:
            detail = f"内容适中，共{total_items}条信息"
        else:
            detail = f"内容较少，仅{total_items}条信息"
        
        return total_score, detail
    
    def _score_timeliness(self, data: Dict) -> tuple:
        """时效性评分（25分）"""
        categorized = data.get("categorized", {})
        
        now = datetime.now()
        recent_count = 0
        total_count = 0
        
        for key, items in categorized.items():
            if isinstance(items, list):
                for item in items:
                    total_count += 1
                    published = item.get("published", "") or item.get("created_at", "") or item.get("updated", "")
                    
                    if published:
                        try:
                            # 尝试解析日期
                            if isinstance(published, str):
                                if "T" in published:
                                    item_date = datetime.fromisoformat(published.replace("Z", "+00:00"))
                                else:
                                    item_date = datetime.strptime(published[:10], "%Y-%m-%d")
                            else:
                                continue
                            
                            # 24小时内为新鲜
                            if (now - item_date).total_seconds() < 86400:
                                recent_count += 1
                        except:
                            pass
        
        # 计算评分
        if total_count == 0:
            timeliness_score = 15  # 无数据时给基础分
            detail = "无法判断时效性"
        else:
            ratio = recent_count / total_count
            timeliness_score = round(min(25, ratio * 30))  # 80%以上新鲜内容为满分
            
            if ratio >= 0.8:
                detail = f"内容新鲜，{int(ratio*100)}%为24小时内发布"
            elif ratio >= 0.5:
                detail = f"时效性良好，{int(ratio*100)}%为近期内容"
            else:
                detail = f"部分内容较旧，仅{int(ratio*100)}%为24小时内"
        
        return timeliness_score, detail
    
    def _score_diversity(self, data: Dict) -> tuple:
        """多样性评分（20分）"""
        categorized = data.get("categorized", {})
        insights = data.get("insights", {})
        
        # 来源多样性
        sources = set()
        for key, items in categorized.items():
            if isinstance(items, list):
                for item in items:
                    source = item.get("source", "") or item.get("_source_name", "")
                    if source:
                        sources.add(source)
        
        # 类型多样性
        content_types = set(categorized.keys())
        
        # 话题多样性
        topics = insights.get("hot_topics", [])
        topic_count = len(topics)
        
        # 计算评分
        source_score = min(8, len(sources) * 1.5)  # 来源多样性（8分）
        type_score = min(7, len(content_types) * 2)  # 类型多样性（7分）
        topic_score = min(5, len(topics) * 1)  # 话题多样性（5分）
        
        total_score = round(source_score + type_score + topic_score)
        
        if len(sources) >= 5 and len(content_types) >= 4:
            detail = f"来源丰富，覆盖{len(sources)}个来源，{len(content_types)}种内容类型"
        else:
            detail = f"覆盖{len(sources)}个来源，{len(content_types)}种内容类型"
        
        return total_score, detail
    
    def _score_readability(self, data: Dict, report_content: str) -> tuple:
        """可读性评分（15分）"""
        if not report_content:
            return 12, "报告结构基本清晰"  # 无内容时给基础分
        
        # 检查结构元素
        has_summary = "## " in report_content and len(report_content.split("## ")) > 3  # 至少3个章节
        has_emoji = any(emoji in report_content for emoji in ["📚", "⭐", "🔥", "📊", "🔗"])
        has_links = "[" in report_content and "](" in report_content
        has_tables = "|" in report_content and "---" in report_content
        has_divider = "---" in report_content
        
        # 计算评分
        structure_score = 4 if has_summary else 2
        formatting_score = 4 if has_emoji else 2
        navigation_score = 4 if has_links else 2
        separation_score = 3 if has_divider else 1
        
        total_score = structure_score + formatting_score + navigation_score + separation_score
        
        # 生成评价
        positive = []
        if has_summary:
            positive.append("结构清晰")
        if has_emoji:
            positive.append("格式美观")
        if has_links:
            positive.append("导航完善")
        if has_tables:
            positive.append("数据规整")
        
        detail = "，".join(positive) if positive else "结构可进一步优化"
        
        return total_score, detail
    
    def _score_practicality(self, data: Dict) -> tuple:
        """实用性评分（15分）"""
        insights = data.get("insights", {})
        categorized = data.get("categorized", {})
        
        practical_count = 0
        total_count = 0
        
        # 检查是否有可操作的链接
        all_items = []
        for key, items in categorized.items():
            if isinstance(items, list):
                all_items.extend(items)
        
        # 有URL/链接的条目
        items_with_urls = [i for i in all_items if i.get("url") or i.get("pdf_url") or i.get("hn_url")]
        practical_count += len(items_with_urls)
        total_count += len(all_items)
        
        # 有摘要/描述的条目
        items_with_desc = [i for i in all_items if i.get("description") or i.get("abstract") or i.get("summary")]
        practical_count += len(items_with_desc)
        
        # 编辑精选质量
        must_read = insights.get("must_read", [])
        if len(must_read) >= 3:
            practical_count += 2
        if len(must_read) >= 5:
            practical_count += 1
        
        # 计算评分
        if total_count > 0:
            url_ratio = len(items_with_urls) / total_count
            desc_ratio = len(items_with_desc) / total_count
            
            action_score = min(8, url_ratio * 10)  # 可操作内容（8分）
            insight_score = min(7, desc_ratio * 8)  # 有洞察内容（7分）
        else:
            action_score = 5
            insight_score = 5
        
        total_score = round(action_score + insight_score)
        
        actionable = len(items_with_urls)
        detail = f"有{actionable}条可立即访问的资源"
        
        return total_score, detail
    
    def _get_stars(self, percentage: float) -> str:
        """根据百分比获取星级"""
        if percentage >= 90:
            return "⭐⭐⭐⭐⭐"
        elif percentage >= 80:
            return "⭐⭐⭐⭐"
        elif percentage >= 70:
            return "⭐⭐⭐"
        elif percentage >= 60:
            return "⭐⭐"
        else:
            return "⭐"
    
    def _get_grade(self, percentage: float) -> str:
        """获取评级"""
        if percentage >= 95:
            return "卓越"
        elif percentage >= 90:
            return "优秀"
        elif percentage >= 80:
            return "良好"
        elif percentage >= 70:
            return "中等"
        elif percentage >= 60:
            return "及格"
        else:
            return "待改进"
    
    def generate_quality_card(self, score_result: Dict) -> str:
        """生成质量评分卡片（Markdown格式）"""
        scores = score_result["scores"]
        details = score_result["details"]
        
        card = """

---

## 📊 报告质量评分

| 维度 | 得分 | 说明 |
|------|------|------|
"""
        
        dimensions = [
            ("information_density", "信息密度", 25),
            ("timeliness", "时效性", 25),
            ("diversity", "多样性", 20),
            ("readability", "可读性", 15),
            ("practicality", "实用性", 15)
        ]
        
        for key, name, max_score in dimensions:
            score = scores.get(key, 0)
            detail = details.get(key, "")
            card += f"| {name} | {score}/{max_score} | {detail} |\n"
        
        card += f"""
**总分：{score_result['total']}/{score_result['max_total']}** ({score_result['percentage']}%)
**评级：{score_result['grade']}** {score_result['stars']}

> 💡 本评分由 AI前沿哨兵 自动生成，仅供参考
"""
        
        return card


# 全局实例
_scorer = None

def get_scorer() -> QualityScorer:
    """获取评分器实例"""
    global _scorer
    if _scorer is None:
        _scorer = QualityScorer()
    return _scorer


def quick_score(data: Dict) -> Dict:
    """快速评分"""
    return get_scorer().score_report(data)


if __name__ == "__main__":
    # 测试代码
    test_data = {
        "categorized": {
            "papers": [
                {"title": "Test Paper 1", "abstract": "A" * 200, "url": "http://test.com"},
                {"title": "Test Paper 2", "abstract": "B" * 200, "url": "http://test.com"}
            ],
            "projects": [
                {"title": "Test Project", "description": "A" * 100, "url": "http://test.com"}
            ]
        },
        "insights": {
            "must_read": [
                {"title": "Important 1", "description": "Key insight"}
            ],
            "hot_topics": ["AI", "ML", "LLM"]
        }
    }
    
    scorer = QualityScorer()
    result = scorer.score_report(test_data)
    print(f"评分结果：{result['total']}/{result['max_total']}")
    print(f"评级：{result['grade']} {result['stars']}")
    print(scorer.generate_quality_card(result))


path: scripts/reporter.py
content: """
AI前沿哨兵 - 报告生成模块
根据采集和分析的数据生成情报报告
"""
import argparse
import json
from datetime import datetime
from pathlib import Path

from jinja2 import Environment, FileSystemLoader, select_autoescape

from quality_scorer import QualityScorer


class ReportGenerator:
    """报告生成器"""
    
    def __init__(self, template_dir=None):
        self.template_dir = template_dir or Path(__file__).parent.parent / "templates"
        self.output_dir = Path(__file__).parent.parent / "output"
        self.output_dir.mkdir(exist_ok=True)
        
        # 初始化Jinja2环境
        self.env = Environment(
            loader=FileSystemLoader(str(self.template_dir)),
            autoescape=select_autoescape(['html', 'xml'])
        )
        
        # 添加自定义过滤器
        self.env.filters['commalist'] = lambda x: f"{x:,}" if isinstance(x, (int, float)) else str(x)
    
    def generate(self, analyzed_data, report_type="morning"):
        """
        生成报告
        
        Args:
            analyzed_data: 分析后的数据
            report_type: 报告类型 (morning/evening)
        
        Returns:
            str: 生成的报告内容
        """
        template_name = f"{report_type}_report.md"
        
        try:
            template = self.env.get_template(template_name)
        except Exception:
            # 如果模板不存在，使用默认模板
            template = self._get_default_template(report_type)
        
        # 准备渲染数据
        context = self._prepare_context(analyzed_data, report_type)
        
        # 渲染报告
        report = template.render(**context)
        
        return report
    
    def generate_and_save(self, analyzed_data, report_type="morning"):
        """
        生成并保存报告
        
        Args:
            analyzed_data: 分析后的数据
            report_type: 报告类型
        
        Returns:
            Path: 报告文件路径
        """
        report = self.generate(analyzed_data, report_type)
        
        # 生成文件名
        date_str = datetime.now().strftime("%Y%m%d")
        time_str = datetime.now().strftime("%H%M")
        filename = f"{report_type}_report_{date_str}_{time_str}.md"
        output_path = self.output_dir / filename
        
        # 保存报告
        with open(output_path, 'w', encoding='utf-8') as f:
            f.write(report)
        
        return output_path
    
    def _prepare_context(self, data, report_type):
        """准备渲染上下文"""
        timestamp = data.get("timestamp", datetime.now().isoformat())
        categorized = data.get("categorized", {})
        insights = data.get("insights", {})
        summary = data.get("summary", {})
        
        # 格式化时间
        try:
            dt = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
            date_str = dt.strftime("%Y年%m月%d日")
            time_str = dt.strftime("%H:%M")
        except:
            date_str = datetime.now().strftime("%Y年%m月%d日")
            time_str = datetime.now().strftime("%H:%M")
        
        # 生成今日摘要
        daily_summary = self._generate_daily_summary(categorized, insights, summary)
        
        # 问候语
        greetings = {
            "morning": "早安",
            "evening": "晚安"
        }
        
        # 质量评分
        scorer = QualityScorer()
        score_result = scorer.score_report(data)
        quality_card = scorer.generate_quality_card(score_result)
        
        return {
            "date_str": date_str,
            "time_str": time_str,
            "report_type": report_type,
            "greeting": greetings.get(report_type, "你好"),
            "total_items": data.get("total_items", 0),
            "summary": summary,
            "categorized": categorized,
            "insights": insights,
            "must_read": insights.get("must_read", [])[:10],
            "hot_topics": insights.get("hot_topics", []),
            "trending_projects": insights.get("trending_projects", [])[:5],
            "important_papers": insights.get("important_papers", [])[:2],  # 只展示最重要的2篇论文
            "top_picks": insights.get("must_read", [])[:5],  # 编辑精选
            "papers": categorized.get("papers", [])[:10],
            "projects": categorized.get("projects", [])[:10],
            "discussions": categorized.get("discussions", [])[:10],
            "news": categorized.get("news", [])[:10],
            "tweets": categorized.get("tweets", [])[:5],
            # 质量评分相关
            "daily_summary": daily_summary,
            "quality_score": score_result,
            "quality_card": quality_card
        }
    
    def _generate_daily_summary(self, categorized, insights, summary):
        """生成今日摘要"""
        summary_parts = []
        
        # 论文
        papers = categorized.get("papers", [])
        if papers:
            summary_parts.append(f"{len(papers)}篇学术论文")
        
        # 项目
        projects = categorized.get("projects", [])
        if projects:
            summary_parts.append(f"{len(projects)}个开源项目")
        
        # 讨论
        discussions = categorized.get("discussions", [])
        if discussions:
            summary_parts.append(f"{len(discussions)}条社区讨论")
        
        # 新闻
        news = categorized.get("news", [])
        if news:
            summary_parts.append(f"{len(news)}条行业动态")
        
        # 热词
        hot_topics = insights.get("hot_topics", [])
        if hot_topics:
            topics_str = "、".join(hot_topics[:3])
            summary_parts.append(f"热门话题：{topics_str}")
        
        if summary_parts:
            return "今日AI领域有新动态，涵盖" + "，".join(summary_parts)
        else:
            return "暂无重要更新"
    
    def _get_default_template(self, report_type):
        """获取默认模板"""
        if report_type == "morning":
            template_str = DEFAULT_MORNING_TEMPLATE
        else:
            template_str = DEFAULT_EVENING_TEMPLATE
        
        from jinja2 import Template
        return Template(template_str)


# 默认晨报模板（需与 templates/morning_report.md 保持同步）
DEFAULT_MORNING_TEMPLATE = """# 🌅 AI前沿哨兵 - {{ date_str }}晨报

> **今日摘要**：{{ daily_summary }}

---

## ⭐ 编辑精选（Top 5）

{% for item in top_picks[:5] %}
### {{ loop.index }}. [{{ item.title }}]({{ item.url or item.hn_url or item.pdf_url or "#" }})

**一句话摘要**：{{ (item.summary or item.description or item.abstract or item.text or "暂无摘要")[:150] if (item.summary or item.description or item.abstract or item.text) else "暂无摘要" }}

**为什么重要**：
{% if item.stars %}- ⭐ 高热度开源项目（{{ item.stars }} stars）
{% elif item.score %}- 🔥 社区热议（{{ item.score }} 分）
{% else %}- 📚 重要学术贡献
{% endif %}

---

{% endfor %}

## 📚 学术前沿（arXiv）

{% if important_papers %}
{% for paper in important_papers[:2] %}
### [{{ paper.title }}]({{ paper.url }})

📝 **一句话描述**：{{ paper.title.split(":")[-1].strip() if ":" in paper.title else paper.abstract[:120] if paper.abstract else "重要AI研究论文" }}

| 信息 | 内容 |
|------|------|
| 👥 作者 | {{ paper.author_str or paper.authors[0] if paper.authors and paper.authors is iterable else "未知" }} |
| 🏷️ 分类 | {{ paper.primary_category or "Unknown" }} |
| 📅 发布 | {{ paper.published[:10] if paper.published else "未知" }} |

{% if paper.abstract %}
**摘要**：{{ paper.abstract[:250] }}...
{% endif %}

🔗 [阅读论文]({{ paper.url }})

---

{% endfor %}
{% else %}
暂无新论文
{% endif %}

## ⚡ 热门开源项目（GitHub）

{% if trending_projects %}
{% for project in trending_projects[:5] %}
### [{{ project.title }}]({{ project.url }})

{{ project.description[:150] if project.description and project.description != "无描述" else "热门AI开源项目" }}

| 指标 | 数值 |
|------|------|
| ⭐ Stars | {{ project.stars|default(0)|int|commalist }} |
| 🍴 Forks | {{ project.forks|default(0)|int|commalist }} |
| 🐙 语言 | {{ project.language or "Unknown" }} |
| 👤 作者 | [{{ project.author or "Unknown" }}]({{ project.author_url or "#" }}) |

---

{% endfor %}
{% else %}
暂无热门项目
{% endif %}

## 💬 社区热议（HackerNews）

{% if discussions %}
{% for item in discussions[:5] %}
### [{{ item.title }}]({{ item.hn_url or item.url or "#" }})

{{ item.text[:120] if item.text else (item.description[:120] if item.description else "社区热门讨论") }}...

| 信息 | 内容 |
|------|------|
| ⬆️ 得分 | {{ item.score|default(0) }} |
| 💬 评论 | {{ item.comments|default(0) }} |
| 👤 作者 | {{ item.author or "Unknown" }} |
| 🕐 时间 | {{ item.time or item.published or "未知" }} |

---

{% endfor %}
{% else %}
暂无社区讨论
{% endif %}

## 📰 行业动态（AI博客）

{% if news %}
{% for item in news[:5] %}
### [{{ item.title }}]({{ item.url or "#" }})

**来源**：{{ item.source or "AI博客" }} · {{ item.published or "未知" }}

{{ (item.summary or item.description or "行业动态")[:150] }}...

{% if item.categories %}
🏷️ 标签：{{ item.categories|join(', ') }}
{% endif %}

---

{% endfor %}
{% else %}
暂无新闻更新
{% endif %}

{% if tweets %}
## 🐦 大咖动态（Twitter）

{% for tweet in tweets[:3] %}
### [{{ tweet.author_name or tweet.author_username }}](https://twitter.com/{{ tweet.author_username }})

{{ tweet.text[:200] }}

| 指标 | 数值 |
|------|------|
| ❤️ 点赞 | {{ tweet.likes|default(0) }} |
| 🔁 转发 | {{ tweet.retweets|default(0) }} |
| 🔗 [查看原推]({{ tweet.url }}) |

---

{% endfor %}
{% endif %}

## 📊 今日数据

| 类型 | 数量 |
|------|------|
| 学术论文 | {{ summary.papers_count }} 篇 |
| 开源项目 | {{ summary.projects_count }} 个 |
| 社区讨论 | {{ summary.discussions_count }} 条 |
| 新闻动态 | {{ summary.news_count }} 条 |
| 社交动态 | {{ summary.tweets_count }} 条 |

### 🔥 今日热词

{% if hot_topics %}
{% for topic in hot_topics %}#{{ topic }} {% endfor %}
{% else %}
暂无热词分析
{% endif %}

---

## 💡 推荐阅读

- 🔍 持续关注 GitHub Trending 新上榜项目
- 📚 定期查看 arXiv 新提交的论文
- 💬 HackerNews 上的技术讨论往往预示趋势

---

{{ quality_card }}
"""


# 默认晚报模板
DEFAULT_EVENING_TEMPLATE = """# 🌙 AI前沿哨兵 - {{ date_str }}晚报

{{ greeting }}！以下是今日AI领域完整动态回顾。

## 📈 今日总结

| 类型 | 数量 |
|------|------|
| 学术论文 | {{ summary.papers_count }} 篇 |
| 开源项目 | {{ summary.projects_count }} 个 |
| 社区讨论 | {{ summary.discussions_count }} 条 |
| 新闻动态 | {{ summary.news_count }} 条 |

{% if hot_topics %}
## 🔥 今日热词

{% for topic in hot_topics %}
- **{{ topic }}**
{% endfor %}
{% endif %}

{% if projects %}
## 💻 开源项目

{% for item in projects %}
### [{{ item.title }}]({{ item.url }})
{{ item.get('description', '')[:150] }}
- ⭐ {{ item.stars }} | 🍴 {{ item.forks }} | 📝 {{ item.language }}
{% endfor %}
{% endif %}

{% if papers %}
## 📚 最新论文

{% for item in papers %}
### [{{ item.title }}]({{ item.url }})
- 👥 {{ item.author_str }}
- 🏷️ {{ item.primary_category }}
{{ item.abstract[:200] }}

{% endfor %}
{% endif %}

{% if discussions %}
## 💬 社区热议

{% for item in discussions %}
### [{{ item.title }}]({{ item.hn_url if item.hn_url else item.url }})
- ⬆️ {{ item.score }} | 💬 {{ item.comments }} | 👤 {{ item.author }}
{% endfor %}
{% endif %}

{% if news %}
## 📰 最新动态

{% for item in news %}
### [{{ item.title }}]({{ item.url }})
{{ item.summary[:200] }}
- 📅 {{ item.published }} | 👤 {{ item.author }}

{% endfor %}
{% endif %}

---
*报告生成时间: {{ date_str }} {{ time_str }}*
*由 AI前沿哨兵 自动生成*
"""


def main():
    parser = argparse.ArgumentParser(description="AI前沿哨兵 - 报告生成工具")
    parser.add_argument("--type", type=str, default="morning", choices=["morning", "evening"],
                        help="报告类型")
    parser.add_argument("--input", type=str, help="分析结果JSON文件路径")
    parser.add_argument("--output", type=str, help="输出文件路径")
    
    args = parser.parse_args()
    
    # 加载分析数据
    if args.input:
        with open(args.input, 'r', encoding='utf-8') as f:
            analyzed_data = json.load(f)
    else:
        # 尝试加载最新的分析结果
        output_dir = Path(__file__).parent.parent / "output"
        files = list(output_dir.glob("analyzed_*.json"))
        if files:
            latest = max(files, key=lambda f: f.stat().st_mtime)
            with open(latest, 'r', encoding='utf-8') as f:
                analyzed_data = json.load(f)
        else:
            print("❌ 未找到分析数据文件")
            return
    
    # 生成报告
    generator = ReportGenerator()
    report = generator.generate(analyzed_data, args.type)
    
    # 输出或保存
    if args.output:
        with open(args.output, 'w', encoding='utf-8') as f:
            f.write(report)
        print(f"✅ 报告已保存到: {args.output}")
    else:
        print(report)


if __name__ == "__main__":
    main()


path: templates/evening_report.md
content: # 🌙 AI前沿哨兵 - {{ date_str }}晚报

{{ greeting }}！欢迎阅读今日AI领域完整动态回顾。

> **今日摘要**：{{ daily_summary }}

---

## 📊 今日总结

| 类型 | 数量 |
|------|------|
| 学术论文 | {{ summary.papers_count }} 篇 |
| 开源项目 | {{ summary.projects_count }} 个 |
| 社区讨论 | {{ summary.discussions_count }} 条 |
| 新闻动态 | {{ summary.news_count }} 条 |

{% if hot_topics %}
## 🔥 今日热词

{% for topic in hot_topics %}
- **#{{ topic }}**
{% endfor %}
{% endif %}

---

## ⭐ 今日亮点

{% if top_picks %}
{% for item in top_picks[:5] %}
### {{ loop.index }}. [{{ item.title }}]({{ item.url or item.hn_url or item.pdf_url or "#" }})

**一句话摘要**：{{ item.summary or item.description or item.abstract[:100] if item.abstract else "暂无摘要" }}

{% if item.stars %}- ⭐ {{ item.stars }} stars{% endif %}
{% if item.score %}- 🔥 {{ item.score }} 分{% endif %}

{% endfor %}
{% else %}
暂无精选内容
{% endif %}

---

## 💻 开源项目

{% if projects %}
{% for item in projects[:10] %}
### [{{ item.title }}]({{ item.url }})

{{ item.description or item.get('summary', '')[:200] }}

| 指标 | 值 |
|------|---|
| ⭐ Stars | {{ item.stars | default(0) | int | commalist }} |
| 🍴 Forks | {{ item.forks | default(0) | int | commalist }} |
| 📝 语言 | {{ item.language or "Unknown" }} |
| 👤 作者 | [{{ item.author or item.author_name or "Unknown" }}]({{ item.author_url or item.author_github or "#" }}) |

{% endfor %}
{% else %}
暂无新项目
{% endif %}

---

## 📚 最新论文

{% if papers %}
{% for item in papers[:10] %}
### [{{ item.title }}]({{ item.url or item.pdf_url or "#" }})

- 👥 **作者**: {{ item.author_str or item.authors | join(', ') if item.authors is iterable and item.authors is not string else item.authors or "未知" }}
- 🏷️ **分类**: {{ item.primary_category or item.categories[0] if item.categories else "Unknown" }}

> {{ item.abstract[:300] if item.abstract else item.summary or "无摘要" }}

{% if item.pdf_url %}
📄 [PDF全文]({{ item.pdf_url }})
{% endif %}

{% endfor %}
{% else %}
暂无新论文
{% endif %}

---

## 💬 社区热议

{% if discussions %}
{% for item in discussions[:10] %}
### [{{ item.title }}]({{ item.hn_url or item.url or "#" }})

| 指标 | 值 |
|------|---|
| ⬆️ 得分 | {{ item.score | default(0) }} |
| 💬 评论 | {{ item.comments | default(0) }} |
| 👤 作者 | [{{ item.author }}]({{ item.author_url or "#" }}) |
| 🕐 时间 | {{ item.time or item.published or "未知" }} |

{% endfor %}
{% else %}
暂无社区讨论
{% endif %}

---

## 📰 最新动态

{% if news %}
{% for item in news[:10] %}
### [{{ item.title }}]({{ item.url or "#" }})

{{ item.summary[:250] if item.summary else item.description or "无描述" }}

- 📅 **发布时间**: {{ item.published or "未知" }}
- 👤 **作者**: {{ item.author or item.source or "未知" }}
{% if item.categories %}
- 🏷️ **分类**: {{ item.categories | join(', ') }}
{% endif %}

{% endfor %}
{% else %}
暂无新闻更新
{% endif %}

{% if tweets %}
---

## 🐦 社交动态

{% for item in tweets[:5] %}
### [{{ item.author_name or item.author_username }}]({{ item.author_url }})

{{ item.text[:280] }}

| 指标 | 值 |
|------|------|
| ❤️ 点赞 | {{ item.likes | default(0) }} |
| 🔁 转发 | {{ item.retweets | default(0) }} |
| 💬 回复 | {{ item.replies | default(0) }} |

{% endfor %}
{% endif %}

---

## 💡 明日预览

建议关注的领域：
- 持续关注 GitHub Trending 新上榜项目
- 定期查看 arXiv 新提交的论文
- HackerNews 上的技术讨论往往预示趋势

---

{{ quality_card }}

---

*报告生成时间: {{ date_str }} {{ time_str }}*
*数据来源：GitHub, arXiv, HackerNews, AI Blogs, Twitter*
*由 AI前沿哨兵 自动生成*


path: templates/morning_report.md
content: # 🌅 AI前沿哨兵 - {{ date_str }}晨报

> **今日摘要**：{{ daily_summary }}

---

## ⭐ 编辑精选（Top 5）

{% for item in top_picks[:5] %}
### {{ loop.index }}. [{{ item.title }}]({{ item.url or item.hn_url or item.pdf_url or "#" }})

**一句话摘要**：{{ (item.summary or item.description or item.abstract or item.text or "暂无摘要")[:150] if (item.summary or item.description or item.abstract or item.text) else "暂无摘要" }}

**为什么重要**：
{% if item.stars %}- ⭐ 高热度开源项目（{{ item.stars }} stars）
{% elif item.score %}- 🔥 社区热议（{{ item.score }} 分）
{% else %}- 📚 重要学术贡献
{% endif %}

---

{% endfor %}

## 📚 学术前沿（arXiv）

{% if important_papers %}
{% for paper in important_papers[:2] %}
### [{{ paper.title }}]({{ paper.url }})

📝 **一句话描述**：{{ paper.title.split(":")[-1].strip() if ":" in paper.title else paper.abstract[:120] if paper.abstract else "重要AI研究论文" }}

| 信息 | 内容 |
|------|------|
| 👥 作者 | {{ paper.author_str or paper.authors[0] if paper.authors and paper.authors is iterable else "未知" }} |
| 🏷️ 分类 | {{ paper.primary_category or "Unknown" }} |
| 📅 发布 | {{ paper.published[:10] if paper.published else "未知" }} |

{% if paper.abstract %}
**摘要**：{{ paper.abstract[:250] }}...
{% endif %}

🔗 [阅读论文]({{ paper.url }})

---

{% endfor %}
{% else %}
暂无新论文
{% endif %}

## ⚡ 热门开源项目（GitHub）

{% if trending_projects %}
{% for project in trending_projects[:5] %}
### [{{ project.title }}]({{ project.url }})

{{ project.description[:150] if project.description and project.description != "无描述" else "热门AI开源项目" }}

| 指标 | 数值 |
|------|------|
| ⭐ Stars | {{ project.stars|default(0)|int|commalist }} |
| 🍴 Forks | {{ project.forks|default(0)|int|commalist }} |
| 🐙 语言 | {{ project.language or "Unknown" }} |
| 👤 作者 | [{{ project.author or "Unknown" }}]({{ project.author_url or "#" }}) |

---

{% endfor %}
{% else %}
暂无热门项目
{% endif %}

## 💬 社区热议（HackerNews）

{% if discussions %}
{% for item in discussions[:5] %}
### [{{ item.title }}]({{ item.hn_url or item.url or "#" }})

{{ item.text[:120] if item.text else (item.description[:120] if item.description else "社区热门讨论") }}...

| 信息 | 内容 |
|------|------|
| ⬆️ 得分 | {{ item.score|default(0) }} |
| 💬 评论 | {{ item.comments|default(0) }} |
| 👤 作者 | {{ item.author or "Unknown" }} |
| 🕐 时间 | {{ item.time or item.published or "未知" }} |

---

{% endfor %}
{% else %}
暂无社区讨论
{% endif %}

## 📰 行业动态（AI博客）

{% if news %}
{% for item in news[:5] %}
### [{{ item.title }}]({{ item.url or "#" }})

**来源**：{{ item.source or "AI博客" }} · {{ item.published or "未知" }}

{{ (item.summary or item.description or "行业动态")[:150] }}...

{% if item.categories %}
🏷️ 标签：{{ item.categories|join(', ') }}
{% endif %}

---

{% endfor %}
{% else %}
暂无新闻更新
{% endif %}

{% if tweets %}
## 🐦 大咖动态（Twitter）

{% for tweet in tweets[:3] %}
### [{{ tweet.author_name or tweet.author_username }}](https://twitter.com/{{ tweet.author_username }})

{{ tweet.text[:200] }}

| 指标 | 数值 |
|------|------|
| ❤️ 点赞 | {{ tweet.likes|default(0) }} |
| 🔁 转发 | {{ tweet.retweets|default(0) }} |
| 🔗 [查看原推]({{ tweet.url }}) |

---

{% endfor %}
{% endif %}

## 📊 今日数据

| 类型 | 数量 |
|------|------|
| 学术论文 | {{ summary.papers_count }} 篇 |
| 开源项目 | {{ summary.projects_count }} 个 |
| 社区讨论 | {{ summary.discussions_count }} 条 |
| 新闻动态 | {{ summary.news_count }} 条 |
| 社交动态 | {{ summary.tweets_count }} 条 |

### 🔥 今日热词

{% if hot_topics %}
{% for topic in hot_topics %}#{{ topic }} {% endfor %}
{% else %}
暂无热词分析
{% endif %}

---

## 💡 推荐阅读

- 🔍 持续关注 GitHub Trending 新上榜项目
- 📚 定期查看 arXiv 新提交的论文
- 💬 HackerNews 上的技术讨论往往预示趋势

---

{{ quality_card }}

---

**报告生成时间**：{{ date_str }} {{ time_str }}
**数据来源**：GitHub, arXiv, HackerNews, AI Blogs, Twitter
**由 AI前沿哨兵 自动生成**


path: templates/xiaohongshu_report.md
content: # 🔥AI前沿情报站｜{{ date_str }}必看清单

姐妹们！今天的AI圈又炸了💥 一口气整理了{{ total_count }}条干货，建议先码再看⭐

---

## 💎 今日重磅（必看TOP5）

{% for item in top_picks[:5] %}
**{{ loop.index }}. {{ item.title }}**
{% if item.stars %}⭐{{ item.stars }}{% elif item.score %}🔥{{ item.score }}分{% endif %}

{{ (item.summary or item.description or "这个真的绝绝子！")[:80] }}

{% endfor %}

---

## 📚 论文党的福音（arXiv精选）

{% if important_papers %}
{% for paper in important_papers[:3] %}
📄 **{{ paper.title[:40] }}...**

{% if paper.abstract %}💡一句话：{{ paper.abstract[:60] }}...{% endif %}
🏷️ {{ paper.primary_category or "AI" }}｜{{ paper.published[:10] if paper.published else "最新" }}

{% endfor %}
{% else %}
今天论文区比较安静，明天蹲一波大的👀
{% endif %}

---

## 💻 开源真香现场（GitHub）

{% if trending_projects %}
{% for project in trending_projects[:3] %}
🔧 **{{ project.title }}**

{{ project.description[:50] if project.description else "宝藏项目！" }}...
⭐{{ project.stars|default(0) }} Stars｜{{ project.language or "多语言" }}

{% endfor %}
{% else %}
今天GitHub有点冷，但明天说不准就有爆款了✨
{% endif %}

---

## 📰 业内动态（不容错过）

{% for source, items in blog_sources.items() %}
### {{ source }}（{{ items|length }}条）
{% for item in items[:2] %}
• {{ item.title[:35] }}...
{% endfor %}

{% endfor %}

---

## 🔥 热门话题（MMChat精选）

{% for topic in mmchat_topics[:5] %}
{{ loop.index }}️⃣ {{ topic }}
{% endfor %}

---

## 📊 今日数据

| 类型 | 数量 |
|------|------|
| 📄 论文 | {{ arxiv_count }} 篇 |
| 💻 项目 | {{ github_count }} 个 |
| 📰 资讯 | {{ blog_count }} 条 |
| 🔥 热点 | {{ mmchat_count }} 个 |

---

💬 **今日份AI情报已送达！觉得有用就点个收藏吧～**
🔖 明天{{ report_time }}准时更新，记得蹲！

#AI #人工智能 #科技资讯 #每日打卡 #干货分享


path: webui/app.js
content: /**
 * AI前沿哨兵 - WebUI主脚本
 * 信息源管理、采集控制、报告生成
 * 
 * 功能：
 * - 多视图导航切换
 * - 信息源管理（内置+自定义）
 * - 预设快速添加模板
 * - 分类筛选
 * - 采集与报告生成
 */

// ============================================
// 应用状态管理
// ============================================
const state = {
    currentView: 'dashboard',      // 当前视图
    sources: [],                     // 用户自定义信息源
    logs: [],                        // 采集日志
    stats: {                         // 统计数据
        github: 0,
        arxiv: 0,
        hn: 0,
        blogs: 0
    },
    keywords: [],                    // 关注关键词
    settings: {                      // 系统设置
        twitterToken: '',
        morningEnabled: true,
        eveningEnabled: true
    }
};

// ============================================
// 内置信息源配置（不可删除）
// ============================================
const builtInSources = [
    {
        id: 'github',
        name: 'GitHub Trending',
        description: '追踪全球AI开源项目的热门榜单，发现最新最热门的开源项目',
        icon: '⌘',
        category: 'tech-community',
        detail: '采集GitHub上AI相关项目的Star、Fork数据，适合发现新兴项目'
    },
    {
        id: 'arxiv',
        name: 'arXiv 论文',
        description: '获取人工智能、机器学习领域的最新学术研究论文',
        icon: '◉',
        category: 'academic',
        detail: '覆盖cs.AI、cs.LG、cs.CL等分类的学术论文'
    },
    {
        id: 'hackernews',
        name: 'Hacker News',
        description: '了解全球开发者社区讨论的AI热点话题和行业动态',
        icon: '◈',
        category: 'tech-community',
        detail: '来自Y Combinator的开发者社区，涵盖技术、创业、AI讨论'
    },
    {
        id: 'twitter',
        name: 'Twitter/X',
        description: '追踪AI大咖和机构的最新动态和见解',
        icon: '✕',
        category: 'social',
        detail: '需要配置Twitter Bearer Token才能使用'
    },
    {
        id: 'blogs',
        name: 'RSS 订阅源',
        description: '订阅任意博客的新闻更新，通过RSS协议获取内容',
        icon: '◎',
        category: 'blog',
        detail: '支持任何提供RSS/Atom订阅的网站'
    }
];

// ============================================
// 快速添加预设模板
// ============================================
const quickAddTemplates = [
    {
        name: 'OpenAI 官方博客',
        url: 'https://openai.com/blog/rss.xml',
        description: '获取OpenAI最新产品发布和技术研究',
        icon: '🤖',
        category: 'official-blog',
        tags: ['官方', 'AI公司', 'GPT']
    },
    {
        name: 'Anthropic 官方博客',
        url: 'https://www.anthropic.com/news/rss',
        description: '了解Claude和Anthropic的最新动态',
        icon: '🧠',
        category: 'official-blog',
        tags: ['官方', 'AI公司', 'Claude']
    },
    {
        name: 'Google AI Blog',
        url: 'https://blog.google/technology/ai/rss/',
        description: 'Google在AI领域的研究进展和产品应用',
        icon: '🔍',
        category: 'official-blog',
        tags: ['官方', 'AI公司', 'Google']
    },
    {
        name: 'DeepMind 研究',
        url: 'https://deepmind.com/blog/feed/basic/',
        description: 'DeepMind的前沿AI研究成果',
        icon: '💡',
        category: 'official-blog',
        tags: ['官方', 'AI公司', '研究']
    },
    {
        name: 'Meta AI 研究',
        url: 'https://ai.meta.com/blog/rss/',
        description: 'Meta在AI领域的开源研究和技术分享',
        icon: '🔷',
        category: 'official-blog',
        tags: ['官方', 'AI公司', '开源']
    },
    {
        name: 'Machine Learning Mastery',
        url: 'https://machinelearningmastery.com/feed/',
        description: '实用的机器学习教程和最佳实践',
        icon: '📊',
        category: 'tutorial',
        tags: ['教程', '机器学习', '实践']
    },
    {
        name: 'Towards Data Science',
        url: 'https://towardsdatascience.com/feed',
        description: '数据科学和机器学习的中文精选文章',
        icon: '📝',
        category: 'tutorial',
        tags: ['博客', '数据科学', '精选']
    },
    {
        name: 'Hugging Face Blog',
        url: 'https://huggingface.co/blog/feed.xml',
        description: '开源LLM和模型库的最新动态',
        icon: '🤗',
        category: 'official-blog',
        tags: ['官方', '开源', 'LLM']
    }
];

// ============================================
// 信息源分类配置
// ============================================
const sourceCategories = [
    { id: 'all', name: '全部', icon: '📦', description: '显示所有信息源' },
    { id: 'official-blog', name: '官方博客', icon: '🏢', description: 'AI公司官方发布的博客' },
    { id: 'tech-community', name: '技术社区', icon: '💬', description: '开发者社区和技术论坛' },
    { id: 'academic', name: '学术论文', icon: '📚', description: '学术机构发表的论文' },
    { id: 'social', name: '社交媒体', icon: '🌐', description: '社交平台上的AI讨论' },
    { id: 'tutorial', name: '教程博客', icon: '📖', description: '学习教程和技术博客' },
    { id: 'custom', name: '自定义', icon: '✏️', description: '您自己添加的信息源' }
];

// 模拟数据（实际使用时替换为真实API调用）
const mockData = {
    github: [
        { title: 'microsoft/DeepSeek-V3', stars: 24500, forks: 2800, description: 'DeepSeek V3: A Powerful MoE LLM', language: 'Python' },
        { title: 'anthropics/anthropic-cookbook', stars: 8200, forks: 890, description: 'A collection of notebooks', language: 'Jupyter' },
        { title: 'openai/openai-o3', stars: 15600, forks: 1200, description: 'Next generation AI model', language: 'Python' }
    ],
    arxiv: [
        { title: 'Attention Is All You Need', authors: 'Vaswani et al.', abstract: 'We propose a new architecture...' },
        { title: 'GPT-4 Technical Report', authors: 'OpenAI Team', abstract: 'We report the development of GPT-4...' }
    ],
    hackernews: [
        { title: 'Show HN: I built an AI coding assistant', score: 892, comments: 234 },
        { title: 'Ask HN: Best practices for LLM deployment?', score: 567, comments: 189 }
    ],
    blogs: [
        { title: 'Anthropic News: Claude 3.5 Release', author: 'Anthropic Team', published: '2小时前' },
        { title: 'OpenAI Blog: New API Features', author: 'OpenAI', published: '5小时前' }
    ]
};

// ============================================
// 初始化
// ============================================
document.addEventListener('DOMContentLoaded', () => {
    // 从本地存储加载数据
    loadFromStorage();
    
    // 初始化各模块
    initNavigation();
    initSources();
    initSettings();
    initActions();
    initAddSourceForm();
    
    // 更新UI
    updateTime();
    loadMockData();
    
    // 定时更新
    setInterval(updateTime, 1000);
});

// ============================================
// 本地存储管理
// ============================================
function loadFromStorage() {
    // 加载自定义信息源
    const savedSources = localStorage.getItem('sentinel-custom-sources');
    if (savedSources) {
        state.sources = JSON.parse(savedSources);
    }
    
    // 加载关键词
    const savedKeywords = localStorage.getItem('sentinel-keywords');
    if (savedKeywords) {
        state.keywords = JSON.parse(savedKeywords);
    }
    
    // 加载设置
    const savedSettings = localStorage.getItem('sentinel-settings');
    if (savedSettings) {
        state.settings = { ...state.settings, ...JSON.parse(savedSettings) };
    }
}

function saveToStorage() {
    localStorage.setItem('sentinel-custom-sources', JSON.stringify(state.sources));
    localStorage.setItem('sentinel-keywords', JSON.stringify(state.keywords));
    localStorage.setItem('sentinel-settings', JSON.stringify(state.settings));
}

// ============================================
// 导航切换
// ============================================
function initNavigation() {
    const navItems = document.querySelectorAll('.nav-item');
    const views = document.querySelectorAll('.view');
    const viewTitle = document.getElementById('viewTitle');
    
    navItems.forEach(item => {
        item.addEventListener('click', () => {
            const viewName = item.dataset.view;
            
            // 更新导航状态
            navItems.forEach(n => n.classList.remove('active'));
            item.classList.add('active');
            
            // 更新视图
            views.forEach(v => v.classList.remove('active'));
            document.getElementById(`${viewName}View`).classList.add('active');
            
            // 更新标题
            const titles = {
                dashboard: '概览',
                sources: '信息源',
                reports: '报告',
                settings: '配置'
            };
            viewTitle.textContent = titles[viewName] || viewName;
            
            state.currentView = viewName;
            
            // 如果是信息源视图，刷新显示
            if (viewName === 'sources') {
                renderSources();
            }
        });
    });
}

// ============================================
// 信息源管理
// ============================================
function initSources() {
    renderSources();
}

function renderSources(filter = 'all') {
    const grid = document.getElementById('sourcesGrid');
    grid.innerHTML = '';
    
    // 根据分类筛选
    let filteredBuiltIn = builtInSources;
    if (filter !== 'all') {
        if (filter === 'custom') {
            // 显示自定义源
            renderCustomSources(grid);
            return;
        }
        filteredBuiltIn = builtInSources.filter(s => s.category === filter);
    }
    
    // 渲染内置信息源
    filteredBuiltIn.forEach(config => {
        const source = state.sources.find(s => s.id === config.id) || { enabled: true, count: 0 };
        const card = createSourceCard(config, source);
        grid.appendChild(card);
    });
    
    // 如果显示全部，再添加自定义源
    if (filter === 'all') {
        renderCustomSources(grid);
    }
}

function renderCustomSources(container) {
    // 添加自定义源标题
    if (state.sources.length > 0) {
        const customTitle = document.createElement('div');
        customTitle.className = 'sources-section-title';
        customTitle.innerHTML = `<span class="sources-section-icon">✏️</span> 自定义信息源 <span class="sources-count">${state.sources.length}</span>`;
        container.appendChild(customTitle);
    }
    
    state.sources.forEach(source => {
        const card = createCustomSourceCard(source);
        container.appendChild(card);
    });
    
    // 如果没有自定义源，显示提示
    if (state.sources.length === 0) {
        const emptyHint = document.createElement('div');
        emptyHint.className = 'sources-empty-hint';
        emptyHint.innerHTML = `
            <p>还没有添加自定义信息源</p>
            <p class="hint-sub">点击上方的"添加信息源"按钮，或使用下方的快速添加模板</p>
        `;
        container.appendChild(emptyHint);
    }
}

function createSourceCard(config, source) {
    const card = document.createElement('div');
    card.className = 'source-card';
    card.id = `source-${config.id}`;
    
    // 获取分类标签
    const category = sourceCategories.find(c => c.id === config.category) || sourceCategories[0];
    
    card.innerHTML = `
        <div class="source-card-header">
            <div class="source-title-row">
                <span class="source-icon">${config.icon}</span>
                <span class="source-name">${config.name}</span>
            </div>
            <label class="source-toggle">
                <input type="checkbox" ${source.enabled ? 'checked' : ''} data-source="${config.id}">
                <span class="toggle-slider"></span>
            </label>
        </div>
        <span class="source-category-tag" data-category="${config.category}">${category.icon} ${category.name}</span>
        <p class="source-description">${config.description}</p>
        <div class="source-detail-hint" title="${config.detail}">
            💡 ${config.detail}
        </div>
        <div class="source-stats">
            <div class="source-stat">
                <div class="source-stat-value" id="count-${config.id}">${source.count}</div>
                <div class="source-stat-label">已采集</div>
            </div>
            <div class="source-stat">
                <div class="source-stat-value">-</div>
                <div class="source-stat-label">上次采集</div>
            </div>
        </div>
        <div class="source-card-actions">
            <button class="source-btn" onclick="collectSource('${config.id}')">立即采集</button>
        </div>
    `;
    
    // 开关事件
    const toggle = card.querySelector('input[type="checkbox"]');
    toggle.addEventListener('change', (e) => {
        source.enabled = e.target.checked;
        showToast(`${config.name}已${source.enabled ? '启用' : '禁用'}`, 'success');
    });
    
    return card;
}

function createCustomSourceCard(source) {
    const card = document.createElement('div');
    card.className = 'source-card source-card-custom';
    card.id = `custom-source-${source.id}`;
    
    card.innerHTML = `
        <div class="source-card-header">
            <div class="source-title-row">
                <span class="source-icon">🔗</span>
                <span class="source-name">${source.name}</span>
            </div>
            <label class="source-toggle">
                <input type="checkbox" ${source.enabled ? 'checked' : ''} data-source="custom-${source.id}">
                <span class="toggle-slider"></span>
            </label>
        </div>
        <span class="source-category-tag" data-category="custom">✏️ 自定义</span>
        <p class="source-description">${source.description || '自定义RSS订阅源'}</p>
        <div class="source-url-preview">
            <span class="url-label">订阅地址:</span>
            <span class="url-value" title="${source.url}">${truncateUrl(source.url)}</span>
        </div>
        ${source.tags && source.tags.length > 0 ? `
        <div class="source-tags">
            ${source.tags.map(tag => `<span class="source-tag">${tag}</span>`).join('')}
        </div>
        ` : ''}
        <div class="source-stats">
            <div class="source-stat">
                <div class="source-stat-value" id="count-custom-${source.id}">${source.count || 0}</div>
                <div class="source-stat-label">已采集</div>
            </div>
            <div class="source-stat">
                <div class="source-stat-value">-</div>
                <div class="source-stat-label">上次采集</div>
            </div>
        </div>
        <div class="source-card-actions">
            <button class="source-btn" onclick="collectCustomSource('${source.id}')">采集</button>
            <button class="source-btn source-btn-danger" onclick="deleteCustomSource('${source.id}')">删除</button>
        </div>
    `;
    
    // 开关事件
    const toggle = card.querySelector('input[type="checkbox"]');
    toggle.addEventListener('change', (e) => {
        source.enabled = e.target.checked;
        showToast(`${source.name}已${source.enabled ? '启用' : '禁用'}`, 'success');
        saveToStorage();
    });
    
    return card;
}

function truncateUrl(url, maxLength = 40) {
    if (url.length <= maxLength) return url;
    return url.substring(0, maxLength) + '...';
}

// 采集自定义源
async function collectCustomSource(sourceId) {
    showLoading(true);
    const source = state.sources.find(s => s.id === sourceId);
    if (!source) return;
    
    addLog(`开始采集 ${source.name}...`, 'pending');
    await delay(1000);
    
    source.count = (source.count || 0) + 1;
    const countEl = document.getElementById(`count-custom-${sourceId}`);
    if (countEl) countEl.textContent = source.count;
    
    addLog(`${source.name} 采集完成`, 'success');
    showLoading(false);
    showToast(`${source.name} 采集完成`, 'success');
    saveToStorage();
}

// 删除自定义源
function deleteCustomSource(sourceId) {
    const source = state.sources.find(s => s.id === sourceId);
    if (!source) return;
    
    if (confirm(`确定要删除 "${source.name}" 吗？`)) {
        state.sources = state.sources.filter(s => s.id !== sourceId);
        saveToStorage();
        renderSources();
        showToast(`已删除 ${source.name}`, 'success');
    }
}

// ============================================
// 添加信息源表单
// ============================================
function initAddSourceForm() {
    const addBtn = document.getElementById('addSourceBtn');
    const modal = document.getElementById('addSourceModal');
    const closeBtn = document.getElementById('closeModal');
    const form = document.getElementById('addSourceForm');
    const quickAddSection = document.getElementById('quickAddSection');
    
    // 点击添加按钮
    addBtn.addEventListener('click', () => {
        modal.classList.add('active');
        renderQuickAddTemplates();
    });
    
    // 点击关闭
    closeBtn.addEventListener('click', () => {
        modal.classList.remove('active');
    });
    
    // 点击模态框外部关闭
    modal.addEventListener('click', (e) => {
        if (e.target === modal) {
            modal.classList.remove('active');
        }
    });
    
    // 表单提交
    form.addEventListener('submit', (e) => {
        e.preventDefault();
        submitCustomSource();
    });
    
    // 分类标签切换
    const categoryTabs = document.querySelectorAll('.category-tab');
    categoryTabs.forEach(tab => {
        tab.addEventListener('click', () => {
            categoryTabs.forEach(t => t.classList.remove('active'));
            tab.classList.add('active');
            filterQuickAddTemplates(tab.dataset.category);
        });
    });
}

function renderQuickAddTemplates(category = 'all') {
    const container = document.getElementById('quickAddTemplates');
    if (!container) return;
    
    container.innerHTML = '';
    
    const templates = category === 'all' 
        ? quickAddTemplates 
        : quickAddTemplates.filter(t => t.category === category);
    
    templates.forEach(template => {
        // 检查是否已添加
        const isAdded = state.sources.some(s => s.url === template.url);
        
        const card = document.createElement('div');
        card.className = `quick-add-card ${isAdded ? 'added' : ''}`;
        card.innerHTML = `
            <div class="quick-add-icon">${template.icon}</div>
            <div class="quick-add-info">
                <div class="quick-add-name">${template.name}</div>
                <div class="quick-add-desc">${template.description}</div>
                <div class="quick-add-tags">
                    ${template.tags.map(tag => `<span class="quick-tag">${tag}</span>`).join('')}
                </div>
            </div>
            <button class="quick-add-btn ${isAdded ? 'added' : ''}" 
                    ${isAdded ? 'disabled' : ''} 
                    onclick="quickAddSource('${template.url}', '${template.name}', '${template.description}', '${template.icon}')">
                ${isAdded ? '✓ 已添加' : '+ 添加'}
            </button>
        `;
        container.appendChild(card);
    });
}

function filterQuickAddTemplates(category) {
    renderQuickAddTemplates(category);
}

function quickAddSource(url, name, description, icon) {
    // 检查是否已存在
    if (state.sources.some(s => s.url === url)) {
        showToast('该信息源已添加', 'warning');
        return;
    }
    
    const newSource = {
        id: 'custom-' + Date.now(),
        name: name,
        url: url,
        description: description,
        icon: icon,
        enabled: true,
        count: 0,
        tags: []
    };
    
    state.sources.push(newSource);
    saveToStorage();
    renderSources();
    renderQuickAddTemplates();
    
    showToast(`已添加 "${name}"`, 'success');
}

function submitCustomSource() {
    const name = document.getElementById('sourceName').value.trim();
    const url = document.getElementById('sourceUrl').value.trim();
    const description = document.getElementById('sourceDesc').value.trim();
    
    if (!name || !url) {
        showToast('请填写名称和订阅地址', 'error');
        return;
    }
    
    // 简单URL验证
    if (!isValidUrl(url)) {
        showToast('请输入有效的URL地址', 'error');
        return;
    }
    
    // 检查是否已存在
    if (state.sources.some(s => s.url === url)) {
        showToast('该信息源已添加', 'warning');
        return;
    }
    
    const newSource = {
        id: 'custom-' + Date.now(),
        name: name,
        url: url,
        description: description || '自定义RSS订阅源',
        enabled: true,
        count: 0,
        tags: []
    };
    
    state.sources.push(newSource);
    saveToStorage();
    
    // 重置表单
    document.getElementById('addSourceForm').reset();
    document.getElementById('addSourceModal').classList.remove('active');
    
    renderSources();
    showToast(`已添加 "${name}"`, 'success');
}

function isValidUrl(string) {
    try {
        new URL(string);
        return true;
    } catch (_) {
        return false;
    }
}

// ============================================
// 设置管理
// ============================================
function initSettings() {
    const tokenInput = document.getElementById('twitterToken');
    const keywordsInput = document.getElementById('newKeyword');
    const keywordsTags = document.getElementById('keywordsTags');
    
    // 恢复设置值
    tokenInput.value = state.settings.twitterToken || '';
    
    // Token输入
    tokenInput.addEventListener('change', (e) => {
        state.settings.twitterToken = e.target.value;
    });
    
    // 关键词添加
    keywordsInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter' && e.target.value.trim()) {
            e.preventDefault();
            addKeyword(e.target.value.trim());
            e.target.value = '';
        }
    });
    
    // 调度开关
    document.getElementById('morningEnabled').addEventListener('change', (e) => {
        state.settings.morningEnabled = e.target.checked;
    });
    
    document.getElementById('eveningEnabled').addEventListener('change', (e) => {
        state.settings.eveningEnabled = e.target.checked;
    });
    
    // 保存按钮
    document.getElementById('saveSettingsBtn').addEventListener('click', saveSettings);
    
    // 恢复调度设置
    document.getElementById('morningEnabled').checked = state.settings.morningEnabled;
    document.getElementById('eveningEnabled').checked = state.settings.eveningEnabled;
    
    // 初始化关键词标签
    state.keywords.forEach(kw => addKeyword(kw, false));
}

function addKeyword(keyword, shouldRender = true) {
    if (state.keywords.includes(keyword)) return;
    
    state.keywords.push(keyword);
    
    if (shouldRender) {
        const container = document.getElementById('keywordsTags');
        const tag = document.createElement('span');
        tag.className = 'tag';
        tag.innerHTML = `
            ${keyword}
            <span class="tag-remove" onclick="removeKeyword('${keyword}')">×</span>
        `;
        container.insertBefore(tag, container.lastElementChild);
    }
}

function removeKeyword(keyword) {
    state.keywords = state.keywords.filter(k => k !== keyword);
    const tags = document.querySelectorAll('.tag');
    tags.forEach(tag => {
        if (tag.textContent.trim().startsWith(keyword)) {
            tag.remove();
        }
    });
}

function saveSettings() {
    saveToStorage();
    showToast('配置已保存', 'success');
}

// ============================================
// 操作按钮
// ============================================
function initActions() {
    // 采集按钮
    document.getElementById('collectBtn').addEventListener('click', collectAll);
    
    // 报告按钮
    document.getElementById('reportBtn').addEventListener('click', () => {
        document.querySelector('[data-view="reports"]').click();
    });
    
    document.getElementById('generateReportBtn').addEventListener('click', generateReport);
}

async function collectAll() {
    showLoading(true);
    addLog('开始采集所有信息源...', 'pending');
    
    const statusIndicator = document.querySelector('.status-indicator');
    statusIndicator.classList.add('working');
    document.querySelector('.status-text').textContent = '采集中';
    
    // 模拟采集过程
    await delay(2000);
    
    // 更新统计数据
    state.stats.github = mockData.github.length;
    state.stats.arxiv = mockData.arxiv.length;
    state.stats.hn = mockData.hackernews.length;
    state.stats.blogs = mockData.blogs.length;
    
    // 更新UI
    updateStats();
    
    // 更新信息源卡片
    [...builtInSources, ...state.sources].forEach(config => {
        const count = state.stats[config.id] || config.count || 0;
        const countEl = document.getElementById(`count-${config.id}`);
        if (countEl) countEl.textContent = count;
    });
    
    // 更新热门内容
    updateHotContent();
    
    addLog('采集完成', 'success');
    
    showLoading(false);
    statusIndicator.classList.remove('working');
    document.querySelector('.status-text').textContent = '就绪';
    
    showToast('采集完成', 'success');
}

async function collectSource(sourceId) {
    showLoading(true);
    addLog(`开始采集 ${sourceId}...`, 'pending');
    
    await delay(1000);
    
    // 更新计数
    const mockCounts = { github: 3, arxiv: 2, hackernews: 2, blogs: 2, twitter: 0 };
    state.stats[sourceId] = mockCounts[sourceId] || 0;
    
    const countEl = document.getElementById(`count-${sourceId}`);
    if (countEl) countEl.textContent = state.stats[sourceId];
    
    addLog(`${sourceId} 采集完成`, 'success');
    showLoading(false);
    showToast(`${sourceId} 采集完成`, 'success');
}

function generateReport() {
    const type = document.getElementById('reportType').value;
    const preview = document.getElementById('reportPreview');
    
    // 生成模拟报告内容
    const reportHtml = `
        <div class="report-content">
            <h1>${type === 'morning' ? '🌅' : '🌙'} AI前沿哨兵 - ${new Date().toLocaleDateString('zh-CN')} ${type === 'morning' ? '晨报' : '晚报'}</h1>
            
            <h2>📊 概览</h2>
            <table style="width: 100%; border-collapse: collapse; margin: 16px 0;">
                <tr style="border-bottom: 1px solid var(--border);">
                    <td style="padding: 8px;">学术论文</td>
                    <td>${state.stats.arxiv} 篇</td>
                </tr>
                <tr style="border-bottom: 1px solid var(--border);">
                    <td style="padding: 8px;">开源项目</td>
                    <td>${state.stats.github} 个</td>
                </tr>
                <tr style="border-bottom: 1px solid var(--border);">
                    <td style="padding: 8px;">社区讨论</td>
                    <td>${state.stats.hn} 条</td>
                </tr>
            </table>
            
            <h2>⭐ 热门项目</h2>
            ${mockData.github.map(p => `
                <h3><a href="#">${p.title}</a></h3>
                <p>${p.description}</p>
                <p style="color: var(--text-muted); font-size: 12px;">⭐ ${p.stars.toLocaleString()} | 🍴 ${p.forks} | 📝 ${p.language}</p>
            `).join('')}
            
            <h2>📚 重要论文</h2>
            ${mockData.arxiv.map(p => `
                <h3><a href="#">${p.title}</a></h3>
                <p style="color: var(--text-muted);">${p.authors}</p>
                <p>${p.abstract}</p>
            `).join('')}
            
            <hr style="border: none; border-top: 1px solid var(--border); margin: 32px 0;">
            <p style="color: var(--text-muted); font-size: 12px;">由 AI前沿哨兵 自动生成</p>
        </div>
    `;
    
    preview.innerHTML = reportHtml;
    showToast('报告已生成', 'success');
}

// ============================================
// UI更新函数
// ============================================
function updateStats() {
    document.getElementById('statGithub').textContent = state.stats.github;
    document.getElementById('statArxiv').textContent = state.stats.arxiv;
    document.getElementById('statHN').textContent = state.stats.hn;
    document.getElementById('statBlogs').textContent = state.stats.blogs;
}

function updateHotContent(filter = 'all') {
    const container = document.getElementById('hotContent');
    container.innerHTML = '';
    
    let items = [];
    
    if (filter === 'all' || filter === 'projects') {
        mockData.github.forEach(item => {
            items.push({
                type: 'projects',
                title: item.title,
                meta: `⭐ ${item.stars.toLocaleString()} | ${item.language}`,
                score: item.stars
            });
        });
    }
    
    if (filter === 'all' || filter === 'papers') {
        mockData.arxiv.forEach(item => {
            items.push({
                type: 'papers',
                title: item.title,
                meta: item.authors,
                score: 100
            });
        });
    }
    
    if (filter === 'all' || filter === 'discussions') {
        mockData.hackernews.forEach(item => {
            items.push({
                type: 'discussions',
                title: item.title,
                meta: `💬 ${item.comments} 评论`,
                score: item.score
            });
        });
    }
    
    // 按分数排序
    items.sort((a, b) => b.score - a.score);
    
    items.forEach(item => {
        const el = document.createElement('div');
        el.className = 'content-item';
        el.innerHTML = `
            <div>
                <div class="content-item-header">
                    <span class="content-source">${item.type}</span>
                </div>
                <a href="#" class="content-title">${item.title}</a>
                <div class="content-meta">${item.meta}</div>
            </div>
            <div class="content-score">
                <div class="score-value">${item.score}</div>
                <div class="score-label">热度</div>
            </div>
        `;
        container.appendChild(el);
    });
    
    // 过滤标签
    const tabs = document.querySelectorAll('.tab');
    tabs.forEach(tab => {
        tab.addEventListener('click', () => {
            tabs.forEach(t => t.classList.remove('active'));
            tab.classList.add('active');
            updateHotContent(tab.dataset.filter);
        });
    });
}

function loadMockData() {
    updateStats();
    updateHotContent();
}

function updateTime() {
    const timeEl = document.getElementById('currentTime');
    if (timeEl) {
        const now = new Date();
        timeEl.textContent = now.toLocaleString('zh-CN', {
            year: 'numeric',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit'
        });
    }
}

function addLog(message, status = 'pending') {
    const logList = document.getElementById('logList');
    const emptyLog = logList.querySelector('.log-empty');
    if (emptyLog) emptyLog.remove();
    
    const log = document.createElement('div');
    log.className = 'log-item';
    
    const now = new Date();
    const time = now.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
    
    log.innerHTML = `
        <span class="log-time">${time}</span>
        <span class="log-message">${message}</span>
        <span class="log-status ${status}">${status === 'pending' ? '进行中' : status === 'success' ? '完成' : '错误'}</span>
    `;
    
    logList.insertBefore(log, logList.firstChild);
    
    // 保持最多20条
    while (logList.children.length > 20) {
        logList.removeChild(logList.lastChild);
    }
    
    state.logs.unshift({ time: now.toISOString(), message, status });
}

function showLoading(show) {
    const overlay = document.getElementById('loadingOverlay');
    overlay.style.display = show ? 'flex' : 'none';
}

function showToast(message, type = 'info') {
    const container = document.getElementById('toastContainer');
    const toast = document.createElement('div');
    toast.className = `toast toast-${type}`;
    toast.textContent = message;
    
    container.appendChild(toast);
    
    // 动画
    requestAnimationFrame(() => {
        toast.classList.add('show');
    });
    
    // 自动移除
    setTimeout(() => {
        toast.classList.remove('show');
        setTimeout(() => toast.remove(), 300);
    }, 3000);
}

function delay(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

// ============================================
// 辅助函数
// ============================================
function getSourceById(id) {
    return builtInSources.find(s => s.id === id) || state.sources.find(s => s.id === id);
}


path: webui/index.html
content: <!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI前沿哨兵 - 控制台</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="app">
        <!-- 侧边栏 -->
        <aside class="sidebar">
            <div class="logo">
                <span class="logo-icon">⌘</span>
                <span class="logo-text">哨兵</span>
            </div>
            
            <nav class="nav">
                <button class="nav-item active" data-view="dashboard">
                    <span class="nav-icon">◈</span>
                    <span class="nav-label">概览</span>
                </button>
                <button class="nav-item" data-view="sources">
                    <span class="nav-icon">◎</span>
                    <span class="nav-label">信息源</span>
                </button>
                <button class="nav-item" data-view="reports">
                    <span class="nav-icon">◫</span>
                    <span class="nav-label">报告</span>
                </button>
                <button class="nav-item" data-view="settings">
                    <span class="nav-icon">⚙</span>
                    <span class="nav-label">配置</span>
                </button>
            </nav>
            
            <div class="sidebar-footer">
                <div class="status-indicator">
                    <span class="status-dot"></span>
                    <span class="status-text">就绪</span>
                </div>
            </div>
        </aside>
        
        <!-- 主内容区 -->
        <main class="main">
            <!-- 顶部栏 -->
            <header class="header">
                <div class="header-title">
                    <h1 id="viewTitle">概览</h1>
                    <span class="header-time" id="currentTime"></span>
                </div>
                <div class="header-actions">
                    <button class="btn btn-secondary" id="collectBtn">
                        <span>⏱</span> 立即采集
                    </button>
                    <button class="btn btn-primary" id="reportBtn">
                        <span>◧</span> 生成报告
                    </button>
                </div>
            </header>
            
            <!-- 内容区域 -->
            <div class="content">
                <!-- ==================== 概览视图 ==================== -->
                <div class="view active" id="dashboardView">
                    <!-- 统计卡片 -->
                    <div class="stats-grid">
                        <div class="stat-card">
                            <div class="stat-value" id="statGithub">-</div>
                            <div class="stat-label">GitHub项目</div>
                        </div>
                        <div class="stat-card">
                            <div class="stat-value" id="statArxiv">-</div>
                            <div class="stat-label">arXiv论文</div>
                        </div>
                        <div class="stat-card">
                            <div class="stat-value" id="statHN">-</div>
                            <div class="stat-label">HN讨论</div>
                        </div>
                        <div class="stat-card">
                            <div class="stat-value" id="statBlogs">-</div>
                            <div class="stat-label">博客文章</div>
                        </div>
                    </div>
                    
                    <!-- 热门内容 -->
                    <section class="section">
                        <div class="section-header">
                            <h2>热门内容</h2>
                            <div class="filter-tabs">
                                <button class="tab active" data-filter="all">全部</button>
                                <button class="tab" data-filter="projects">项目</button>
                                <button class="tab" data-filter="papers">论文</button>
                                <button class="tab" data-filter="discussions">讨论</button>
                            </div>
                        </div>
                        <div class="content-list" id="hotContent">
                            <!-- 动态内容 -->
                        </div>
                    </section>
                    
                    <!-- 最近采集 -->
                    <section class="section">
                        <div class="section-header">
                            <h2>最近采集记录</h2>
                        </div>
                        <div class="log-list" id="logList">
                            <div class="log-empty">暂无采集记录</div>
                        </div>
                    </section>
                </div>
                
                <!-- ==================== 信息源视图 ==================== -->
                <div class="view" id="sourcesView">
                    <!-- 信息源页面头部 -->
                    <div class="sources-header">
                        <div class="sources-header-left">
                            <h2>信息源管理</h2>
                            <p class="sources-header-desc">管理和监控您的AI情报采集来源</p>
                        </div>
                        <button class="btn btn-primary" id="addSourceBtn">
                            <span>+</span> 添加信息源
                        </button>
                    </div>
                    
                    <!-- 分类标签 -->
                    <div class="sources-filters">
                        <button class="filter-btn active" data-filter="all">
                            <span>📦</span> 全部
                        </button>
                        <button class="filter-btn" data-filter="official-blog">
                            <span>🏢</span> 官方博客
                        </button>
                        <button class="filter-btn" data-filter="tech-community">
                            <span>💬</span> 技术社区
                        </button>
                        <button class="filter-btn" data-filter="academic">
                            <span>📚</span> 学术论文
                        </button>
                        <button class="filter-btn" data-filter="social">
                            <span>🌐</span> 社交媒体
                        </button>
                        <button class="filter-btn" data-filter="tutorial">
                            <span>📖</span> 教程博客
                        </button>
                        <button class="filter-btn" data-filter="custom">
                            <span>✏️</span> 自定义
                        </button>
                    </div>
                    
                    <!-- 信息源网格 -->
                    <div class="sources-grid" id="sourcesGrid">
                        <!-- 动态生成 -->
                    </div>
                    
                    <!-- 帮助提示 -->
                    <div class="sources-help">
                        <div class="help-card">
                            <div class="help-icon">💡</div>
                            <div class="help-content">
                                <h4>什么是信息源？</h4>
                                <p>信息源是您获取AI情报的渠道，比如某个博客、论坛或论文网站。每个信息源会定期采集最新内容，帮助您及时了解行业动态。</p>
                            </div>
                        </div>
                        <div class="help-card">
                            <div class="help-icon">🔗</div>
                            <div class="help-content">
                                <h4>如何添加自定义信息源？</h4>
                                <p>点击"添加信息源"按钮，填写博客名称和RSS订阅地址即可。大多数博客网站底部都有RSS图标，点击即可获取订阅地址。</p>
                            </div>
                        </div>
                        <div class="help-card">
                            <div class="help-icon">⚡</div>
                            <div class="help-content">
                                <h4>快速添加模板</h4>
                                <p>在添加信息源弹窗中，我们提供了一些常用的AI博客模板，点击即可一键添加，省去手动输入的麻烦。</p>
                            </div>
                        </div>
                        <div class="help-card">
                            <div class="help-icon">🐦</div>
                            <div class="help-content">
                                <h4>如何添加Twitter信息源？</h4>
                                <p>访问<a href="https://developer.twitter.com" target="_blank">Twitter开发者平台</a>申请Bearer Token，然后在"配置"页面填入即可启用Twitter采集功能。</p>
                                <p style="color: #ffa726; font-size: 12px;">⚠️ 需要Twitter开发者账号（通常1-2天审核）</p>
                            </div>
                        </div>
                    </div>
                </div>
                
                <!-- ==================== 报告视图 ==================== -->
                <div class="view" id="reportsView">
                    <div class="report-controls">
                        <select class="select" id="reportType">
                            <option value="morning">晨报</option>
                            <option value="evening">晚报</option>
                        </select>
                        <button class="btn btn-primary" id="generateReportBtn">生成报告</button>
                    </div>
                    <div class="report-preview" id="reportPreview">
                        <div class="report-empty">
                            <p>点击"生成报告"按钮预览报告内容</p>
                        </div>
                    </div>
                </div>
                
                <!-- ==================== 配置视图 ==================== -->
                <div class="view" id="settingsView">
                    <div class="settings-group">
                        <h3>API配置</h3>
                        <div class="setting-item">
                            <label>Twitter Bearer Token</label>
                            <input type="password" id="twitterToken" placeholder="请输入Token">
                            <span class="setting-hint">用于采集Twitter内容，可选配置。获取方式：<a href="https://developer.twitter.com" target="_blank">Twitter开发者平台</a></span>
                        </div>
                    </div>
                    
                    <div class="settings-group">
                        <h3>关键词设置</h3>
                        <div class="setting-item">
                            <label>关注关键词</label>
                            <div class="tags-input" id="keywordsTags">
                                <input type="text" id="newKeyword" placeholder="输入关键词后按回车添加">
                            </div>
                            <span class="setting-hint">相关内容会获得额外加分。例如：LLM、GPT、Claude、Stable Diffusion 等</span>
                        </div>
                    </div>
                    
                    <div class="settings-group">
                        <h3>调度设置</h3>
                        <div class="setting-item">
                            <label class="toggle-label">
                                <input type="checkbox" id="morningEnabled" checked>
                                <span>晨报 (08:00) - 推送隔夜重要动态</span>
                            </label>
                        </div>
                        <div class="setting-item">
                            <label class="toggle-label">
                                <input type="checkbox" id="eveningEnabled" checked>
                                <span>晚报 (20:00) - 推送全天热点汇总</span>
                            </label>
                        </div>
                    </div>
                    
                    <div class="settings-actions">
                        <button class="btn btn-primary" id="saveSettingsBtn">保存配置</button>
                    </div>
                </div>
            </div>
        </main>
    </div>
    
    <!-- ==================== 添加信息源模态框 ==================== -->
    <div class="modal" id="addSourceModal">
        <div class="modal-content">
            <div class="modal-header">
                <h3>添加信息源</h3>
                <button class="modal-close" id="closeModal">×</button>
            </div>
            
            <div class="modal-body">
                <!-- 快速添加区域 -->
                <div class="quick-add-section" id="quickAddSection">
                    <h4>🏎️ 快速添加 - 热门AI博客</h4>
                    <p class="quick-add-hint">点击即可一键添加以下常用信息源</p>
                    
                    <!-- 分类标签 -->
                    <div class="category-tabs">
                        <button class="category-tab active" data-category="all">全部</button>
                        <button class="category-tab" data-category="official-blog">🏢 官方博客</button>
                        <button class="category-tab" data-category="tutorial">📖 教程博客</button>
                    </div>
                    
                    <div class="quick-add-grid" id="quickAddTemplates">
                        <!-- 动态生成 -->
                    </div>
                </div>
                
                <!-- 手动添加表单 -->
                <div class="manual-add-section">
                    <h4>✏️ 手动添加自定义源</h4>
                    <form id="addSourceForm">
                        <div class="form-group">
                            <label for="sourceName">信息源名称 *</label>
                            <input type="text" id="sourceName" placeholder="例如：OpenAI 官方博客" required>
                        </div>
                        <div class="form-group">
                            <label for="sourceUrl">RSS订阅地址 *</label>
                            <input type="url" id="sourceUrl" placeholder="https://example.com/feed.xml" required>
                            <span class="form-hint">大多数网站底部有RSS图标，或在URL后加 /feed、/rss</span>
                        </div>
                        <div class="form-group">
                            <label for="sourceDesc">描述（可选）</label>
                            <input type="text" id="sourceDesc" placeholder="简短描述这个信息源">
                        </div>
                        <button type="submit" class="btn btn-primary btn-block">添加信息源</button>
                    </form>
                </div>
                
                <!-- RSS说明 -->
                <div class="rss-guide">
                    <h4>🔍 如何找到RSS订阅地址？</h4>
                    <ol>
                        <li>打开目标博客或新闻网站</li>
                        <li>查找页面底部是否有 <code>RSS</code>、<code>Feed</code> 或 <code>订阅</code> 图标</li>
                        <li>点击RSS图标，复制浏览器地址栏的URL</li>
                        <li>如果找不到，可以尝试在网站域名后加 <code>/feed</code>、<code>/rss</code> 或 <code>/atom.xml</code></li>
                    </ol>
                    <p class="rss-tip">💡 <strong>提示：</strong>大多数主流博客平台（WordPress、Medium等）都支持RSS</p>
                </div>
            </div>
        </div>
    </div>
    
    <!-- Toast通知 -->
    <div class="toast-container" id="toastContainer"></div>
    
    <!-- 加载状态 -->
    <div class="loading-overlay" id="loadingOverlay">
        <div class="loading-spinner"></div>
        <span>采集中...</span>
    </div>
    
    <script src="app.js"></script>
    
    <!-- 信息源分类筛选脚本 -->
    <script>
        // 页面加载完成后绑定分类筛选事件
        document.addEventListener('DOMContentLoaded', () => {
            const filterBtns = document.querySelectorAll('.sources-filters .filter-btn');
            filterBtns.forEach(btn => {
                btn.addEventListener('click', () => {
                    filterBtns.forEach(b => b.classList.remove('active'));
                    btn.classList.add('active');
                    const filter = btn.dataset.filter;
                    if (typeof renderSources === 'function') {
                        renderSources(filter);
                    }
                });
            });
        });
    </script>
</body>
</html>


path: webui/style.css
content: /* AI前沿哨兵 - 控制台样式 */
/* 深色主题，克制动效，强调功能性 */

/* ============================================
   变量定义
   ============================================ */
:root {
    /* 色彩系统 */
    --bg-primary: #0d0d0f;
    --bg-secondary: #141418;
    --bg-tertiary: #1a1a1f;
    --bg-hover: #222228;
    
    --text-primary: #e8e8eb;
    --text-secondary: #8e8e96;
    --text-muted: #5c5c64;
    
    --accent: #00d4aa;      /* 青色主色 */
    --accent-hover: #00e8bb;
    --accent-muted: rgba(0, 212, 170, 0.15);
    
    --warning: #f59e0b;
    --error: #ef4444;
    --success: #10b981;
    
    /* 边框 */
    --border: #2a2a32;
    --border-light: #3a3a44;
    
    /* 间距 */
    --sidebar-width: 200px;
    --header-height: 64px;
    --radius: 8px;
    --radius-sm: 4px;
    --radius-lg: 12px;
}

/* ============================================
   基础样式
   ============================================ */
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    font-size: 14px;
    line-height: 1.5;
    color: var(--text-primary);
    background: var(--bg-primary);
    -webkit-font-smoothing: antialiased;
}

/* ============================================
   应用布局
   ============================================ */
.app {
    display: flex;
    min-height: 100vh;
}

/* ============================================
   侧边栏
   ============================================ */
.sidebar {
    width: var(--sidebar-width);
    background: var(--bg-secondary);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
}

.logo {
    padding: 24px 20px;
    display: flex;
    align-items: center;
    gap: 12px;
}

.logo-icon {
    font-size: 24px;
    color: var(--accent);
}

.logo-text {
    font-size: 18px;
    font-weight: 600;
    letter-spacing: 2px;
}

.nav {
    flex: 1;
    padding: 8px 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.nav-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    background: transparent;
    border: none;
    border-radius: var(--radius);
    color: var(--text-secondary);
    cursor: pointer;
    transition: all 0.15s ease;
    text-align: left;
    width: 100%;
    font-size: 14px;
}

.nav-item:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.nav-item.active {
    background: var(--accent-muted);
    color: var(--accent);
}

.nav-icon {
    font-size: 16px;
    width: 20px;
    text-align: center;
}

.sidebar-footer {
    padding: 16px 20px;
    border-top: 1px solid var(--border);
}

.status-indicator {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    color: var(--text-muted);
}

.status-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--success);
}

.status-indicator.working .status-dot {
    background: var(--warning);
    animation: pulse 1.5s infinite;
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.5; }
}

/* ============================================
   主内容区
   ============================================ */
.main {
    flex: 1;
    margin-left: var(--sidebar-width);
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

/* 顶部栏 */
.header {
    height: var(--header-height);
    padding: 0 32px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: space-between;
    background: var(--bg-secondary);
    position: sticky;
    top: 0;
    z-index: 100;
}

.header-title {
    display: flex;
    align-items: baseline;
    gap: 16px;
}

.header-title h1 {
    font-size: 20px;
    font-weight: 600;
}

.header-time {
    font-size: 13px;
    color: var(--text-muted);
}

.header-actions {
    display: flex;
    gap: 12px;
}

/* ============================================
   按钮
   ============================================ */
.btn {
    padding: 10px 20px;
    border-radius: var(--radius);
    border: none;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.15s ease;
    display: inline-flex;
    align-items: center;
    gap: 8px;
}

.btn-primary {
    background: var(--accent);
    color: var(--bg-primary);
}

.btn-primary:hover {
    background: var(--accent-hover);
}

.btn-secondary {
    background: var(--bg-tertiary);
    color: var(--text-primary);
    border: 1px solid var(--border);
}

.btn-secondary:hover {
    background: var(--bg-hover);
    border-color: var(--border-light);
}

.btn-block {
    width: 100%;
    justify-content: center;
}

/* ============================================
   内容区域
   ============================================ */
.content {
    flex: 1;
    padding: 32px;
}

.view {
    display: none;
}

.view.active {
    display: block;
}

/* ============================================
   统计卡片
   ============================================ */
.stats-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 16px;
    margin-bottom: 32px;
}

.stat-card {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 20px 24px;
}

.stat-value {
    font-size: 32px;
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 4px;
}

.stat-label {
    font-size: 13px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* ============================================
   区块
   ============================================ */
.section {
    margin-bottom: 32px;
}

.section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 16px;
}

.section-header h2 {
    font-size: 16px;
    font-weight: 600;
}

/* 过滤标签 */
.filter-tabs {
    display: flex;
    gap: 4px;
}

.tab {
    padding: 6px 12px;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 12px;
    cursor: pointer;
    transition: all 0.15s ease;
}

.tab:hover {
    border-color: var(--border-light);
    color: var(--text-primary);
}

.tab.active {
    background: var(--accent-muted);
    border-color: var(--accent);
    color: var(--accent);
}

/* ============================================
   内容列表
   ============================================ */
.content-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.content-item {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 16px 20px;
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 16px;
    align-items: center;
    transition: border-color 0.15s ease;
}

.content-item:hover {
    border-color: var(--border-light);
}

.content-item-header {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 8px;
}

.content-source {
    font-size: 11px;
    padding: 2px 8px;
    background: var(--bg-tertiary);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    text-transform: uppercase;
}

.content-title {
    font-size: 14px;
    font-weight: 500;
    color: var(--text-primary);
    text-decoration: none;
}

.content-title:hover {
    color: var(--accent);
}

.content-meta {
    font-size: 12px;
    color: var(--text-muted);
    display: flex;
    align-items: center;
    gap: 12px;
}

.content-score {
    text-align: right;
}

.score-value {
    font-size: 18px;
    font-weight: 600;
    color: var(--accent);
}

.score-label {
    font-size: 11px;
    color: var(--text-muted);
}

/* ============================================
   日志列表
   ============================================ */
.log-list {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    max-height: 300px;
    overflow-y: auto;
}

.log-empty {
    padding: 40px;
    text-align: center;
    color: var(--text-muted);
}

.log-item {
    padding: 12px 16px;
    border-bottom: 1px solid var(--border);
    display: flex;
    align-items: center;
    gap: 12px;
    font-size: 13px;
}

.log-item:last-child {
    border-bottom: none;
}

.log-time {
    color: var(--text-muted);
    font-family: monospace;
    font-size: 12px;
}

.log-message {
    flex: 1;
}

.log-status {
    padding: 2px 8px;
    border-radius: var(--radius-sm);
    font-size: 11px;
    text-transform: uppercase;
}

.log-status.success {
    background: rgba(16, 185, 129, 0.15);
    color: var(--success);
}

.log-status.error {
    background: rgba(239, 68, 68, 0.15);
    color: var(--error);
}

.log-status.pending {
    background: rgba(245, 158, 11, 0.15);
    color: var(--warning);
}

/* ============================================
   信息源管理页面
   ============================================ */
.sources-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    margin-bottom: 24px;
}

.sources-header-left h2 {
    font-size: 20px;
    font-weight: 600;
    margin-bottom: 4px;
}

.sources-header-desc {
    color: var(--text-muted);
    font-size: 14px;
}

/* 分类筛选按钮 */
.sources-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 24px;
    padding: 16px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.filter-btn {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-secondary);
    font-size: 13px;
    cursor: pointer;
    transition: all 0.15s ease;
}

.filter-btn:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
    border-color: var(--border-light);
}

.filter-btn.active {
    background: var(--accent-muted);
    border-color: var(--accent);
    color: var(--accent);
}

/* 信息源网格 */
.sources-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 16px;
    margin-bottom: 32px;
}

.source-card {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 20px;
    transition: border-color 0.15s ease;
}

.source-card:hover {
    border-color: var(--border-light);
}

.source-card-custom {
    border-color: var(--accent);
    border-style: dashed;
}

.source-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
}

.source-title-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

.source-icon {
    font-size: 18px;
}

.source-name {
    font-size: 15px;
    font-weight: 600;
}

.source-category-tag {
    display: inline-block;
    padding: 4px 10px;
    background: var(--bg-tertiary);
    border-radius: var(--radius-sm);
    font-size: 11px;
    color: var(--text-muted);
    margin-bottom: 12px;
}

.source-description {
    color: var(--text-secondary);
    font-size: 13px;
    line-height: 1.6;
    margin-bottom: 12px;
}

.source-detail-hint {
    padding: 10px 12px;
    background: var(--bg-tertiary);
    border-radius: var(--radius-sm);
    font-size: 12px;
    color: var(--text-muted);
    margin-bottom: 16px;
    cursor: help;
}

.source-url-preview {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: var(--bg-tertiary);
    border-radius: var(--radius-sm);
    margin-bottom: 12px;
    font-size: 12px;
}

.url-label {
    color: var(--text-muted);
}

.url-value {
    color: var(--accent);
    font-family: monospace;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.source-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 12px;
}

.source-tag {
    padding: 2px 8px;
    background: var(--accent-muted);
    border-radius: var(--radius-sm);
    font-size: 11px;
    color: var(--accent);
}

/* 开关 */
.source-toggle {
    position: relative;
    width: 44px;
    height: 24px;
}

.source-toggle input {
    opacity: 0;
    width: 0;
    height: 0;
}

.toggle-slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--bg-tertiary);
    border-radius: 24px;
    transition: 0.2s;
}

.toggle-slider:before {
    position: absolute;
    content: "";
    height: 18px;
    width: 18px;
    left: 3px;
    bottom: 3px;
    background: var(--text-muted);
    border-radius: 50%;
    transition: 0.2s;
}

.source-toggle input:checked + .toggle-slider {
    background: var(--accent-muted);
}

.source-toggle input:checked + .toggle-slider:before {
    transform: translateX(20px);
    background: var(--accent);
}

/* 信息源统计 */
.source-stats {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 12px;
    margin-bottom: 16px;
}

.source-stat {
    text-align: center;
    padding: 12px;
    background: var(--bg-tertiary);
    border-radius: var(--radius-sm);
}

.source-stat-value {
    font-size: 20px;
    font-weight: 600;
    color: var(--text-primary);
}

.source-stat-label {
    font-size: 11px;
    color: var(--text-muted);
    text-transform: uppercase;
}

/* 信息源卡片操作按钮 */
.source-card-actions {
    margin-top: 16px;
    display: flex;
    gap: 8px;
}

.source-btn {
    flex: 1;
    padding: 8px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 12px;
    cursor: pointer;
    transition: all 0.15s ease;
}

.source-btn:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.source-btn-danger {
    color: var(--error);
    border-color: var(--error);
}

.source-btn-danger:hover {
    background: rgba(239, 68, 68, 0.1);
}

/* 信息源分类标题 */
.sources-section-title {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 16px 0;
    font-size: 14px;
    font-weight: 600;
    color: var(--accent);
    border-bottom: 1px dashed var(--border);
    margin-top: 8px;
}

.sources-section-icon {
    font-size: 16px;
}

.sources-count {
    padding: 2px 8px;
    background: var(--accent-muted);
    border-radius: var(--radius-sm);
    font-size: 12px;
    color: var(--accent);
}

/* 空状态提示 */
.sources-empty-hint {
    grid-column: 1 / -1;
    text-align: center;
    padding: 48px 24px;
    background: var(--bg-secondary);
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    color: var(--text-muted);
}

.sources-empty-hint p {
    font-size: 14px;
    margin-bottom: 8px;
}

.hint-sub {
    font-size: 13px !important;
    color: var(--text-muted);
}

/* 帮助提示卡片 */
.sources-help {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 16px;
    margin-top: 32px;
    padding-top: 32px;
    border-top: 1px solid var(--border);
}

.help-card {
    display: flex;
    gap: 16px;
    padding: 20px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.help-icon {
    font-size: 24px;
    flex-shrink: 0;
}

.help-content h4 {
    font-size: 14px;
    font-weight: 600;
    margin-bottom: 8px;
    color: var(--text-primary);
}

.help-content p {
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.6;
}

/* ============================================
   模态框
   ============================================ */
.modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    z-index: 1000;
    align-items: center;
    justify-content: center;
    backdrop-filter: blur(4px);
}

.modal.active {
    display: flex;
}

.modal-content {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    width: 90%;
    max-width: 700px;
    max-height: 90vh;
    overflow-y: auto;
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 20px 24px;
    border-bottom: 1px solid var(--border);
}

.modal-header h3 {
    font-size: 18px;
    font-weight: 600;
}

.modal-close {
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: none;
    color: var(--text-muted);
    font-size: 24px;
    cursor: pointer;
    border-radius: var(--radius-sm);
    transition: all 0.15s ease;
}

.modal-close:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.modal-body {
    padding: 24px;
}

/* 快速添加区域 */
.quick-add-section {
    margin-bottom: 32px;
}

.quick-add-section h4 {
    font-size: 15px;
    font-weight: 600;
    margin-bottom: 8px;
}

.quick-add-hint {
    font-size: 13px;
    color: var(--text-muted);
    margin-bottom: 16px;
}

/* 分类标签 */
.category-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 16px;
}

.category-tab {
    padding: 6px 12px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 12px;
    cursor: pointer;
    transition: all 0.15s ease;
}

.category-tab:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.category-tab.active {
    background: var(--accent-muted);
    border-color: var(--accent);
    color: var(--accent);
}

/* 快速添加网格 */
.quick-add-grid {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.quick-add-card {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: all 0.15s ease;
}

.quick-add-card:hover {
    border-color: var(--border-light);
}

.quick-add-card.added {
    opacity: 0.6;
}

.quick-add-icon {
    font-size: 24px;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-secondary);
    border-radius: var(--radius);
}

.quick-add-info {
    flex: 1;
}

.quick-add-name {
    font-size: 14px;
    font-weight: 500;
    margin-bottom: 2px;
}

.quick-add-desc {
    font-size: 12px;
    color: var(--text-muted);
    margin-bottom: 4px;
}

.quick-add-tags {
    display: flex;
    gap: 4px;
}

.quick-tag {
    padding: 1px 6px;
    background: var(--accent-muted);
    border-radius: var(--radius-sm);
    font-size: 10px;
    color: var(--accent);
}

.quick-add-btn {
    padding: 8px 16px;
    background: var(--accent);
    border: none;
    border-radius: var(--radius-sm);
    color: var(--bg-primary);
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.15s ease;
}

.quick-add-btn:hover {
    background: var(--accent-hover);
}

.quick-add-btn.added,
.quick-add-btn:disabled {
    background: var(--bg-hover);
    color: var(--text-muted);
    cursor: not-allowed;
}

/* 手动添加区域 */
.manual-add-section {
    padding-top: 24px;
    border-top: 1px solid var(--border);
    margin-bottom: 24px;
}

.manual-add-section h4 {
    font-size: 15px;
    font-weight: 600;
    margin-bottom: 16px;
}

.form-group {
    margin-bottom: 16px;
}

.form-group label {
    display: block;
    font-size: 13px;
    font-weight: 500;
    margin-bottom: 6px;
    color: var(--text-primary);
}

.form-group input {
    width: 100%;
    padding: 10px 14px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 14px;
    transition: border-color 0.15s ease;
}

.form-group input:focus {
    outline: none;
    border-color: var(--accent);
}

.form-group input::placeholder {
    color: var(--text-muted);
}

.form-hint {
    display: block;
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 6px;
}

/* RSS指南 */
.rss-guide {
    padding: 16px;
    background: var(--bg-tertiary);
    border-radius: var(--radius);
}

.rss-guide h4 {
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 12px;
}

.rss-guide ol {
    margin-left: 20px;
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.8;
}

.rss-guide code {
    padding: 2px 6px;
    background: var(--bg-secondary);
    border-radius: var(--radius-sm);
    font-family: monospace;
    color: var(--accent);
}

.rss-tip {
    margin-top: 12px;
    font-size: 12px;
    color: var(--text-muted);
}

.rss-tip strong {
    color: var(--text-secondary);
}

/* ============================================
   报告预览
   ============================================ */
.report-controls {
    display: flex;
    gap: 12px;
    margin-bottom: 24px;
}

.select {
    padding: 10px 16px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 14px;
    cursor: pointer;
}

.select:focus {
    outline: none;
    border-color: var(--accent);
}

.report-preview {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 32px;
    min-height: 400px;
    max-height: 600px;
    overflow-y: auto;
}

.report-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 300px;
    color: var(--text-muted);
}

.report-content {
    font-size: 14px;
    line-height: 1.8;
}

.report-content h1 {
    font-size: 24px;
    margin-bottom: 24px;
    padding-bottom: 16px;
    border-bottom: 1px solid var(--border);
}

.report-content h2 {
    font-size: 18px;
    margin: 24px 0 16px;
    color: var(--accent);
}

.report-content h3 {
    font-size: 15px;
    margin: 16px 0 12px;
}

.report-content p {
    margin-bottom: 12px;
}

.report-content ul {
    margin-left: 20px;
    margin-bottom: 12px;
}

.report-content li {
    margin-bottom: 6px;
}

.report-content a {
    color: var(--accent);
    text-decoration: none;
}

.report-content a:hover {
    text-decoration: underline;
}

/* ============================================
   配置页面
   ============================================ */
.settings-group {
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 24px;
    margin-bottom: 24px;
}

.settings-group h3 {
    font-size: 15px;
    font-weight: 600;
    margin-bottom: 20px;
    padding-bottom: 12px;
    border-bottom: 1px solid var(--border);
}

.setting-item {
    margin-bottom: 20px;
}

.setting-item:last-child {
    margin-bottom: 0;
}

.setting-item > label {
    display: block;
    font-size: 13px;
    font-weight: 500;
    margin-bottom: 8px;
    color: var(--text-primary);
}

.setting-item input[type="text"],
.setting-item input[type="password"] {
    width: 100%;
    max-width: 400px;
    padding: 10px 14px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 14px;
}

.setting-item input:focus {
    outline: none;
    border-color: var(--accent);
}

.setting-hint {
    display: block;
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 6px;
}

.setting-hint a {
    color: var(--accent);
    text-decoration: none;
}

.setting-hint a:hover {
    text-decoration: underline;
}

/* 开关标签 */
.toggle-label {
    display: flex;
    align-items: center;
    gap: 12px;
    cursor: pointer;
}

.toggle-label input[type="checkbox"] {
    width: 40px;
    height: 22px;
    appearance: none;
    background: var(--bg-tertiary);
    border-radius: 11px;
    position: relative;
    cursor: pointer;
    transition: background 0.2s;
}

.toggle-label input[type="checkbox"]:before {
    content: "";
    position: absolute;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--text-muted);
    top: 2px;
    left: 2px;
    transition: all 0.2s;
}

.toggle-label input[type="checkbox"]:checked {
    background: var(--accent-muted);
}

.toggle-label input[type="checkbox"]:checked:before {
    transform: translateX(18px);
    background: var(--accent);
}

/* 标签输入 */
.tags-input {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    padding: 8px 12px;
    background: var(--bg-tertiary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    max-width: 500px;
}

.tags-input input {
    flex: 1;
    min-width: 150px;
    padding: 4px 0;
    background: transparent;
    border: none;
    color: var(--text-primary);
    font-size: 14px;
}

.tags-input input:focus {
    outline: none;
}

.tag {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 10px;
    background: var(--accent-muted);
    border-radius: var(--radius-sm);
    font-size: 12px;
    color: var(--accent);
}

.tag-remove {
    cursor: pointer;
    opacity: 0.7;
}

.tag-remove:hover {
    opacity: 1;
}

.settings-actions {
    margin-top: 24px;
}

/* ============================================
   Toast通知
   ============================================ */
.toast-container {
    position: fixed;
    top: 20px;
    right: 20px;
    z-index: 2000;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.toast {
    padding: 12px 20px;
    background: var(--bg-secondary);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-primary);
    font-size: 14px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    transform: translateX(100%);
    opacity: 0;
    transition: all 0.3s ease;
}

.toast.show {
    transform: translateX(0);
    opacity: 1;
}

.toast-success {
    border-color: var(--success);
    background: rgba(16, 185, 129, 0.1);
}

.toast-error {
    border-color: var(--error);
    background: rgba(239, 68, 68, 0.1);
}

.toast-warning {
    border-color: var(--warning);
    background: rgba(245, 158, 11, 0.1);
}

/* ============================================
   加载状态
   ============================================ */
.loading-overlay {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.7);
    z-index: 3000;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 16px;
    color: var(--text-primary);
}

.loading-spinner {
    width: 40px;
    height: 40px;
    border: 3px solid var(--border);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

/* ============================================
   响应式设计
   ============================================ */
@media (max-width: 1024px) {
    .stats-grid {
        grid-template-columns: repeat(2, 1fr);
    }
    
    .sources-grid {
        grid-template-columns: 1fr;
    }
    
    .sources-help {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 768px) {
    .sidebar {
        width: 60px;
    }
    
    .logo-text,
    .nav-label {
        display: none;
    }
    
    .main {
        margin-left: 60px;
    }
    
    .content {
        padding: 16px;
    }
    
    .header {
        padding: 0 16px;
    }
    
    .sources-filters {
        overflow-x: auto;
        flex-wrap: nowrap;
        padding-bottom: 12px;
    }
    
    .filter-btn {
        flex-shrink: 0;
    }
}


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