Showing preview only (1,864K chars total). Download the full file or copy to clipboard to get everything.
Repository: sansan0/TrendRadar
Branch: master
Commit: 1b41881ec431
Files: 110
Total size: 1.8 MB
Directory structure:
gitextract_ws4303_m/
├── .dockerignore
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── 01-bug-report.yml
│ │ ├── 02-feature-request.yml
│ │ ├── 03-ai-and-config.yml
│ │ └── config.yml
│ └── workflows/
│ ├── clean-crawler.yml
│ ├── crawler.yml
│ └── docker.yml
├── LICENSE
├── README-Cherry-Studio.md
├── README-EN.md
├── README-MCP-FAQ-EN.md
├── README-MCP-FAQ.md
├── README.md
├── config/
│ ├── ai_analysis_prompt.txt
│ ├── ai_filter/
│ │ ├── extract_prompt.txt
│ │ ├── prompt.txt
│ │ └── update_tags_prompt.txt
│ ├── ai_interests.txt
│ ├── ai_translation_prompt.txt
│ ├── config.yaml
│ ├── custom/
│ │ ├── ai/
│ │ │ └── .gitkeep
│ │ └── keyword/
│ │ └── .gitkeep
│ ├── frequency_words.txt
│ └── timeline.yaml
├── docker/
│ ├── Dockerfile
│ ├── Dockerfile.mcp
│ ├── docker-compose-build.yml
│ ├── docker-compose.yml
│ ├── entrypoint.sh
│ └── manage.py
├── docs/
│ ├── assets/
│ │ ├── script.js
│ │ └── style.css
│ └── index.html
├── index.html
├── mcp_server/
│ ├── __init__.py
│ ├── server.py
│ ├── services/
│ │ ├── __init__.py
│ │ ├── cache_service.py
│ │ ├── data_service.py
│ │ └── parser_service.py
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── analytics.py
│ │ ├── article_reader.py
│ │ ├── config_mgmt.py
│ │ ├── data_query.py
│ │ ├── notification.py
│ │ ├── search_tools.py
│ │ ├── storage_sync.py
│ │ └── system.py
│ └── utils/
│ ├── __init__.py
│ ├── date_parser.py
│ ├── errors.py
│ └── validators.py
├── pyproject.toml
├── requirements.txt
├── setup-mac.sh
├── setup-windows-en.bat
├── setup-windows.bat
├── start-http.bat
├── start-http.sh
├── trendradar/
│ ├── __init__.py
│ ├── __main__.py
│ ├── ai/
│ │ ├── __init__.py
│ │ ├── analyzer.py
│ │ ├── client.py
│ │ ├── filter.py
│ │ ├── formatter.py
│ │ └── translator.py
│ ├── context.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── analyzer.py
│ │ ├── config.py
│ │ ├── data.py
│ │ ├── frequency.py
│ │ ├── loader.py
│ │ └── scheduler.py
│ ├── crawler/
│ │ ├── __init__.py
│ │ ├── fetcher.py
│ │ └── rss/
│ │ ├── __init__.py
│ │ ├── fetcher.py
│ │ └── parser.py
│ ├── notification/
│ │ ├── __init__.py
│ │ ├── batch.py
│ │ ├── dispatcher.py
│ │ ├── formatters.py
│ │ ├── renderer.py
│ │ ├── senders.py
│ │ └── splitter.py
│ ├── report/
│ │ ├── __init__.py
│ │ ├── formatter.py
│ │ ├── generator.py
│ │ ├── helpers.py
│ │ ├── html.py
│ │ └── rss_html.py
│ ├── storage/
│ │ ├── __init__.py
│ │ ├── ai_filter_schema.sql
│ │ ├── base.py
│ │ ├── local.py
│ │ ├── manager.py
│ │ ├── remote.py
│ │ ├── rss_schema.sql
│ │ ├── schema.sql
│ │ └── sqlite_mixin.py
│ └── utils/
│ ├── __init__.py
│ ├── time.py
│ └── url.py
├── version
├── version_configs
└── version_mcp
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
.git/
.gitignore
*.md
README.md
output/
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
.pytest_cache/
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
docker/.env
_image/
.github/
*.log
.env.local
.env.*.local
version
index.html
================================================
FILE: .github/ISSUE_TEMPLATE/01-bug-report.yml
================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
name: 🐛 遇到问题了
description: 程序运行不正常、报错或功能失效(含 AI 分析问题)
title: "[问题] "
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
### ⚠️ 提交前必读
**请确保你正在使用 TrendRadar 的最新版本。**
很多问题在最新代码中可能已经修复。如果你使用的是旧版本,我将无法处理,请先更新后再试。
**简单的描述 + 关键截图** 是最有效的沟通方式。
---
### 📌 如何查看版本号?
| 部署方式 | 查看方法 |
|---------|---------|
| **Docker** | 查看容器启动日志,版本号显示在日志开头 |
| **GitHub Actions** | 查看 [README 文档](https://github.com/sansan0/TrendRadar) 顶部的  徽章 |
| **本地 Python** | 查看项目根目录的 `version` 文件 |
- type: input
id: version
attributes:
label: 📦 TrendRadar 版本
description: |
请务必提供版本号(如:v5.2.0 或 git commit id)
💡 Docker 用户:查看容器启动日志 | GitHub Actions 用户:查看文档顶部 version 徽章
placeholder: v5.2.0 或 commit hash
validations:
required: true
- type: input
id: mcp-version
attributes:
label: 🔌 MCP Server 版本 (可选)
description: 如果你是通过 MCP 使用,请填写 MCP Server 的版本。
placeholder: v3.1.6 (非 MCP 用户留空)
validations:
required: false
- type: dropdown
id: bug-category
attributes:
label: 🏷️ 问题类别
options:
- AI 分析相关(报错、内容异常、提示词失效等)
- 数据获取相关(爬不到新闻、平台失效等)
- 通知推送相关(收不到消息、推送报错等)
- 部署运行相关(Docker、Actions、Python 报错)
- 其他
validations:
required: true
- type: input
id: ai-model
attributes:
label: 🤖 AI 模型名称(AI 问题必填)
description: |
如果是 AI 分析相关问题,请提供你使用的具体模型名称。
AI 问题与模型能力密切相关,不同模型表现差异很大。
placeholder: "例如:deepseek/deepseek-chat、openai/gpt-4o、gemini/gemini-2.5-flash"
validations:
required: false
- type: textarea
id: bug-description
attributes:
label: 📝 描述发生了什么
placeholder: |
请描述:
1. 你在做什么?
2. 出现了什么错误?(如果是 AI 问题,请贴出分析失败的错误提示)
3. 建议上传一张截图,这比文字更有力!
validations:
required: true
- type: textarea
id: error-logs
attributes:
label: 📋 错误日志/配置(可选)
description: |
贴出相关的错误日志或 config.yaml 片段(记得隐藏 API Key 等敏感信息)
💡 Docker 用户:使用 `docker logs trendradar` 查看日志
placeholder: |
贴出相关的错误日志或 config.yaml 片段:
```
在这里贴内容...
```
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: 📷 截图(强烈建议)
description: |
⚠️ **重要提示**:请提供**完整截图**,不要只截取局部!
- 错误截图应包含完整的错误信息和上下文
- 推送截图应包含完整的消息内容
- 配置截图应包含相关配置段的完整内容
局部截图往往缺少关键信息,会导致问题难以定位。
placeholder: 拖拽截图到这里,请确保截图完整,包含足够的上下文信息。
validations:
required: false
- type: dropdown
id: environment
attributes:
label: 🖥️ 使用环境
options:
- Docker (本地/NAS)
- GitHub Actions
- 本地 Python 运行
- MCP Server 客户端 (Cherry Studio等)
validations:
required: true
================================================
FILE: .github/ISSUE_TEMPLATE/02-feature-request.yml
================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
name: 💡 我有个想法
description: 建议新功能、推送样式改进或体验优化
title: "[建议] "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
### 💝 欢迎分享你的创意
你的好点子能让 TrendRadar 变得更好!
目前主要关注以下方向的改进:
- ✨ **AI 分析能力**:更智能的解读、更丰富的分析维度
- 🎨 **推送体验**:更好看的排版、更合理的信息展示
- 🛠️ **易用性优化**:配置更简单、运行更稳定
*注:目前暂不接受新爬虫平台的接入申请,感谢理解。*
- type: textarea
id: feature-description
attributes:
label: 💭 你的想法是什么?
placeholder: |
请简要描述:
- 你希望增加什么功能?
- 它能解决什么问题?
- 如果有参考的图片或工具,欢迎上传截图。
validations:
required: true
- type: textarea
id: use-case
attributes:
label: 🎯 使用场景(可选)
placeholder: 例如:当我在...的时候,如果能...就太棒了。
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/03-ai-and-config.yml
================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json
name: ✨ AI 提示词分享与配置求助
description: 分享你调优的 ai_analysis_prompt.txt 或寻求设置帮助
title: "[AI/配置] "
labels: ["config", "AI"]
body:
- type: markdown
attributes:
value: |
### ✨ 提示词分享计划
欢迎在此分享你精心调优的 `ai_analysis_prompt.txt` 内容!
优秀的提示词可以让 AI 分析更精准、更有趣。
---
如果是**寻求配置帮助**,请尽量贴出你的错误表现。
---
### 📌 如何查看版本号?
| 部署方式 | 查看方法 |
|---------|---------|
| **Docker** | 查看容器启动日志,版本号显示在日志开头 |
| **GitHub Actions** | 查看 [README 文档](https://github.com/sansan0/TrendRadar) 顶部的  徽章 |
| **本地 Python** | 查看项目根目录的 `version` 文件 |
- type: dropdown
id: category
attributes:
label: 🏷️ 目的
options:
- 分享我的 AI 提示词 (ai_analysis_prompt.txt)
- 寻求 AI 分析设置帮助
- 寻求基础功能配置帮助 (Webhook/RSS等)
validations:
required: true
- type: input
id: version
attributes:
label: 📦 TrendRadar 版本(求助时必填)
description: |
如果是寻求帮助,请提供版本号。
💡 Docker 用户:查看容器启动日志 | GitHub Actions 用户:查看文档顶部 version 徽章
placeholder: v5.2.0 或 commit hash(分享提示词可留空)
validations:
required: false
- type: input
id: ai-model
attributes:
label: 🤖 AI 模型名称
description: |
请提供你使用的具体模型名称。
AI 分析效果与模型能力密切相关,不同模型表现差异很大。
分享提示词时也请注明,方便其他用户参考。
placeholder: "例如:deepseek/deepseek-chat、openai/gpt-4o、gemini/gemini-2.5-flash"
validations:
required: false
- type: textarea
id: share-content
attributes:
label: 📄 内容描述
placeholder: |
- 如果是分享:请贴出你的提示词代码块,并简述它的分析风格。
- 如果是求助:请贴出你的配置片段(隐藏 Key)和遇到的现象。
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: 📷 效果截图(推荐)
description: |
⚠️ **重要提示**:请提供**完整截图**,不要只截取局部!
- 分享时:展示 AI 分析的完整输出效果
- 求助时:展示完整的错误信息或异常表现
局部截图往往缺少关键信息,会导致问题难以定位。
placeholder: 拖拽分析结果截图或配置截图到这里,请确保截图完整。
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json
blank_issues_enabled: false
================================================
FILE: .github/workflows/clean-crawler.yml
================================================
name: Check In
# ✅ 签到续期:运行此 workflow 可重置 7 天计时,保持 "Get Hot News" 正常运行
# ✅ Renewal: Run this workflow to reset the 7-day timer and keep "Get Hot News" active
#
# 📌 操作方法 / How to use:
# 1. 点击 "Run workflow" 按钮 / Click "Run workflow" button
# 2. 每 7 天内至少运行一次 / Run at least once every 7 days
on:
workflow_dispatch:
jobs:
del_runs:
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
steps:
- name: Delete all workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: 0
keep_minimum_runs: 0
delete_workflow_by_state_pattern: "ALL"
delete_run_by_conclusion_pattern: "ALL"
================================================
FILE: .github/workflows/crawler.yml
================================================
name: Get Hot News
on:
schedule:
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# ⚠️ 试用版说明 / Trial Mode
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#
# 🔄 运行机制 / How it works:
# - 每个周期为 7 天,届时自动停止
# - 运行 "Check In" 会重置周期(重新开始 7 天倒计时,而非累加)
# - Each cycle is 7 days, then auto-stops
# - "Check In" resets the cycle (restarts 7-day countdown, not cumulative)
#
# 💡 设计初衷 / Why this design:
# 如果 7 天都忘了签到,或许这些资讯对你来说并非刚需
# 适时的暂停,能帮你从信息流中抽离,给大脑留出喘息的空间
# If you forget for 7 days, maybe you don't really need it
# A timely pause helps you detach from the stream and gives your mind space
#
# 🙏 珍惜资源 / Respect shared resources:
# GitHub Actions 是平台提供的公共资源,每次运行都会消耗算力
# 签到机制确保资源分配给真正需要的用户,感谢你的理解与配合
# GitHub Actions is a shared public resource provided by the platform
# Check-in ensures resources go to those who truly need it — thank you
#
# 🚀 长期使用请部署 Docker 版本 / For long-term use, deploy Docker version
#
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
#
# 📝 修改运行时间:只改第一个数字(0-59),表示每小时第几分钟运行
# 📝 Change time: Only modify the first number (0-59) = minute of each hour
#
# 示例 / Examples:
# "15 * * * *" → 每小时第15分钟 / minute 15 every hour
# "30 0-14 * * *" → 北京时间 8:00-22:00 每小时第30分钟 / Beijing 8am-10pm
#
- cron: "33 * * * *"
workflow_dispatch:
concurrency:
group: crawler-${{ github.ref_name }}
cancel-in-progress: true
permissions:
contents: read
actions: write
jobs:
crawl:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
clean: true
- name: Check Expiration
env:
GH_TOKEN: ${{ github.token }}
run: |
WORKFLOW_FILE="crawler.yml"
API_URL="repos/${{ github.repository }}/actions/workflows/$WORKFLOW_FILE/runs"
TOTAL=$(gh api "$API_URL" --jq '.total_count')
if [ -z "$TOTAL" ] || [ "$TOTAL" -eq 0 ]; then
echo "No previous runs found, skipping expiration check"
exit 0
fi
LAST_PAGE=$(( (TOTAL + 99) / 100 ))
FIRST_RUN_DATE=$(gh api "$API_URL?per_page=100&page=$LAST_PAGE" --jq '.workflow_runs[-1].created_at')
if [ -n "$FIRST_RUN_DATE" ]; then
CURRENT_TIMESTAMP=$(date +%s)
FIRST_RUN_TIMESTAMP=$(date -d "$FIRST_RUN_DATE" +%s)
DIFF_SECONDS=$((CURRENT_TIMESTAMP - FIRST_RUN_TIMESTAMP))
LIMIT_SECONDS=604800
if [ $DIFF_SECONDS -gt $LIMIT_SECONDS ]; then
echo "⚠️ 试用期已结束,请运行 'Check In' 签到续期"
echo "⚠️ Trial expired. Run 'Check In' to renew."
gh workflow disable "$WORKFLOW_FILE"
exit 1
else
DAYS_LEFT=$(( (LIMIT_SECONDS - DIFF_SECONDS) / 86400 ))
echo "✅ 试用期剩余 ${DAYS_LEFT} 天,到期前请运行 'Check In' 签到续期"
echo "✅ Trial: ${DAYS_LEFT} days left. Run 'Check In' before expiry to renew."
fi
fi
# --------------------------------------------------------------------------------
# 🚦 TRAFFIC CONTROL / 流量控制
# --------------------------------------------------------------------------------
# EN: Generates a random delay between 1 and 300 seconds (5 minutes).
# Critical for load balancing.
#
# CN: 生成 1 到 300 秒(5分钟)之间的随机延迟。
# 这对负载均衡至关重要。
# - name: Random Delay (Traffic Control)
# if: success()
# run: |
# echo "🎲 Traffic Control: Generating random delay..."
# DELAY=$(( ( RANDOM % 300 ) + 1 ))
# echo "⏸️ Sleeping for ${DELAY} seconds to spread the load..."
# sleep ${DELAY}s
# echo "▶️ Delay finished. Starting crawler..."
- name: Set up Python
if: success()
uses: actions/setup-python@v5
with:
python-version: "3.10"
cache: "pip"
- name: Install dependencies
if: success()
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Verify required files
if: success()
run: |
if [ ! -f config/config.yaml ]; then
echo "Error: Config missing"
exit 1
fi
- name: Run crawler
if: success()
env:
FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }}
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
DINGTALK_WEBHOOK_URL: ${{ secrets.DINGTALK_WEBHOOK_URL }}
WEWORK_WEBHOOK_URL: ${{ secrets.WEWORK_WEBHOOK_URL }}
WEWORK_MSG_TYPE: ${{ secrets.WEWORK_MSG_TYPE }}
EMAIL_FROM: ${{ secrets.EMAIL_FROM }}
EMAIL_PASSWORD: ${{ secrets.EMAIL_PASSWORD }}
EMAIL_TO: ${{ secrets.EMAIL_TO }}
EMAIL_SMTP_SERVER: ${{ secrets.EMAIL_SMTP_SERVER }}
EMAIL_SMTP_PORT: ${{ secrets.EMAIL_SMTP_PORT }}
NTFY_TOPIC: ${{ secrets.NTFY_TOPIC }}
NTFY_SERVER_URL: ${{ secrets.NTFY_SERVER_URL }}
NTFY_TOKEN: ${{ secrets.NTFY_TOKEN }}
BARK_URL: ${{ secrets.BARK_URL }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# 通用Webhook配置
GENERIC_WEBHOOK_URL: ${{ secrets.GENERIC_WEBHOOK_URL }}
GENERIC_WEBHOOK_TEMPLATE: ${{ secrets.GENERIC_WEBHOOK_TEMPLATE }}
# AI 配置(ai_analysis 和 ai_translation 共享模型配置)
AI_ANALYSIS_ENABLED: ${{ secrets.AI_ANALYSIS_ENABLED }}
AI_API_KEY: ${{ secrets.AI_API_KEY }}
AI_MODEL: ${{ secrets.AI_MODEL }}
AI_API_BASE: ${{ secrets.AI_API_BASE }}
# 远程存储配置
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
S3_ENDPOINT_URL: ${{ secrets.S3_ENDPOINT_URL }}
S3_REGION: ${{ secrets.S3_REGION }}
GITHUB_ACTIONS: true
run: python -m trendradar
================================================
FILE: .github/workflows/docker.yml
================================================
name: Build and Push Docker Images
on:
push:
tags:
- "v*" # 主项目版本
- "mcp-v*" # MCP 版本
workflow_dispatch:
inputs:
image:
description: "选择要构建的镜像"
required: true
default: "all"
type: choice
options:
- all
- crawler
- mcp
env:
REGISTRY: docker.io
jobs:
build-crawler:
runs-on: ubuntu-latest
# 条件:v* 标签(排除 mcp-v*)或手动触发选择 all/crawler
if: |
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && !startsWith(github.ref, 'refs/tags/mcp-v')) ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.image == 'all' || github.event.inputs.image == 'crawler'))
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: wantcat/trendradar
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v5
env:
BUILDKIT_PROGRESS: plain
with:
context: .
file: ./docker/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-mcp:
runs-on: ubuntu-latest
# 条件:mcp-v* 标签 或手动触发选择 all/mcp
if: |
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/mcp-v')) ||
(github.event_name == 'workflow_dispatch' && (github.event.inputs.image == 'all' || github.event.inputs.image == 'mcp'))
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver-opts: |
network=host
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract version from tag
id: version
run: |
if [[ "${{ github.ref }}" == refs/tags/mcp-v* ]]; then
VERSION="${GITHUB_REF#refs/tags/mcp-v}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "major_minor=$(echo $VERSION | cut -d. -f1,2)" >> $GITHUB_OUTPUT
else
echo "version=latest" >> $GITHUB_OUTPUT
echo "major_minor=latest" >> $GITHUB_OUTPUT
fi
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: wantcat/trendradar-mcp
tags: |
type=raw,value=${{ steps.version.outputs.version }}
type=raw,value=${{ steps.version.outputs.major_minor }}
type=raw,value=latest
- name: Build and push
uses: docker/build-push-action@v5
env:
BUILDKIT_PROGRESS: plain
with:
context: .
file: ./docker/Dockerfile.mcp
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README-Cherry-Studio.md
================================================
# TrendRadar × Cherry Studio 部署指南 🍒
> **适合人群**:零编程基础的用户
> **客户端**:Cherry Studio(免费开源 GUI 客户端)
---
## 📥 第一步:下载 Cherry Studio
### Windows 用户
访问官网下载:https://cherry-ai.com/
或直接下载:[Cherry-Studio-Windows.exe](https://github.com/kangfenmao/cherry-studio/releases/latest)
### Mac 用户
访问官网下载:https://cherry-ai.com/
或直接下载:[Cherry-Studio-Mac.dmg](https://github.com/kangfenmao/cherry-studio/releases/latest)
---
## 📦 第二步:获取项目代码
为什么需要获取项目代码?
AI 分析功能需要读取项目中的新闻数据才能工作。无论你使用 GitHub Actions 还是 Docker 部署,爬虫生成的新闻数据都保存在项目的 output 目录中。因此,在配置 MCP 服务器之前,需要先获取完整的项目代码(包含数据文件)。
根据你的技术水平,可以选择以下任一方式获取::
### 方法一:Git Clone(推荐给技术用户)
如果你熟悉 Git,可以使用以下命令克隆项目:
```bash
git clone https://github.com/你的用户名/你的项目名.git
cd 你的项目名
```
**优点**:
- 可以随时拉取一个命令就可以更新最新数据到本地了(`git pull`)
### 方法二:直接下载 ZIP 压缩包(推荐给初学者)
1. **访问 GitHub 项目页面**
- 项目链接:`https://github.com/你的用户名/你的项目名`
2. **下载压缩包**
- 点击绿色的 "Code" 按钮
- 选择 "Download ZIP"
- 或直接访问:`https://github.com/你的用户名/你的项目名/archive/refs/heads/master.zip`
**注意事项**:
- 步骤稍微麻烦,后续更新数据需要重复上面步骤,然后覆盖本地数据(output 目录)
---
## 🚀 第三步:一键部署 MCP 服务器
### Windows 用户
1. **双击运行**项目文件夹中的 `setup-windows.bat`,如果有问题,就运行 `setup-windows-en.bat`
2. **等待安装完成**
3. **记录显示的配置信息**(命令路径和参数)
### Mac 用户
1. **打开终端**(在启动台搜索"终端")
2. **拖拽**项目文件夹中的 `setup-mac.sh` 到终端窗口
3. **按回车键**
4. **记录显示的配置信息**
---
## 🔧 第四步:配置 Cherry Studio
### 1. 打开设置
启动 Cherry Studio,点击右上角 ⚙️ **设置** 按钮
### 2. 添加 MCP 服务器
在设置页面找到:**MCP** → 点击 **添加**
### 3. 填写配置(重要!)
根据刚才的安装脚本显示的信息填写
### 4. 保存并启用
- 点击 **保存** 按钮
- 确保 MCP 服务器列表中的开关是 **开启** 状态 ✅
---
## ✅ 第五步:验证是否成功
### 1. 测试连接
在 Cherry Studio 的对话框中输入:
```
帮我爬取最新的新闻
```
或者尝试其他测试命令:
```
搜索最近3天关于"人工智能"的新闻
查找2025年1月的"特斯拉"相关报道
分析"iPhone"的热度趋势
```
**提示**:当你说"最近3天"时,AI会自动计算日期范围并搜索。
### 2. 成功标志
如果配置成功,AI 会:
- ✅ 调用 TrendRadar 工具
- ✅ 返回真实的新闻数据
- ✅ 显示平台、标题、排名等信息
---
## 🎯 进阶配置
### HTTP 模式(可选)
如果需要远程访问或多客户端共享,可以使用 HTTP 模式:
#### Windows
双击运行 `start-http.bat`
#### Mac
```bash
./start-http.sh
```
然后在 Cherry Studio 中配置:
```
类型: streamableHttp
URL: http://localhost:3333/mcp
```
================================================
FILE: README-EN.md
================================================
<div align="center" id="trendradar">
<a href="https://github.com/sansan0/TrendRadar" title="TrendRadar">
<img src="/_image/banner.webp" alt="TrendRadar Banner" width="80%">
</a>
Deploy in <strong>30 seconds</strong> — Say goodbye to endless scrolling, only see the news you truly care about
<a href="https://trendshift.io/repositories/14726" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14726" alt="sansan0%2FTrendRadar | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[](https://github.com/sansan0/TrendRadar/stargazers)
[](https://github.com/sansan0/TrendRadar/network/members)
[](LICENSE)
[](https://github.com/sansan0/TrendRadar)
[](https://github.com/sansan0/TrendRadar)
[](https://github.com/sansan0/TrendRadar)
[](https://github.com/sansan0/TrendRadar)
[](https://work.weixin.qq.com/)
[](https://weixin.qq.com/)
[](https://telegram.org/)
[](#)
[](https://www.feishu.cn/)
[](#)
[](https://github.com/binwiederhier/ntfy)
[](https://github.com/Finb/Bark)
[](https://slack.com/)
[](#)
[](https://github.com/sansan0/TrendRadar)
[](https://sansan0.github.io/TrendRadar)
[](https://hub.docker.com/r/wantcat/trendradar)
[](https://modelcontextprotocol.io/)
[](#)
[](#)
</div>
<div align="center">
**[中文](README.md)** | **English**
</div>
> This project is designed to be lightweight and easy to deploy
<br>
## 📑 Quick Navigation
> 💡 **Click the links below** to jump to the corresponding section. Start with "**Quick Start**" for deployment, see "**Configuration Guide**" for detailed customization
<div align="center">
| | | |
|:---:|:---:|:---:|
| [🚀 **Quick Start**](#-quick-start) | [AI Analysis](#-ai-analysis) | [⚙️ **Configuration Guide**](#configuration-guide) |
| [Docker Deployment](#6-docker-deployment) | [MCP Clients](#-mcp-clients) | [📝 **Changelog**](#-changelog) |
| [🎯 **Core Features**](#-core-features) | [☕ **Support Project**](#-support-project) | [📚 **Related Projects**](#-related-projects) |
</div>
<br>
- Thanks to **stargazers**, your stars and forks are the best support for open source 😍
<details>
<summary>👉 Click to view <strong>Acknowledgments</strong> (Angel Round Honor Roll 🔥73+🔥 supporters)</summary>
### Acknowledgments to Early Supporters
> 💡 **Special Note**:
>
> 1. **About the List**: The table below records supporters from the early stage (Angel Round) of the project. Due to the manual nature of statistics in the early days, **there may be omissions or incomplete records. If anyone was missed, it was unintentional, and we ask for your kind understanding**.
> 2. **Future Plan**: To focus limited energy back on code development and feature iteration, **this list will no longer be manually maintained as of today**.
>
> Whether your name is on the list or not, your every bit of support is the cornerstone that allows TrendRadar to be where it is today. 🙏
### Infrastructure Support
Thanks to **GitHub** for providing free infrastructure, which is the biggest prerequisite for this project to run conveniently with **one-click fork**.
### Data Support
This project uses the API from [newsnow](https://github.com/ourongxing/newsnow) to fetch multi-platform data. Special thanks to the author for providing this service.
After communication, the author indicated no concerns about server pressure, but this is based on their goodwill and trust. Please everyone:
- **Visit the [newsnow project](https://github.com/ourongxing/newsnow) and give it a star**
- When deploying with Docker, please control the frequency reasonably and avoid being overly greedy
### Promotion Support
> Thanks to the following platforms and individuals for recommendations (in chronological order)
- [Appinn (小众软件)](https://mp.weixin.qq.com/s/fvutkJ_NPUelSW9OGK39aA) - Open source software recommendation platform
- [LinuxDo Community](https://linux.do/) - Tech enthusiasts community
- [Ruan Yifeng's Weekly](https://github.com/ruanyf/weekly) - Influential tech weekly in Chinese tech circle
### Community Support
> Thanks to **financial supporters**. Your generosity has transformed into snacks and drinks beside my keyboard, accompanying every iteration of this project
>
> **Return of "One-Yuan Appreciation"**:
> With the release of v5.0.0, the project enters a new phase. To support growing API costs and caffeine consumption, the "One-Yuan Appreciation" channel is now reopened. Every bit of your kindness translates into Tokens and motivation in the code world. 🚀 [Support Now](#-support-project)
| Supporter | Amount (CNY) | Date | Note |
| :-------: | :----------: | :--: | :--: |
| D*5 | 1.8 * 3 | 2025.11.24 | |
| *鬼 | 1 | 2025.11.17 | |
| *超 | 10 | 2025.11.17 | |
| R*w | 10 | 2025.11.17 | Great agent work! |
| J*o | 1 | 2025.11.17 | Thanks for open source |
| *晨 | 8.88 | 2025.11.16 | Nice project |
| *海 | 1 | 2025.11.15 | |
| *德 | 1.99 | 2025.11.15 | |
| *疏 | 8.8 | 2025.11.14 | Great project |
| M*e | 10 | 2025.11.14 | Open source is not easy |
| **柯 | 1 | 2025.11.14 | |
| *云 | 88 | 2025.11.13 | Good project |
| *W | 6 | 2025.11.13 | |
| *凯 | 1 | 2025.11.13 | |
| 对*. | 1 | 2025.11.13 | Thanks for TrendRadar |
| s*y | 1 | 2025.11.13 | |
| **翔 | 10 | 2025.11.13 | Wish I found it earlier |
| *韦 | 9.9 | 2025.11.13 | TrendRadar is awesome |
| h*p | 5 | 2025.11.12 | Support Chinese open source |
| c*r | 6 | 2025.11.12 | |
| a*n | 5 | 2025.11.12 | |
| 。*c | 1 | 2025.11.12 | Thanks for sharing |
| ... | ... | ... | **(More 50+ supporters)** |
</details>
<br>
## 🪄 Sponsors
<div align="center">
> **Sponsorship Open**
</div>
<br>
<a name="-support-project"></a>
### ❤️ Find it useful? Support TrendRadar
> If TrendRadar has captured value for you, give it some fuel to keep evolving
>
> Any amount is welcome; even 1 RMB is a gesture of encouragement for open source. Feel free to leave a note with your donation (´▽`ʃ♡ƪ)
<div align="center">
| WeChat Pay | Alipay |
| --- | --- |
| <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F2ae0a88d98079f7e876c2b4dc85233c6-9e8025.JPG" width="240" alt="WeChat Pay"> | <img src="https://cdn-1258574687.cos.ap-shanghai.myqcloud.com/img/%2F2025%2F07%2F17%2F1ed4f20ab8e35be51f8e84c94e6e239b4-fe4947.JPG" width="240" alt="Alipay"> |
</div>
### 🤝 Attribution & Secondary Development
If you utilize the core code or draw inspiration from the logic of this project, **it would be greatly appreciated** if you could acknowledge the source in your README or documentation and include a link to this repository.
This contributes to the sustainable maintenance of the project and the growth of the community. Thank you for your respect and support! ❤️
### 💬 Feedback & Community
* **GitHub Issues**: Best for specific technical issues. Please provide complete information (screenshots, error logs, etc.) to help locate the problem quickly.
* **WeChat Official Account**: It is recommended to leave comments under relevant articles. If you need to ask questions in the background, **liking/recommending** the article first is the best "icebreaker," and I can feel your appreciation (´▽`ʃ♡ƪ).
> **Friendly Reminder**:
> This project is for open-source sharing, not a commercial product. Treat the author as a friend, not customer service, for better communication efficiency!
<div align="center">
| Follow on WeChat |
| --- |
| <img src="_image/weixin.png" width="500" title="Silicon-based Tea Room"/> |
</div>
<br>
## 📝 Changelog
>**📌 Check Latest Updates**: **[Original Repository Changelog](https://github.com/sansan0/TrendRadar?tab=readme-ov-file#-changelog)**:
- **Tip**: Check [Changelog] to understand specific [Features]
### 2026/03/12 - v6.5.0
- **AI Smart News Filtering**: No more manual keyword setup! Describe your interests in everyday language in `ai_interests.txt` (e.g., "I want AI and renewable energy news"), and AI automatically extracts tags, scores every headline, and only pushes what truly matters to you. If AI filtering encounters issues, it auto-falls back to keyword matching — push delivery never stops
- **Per-Period Filter Strategy & Interests**: Each time period in Timeline can now independently choose its filtering method and what topics to focus on. For example: mornings use a "tech keyword list" for quick filtering, evenings switch to "finance AI interests" for in-depth AI filtering — same system, different content at different times
- **AI Analysis Independent from Push Mode**: AI analysis scope can differ from push content. For example: push only delivers new items (avoiding repeated notifications), while AI analyzes the full day's news (capturing complete trends). Each time period can also set its own AI analysis mode
- **AI Filter Token Savings**: Previously analyzed news won't be re-processed; when you edit your interests, AI auto-evaluates the change magnitude — minor tweaks only update affected tags, major changes trigger full reclassification
- **Multi-File Config & Tag Isolation**: Custom keyword files go in `config/custom/keyword/`, AI interest files go in `config/custom/ai/` — tags from different files are fully isolated and independent
- **AI Translation Precision Control**: Independently toggle translation for hotlist, RSS, and standalone sections; regions with display turned off are automatically skipped, saving tokens
- **Remote Storage Batch Upload**: Multiple write operations are batched and submitted to cloud in one go, reducing API call count
- **Per-Group Display Limit**: New `max_news_per_keyword` controls max items shown per keyword/tag group, preventing a single hot topic from filling the entire push
- **Time Period Conflict Detection**: Overlapping time periods are automatically detected — system alerts you to fix the config, preventing unexpected behavior
- Various bug fixes
### 2026/02/09 - mcp-v4.0.0
- **🔥 Push any AI message to all channels**: Send AI-generated content to Feishu, DingTalk, Telegram, Email and all 9 channels with one call — Markdown auto-adapts to each platform's native format
- **New format guide tool**: `get_channel_format_guide` tells AI what each channel supports and its limitations, so generated content looks great everywhere
- **Smart batch splitting**: Long messages auto-split per channel byte limits (Feishu 30KB, DingTalk 20KB, etc.), reads config from config.yaml
- **Fixed channel detection**: ntfy no longer falsely reported as "configured" due to default server URL
- **Code reuse**: Batch utilities now imported from trendradar core instead of duplicated
<details>
<summary>👉 Click to expand: <strong>Historical Updates</strong></summary>
### 2026/02/09 - v6.0.0
> **Breaking Change**: Config file upgrade (config.yaml 2.0.0), old `push_window` and `analysis_window` configs are no longer compatible, please refer to the new config.yaml for migration
- **Unified Scheduling System**: New `timeline.yaml` — one config to control when to crawl / push / AI analyze
- **5 Preset Templates**: `always_on` (24/7, default), `morning_evening` (morning & evening summary), `office_hours` (work hours), `night_owl` (late night), `custom` (fully customizable); you can also add your own templates under `presets:` — just use a unique key, then set it in config.yaml
- **Flexible Time Period Config**: Supports weekday/weekend differentiation, cross-midnight time periods, per-period once deduplication
- **Visual Config Editor**:
- New `timeline.yaml` editor tab, alongside config.yaml / frequency_words.txt
- Preset mode card selector: click to switch, auto-syncs config.yaml's `schedule.preset`
- Week view timeline: 7 days × 24 hours horizontal bars, color-coded for push/analysis/crawl status
- Interactive controls: toggles, dropdowns, time pickers — right-side changes sync to left-side YAML in real time
- Week mapping dropdown: dynamically populated from day plans, configure scheduling by drag and click
- **AI Prompt Stability Overhaul** (ai_analysis_prompt.txt v2.0.0):
- Formatting rules extracted from JSON values into a standalone spec section, reducing AI output format inconsistencies
- JSON template simplified: field descriptions shortened to one sentence + word limit
- Removed Markdown from system prompt to align with the "no Markdown" instruction
- All JSON fields declared optional — missing any field won't cause errors, improving fault tolerance
- **Standalone Source AI Summaries** (`ai_analysis.include_standalone`):
- New independent toggle: when enabled, AI generates a concise summary for each standalone source
- Decoupled from display: AI can analyze full hotlist data without enabling standalone display in push notifications
- Supports both trending platforms and RSS feeds, including rank/time/trajectory data
- Trajectory analysis linked with `include_rank_timeline`: uses trajectory data for deep trend analysis when enabled, falls back to rank-based summary when disabled
- New `standalone_summaries` JSON field ("Source Snapshot"), all notification channels adapted for rendering
### 2026/01/28 - v5.5.0
> Like the MCP feature, I'm not creating a separate repo for this tool either — it's pure frontend, so bundling it together
- Added visual configuration editor for TrendRadar
### 2026/02/02 - mcp-v3.2.0
- **New read_article tool**: Read a single article body via Jina AI Reader (Markdown format)
- **New read_articles_batch tool**: Batch read multiple articles (up to 5, auto rate-limited)
- **Recommended workflow**: `search_news(query="keyword", include_url=True)` → `read_article(url=...)` to read article body
- **Docs update**: README-MCP-FAQ.md and README-MCP-FAQ-EN.md added Q19-Q20 for article reading
### 2026/01/23 - v5.4.0
- Added independent control for AI analysis mode, options: follow_report | daily | current | incremental
- Added time window control for AI analysis, supporting custom execution periods and daily frequency limits
- Added configuration file version management function
- Fixed several bugs
### 2026/01/19 - v5.3.0
> **Major Refactor: AI Module Migration to LiteLLM**
- **Unified AI Interface**: Replaced manual implementation with LiteLLM, supporting 100+ AI providers
- **Simplified Configuration**: Removed `provider` field, now using `model: "provider/model_name"` format
- **New Features**: Auto-retry (`num_retries`), fallback models (`fallback_models`)
- **Configuration Changes**:
- `ai.provider` → Removed (merged into model)
- `ai.base_url` → `ai.api_base`
- `AI_PROVIDER` environment variable → Removed
- `AI_BASE_URL` environment variable → `AI_API_BASE`
- **Model Format Examples**:
- DeepSeek: `deepseek/deepseek-chat`
- OpenAI: `openai/gpt-4o`
- Gemini: `gemini/gemini-2.5-flash`
- Anthropic: `anthropic/claude-3-5-sonnet`
### 2026/01/17 - v5.2.0
> See config.yaml for details
**🌐 AI Translation**
- **Multi-language Translation**: Translate push content to any language
- **Batch Translation**: Smart batch processing to reduce API calls
- **Custom Prompts**: Customize translation style
**🔧 Configuration Optimization**
- **Standalone AI Model Config**: Analysis and translation share model config
- **Unified Region Switches**: Unified management of push region display
- **Custom Region Order**: Customize display order of each region
**✨ AI Analysis Enhancement**
- **AI Analysis Embedded in HTML**: Analysis results directly embedded in HTML reports, used by email notifications
- **Rich Style AI Section**: Gradient blue card layout, clearly separating analysis dimensions
- **Ranking Timeline Support**: AI can access precise ranking at each crawl time point
- **Section Reorganization (7→4)**: Consolidated into Core Trends, Sentiment & Controversy, Signals & Anomalies, Outlook & Strategy
**🔧 Multi-Model Adaptation**
- **Universal Parameter Passthrough**: Pass any advanced parameters to API
- **Gemini Adaptation**: Native parameter support with relaxed safety settings
**🐛 Bug Fixes**
- Fixed various known issues, improved system stability
### 2026/01/10 - mcp-v3.0.0~v3.1.5
- **Breaking Change**: All tool return values unified to `{success, summary, data, error}` structure
- **Async Consistency**: All 21 tool functions wrapped with `asyncio.to_thread()` for sync calls
- **MCP Resources**: Added 4 resources (platforms, rss-feeds, available-dates, keywords)
- **RSS Enhancement**: `get_latest_rss` supports multi-day queries (days param), cross-date URL deduplication
- **Regex Matching Fix**: `get_trending_topics` supports `/pattern/` regex syntax and `display_name`
- **Cache Optimization**: Added `make_cache_key()` function with param sorting + MD5 hash for consistency
- **New check_version Tool**: Check TrendRadar and MCP Server version updates simultaneously
### 2026/01/10 - v5.0.0
> **Dev Anecdote**:
> A salute to a certain 'C' model provider that accompanied me for over two years, only to slap me with `"This organization has been disabled"` right after I renewed my subscription.
**✨ "Five Major Sections" Content Refactoring**
This update refactors the push message structure into five distinct core sections:
1. **📊 Trending News**: Aggregated trending topics from across the web, precisely filtered by your keywords.
2. **📰 RSS Feeds**: Your personalized subscription content, supporting keyword-based grouping.
3. **🆕 New Items**: Real-time capture of brand new trending topics since the last run (marked with 🆕).
4. **📋 Independent Display**: Complete trending lists or RSS feeds from specified platforms, **completely unaffected by keyword filtering**.
5. **✨ AI Analysis**: Deep insights driven by AI, including trend overview, popularity trends, and **critically important** sentiment analysis.
**✨ AI Smart Analysis Push Feature**
- **AI Analysis Integration**: Use AI models to deeply analyze push content, automatically generate trending insights, keyword analysis, cross-platform correlation, potential impact assessment
- **Sentiment Analysis**: New deep sentiment recognition to accurately capture positive, negative, controversial, or concerned public opinions (v5.0.0 key enhancement)
- **Multi AI Provider Support**: Supports DeepSeek (default, cost-effective), OpenAI, Google Gemini, and any OpenAI-compatible API
- **Two Push Modes**: `only_analysis` (AI analysis only), `both` (push both)
- **Custom Prompts**: Customize AI analysis role and output format via `config/ai_analysis_prompt.txt`
- **Multi-dimensional Analysis**: AI can analyze ranking changes, trending duration, cross-platform performance, trend prediction
### 2026/01/02 - v4.7.0
- **Fix RSS HTML Display**: Fixed RSS data format mismatch causing rendering issues, now displays correctly grouped by keyword
- **New Regex Syntax**: Keyword config supports `/pattern/` regex syntax, solves English substring mismatch issues (e.g., `ai` matching `training`) [📖 View Syntax Details](#keyword-basic-syntax)
- **New Display Name Syntax**: Use `=> alias` to give complex regex a friendly name, cleaner push notifications (e.g., `/\bai\b/ => AI Related`)
- **Can't Write Regex?** README now includes AI prompt guide - just tell ChatGPT/Gemini/DeepSeek what you want to match
### 2025/12/30 - mcp-v2.0.0
- **Architecture Refactoring**: Removed TXT support, unified to SQLite database
- **RSS Query**: Added `get_latest_rss`, `search_rss`, `get_rss_feeds_status`
- **Unified Search**: `search_news` supports `include_rss` parameter to search both trending and RSS
### 2026/01/01 - v4.6.0
- **Fix RSS HTML Display**: Merged RSS content into trending HTML page, grouped by source
- **New display_mode Config**: Support `keyword` (group by keyword) and `platform` (group by platform) display modes
### 2025/12/30 - v4.5.0
- **RSS Feed Support**: Added RSS/Atom feed crawling, keyword-based grouping and statistics (consistent with trending format)
- **Storage Structure Refactoring**: Flattened directory structure `output/{type}/{date}.db`
- **Unified Sorting Config**: `sort_by_position_first` affects both trending and RSS
- **Config Structure Refactoring**: `config.yaml` reorganized into 7 logical groups (app, report, notification, storage, platforms, rss, advanced) with clearer config paths
### 2025/12/26 - mcp-v1.2.0
**MCP Module Update - Optimized toolset, added aggregation & comparison features, merged redundant tools:**
- Added `aggregate_news` tool - Cross-platform news deduplication and aggregation
- Added `compare_periods` tool - Period comparison analysis (week-over-week/month-over-month)
- Merged `find_similar_news` + `search_related_news_history` → `find_related_news`
- Enhanced `get_trending_topics` - Added `auto_extract` mode for automatic trending extraction
- Fixed miscellaneous bugs
- Updated README-MCP-FAQ.md documentation in both Chinese and English (Q1-Q18)
### 2025/12/20 - v4.0.3
- Added URL normalization to fix duplicate push issues caused by dynamic parameters (e.g., Weibo's `band_rank`)
- Fixed incremental mode detection logic to correctly identify historical titles
### 2025/12/13 - mcp-v1.1.0
**MCP Module Update:**
- Adapted for v4.0.0, while maintaining compatibility with v3.x data.
- Added storage sync tools:
- `sync_from_remote`: Pull data from remote storage to local
- `get_storage_status`: Get storage configuration and status
- `list_available_dates`: List available dates in local/remote storage
### 2025/12/17 - v4.0.1
- StorageManager adds push record proxy methods
- S3 client switches to virtual-hosted style for better compatibility (supports Tencent Cloud COS and more services)
### 2025/12/13 - v4.0.0
**🎉 Major Update: Comprehensive Refactoring of Storage and Core Architecture**
- **Multi-Storage Backend Support**: Introduced a brand new storage module supporting local SQLite and remote cloud storage (S3-compatible protocols, e.g., Cloudflare R2), adaptable to GitHub Actions, Docker, and local environments.
- **Database Structure Optimization**: Refactored SQLite database table structures to improve data efficiency and query performance.
- **Enhanced Features**: Implemented date format standardization, data retention policies, timezone configuration support, and optimized time display. Fixed remote storage data persistence issues to ensure accurate data merging.
- **Cleanup and Compatibility**: Removed most legacy compatibility code and unified data storage and retrieval methods.
### 2025/12/03 - v3.5.0
**🎉 Core Feature Enhancements**
1. **Multi-Account Push Support**
- All push channels (Feishu, DingTalk, WeWork, Telegram, ntfy, Bark, Slack) support multiple account configuration
- Use semicolon `;` to separate multiple accounts, e.g., `FEISHU_WEBHOOK_URL=url1;url2`
- Automatic validation for paired configurations (e.g., Telegram's token and chat_id)
2. **Push Region Configuration**
- Customize display order of all regions via `display.region_order` (v5.2.0, replaces `reverse_content_order`)
- Control visibility of each region via `display.regions` (hotlist, new items, RSS, standalone, AI analysis)
3. **Global Filter Keywords**
- Added `[GLOBAL_FILTER]` region marker for filtering unwanted content globally
- Use cases: Filter ads, marketing, low-quality content, etc.
**🐳 Docker Dual-Path HTML Generation Optimization**
- **Bug Fix**: Resolved issue where `index.html` could not sync to host in Docker environment
- **Dual-Path Generation**: Daily summary HTML is generated to two locations simultaneously
- `index.html` (project root): For GitHub Pages access
- `output/index.html`: Accessible on host via Docker Volume mount
- **Compatibility**: Ensures web reports are accessible in Docker, GitHub Actions, and local environments
**🐳 Docker MCP Image Support**
- Added independent MCP service image `wantcat/trendradar-mcp`
- Supports Docker deployment of AI analysis features via HTTP interface (port 3333)
- Dual-container architecture: News push service and MCP service run independently, can be scaled and restarted separately
- See [Docker Deployment - MCP Service](#6-docker-deployment) for details
**🌐 Web Server Support**
- Added built-in web server for browser access to generated reports
- Control via `manage.py` commands: `docker exec -it trendradar python manage.py start_webserver`
- Access URL: `http://localhost:8080` (port configurable)
- Security features: Static file service, directory restriction, localhost binding
- Supports both auto-start and manual control modes
**📖 Documentation Optimization**
- Added [Report Configuration](#7-report-configuration) section: report-related parameter details
- Added [Push Window Configuration](#8-push-window-configuration) section: push_window configuration tutorial
- Added [Execution Frequency Configuration](#9-execution-frequency-configuration) section: Cron expression explanation and common examples
- Added [Multi-Account Push Configuration](#10-multiple-account-push-configuration) section: multi-account push configuration details
- Optimized all configuration sections: Unified "Configuration Location" instructions
- Simplified Quick Start configuration: Three core files at a glance
- Optimized [Docker Deployment](#6-docker-deployment) section: Added image description, recommended git clone deployment, reorganized deployment methods
**🔧 Upgrade Instructions**:
- **GitHub Fork Users**: Update `main.py`, `config/config.yaml` (Added multi-account push support, existing single-account configuration unaffected)
- **Docker Users**: Update `.env`, `docker-compose.yml` or set environment variables `REVERSE_CONTENT_ORDER`, `MAX_ACCOUNTS_PER_CHANNEL`
- **Multi-Account Push**: New feature, disabled by default, existing single-account configuration unaffected
### 2025/11/28 - v3.4.1
**🔧 Format Optimization**
1. **Bark Push Enhancement**
- Bark now supports Markdown rendering
- Enabled native Markdown format: bold, links, lists, code blocks, etc.
- Removed plain text conversion to fully utilize Bark's native rendering capabilities
2. **Slack Format Precision**
- Use dedicated mrkdwn format for batch content processing
- Improved byte size estimation accuracy (avoid message overflow)
- Optimized link format: `<url|text>` and bold syntax: `*text*`
3. **Performance Improvement**
- Format conversion completed during batching process, avoiding secondary processing
- Accurate message size estimation reduces send failure rate
**🔧 Upgrade Instructions**:
- **GitHub Fork Users**: Update `main.py`,`config.yaml`
### 2025/11/26 - mcp-v1.0.3
**MCP Module Update:**
- Added date parsing tool resolve_date_range to resolve AI model date calculation inconsistencies
- Support natural language date expression parsing (this week, last 7 days, last month, etc.)
- Tool count increased from 13 to 14
### 2025/11/25 - v3.4.0
**🎉 Added Slack Push Support**
1. **Team Collaboration Push Channel**
- Supports Slack Incoming Webhooks (globally popular team collaboration tool)
- Centralized message management, suitable for team-shared trending news
- Supports mrkdwn format (bold, links, etc.)
2. **Multiple Deployment Methods**
- GitHub Actions: Configure `SLACK_WEBHOOK_URL` Secret
- Docker: Environment variable `SLACK_WEBHOOK_URL`
- Local: `config/config.yaml` configuration file
> 📖 **Detailed Configuration Tutorial**: [Quick Start - Slack Push](#-quick-start)
- Optimized the one-click installation experience for setup-windows.bat and setup-windows-en.bat
**🔧 Upgrade Instructions**:
- **GitHub Fork Users**: Update `main.py`, `config/config.yaml`, `.github/workflows/crawler.yml`
### 2025/11/24 - v3.3.0
**🎉 Added Bark Push Support**
1. **iOS Exclusive Push Channel**
- Supports Bark push (based on APNs, iOS platform)
- Free, open-source, clean, efficient, ad-free
- Supports both official server and self-hosted server
2. **Multiple Deployment Methods**
- GitHub Actions: Configure `BARK_URL` Secret
- Docker: Environment variable `BARK_URL`
- Local: `config/config.yaml` configuration file
> 📖 **Detailed Configuration Tutorial**: [Quick Start - Bark Push](#-quick-start)
**🐛 Bug Fix**
- Fixed issue where `ntfy_server_url` in `config.yaml` was ignored ([#345](https://github.com/sansan0/TrendRadar/issues/345))
**🔧 Upgrade Instructions**:
- **GitHub Fork Users**: Update `main.py`, `config/config.yaml`, `.github/workflows/crawler.yml`
### 2025/11/23 - v3.2.0
**🎯 New Advanced Customization Features**
1. **Keyword Sorting Priority Configuration**
- Two sorting strategies: Popularity first vs Config order first
- For different use cases: Hot topic tracking or personalized focus
2. **Display Count Precise Control**
- Global config: Unified limit for all keywords
- Individual config: Use `@number` syntax to set specific limits
- Effectively control push length, highlight key content
> 📖 **Detailed Tutorial**: [Keyword Configuration - Advanced Settings](#keyword-advanced-settings)
**🔧 Upgrade Instructions**:
- **GitHub Fork Users**: Update `main.py`, `config/config.yaml`
### 2025/11/22 - v3.1.1
- **Fixed data anomaly crash issue**: Resolved `'float' object has no attribute 'lower'` error encountered by some users in GitHub Actions environment
- Added dual protection mechanism: Filter invalid titles (None, float, empty strings) at data acquisition stage, with type checking at function call sites
- Enhanced system stability to ensure normal operation even when data sources return abnormal formats
**Upgrade Instructions** (GitHub Fork Users):
- Required update: `main.py`
- Recommended: Use minor version upgrade method - copy and replace the file above
### 2025/11/18 - mcp-v1.0.2
**MCP Module Update:**
- Fix issue where today's news query may return articles from past dates
### 2025/11/20 - v3.1.0
- **Added Personal WeChat Push Support**: WeWork application can push to personal WeChat without installing WeWork APP
- Supports two message formats: `markdown` (WeWork group bot) and `text` (personal WeChat app)
- Added `WEWORK_MSG_TYPE` environment variable configuration, supporting GitHub Actions, Docker, docker compose and other deployment methods
- `text` mode automatically strips Markdown syntax for clean plain text push
- See "Personal WeChat Push" configuration in Quick Start
**Upgrade Instructions** (GitHub Fork Users):
- Required updates: `main.py`, `config/config.yaml`
- Optional update: `.github/workflows/crawler.yml` (if using GitHub Actions)
- Recommended: Use minor version upgrade method - copy and replace the files above
### 2025/11/12 - v3.0.5
- Fixed email sending SSL/TLS port configuration logic error
- Optimized email service providers (QQ/163/126) to default use port 465 (SSL)
- **Added Docker environment variable support**: Core config items (`enable_crawler`, `report_mode`, `push_window`, etc.) support override via environment variables, solving config file modification issues for NAS users (see [🐳 Docker Deployment](#-docker-deployment) chapter)
### 2025/10/26 - mcp-v1.0.1
**MCP Module Update:**
- Fixed date query parameter passing error
- Unified time parameter format for all tools
### 2025/10/31 - v3.0.4
- Solved Feishu error due to overly long push content, implemented batch pushing
### 2025/10/23 - v3.0.3
- Expanded ntfy error message display range
### 2025/10/21 - v3.0.2
- Fixed ntfy push encoding issue
### 2025/10/20 - v3.0.0
**Major Update - AI Analysis Feature Launched** ✨
- **Core Features**:
- New MCP (Model Context Protocol) based AI analysis server
- 13 smart analysis tools: basic query, smart search, advanced analysis, system management
- Natural language interaction: Query and analyze news data through conversation
- Multi-client support: Claude Desktop, Cherry Studio, Cursor, Cline, etc.
- **Analysis Capabilities**:
- Topic trend analysis (popularity tracking, lifecycle, viral detection, trend prediction)
- Data insights (platform comparison, activity stats, keyword co-occurrence)
- Sentiment analysis, similar news finding, smart summary generation
- Historical related news search, multi-mode search
- **Update Note**:
- This is an independent AI analysis feature, does not affect existing push functionality
- Optional use, no need to upgrade existing deployment
### 2025/10/15 - v2.4.4
- **Updates**:
- Fixed ntfy push encoding issue + 1
- Fixed push time window judgment issue
- **Upgrade Note**:
- Recommended minor version upgrade
### 2025/10/10 - v2.4.3
> Thanks to [nidaye996](https://github.com/sansan0/TrendRadar/issues/98) for discovering the UX issue
- **Updates**:
- Refactored "Silent Push Mode" naming to "Push Time Window Control", improving feature comprehension
- Clarified push time window as optional additional feature, can work with three push modes
- Improved comments and documentation, making feature positioning clearer
- **Upgrade Note**:
- This is just refactoring, upgrade optional
### 2025/10/8 - v2.4.2
- **Updates**:
- Fixed ntfy push encoding issue
- Fixed missing config file issue
- Optimized ntfy push effect
- Added GitHub Pages image segmented export feature
- **Upgrade Note**:
- Recommend major version update
### 2025/10/2 - v2.4.0
**Added ntfy Push Notification**
- **Core Features**:
- Supports ntfy.sh public service and self-hosted servers
- **Use Cases**:
- Suitable for privacy-conscious users (supports self-hosting)
- Cross-platform push (iOS, Android, Desktop, Web)
- No account registration needed (public servers)
- Open-source and free (MIT License)
- **Upgrade Note**:
- Recommend major version update
### 2025/09/26 - v2.3.2
- Fixed email notification config check being missed ([#88](https://github.com/sansan0/TrendRadar/issues/88))
**Fix Description**:
- Solved the issue where system still prompted "No webhook configured" even with correct email notification setup
### 2025/09/22 - v2.3.1
- **Added email push feature**, supports sending trending news reports to email
- **Smart SMTP Recognition**: Auto-detects Gmail, QQ Mail, Outlook, NetEase Mail and 10+ email service providers
- **Beautiful HTML Format**: Email content uses same HTML format as web version, well-formatted, mobile-adapted
- **Batch Sending Support**: Supports multiple recipients, separated by commas
- **Custom SMTP**: Can customize SMTP server and port
- Fixed Docker build network connection issue
**Usage Notes**:
- Use cases: Suitable for users needing email archiving, team sharing, scheduled reports
- Supported emails: Gmail, QQ Mail, Outlook/Hotmail, 163/126 Mail, Sina Mail, Sohu Mail, etc.
**Upgrade Note**:
- This update has many changes, if upgrading, recommend major version upgrade
### 2025/09/17 - v2.2.0
- Added one-click save news as image feature, easily share trending topics you care about
**Usage Notes**:
- Use case: After enabling web version feature (GitHub Pages)
- How to use: Open webpage on phone or PC, click "Save as Image" button at top
- Actual effect: System auto-creates beautiful image of current news report, saves to phone album or desktop
- Sharing convenience: Directly send this image to friends, Moments, or work groups, letting others see your discovered important info
### 2025/09/13 - v2.1.2
- Solved DingTalk push capacity limit causing news push failure (using batch push)
### 2025/09/04 - v2.1.1
- Fixed Docker unable to run properly on certain architectures
- Officially released official Docker image wantcat/trendradar, supports multi-architecture
- Optimized Docker deployment process, can use quickly without local build
### 2025/08/30 - v2.1.0
**Core Improvements**:
- **Push Logic Optimization**: Changed from "push every execution" to "controllable push within time window"
- **Time Window Control**: Can set push time range, avoid non-work hour disturbance
- **Push Frequency Options**: Supports single push or multiple pushes within time window
**Upgrade Note**:
- This feature is disabled by default, need to manually enable push time window control in config.yaml
- Upgrade requires updating both main.py and config.yaml files
### 2025/08/27 - v2.0.4
- This version is not a bug fix, but an important reminder
- Please keep webhooks properly, do not make public, do not make public, do not make public
- If you deployed this project on GitHub via fork, please put webhooks in GitHub Secret, not config.yaml
- If you already exposed webhooks or put them in config.yaml, suggest deleting and regenerating
### 2025/08/06 - v2.0.3
- Optimized GitHub Pages web version effect, convenient for mobile use
### 2025/07/28 - v2.0.2
- Refactored code
- Solved version number easily being missed for modification
### 2025/07/27 - v2.0.1
**Fixed Issues**:
1. Docker shell script line ending as CRLF causing execution exception issue
2. frequency_words.txt being empty causing news sending also empty logic issue
- After fix, when you choose frequency_words.txt empty, will **push all news**, but limited by message push size, please adjust as follows
- Option 1: Turn off mobile push, only choose GitHub Pages deployment (this is the way to get most complete info, will re-sort all platform trending by your **custom trending algorithm**)
- Option 2: Reduce push platforms, prioritize **WeWork** or **Telegram**, these two pushes I made batch push feature (because batch push affects push experience, and only these two platforms give very little push capacity, so had to make batch push feature, but at least can ensure complete info)
- Option 3: Can combine with Option 2, mode choose current or incremental can effectively reduce one-time push content
### 2025/07/17 - v2.0.0
**Major Refactoring**:
- Config management refactoring: All configs now managed through `config/config.yaml` file (main.py I still didn't split, convenient for you to copy and upgrade)
- Run mode upgrade: Supports three modes - `daily` (daily summary), `current` (current rankings), `incremental` (incremental monitoring)
- Docker support: Complete Docker deployment solution, supports containerized operation
**Config File Description**:
- `config/config.yaml` - Main config file (application settings, crawler config, notification config, platform config, etc.)
- `config/frequency_words.txt` - Keyword config (monitoring vocabulary settings)
### 2025/07/09 - v1.4.1
**New Feature**: Added incremental push (configure FOCUS_NEW_ONLY at top of main.py), this switch only cares about new topics not sustained heat, only sends notification when new content appears.
**Fixed Issue**: Under certain circumstances, some news containing special symbols caused occasional formatting exceptions.
### 2025/06/23 - v1.3.0
WeWork and Telegram push messages have length limits, I adopted splitting messages for pushing. Development docs see [WeWork](https://developer.work.weixin.qq.com/document/path/91770) and [Telegram](https://core.telegram.org/bots/api)
### 2025/06/21 - v1.2.1
Before this version, not only main.py needs copy replacement, crawler.yml also needs you to copy replacement
https://github.com/sansan0/TrendRadar/blob/master/.github/workflows/crawler.yml
### 2025/06/19 - v1.2.0
> Thanks to Claude Research for organizing various platform APIs, helping me quickly complete platform adaptation (although code is more redundant~
1. Supports Telegram, WeWork, DingTalk push channels, supports multi-channel config and simultaneous push
### 2025/06/18 - v1.1.0
> **200 stars⭐** reached, continue celebrating with everyone~
1. Important update, added weight, news you see now is hottest most concerned appearing at top
2. Updated documentation usage, because recently updated many features, and previous usage docs I was lazy wrote simple (see ⚙️ frequency_words.txt complete configuration tutorial below)
### 2025/06/16 - v1.0.0
1. Added project new version update reminder, default on, if want to turn off, can change "FEISHU_SHOW_VERSION_UPDATE": True to False in main.py
### 2025/06/13+14
1. Removed compatibility code, students who forked before, directly copying code will show exception on same day (will recover normal next day)
2. Feishu and html bottom added new news display
### 2025/06/09
**100 stars⭐** reached, writing small feature to celebrate
frequency_words.txt file added **required word** feature, using + sign
1. Required word syntax as follows:
Tang Monk or Pig must both appear in title, will be included in push news
```
+Tang Monk
+Pig
```
2. Filter word priority higher:
If title filter word matches Tang Monk reciting sutras, then even if required word has Tang Monk, also not display
```
+Tang Monk
!Tang Monk reciting sutras
```
### 2025/06/02
1. **Webpage** and **Feishu messages** support phone directly jumping to detailed news
2. Optimized display effect + 1
### 2025/05/26
1. Feishu message display effect optimized
</details>
<br>
## ✨ Core Features
### **Multi-Platform Trending News Aggregation**
- Zhihu (知乎)
- Douyin (抖音)
- Bilibili Hot Search
- Wallstreetcn (华尔街见闻)
- Tieba (贴吧)
- Baidu Hot Search
- Yicai (财联社)
- Thepaper (澎湃新闻)
- Ifeng (凤凰网)
- Toutiao (今日头条)
- Weibo (微博)
Default monitoring of 11 mainstream platforms, with support for adding custom platforms.
> 💡 For detailed configuration, see [Configuration Guide - Platform Configuration](#1-platform-configuration)
### **RSS Feed Support** (v4.5.0 New)
Supports RSS/Atom feed crawling, keyword-based grouping and statistics (consistent with trending format):
- **Unified Format**: RSS and trending use the same keyword matching and display format
- **Simple Config**: Add RSS sources directly in `config.yaml`
- **Merged Push**: Trending and RSS are merged into a single notification
- **Freshness Filter**: Automatically filters out articles older than a specified number of days to avoid repeated pushes. Supports both global default and per-feed settings
> 💡 RSS uses the same `frequency_words.txt` for keyword filtering as trending
### **Visual Configuration Editor**
A web-based graphical configuration interface — no need to manually edit YAML files. Complete all configuration changes and exports through simple forms.
👉 **Try it online**: [https://sansan0.github.io/TrendRadar/](https://sansan0.github.io/TrendRadar/)
<img src="/_image/editor.png" alt="Visual Configuration Editor" width="80%">
### **Smart Push Strategies**
**Three Push Modes**:
| Mode | Target Users | Push Feature |
|------|--------------|--------------|
| **Daily Summary** (daily) | Managers/Regular Users | Push all matched news of the day (includes previously pushed) |
| **Current Rankings** (current) | Content Creators | Push current ranking matches (continuously ranked news appear each time) |
| **Incremental Monitor** (incremental) | Traders/Investors | Push only new content, zero duplication |
> 💡 **Quick Selection Guide:**
> - Don't want duplicate news → Use `incremental`
> - Want complete ranking trends → Use `current`
> - Need daily summary reports → Use `daily`
>
> For detailed comparison and configuration, see [Configuration Guide - Push Mode Details](#3-push-mode-details)
**Additional Features** (Optional):
| Feature | Description | Default |
|---------|-------------|---------|
| **Scheduling System** | Per-day-of-week scheduling: assign different time periods, push modes, and AI analysis strategies to each day (Mon–Sun). **Each period can independently set its filter method (keyword/AI) and interest focus**, enabling different content at different times. 5 built-in presets (always_on / morning_evening / office_hours / night_owl / custom), or define your own. Supports weekday vs weekend differentiation, cross-midnight periods, per-period once-only dedup, and overlap conflict detection (v6.0.0 + v6.5.0) | morning_evening |
| **Content Order Configuration** | Use `display.region_order` to adjust display order of all regions (hotlist, new items, RSS, standalone, AI analysis); use `display.regions` to toggle each region on/off (v5.2.0) | See config |
| **Display Mode Switch** | `keyword`=group by keyword, `platform`=group by platform (v4.6.0 new) | keyword |
> 💡 For detailed configuration, see [Configuration Guide - Report Configuration](#7-report-configuration) and [Configuration Guide - Scheduling System](#8-when-will-i-receive-pushes)
### **Precise Content Filtering**
Set personal keywords (e.g., AI, BYD, Education Policy) to receive only relevant trending news, filtering out noise.
> 💡 **Basic Configuration**: [Keyword Configuration - Basic Syntax](#keyword-basic-syntax)
>
> 💡 **Advanced Configuration**: [Keyword Configuration - Advanced Settings](#keyword-advanced-settings)
>
> 💡 You can also skip filtering and receive all trending news (leave frequency_words.txt empty)
### **AI Smart News Filtering** (v6.5.0 New)
Describe your interests in natural language and let AI automatically classify news — replacing traditional keyword matching
- **Natural Language Interests**: Write your focus areas in everyday language in `ai_interests.txt`, no keyword syntax to learn
- **Two-Stage Smart Processing**: AI first extracts structured tags from interest descriptions, then batch-classifies and scores news against those tags
- **Score Threshold Control**: Fine-tune push quality with `ai_filter.min_score` — only highly relevant news gets delivered
- **Auto Fallback**: Automatically falls back to keyword matching if AI filtering fails, ensuring uninterrupted push delivery
- **Smart Tag Updates**: When interests change, AI evaluates the change magnitude to decide incremental or full reclassification
- **Flexible Switching**: `filter.method` supports `keyword` (default) and `ai` modes, Timeline can override per time period
- **Per-Period Personalization**: Different time periods can use different keyword files or AI interest descriptions. For example: mornings use a "tech keyword list" for quick filtering, evenings switch to "finance interests" for AI deep filtering
```yaml
# config.yaml quick enable example
filter:
method: ai # keyword (default) | ai
ai_filter:
min_score: 6 # Minimum push score threshold (1-10)
```
> 💡 AI filtering shares model config with AI analysis/translation — just configure `ai.api_key` once
### **Trending Analysis**
Real-time tracking of news popularity changes helps you understand not just "what's trending" but "how trends evolve."
- **Timeline Tracking**: Records complete time span from first to last appearance
- **Popularity Changes**: Tracks ranking changes and appearance frequency across time periods
- **New Detection**: Real-time identification of emerging topics, marked with 🆕
- **Continuity Analysis**: Distinguishes between one-time hot topics and continuously developing news
- **Cross-Platform Comparison**: Same news across different platforms, showing media attention differences
> 💡 Push format reference: [Configuration Guide - Push Format Reference](#5-push-format-reference)
### **Personalized Trending Algorithm**
No longer controlled by platform algorithms, TrendRadar reorganizes all trending searches
> 💡 Weight adjustment guide: [Configuration Guide - Advanced Configuration](#4-advanced-configuration---hotspot-weight-adjustment)
### **Multi-Channel Multi-Account Push**
Supports **WeWork** (+ WeChat push solution), **Feishu**, **DingTalk**, **Telegram**, **Email**, **ntfy**, **Bark**, **Slack**, **Generic Webhook** (connect to Discord, IFTTT, or any platform) — messages delivered directly to phone and email.
> 💡 For detailed configuration, see [Configuration Guide - Multi-Account Push Configuration](#10-multiple-account-push-configuration)
### **AI Multi-Language Translation** (v5.2.0 New)
Translate push content into any language, breaking language barriers — whether reading domestic trends or subscribing to international news via RSS, access everything in your native language
- **One-Click Translation**: Set `ai_translation.enabled: true` and target language in `config.yaml`
- **Multi-Language Support**: Supports English, Korean, Japanese, French, and any other language
- **Smart Batch Processing**: Automatically batches translations to reduce API calls and save costs
- **Custom Style**: Customize translation style and terminology via `ai_translation_prompt.txt`
- **Shared Model Config**: Shares the `ai` config section with AI analysis feature
```yaml
# config.yaml quick enable example
ai_translation:
enabled: true
language: "English" # Target translation language
```
> 💡 Translation shares model config with AI analysis — just configure `ai.api_key` once to use both features
**RSS Source References**: Here are some RSS feed collections for your reference
- [awesome-tech-rss](https://github.com/tuan3w/awesome-tech-rss) - Tech, startup, and programming blogs & media
- [awesome-rss-feeds](https://github.com/plenaryapp/awesome-rss-feeds) - Mainstream news media RSS from countries worldwide
> ⚠️ Some international media content may involve sensitive topics that AI models might refuse to translate. Please filter subscription sources based on your actual needs
### **Flexible Storage Architecture (v4.0.0 Major Update)**
**Multi-Backend Support**:
- **Remote Cloud Storage**: GitHub Actions environment default, supports S3-compatible protocols (R2/OSS/COS, etc.), data stored in cloud, keeping repository clean
- **Local SQLite**: Traditional SQLite database, stable and efficient (Docker/local deployment)
- **Auto Selection**: Auto-selects appropriate backend based on runtime environment
> 💡 For storage configuration details, see [Configuration Details - Storage Configuration](#11-storage-configuration-v400-new)
### **Multi-Platform Deployment**
- **GitHub Actions**: Cloud automated operations (7-day check-in cycle + remote cloud storage)
- **Docker Deployment**: Supports multi-architecture containerized operation
- **Local Running**: Python environment direct execution
### **AI Analysis Push (v5.0.0 New)**
Use AI models to deeply analyze push content, automatically generate trending insights report
- **Smart Analysis**: Automatically analyze trending topics, keyword popularity, cross-platform correlation, potential impact
- **Multi Provider**: Built on LiteLLM unified interface, supports 100+ AI providers (DeepSeek, OpenAI, Gemini, Anthropic, local Ollama, etc.), with automatic fallback model switching
- **Independent Analysis Mode**: AI analysis scope can differ from push content — push only new items (less noise), while AI analyzes the full day's news (complete trend picture)
- **Flexible Push**: Choose original content only, AI analysis only, or both
- **Custom Prompts**: Customize analysis perspective via `config/ai_analysis_prompt.txt`
> 💡 Detailed configuration tutorial: [Let AI help me analyze hot topics](#12-let-ai-help-me-analyze-hot-topics)
### **Independent Display Section (v5.0.0 New)**
Provide complete trending display for specified platforms, unaffected by keyword filtering
- **Full Trending**: Specified platforms show complete trending list, for users who want to see full rankings
- **RSS Independent Display**: RSS source content can be fully displayed, not limited by keywords
- **AI Deep Analysis**: Independently enable AI trend analysis on full hotlists, without displaying in push
- **Flexible Configuration**: Support configuring display platforms, RSS sources, max count
> 💡 Detailed configuration tutorial: [Report Configuration - Independent Display](#7-report-configuration)
### **AI Smart Analysis (v3.0.0 New)**
AI conversational analysis system based on MCP (Model Context Protocol), enabling deep data mining with natural language.
> **💡 Usage Tip**: AI features require local news data support
> - Project includes test data for immediate feature experience
> - Recommend deploying the project yourself to get more real-time data
>
> See [AI Analysis](#-ai-analysis) for details
### **Web Deployment**
After running, the `index.html` generated in the root directory is the complete news report page.
> **Deployment**: Click **Use this template** to create your repository, then deploy to Cloudflare Pages or GitHub Pages.
>
> **💡 Tip**: Enable GitHub Pages for an online URL. Go to Settings → Pages to enable. [Preview Effect](https://sansan0.github.io/TrendRadar/)
>
> ⚠️ The GitHub Actions auto-storage feature has been discontinued (this approach caused excessive load on GitHub servers, affecting platform stability).
### **Reduce APP Dependencies**
Transform from "algorithm recommendation captivity" to "actively getting the information you want"
**Target Users:** Investors, content creators, PR professionals, news-conscious general users
**Typical Scenarios:** Stock investment monitoring, brand sentiment tracking, industry trend watching, lifestyle news gathering
| Web Effect (Email Push) | Feishu Push Effect | AI Analysis Push Effect |
|:---:|:---:|:---:|
|  |  |  |
<br>
## 🚀 Quick Start
> **Reminder**: You should first **[check the latest official documentation](https://github.com/sansan0/TrendRadar?tab=readme-ov-file)** to ensure the configuration steps are up to date.
### Choose the Deployment Method That Fits You
#### 🅰️ Option A: Docker Deployment (Recommended 🔥)
* **Features**: More stable than GitHub Actions
* **Best for**: Users with their own server, NAS, or an always-on PC
👉 **[Jump to Docker Deployment Tutorial](#6-docker-deployment)**
#### 🅱️ Option B: GitHub Actions Deployment (This Chapter ⬇️)
* **Features**: Data is stored in **Remote Cloud Storage** (no longer written to Git repo)
* **Storage**: Configure cloud storage service (e.g. Cloudflare R2, Alibaba Cloud OSS, Tencent Cloud COS, etc.)
* **Note**: Requires periodic check-in renewal (every 7 days)
### 1️⃣ Step 1: Get project code
Click the green **[Use this template]** button in the upper right corner of this repository → select "Create a new repository".
> ⚠️ Note:
> - Any mention of "Fork" in this document can be understood as "Use this template"
> - Using Fork may cause runtime issues, see [Issue #606](https://github.com/sansan0/TrendRadar/issues/606)
<br>
### 2️⃣ Step 2: Setup GitHub Secrets
In your Forked repository, go to `Settings` > `Secrets and variables` > `Actions` > `New repository secret`
**📌 Important Instructions (Please Read Carefully):**
- **One Name for One Secret**: For each configuration item, click the "New repository secret" button once and fill in a pair of "Name" and "Secret"
- **Cannot See Value After Saving is Normal**: For security reasons, after saving, you can only see the Name when re-editing, but not the Secret value
- **DO NOT Create Custom Names**: The Secret Name must **strictly use** the names listed below (e.g., `WEWORK_WEBHOOK_URL`, `FEISHU_WEBHOOK_URL`, etc.). Do not modify or create new names arbitrarily, or the system will not recognize them
- **Can Configure Multiple Platforms**: The system will send notifications to all configured platforms
**Configuration Example:**
<img src="_image/secrets.png" alt="GitHub Secrets Configuration Example"/>
As shown above, each row is a configuration item:
- **Name**: Must use the fixed names listed in the expanded sections below (e.g., `WEWORK_WEBHOOK_URL`)
- **Secret (Value)**: Fill in the actual content obtained from the corresponding platform (e.g., Webhook URL, Token, etc.)
<br>
<details>
<summary> <strong>👉 Click to expand: WeWork Bot</strong> (Simplest and fastest configuration)</summary>
<br>
**GitHub Secret Configuration (⚠️ Name must match exactly):**
- **Name**: `WEWORK_WEBHOOK_URL` (Please copy and paste this name, do not type manually to avoid typos)
- **Secret (Value)**: Your WeWork bot Webhook address
<br>
**Bot Setup Steps:**
#### Mobile Setup:
1. Open WeWork App → Enter target internal group chat
2. Click "…" button at top right → Select "Message Push"
3. Click "Add" → Name input "TrendRadar"
4. Copy Webhook address, click save, paste the copied content into GitHub Secret above
#### PC Setup Process Similar
</details>
<details>
<summary> <strong>👉 Click to expand: Personal WeChat Push</strong> (Based on WeWork app, push to personal WeChat)</summary>
<br>
> This solution is based on WeWork's plugin mechanism. The push style is plain text (no markdown format), but it can push directly to personal WeChat without installing WeWork App.
**GitHub Secret Configuration (⚠️ Name must match exactly):**
- **Name**: `WEWORK_WEBHOOK_URL` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Your WeWork app Webhook address
- **Name**: `WEWORK_MSG_TYPE` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: `text`
<br>
**Setup Steps:**
1. Complete the WeWork bot Webhook setup above
2. Add `WEWORK_MSG_TYPE` Secret with value `text`
3. Follow the image below to link personal WeChat
4. After configuration, WeWork App can be deleted from phone
<img src="_image/wework.png" title="Personal WeChat Push Configuration"/>
**Notes**:
- Uses the same Webhook address as WeWork bot
- Difference is message format: `text` for plain text, `markdown` for rich text (default)
- Plain text format will automatically remove all markdown syntax (bold, links, etc.)
</details>
<details>
<summary> <strong>👉 Click to expand: Feishu Bot</strong> (Message display is relatively friendly)</summary>
<br>
**Note**: If **AI Analysis** is enabled, Feishu push notifications may occasionally (approx. 5% probability) experience a few minutes of delay. This is likely due to the platform's internal compliance auditing for AI-generated content.
**GitHub Secret Configuration (⚠️ Name must match exactly):**
- **Name**: `FEISHU_WEBHOOK_URL` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Your Feishu bot Webhook address (link starts with https://www.feishu.cn/flow/api/trigger-webhook/********)
<br>
Two methods available, **Method 1** is simpler, **Method 2** is more complex (but stable push)
Method 1 discovered and suggested by **ziventian**, thanks to them. Default is personal push, group push can be configured via [#97](https://github.com/sansan0/TrendRadar/issues/97)
**Method 1:**
> For some users, additional operations needed to avoid "System Error". Need to search for the bot on mobile and enable Feishu bot application (suggestion from community, can refer)
1. Open in PC browser https://botbuilder.feishu.cn/home/my-command
2. Click "New Bot Command"
3. Click "Select Trigger", scroll down, click "Webhook Trigger"
4. Now you'll see "Webhook Address", copy this link to local notepad temporarily, continue with next steps
5. In "Parameters" put the following content, then click "Done"
```json
{
"message_type": "text",
"content": {
"text": "{{Content}}"
}
}
```
6. Click "Select Action" > "Send via Official Bot"
7. Message title fill "TrendRadar Trending Monitor"
8. Most critical part, click + button, select "Webhook Trigger", then arrange as shown in image

9. After configuration, put Webhook address from step 4 into GitHub Secrets `FEISHU_WEBHOOK_URL`
<br>
**Method 2:**
1. Open in PC browser https://botbuilder.feishu.cn/home/my-app
2. Click "New Bot Application"
3. After entering the created application, click "Process Design" > "Create Process" > "Select Trigger"
4. Scroll down, click "Webhook Trigger"
5. Now you'll see "Webhook Address", copy this link to local notepad temporarily, continue with next steps
6. In "Parameters" put the following content, then click "Done"
```json
{
"message_type": "text",
"content": {
"text": "{{Content}}"
}
}
```
7. Click "Select Action" > "Send Feishu Message", check "Group Message", then click the input box below, click "Groups I Manage" (if no group, you can create one in Feishu app)
8. Message title fill "TrendRadar Trending Monitor"
9. Most critical part, click + button, select "Webhook Trigger", then arrange as shown in image

10. After configuration, put Webhook address from step 5 into GitHub Secrets `FEISHU_WEBHOOK_URL`
</details>
<details>
<summary> <strong>👉 Click to expand: DingTalk Bot</strong></summary>
<br>
**GitHub Secret Configuration (⚠️ Name must match exactly):**
- **Name**: `DINGTALK_WEBHOOK_URL` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Your DingTalk bot Webhook address
<br>
**Bot Setup Steps:**
1. **Create Bot (PC Only)**:
- Open DingTalk PC client, enter target group chat
- Click group settings icon (⚙️) → Scroll down to find "Bot" and click
- Select "Add Bot" → "Custom"
2. **Configure Bot**:
- Set bot name
- **Security Settings**:
- **Custom Keywords**: Set "Trending" or "热点"
3. **Complete Setup**:
- Check service terms agreement → Click "Done"
- Copy the obtained Webhook URL
- Put URL into GitHub Secrets `DINGTALK_WEBHOOK_URL`
**Note**: Mobile can only receive messages, cannot create new bots.
</details>
<details>
<summary> <strong>👉 Click to expand: Telegram Bot</strong></summary>
<br>
**GitHub Secret Configuration (⚠️ Name must match exactly):**
- **Name**: `TELEGRAM_BOT_TOKEN` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Your Telegram Bot Token
- **Name**: `TELEGRAM_CHAT_ID` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Your Telegram Chat ID
**Note**: Telegram requires **two** Secrets, please click "New repository secret" button twice to add them separately
<br>
**Bot Setup Steps:**
1. **Create Bot**:
- Search `@BotFather` in Telegram (note case, has blue verification checkmark, shows ~37849827 monthly users, this is official, beware of fake accounts)
- Send `/newbot` command to create new bot
- Set bot name (must end with "bot", easily runs into duplicate names, so think creatively)
- Get Bot Token (format like: `123456789:AAHfiqksKZ8WmR2zSjiQ7_v4TMAKdiHm9T0`)
2. **Get Chat ID**:
**Method 1: Via Official API**
- First send a message to your bot
- Visit: `https://api.telegram.org/bot<Your Bot Token>/getUpdates`
- Find the number in `"chat":{"id":number}` in returned JSON
**Method 2: Using Third-Party Tool**
- Search `@userinfobot` and send `/start`
- Get your user ID as Chat ID
3. **Configure to GitHub**:
- `TELEGRAM_BOT_TOKEN`: Fill in Bot Token from step 1
- `TELEGRAM_CHAT_ID`: Fill in Chat ID from step 2
</details>
<details>
<summary> <strong>👉 Click to expand: Email Push</strong> (Supports all mainstream email providers)</summary>
<br>
- Note: To prevent email bulk sending abuse, current bulk sending allows all recipients to see each other's email addresses.
- If you haven't configured email sending before, not recommended to try
> ⚠️ **Important Configuration Dependency**: Email push requires HTML report file. Make sure `storage.formats.html` is set to `true` in `config/config.yaml`:
> ```yaml
> storage:
> formats:
> sqlite: true
> txt: false
> html: true # Must be enabled, otherwise email push will fail
> ```
> If set to `false`, email push will report error: `Error: HTML file does not exist or not provided: None`
<br>
**GitHub Secret Configuration (⚠️ Name must match exactly):**
- **Name**: `EMAIL_FROM` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Sender email address
- **Name**: `EMAIL_PASSWORD` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Email password or authorization code
- **Name**: `EMAIL_TO` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Recipient email address (multiple separated by comma, or can be same as EMAIL_FROM to send to yourself)
- **Name**: `EMAIL_SMTP_SERVER` (Optional, please copy and paste this name)
- **Secret (Value)**: SMTP server address (leave empty for auto-detection)
- **Name**: `EMAIL_SMTP_PORT` (Optional, please copy and paste this name)
- **Secret (Value)**: SMTP port (leave empty for auto-detection)
**Note**: Email push requires at least **3 required** Secrets (EMAIL_FROM, EMAIL_PASSWORD, EMAIL_TO), the last two are optional
<br>
**Supported Email Providers** (Auto-detect SMTP config):
| Provider | Domain | SMTP Server | Port | Encryption |
|----------|--------|-------------|------|-----------|
| **Gmail** | gmail.com | smtp.gmail.com | 587 | TLS |
| **QQ Mail** | qq.com | smtp.qq.com | 465 | SSL |
| **Outlook** | outlook.com | smtp-mail.outlook.com | 587 | TLS |
| **Hotmail** | hotmail.com | smtp-mail.outlook.com | 587 | TLS |
| **Live** | live.com | smtp-mail.outlook.com | 587 | TLS |
| **163 Mail** | 163.com | smtp.163.com | 465 | SSL |
| **126 Mail** | 126.com | smtp.126.com | 465 | SSL |
| **Sina Mail** | sina.com | smtp.sina.com | 465 | SSL |
| **Sohu Mail** | sohu.com | smtp.sohu.com | 465 | SSL |
| **189 Mail** | 189.cn | smtp.189.cn | 465 | SSL |
| **Aliyun Mail** | aliyun.com | smtp.aliyun.com | 465 | TLS |
| **Yandex Mail** | yandex.com | smtp.yandex.com | 465 | TLS |
| **iCloud Mail** | icloud.com | smtp.mail.me.com | 587 | SSL |
> **Auto-detect**: When using above emails, no need to manually configure `EMAIL_SMTP_SERVER` and `EMAIL_SMTP_PORT`, system auto-detects.
>
> **Feedback Notice**:
> - If you successfully test with **other email providers**, please open an [Issue](https://github.com/sansan0/TrendRadar/issues) to let us know, we'll add to support list
> - If above email configurations are incorrect or unusable, please also open an [Issue](https://github.com/sansan0/TrendRadar/issues) for feedback to help improve the project
>
> **Special Thanks**:
> - Thanks to [@DYZYD](https://github.com/DYZYD) for contributing 189 Mail (189.cn) configuration and completing self-send-receive testing ([#291](https://github.com/sansan0/TrendRadar/issues/291))
> - Thanks to [@longzhenren](https://github.com/longzhenren) for contributing Aliyun Mail (aliyun.com) configuration and completing testing ([#344](https://github.com/sansan0/TrendRadar/issues/344))
> - Thanks to [@ACANX](https://github.com/ACANX) for contributing Yandex Mail (yandex.com) configuration and completing testing ([#663](https://github.com/sansan0/TrendRadar/issues/663))
> - Thanks to [@Sleepy-Tianhao](https://github.com/Sleepy-Tianhao) for contributing iCloud Mail (icloud.com) configuration and completing testing ([#728](https://github.com/sansan0/TrendRadar/issues/728))
**Common Email Settings:**
#### QQ Mail:
1. Login QQ Mail web version → Settings → Account
2. Enable POP3/SMTP service
3. Generate authorization code (16-letter code)
4. `EMAIL_PASSWORD` fill authorization code, not QQ password
#### Gmail:
1. Enable two-step verification
2. Generate app-specific password
3. `EMAIL_PASSWORD` fill app-specific password
#### 163/126 Mail:
1. Login web version → Settings → POP3/SMTP/IMAP
2. Enable SMTP service
3. Set client authorization code
4. `EMAIL_PASSWORD` fill authorization code
<br>
**Advanced Configuration**:
If auto-detect fails, manually configure SMTP:
- `EMAIL_SMTP_SERVER`: Like smtp.gmail.com
- `EMAIL_SMTP_PORT`: Like 587 (TLS) or 465 (SSL)
<br>
**Multiple Recipients (note: English comma separator)**:
- EMAIL_TO="user1@example.com,user2@example.com,user3@example.com"
</details>
<details>
<summary> <strong>👉 Click to expand: ntfy Push</strong> (Open-source, free, self-hostable)</summary>
<br>
**Two Usage Methods:**
### Method 1: Free Use (Recommended for Beginners) 🆓
**Features**:
- ✅ No account registration, use immediately
- ✅ 250 messages/day (enough for 90% users)
- ✅ Topic name is "password" (need to choose hard-to-guess name)
- ⚠️ Messages unencrypted, not for sensitive info, but suitable for our non-sensitive project info
**Quick Start:**
1. **Download ntfy App**:
- Android: [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) / [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/)
- iOS: [App Store](https://apps.apple.com/us/app/ntfy/id1625396347)
- Desktop: Visit [ntfy.sh](https://ntfy.sh)
2. **Subscribe to Topic** (choose a hard-to-guess name):
```
Suggested format: trendradar-{your initials}-{random numbers}
Cannot use Chinese
✅ Good example: trendradar-zs-8492
❌ Bad example: news, alerts (too easy to guess)
```
3. **Configure GitHub Secret (⚠️ Name must match exactly)**:
- **Name**: `NTFY_TOPIC` (Please copy and paste this name, do not type manually)
- **Secret (Value)**: Fill in your subscribed topic name
- **Name**: `NTFY_SERVER_URL` (Optional, please copy and paste this name)
- **Secret (Value)**: Leave empty (default uses ntfy.sh)
- **Name**: `NTFY_TOKEN` (Optional, please copy and paste this name)
- **Secret (Value)**: Leave empty
**Note**: ntfy requires at least 1 required Secret (NTFY_TOPIC), the last two are optional
4. **Test**:
```bash
curl -d "Test message" ntfy.sh/your-topic-name
```
---
### Method 2: Self-Hosting (Complete Privacy Control) 🔒
**Target Users**: Have server, pursue complete privacy, strong technical ability
**Advantages**:
- ✅ Completely open-source (Apache 2.0 + GPLv2)
- ✅ Complete data self-control
- ✅ No restrictions
- ✅ Zero cost
**Docker One-Click Deploy**:
```bash
docker run -d \
--name ntfy \
-p 80:80 \
-v /var/cache/ntfy:/var/cache/ntfy \
binwiederhier/ntfy \
serve --cache-file /var/cache/ntfy/cache.db
```
**Configure TrendRadar**:
```yaml
NTFY_SERVER_URL: https://ntfy.yourdomain.com
NTFY_TOPIC: trendradar-alerts # Self-hosting can use simple name
NTFY_TOKEN: tk_your_token # Optional: Enable access control
```
**Subscribe in App**:
- Click "Use another server"
- Enter your server address
- Enter topic name
- (Optional) Enter login credentials
---
**FAQ:**
<details>
<summary><strong>Q1: Is the free version enough?</strong></summary>
250 messages/day is enough for most users. With 30-minute crawl intervals, about 48 pushes/day, completely sufficient.
</details>
<details>
<summary><strong>Q2: Is the Topic name really secure?</strong></summary>
If you choose a random, sufficiently long name (like `trendradar-zs-8492-news`), brute force is nearly impossible:
- ntfy has strict rate limiting (1 request/second)
- 64 character choices (A-Z, a-z, 0-9, _, -)
- 10 random characters have 64^10 possibilities (would take years to crack)
</details>
---
**Recommended Choice:**
| User Type | Recommended | Reason |
|-----------|-------------|--------|
| Regular Users | Method 1 (Free) | Simple, fast, enough |
| Technical Users | Method 2 (Self-Host) | Complete control, unlimited |
| High-Frequency Users | Method 3 (Paid) | Check official website |
**Related Links:**
- [ntfy Official Docs](https://docs.ntfy.sh/)
- [Self-Hosting Tutorial](https://docs.ntfy.sh/install/)
- [GitHub Repository](https://github.com/binwiederhier/ntfy)
</details>
<details>
<summary>👉 Click to expand: <strong>Bark Push</strong> (iOS exclusive, clean & efficient)</summary>
<br>
**GitHub Secret Configuration (⚠️ Name must be exact):**
- **Name**: `BARK_URL` (copy and paste this name, don't type manually)
- **Secret**: Your Bark push URL
<br>
**Bark Introduction:**
Bark is a free open-source push tool for iOS platform, featuring simplicity, speed, and no ads.
**Usage Methods:**
### Method 1: Use Official Server (Recommended for beginners) 🆓
1. **Download Bark App**:
- iOS: [App Store](https://apps.apple.com/us/app/bark-customed-notifications/id1403753865)
2. **Get Push URL**:
- Open Bark App
- Copy the push URL displayed on the home page (format: `https://api.day.app/your_device_key`)
- Configure the URL to GitHub Secrets as `BARK_URL`
### Method 2: Self-Hosted Server (Complete Privacy Control) 🔒
**Suitable for**: Users with servers, pursuing complete privacy, strong technical skills
**Docker One-Click Deployment**:
```bash
docker run -d \
--name bark-server \
-p 8080:8080 \
finab/bark-server
```
**Configure TrendRadar**:
```yaml
BARK_URL: http://your-server-ip:8080/your_device_key
```
---
**Notes:**
- ✅ Bark uses APNs push, max 4KB per message
- ✅ Supports automatic batch sending, no worry about long messages
- ✅ Push format is plain text (automatically removes Markdown syntax)
- ⚠️ Only supports iOS platform
**Related Links:**
- [Bark Official Website](https://bark.day.app/)
- [Bark GitHub Repository](https://github.com/Finb/Bark)
- [Bark Server Self-Hosting Tutorial](https://github.com/Finb/bark-server)
</details>
<details>
<summary>👉 Click to expand: <strong>Slack Push</strong></summary>
<br>
**GitHub Secret Configuration (⚠️ Name must be exact):**
- **Name**: `SLACK_WEBHOOK_URL` (copy and paste this name, don't type manually)
- **Secret**: Your Slack Incoming Webhook URL
<br>
**Slack Introduction:**
Slack is a team collaboration tool, Incoming Webhooks can push messages to Slack channels.
**Setup Steps:**
### Step 1: Create Slack App
1. **Visit Slack API Page**:
- Open https://api.slack.com/apps?new_app=1
- Login to your Slack workspace if not logged in
2. **Choose Creation Method**:
- Click **"From scratch"**
3. **Fill in App Information**:
- **App Name**: Enter app name (e.g., `TrendRadar` or `Hot News Monitor`)
- **Workspace**: Select your workspace from dropdown
- Click **"Create App"** button
### Step 2: Enable Incoming Webhooks
1. **Navigate to Incoming Webhooks**:
- Find and click **"Incoming Webhooks"** in left menu
2. **Enable Feature**:
- Find **"Activate Incoming Webhooks"** toggle
- Switch from `OFF` to `ON`
- Page will auto-refresh showing new configuration options
### Step 3: Generate Webhook URL
1. **Add New Webhook**:
- Scroll to page bottom
- Click **"Add New Webhook to Workspace"** button
2. **Select Target Channel**:
- System will show authorization page
- Select channel to receive messages from dropdown (e.g., `#hot-news`)
- ⚠️ For private channels, must join the channel first
3. **Authorize App**:
- Click **"Allow"** button to complete authorization
- System will auto-redirect back to config page
### Step 4: Copy and Save Webhook URL
1. **View Generated URL**:
- In "Webhook URLs for Your Workspace" section
- You'll see the newly generated Webhook URL
- Format: `https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX`
2. **Copy URL**:
- Click **"Copy"** button on the right of URL
- Or manually select and copy URL
3. **Configure to TrendRadar**:
- **GitHub Actions**: Add URL to GitHub Secrets as `SLACK_WEBHOOK_URL`
- **Local Testing**: Fill URL in `config/config.yaml` `slack_webhook_url` field
- **Docker Deployment**: Add URL to `docker/.env` file as `SLACK_WEBHOOK_URL` variable
---
**Notes:**
- ✅ Supports Markdown format (auto-converts to Slack mrkdwn)
- ✅ Supports automatic batch sending (4KB per batch)
- ✅ Suitable for team collaboration, centralized message management
- ⚠️ Webhook URL contains secret key, never make it public
**Message Format Preview:**
```
*[Batch 1/2]*
📊 *Trending Topics Statistics*
🔥 *[1/3] AI ChatGPT* : 2 articles
1. [Baidu Hot] 🆕 ChatGPT-5 Official Release *[1]* - 09:15 (1 time)
2. [Toutiao] AI Chip Stocks Surge *[3]* - [08:30 ~ 10:45] (3 times)
```
**Related Links:**
- [Slack Incoming Webhooks Official Docs](https://api.slack.com/messaging/webhooks)
- [Slack API App Management](https://api.slack.com/apps)
</details>
<details>
<summary>👉 Click to expand: <strong>Generic Webhook Push</strong> (Supports Discord, Matrix, IFTTT, etc.)</summary>
<br>
**GitHub Secret Configuration (⚠️ Name must be exact):**
- **Name**: `GENERIC_WEBHOOK_URL` (copy and paste this name, don't type manually)
- **Secret**: Your Webhook URL
- **Name**: `GENERIC_WEBHOOK_TEMPLATE` (optional, copy and paste this name)
- **Secret**: JSON template string, supports `{title}` and `{content}` placeholders
<br>
**Generic Webhook Introduction:**
Generic Webhook supports any platform that accepts HTTP POST requests, including but not limited to:
- **Discord**: Push to channels via Webhook
- **Matrix**: Push via Webhook bridge
- **IFTTT**: Trigger automation workflows
- **Custom Services**: Any custom service supporting Webhooks
**Configuration Examples:**
### Discord Configuration
1. **Get Webhook URL**:
- Go to Discord Server Settings → Integrations → Webhooks
- Create new Webhook, copy URL
2. **Configure Template**:
```json
{"content": "{content}"}
```
3. **GitHub Secret Configuration**:
- `GENERIC_WEBHOOK_URL`: Discord Webhook URL
- `GENERIC_WEBHOOK_TEMPLATE`: `{"content": "{content}"}`
### Custom Templates
Templates support two placeholders:
- `{title}` - Message title
- `{content}` - Message content
**Template Examples**:
```json
# Default format (used when empty)
{"title": "{title}", "content": "{content}"}
# Discord format
{"content": "{content}"}
# Custom format
{"text": "{content}", "username": "TrendRadar"}
```
---
**Notes:**
- ✅ Supports Markdown format (same as WeWork format)
- ✅ Supports automatic batch sending
- ✅ Supports multi-account configuration (use `;` separator)
- ⚠️ Template must be valid JSON format
- ⚠️ Different platforms have different message format requirements, please refer to target platform documentation
</details>
> ⚠️ Note:
> - For first deployment, suggest completing **GitHub Secrets** configuration first (choose one push platform), then jump to [Step 3] to test push success.
> - **Don't modify** `config/config.yaml` and `frequency_words.txt` temporarily, adjust these configs after push test succeeds as needed.
<br>
### 3️⃣ Step 3: Manual Test News Push
> ⚠️ Reminder:
> - Complete Step 1-2 first, then test immediately! Test success first, then adjust configuration (Step 4) as needed.
> - IMPORTANT: Enter your own forked project, not this project!
**How to find your Actions page**:
- **Method 1**: Open your forked project homepage, click the **Actions** tab at the top
- **Method 2**: Direct access `https://github.com/YourUsername/TrendRadar/actions`
**Example comparison**:
- ❌ Author's project: `https://github.com/sansan0/TrendRadar/actions`
- ✅ Your project: `https://github.com/YourUsername/TrendRadar/actions`
**Testing steps**:
1. Enter your project's Actions page
2. Find **"Hot News Crawler"** and click in
- If you don't see this text, refer to [#109](https://github.com/sansan0/TrendRadar/issues/109) to solve
3. Click **"Run workflow"** button on the right to run
4. Wait about 1 minute, messages will be pushed to your configured platform
> ⚠️ Note:
> - Don't test too frequently to avoid triggering GitHub Actions limits
> - After clicking Run workflow, you need to **refresh the browser page** to see the new run record
<br>
### 4️⃣ Step 4: Configuration Notes (Optional)
The default configuration is ready to use. If you need personalized adjustments, just understand the following files:
| File | Purpose |
|------|---------|
| `config/config.yaml` | Main config file: push mode, time window, platform list, hotspot weights, etc. |
| `config/frequency_words.txt` | Keyword file: set your interested keywords, filter push content |
| `.github/workflows/crawler.yml` | Execution frequency: control how often to run (⚠️ modify carefully) |
👉 **Detailed Configuration Tutorial**: [Configuration Guide](#configuration-guide)
<br>
### 5️⃣ Step 5: GitHub Actions Check-In & Remote Cloud Storage
**v4.0.0 Important Change**: Introduced the "Activity Detection" mechanism; GitHub Actions need periodic check-ins to maintain operation.
- **Running Cycle**: Valid for **7 days**—service will automatically suspend when countdown ends.
- **Renewal Method**: Manually trigger the "Check In" workflow on the Actions page to reset the 7-day validity period.
- **Operation Path**: `Actions` → `Check In` → `Run workflow`
- **Design Philosophy**:
- If you forget for 7 days, maybe you don't really need it. Letting it stop is a digital detox, freeing you from the constant impact.
- GitHub Actions is a valuable public computing resource. The check-in mechanism aims to prevent wasted computing cycles, ensuring resources are allocated to truly active users who need them. Thank you for your understanding and support.
---
**You can also choose NOT to configure remote cloud storage**, but then you will be in **Lite Mode** with some advanced features unavailable.
**Two Deployment Modes Comparison:**
| Mode | Configuration Required | Features |
|------|------------------------|----------|
| **Lite Mode** | No storage configuration needed | Real-time crawling + Keyword filtering + Multi-channel push |
| **Full Mode** | Configure remote cloud storage | Lite Mode + New detection + Trend tracking + Incremental push + AI analysis |
**Lite Mode Description**:
- ✅ Available: Real-time news crawling, keyword filtering, hotspot weight ranking, current list push
- ❌ Not Available: New news detection (🆕), trend tracking, incremental mode, daily summary accumulation, MCP AI analysis
**Full Mode Description**: Configure remote cloud storage to unlock all features. Continue with the configuration below.
<details>
<summary>👉 Click to expand: <strong>Remote Cloud Storage Configuration (Determines Feature Completeness) (Optional)</strong></summary>
<br>
**⚠️ Prerequisites for Cloudflare R2 Configuration:**
According to Cloudflare platform rules, enabling R2 requires binding a payment method.
* **Purpose**: Verify identity only, **no charges will be incurred**.
* **Payment**: Supports dual-currency credit cards or regional PayPal.
* **Usage**: R2's free tier (10GB storage/month) is sufficient for this project's daily operation, no need to worry about costs.
---
**GitHub Secret Configuration:**
**Required Configuration (4 items):**
| Name | Secret (Value) Description |
|------|----------------------------|
| `S3_BUCKET_NAME` | Bucket name (e.g., `trendradar-data`) |
| `S3_ACCESS_KEY_ID` | Access key ID |
| `S3_SECRET_ACCESS_KEY` | Access key |
| `S3_ENDPOINT_URL` | S3 API endpoint (e.g., R2: `https://<account-id>.r2.cloudflarestorage.com`) |
**Optional Configuration:**
| Name | Secret (Value) Description |
|------|----------------------------|
| `S3_REGION` | Region (default `auto`, some providers may require specification) |
> 💡 **More storage configuration options**: See [Storage Configuration Details](#11-storage-configuration-v400-new)
<br>
**How to Get Credentials (Using Cloudflare R2 as Example):**
1. Visit [Cloudflare Dashboard](https://dash.cloudflare.com/) and log in
2. Select `R2` in left menu → Click `Create Bucket` → Enter name (e.g., `trendradar-data`)
3. Click `Manage R2 API Tokens` at top right → `Create API Token`
4. Select `Object Read & Write` permission → After creation, it will display `Access Key ID` and `Secret Access Key`
5. Endpoint URL can be found in bucket details page (format: `https://<account-id>.r2.cloudflarestorage.com`)
</details>
<br>
### 6️⃣ Step 6: Enable AI Analysis Push
This is a core feature of v5.0.0, letting AI summarize and analyze news for you. Highly recommended.
**Configuration Method:**
Add the following to GitHub Secrets (or `.env` / `config.yaml`):
- `AI_API_KEY`: Your API Key (Supports DeepSeek, OpenAI, etc.)
- `AI_PROVIDER`: Provider name (e.g., `deepseek`, `openai`)
That's it! No complex deployment needed. You'll see the smart analysis report in the next push.
<br>
### 7️⃣ Step 7: 🎉 Deployment Success!
Congratulations! Now you can start enjoying the efficient information flow brought by TrendRadar.
💬 Many users are sharing their experiences on the official account, we look forward to your insights~
- Want to learn more tips and advanced techniques?
- Need quick answers to problems?
- Have great ideas to share?
👉 Follow the WeChat Official Account「**[Silicon Tea Room](#-support-project)**」, your likes and comments are the motivation for continuous updates.
<br>
### 8️⃣ Step 8: Advanced: Choose Your AI Assistant
TrendRadar provides two ways to use AI to meet different needs:
| Feature | ✨ AI Analysis Push (Step 6) | 🧠 AI Smart Analysis |
| :--- | :--- | :--- |
| **Mode** | **Passive Receipt** (Daily Report) | **Active Conversation** (Deep Research) |
| **Scenario** | "What's big today?" | "Analyze AI industry changes over the past week" |
| **Deployment** | Minimalist (Just add Key) | Advanced (Requires Local/Docker) |
| **Client** | Mobile | PC |
👉 **Conclusion**: Start with **AI Analysis Push** for daily needs; if you are a data analyst or need deep mining, try **[MCP Smart Analysis](#-ai-analysis)**.
<br>
<a name="configuration-guide"></a>
## ⚙️ Configuration Guide
> **📖 Reminder**: This chapter provides detailed configuration explanations. Suggest completing [Quick Start](#-quick-start) basic configuration first, then refer to detailed options here as needed.
### 1. Platform Configuration
<details id="custom-monitoring-platforms">
<summary>👉 Click to expand: <strong>Custom Monitoring Platforms</strong></summary>
<br>
**Configuration Location:** `platforms` section in `config/config.yaml`
This project's news data comes from [newsnow](https://github.com/ourongxing/newsnow). You can click the [website](https://newsnow.busiyi.world/), click [More], to see if there are platforms you want.
For specific additions, visit [project source code](https://github.com/ourongxing/newsnow/tree/main/server/sources), based on the file names there, modify the `platforms` configuration in `config/config.yaml` file:
```yaml
platforms:
enabled: true # Enable trending platform crawling
sources:
- id: "toutiao"
name: "Toutiao"
- id: "baidu"
name: "Baidu Hot Search"
- id: "wallstreetcn-hot"
name: "Wallstreetcn"
# Add more platforms...
```
> 💡 **Shortcut**: If you don't know how to read source code, you can copy from others' organized [Platform Configuration Summary](https://github.com/sansan0/TrendRadar/issues/95)
> ⚠️ **Note**: More platforms is not always better, suggest choosing 10-15 core platforms. Too many platforms will cause information overload and actually reduce user experience.
</details>
### 2. Keyword Configuration
**Configuration Location:** `config/frequency_words.txt`
Configure monitoring keywords in `frequency_words.txt` with seven syntax types, region markers, and grouping features.
| Syntax Type | Symbol | Purpose | Example | Matching Logic |
|------------|--------|---------|---------|----------------|
| **Normal** | None | Basic matching | `Huawei` | Match any one |
| **Required** | `+` | Scope limiting | `+phone` | Must include both |
| **Filter** | `!` | Noise exclusion | `!ad` | Exclude if included |
| **Count Limit** | `@` | Control display count | `@10` | Max 10 news (v3.2.0 new) |
| **Global Filter** | `[GLOBAL_FILTER]` | Globally exclude content | See example below | Filter under any circumstances (v3.5.0 new) |
| **Regex** | `/pattern/` | Precise matching | `/\bai\b/` | Match using regex (v4.7.0 new) |
| **Display Name** | `=> alias` | Custom display text | `/\bai\b/ => AI Related` | Show alias in push/HTML (v4.7.0 new) |
#### 2.1 Basic Syntax
<a name="keyword-basic-syntax"></a>
<details>
<summary>👉 Click to expand: <strong>Basic Syntax Tutorial</strong></summary>
<br>
##### 1. **Normal Keywords** - Basic Matching
```txt
Huawei
OPPO
Apple
```
**Effect:** News containing **any one** of these words will be captured
##### 2. **Required Words** `+word` - Scope Limiting
```txt
Huawei
OPPO
+phone
```
**Effect:** Must include both normal word **and** required word to be captured
##### 3. **Filter Words** `!word` - Noise Exclusion
```txt
Apple
Huawei
!fruit
!price
```
**Effect:** News containing filter words will be **excluded**, even if it contains keywords
##### 4. **Count Limit** `@number` - Control Display Count (v3.2.0 new)
```txt
Tesla
Musk
@5
```
**Effect:** Limit maximum news count for this keyword group
**Priority:** `@number` > Global config > Unlimited
##### 5. **Global Filter** `[GLOBAL_FILTER]` - Globally Exclude Content (v3.5.0 new)
```txt
[GLOBAL_FILTER]
advertisement
promotion
marketing
shocking
clickbait
[WORD_GROUPS]
technology
AI
Huawei
HarmonyOS
!car
```
**Effect:** Filters news containing specified words under **any circumstances**, with **highest priority**
**Use Cases:**
- Filter low-quality content: shocking, clickbait, breaking news, etc.
- Filter marketing content: advertisement, promotion, sponsorship, etc.
- Filter specific topics: entertainment, gossip (based on needs)
**Filter Priority:** Global Filter > Group Filter(`!`) > Group Matching
**Region Markers:**
- `[GLOBAL_FILTER]`: Global filter region, words are filtered under any circumstances
- `[WORD_GROUPS]`: Keyword groups region, maintains existing syntax (`!`, `+`, `@`)
- If no region markers are used, all content is treated as keyword groups (backward compatible)
**Matching Examples:**
```txt
[GLOBAL_FILTER]
advertisement
[WORD_GROUPS]
technology
AI
```
- ❌ "Advertisement: Latest tech product launch" ← Contains global filter word "advertisement", rejected
- ✅ "Tech company launches new AI product" ← No global filter words, matches "technology" group
- ✅ "AI technology breakthrough draws attention" ← No global filter words, matches "AI" in "technology" group
**Important Notes:**
- Use global filter words carefully to avoid over-filtering and missing valuable content
- Recommended to keep global filter words under 5-15
- For group-specific filtering, prioritize using group filter words (`!` prefix)
##### 6. **Regex** `/pattern/` - Precise Matching (v4.7.0 new)
Normal keywords use substring matching, which is convenient for Chinese but may cause false matches in English. For example, `ai` would match the `ai` in `training`.
Use regex syntax `/pattern/` to achieve precise matching:
```txt
/(?<![a-z])ai(?![a-z])/
artificial intelligence
```
**Effect:** Match using regular expressions, supports all Python regex syntax
**Common Regex Patterns:**
| Need | Regex | Description |
|------|-------|-------------|
| Word boundary | `/\bword\b/` | Match standalone word, e.g., `/\bai\b/` matches "AI" but not "training" |
| Non-letter boundary | `/(?<![a-z])ai(?![a-z])/` | Looser boundary, suitable for mixed Chinese-English |
| Start match | `/^breaking/` | Only match titles starting with "breaking" |
| End match | `/release$/` | Only match titles ending with "release" |
| Multiple options | `/apple\|huawei\|xiaomi/` | Match any one (note escaped `\|`) |
**Matching Examples:**
```txt
# Config
/(?<![a-z])ai(?![a-z])/
artificial intelligence
```
- ✅ "AI is the future" ← Matches standalone "AI"
- ✅ "Hello ai here" ← Non-letter boundaries, matches "ai"
- ✅ "Artificial intelligence grows rapidly" ← Matches "artificial intelligence"
- ❌ "Resistance training is important" ← "ai" in "training" doesn't match
- ❌ "The maid cleaned the room" ← "ai" in "maid" doesn't match
**Combined Usage:**
```txt
# Regex + Normal + Filter
/\bai\b/
artificial intelligence
machine learning
!advertisement
```
**Notes:**
- Regex automatically enables case-insensitive matching (`re.IGNORECASE`)
- Supports JavaScript-style `/pattern/i` syntax (flags are ignored since case-insensitive is always enabled)
- Invalid regex syntax will be treated as normal words
- Regex can be used for normal words, required words(`+`), and filter words(`!`)
**💡 Can't Write Regex? Let AI Help!**
If you're not familiar with regular expressions, just ask ChatGPT / Gemini / DeepSeek to generate one:
> I need a Python regex to match the word "ai" but not match "ai" in "training".
> Please give me the regex in `/pattern/` format without extra explanation.
AI will give you something like: `/(?<![a-zA-Z])ai(?![a-zA-Z])/`
##### 7. **Display Name** `=> alias` - Custom Display Text (v4.7.0 new)
Regex patterns can look unfriendly in push notifications and HTML pages. Use `=> alias` syntax to set a display name:
```txt
/(?<![a-zA-Z])ai(?![a-zA-Z])/ => AI Related
artificial intelligence
```
**Effect:** Push notifications and HTML pages show "AI Related" instead of the complex regex
**Syntax Format:**
```txt
# Regex + Display Name
/pattern/ => Display Name
/pattern/i => Display Name # Supports flags syntax (flags are ignored)
/pattern/=>Display Name # Spaces around => are optional
# Normal Word + Display Name
deepseek => DeepSeek News
```
**Example:**
```txt
# Config
/(?<![a-zA-Z])ai(?![a-zA-Z])/ => AI Related
artificial intelligence
```
| Original Config | Push/HTML Display |
|----------------|-------------------|
| `/(?<![a-z])ai(?![a-z])/` + `artificial intelligence` | `(?<![a-z])ai(?![a-z]) artificial intelligence` |
| `/(?<![a-z])ai(?![a-z])/ => AI Related` + `artificial intelligence` | **`AI Related`** |
**Notes:**
- Display name only needs to be set on the first word of a group
- If multiple words have display names, the first one is used
- Without display name, all words in the group are concatenated
---
#### 🔗 Group Feature - Importance of Empty Lines
**Core Rule:** Use **empty lines** to separate different groups, each group is independently counted
##### Example Configuration:
```txt
iPhone
Huawei
OPPO
+launch
A-shares
Shanghai Index
Shenzhen Index
+fluctuation
!prediction
World Cup
Euro Cup
Asian Cup
+match
```
##### Group Explanation and Matching Effects:
**Group 1 - Phone Launches:**
- Keywords: iPhone, Huawei, OPPO
- Required: launch
- Effect: Must include phone brand name and "launch"
**Matching Examples:**
- ✅ "iPhone 15 officially launched with pricing" ← Has "iPhone" + "launch"
- ✅ "Huawei Mate60 series launch livestream" ← Has "Huawei" + "launch"
- ✅ "OPPO Find X7 launch date confirmed" ← Has "OPPO" + "launch"
- ❌ "iPhone sales hit record high" ← Has "iPhone" but missing "launch"
**Group 2 - Stock Market:**
- Keywords: A-shares, Shanghai Index, Shenzhen Index
- Required: fluctuation
- Filter: prediction
- Effect: Include stock-related words and "fluctuation", but exclude "prediction"
**Matching Examples:**
- ✅ "A-shares major fluctuation analysis today" ← Has "A-shares" + "fluctuation"
- ✅ "Shanghai Index fluctuation reasons explained" ← Has "Shanghai Index" + "fluctuation"
- ❌ "Experts predict A-shares fluctuation trends" ← Has "A-shares" + "fluctuation" but contains "prediction"
- ❌ "A-shares trading volume hits new high" ← Has "A-shares" but missing "fluctuation"
**Group 3 - Football Events:**
- Keywords: World Cup, Euro Cup, Asian Cup
- Required: match
- Effect: Must include cup name and "match"
**Matching Examples:**
- ✅ "World Cup group stage match results" ← Has "World Cup" + "match"
- ✅ "Euro Cup final match time" ← Has "Euro Cup" + "match"
- ❌ "World Cup tickets on sale" ← Has "World Cup" but missing "match"
#### 🎯 Configuration Tips
##### 1. **From Broad to Strict Strategy**
```txt
# Step 1: Start with broad keywords for testing
Artificial Intelligence
AI
ChatGPT
# Step 2: After finding mismatches, add required words
Artificial Intelligence
AI
ChatGPT
+technology
# Step 3: After finding noise, add filter words
Artificial Intelligence
AI
ChatGPT
+technology
!advertisement
!training
```
##### 2. **Avoid Over-Complexity**
❌ **Not Recommended:** Too many words in one group
```txt
Huawei
OPPO
Apple
Samsung
vivo
OnePlus
Meizu
+phone
+launch
+sales
!fake
!repair
!second-hand
```
**Recommended:** Split into precise groups
```txt
Huawei
OPPO
+new product
Apple
Samsung
+launch
phone
sales
+market
```
</details>
#### 2.2 Advanced Settings (v3.2.0 new)
<a name="keyword-advanced-settings"></a>
<details>
<summary>👉 Click to expand: <strong>Advanced Settings Tutorial</strong></summary>
<br>
##### Keyword Sorting Priority
**Config Location:** `config/config.yaml`
```yaml
report:
sort_by_position_first: false # Sorting priority config
```
| Value | Sorting Rule | Use Case |
|-------|-------------|----------|
| `false` (default) | News count ↓ → Config position ↑ | Focus on popularity trends |
| `true` | Config position ↑ → News count ↓ | Focus on personal priority |
**Example:** Config order A, B, C, news count A(3), B(10), C(5)
- `false`: B(10) → C(5) → A(3)
- `true`: A(3) → B(10) → C(5)
##### Global Display Count Limit
```yaml
report:
max_news_per_keyword: 10 # Max 10 per keyword (0=unlimited)
```
**Docker Environment Variables:**
```bash
SORT_BY_POSITION_FIRST=true
MAX_NEWS_PER_KEYWORD=10
```
**Combined Example:**
```yaml
# config.yaml
report:
sort_by_position_first: true # Config order priority
max_news_per_keyword: 10 # Global default max 10 per keyword
```
```txt
# frequency_words.txt
Tesla
Musk
@20 # Key focus, show 20 (override global)
Huawei # Use global config, show 10
BYD
@5 # Limit to 5
```
**Final Effect:** Display in config order: Tesla(20) → Huawei(10) → BYD(5)
</details>
### 3. Which push mode should I choose?
<details>
<summary>👉 Click to expand: <strong>Detailed Comparison of 3 Modes</strong></summary>
<br>
**Configuration Location:** `report.mode` in `config/config.yaml`
```yaml
report:
mode: "daily" # Options: "daily" | "incremental" | "current"
```
#### Detailed Comparison Table
| Mode | Target Users | Push Timing | Display Content | Typical Use Case |
|------|----------|----------|----------|--------------|
| **Daily Summary**<br/>`daily` | 📋 Managers/Regular Users | Scheduled push (default hourly) | All matched news of the day<br/>+ New news section | **Example**: Check all important news of the day at 6 PM<br/>**Feature**: See full-day trend, don't miss any hot topic<br/>**Note**: Will include previously pushed news |
| **Current Rankings**<br/>`current` | 📰 Content Creators | Scheduled push (default hourly) | Current ranking matches<br/>+ New news section | **Example**: Track "which topics are hottest now" hourly<br/>**Feature**: Real-time understanding of current popularity ranking changes<br/>**Note**: Continuously ranked news appear each time |
| **Incremental Monitor**<br/>`incremental` | 📈 Traders/Investors | Push only when new | Newly appeared frequency word matches | **Example**: Monitor "Tesla", only notify when new news appears<br/>**Feature**: Zero duplication, only see first-time news<br/>**Suitable for**: High-frequency monitoring, avoid information disturbance |
#### Actual Push Effect Example
Assume you monitor "Apple" keyword, execute once per hour:
| Time | daily Mode Push | current Mode Push | incremental Mode Push |
|-----|--------------|----------------|-------------------|
| 10:00 | News A, News B | News A, News B | News A, News B |
| 11:00 | News A, News B, News C | News B, News C, News D | **Only** News C |
| 12:00 | News A, News B, News C | News C, News D, News E | **Only** News D, News E |
**Explanation**:
- `daily`: Cumulative display of all news of the day (A, B, C all retained)
- `current`: Display current ranking news (ranking changed, News D on list, News A off list)
- `incremental`: **Only push newly appeared news** (avoid duplicate disturbance)
#### Common Questions
> **💡 Encountered this problem?** 👉 "Execute once per hour, news output in first execution still appears in next hour execution"
> - **Reason**: You might have selected `daily` (Daily Summary) or `current` (Current Rankings) mode
> - **Solution**: Change to `incremental` (Incremental Monitor) mode, only push new content
#### ⚠️ Incremental Mode Important Notice
> **Users who selected `incremental` (Incremental Monitor) mode, please note:**
>
> 📌 **Incremental mode only pushes when there are new matching news**
>
> **If you haven't received push notifications for a long time, it may be because:**
> 1. No new hot topics matching your keywords in current time period
> 2. Keyword configuration is too strict or too broad
> 3. Too few monitoring platforms
>
> **Solutions:**
> - Solution 1: 👉 [Optimize Keyword Configuration](#2-keyword-configuration) - Adjust keyword precision, add or modify monitoring keywords
> - Solution 2: Switch push mode - Change to `current` or `daily` mode for scheduled push notifications
> - Solution 3: 👉 [Add More Platforms](#1-platform-configuration) - Add more news platforms to expand information sources
</details>
### 4. How to adjust hotness algorithm?
<details>
<summary>👉 Click to expand: <strong>Customize Hotspot Weights</strong></summary>
<br>
**Configuration Location:** `advanced.weight` section in `config/config.yaml`
```yaml
advanced:
weight:
rank: 0.6 # Ranking weight
frequency: 0.3 # Frequency weight
hotness: 0.1 # Hotness weight
```
Current default configuration is balanced.
#### Two Core Scenarios
**Real-Time Trending Type**:
```yaml
advanced:
weight:
rank: 0.8 # Mainly focus on ranking
frequency: 0.1 # Less concern about continuity
hotness: 0.1
```
**Target Users**: Content creators, marketers, users wanting to quickly understand current hot topics
**In-Depth Topic Type**:
```yaml
advanced:
weight:
rank: 0.4 # Moderate ranking focus
frequency: 0.5 # Emphasize sustained heat within the day
hotness: 0.1
```
**Target Users**: Investors, researchers, journalists, users needing deep trend analysis
#### Adjustment Method
1. **Three numbers must sum to 1.0**
2. **Increase what's important**: Increase `rank` for rankings, `frequency` for continuity
3. **Suggest adjusting 0.1-0.2 at a time**, observe effects
Core idea: Users pursuing speed and timeliness increase ranking weight, users pursuing depth and stability increase frequency weight.
</details>
### 5. What will the messages look like?
<details>
<summary>👉 Click to expand: <strong>Message Style Preview</strong></summary>
<br>
#### Push Example
📊 Trending Keywords Stats
🔥 [1/3] AI ChatGPT : 2 items
1. [Baidu Hot] 🆕 ChatGPT-5 officially launched [**1**] - 09:15 (1 time)
2. [Toutiao] AI chip concept stocks surge [**3**] - [08:30 ~ 10:45] (3 times)
━━━━━━━━━━━━━━━━━━━
📈 [2/3] BYD Tesla : 2 items
1. [Weibo] 🆕 BYD monthly sales break record [**2**] - 10:20 (1 time)
2. [Douyin] Tesla price reduction promotion [**4**] - [07:45 ~ 09:15] (2 times)
━━━━━━━━━━━━━━━━━━━
📌 [3/3] A-shares Stock Market : 1 item
1. [Wallstreetcn] A-shares midday review [**5**] - [11:30 ~ 12:00] (2 times)
🆕 New Trending News (Total 2 items)
**Baidu Hot** (1 item):
1. ChatGPT-5 officially launched [**1**]
**Weibo** (1 item):
1. BYD monthly sales break record [**2**]
Updated: 2025-01-15 12:30:15
#### Message Format Explanation
| Format Element | Example | Meaning | Description |
| ------------- | ------- | -------- | ----------- |
| 🔥📈📌 | 🔥 [1/3] AI ChatGPT | Popularity Level | 🔥 High (≥10) 📈 Medium (5-9) 📌 Normal (<5) |
| [Number/Total] | [1/3] | Rank Position | Current group rank among all matches |
| Keyword Group | AI ChatGPT | Keyword Group | Group from config, title must contain words |
| : N items | : 2 items | Match Count | Total news matching this group |
| [Platform] | [Baidu Hot] | Source Platform | Platform name of the news |
| 🆕 | 🆕 ChatGPT-5 officially launched | New Mark | First appearance in this round |
| [**number**] | [**1**] | High Rank | Rank ≤ threshold, bold red display |
| [number] | [7] | Normal Rank | Rank > threshold, normal display |
| - time | - 09:15 | First Time | Time when news was first discovered |
| [time~time] | [08:30 ~ 10:45] | Duration | Time range from first to last appearance |
| (N times) | (3 times) | Frequency | Total appearances during monitoring |
| **New Section** | 🆕 **New Trending News** | New Topic Summary | Separately shows newly appeared topics |
</details>
### 6. Docker Deployment
<details>
<summary>👉 Click to expand: <strong>Complete Docker Deployment Guide</strong></summary>
<br>
**Image Description:**
TrendRadar provides two independent Docker images, deploy according to your needs:
| Image Name | Purpose | Description |
|---------|------|------|
| `wantcat/trendradar` | News Push Service | Scheduled news crawling, push notifications (Required) |
| `wantcat/trendradar-mcp` | AI Analysis Service | MCP protocol support, AI dialogue analysis (Optional) |
> 💡 **Recommendations**:
> - Only need push functionality: Deploy `wantcat/trendradar` image only
> - Need AI analysis: Deploy both images
---
#### Method 1: Using docker compose (Recommended)
1. **Create Project Directory and Config**:
```bash
# Clone project to local
git clone https://github.com/sansan0/TrendRadar.git
cd TrendRadar
```
> 💡 **Note**: Key directory structure required for Docker deployment:
```
current directory/
├── config/
│ ├── config.yaml # Core config (required)
│ ├── frequency_words.txt # Keyword config (required)
│ ├── timeline.yaml # Timeline config
│ ├── ai_analysis_prompt.txt # AI analysis prompt (optional)
│ ├── ai_translation_prompt.txt # AI translation prompt (optional)
│ ├── ai_interests.txt # AI interest filtering config (optional)
│ ├── ai_filter/ # AI filter prompts
│ │ ├── prompt.txt
│ │ ├── extract_prompt.txt
│ │ └── update_tags_prompt.txt
│ └── custom/ # User custom config (optional)
│ ├── ai/ # Custom AI prompts
│ └── keyword/ # Custom keyword files
└── docker/
├── .env # Sensitive info + Docker-specific config
└── docker-compose.yml # Docker Compose orchestration file
```
2. **Config File Description**:
**Configuration Division Principles (v4.6.0 optimized)**:
| File | Purpose | Change Frequency | Description |
|------|---------|-----------------|-------------|
| `config/config.yaml` | **Core config** | Low | Report mode, push settings, storage format, push window, AI analysis toggle, platform enable/disable, etc. |
| `config/frequency_words.txt` | **Keyword config** | High | Set your interested trending keywords, supports groups, regex, aliases, and advanced syntax |
| `config/timeline.yaml` | **Timeline config** | Low | Controls news timeline display and filtering rules |
| `config/ai_analysis_prompt.txt` | **AI analysis prompt** | Medium | Customize AI analysis role definition and output format (v5.0.0+) |
| `config/ai_translation_prompt.txt` | **AI translation prompt** | Low | Customize AI translation prompt template |
| `config/ai_interests.txt` | **AI interest filtering** | Medium | Define rules for AI to auto-filter news based on interests |
| `config/ai_filter/` | **AI filter prompts** | Low | Internal prompts for AI filter module (usually no need to modify) |
| `config/custom/` | **User custom extensions** | As needed | `custom/ai/` for custom AI prompts, `custom/keyword/` for custom keyword files |
| `docker/.env` | **Sensitive info + Docker-specific config** | Low | Webhook URLs, API Keys, S3 credentials, scheduled tasks, **not tracked by git** |
> 💡 **Division Guidelines**:
> - **Feature behavior** → Edit `config.yaml` (e.g., enable/disable platforms, adjust push mode)
> - **Content of interest** → Edit `frequency_words.txt` (e.g., add new keywords to follow)
> - **AI output style** → Edit `ai_analysis_prompt.txt` or `ai_translation_prompt.txt`
> - **Keys & credentials** → Edit `docker/.env` (API Keys, Webhook URLs, and other sensitive info go here)
> - **Custom extensions** → Use `config/custom/` directory to avoid default configs being overwritten by upgrades
**⚙️ Environment Variable Override Mechanism (v3.0.5+)**
If you encounter **config.yaml modifications not taking effect** in NAS or other Docker environments, you can directly override configs via environment variables:
| Environment Variable | Corresponding Config | Example Value | Description |
|---------------------|---------------------|---------------|-------------|
| `ENABLE_WEBSERVER` | - | `true` / `false` | Auto-start web server |
| `WEBSERVER_PORT` | - | `8080` | Web server port |
| `WEBSERVER_WATCHDOG` | - | `true` / `false` | Turn on "auto-recover web page service" (restarts it if it crashes) |
| `WEBSERVER_WATCHDOG_INTERVAL` | - | `60` | How often to check and auto-recover (seconds) |
| `FEISHU_WEBHOOK_URL` | `notification.channels.feishu.webhook_url` | `https://...` | Feishu Webhook (multi-account use `;` separator) |
| `AI_ANALYSIS_ENABLED` | `ai_analysis.enabled` | `true` / `false` | Enable AI analysis (v5.0.0 new) |
| `AI_API_KEY` | `ai.api_key` | `sk-xxx...` | AI API Key (shared by ai_analysis and ai_translation) |
| `AI_PROVIDER` | `ai.provider` | `deepseek` / `openai` / `gemini` | AI provider (v5.0.0 new) |
| `S3_*` | `storage.remote.*` | - | Remote storage config (5 params) |
**Config Priority**: Environment Variables > config.yaml
**Usage Method**:
- Modify `.env` file, uncomment and fill in needed configs
- Or add directly in NAS/Synology Docker management interface's "Environment Variables"
- Restart container to take effect: `docker compose up -d`
3. **Start Service**:
**Option A: Start All Services (Push + AI Analysis)**
```bash
# Pull latest images
docker compose pull
# Start all services (trendradar + trendradar-mcp)
docker compose up -d
```
**Option B: Start News Push Service Only**
```bash
# Start trendradar only (scheduled crawling and push)
docker compose pull trendradar
docker compose up -d trendradar
```
**Option C: Start MCP AI Analysis Service Only**
```bash
# Start trendradar-mcp only (AI analysis interface)
docker compose pull trendradar-mcp
docker compose up -d trendradar-mcp
```
> 💡 **Tips**:
> - Most users only need to start `trendradar` for news push functionality
> - Only need to start `trendradar-mcp` when using ChatGPT/Gemini for AI dialogue analysis
> - Both services are independent and can be flexibly combined
4. **Check Running Status**:
```bash
# View news push service logs
docker logs -f trendradar
# View MCP AI analysis service logs
docker logs -f trendradar-mcp
# View all container status
docker ps | grep trendradar
# Stop specific service
docker compose stop trendradar # Stop push service
docker compose stop trendradar-mcp # Stop MCP service
```
#### Method 2: Local Build (Developer Option)
If you need custom code modifications or build your own image:
```bash
# Clone project
git clone https://github.com/sansan0/TrendRadar.git
cd TrendRadar
# Modify config files
vim config/config.yaml
vim config/frequency_words.txt
# Use build version docker compose
cd docker
cp docker-compose-build.yml docker-compose.yml
```
**Build and Start Services**:
```bash
# Option A: Build and start all services
docker compose build
docker compose up -d
# Option B: Build and start news push service only
docker compose build trendradar
docker compose up -d trendradar
# Option C: Build and start MCP AI analysis service only
docker compose build trendradar-mcp
docker compose up -d trendradar-mcp
```
> 💡 **Architecture Parameter Notes**:
> - Default builds `amd64` architecture images (suitable for most x86_64 servers)
> - To build `arm64` architecture (Apple Silicon, Raspberry Pi, etc.), set environment variable:
> ```bash
> export DOCKER_ARCH=arm64
> docker compose build
> ```
#### Image Update
```bash
# Method 1: Manual update (Crawler + MCP images)
docker pull wantcat/trendradar:latest
docker pull wantcat/trendradar-mcp:latest
docker compose down
docker compose up -d
# Method 2: Using docker compose update
docker compose pull
docker compose up -d
```
**Available Images**:
| Image Name | Purpose | Description |
|---------|------|---------|
| `wantcat/trendradar` | News Push Service | Scheduled news crawling, push notifications |
| `wantcat/trendradar-mcp` | MCP Service | AI analysis features (optional) |
#### Service Management Commands
```bash
# View running status
docker exec -it trendradar python manage.py status
# Manually execute crawler once
docker exec -it trendradar python manage.py run
# View real-time logs
docker exec -it trendradar python manage.py logs
# Display current config
docker exec -it trendradar python manage.py config
# Display output files
docker exec -it trendradar python manage.py files
# Web server management (for browser access to generated reports)
docker exec -it trendradar python manage.py start_webserver # Start web server
docker exec -it trendradar python manage.py stop_webserver # Stop web server
docker exec -it trendradar python manage.py webserver_status # Check web server status
# View help info
docker exec -it trendradar python manage.py help
# Restart container
docker restart trendradar
# Stop container
docker stop trendradar
# Remove container (keep data)
docker rm trendradar
```
> 💡 **Web Server Notes**:
> - After starting, access latest report at `http://localhost:8080`
> - Access historical reports via directory navigation (e.g., `http://localhost:8080/2025-xx-xx/`)
> - Port can be configured in `.env` file with `WEBSERVER_PORT` parameter
> - Auto-start: Set `ENABLE_WEBSERVER=true` in `.env`
> - Auto-recover: `WEBSERVER_WATCHDOG=true` (default). It checks every `WEBSERVER_WATCHDOG_INTERVAL` seconds and restarts the web page service if needed
> - `stop_webserver` means you manually turn off the web page service (command: `docker exec -it trendradar python manage.py stop_webserver`)
> - "Auto restart" means the system turns that web page service back on automatically. If you stopped it manually and want it back, run `docker exec -it trendradar python manage.py start_webserver`
> - Security: Static files only, limited to output directory, localhost binding only
#### Data Persistence
Generated reports and data are saved in `./output` directory by default. Data persists even if container is restarted or removed.
**📊 Web Report Access Paths**:
TrendRadar generates daily summary HTML reports to two locations simultaneously:
| File Location | Access Method | Use Case |
|--------------|---------------|----------|
| `output/index.html` | Direct host access | **Docker Deployment** (via Volume mount, visible on host) |
| `index.html` | Root directory access | **GitHub Pages** (repository root, auto-detected by Pages) |
| `output/html/YYYY-MM-DD/当日汇总.html` | Historical reports | All environments (archived by date) |
**Local Access Examples**:
```bash
# Method 1: Via Web Server (recommended, Docker environment)
# 1. Start web server
docker exec -it trendradar python manage.py start_webserver
# 2. Access in browser
http://localhost:8080 # Access latest report (default index.html)
http://localhost:8080/html/2025-xx-xx/ # Access reports for specific date
# Method 2: Direct file access (local environment)
open ./output/index.html # macOS
start ./output/index.html # Windows
xdg-open ./output/index.html # Linux
# Method 3: Access historical archives
open ./output/html/2025-xx-xx/当日汇总.html
```
**Why two index.html files?**
- `output/index.html`: Docker Volume mounted to host, can be opened locally
- `index.html`: Pushed to repository by GitHub Actions, auto-deployed by GitHub Pages
> 💡 **Tip**: Both files have identical content, choose either one to access.
#### Troubleshooting
```bash
# Check container status
docker inspect trendradar
# View container logs
docker logs --tail 100 trendradar
# Enter container for debugging
docker exec -it trendradar /bin/bash
# Verify config files
docker exec -it trendradar ls -la /app/config/
```
#### MCP Service Deployment (AI Analysis Feature)
If you need to use AI analysis features, you can deploy the standalone MCP service container.
**Architecture Description**:
```mermaid
flowchart TB
subgraph trendradar["trendradar"]
A1[Scheduled News Fetching]
A2[Push Notifications]
end
subgraph trendradar-mcp["trendradar-mcp"]
B1[127.0.0.1:3333]
B2[AI Analysis API]
end
subgraph shared["Shared Volume"]
C1["config/ (ro)"]
C2["output/ (ro)"]
end
trendradar --> shared
trendradar-mcp --> shared
```
**Quick Start**:
Use docker compose to start both news push and MCP services:
```bash
# Clone project (Recommended)
git clone https://github.com/sansan0/TrendRadar.git
cd TrendRadar/docker
docker compose up -d
# Check running status
docker ps | grep trendradar
```
**Start MCP Service Separately**:
```bash
# Linux/Mac
docker run -d --name trendradar-mcp \
-p 127.0.0.1:3333:3333 \
-v $(pwd)/config:/app/config:ro \
-v $(pwd)/output:/app/output:ro \
-e TZ=Asia/Shanghai \
wantcat/trendradar-mcp:latest
# Windows PowerShell
docker run -d --name trendradar-mcp `
-p 127.0.0.1:3333:3333 `
-v ${PWD}/config:/app/config:ro `
-v ${PWD}/output:/app/output:ro `
-e TZ=Asia/Shanghai `
wantcat/trendradar-mcp:latest
```
> ⚠️ **Note**: Ensure `config/` and `output/` folders exist in current directory with config files and news data before running.
**Verify Service**:
```bash
# Check MCP service health
curl http://127.0.0.1:3333/mcp
# View MCP service logs
docker logs -f trendradar-mcp
```
**Configure in AI Clients**:
After MCP service starts, configure based on your client:
**Cherry Studio** (Recommended, GUI config):
- Settings → MCP Server → Add
- Type: `streamableHttp`
- URL: `http://127.0.0.1:3333/mcp`
**Claude Desktop / Cline** (JSON config):
```json
{
"mcpServers": {
"trendradar": {
"url": "http://127.0.0.1:3333/mcp",
"type": "streamableHttp"
}
}
}
```
> 💡 **Tip**: MCP service only listens on local port (127.0.0.1) for security. For remote access, configure reverse proxy and authentication yourself.
</details>
### 7. How is the push content displayed?
<details>
<summary>👉 Click to expand: <strong>Customize Push Style and Content</strong></summary>
<br>
**Configuration Location:** `report` and `display` sections in `config/config.yaml`
```yaml
report:
mode: "daily" # Push mode
display_mode: "keyword" # Display mode (v4.6.0 new)
rank_threshold: 5 # Ranking highlight threshold
sort_by_position_first: false # Sorting priority
max_news_per_keyword: 0 # Maximum display count per keyword
display:
region_order: # Region display order (v5.2.0 new)
- new_items # New trending section
- hotlist # Hotlist section
- rss # RSS subscription section
- standalone # Independent display section
- ai_analysis # AI analysis section
```
#### Configuration Details
| Config Item | Type | Default | Description |
|------------|------|---------|-------------|
| `mode` | string | `daily` | Push mode, options: `daily`/`incremental`/`current`, see [Push Mode Details](#3-push-mode-details) |
| `display_mode` | string | `keyword` | Display mode, options: `keyword`/`platform`, see below |
| `rank_threshold` | int | `5` | Ranking highlight threshold, news with rank ≤ this value will be displayed in bold |
| `sort_by_position_first` | bool | `false` | Sorting priority: `false`=sort by news count, `true`=sort by config position |
| `max_news_per_keyword` | int | `0` | Maximum display count per keyword, `0`=unlimited |
| `display.region_order` | list | See config above | Adjust list order to control region display positions |
#### Display Mode Configuration (v4.6.0 New)
Controls how news is grouped in push messages and HTML reports:
| Mode | Grouping | Title Prefix | Use Case |
|------|----------|--------------|----------|
| `keyword` (default) | Group by keyword | `[Platform]` | Users focusing on specific topics |
| `platform` | Group by platform | `[Keyword]` | Users focusing on specific platforms |
**Example Comparison:**
```
# keyword mode (group by keyword)
📊 Trending Keywords Stats
🔥 [1/3] AI : 12 items
1. [Weibo] OpenAI releases GPT-5 #1-#3 - 08:30 (5 times)
2. [Zhihu] How to view AI replacing programmers #2 - 09:15 (3 times)
# platform mode (group by platform)
📊 Trending News Stats
🔥 [1/4] Weibo : 12 items
1. [AI] OpenAI releases GPT-5 #1-#3 - 08:30 (5 times)
2. [Trump] Trump announces major policy #2 - 09:15 (3 times)
```
#### Region Display Order (region_order)
Control the display position of each section in push messages by adjusting the order of `display.region_order` list.
**Default Order**: New Items → Hotlist → RSS → Standalone → AI Analysis
**Custom Example**: Want AI analysis at the top?
```yaml
display:
region_order:
- ai_analysis # Move to first line
- new_items
- hotlist
- rss
- standalone
```
**Note**: A region will only be displayed when both conditions are met:
1. Listed in `region_order`
2. Corresponding switch in `display.regions` is `true`
#### Region Switches (regions)
Control whether each region is displayed in push notifications via `display.regions`:
```yaml
display:
regions:
hotlist: true # Hotlist region (keyword-matched trending news)
new_items: false # New items region (new hotlist + new RSS items)
rss: true # RSS region (keyword-matched RSS content)
standalone: false # Standalone section (full hotlist/RSS, unfiltered by keywords)
ai_analysis: true # AI analysis region
```
| Region | Config Key | Default | Description |
|--------|-----------|---------|-------------|
| **Hotlist** | `hotlist` | `true` | Keyword-matched trending news aggregation |
| **New Items** | `new_items` | `false` | Newly appeared topics in this crawl cycle (hotlist + RSS). Note: the 🆕 markers in the hotlist region are not affected by this switch |
| **RSS** | `rss` | `true` | Keyword-matched RSS subscription content. When disabled, RSS analysis is skipped, but RSS in standalone section is unaffected |
| **Standalone** | `standalone` | `false` | Full content display for specified platforms/RSS, unfiltered by keywords |
| **AI Analysis** | `ai_analysis` | `true` | AI-generated trending analysis summary |
#### Sorting Priority Configuration
**Example Scenario:** Config order A, B, C, news count A(3), B(10), C(5)
| Config Value | Display Order | Use Case |
|-------------|--------------|----------|
| `false` (default) | B(10) → C(5) → A(3) | Focus on popularity trends |
| `true` | A(3) → B(10) → C(5) | Focus on personal priority |
**Docker Environment Variables:**
```bash
SORT_BY_POSITION_FIRST=true
MAX_NEWS_PER_KEYWORD=10
```
#### Independent Display Section Configuration (v5.0.0 New)
Provides full trending list display for specified platforms, unaffected by `frequency_words.txt` keyword filtering.
**Configuration Location:** `display` section in `config/config.yaml`
```yaml
display:
regions:
standalone: true # Show standalone section in push (disabling doesn't affect AI analysis)
standalone:
platforms: ["zhihu", "weibo"] # Trending platform ID list
rss_feeds: ["hacker-news"] # RSS feed ID list
max_items: 20 # Max display count per source (0=unlimited)
```
> 💡 **Display and AI analysis are independently controlled**: `regions.standalone` only controls whether the standalone section appears in push notifications. Even with display disabled, setting `include_standalone: true` in the AI config still allows AI to analyze full hotlist data from these platforms. Ideal for users who want deeper AI insights without longer push messages.
**Use Cases:**
- Want to view the complete trending ranking of a platform (like Zhihu) instead of just keyword-matched content
- Subscribed to RSS feeds with few updates (like personal blogs) and want full push every time
**Effect Example:**
```
📋 Independent Display Section (Total 15 items)
Zhihu Trending (10 items):
1. [Zhihu] How to view OpenAI releasing Sora?
2. [Zhihu] 2024 postgraduate entrance exam scores released...
...
Hacker News (5 items):
1. [Hacker News] Launch HN: TrendRadar...
...
```
</details>
### 8. When will I receive pushes?
<details>
<summary>👉 Click to expand: <strong>Set Push Time (Scheduling System)</strong></summary>
<br>
**Configuration Location:** `schedule` section in `config/config.yaml` + `config/timeline.yaml`
#### Quick Start
Just pick a preset template in `config.yaml` — no need to edit `timeline.yaml`:
```yaml
schedule:
enabled: true
preset: "morning_evening" # Change this line
```
#### Available Preset Templates
| Template | Description | Push Behavior |
|----------|-------------|---------------|
| `morning_evening` | Incremental + evening summary (recommended) | Push new content all day + 19:00-21:00 daily summary |
| `always_on` | 24/7 monitoring | Push whenever new content appears, no time restrictions |
| `office_hours` | Office hours | Three-phase weekday push (morning briefing → noon update → closing summary), weekends incremental |
| `night_owl` | Night owl | Afternoon peek + late-night daily summary (22:00-01:00 cross-midnight) |
| `custom` | Fully customizable | Edit the `custom` section at the bottom of `timeline.yaml` |
#### Full Customization
If none of the preset templates fit your needs, edit the `custom` section at the bottom of `config/timeline.yaml` to freely define time periods, day plans, and week mappings. See the comments in `timeline.yaml` for details.
#### Important Notice
> ⚠️ **Users upgrading from older versions:**
> - v6.0.0 removed the old `notification.push_window` and `ai_analysis.analysis_window` configs
> - Please switch to the new `schedule` + `timeline.yaml` scheduling system
> - Old "push once per day" can be replaced with the `morning_evening` preset
> - Old "working hours push" can be replaced with the `office_hours` preset
> ⚠️ **GitHub Actions Users Note:**
> - GitHub Actions execution time is unstable, may have ±15 minutes deviation
> - Time period ranges should be at least **2 hours** wide
> - For precise timed push, recommend **Docker deployment** on personal server
</details>
### 9. How often does it run?
<details>
<summary>👉 Click to expand: <strong>Set Auto-Run Frequency</strong></summary>
<br>
**Configuration Location:** `schedule` section in `.github/workflows/crawler.yml`
```yaml
on:
schedule:
- cron: "0 * * * *" # Run every hour
```
#### How to change the schedule?
GitHub Actions uses a time format called "Cron". You don't need to understand it deeply; just copy and replace the code below.
**Configuration Location**: `schedule` section in `.github/workflows/crawler.yml`
| I want... | Copy this line | Note |
|-----------|----------------|------|
| **Every Hour** | `- cron: "0 * * * *"` | **Default**, runs at minute 0 |
| **Every 30 Mins** | `- cron: "*/30 * * * *"` | Runs every 30 minutes |
| **Daily at 8 AM** | `- cron: "0 0 * * *"` | ⚠️ `0` because UTC 0:00 = Beijing 8:00 AM |
| **Work Hours (30m)** | `- cron: "*/30 0-14 * * *"` | Beijing 8:00 - 22:00 |
| **3 Times Daily** | `- cron: "0 0,6,12 * * *"` | Beijing 8:00, 14:00, 20:00 |
#### ⚠️ Two Important Notes
1. **Time Zone**: GitHub servers use **UTC time**.
- **Math**: Your desired Beijing time **minus 8 hours** = value to fill.
- *Example: For Beijing 20:00, fill in 12:00.*
2. **Don't run too often**: Suggest intervals no shorter than 30 minutes.
- GitHub free resources are limited; running too frequently might get flagged.
- Actions have startup delays, so precise timing isn't guaranteed anyway.
#### Step-by-Step Guide
1. In your GitHub repository, find `.github/workflows/crawler.yml`.
2. Click the ✏️ (Edit) button top right.
3. Find the line `cron: "..."` and replace the content inside quotes with the code above.
4. Click the green **Commit changes** button to save.
</details>
### 10. Push to multiple groups/devices
<details>
<summary>👉 Click to expand: <strong>Send to Multiple Recipients</strong></summary>
<br>
**Configuration Location:** `notification` section in `config/config.yaml`
> ### ⚠️ **Security First**
> **DO NOT write passwords/Tokens directly in `config.yaml`!**
> If you upload a file containing passwords to GitHub, the whole world can see it.
>
> **Correct Method**:
> - **GitHub Actions Users**: Add in Settings -> Secrets
> - **Docker Users**: Write in `.env` file (this file won't be uploaded)
#### How to push to multiple places?
Simple, just separate multiple addresses with a semicolon `;`.
**Example**:
Suppose you have two Feishu groups:
- Group 1: `https://.../webhook/aaa`
- Group 2: `https://.../webhook/bbb`
Config value:
`https://.../webhook/aaa;https://.../webhook/bbb`
#### Supported Platforms
| Platform | Method | Note |
|----------|--------|------|
| **Feishu/DingTalk/WeWork** | Separate URLs with `;` | Just chain them up |
| **Bark (iOS)** | Separate URLs with `;` | Push to multiple iPhones |
| **Telegram** | Separate Tokens and ChatIDs with `;` | ⚠️ **Order must match**: <br>Token1 ↔ ChatID1<br>Token2 ↔ ChatID2 |
| **ntfy** | Separate Topics and Tokens with `;` | If a topic needs no token, leave empty:<br>`token1;;token3` (middle is empty) |
#### Common Config Examples (GitHub Secrets / .env)
```bash
# Send to 3 Feishu groups
FEISHU_WEBHOOK_URL=https://hook1...;https://hook2...;https://hook3...
# Send to 2 DingTalk groups
DINGTALK_WEBHOOK_URL=https://oapi...;https://oapi...
# Send to 2 Telegram users (Match one-to-one)
TELEGRAM_BOT_TOKEN=tokenA;tokenB
TELEGRAM_CHAT_ID=userA;userB
```
> **Tip**: Default limit is 3 accounts per platform to prevent abuse. Adjust `MAX_ACCOUNTS_PER_CHANNEL` if needed.
</details>
<br>
### 11. Where is the data saved?
<details id="storage-config">
<summary>👉 Click to expand: <strong>Choose Data Storage Location</strong></summary>
<br>
#### Where is the data saved?
The system automatically selects the best location for you, so you usually don't need to worry about it:
| Your Environment | Data Location | Description |
|------------------|---------------|-------------|
| **Docker / Local** | **Local Disk** | Saved in the `output/` folder within the project directory. |
| **GitHub Actions** | **Cloud Storage** | Since GitHub Actions environments are destroyed after running, cloud storage (e.g., Cloudflare R2) is required. |
#### How to configure cloud storage? (For GitHub Actions Users)
If you run on GitHub Actions, you need a "cloud drive" to save data. For example, Cloudflare R2 (free tier is generous).
**Add these 5 variables to GitHub Secrets:**
| Variable Name | Value |
|---------------|-------|
| `STORAGE_BACKEND` | `remote` |
| `S3_BUCKET_NAME` | Your bucket name |
| `S3_ACCESS_KEY_ID` | Your Access Key |
| `S3_SECRET_ACCESS_KEY` | Your Secret Key |
| `S3_ENDPOINT_URL` | Your R2 endpoint URL |
> 💡 **Tutorial**: How to apply for R2? See [Quick Start - Remote Storage Configuration](#-quick-start)
#### How long is data kept?
By default, we never delete your data. If you want to save space, you can enable "Auto Cleanup".
**Config Location**: `config/config.yaml`
```yaml
storage:
local:
retention_days: 30 # Keep local data for 30 days (0 = forever)
remote:
retention_days: 30 # Keep cloud data for 30 days
```
#### Push time is wrong? (Timezone Settings)
If you are overseas or find the push time doesn't match your local time, you can change the timezone.
**Config Location**: `config/config.yaml`
```yaml
app:
timezone: "Asia/Shanghai" # Default is China Standard Time
```
- Example for Los Angeles: `America/Los_Angeles`
- Example for London: `Europe/London`
</details>
### 12. Let AI help me analyze hot topics
<details id="ai-analysis-config">
<summary>👉 Click to expand: <strong>Enable AI Smart Analysis</strong></summary>
#### What can AI do for me?
After enabling this feature, AI acts as a professional analyst. When pushing a batch of news, it will:
1. **Auto-Read**: Read all matched trending news.
2. **Deep Think**: Analyze connections between seemingly isolated news items.
3. **Write Report**: Append a concise and profound "Insight Report" at the end of the push message.
**Includes**: Trending topic summary, public opinion direction, cross-platform correlation, potential impact assessment, etc.
#### How to enable AI Analysis?
The simplest way is via environment variables (Recommended for GitHub Secrets or .env).
**Required Configurations**:
| Variable Name | Value | Description |
|--------------|-------|-------------|
| `AI_ANALYSIS_ENABLED` | `true` | Enable switch |
| `AI_API_KEY` | `sk-xxxxxx` | Your API Key |
| `AI_MODEL` | `deepseek/deepseek-chat` | Model identifier (format: `provider/model`) |
**Supported AI Providers** (Based on LiteLLM, supports 100+ providers):
| Provider | AI_MODEL Value | Description |
|----------|----------------|-------------|
| **DeepSeek** (Recommended) | `deepseek/deepseek-chat` | Excellent cost-performance ratio for high-frequency analysis |
| **OpenAI** | `openai/gpt-4o`<br>`openai/gpt-4o-mini` | GPT-4o series |
| **Google Gemini** | `gemini/gemini-1.5-flash`<br>`gemini/gemini-1.5-pro` | Gemini series |
| **Custom API** | Any format | Use with `AI_API_BASE` |
> 💡 **New Feature**: Now based on [LiteLLM](https://github.com/BerriAI/litellm) unified interface, supporting 100+ AI providers with simpler configuration and better error handling.
**Optional Configurations**:
| Variable Name | Default | Description |
|--------------|---------|-------------|
| `AI_API_BASE` | (auto) | Custom API endpoint (e.g., OneAPI, local models) |
| `AI_TEMPERATURE` | `1.0` | Sampling temperature (0-2, higher = more random) |
| `AI_MAX_TOKENS` | `5000` | Maximum tokens to generate |
| `AI_TIMEOUT` | `120` | Request timeout (seconds) |
| `AI_NUM_RETRIES` | `2` | Number of retries on failure |
#### Advanced: AI Translation
If you subscribe to foreign RSS feeds (like Hacker News), AI can translate the content into your native language.
**Configuration Location**: `config/config.yaml`
```yaml
ai_translation:
enabled: true # Enable translation
language: "Chinese" # Target language (Chinese, English, Japanese...)
```
#### Advanced: Customize AI "Persona"
Think the AI sounds too official? You can modify its prompt to change its style (e.g., "Sarcastic Commentator", "Senior Investment Advisor").
- **File**: `config/ai_analysis_prompt.txt`
- **Method**: Edit with a text editor, tell AI what analysis style you want.
</details>
<br>
## ✨ AI Analysis
TrendRadar v3.0.0 added **MCP (Model Context Protocol)** based AI analysis feature, allowing natural language conversations with news data for deep analysis.
### ⚠️ Important Notice Before Use
**Critical: AI features require local news data support**
AI analysis **does not** query real-time online data directly, but analyzes **locally accumulated news data** (stored in the `output` folder)
#### Usage Instructions:
1. **Built-in Test Data**: The `output` directory includes one week of trending news data from **December 21-27, 2025** for quick feature testing
2. **Query Limitations**:
- ✅ Only query data within available date range (Dec 21-27, 7 days total)
- ❌ Cannot query real-time news or future dates
3. **Getting Latest Data**:
- Test data is for quick experience only, **recommend deploying the project yourself** to get real-time data
- Follow [Quick Start](#-quick-start) to deploy and run the project
- After accumulating news data for at least 1 day, you can query the latest trending topics
---
### 1. Quick Deployment
Cherry Studio provides GUI config interface, 5-minute quick deployment, complex parts are one-click install.
**Illustrated Deployment Tutorial**: Now updated to my WeChat Official Account (see [Support Project](#-support-project)), reply "mcp" to get
**Detailed Deployment Tutorial**: [README-Cherry-Studio.md](README-Cherry-Studio.md)
**Deployment Mode Description**:
- **STDIO Mode (Recommended)**: One-time configuration, no need to reconfigure later. The **illustrated deployment tutorial** only demonstrates this mode's configuration.
- **HTTP Mode (Alternative)**: If STDIO mode configuration encounters issues, you can use HTTP mode. This mode's configuration is basically the same as STDIO, but only requires copy-pasting one line, less error-prone. The only thing to note is that you need to manually start the service before each use. For details, refer to the HTTP mode section at the bottom of [README-Cherry-Studio.md](README-Cherry-Studio.md).
### 2. Learning to Talk with AI
**Detailed Conversation Tutorial**: [README-MCP-FAQ.md](README-MCP-FAQ.md)
**Question Effect**:
> 💡 **Tip**: Actually not recommended to ask multiple questions at once. If your chosen AI model cannot even sequentially call as shown below, suggest switching models.
<img src="/_image/ai4.png" alt="MCP usage effect" width="600">
<br>
## 🔌 MCP Clients
TrendRadar MCP service supports standard Model Context Protocol (MCP), can connect to various AI clients supporting MCP for smart analysis.
### Supported Clients
**Note**:
- Replace `/path/to/TrendRadar` with your actual project path
- Windows paths use double backslashes: `C:\\Users\\YourName\\TrendRadar`
- Remember to restart after saving
<details>
<summary><b>👉 Click to expand: Cursor</b></summary>
#### Method 1: HTTP Mode
1. **Start HTTP Service**:
```bash
# Windows
start-http.bat
# Mac/Linux
./start-http.sh
```
2. **Configure Cursor**:
**Project Level Config** (Recommended):
Create `.cursor/mcp.json` in project root:
```json
{
"mcpServers": {
"trendradar": {
"url": "http://localhost:3333/mcp",
"description": "TrendRadar News Trending Aggregation Analysis"
}
}
}
```
**Global Config**:
Create `~/.cursor/mcp.json` in user directory (same content)
3. **Usage Steps**:
- Restart Cursor after saving config
- Check connected tools in chat interface "Available Tools"
- Start using: `Search today's "AI" related news`
#### Method 2: STDIO Mode (Recommended)
Create `.cursor/mcp.json`:
```json
{
"mcpServers": {
"trendradar": {
"command": "uv",
"args": [
"--directory",
"/path/to/TrendRadar",
"run",
"python",
"-m",
"mcp_server.server"
]
}
}
}
```
</details>
<details>
<summary><b>👉 Click to expand: VSCode (Cline/Continue)</b></summary>
#### Cline Configuration
Add in Cline's MCP settings:
**HTTP Mode**:
```json
{
"trendradar": {
"url": "http://localhost:3333/mcp",
"type": "streamableHttp",
"autoApprove": [],
"disabled": false
}
}
```
**STDIO Mode** (Recommended):
```json
{
"trendradar": {
"command": "uv",
"args": [
"--directory",
"/path/to/TrendRadar",
"run",
"python",
"-m",
"mcp_server.server"
],
"type": "stdio",
"disabled": false
}
}
```
#### Continue Configuration
Edit `~/.continue/config.json`:
```json
{
"experimental": {
"modelContextProtocolServers": [
{
"transport": {
"type": "stdio",
"command": "uv",
"args": [
"--directory",
"/path/to/TrendRadar",
"run",
"python",
"-m",
"mcp_server.server"
]
}
}
]
}
}
```
**Usage Examples**:
```
Analyze recent 7 days "Tesla" popularity trend
Generate today's trending summary report
Search "Bitcoin" related news and analyze sentiment
```
</details>
<details>
<summary><b>👉 Click to expand: MCP Inspector</b> (Debug Tool)</summary>
<br>
MCP Inspector is the official debug tool for testing MCP connections:
#### Usage Steps
1. **Start TrendRadar HTTP Service**:
```bash
# Windows
start-http.bat
# Mac/Linux
./start-http.sh
```
2. **Start MCP Inspector**:
```bash
npx @modelcontextprotocol/inspector
```
3. **Connect in Browser**:
- Visit: `http://localhost:3333/mcp`
- Test "Ping Server" function to verify connection
- Check "List Tools" returns 14 tools:
- Date Parsing: resolve_date_range
- Basic Query: get_latest_news, get_news_by_date, get_trending_topics
- Smart Search: search_news, search_related_news_history
- Advanced Analysis: analyze_topic_trend, analyze_data_insights, analyze_sentiment, find_similar_news, generate_summary_report
- System Management: get_current_config, get_system_status, trigger_crawl
</details>
<details>
<summary><b>👉 Click to expand: Other MCP-Compatible Clients</b></summary>
<br>
Any client supporting Model Context Protocol can connect to TrendRadar:
#### HTTP Mode
**Service Address**: `http://localhost:3333/mcp`
**Basic Config Template**:
```json
{
"name": "trendradar",
"url": "http://localhost:3333/mcp",
"type": "http",
"description": "News Trending Aggregation Analysis"
}
```
#### STDIO Mode (Recommended)
**Basic Config Template**:
```json
{
"name": "trendradar",
"command": "uv",
"args": [
"--directory",
"/path/to/TrendRadar",
"run",
"python",
"-m",
"mcp_server.server"
],
"type": "stdio"
}
```
**Notes**:
- Replace `/path/to/TrendRadar` with actual project path
- Windows paths use backslash escape: `C:\\Users\\...`
- Ensure project dependencies installed (ran setup script)
</details>
### Common Questions
<details>
<summary><b>👉 Click to expand: Q1: HTTP Service Cannot Start?</b></summary>
gitextract_ws4303_m/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── 01-bug-report.yml │ │ ├── 02-feature-request.yml │ │ ├── 03-ai-and-config.yml │ │ └── config.yml │ └── workflows/ │ ├── clean-crawler.yml │ ├── crawler.yml │ └── docker.yml ├── LICENSE ├── README-Cherry-Studio.md ├── README-EN.md ├── README-MCP-FAQ-EN.md ├── README-MCP-FAQ.md ├── README.md ├── config/ │ ├── ai_analysis_prompt.txt │ ├── ai_filter/ │ │ ├── extract_prompt.txt │ │ ├── prompt.txt │ │ └── update_tags_prompt.txt │ ├── ai_interests.txt │ ├── ai_translation_prompt.txt │ ├── config.yaml │ ├── custom/ │ │ ├── ai/ │ │ │ └── .gitkeep │ │ └── keyword/ │ │ └── .gitkeep │ ├── frequency_words.txt │ └── timeline.yaml ├── docker/ │ ├── Dockerfile │ ├── Dockerfile.mcp │ ├── docker-compose-build.yml │ ├── docker-compose.yml │ ├── entrypoint.sh │ └── manage.py ├── docs/ │ ├── assets/ │ │ ├── script.js │ │ └── style.css │ └── index.html ├── index.html ├── mcp_server/ │ ├── __init__.py │ ├── server.py │ ├── services/ │ │ ├── __init__.py │ │ ├── cache_service.py │ │ ├── data_service.py │ │ └── parser_service.py │ ├── tools/ │ │ ├── __init__.py │ │ ├── analytics.py │ │ ├── article_reader.py │ │ ├── config_mgmt.py │ │ ├── data_query.py │ │ ├── notification.py │ │ ├── search_tools.py │ │ ├── storage_sync.py │ │ └── system.py │ └── utils/ │ ├── __init__.py │ ├── date_parser.py │ ├── errors.py │ └── validators.py ├── pyproject.toml ├── requirements.txt ├── setup-mac.sh ├── setup-windows-en.bat ├── setup-windows.bat ├── start-http.bat ├── start-http.sh ├── trendradar/ │ ├── __init__.py │ ├── __main__.py │ ├── ai/ │ │ ├── __init__.py │ │ ├── analyzer.py │ │ ├── client.py │ │ ├── filter.py │ │ ├── formatter.py │ │ └── translator.py │ ├── context.py │ ├── core/ │ │ ├── __init__.py │ │ ├── analyzer.py │ │ ├── config.py │ │ ├── data.py │ │ ├── frequency.py │ │ ├── loader.py │ │ └── scheduler.py │ ├── crawler/ │ │ ├── __init__.py │ │ ├── fetcher.py │ │ └── rss/ │ │ ├── __init__.py │ │ ├── fetcher.py │ │ └── parser.py │ ├── notification/ │ │ ├── __init__.py │ │ ├── batch.py │ │ ├── dispatcher.py │ │ ├── formatters.py │ │ ├── renderer.py │ │ ├── senders.py │ │ └── splitter.py │ ├── report/ │ │ ├── __init__.py │ │ ├── formatter.py │ │ ├── generator.py │ │ ├── helpers.py │ │ ├── html.py │ │ └── rss_html.py │ ├── storage/ │ │ ├── __init__.py │ │ ├── ai_filter_schema.sql │ │ ├── base.py │ │ ├── local.py │ │ ├── manager.py │ │ ├── remote.py │ │ ├── rss_schema.sql │ │ ├── schema.sql │ │ └── sqlite_mixin.py │ └── utils/ │ ├── __init__.py │ ├── time.py │ └── url.py ├── version ├── version_configs └── version_mcp
SYMBOL INDEX (908 symbols across 54 files)
FILE: docker/manage.py
function _env_bool (line 22) | def _env_bool(name: str, default: bool) -> bool:
function get_timestamp (line 33) | def get_timestamp():
function run_command (line 38) | def run_command(cmd, shell=True, capture_output=True):
function manual_run (line 49) | def manual_run():
function parse_cron_schedule (line 64) | def parse_cron_schedule(cron_expr):
function show_status (line 145) | def show_status():
function show_config (line 291) | def show_config():
function show_files (line 350) | def show_files():
function show_logs (line 418) | def show_logs():
function restart_supercronic (line 444) | def restart_supercronic():
function _read_proc_cmdline (line 468) | def _read_proc_cmdline(pid: int) -> str:
function _is_expected_webserver_process (line 480) | def _is_expected_webserver_process(pid: int) -> bool:
function _is_manual_stop_requested (line 488) | def _is_manual_stop_requested() -> bool:
function _set_manual_stop_marker (line 493) | def _set_manual_stop_marker():
function _clear_manual_stop_marker (line 502) | def _clear_manual_stop_marker():
function _terminate_webserver_process (line 511) | def _terminate_webserver_process(pid: int, require_expected: bool = True...
function _is_webserver_running (line 539) | def _is_webserver_running(pid: int) -> bool:
function _cleanup_stale_pid (line 565) | def _cleanup_stale_pid():
function start_webserver (line 580) | def start_webserver(force: bool = False):
function stop_webserver (line 651) | def stop_webserver():
function webserver_status (line 677) | def webserver_status():
function webserver_autofix (line 707) | def webserver_autofix():
function show_help (line 744) | def show_help():
function main (line 801) | def main():
FILE: docs/assets/script.js
function applyHighlight (line 13) | function applyHighlight(text) {
function updateBackdrop (line 25) | function updateBackdrop(textareaId, backdropId) {
function syncScroll (line 34) | function syncScroll(textareaId, backdropId) {
constant QR_MODAL_DATA (line 47) | const QR_MODAL_DATA = {
function openQrModal (line 68) | function openQrModal(type) {
function closeQrModal (line 82) | function closeQrModal() {
constant MODULE_DEFS (line 89) | const MODULE_DEFS = [
constant INITIAL_YAML (line 106) | const INITIAL_YAML = `# 在此粘贴你的 config.yaml...
constant STORAGE_KEY_CONFIG (line 111) | const STORAGE_KEY_CONFIG = 'trendradar_config_yaml';
constant STORAGE_KEY_FREQUENCY (line 112) | const STORAGE_KEY_FREQUENCY = 'trendradar_frequency_txt';
constant STORAGE_KEY_TIMELINE (line 113) | const STORAGE_KEY_TIMELINE = 'trendradar_timeline_yaml';
constant STORAGE_KEY_CONFIG_TIME (line 114) | const STORAGE_KEY_CONFIG_TIME = 'trendradar_config_time';
constant STORAGE_KEY_FREQUENCY_TIME (line 115) | const STORAGE_KEY_FREQUENCY_TIME = 'trendradar_frequency_time';
constant STORAGE_KEY_TIMELINE_TIME (line 116) | const STORAGE_KEY_TIMELINE_TIME = 'trendradar_timeline_time';
constant REMOTE_CONFIG_URL (line 119) | const REMOTE_CONFIG_URL = 'https://raw.githubusercontent.com/sansan0/Tre...
constant REMOTE_FREQUENCY_URL (line 120) | const REMOTE_FREQUENCY_URL = 'https://raw.githubusercontent.com/sansan0/...
constant REMOTE_TIMELINE_URL (line 121) | const REMOTE_TIMELINE_URL = 'https://raw.githubusercontent.com/sansan0/T...
constant REMOTE_VERSION_URL (line 122) | const REMOTE_VERSION_URL = 'https://raw.githubusercontent.com/sansan0/Tr...
function debounceSaveConfig (line 235) | function debounceSaveConfig() {
function debounceSaveFrequency (line 243) | function debounceSaveFrequency() {
function debounceSaveTimeline (line 251) | function debounceSaveTimeline() {
function initDragAndDrop (line 261) | function initDragAndDrop(editor, type) {
function handleFileDrop (line 318) | function handleFileDrop(e, type) {
function saveConfigToLocalStorage (line 388) | function saveConfigToLocalStorage() {
function saveFrequencyToLocalStorage (line 402) | function saveFrequencyToLocalStorage() {
function saveTimelineToLocalStorage (line 416) | function saveTimelineToLocalStorage() {
function saveAllToLocalStorage (line 430) | function saveAllToLocalStorage() {
function saveToLocalStorage (line 437) | function saveToLocalStorage() {
function formatSaveTime (line 442) | function formatSaveTime(isoString) {
function updateSaveTimeDisplay (line 460) | function updateSaveTimeDisplay() {
function showToast (line 651) | function showToast(message, type = 'info') {
function renderModules (line 688) | function renderModules() {
function renderModuleNav (line 723) | function renderModuleNav() {
function renderControls (line 849) | function renderControls(mod) {
function syncYamlToUI (line 1006) | function syncYamlToUI() {
function updateYamlFromUI (line 1042) | function updateYamlFromUI(modKey, path, el) {
function createToggleControl (line 1176) | function createToggleControl(mod, path, label) {
function createInputControl (line 1189) | function createInputControl(mod, path, label, type = "text") {
function createNumberControl (line 1198) | function createNumberControl(mod, path, label) {
function createSelectControl (line 1207) | function createSelectControl(mod, path, label, options) {
function parseFrequencyText (line 1340) | function parseFrequencyText(text) {
function buildFrequencyText (line 1495) | function buildFrequencyText(data) {
function syncFrequencyToUI (line 1666) | function syncFrequencyToUI() {
function renderFrequencyPanel (line 1672) | function renderFrequencyPanel(data) {
function parsePlatformsFromYaml (line 2388) | function parsePlatformsFromYaml() {
function renderPlatformsList (line 2399) | function renderPlatformsList() {
function reorderPlatforms (line 2448) | function reorderPlatforms(oldIndex, newIndex) {
function updatePlatformsInYaml (line 2456) | function updatePlatformsInYaml(platforms) {
constant DISPLAY_REGIONS_DEF (line 2561) | const DISPLAY_REGIONS_DEF = [
function parseDisplayRegionsFromYaml (line 2570) | function parseDisplayRegionsFromYaml() {
function renderDisplayRegionsList (line 2616) | function renderDisplayRegionsList() {
function updateDisplayRegionsInYaml (line 2677) | function updateDisplayRegionsInYaml(regions) {
function parseRssFeedsFromYaml (line 2814) | function parseRssFeedsFromYaml() {
function renderRssFeedsList (line 2825) | function renderRssFeedsList() {
function updateRssFeedsInYaml (line 2894) | function updateRssFeedsInYaml(feeds) {
function openRssModalWithData (line 3007) | function openRssModalWithData(feed, editIndex) {
function parseStandaloneConfigFromYaml (line 3075) | function parseStandaloneConfigFromYaml() {
function renderStandaloneLists (line 3088) | function renderStandaloneLists() {
function updateStandaloneConfigInYaml (line 3154) | function updateStandaloneConfigInYaml(type, list) {
function extractVersion (line 3212) | function extractVersion(text) {
function compareVersions (line 3222) | function compareVersions(v1, v2) {
function getCurrentTab (line 3292) | function getCurrentTab() {
function showVersionComparisonModal (line 3297) | function showVersionComparisonModal(fileName, currentVersion, latestVers...
constant PRESET_PLATFORMS (line 3406) | const PRESET_PLATFORMS = [
function renderAvailablePlatforms (line 3488) | function renderAvailablePlatforms() {
function toggleRssTips (line 3640) | function toggleRssTips() {
function fillRssUrl (line 3651) | function fillRssUrl(url) {
constant PRESET_META (line 3667) | const PRESET_META = {
constant DAY_NAMES (line 3675) | const DAY_NAMES = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
function getActivePreset (line 3680) | function getActivePreset() {
function parseTimelineData (line 3690) | function parseTimelineData() {
function getPresetConfig (line 3701) | function getPresetConfig(data, presetName) {
function syncTimelineToUI (line 3710) | function syncTimelineToUI() {
function renderWeekView (line 3805) | function renderWeekView(config, presetName) {
function mergeWithDefault (line 3888) | function mergeWithDefault(period, defaultCfg) {
function getBlockColorClass (line 3900) | function getBlockColorClass(merged) {
function computeBlocks (line 3914) | function computeBlocks(startStr, endStr) {
function parseTime (line 3923) | function parseTime(str) {
function escapeAttr (line 3928) | function escapeAttr(s) {
function showTlTooltip (line 3937) | function showTlTooltip(event, name, start, end, push, analyze, mode) {
function hideTlTooltip (line 3968) | function hideTlTooltip() {
function renderPeriodDetails (line 3978) | function renderPeriodDetails(config, presetName) {
function renderBehaviorToggles (line 4158) | function renderBehaviorToggles(cfg, presetName, periodKey, rawCfg = null) {
function updateTimelineField (line 4395) | function updateTimelineField(presetName, periodKey, field, value) {
function resolveTimelineSection (line 4505) | function resolveTimelineSection(lines, presetName) {
function resolveTimelineTarget (line 4556) | function resolveTimelineTarget(lines, presetName, periodKey) {
function applyTimelineEditorChanges (line 4581) | function applyTimelineEditorChanges(editor, lines) {
function removeTimelineField (line 4590) | function removeTimelineField(presetName, periodKey, field) {
function updateTimelineSectionField (line 4637) | function updateTimelineSectionField(presetName, field, value) {
function findChildKey (line 4670) | function findChildKey(lines, start, end, parentIndent, key) {
function findBlockEnd (line 4687) | function findBlockEnd(lines, start, indent, maxEnd) {
function replaceLineValue (line 4700) | function replaceLineValue(lines, idx, value) {
function insertTimelineField (line 4733) | function insertTimelineField(lines, targetStart, targetEnd, targetIndent...
function scrollTimelineEditorToPreset (line 4820) | function scrollTimelineEditorToPreset(presetName) {
function buildEmptyPresetBlock (line 4982) | function buildEmptyPresetBlock(key, name, desc) {
function buildPresetYamlBlock (line 5015) | function buildPresetYamlBlock(key, cfg) {
constant PROTECTED_PRESETS (line 5207) | const PROTECTED_PRESETS = ['morning_evening', 'always_on', 'office_hours...
function findPresetSection (line 5461) | function findPresetSection(lines, presetName) {
function removePeriodFromDayPlans (line 5499) | function removePeriodFromDayPlans(lines, sectionInfo, periodKey) {
function removePeriodFromDayPlanInLines (line 5532) | function removePeriodFromDayPlanInLines(lines, sectionInfo, planKey, per...
function updatePresetMeta (line 5611) | function updatePresetMeta(presetName, field, value) {
function initDayPlanSortable (line 5705) | function initDayPlanSortable(presetName) {
function reorderDayPlanPeriods (line 5732) | function reorderDayPlanPeriods(presetName, planKey, orderedKeys) {
function toggleSupportSidebar (line 5776) | function toggleSupportSidebar() {
FILE: mcp_server/server.py
function _get_tools (line 33) | def _get_tools(project_root: Optional[str] = None):
function get_platforms_resource (line 50) | async def get_platforms_resource() -> str:
function get_rss_feeds_resource (line 67) | async def get_rss_feeds_resource() -> str:
function get_available_dates_resource (line 82) | async def get_available_dates_resource() -> str:
function get_keywords_resource (line 99) | async def get_keywords_resource() -> str:
function resolve_date_range (line 119) | async def resolve_date_range(
function get_latest_news (line 190) | async def get_latest_news(
function get_trending_topics (line 220) | async def get_trending_topics(
function get_latest_rss (line 255) | async def get_latest_rss(
function search_rss (line 288) | async def search_rss(
function get_rss_feeds_status (line 328) | async def get_rss_feeds_status() -> str:
function get_news_by_date (line 351) | async def get_news_by_date(
function analyze_topic_trend (line 388) | async def analyze_topic_trend(
function analyze_data_insights (line 440) | async def analyze_data_insights(
function analyze_sentiment (line 484) | async def analyze_sentiment(
function find_related_news (line 525) | async def find_related_news(
function generate_summary_report (line 565) | async def generate_summary_report(
function aggregate_news (line 592) | async def aggregate_news(
function compare_periods (line 631) | async def compare_periods(
function search_news (line 693) | async def search_news(
function get_current_config (line 753) | async def get_current_config(
function get_system_status (line 776) | async def get_system_status() -> str:
function check_version (line 791) | async def check_version(
function trigger_crawl (line 815) | async def trigger_crawl(
function sync_from_remote (line 846) | async def sync_from_remote(
function get_storage_status (line 887) | async def get_storage_status() -> str:
function list_available_dates (line 902) | async def list_available_dates(
function read_article (line 931) | async def read_article(
function read_articles_batch (line 971) | async def read_articles_batch(
function get_channel_format_guide (line 1013) | async def get_channel_format_guide(channel: Optional[str] = None) -> str:
function get_notification_channels (line 1053) | async def get_notification_channels() -> str:
function send_notification (line 1072) | async def send_notification(
function run_server (line 1117) | def run_server(
FILE: mcp_server/services/cache_service.py
function make_cache_key (line 14) | def make_cache_key(namespace: str, **params) -> str:
class CacheService (line 60) | class CacheService:
method __init__ (line 63) | def __init__(self):
method get (line 69) | def get(self, key: str, ttl: int = 900) -> Optional[Any]:
method set (line 91) | def set(self, key: str, value: Any) -> None:
method delete (line 103) | def delete(self, key: str) -> bool:
method clear (line 120) | def clear(self) -> None:
method cleanup_expired (line 126) | def cleanup_expired(self, ttl: int = 900) -> int:
method get_stats (line 149) | def get_stats(self) -> dict:
function get_cache (line 174) | def get_cache() -> CacheService:
FILE: mcp_server/services/data_service.py
class DataService (line 17) | class DataService:
method __init__ (line 36) | def __init__(self, project_root: str = None):
method get_latest_news (line 46) | def get_latest_news(
method get_news_by_date (line 120) | def get_news_by_date(
method search_news_by_keyword (line 200) | def search_news_by_keyword(
method _extract_words_from_title (line 301) | def _extract_words_from_title(self, title: str, min_length: int = 2) -...
method get_trending_topics (line 330) | def get_trending_topics(
method _get_mode_description (line 448) | def _get_mode_description(self, mode: str, extract_mode: str = "keywor...
method get_current_config (line 462) | def get_current_config(self, section: str = "all") -> Dict:
method get_available_date_range (line 558) | def get_available_date_range(self, db_type: str = "news") -> Tuple[Opt...
method get_system_status (line 575) | def get_system_status(self) -> Dict:
method get_latest_rss (line 624) | def get_latest_rss(
method search_rss (line 715) | def search_rss(
method get_rss_feeds_status (line 798) | def get_rss_feeds_status(self) -> Dict:
FILE: mcp_server/services/parser_service.py
class ParserService (line 20) | class ParserService:
method __init__ (line 23) | def __init__(self, project_root: str = None):
method clean_title (line 43) | def clean_title(title: str) -> str:
method get_date_folder_name (line 49) | def get_date_folder_name(self, date: datetime = None) -> str:
method _get_db_path (line 63) | def _get_db_path(self, date: datetime = None, db_type: str = "news") -...
method _read_from_sqlite (line 82) | def _read_from_sqlite(
method _read_news_from_sqlite (line 124) | def _read_news_from_sqlite(
method _read_rss_from_sqlite (line 225) | def _read_rss_from_sqlite(
method read_all_titles_for_date (line 307) | def read_all_titles_for_date(
method parse_yaml_config (line 348) | def parse_yaml_config(self, config_path: str = None) -> dict:
method parse_frequency_words (line 376) | def parse_frequency_words(self, words_file: str = None) -> List[Dict]:
method get_available_dates (line 425) | def get_available_dates(self, db_type: str = "news") -> List[str]:
method get_available_date_range (line 447) | def get_available_date_range(self, db_type: str = "news") -> Tuple[Opt...
FILE: mcp_server/tools/analytics.py
function _get_weight_config (line 42) | def _get_weight_config() -> Dict:
function calculate_news_weight (line 82) | def calculate_news_weight(news_data: Dict, rank_threshold: int = 5) -> f...
class AnalyticsTools (line 99) | class AnalyticsTools:
method __init__ (line 102) | def __init__(self, project_root: str = None):
method analyze_data_insights_unified (line 111) | def analyze_data_insights_unified(
method analyze_topic_trend_unified (line 178) | def analyze_topic_trend_unified(
method get_topic_trend_analysis (line 266) | def get_topic_trend_analysis(
method compare_platforms (line 425) | def compare_platforms(
method analyze_keyword_cooccurrence (line 549) | def analyze_keyword_cooccurrence(
method analyze_sentiment (line 657) | def analyze_sentiment(
method _create_sentiment_analysis_prompt (line 845) | def _create_sentiment_analysis_prompt(
method find_similar_news (line 937) | def find_similar_news(
method search_by_entity (line 1052) | def search_by_entity(
method generate_summary_report (line 1183) | def generate_summary_report(
method get_platform_activity_stats (line 1363) | def get_platform_activity_stats(
method analyze_topic_lifecycle (line 1490) | def analyze_topic_lifecycle(
method detect_viral_topics (line 1648) | def detect_viral_topics(
method predict_trending_topics (line 1787) | def predict_trending_topics(
method _extract_keywords (line 1956) | def _extract_keywords(self, title: str, min_length: int = 2) -> List[s...
method _calculate_similarity (line 1984) | def _calculate_similarity(self, text1: str, text2: str) -> float:
method _find_unique_topics (line 1998) | def _find_unique_topics(self, platform_stats: Dict) -> Dict[str, List[...
method aggregate_news (line 2033) | def aggregate_news(
method _aggregate_similar_news (line 2174) | def _aggregate_similar_news(
method compare_periods (line 2314) | def compare_periods(
method _parse_period (line 2406) | def _parse_period(self, period: Union[Dict[str, str], str]) -> Optiona...
method _collect_period_data (line 2439) | def _collect_period_data(
method _compare_overview (line 2498) | def _compare_overview(
method _compare_topic_shift (line 2541) | def _compare_topic_shift(
method _compare_platform_activity (line 2594) | def _compare_platform_activity(
FILE: mcp_server/tools/article_reader.py
class ArticleReaderTools (line 24) | class ArticleReaderTools:
method __init__ (line 27) | def __init__(self, project_root: str = None, jina_api_key: str = None):
method _build_headers (line 39) | def _build_headers(self) -> Dict[str, str]:
method _throttle (line 50) | def _throttle(self):
method read_article (line 58) | def read_article(
method read_articles_batch (line 139) | def read_articles_batch(
FILE: mcp_server/tools/config_mgmt.py
class ErrorInfo (line 14) | class ErrorInfo(TypedDict, total=False):
class ConfigResult (line 21) | class ConfigResult(TypedDict):
class ConfigManagementTools (line 29) | class ConfigManagementTools:
method __init__ (line 32) | def __init__(self, project_root: str = None):
method get_current_config (line 41) | def get_current_config(self, section: Optional[str] = None) -> ConfigR...
FILE: mcp_server/tools/data_query.py
class DataQueryTools (line 23) | class DataQueryTools:
method __init__ (line 26) | def __init__(self, project_root: str = None):
method get_latest_news (line 35) | def get_latest_news(
method search_news_by_keyword (line 95) | def search_news_by_keyword(
method get_trending_topics (line 159) | def get_trending_topics(
method get_news_by_date (line 232) | def get_news_by_date(
method get_latest_rss (line 324) | def get_latest_rss(
method search_rss (line 379) | def search_rss(
method get_rss_feeds_status (line 442) | def get_rss_feeds_status(self) -> Dict:
FILE: mcp_server/tools/notification.py
function _split_text_into_batches (line 97) | def _split_text_into_batches(text: str, max_bytes: int) -> List[str]:
function _format_for_channel (line 163) | def _format_for_channel(message: str, channel_id: str) -> str:
function _prepare_batches (line 195) | def _prepare_batches(message: str, channel_id: str, batch_sizes: Dict = ...
function _adapt_markdown_for_feishu (line 483) | def _adapt_markdown_for_feishu(text: str) -> str:
function _adapt_markdown_for_dingtalk (line 498) | def _adapt_markdown_for_dingtalk(text: str) -> str:
function _adapt_markdown_for_wework (line 516) | def _adapt_markdown_for_wework(text: str) -> str:
function _adapt_markdown_for_ntfy (line 535) | def _adapt_markdown_for_ntfy(text: str) -> str:
function _adapt_markdown_for_bark (line 548) | def _adapt_markdown_for_bark(text: str) -> str:
function _markdown_to_telegram_html (line 571) | def _markdown_to_telegram_html(text: str) -> str:
function _convert_markdown_to_slack (line 651) | def _convert_markdown_to_slack(text: str) -> str:
function _markdown_to_simple_html (line 677) | def _markdown_to_simple_html(text: str) -> str:
function _send_feishu (line 724) | def _send_feishu(webhook_url: str, content: str, title: str) -> Dict:
function _send_dingtalk (line 747) | def _send_dingtalk(webhook_url: str, content: str, title: str) -> Dict:
function _send_wework (line 762) | def _send_wework(webhook_url: str, content: str, title: str, msg_type: s...
function _send_telegram (line 778) | def _send_telegram(bot_token: str, chat_id: str, content: str, title: st...
function _send_email (line 796) | def _send_email(
function _send_ntfy (line 856) | def _send_ntfy(server_url: str, topic: str, content: str, title: str, to...
function _send_bark (line 895) | def _send_bark(bark_url: str, content: str, title: str) -> Dict:
function _send_slack (line 921) | def _send_slack(webhook_url: str, content: str, title: str) -> Dict:
function _send_generic_webhook (line 933) | def _send_generic_webhook(
class NotificationTools (line 963) | class NotificationTools:
method __init__ (line 966) | def __init__(self, project_root: str = None):
method _load_merged_config (line 973) | def _load_merged_config(self) -> Dict[str, Any]:
method _detect_config_source (line 991) | def _detect_config_source(self, env_key: str, yaml_value: str) -> str:
method get_channel_format_guide (line 1000) | def get_channel_format_guide(self, channel: Optional[str] = None) -> D...
method get_notification_channels (line 1037) | def get_notification_channels(self) -> Dict:
method send_notification (line 1108) | def send_notification(
method _dispatch_to_channel (line 1205) | def _dispatch_to_channel(
method _get_batch_sizes (line 1280) | def _get_batch_sizes(self) -> Dict:
method _get_batch_interval (line 1303) | def _get_batch_interval(self) -> float:
method _send_batched_multi_account (line 1315) | def _send_batched_multi_account(
method _send_batched_telegram (line 1343) | def _send_batched_telegram(
method _send_batched_ntfy (line 1374) | def _send_batched_ntfy(
FILE: mcp_server/tools/search_tools.py
class SearchTools (line 18) | class SearchTools:
method __init__ (line 21) | def __init__(self, project_root: str = None):
method search_news_unified (line 30) | def search_news_unified(
method _search_by_keyword_mode (line 255) | def _search_by_keyword_mode(
method _search_by_fuzzy_mode (line 304) | def _search_by_fuzzy_mode(
method _search_by_entity_mode (line 356) | def _search_by_entity_mode(
method _calculate_similarity (line 404) | def _calculate_similarity(self, text1: str, text2: str) -> float:
method _fuzzy_match (line 418) | def _fuzzy_match(self, query: str, text: str, threshold: float = 0.3) ...
method _extract_keywords (line 455) | def _extract_keywords(self, text: str, min_length: int = 2) -> List[str]:
method _calculate_keyword_overlap (line 478) | def _calculate_keyword_overlap(self, keywords1: List[str], keywords2: ...
method _jaccard_similarity (line 504) | def _jaccard_similarity(self, list1: List[str], list2: List[str]) -> f...
method search_related_news_history (line 529) | def search_related_news_history(
method find_related_news_unified (line 739) | def find_related_news_unified(
method _search_rss_by_keyword (line 897) | def _search_rss_by_keyword(
FILE: mcp_server/tools/storage_sync.py
class StorageSyncTools (line 19) | class StorageSyncTools:
method __init__ (line 22) | def __init__(self, project_root: str = None):
method _load_config (line 38) | def _load_config(self) -> dict:
method _get_storage_config (line 49) | def _get_storage_config(self) -> dict:
method _get_remote_config (line 54) | def _get_remote_config(self) -> dict:
method _has_remote_config (line 69) | def _has_remote_config(self) -> bool:
method _get_remote_backend (line 79) | def _get_remote_backend(self):
method _get_local_data_dir (line 110) | def _get_local_data_dir(self) -> Path:
method _parse_date_folder_name (line 117) | def _parse_date_folder_name(self, folder_name: str) -> Optional[dateti...
method _get_local_dates (line 151) | def _get_local_dates(self, db_type: str = "news") -> List[str]:
method _get_all_local_dates (line 183) | def _get_all_local_dates(self) -> Dict[str, List[str]]:
method _calculate_dir_size (line 204) | def _calculate_dir_size(self, path: Path) -> int:
method sync_from_remote (line 213) | def sync_from_remote(self, days: int = 7) -> Dict:
method get_storage_status (line 333) | def get_storage_status(self) -> Dict:
method list_available_dates (line 435) | def list_available_dates(self, source: str = "both") -> Dict:
FILE: mcp_server/tools/system.py
class SystemManagementTools (line 15) | class SystemManagementTools:
method __init__ (line 18) | def __init__(self, project_root: str = None):
method get_system_status (line 33) | def get_system_status(self) -> Dict:
method trigger_crawl (line 71) | def trigger_crawl(self, platforms: Optional[List[str]] = None, save_to...
method _generate_simple_html (line 293) | def _generate_simple_html(self, results: Dict, id_to_name: Dict, faile...
method _html_escape (line 371) | def _html_escape(self, text: str) -> str:
method check_version (line 383) | def check_version(self, proxy_url: Optional[str] = None) -> Dict:
FILE: mcp_server/utils/date_parser.py
class DateParser (line 14) | class DateParser:
method parse_date_query (line 92) | def parse_date_query(date_query: str) -> datetime:
method _get_date_by_weekday (line 251) | def _get_date_by_weekday(target_weekday: int, is_last_week: bool) -> d...
method format_date_folder (line 278) | def format_date_folder(date: datetime) -> str:
method validate_date_not_future (line 295) | def validate_date_not_future(date: datetime) -> None:
method validate_date_not_too_old (line 312) | def validate_date_not_too_old(date: datetime, max_days: int = 365) -> ...
method resolve_date_range_expression (line 331) | def resolve_date_range_expression(expression: str) -> Dict:
method _calculate_date_range (line 426) | def _calculate_date_range(
method get_supported_expressions (line 493) | def get_supported_expressions() -> Dict[str, list]:
FILE: mcp_server/utils/errors.py
function _load_supported_platforms (line 15) | def _load_supported_platforms() -> List[str]:
class MCPError (line 28) | class MCPError(Exception):
method __init__ (line 31) | def __init__(self, message: str, code: str = "MCP_ERROR", suggestion: ...
method to_dict (line 37) | def to_dict(self) -> dict:
class DataNotFoundError (line 48) | class DataNotFoundError(MCPError):
method __init__ (line 51) | def __init__(self, message: str, suggestion: Optional[str] = None):
class InvalidParameterError (line 59) | class InvalidParameterError(MCPError):
method __init__ (line 62) | def __init__(self, message: str, suggestion: Optional[str] = None):
class ConfigurationError (line 70) | class ConfigurationError(MCPError):
method __init__ (line 73) | def __init__(self, message: str, suggestion: Optional[str] = None):
class PlatformNotSupportedError (line 81) | class PlatformNotSupportedError(MCPError):
method __init__ (line 84) | def __init__(self, platform: str):
class CrawlTaskError (line 94) | class CrawlTaskError(MCPError):
method __init__ (line 97) | def __init__(self, message: str, suggestion: Optional[str] = None):
class FileParseError (line 105) | class FileParseError(MCPError):
method __init__ (line 108) | def __init__(self, file_path: str, reason: str):
FILE: mcp_server/utils/validators.py
function _parse_string_to_list (line 21) | def _parse_string_to_list(value: str) -> List[str]:
function _parse_string_to_int (line 73) | def _parse_string_to_int(value: str, param_name: str = "参数") -> int:
function _parse_string_to_float (line 105) | def _parse_string_to_float(value: str, param_name: str = "参数") -> float:
function _parse_string_to_bool (line 130) | def _parse_string_to_bool(value: str) -> bool:
function get_supported_platforms (line 157) | def get_supported_platforms() -> List[str]:
function validate_platforms (line 196) | def validate_platforms(platforms: Optional[Union[List[str], str]]) -> Li...
function validate_limit (line 257) | def validate_limit(limit: Optional[Union[int, str]], default: int = 20, ...
function validate_date (line 294) | def validate_date(date_str: str) -> datetime:
function normalize_date_range (line 316) | def normalize_date_range(date_range: Optional[Union[dict, str]]) -> Opti...
function validate_date_range (line 363) | def validate_date_range(date_range: Optional[Union[dict, str]]) -> Optio...
function validate_keyword (line 482) | def validate_keyword(keyword: str) -> str:
function validate_top_n (line 515) | def validate_top_n(top_n: Optional[Union[int, str]], default: int = 10) ...
function validate_mode (line 532) | def validate_mode(mode: Optional[str], valid_modes: List[str], default: ...
function validate_config_section (line 562) | def validate_config_section(section: Optional[str]) -> str:
function validate_threshold (line 579) | def validate_threshold(
function validate_date_query (line 628) | def validate_date_query(
FILE: trendradar/__main__.py
function _parse_version (line 33) | def _parse_version(version_str: str) -> Tuple[int, int, int]:
function _compare_version (line 44) | def _compare_version(local: str, remote: str) -> str:
function _fetch_remote_version (line 57) | def _fetch_remote_version(version_url: str, proxy_url: Optional[str] = N...
function _parse_config_versions (line 78) | def _parse_config_versions(content: str) -> Dict[str, str]:
function check_all_versions (line 95) | def check_all_versions(
class NewsAnalyzer (line 192) | class NewsAnalyzer:
method __init__ (line 217) | def __init__(self, config: Optional[Dict] = None):
method _init_storage_manager (line 246) | def _init_storage_manager(self) -> None:
method _detect_docker_environment (line 261) | def _detect_docker_environment(self) -> bool:
method _should_open_browser (line 274) | def _should_open_browser(self) -> bool:
method _setup_proxy (line 278) | def _setup_proxy(self) -> None:
method _set_update_info_from_config (line 288) | def _set_update_info_from_config(self) -> None:
method _get_mode_strategy (line 306) | def _get_mode_strategy(self) -> Dict:
method _has_notification_configured (line 310) | def _has_notification_configured(self) -> bool:
method _has_valid_content (line 331) | def _has_valid_content(
method _prepare_ai_analysis_data (line 351) | def _prepare_ai_analysis_data(
method _run_ai_analysis (line 457) | def _run_ai_analysis(
method _load_analysis_data (line 585) | def _load_analysis_data(
method _prepare_current_title_info (line 624) | def _prepare_current_title_info(self, results: Dict, time_info: str) -...
method _prepare_standalone_data (line 644) | def _prepare_standalone_data(
method _run_analysis_pipeline (line 794) | def _run_analysis_pipeline(
method _send_notification_if_needed (line 890) | def _send_notification_if_needed(
method _initialize_and_check_config (line 1012) | def _initialize_and_check_config(self) -> None:
method _crawl_data (line 1033) | def _crawl_data(self) -> Tuple[Dict, Dict, List]:
method _crawl_rss_data (line 1070) | def _crawl_rss_data(self) -> Tuple[Optional[List[Dict]], Optional[List...
method _process_rss_data_by_mode (line 1169) | def _process_rss_data_by_mode(self, rss_data) -> Tuple[Optional[List[D...
method _convert_rss_items_to_list (line 1341) | def _convert_rss_items_to_list(self, items_dict: Dict, id_to_name: Dic...
method _filter_rss_by_keywords (line 1413) | def _filter_rss_by_keywords(self, rss_items: List[Dict]) -> List[Dict]:
method _generate_rss_html_report (line 1437) | def _generate_rss_html_report(self, rss_items: list, feeds_info: dict)...
method _execute_mode_strategy (line 1467) | def _execute_mode_strategy(
method run (line 1687) | def run(self) -> None:
function _record_doctor_result (line 1716) | def _record_doctor_result(results: List[Tuple[str, str, str]], status: s...
function _save_doctor_report (line 1728) | def _save_doctor_report(
function _run_doctor (line 1765) | def _run_doctor(config_path: Optional[str] = None) -> bool:
function _build_test_report_data (line 1974) | def _build_test_report_data(ctx: AppContext) -> Dict:
function _create_test_html_file (line 2007) | def _create_test_html_file(ctx: AppContext) -> Optional[str]:
function _run_test_notification (line 2030) | def _run_test_notification(config: Dict) -> bool:
function main (line 2119) | def main():
function _handle_status_commands (line 2215) | def _handle_status_commands(config: Dict) -> None:
FILE: trendradar/ai/analyzer.py
class AIAnalysisResult (line 18) | class AIAnalysisResult:
class AIAnalyzer (line 42) | class AIAnalyzer:
method __init__ (line 45) | def __init__(
method _load_prompt_template (line 86) | def _load_prompt_template(self, prompt_file: str) -> tuple:
method analyze (line 117) | def analyze(
method _prepare_news_content (line 262) | def _prepare_news_content(
method _call_ai (line 381) | def _call_ai(self, user_prompt: str) -> str:
method _retry_fix_json (line 390) | def _retry_fix_json(self, original_response: str, error_msg: str) -> O...
method _format_time_range (line 433) | def _format_time_range(self, first_time: str, last_time: str) -> str:
method _format_rank_timeline (line 460) | def _format_rank_timeline(self, rank_timeline: List[Dict]) -> str:
method _prepare_standalone_content (line 478) | def _prepare_standalone_content(self, standalone_data: Dict) -> str:
method _parse_response (line 560) | def _parse_response(self, response: str) -> AIAnalysisResult:
FILE: trendradar/ai/client.py
class AIClient (line 15) | class AIClient:
method __init__ (line 18) | def __init__(self, config: Dict[str, Any]):
method chat (line 42) | def chat(
method validate_config (line 104) | def validate_config(self) -> tuple[bool, str]:
FILE: trendradar/ai/filter.py
class AIFilterResult (line 20) | class AIFilterResult:
class AIFilter (line 35) | class AIFilter:
method __init__ (line 38) | def __init__(
method _load_prompt (line 62) | def _load_prompt(self, filename: str) -> tuple:
method compute_interests_hash (line 89) | def compute_interests_hash(self, interests_content: str, filename: str...
method load_interests_content (line 101) | def load_interests_content(self, interests_file: Optional[str] = None)...
method extract_tags (line 142) | def extract_tags(self, interests_content: str) -> List[Dict]:
method update_tags (line 204) | def update_tags(self, old_tags: List[Dict], interests_content: str) ->...
method _parse_update_tags_response (line 269) | def _parse_update_tags_response(self, response: str) -> Optional[Dict]:
method _parse_tags_response (line 313) | def _parse_tags_response(self, response: str) -> List[Dict]:
method classify_batch (line 333) | def classify_batch(
method _parse_classify_response (line 408) | def _parse_classify_response(
method _extract_json (line 544) | def _extract_json(self, response: str) -> Optional[str]:
method _print_formatted_json (line 565) | def _print_formatted_json(self, response: str) -> None:
FILE: trendradar/ai/formatter.py
function _escape_html (line 13) | def _escape_html(text: str) -> str:
function _format_list_content (line 18) | def _format_list_content(text: str) -> str:
function _format_standalone_summaries (line 68) | def _format_standalone_summaries(summaries: dict) -> str:
function render_ai_analysis_markdown (line 79) | def render_ai_analysis_markdown(result: AIAnalysisResult) -> str:
function render_ai_analysis_feishu (line 115) | def render_ai_analysis_feishu(result: AIAnalysisResult) -> str:
function render_ai_analysis_dingtalk (line 151) | def render_ai_analysis_dingtalk(result: AIAnalysisResult) -> str:
function render_ai_analysis_html (line 193) | def render_ai_analysis_html(result: AIAnalysisResult) -> str:
function render_ai_analysis_plain (line 279) | def render_ai_analysis_plain(result: AIAnalysisResult) -> str:
function get_ai_analysis_renderer (line 311) | def get_ai_analysis_renderer(channel: str):
function render_ai_analysis_html_rich (line 326) | def render_ai_analysis_html_rich(result: AIAnalysisResult) -> str:
FILE: trendradar/ai/translator.py
class TranslationResult (line 17) | class TranslationResult:
class BatchTranslationResult (line 26) | class BatchTranslationResult:
class AITranslator (line 37) | class AITranslator:
method __init__ (line 40) | def __init__(self, translation_config: Dict[str, Any], ai_config: Dict...
method _load_prompt_template (line 64) | def _load_prompt_template(self, prompt_file: str) -> tuple:
method translate (line 93) | def translate(self, text: str) -> TranslationResult:
method translate_batch (line 138) | def translate_batch(self, texts: List[str]) -> BatchTranslationResult:
method _format_batch_content (line 232) | def _format_batch_content(self, texts: List[str]) -> str:
method _parse_batch_response (line 239) | def _parse_batch_response(self, response: str, expected_count: int) ->...
method _call_ai (line 303) | def _call_ai(self, user_prompt: str) -> str:
FILE: trendradar/context.py
class AppContext (line 46) | class AppContext:
method __init__ (line 68) | def __init__(self, config: Dict[str, Any]):
method timezone (line 82) | def timezone(self) -> str:
method rank_threshold (line 87) | def rank_threshold(self) -> int:
method weight_config (line 92) | def weight_config(self) -> Dict:
method platforms (line 97) | def platforms(self) -> List[Dict]:
method platform_ids (line 102) | def platform_ids(self) -> List[str]:
method rss_config (line 107) | def rss_config(self) -> Dict:
method rss_enabled (line 112) | def rss_enabled(self) -> bool:
method rss_feeds (line 117) | def rss_feeds(self) -> List[Dict]:
method display_mode (line 122) | def display_mode(self) -> str:
method show_new_section (line 127) | def show_new_section(self) -> bool:
method region_order (line 132) | def region_order(self) -> List[str]:
method filter_method (line 138) | def filter_method(self) -> str:
method ai_priority_sort_enabled (line 143) | def ai_priority_sort_enabled(self) -> bool:
method ai_filter_config (line 148) | def ai_filter_config(self) -> Dict:
method ai_filter_enabled (line 153) | def ai_filter_enabled(self) -> bool:
method get_time (line 159) | def get_time(self) -> datetime:
method format_date (line 163) | def format_date(self) -> str:
method format_time (line 167) | def format_time(self) -> str:
method get_time_display (line 171) | def get_time_display(self) -> str:
method convert_time_display (line 176) | def convert_time_display(time_str: str) -> str:
method get_storage_manager (line 182) | def get_storage_manager(self):
method get_output_path (line 210) | def get_output_path(self, subfolder: str, filename: str) -> str:
method read_today_titles (line 218) | def read_today_titles(
method detect_new_titles (line 224) | def detect_new_titles(
method is_first_crawl (line 230) | def is_first_crawl(self) -> bool:
method load_frequency_words (line 236) | def load_frequency_words(
method matches_word_groups (line 242) | def matches_word_groups(
method count_frequency (line 254) | def count_frequency(
method prepare_report (line 287) | def prepare_report(
method generate_html (line 309) | def generate_html(
method render_html (line 342) | def render_html(
method render_feishu (line 371) | def render_feishu(
method render_dingtalk (line 388) | def render_dingtalk(
method split_content (line 404) | def split_content(
method create_notification_dispatcher (line 464) | def create_notification_dispatcher(self) -> NotificationDispatcher:
method create_scheduler (line 480) | def create_scheduler(self) -> Scheduler:
method _with_ordered_priorities (line 502) | def _with_ordered_priorities(tags: List[Dict], start_priority: int = 1...
method run_ai_filter (line 519) | def run_ai_filter(self, interests_file: Optional[str] = None) -> Optio...
method _build_filter_result (line 840) | def _build_filter_result(
method convert_ai_filter_to_report_data (line 921) | def convert_ai_filter_to_report_data(
method cleanup (line 1116) | def cleanup(self):
FILE: trendradar/core/analyzer.py
function calculate_news_weight (line 17) | def calculate_news_weight(
function format_time_display (line 64) | def format_time_display(
function count_word_frequency (line 91) | def count_word_frequency(
function count_rss_frequency (line 492) | def count_rss_frequency(
function convert_keyword_stats_to_platform_stats (line 710) | def convert_keyword_stats_to_platform_stats(
FILE: trendradar/core/config.py
function parse_multi_account_config (line 11) | def parse_multi_account_config(config_value: str, separator: str = ";") ...
function validate_paired_configs (line 40) | def validate_paired_configs(
function limit_accounts (line 97) | def limit_accounts(
function get_account_at_index (line 128) | def get_account_at_index(accounts: List[str], index: int, default: str =...
FILE: trendradar/core/data.py
function read_all_today_titles_from_storage (line 15) | def read_all_today_titles_from_storage(
function read_all_today_titles (line 83) | def read_all_today_titles(
function detect_latest_new_titles_from_storage (line 113) | def detect_latest_new_titles_from_storage(
function detect_latest_new_titles (line 198) | def detect_latest_new_titles(
FILE: trendradar/core/frequency.py
function _parse_word (line 22) | def _parse_word(word: str) -> Dict:
function _word_matches (line 73) | def _word_matches(word_config: Union[str, Dict], title_lower: str) -> bool:
function load_frequency_words (line 96) | def load_frequency_words(
function matches_word_groups (line 246) | def matches_word_groups(
FILE: trendradar/core/loader.py
function _get_env_bool (line 18) | def _get_env_bool(key: str) -> Optional[bool]:
function _get_env_int (line 26) | def _get_env_int(key: str, default: int = 0) -> int:
function _get_env_int_or_none (line 37) | def _get_env_int_or_none(key: str) -> Optional[int]:
function _get_env_str (line 48) | def _get_env_str(key: str, default: str = "") -> str:
function _load_app_config (line 53) | def _load_app_config(config_data: Dict) -> Dict:
function _load_crawler_config (line 66) | def _load_crawler_config(config_data: Dict) -> Dict:
function _load_report_config (line 79) | def _load_report_config(config_data: Dict) -> Dict:
function _load_notification_config (line 96) | def _load_notification_config(config_data: Dict) -> Dict:
function _load_schedule_config (line 115) | def _load_schedule_config(config_data: Dict) -> Dict:
function _load_timeline_data (line 136) | def _load_timeline_data(config_dir: str = "config") -> Dict:
function _load_weight_config (line 173) | def _load_weight_config(config_data: Dict) -> Dict:
function _load_rss_config (line 184) | def _load_rss_config(config_data: Dict) -> Dict:
function _load_display_config (line 223) | def _load_display_config(config_data: Dict) -> Dict:
function _load_ai_config (line 261) | def _load_ai_config(config_data: Dict) -> Dict:
function _load_ai_analysis_config (line 285) | def _load_ai_analysis_config(config_data: Dict) -> Dict:
function _load_ai_translation_config (line 303) | def _load_ai_translation_config(config_data: Dict) -> Dict:
function _load_ai_filter_config (line 323) | def _load_ai_filter_config(config_data: Dict) -> Dict:
function _load_filter_config (line 339) | def _load_filter_config(config_data: Dict) -> Dict:
function _load_storage_config (line 362) | def _load_storage_config(config_data: Dict) -> Dict:
function _load_webhook_config (line 400) | def _load_webhook_config(config_data: Dict) -> Dict:
function _print_notification_sources (line 447) | def _print_notification_sources(config: Dict) -> None:
function load_config (line 529) | def load_config(config_path: Optional[str] = None) -> Dict[str, Any]:
FILE: trendradar/core/scheduler.py
class ResolvedSchedule (line 18) | class ResolvedSchedule:
class Scheduler (line 35) | class Scheduler:
method __init__ (line 48) | def __init__(
method _build_timeline (line 77) | def _build_timeline(
method resolve (line 102) | def resolve(self) -> ResolvedSchedule:
method _find_active_period (line 190) | def _find_active_period(
method _in_range (line 236) | def _in_range(now_hhmm: str, start: str, end: str) -> bool:
method _merge_with_default (line 255) | def _merge_with_default(self, period_key: Optional[str]) -> Dict[str, ...
method _resolve_ai_mode (line 277) | def _resolve_ai_mode(cfg: Dict[str, Any]) -> str:
method already_executed (line 284) | def already_executed(self, period_key: str, action: str, date_str: str...
method record_execution (line 298) | def record_execution(self, period_key: str, action: str, date_str: str...
method _validate_timeline (line 313) | def _validate_timeline(self, timeline: Dict[str, Any]) -> None:
method _check_period_overlaps (line 363) | def _check_period_overlaps(self, timeline: Dict[str, Any]) -> None:
method _ranges_overlap (line 398) | def _ranges_overlap(s1: str, e1: str, s2: str, e2: str) -> bool:
method _validate_hhmm (line 425) | def _validate_hhmm(value: str, field_name: str) -> None:
FILE: trendradar/crawler/fetcher.py
class DataFetcher (line 20) | class DataFetcher:
method __init__ (line 35) | def __init__(
method fetch_data (line 50) | def fetch_data(
method crawl_websites (line 117) | def crawl_websites(
FILE: trendradar/crawler/rss/fetcher.py
class RSSFeedConfig (line 21) | class RSSFeedConfig:
class RSSFetcher (line 31) | class RSSFetcher:
method __init__ (line 34) | def __init__(
method _create_session (line 70) | def _create_session(self) -> requests.Session:
method _filter_by_freshness (line 87) | def _filter_by_freshness(
method fetch_feed (line 129) | def fetch_feed(self, feed: RSSFeedConfig) -> Tuple[List[RSSItem], Opti...
method fetch_all (line 195) | def fetch_all(self) -> RSSData:
method from_config (line 241) | def from_config(cls, config: Dict) -> "RSSFetcher":
FILE: trendradar/crawler/rss/parser.py
class ParsedRSSItem (line 25) | class ParsedRSSItem:
class RSSParser (line 35) | class RSSParser:
method __init__ (line 38) | def __init__(self, max_summary_length: int = 500):
method parse (line 50) | def parse(self, content: str, feed_url: str = "") -> List[ParsedRSSItem]:
method _is_json_feed (line 79) | def _is_json_feed(self, content: str) -> bool:
method _parse_json_feed (line 96) | def _parse_json_feed(self, content: str, feed_url: str = "") -> List[P...
method _parse_json_feed_item (line 126) | def _parse_json_feed_item(self, item_data: Dict[str, Any]) -> Optional...
method _parse_iso_date (line 180) | def _parse_iso_date(self, date_str: str) -> Optional[str]:
method parse_url (line 196) | def parse_url(self, url: str, timeout: int = 10) -> List[ParsedRSSItem]:
method _parse_entry (line 216) | def _parse_entry(self, entry: Any) -> Optional[ParsedRSSItem]:
method _clean_text (line 247) | def _clean_text(self, text: str) -> str:
method _parse_date (line 263) | def _parse_date(self, entry: Any) -> Optional[str]:
method _parse_summary (line 293) | def _parse_summary(self, entry: Any) -> Optional[str]:
method _parse_author (line 314) | def _parse_author(self, entry: Any) -> Optional[str]:
FILE: trendradar/notification/batch.py
function get_batch_header (line 11) | def get_batch_header(format_type: str, batch_num: int, total_batches: in...
function get_max_batch_header_size (line 34) | def get_max_batch_header_size(format_type: str) -> int:
function truncate_to_bytes (line 50) | def truncate_to_bytes(text: str, max_bytes: int) -> str:
function add_batch_headers (line 78) | def add_batch_headers(
FILE: trendradar/notification/dispatcher.py
class NotificationDispatcher (line 46) | class NotificationDispatcher:
method __init__ (line 54) | def __init__(
method _translate_content (line 76) | def _translate_content(
method dispatch_all (line 216) | def dispatch_all(
method _send_to_multi_accounts (line 323) | def _send_to_multi_accounts(
method _send_feishu (line 357) | def _send_feishu(
method _send_dingtalk (line 398) | def _send_dingtalk(
method _send_wework (line 438) | def _send_wework(
method _send_telegram (line 479) | def _send_telegram(
method _send_ntfy (line 542) | def _send_ntfy(
method _send_bark (line 604) | def _send_bark(
method _send_slack (line 644) | def _send_slack(
method _send_generic_webhook (line 684) | def _send_generic_webhook(
method _send_email (line 746) | def _send_email(
method dispatch_rss (line 769) | def dispatch_rss(
method _send_rss_feishu (line 854) | def _send_rss_feishu(
method _send_rss_dingtalk (line 913) | def _send_rss_dingtalk(
method _send_rss_markdown (line 964) | def _send_rss_markdown(
method _send_rss_wework (line 996) | def _send_rss_wework(self, content: str, proxy_url: Optional[str]) -> ...
method _send_rss_telegram (line 1032) | def _send_rss_telegram(self, content: str, proxy_url: Optional[str]) -...
method _send_rss_ntfy (line 1076) | def _send_rss_ntfy(self, content: str, proxy_url: Optional[str]) -> bool:
method _send_rss_bark (line 1121) | def _send_rss_bark(self, content: str, proxy_url: Optional[str]) -> bool:
method _send_rss_slack (line 1157) | def _send_rss_slack(self, content: str, proxy_url: Optional[str]) -> b...
FILE: trendradar/notification/formatters.py
function strip_markdown (line 11) | def strip_markdown(text: str) -> str:
function convert_markdown_to_mrkdwn (line 72) | def convert_markdown_to_mrkdwn(content: str) -> str:
FILE: trendradar/notification/renderer.py
function render_feishu_content (line 18) | def render_feishu_content(
function render_dingtalk_content (line 150) | def render_dingtalk_content(
function render_rss_feishu_content (line 287) | def render_rss_feishu_content(
function render_rss_dingtalk_content (line 360) | def render_rss_dingtalk_content(
function render_rss_markdown_content (line 427) | def render_rss_markdown_content(
function _render_rss_section_feishu (line 488) | def _render_rss_section_feishu(rss_items: list, separator: str = "---") ...
function _render_rss_section_markdown (line 531) | def _render_rss_section_markdown(rss_items: list) -> str:
FILE: trendradar/notification/senders.py
function _render_ai_analysis (line 36) | def _render_ai_analysis(ai_analysis: Any, channel: str) -> str:
function send_to_feishu (line 77) | def send_to_feishu(
function send_to_dingtalk (line 209) | def send_to_dingtalk(
function send_to_wework (line 337) | def send_to_wework(
function send_to_telegram (line 476) | def send_to_telegram(
function send_to_email (line 603) | def send_to_email(
function send_to_ntfy (line 762) | def send_to_ntfy(
function send_to_bark (line 965) | def send_to_bark(
function send_to_slack (line 1139) | def send_to_slack(
function send_to_generic_webhook (line 1257) | def send_to_generic_webhook(
FILE: trendradar/notification/splitter.py
function split_content_into_batches (line 28) | def split_content_into_batches(
function _process_rss_stats_section (line 827) | def _process_rss_stats_section(
function _process_rss_new_titles_section (line 1050) | def _process_rss_new_titles_section(
function _format_rss_item_line (line 1236) | def _format_rss_item_line(
function _process_standalone_section (line 1290) | def _process_standalone_section(
function _format_standalone_platform_item (line 1503) | def _format_standalone_platform_item(item: Dict, index: int, format_type...
function _format_standalone_rss_item (line 1608) | def _format_standalone_rss_item(
FILE: trendradar/report/formatter.py
function format_title_for_platform (line 13) | def format_title_for_platform(
FILE: trendradar/report/generator.py
function prepare_report_data (line 14) | def prepare_report_data(
function generate_html_report (line 142) | def generate_html_report(
FILE: trendradar/report/helpers.py
function clean_title (line 12) | def clean_title(title: str) -> str:
function html_escape (line 34) | def html_escape(text: str) -> str:
function format_rank_display (line 62) | def format_rank_display(ranks: List[int], rank_threshold: int, format_ty...
FILE: trendradar/report/html.py
function render_html_content (line 16) | def render_html_content(
FILE: trendradar/report/rss_html.py
function render_rss_html_content (line 14) | def render_rss_html_content(
FILE: trendradar/storage/ai_filter_schema.sql
type ai_filter_tags (line 10) | CREATE TABLE IF NOT EXISTS ai_filter_tags (
type ai_filter_results (line 28) | CREATE TABLE IF NOT EXISTS ai_filter_results (
type ai_filter_analyzed_news (line 45) | CREATE TABLE IF NOT EXISTS ai_filter_analyzed_news (
type idx_ai_filter_tags_status (line 58) | CREATE INDEX IF NOT EXISTS idx_ai_filter_tags_status ON ai_filter_tags(s...
type idx_ai_filter_tags_version (line 59) | CREATE INDEX IF NOT EXISTS idx_ai_filter_tags_version ON ai_filter_tags(...
type idx_ai_filter_tags_file (line 60) | CREATE INDEX IF NOT EXISTS idx_ai_filter_tags_file ON ai_filter_tags(int...
type idx_ai_filter_tags_priority (line 61) | CREATE INDEX IF NOT EXISTS idx_ai_filter_tags_priority ON ai_filter_tags...
type idx_ai_filter_results_status (line 62) | CREATE INDEX IF NOT EXISTS idx_ai_filter_results_status ON ai_filter_res...
type idx_ai_filter_results_news (line 63) | CREATE INDEX IF NOT EXISTS idx_ai_filter_results_news ON ai_filter_resul...
type idx_ai_filter_results_tag (line 64) | CREATE INDEX IF NOT EXISTS idx_ai_filter_results_tag ON ai_filter_result...
type idx_analyzed_news_lookup (line 65) | CREATE INDEX IF NOT EXISTS idx_analyzed_news_lookup ON ai_filter_analyze...
type idx_analyzed_news_hash (line 66) | CREATE INDEX IF NOT EXISTS idx_analyzed_news_hash ON ai_filter_analyzed_...
FILE: trendradar/storage/base.py
class NewsItem (line 14) | class NewsItem:
method to_dict (line 34) | def to_dict(self) -> Dict[str, Any]:
method from_dict (line 52) | def from_dict(cls, data: Dict[str, Any]) -> "NewsItem":
class RSSItem (line 71) | class RSSItem:
method to_dict (line 88) | def to_dict(self) -> Dict[str, Any]:
method from_dict (line 105) | def from_dict(cls, data: Dict[str, Any]) -> "RSSItem":
class RSSData (line 123) | class RSSData:
method to_dict (line 141) | def to_dict(self) -> Dict[str, Any]:
method from_dict (line 156) | def from_dict(cls, data: Dict[str, Any]) -> "RSSData":
method get_total_count (line 171) | def get_total_count(self) -> int:
class NewsData (line 177) | class NewsData:
method to_dict (line 195) | def to_dict(self) -> Dict[str, Any]:
method from_dict (line 210) | def from_dict(cls, data: Dict[str, Any]) -> "NewsData":
method get_total_count (line 225) | def get_total_count(self) -> int:
method merge_with (line 229) | def merge_with(self, other: "NewsData") -> "NewsData":
class StorageBackend (line 298) | class StorageBackend(ABC):
method save_news_data (line 310) | def save_news_data(self, data: NewsData) -> bool:
method get_today_all_data (line 323) | def get_today_all_data(self, date: Optional[str] = None) -> Optional[N...
method get_latest_crawl_data (line 336) | def get_latest_crawl_data(self, date: Optional[str] = None) -> Optiona...
method detect_new_titles (line 349) | def detect_new_titles(self, current_data: NewsData) -> Dict[str, Dict]:
method save_txt_snapshot (line 362) | def save_txt_snapshot(self, data: NewsData) -> Optional[str]:
method save_html_report (line 375) | def save_html_report(self, html_content: str, filename: str) -> Option...
method is_first_crawl_today (line 389) | def is_first_crawl_today(self, date: Optional[str] = None) -> bool:
method cleanup (line 402) | def cleanup(self) -> None:
method cleanup_old_data (line 409) | def cleanup_old_data(self, retention_days: int) -> int:
method backend_name (line 423) | def backend_name(self) -> str:
method supports_txt (line 431) | def supports_txt(self) -> bool:
method has_period_executed (line 439) | def has_period_executed(self, date_str: str, period_key: str, action: ...
method record_period_execution (line 453) | def record_period_execution(self, date_str: str, period_key: str, acti...
method begin_batch (line 469) | def begin_batch(self) -> None:
method end_batch (line 473) | def end_batch(self) -> None:
method get_active_ai_filter_tags (line 477) | def get_active_ai_filter_tags(self, date: Optional[str] = None, intere...
method get_latest_prompt_hash (line 480) | def get_latest_prompt_hash(self, date: Optional[str] = None, interests...
method get_latest_ai_filter_tag_version (line 483) | def get_latest_ai_filter_tag_version(self, date: Optional[str] = None)...
method deprecate_all_ai_filter_tags (line 486) | def deprecate_all_ai_filter_tags(self, date: Optional[str] = None, int...
method save_ai_filter_tags (line 489) | def save_ai_filter_tags(self, tags: List[Dict], version: int, prompt_h...
method save_ai_filter_results (line 492) | def save_ai_filter_results(self, results: List[Dict], date: Optional[s...
method get_active_ai_filter_results (line 495) | def get_active_ai_filter_results(self, date: Optional[str] = None, int...
method deprecate_specific_ai_filter_tags (line 498) | def deprecate_specific_ai_filter_tags(self, tag_ids: List[int], date: ...
method update_ai_filter_tags_hash (line 501) | def update_ai_filter_tags_hash(self, interests_file: str, new_hash: st...
method update_ai_filter_tag_descriptions (line 504) | def update_ai_filter_tag_descriptions(self, tag_updates: List[Dict], d...
method update_ai_filter_tag_priorities (line 507) | def update_ai_filter_tag_priorities(self, tag_priorities: List[Dict], ...
method save_analyzed_news (line 510) | def save_analyzed_news(self, news_ids: List[str], source_type: str, in...
method get_analyzed_news_ids (line 513) | def get_analyzed_news_ids(self, source_type: str = "hotlist", date: Op...
method clear_analyzed_news (line 516) | def clear_analyzed_news(self, date: Optional[str] = None, interests_fi...
method clear_unmatched_analyzed_news (line 519) | def clear_unmatched_analyzed_news(self, date: Optional[str] = None, in...
method get_all_news_ids (line 522) | def get_all_news_ids(self, date: Optional[str] = None) -> List[Dict]:
method get_all_rss_ids (line 525) | def get_all_rss_ids(self, date: Optional[str] = None) -> List[Dict]:
function convert_crawl_results_to_news_data (line 529) | def convert_crawl_results_to_news_data(
FILE: trendradar/storage/local.py
class LocalStorageBackend (line 26) | class LocalStorageBackend(SQLiteStorageMixin, StorageBackend):
method __init__ (line 36) | def __init__(
method backend_name (line 59) | def backend_name(self) -> str:
method supports_txt (line 63) | def supports_txt(self) -> bool:
method _get_configured_time (line 70) | def _get_configured_time(self) -> datetime:
method _format_date_folder (line 74) | def _format_date_folder(self, date: Optional[str] = None) -> str:
method _format_time_filename (line 78) | def _format_time_filename(self) -> str:
method _get_db_path (line 82) | def _get_db_path(self, date: Optional[str] = None, db_type: str = "new...
method _get_connection (line 102) | def _get_connection(self, date: Optional[str] = None, db_type: str = "...
method save_news_data (line 127) | def save_news_data(self, data: NewsData) -> bool:
method get_today_all_data (line 150) | def get_today_all_data(self, date: Optional[str] = None) -> Optional[N...
method get_latest_crawl_data (line 157) | def get_latest_crawl_data(self, date: Optional[str] = None) -> Optiona...
method detect_new_titles (line 164) | def detect_new_titles(self, current_data: NewsData) -> Dict[str, Dict]:
method is_first_crawl_today (line 168) | def is_first_crawl_today(self, date: Optional[str] = None) -> bool:
method get_crawl_times (line 175) | def get_crawl_times(self, date: Optional[str] = None) -> List[str]:
method has_period_executed (line 186) | def has_period_executed(self, date_str: str, period_key: str, action: ...
method record_period_execution (line 190) | def record_period_execution(self, date_str: str, period_key: str, acti...
method save_rss_data (line 202) | def save_rss_data(self, data: RSSData) -> bool:
method get_rss_data (line 215) | def get_rss_data(self, date: Optional[str] = None) -> Optional[RSSData]:
method detect_new_rss_items (line 219) | def detect_new_rss_items(self, current_data: RSSData) -> Dict[str, Lis...
method get_latest_rss_data (line 223) | def get_latest_rss_data(self, date: Optional[str] = None) -> Optional[...
method get_active_ai_filter_tags (line 234) | def get_active_ai_filter_tags(self, date=None, interests_file="ai_inte...
method get_latest_prompt_hash (line 237) | def get_latest_prompt_hash(self, date=None, interests_file="ai_interes...
method get_latest_ai_filter_tag_version (line 240) | def get_latest_ai_filter_tag_version(self, date=None):
method deprecate_all_ai_filter_tags (line 243) | def deprecate_all_ai_filter_tags(self, date=None, interests_file="ai_i...
method save_ai_filter_tags (line 246) | def save_ai_filter_tags(self, tags, version, prompt_hash, date=None, i...
method save_ai_filter_results (line 249) | def save_ai_filter_results(self, results, date=None):
method get_active_ai_filter_results (line 252) | def get_active_ai_filter_results(self, date=None, interests_file="ai_i...
method deprecate_specific_ai_filter_tags (line 255) | def deprecate_specific_ai_filter_tags(self, tag_ids, date=None):
method update_ai_filter_tags_hash (line 258) | def update_ai_filter_tags_hash(self, interests_file, new_hash, date=No...
method update_ai_filter_tag_descriptions (line 261) | def update_ai_filter_tag_descriptions(self, tag_updates, date=None, in...
method update_ai_filter_tag_priorities (line 264) | def update_ai_filter_tag_priorities(self, tag_priorities, date=None, i...
method save_analyzed_news (line 267) | def save_analyzed_news(self, news_ids, source_type, interests_file, pr...
method get_analyzed_news_ids (line 270) | def get_analyzed_news_ids(self, source_type="hotlist", date=None, inte...
method clear_analyzed_news (line 273) | def clear_analyzed_news(self, date=None, interests_file="ai_interests....
method clear_unmatched_analyzed_news (line 276) | def clear_unmatched_analyzed_news(self, date=None, interests_file="ai_...
method get_all_news_ids (line 279) | def get_all_news_ids(self, date=None):
method get_all_rss_ids (line 282) | def get_all_rss_ids(self, date=None):
method save_txt_snapshot (line 289) | def save_txt_snapshot(self, data: NewsData) -> Optional[str]:
method save_html_report (line 347) | def save_html_report(self, html_content: str, filename: str) -> Option...
method cleanup (line 384) | def cleanup(self) -> None:
method cleanup_old_data (line 395) | def cleanup_old_data(self, retention_days: int) -> int:
method __del__ (line 492) | def __del__(self):
FILE: trendradar/storage/manager.py
class StorageManager (line 19) | class StorageManager:
method __init__ (line 30) | def __init__(
method is_github_actions (line 73) | def is_github_actions() -> bool:
method is_docker (line 78) | def is_docker() -> bool:
method _resolve_backend_type (line 94) | def _resolve_backend_type(self) -> str:
method _has_remote_config (line 108) | def _has_remote_config(self) -> bool:
method _create_remote_backend (line 127) | def _create_remote_backend(self) -> Optional[StorageBackend]:
method get_backend (line 150) | def get_backend(self) -> StorageBackend:
method pull_from_remote (line 176) | def pull_from_remote(self) -> int:
method save_news_data (line 201) | def save_news_data(self, data: NewsData) -> bool:
method save_rss_data (line 205) | def save_rss_data(self, data: RSSData) -> bool:
method get_rss_data (line 209) | def get_rss_data(self, date: Optional[str] = None) -> Optional[RSSData]:
method get_latest_rss_data (line 213) | def get_latest_rss_data(self, date: Optional[str] = None) -> Optional[...
method detect_new_rss_items (line 217) | def detect_new_rss_items(self, current_data: RSSData) -> dict:
method get_today_all_data (line 221) | def get_today_all_data(self, date: Optional[str] = None) -> Optional[N...
method get_latest_crawl_data (line 225) | def get_latest_crawl_data(self, date: Optional[str] = None) -> Optiona...
method detect_new_titles (line 229) | def detect_new_titles(self, current_data: NewsData) -> dict:
method save_txt_snapshot (line 233) | def save_txt_snapshot(self, data: NewsData) -> Optional[str]:
method save_html_report (line 237) | def save_html_report(self, html_content: str, filename: str) -> Option...
method is_first_crawl_today (line 241) | def is_first_crawl_today(self, date: Optional[str] = None) -> bool:
method cleanup (line 245) | def cleanup(self) -> None:
method cleanup_old_data (line 252) | def cleanup_old_data(self) -> int:
method backend_name (line 275) | def backend_name(self) -> str:
method supports_txt (line 280) | def supports_txt(self) -> bool:
method has_period_executed (line 284) | def has_period_executed(self, date_str: str, period_key: str, action: ...
method record_period_execution (line 288) | def record_period_execution(self, date_str: str, period_key: str, acti...
method begin_batch (line 294) | def begin_batch(self):
method end_batch (line 298) | def end_batch(self):
method get_active_ai_filter_tags (line 302) | def get_active_ai_filter_tags(self, date=None, interests_file="ai_inte...
method get_latest_prompt_hash (line 306) | def get_latest_prompt_hash(self, date=None, interests_file="ai_interes...
method get_latest_ai_filter_tag_version (line 310) | def get_latest_ai_filter_tag_version(self, date=None):
method deprecate_all_ai_filter_tags (line 314) | def deprecate_all_ai_filter_tags(self, date=None, interests_file="ai_i...
method save_ai_filter_tags (line 318) | def save_ai_filter_tags(self, tags, version, prompt_hash, date=None, i...
method save_ai_filter_results (line 322) | def save_ai_filter_results(self, results, date=None):
method get_active_ai_filter_results (line 326) | def get_active_ai_filter_results(self, date=None, interests_file="ai_i...
method deprecate_specific_ai_filter_tags (line 330) | def deprecate_specific_ai_filter_tags(self, tag_ids, date=None):
method update_ai_filter_tags_hash (line 334) | def update_ai_filter_tags_hash(self, interests_file, new_hash, date=No...
method update_ai_filter_tag_descriptions (line 338) | def update_ai_filter_tag_descriptions(self, tag_updates, date=None, in...
method update_ai_filter_tag_priorities (line 342) | def update_ai_filter_tag_priorities(self, tag_priorities, date=None, i...
method save_analyzed_news (line 346) | def save_analyzed_news(self, news_ids, source_type, interests_file, pr...
method get_analyzed_news_ids (line 350) | def get_analyzed_news_ids(self, source_type="hotlist", date=None, inte...
method clear_analyzed_news (line 354) | def clear_analyzed_news(self, date=None, interests_file="ai_interests....
method clear_unmatched_analyzed_news (line 358) | def clear_unmatched_analyzed_news(self, date=None, interests_file="ai_...
method get_all_news_ids (line 362) | def get_all_news_ids(self, date=None):
method get_all_rss_ids (line 366) | def get_all_rss_ids(self, date=None):
function get_storage_manager (line 372) | def get_storage_manager(
FILE: trendradar/storage/remote.py
class RemoteStorageBackend (line 41) | class RemoteStorageBackend(SQLiteStorageMixin, StorageBackend):
method __init__ (line 54) | def __init__(
method backend_name (line 129) | def backend_name(self) -> str:
method supports_txt (line 133) | def supports_txt(self) -> bool:
method _get_configured_time (line 140) | def _get_configured_time(self) -> datetime:
method _format_date_folder (line 144) | def _format_date_folder(self, date: Optional[str] = None) -> str:
method _format_time_filename (line 148) | def _format_time_filename(self) -> str:
method _get_remote_db_key (line 152) | def _get_remote_db_key(self, date: Optional[str] = None, db_type: str ...
method _get_local_db_path (line 166) | def _get_local_db_path(self, date: Optional[str] = None, db_type: str ...
method _check_object_exists (line 182) | def _check_object_exists(self, r2_key: str) -> bool:
method _download_sqlite (line 207) | def _download_sqlite(self, date: Optional[str] = None, db_type: str = ...
method begin_batch (line 255) | def begin_batch(self):
method end_batch (line 260) | def end_batch(self):
method _upload_sqlite (line 267) | def _upload_sqlite(self, date: Optional[str] = None, db_type: str = "n...
method _get_connection (line 323) | def _get_connection(self, date: Optional[str] = None, db_type: str = "...
method save_news_data (line 356) | def save_news_data(self, data: NewsData) -> bool:
method get_today_all_data (line 402) | def get_today_all_data(self, date: Optional[str] = None) -> Optional[N...
method get_latest_crawl_data (line 406) | def get_latest_crawl_data(self, date: Optional[str] = None) -> Optiona...
method detect_new_titles (line 410) | def detect_new_titles(self, current_data: NewsData) -> Dict[str, Dict]:
method is_first_crawl_today (line 414) | def is_first_crawl_today(self, date: Optional[str] = None) -> bool:
method has_period_executed (line 422) | def has_period_executed(self, date_str: str, period_key: str, action: ...
method record_period_execution (line 426) | def record_period_execution(self, date_str: str, period_key: str, acti...
method save_rss_data (line 448) | def save_rss_data(self, data: RSSData) -> bool:
method get_rss_data (line 473) | def get_rss_data(self, date: Optional[str] = None) -> Optional[RSSData]:
method detect_new_rss_items (line 477) | def detect_new_rss_items(self, current_data: RSSData) -> Dict[str, Lis...
method get_latest_rss_data (line 481) | def get_latest_rss_data(self, date: Optional[str] = None) -> Optional[...
method get_active_ai_filter_tags (line 489) | def get_active_ai_filter_tags(self, date=None, interests_file="ai_inte...
method get_latest_prompt_hash (line 492) | def get_latest_prompt_hash(self, date=None, interests_file="ai_interes...
method get_latest_ai_filter_tag_version (line 495) | def get_latest_ai_filter_tag_version(self, date=None):
method deprecate_all_ai_filter_tags (line 498) | def deprecate_all_ai_filter_tags(self, date=None, interests_file="ai_i...
method save_ai_filter_tags (line 504) | def save_ai_filter_tags(self, tags, version, prompt_hash, date=None, i...
method save_ai_filter_results (line 510) | def save_ai_filter_results(self, results, date=None):
method get_active_ai_filter_results (line 516) | def get_active_ai_filter_results(self, date=None, interests_file="ai_i...
method deprecate_specific_ai_filter_tags (line 519) | def deprecate_specific_ai_filter_tags(self, tag_ids, date=None):
method update_ai_filter_tags_hash (line 525) | def update_ai_filter_tags_hash(self, interests_file, new_hash, date=No...
method update_ai_filter_tag_descriptions (line 531) | def update_ai_filter_tag_descriptions(self, tag_updates, date=None, in...
method update_ai_filter_tag_priorities (line 537) | def update_ai_filter_tag_priorities(self, tag_priorities, date=None, i...
method save_analyzed_news (line 543) | def save_analyzed_news(self, news_ids, source_type, interests_file, pr...
method get_analyzed_news_ids (line 549) | def get_analyzed_news_ids(self, source_type="hotlist", date=None, inte...
method clear_analyzed_news (line 552) | def clear_analyzed_news(self, date=None, interests_file="ai_interests....
method clear_unmatched_analyzed_news (line 558) | def clear_unmatched_analyzed_news(self, date=None, interests_file="ai_...
method get_all_news_ids (line 564) | def get_all_news_ids(self, date=None):
method get_all_rss_ids (line 567) | def get_all_rss_ids(self, date=None):
method save_txt_snapshot (line 574) | def save_txt_snapshot(self, data: NewsData) -> Optional[str]:
method save_html_report (line 620) | def save_html_report(self, html_content: str, filename: str) -> Option...
method cleanup (line 646) | def cleanup(self) -> None:
method cleanup_old_data (line 680) | def cleanup_old_data(self, retention_days: int) -> int:
method __del__ (line 758) | def __del__(self):
method pull_recent_days (line 773) | def pull_recent_days(self, days: int, local_data_dir: str = "output") ...
method list_remote_dates (line 831) | def list_remote_dates(self) -> List[str]:
FILE: trendradar/storage/rss_schema.sql
type rss_feeds (line 8) | CREATE TABLE IF NOT EXISTS rss_feeds (
type rss_items (line 24) | CREATE TABLE IF NOT EXISTS rss_items (
type rss_crawl_records (line 44) | CREATE TABLE IF NOT EXISTS rss_crawl_records (
type rss_crawl_status (line 55) | CREATE TABLE IF NOT EXISTS rss_crawl_status (
type rss_push_records (line 70) | CREATE TABLE IF NOT EXISTS rss_push_records (
type idx_rss_feed (line 86) | CREATE INDEX IF NOT EXISTS idx_rss_feed ON rss_items(feed_id)
type idx_rss_published (line 89) | CREATE INDEX IF NOT EXISTS idx_rss_published ON rss_items(published_at D...
type idx_rss_crawl_time (line 92) | CREATE INDEX IF NOT EXISTS idx_rss_crawl_time ON rss_items(last_crawl_time)
type idx_rss_title (line 95) | CREATE INDEX IF NOT EXISTS idx_rss_title ON rss_items(title)
type idx_rss_url_feed (line 98) | CREATE UNIQUE INDEX IF NOT EXISTS idx_rss_url_feed
type idx_rss_crawl_status_record (line 102) | CREATE INDEX IF NOT EXISTS idx_rss_crawl_status_record ON rss_crawl_stat...
FILE: trendradar/storage/schema.sql
type platforms (line 7) | CREATE TABLE IF NOT EXISTS platforms (
type news_items (line 18) | CREATE TABLE IF NOT EXISTS news_items (
type title_changes (line 37) | CREATE TABLE IF NOT EXISTS title_changes (
type rank_history (line 50) | CREATE TABLE IF NOT EXISTS rank_history (
type crawl_records (line 63) | CREATE TABLE IF NOT EXISTS crawl_records (
type crawl_source_status (line 74) | CREATE TABLE IF NOT EXISTS crawl_source_status (
type period_executions (line 88) | CREATE TABLE IF NOT EXISTS period_executions (
type idx_news_platform (line 102) | CREATE INDEX IF NOT EXISTS idx_news_platform ON news_items(platform_id)
type idx_news_crawl_time (line 105) | CREATE INDEX IF NOT EXISTS idx_news_crawl_time ON news_items(last_crawl_...
type idx_news_title (line 108) | CREATE INDEX IF NOT EXISTS idx_news_title ON news_items(title)
type idx_news_url_platform (line 111) | CREATE UNIQUE INDEX IF NOT EXISTS idx_news_url_platform
type idx_crawl_status_record (line 115) | CREATE INDEX IF NOT EXISTS idx_crawl_status_record ON crawl_source_statu...
type idx_rank_history_news (line 118) | CREATE INDEX IF NOT EXISTS idx_rank_history_news ON rank_history(news_it...
type idx_period_exec_lookup (line 121) | CREATE INDEX IF NOT EXISTS idx_period_exec_lookup
FILE: trendradar/storage/sqlite_mixin.py
class SQLiteStorageMixin (line 18) | class SQLiteStorageMixin:
method _get_connection (line 34) | def _get_connection(self, date: Optional[str] = None, db_type: str = "...
method _get_configured_time (line 39) | def _get_configured_time(self) -> datetime:
method _format_date_folder (line 44) | def _format_date_folder(self, date: Optional[str] = None) -> str:
method _format_time_filename (line 49) | def _format_time_filename(self) -> str:
method _get_schema_path (line 57) | def _get_schema_path(self, db_type: str = "news") -> Path:
method _get_ai_filter_schema_path (line 71) | def _get_ai_filter_schema_path(self) -> Path:
method _init_tables (line 75) | def _init_tables(self, conn: sqlite3.Connection, db_type: str = "news"...
method _save_news_data_impl (line 105) | def _save_news_data_impl(self, data: NewsData, log_prefix: str = "[存储]...
method _get_today_all_data_impl (line 324) | def _get_today_all_data_impl(self, date: Optional[str] = None) -> Opti...
method _get_latest_crawl_data_impl (line 457) | def _get_latest_crawl_data_impl(self, date: Optional[str] = None) -> O...
method _detect_new_titles_impl (line 589) | def _detect_new_titles_impl(self, current_data: NewsData) -> Dict[str,...
method _is_first_crawl_today_impl (line 648) | def _is_first_crawl_today_impl(self, date: Optional[str] = None) -> bool:
method _get_crawl_times_impl (line 676) | def _get_crawl_times_impl(self, date: Optional[str] = None) -> List[str]:
method _has_period_executed_impl (line 706) | def _has_period_executed_impl(self, date_str: str, period_key: str, ac...
method _record_period_execution_impl (line 741) | def _record_period_execution_impl(self, date_str: str, period_key: str...
method _save_rss_data_impl (line 787) | def _save_rss_data_impl(self, data: RSSData, log_prefix: str = "[存储]")...
method _get_rss_data_impl (line 931) | def _get_rss_data_impl(self, date: Optional[str] = None) -> Optional[R...
method _detect_new_rss_items_impl (line 1016) | def _detect_new_rss_items_impl(self, current_data: RSSData) -> Dict[st...
method _get_latest_rss_data_impl (line 1073) | def _get_latest_rss_data_impl(self, date: Optional[str] = None) -> Opt...
method _get_active_tags_impl (line 1168) | def _get_active_tags_impl(self, date: Optional[str] = None, interests_...
method _get_latest_prompt_hash_impl (line 1192) | def _get_latest_prompt_hash_impl(self, date: Optional[str] = None, int...
method _get_latest_tag_version_impl (line 1210) | def _get_latest_tag_version_impl(self, date: Optional[str] = None) -> ...
method _deprecate_all_tags_impl (line 1225) | def _deprecate_all_tags_impl(self, date: Optional[str] = None, interes...
method _save_tags_impl (line 1266) | def _save_tags_impl(
method _deprecate_specific_tags_impl (line 1304) | def _deprecate_specific_tags_impl(
method _update_tags_hash_impl (line 1336) | def _update_tags_hash_impl(
method _update_tag_descriptions_impl (line 1361) | def _update_tag_descriptions_impl(
method _update_tag_priorities_impl (line 1389) | def _update_tag_priorities_impl(
method _save_analyzed_news_impl (line 1425) | def _save_analyzed_news_impl(
method _get_analyzed_news_ids_impl (line 1457) | def _get_analyzed_news_ids_impl(
method _clear_analyzed_news_impl (line 1476) | def _clear_analyzed_news_impl(
method _clear_unmatched_analyzed_news_impl (line 1496) | def _clear_unmatched_analyzed_news_impl(
method _save_filter_results_impl (line 1520) | def _save_filter_results_impl(
method _get_active_filter_results_impl (line 1553) | def _get_active_filter_results_impl(self, date: Optional[str] = None, ...
method _get_all_news_ids_impl (line 1672) | def _get_all_news_ids_impl(self, date: Optional[str] = None) -> List[D...
method _get_all_rss_ids_impl (line 1696) | def _get_all_rss_ids_impl(self, date: Optional[str] = None) -> List[Di...
FILE: trendradar/utils/time.py
function get_configured_time (line 17) | def get_configured_time(timezone: str = DEFAULT_TIMEZONE) -> datetime:
function format_date_folder (line 35) | def format_date_folder(
function format_time_filename (line 53) | def format_time_filename(timezone: str = DEFAULT_TIMEZONE) -> str:
function get_current_time_display (line 68) | def get_current_time_display(timezone: str = DEFAULT_TIMEZONE) -> str:
function convert_time_for_display (line 81) | def convert_time_for_display(time_str: str) -> str:
function format_iso_time_friendly (line 96) | def format_iso_time_friendly(
function is_within_days (line 175) | def is_within_days(
function calculate_days_old (line 242) | def calculate_days_old(iso_time: str, timezone: str = DEFAULT_TIMEZONE) ...
class TimeWindowChecker (line 289) | class TimeWindowChecker:
method __init__ (line 299) | def __init__(
method is_in_time_range (line 317) | def is_in_time_range(self, start_time: str, end_time: str) -> bool:
method _normalize_time (line 353) | def _normalize_time(self, time_str: str) -> str:
method check_window (line 371) | def check_window(
method get_status (line 414) | def get_status(self, window_config: dict, check_once_per_day_func=None...
FILE: trendradar/utils/url.py
function normalize_url (line 38) | def normalize_url(url: str, platform_id: str = "") -> str:
function get_url_signature (line 131) | def get_url_signature(url: str, platform_id: str = "") -> str:
Condensed preview — 110 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,942K chars).
[
{
"path": ".dockerignore",
"chars": 235,
"preview": ".git/\n.gitignore\n*.md\nREADME.md\n\noutput/\n\n__pycache__/\n*.pyc\n*.pyo\n*.pyd\n.Python\n*.so\n.pytest_cache/\n\n.vscode/\n.idea/\n*."
},
{
"path": ".github/ISSUE_TEMPLATE/01-bug-report.yml",
"chars": 2976,
"preview": "# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json\n\nname: 🐛 遇到问题了\ndescription: 程序运行不正常"
},
{
"path": ".github/ISSUE_TEMPLATE/02-feature-request.yml",
"chars": 863,
"preview": "# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json\n\nname: 💡 我有个想法\ndescription: 建议新功能、推"
},
{
"path": ".github/ISSUE_TEMPLATE/03-ai-and-config.yml",
"chars": 2103,
"preview": "# yaml-language-server: $schema=https://json.schemastore.org/github-issue-forms.json\n\nname: ✨ AI 提示词分享与配置求助\ndescription:"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 114,
"preview": "# yaml-language-server: $schema=https://json.schemastore.org/github-issue-config.json\n\nblank_issues_enabled: false"
},
{
"path": ".github/workflows/clean-crawler.yml",
"chars": 775,
"preview": "name: Check In\n\n# ✅ 签到续期:运行此 workflow 可重置 7 天计时,保持 \"Get Hot News\" 正常运行\n# ✅ Renewal: Run this workflow to reset the 7-day"
},
{
"path": ".github/workflows/crawler.yml",
"chars": 6273,
"preview": "name: Get Hot News\n\non:\n schedule:\n # ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n "
},
{
"path": ".github/workflows/docker.yml",
"chars": 3887,
"preview": "name: Build and Push Docker Images\n\non:\n push:\n tags:\n - \"v*\" # 主项目版本\n - \"mcp-v*\" # MCP 版本\n workflow_disp"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README-Cherry-Studio.md",
"chars": 2032,
"preview": "# TrendRadar × Cherry Studio 部署指南 🍒\n\n> **适合人群**:零编程基础的用户\n> **客户端**:Cherry Studio(免费开源 GUI 客户端)\n\n---\n\n## 📥 第一步:下载 Cherry "
},
{
"path": "README-EN.md",
"chars": 147431,
"preview": "<div align=\"center\" id=\"trendradar\">\n\n<a href=\"https://github.com/sansan0/TrendRadar\" title=\"TrendRadar\">\n <img src=\"/_"
},
{
"path": "README-MCP-FAQ-EN.md",
"chars": 29317,
"preview": "<div align=\"center\">\n\n**[中文](README-MCP-FAQ.md)** | **English**\n\n</div>\n\n# TrendRadar MCP Tool Usage Q&A\n\n> AI Query Gui"
},
{
"path": "README-MCP-FAQ.md",
"chars": 14415,
"preview": "<div align=\"center\">\n\n**中文** | **[English](README-MCP-FAQ-EN.md)**\n\n</div>\n\n# TrendRadar MCP 工具使用问答\n\n> AI 提问指南 - 如何通过自然对"
},
{
"path": "README.md",
"chars": 92025,
"preview": "<div align=\"center\" id=\"trendradar\">\n\n<a href=\"https://github.com/sansan0/TrendRadar\" title=\"TrendRadar\">\n <img src=\"/_"
},
{
"path": "config/ai_analysis_prompt.txt",
"chars": 5251,
"preview": "# ═══════════════════════════════════════════════════════════════\n# TrendRadar AI 分析提示词配置\n# "
},
{
"path": "config/ai_filter/extract_prompt.txt",
"chars": 391,
"preview": "[system]\n你是一个兴趣标签提取专家。你的任务是从用户的兴趣描述中提取出结构化的新闻分类标签。\n\n提取规则:\n1. 每个标签简洁(2-6个字),同时配一句描述说明该标签涵盖哪些话题和关键词\n2. 标签之间尽量不重叠\n3. 标签数量控制"
},
{
"path": "config/ai_filter/prompt.txt",
"chars": 498,
"preview": "[system]\n你是一个高效的新闻分类专家。根据给定的标签列表,快速判断每条新闻标题最适合哪个标签。\n\n分类规则:\n1. 每条新闻只归入一个最相关的标签(选相关度最高的那个)\n2. 不匹配任何标签的新闻不要输出(不要返回空 tags)\n3"
},
{
"path": "config/ai_filter/update_tags_prompt.txt",
"chars": 763,
"preview": "[system]\n你是一个标签管理专家。用户修改了兴趣描述后,你需要对比旧标签集和新的兴趣描述,给出标签更新方案。\n\n核心原则:\n1. 语义等价的标签视为同一个标签(如\"AI/大模型\"和\"AI与大模型\"是同一个标签),优先保留旧标签名\n2."
},
{
"path": "config/ai_interests.txt",
"chars": 1419,
"preview": "# ═══════════════════════════════════════════════════════════════\n# TrendRadar AI 兴趣描述文件\n# "
},
{
"path": "config/ai_translation_prompt.txt",
"chars": 883,
"preview": "# ═══════════════════════════════════════════════════════════════\n# TrendRadar AI 翻译提示词配置\n# "
},
{
"path": "config/config.yaml",
"chars": 19133,
"preview": "# ═══════════════════════════════════════════════════════════════\n# TrendRadar 配置文件\n# "
},
{
"path": "config/custom/ai/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "config/custom/keyword/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "config/frequency_words.txt",
"chars": 5390,
"preview": "# ═══════════════════════════════════════════════════════════════\n# TrendRadar 频率词配置文件\n# "
},
{
"path": "config/timeline.yaml",
"chars": 17383,
"preview": "# ═══════════════════════════════════════════════════════════════\n# TrendRadar 时间线配置\n# "
},
{
"path": "docker/Dockerfile",
"chars": 2470,
"preview": "FROM python:3.12-slim-bookworm\n\nWORKDIR /app\n\n# Latest releases available at https://github.com/aptible/supercronic/rele"
},
{
"path": "docker/Dockerfile.mcp",
"chars": 575,
"preview": "FROM python:3.12-slim-bookworm\n\nWORKDIR /app\n\n# 安装依赖\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requireme"
},
{
"path": "docker/docker-compose-build.yml",
"chars": 2423,
"preview": "services:\n trendradar:\n build:\n context: ..\n dockerfile: docker/Dockerfile\n container_name: trendradar\n"
},
{
"path": "docker/docker-compose.yml",
"chars": 2367,
"preview": "services:\n trendradar:\n image: wantcat/trendradar:latest\n container_name: trendradar\n restart: unless-stopped\n"
},
{
"path": "docker/entrypoint.sh",
"chars": 1990,
"preview": "#!/bin/bash\nset -e\n\n# 检查配置文件\nif [ ! -f \"/app/config/config.yaml\" ] || [ ! -f \"/app/config/frequency_words.txt\" ]; then\n "
},
{
"path": "docker/manage.py",
"chars": 25234,
"preview": "#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\n\"\"\"\n新闻爬虫容器管理工具 - supercronic\n\"\"\"\n\nimport os\nimport sys\nimport subprocess\n"
},
{
"path": "docs/assets/script.js",
"chars": 221975,
"preview": "/**\n * TrendRadar 配置文件编辑器核心逻辑\n * 特点:确保原始 YAML 的注释和格式 100% 保留\n */\n\n// ==========================================\n// 0. 注释"
},
{
"path": "docs/assets/style.css",
"chars": 25397,
"preview": "/* 编辑器区域滚动条 */\n#yaml-editor::-webkit-scrollbar,\n#frequency-editor::-webkit-scrollbar,\n#timeline-editor::-webkit-scrollba"
},
{
"path": "docs/index.html",
"chars": 35421,
"preview": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-wi"
},
{
"path": "index.html",
"chars": 26298,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-"
},
{
"path": "mcp_server/__init__.py",
"chars": 82,
"preview": "\"\"\"\nTrendRadar MCP Server\n\n提供基于MCP协议的新闻聚合数据查询和系统管理接口。\n\n\"\"\"\n\n__version__ = \"4.0.0\"\n"
},
{
"path": "mcp_server/server.py",
"chars": 34951,
"preview": "\"\"\"\nTrendRadar MCP Server - FastMCP 2.0 实现\n\n使用 FastMCP 2.0 提供生产级 MCP 工具服务器。\n支持 stdio 和 HTTP 两种传输模式。\n\"\"\"\n\nimport asyncio\n"
},
{
"path": "mcp_server/services/__init__.py",
"chars": 34,
"preview": "\"\"\"\n服务层模块\n\n提供数据访问、缓存、解析等核心服务。\n\"\"\"\n"
},
{
"path": "mcp_server/services/cache_service.py",
"chars": 4309,
"preview": "\"\"\"\n缓存服务\n\n实现TTL缓存机制,提升数据访问性能。\n\"\"\"\n\nimport hashlib\nimport json\nimport time\nfrom typing import Any, Optional\nfrom threadin"
},
{
"path": "mcp_server/services/data_service.py",
"chars": 26856,
"preview": "\"\"\"\n数据访问服务\n\n提供统一的数据查询接口,封装数据访问逻辑。\n\"\"\"\n\nimport re\nfrom collections import Counter\nfrom datetime import datetime, timedelt"
},
{
"path": "mcp_server/services/parser_service.py",
"chars": 14200,
"preview": "\"\"\"\n数据解析服务\n\nv2.0.0: 仅支持 SQLite 数据库,移除 TXT 文件支持\n新存储结构:output/{type}/{date}.db\n\"\"\"\n\nimport re\nimport sqlite3\nfrom pathlib "
},
{
"path": "mcp_server/tools/__init__.py",
"chars": 32,
"preview": "\"\"\"\nMCP 工具模块\n\n包含所有MCP工具的实现。\n\"\"\"\n"
},
{
"path": "mcp_server/tools/analytics.py",
"chars": 89673,
"preview": "\"\"\"\n高级数据分析工具\n\n提供热度趋势分析、平台对比、关键词共现、情感分析等高级分析功能。\n\"\"\"\n\nimport os\nimport re\nfrom collections import Counter, defaultdict\nfro"
},
{
"path": "mcp_server/tools/article_reader.py",
"chars": 5885,
"preview": "\"\"\"\n文章内容读取工具\n\n通过 Jina AI Reader API 将 URL 转换为 LLM 友好的 Markdown 格式。\n支持单篇和批量读取,内置速率限制和并发控制。\n\n\"\"\"\n\nimport time\nfrom typing "
},
{
"path": "mcp_server/tools/config_mgmt.py",
"chars": 2008,
"preview": "\"\"\"\n配置管理工具\n\n实现配置查询和管理功能。\n\"\"\"\n\nfrom typing import Dict, Optional, Any, TypedDict\n\nfrom ..services.data_service import Dat"
},
{
"path": "mcp_server/tools/data_query.py",
"chars": 13080,
"preview": "\"\"\"\n数据查询工具\n\n实现P0核心的数据查询工具。\n\"\"\"\n\nfrom typing import Dict, List, Optional, Union\n\nfrom ..services.data_service import Data"
},
{
"path": "mcp_server/tools/notification.py",
"chars": 46661,
"preview": "# coding=utf-8\n\"\"\"\n通知推送工具\n\n支持向已配置的通知渠道发送消息,自动检测 config.yaml 和 .env 中的渠道配置。\n接受 markdown 格式内容,内部按各渠道要求自动转换格式后发送。\n\"\"\"\n\nimpo"
},
{
"path": "mcp_server/tools/search_tools.py",
"chars": 34415,
"preview": "\"\"\"\n智能新闻检索工具\n\n提供模糊搜索、链接查询、历史相关新闻检索等高级搜索功能。\n\"\"\"\n\nimport re\nfrom collections import Counter\nfrom datetime import datetime,"
},
{
"path": "mcp_server/tools/storage_sync.py",
"chars": 18694,
"preview": "# coding=utf-8\n\"\"\"\n存储同步工具\n\n实现从远程存储拉取数据到本地、获取存储状态、列出可用日期等功能。\n\"\"\"\n\nimport os\nimport re\nfrom pathlib import Path\nfrom datet"
},
{
"path": "mcp_server/tools/system.py",
"chars": 20161,
"preview": "\"\"\"\n系统管理工具\n\n实现系统状态查询和爬虫触发功能。\n\"\"\"\n\nfrom pathlib import Path\nfrom typing import Dict, List, Optional\n\nfrom ..services.data"
},
{
"path": "mcp_server/utils/__init__.py",
"chars": 33,
"preview": "\"\"\"\n工具类模块\n\n提供参数验证、错误处理等辅助功能。\n\"\"\"\n"
},
{
"path": "mcp_server/utils/date_parser.py",
"chars": 16136,
"preview": "\"\"\"\n日期解析工具\n\n支持多种自然语言日期格式解析,包括相对日期和绝对日期。\n\"\"\"\n\nimport re\nfrom datetime import datetime, timedelta\nfrom typing import Tuple"
},
{
"path": "mcp_server/utils/errors.py",
"chars": 2953,
"preview": "\"\"\"\n自定义错误类\n\n定义MCP Server使用的所有自定义异常类型。\n\"\"\"\n\nfrom typing import Optional, List, Callable\n\n\n# ==================== 延迟加载支持的平"
},
{
"path": "mcp_server/utils/validators.py",
"chars": 17121,
"preview": "\"\"\"\n参数验证工具\n\n提供统一的参数验证功能。\n支持 MCP 客户端将参数序列化为字符串的情况。\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import List, Optional, "
},
{
"path": "pyproject.toml",
"chars": 715,
"preview": "[project]\nname = \"trendradar\"\nversion = \"6.5.0\"\ndescription = \"TrendRadar - 热点新闻聚合与分析工具\"\nrequires-python = \">=3.10\"\ndepe"
},
{
"path": "requirements.txt",
"chars": 198,
"preview": "requests>=2.32.5,<3.0.0\npytz>=2025.2,<2026.0\nPyYAML>=6.0.3,<7.0.0\nfastmcp>=2.12.0,<2.14.0\nwebsockets>=13.0,<14.0\nboto3>="
},
{
"path": "setup-mac.sh",
"chars": 2745,
"preview": "#!/bin/bash\n\n# 颜色定义\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nBLUE='\\033[0;34m'\nBOLD='\\033[1m'\nNC='\\033[0m"
},
{
"path": "setup-windows-en.bat",
"chars": 4630,
"preview": "@echo off\nsetlocal enabledelayedexpansion\n\necho ==========================================\necho TrendRadar MCP Setup ("
},
{
"path": "setup-windows.bat",
"chars": 3824,
"preview": "@echo off\nchcp 65001 >nul\nsetlocal enabledelayedexpansion\n\necho ==========================================\necho TrendR"
},
{
"path": "start-http.bat",
"chars": 556,
"preview": "@echo off\nchcp 65001 >nul\n\necho ============================================================\necho TrendRadar MCP Serve"
},
{
"path": "start-http.sh",
"chars": 471,
"preview": "#!/bin/bash\n\necho \"╔════════════════════════════════════════╗\"\necho \"║ TrendRadar MCP Server (HTTP 模式) ║\"\necho \"╚═══"
},
{
"path": "trendradar/__init__.py",
"chars": 236,
"preview": "# coding=utf-8\n\"\"\"\nTrendRadar - 热点新闻聚合与分析工具\n\n使用方式:\n python -m trendradar # 模块执行\n trendradar # "
},
{
"path": "trendradar/__main__.py",
"chars": 83405,
"preview": "# coding=utf-8\n\"\"\"\nTrendRadar 主程序\n\n热点新闻聚合与分析工具\n支持: python -m trendradar\n\"\"\"\n\nimport argparse\nimport copy\nimport json\nimp"
},
{
"path": "trendradar/ai/__init__.py",
"chars": 935,
"preview": "# coding=utf-8\n\"\"\"\nTrendRadar AI 模块\n\n提供 AI 大模型对热点新闻的深度分析和翻译功能\n\"\"\"\n\nfrom .analyzer import AIAnalyzer, AIAnalysisResult\nfr"
},
{
"path": "trendradar/ai/analyzer.py",
"chars": 22652,
"preview": "# coding=utf-8\n\"\"\"\nAI 分析器模块\n\n调用 AI 大模型对热点新闻进行深度分析\n基于 LiteLLM 统一接口,支持 100+ AI 提供商\n\"\"\"\n\nimport json\nfrom dataclasses impor"
},
{
"path": "trendradar/ai/client.py",
"chars": 3459,
"preview": "# coding=utf-8\n\"\"\"\nAI 客户端模块\n\n基于 LiteLLM 的统一 AI 模型接口\n支持 100+ AI 提供商(OpenAI、DeepSeek、Gemini、Claude、国内模型等)\n\"\"\"\n\nimport os\nf"
},
{
"path": "trendradar/ai/filter.py",
"chars": 20233,
"preview": "# coding=utf-8\n\"\"\"\nAI 智能筛选模块\n\n通过 AI 对新闻进行标签分类:\n1. 阶段 A:从用户兴趣描述中提取结构化标签\n2. 阶段 B:对新闻标题按标签进行批量分类\n\"\"\"\n\nimport hashlib\nimport"
},
{
"path": "trendradar/ai/formatter.py",
"chars": 13114,
"preview": "# coding=utf-8\n\"\"\"\nAI 分析结果格式化模块\n\n将 AI 分析结果格式化为各推送渠道的样式\n\"\"\"\n\nimport html as html_lib\nimport re\nfrom .analyzer import AIAn"
},
{
"path": "trendradar/ai/translator.py",
"chars": 9808,
"preview": "# coding=utf-8\n\"\"\"\nAI 翻译器模块\n\n对推送内容进行多语言翻译\n基于 LiteLLM 统一接口,支持 100+ AI 提供商\n\"\"\"\n\nfrom dataclasses import dataclass, field\nf"
},
{
"path": "trendradar/context.py",
"chars": 44096,
"preview": "# coding=utf-8\n\"\"\"\n应用上下文模块\n\n提供配置上下文类,封装所有依赖配置的操作,消除全局状态和包装函数。\n\"\"\"\n\nfrom datetime import datetime\nfrom pathlib import Pat"
},
{
"path": "trendradar/core/__init__.py",
"chars": 1237,
"preview": "# coding=utf-8\n\"\"\"\n核心模块 - 配置管理和核心工具\n\"\"\"\n\nfrom trendradar.core.config import (\n parse_multi_account_config,\n valida"
},
{
"path": "trendradar/core/analyzer.py",
"chars": 25827,
"preview": "# coding=utf-8\n\"\"\"\n统计分析模块\n\n提供新闻统计和分析功能:\n- calculate_news_weight: 计算新闻权重\n- format_time_display: 格式化时间显示\n- count_word_freq"
},
{
"path": "trendradar/core/config.py",
"chars": 3687,
"preview": "# coding=utf-8\n\"\"\"\n配置工具模块 - 多账号配置解析和验证\n\n提供多账号推送配置的解析、验证和限制功能\n\"\"\"\n\nfrom typing import Dict, List, Optional, Tuple\n\n\ndef p"
},
{
"path": "trendradar/core/data.py",
"chars": 6682,
"preview": "# coding=utf-8\n\"\"\"\n数据处理模块\n\n提供数据读取和检测功能:\n- read_all_today_titles: 从存储后端读取当天所有标题\n- detect_latest_new_titles: 检测最新批次的新增标题\n\n"
},
{
"path": "trendradar/core/frequency.py",
"chars": 8645,
"preview": "# coding=utf-8\n\"\"\"\n频率词配置加载模块\n\n负责从配置文件加载频率词规则,支持:\n- 普通词组\n- 必须词(+前缀)\n- 过滤词(!前缀)\n- 全局过滤词([GLOBAL_FILTER] 区域)\n- 最大显示数量(@前缀)\n"
},
{
"path": "trendradar/core/loader.py",
"chars": 22555,
"preview": "# coding=utf-8\n\"\"\"\n配置加载模块\n\n负责从 YAML 配置文件和环境变量加载配置。\n\"\"\"\n\nimport os\nfrom pathlib import Path\nfrom typing import Dict, Any,"
},
{
"path": "trendradar/core/scheduler.py",
"chars": 14083,
"preview": "# coding=utf-8\n\"\"\"\n时间线调度器\n\n统一的时间线调度系统,替代分散的 push_window / analysis_window 逻辑。\n基于 periods + day_plans + week_map 模型实现灵活的时"
},
{
"path": "trendradar/crawler/__init__.py",
"chars": 116,
"preview": "# coding=utf-8\n\"\"\"\n爬虫模块 - 数据抓取功能\n\"\"\"\n\nfrom trendradar.crawler.fetcher import DataFetcher\n\n__all__ = [\"DataFetcher\"]\n"
},
{
"path": "trendradar/crawler/fetcher.py",
"chars": 5711,
"preview": "# coding=utf-8\n\"\"\"\n数据获取器模块\n\n负责从 NewsNow API 抓取新闻数据,支持:\n- 单个平台数据获取\n- 批量平台数据爬取\n- 自动重试机制\n- 代理支持\n\"\"\"\n\nimport json\nimport ran"
},
{
"path": "trendradar/crawler/rss/__init__.py",
"chars": 211,
"preview": "# coding=utf-8\n\"\"\"\nRSS 抓取模块\n\n提供 RSS 2.0、Atom 和 JSON Feed 1.1 订阅源的解析和抓取功能\n\"\"\"\n\nfrom .parser import RSSParser\nfrom .fetche"
},
{
"path": "trendradar/crawler/rss/fetcher.py",
"chars": 9489,
"preview": "# coding=utf-8\n\"\"\"\nRSS 抓取器\n\n负责从配置的 RSS 源抓取数据并转换为标准格式\n\"\"\"\n\nimport time\nimport random\nfrom dataclasses import dataclass\nfr"
},
{
"path": "trendradar/crawler/rss/parser.py",
"chars": 9188,
"preview": "# coding=utf-8\n\"\"\"\nRSS 解析器\n\n支持 RSS 2.0、Atom 和 JSON Feed 1.1 格式的解析\n\"\"\"\n\nimport re\nimport html\nimport json\nfrom dataclasse"
},
{
"path": "trendradar/notification/__init__.py",
"chars": 1571,
"preview": "# coding=utf-8\n\"\"\"\n通知推送模块\n\n提供多渠道通知推送功能,包括:\n- 飞书、钉钉、企业微信\n- Telegram、Slack\n- Email、ntfy、Bark\n\n模块结构:\n- formatters: 内容格式转换\n-"
},
{
"path": "trendradar/notification/batch.py",
"chars": 2766,
"preview": "# coding=utf-8\n\"\"\"\n批次处理模块\n\n提供消息分批发送的辅助函数\n\"\"\"\n\nfrom typing import List\n\n\ndef get_batch_header(format_type: str, batch_num"
},
{
"path": "trendradar/notification/dispatcher.py",
"chars": 47367,
"preview": "# coding=utf-8\n\"\"\"\n通知调度器模块\n\n提供统一的通知分发接口。\n支持所有通知渠道的多账号配置,使用 `;` 分隔多个账号。\n\n使用示例:\n dispatcher = NotificationDispatcher(co"
},
{
"path": "trendradar/notification/formatters.py",
"chars": 2237,
"preview": "# coding=utf-8\n\"\"\"\n通知内容格式转换模块\n\n提供不同推送平台间的格式转换功能\n\"\"\"\n\nimport re\n\n\ndef strip_markdown(text: str) -> str:\n \"\"\"去除文本中的 mar"
},
{
"path": "trendradar/notification/renderer.py",
"chars": 17433,
"preview": "# coding=utf-8\n\"\"\"\n通知内容渲染模块\n\n提供多平台通知内容渲染功能,生成格式化的推送消息\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Dict, List, "
},
{
"path": "trendradar/notification/senders.py",
"chars": 46491,
"preview": "# coding=utf-8\n\"\"\"\n消息发送器模块\n\n将报告数据发送到各种通知渠道:\n- 飞书 (Feishu/Lark)\n- 钉钉 (DingTalk)\n- 企业微信 (WeCom/WeWork)\n- Telegram\n- 邮件 (Em"
},
{
"path": "trendradar/notification/splitter.py",
"chars": 67989,
"preview": "# coding=utf-8\n\"\"\"\n消息分批处理模块\n\n提供消息内容分批拆分功能,确保消息大小不超过各平台限制\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Dict, Lis"
},
{
"path": "trendradar/report/__init__.py",
"chars": 726,
"preview": "# coding=utf-8\n\"\"\"\n报告生成模块\n\n提供报告生成和格式化功能,包括:\n- HTML 报告生成\n- 标题格式化工具\n\n模块结构:\n- helpers: 报告辅助函数(清理、转义、格式化)\n- formatter: 平台标题格"
},
{
"path": "trendradar/report/formatter.py",
"chars": 8221,
"preview": "# coding=utf-8\n\"\"\"\n平台标题格式化模块\n\n提供多平台标题格式化功能\n\"\"\"\n\nfrom typing import Dict\n\nfrom trendradar.report.helpers import clean_tit"
},
{
"path": "trendradar/report/generator.py",
"chars": 7715,
"preview": "# coding=utf-8\n\"\"\"\n报告生成模块\n\n提供报告数据准备和 HTML 生成功能:\n- prepare_report_data: 准备报告数据\n- generate_html_report: 生成 HTML 报告\n\"\"\"\n\nfr"
},
{
"path": "trendradar/report/helpers.py",
"chars": 3325,
"preview": "# coding=utf-8\n\"\"\"\n报告辅助函数模块\n\n提供报告生成相关的通用辅助函数\n\"\"\"\n\nimport re\nfrom typing import List\n\n\ndef clean_title(title: str) -> str"
},
{
"path": "trendradar/report/html.py",
"chars": 57616,
"preview": "# coding=utf-8\n\"\"\"\nHTML 报告渲染模块\n\n提供 HTML 格式的热点新闻报告生成功能\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Any, Dict, L"
},
{
"path": "trendradar/report/rss_html.py",
"chars": 14502,
"preview": "# coding=utf-8\n\"\"\"\nRSS HTML 报告渲染模块\n\n提供 RSS 订阅内容的 HTML 格式报告生成功能\n\"\"\"\n\nfrom datetime import datetime\nfrom typing import Dic"
},
{
"path": "trendradar/storage/__init__.py",
"chars": 1055,
"preview": "# coding=utf-8\n\"\"\"\n存储模块 - 支持多种存储后端\n\n支持的存储后端:\n- local: 本地 SQLite + TXT/HTML 文件\n- remote: 远程云存储(S3 兼容协议:R2/OSS/COS/S3 等)\n-"
},
{
"path": "trendradar/storage/ai_filter_schema.sql",
"chars": 3192,
"preview": "-- AI 智能筛选相关表结构\n-- 在 news 库中创建,与 news_items 同库\n\n-- ============================================\n-- AI 筛选兴趣标签表\n-- 存储从用户兴趣"
},
{
"path": "trendradar/storage/base.py",
"chars": 17192,
"preview": "# coding=utf-8\n\"\"\"\n存储后端抽象基类和数据模型\n\n定义统一的存储接口,所有存储后端都需要实现这些方法\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom dataclasses im"
},
{
"path": "trendradar/storage/local.py",
"chars": 16896,
"preview": "# coding=utf-8\n\"\"\"\n本地存储后端 - SQLite + TXT/HTML\n\n使用 SQLite 作为主存储,支持可选的 TXT 快照和 HTML 报告\n\"\"\"\n\nimport sqlite3\nimport shutil\ni"
},
{
"path": "trendradar/storage/manager.py",
"chars": 15172,
"preview": "# coding=utf-8\n\"\"\"\n存储管理器 - 统一管理存储后端\n\n根据环境和配置自动选择合适的存储后端\n\"\"\"\n\nimport os\nfrom typing import Optional\n\nfrom trendradar.stor"
},
{
"path": "trendradar/storage/remote.py",
"chars": 28323,
"preview": "# coding=utf-8\n\"\"\"\n远程存储后端(S3 兼容协议)\n\n支持 Cloudflare R2、阿里云 OSS、腾讯云 COS、AWS S3、MinIO 等\n使用 S3 兼容 API (boto3) 访问对象存储\n数据流程:下载当"
},
{
"path": "trendradar/storage/rss_schema.sql",
"chars": 3915,
"preview": "-- TrendRadar RSS 数据库表结构\n-- 用于存储 RSS/Atom 订阅源数据\n\n-- ============================================\n-- RSS 源配置表\n-- 存储订阅源的基本"
},
{
"path": "trendradar/storage/schema.sql",
"chars": 4019,
"preview": "-- TrendRadar 数据库表结构\n\n-- ============================================\n-- 平台信息表\n-- 核心:id 不变,name 可变\n-- =================="
},
{
"path": "trendradar/storage/sqlite_mixin.py",
"chars": 63635,
"preview": "# coding=utf-8\n\"\"\"\nSQLite 存储 Mixin\n\n提供共用的 SQLite 数据库操作逻辑,供 LocalStorageBackend 和 RemoteStorageBackend 复用。\n\"\"\"\n\nimport sq"
},
{
"path": "trendradar/utils/__init__.py",
"chars": 483,
"preview": "# coding=utf-8\n\"\"\"\n工具模块 - 公共工具函数\n\"\"\"\n\nfrom trendradar.utils.time import (\n get_configured_time,\n format_date_folde"
},
{
"path": "trendradar/utils/time.py",
"chars": 12026,
"preview": "# coding=utf-8\n\"\"\"\n时间工具模块\n\n本模块提供统一的时间处理函数,所有时区相关操作都应使用 DEFAULT_TIMEZONE 常量。\n\"\"\"\n\nfrom datetime import datetime\nfrom typi"
},
{
"path": "trendradar/utils/url.py",
"chars": 3502,
"preview": "# coding=utf-8\n\"\"\"\nURL 处理工具模块\n\n提供 URL 标准化功能,用于去重时消除动态参数的影响:\n- normalize_url: 标准化 URL,去除动态参数\n\"\"\"\n\nfrom urllib.parse impor"
},
{
"path": "version",
"chars": 5,
"preview": "6.5.0"
},
{
"path": "version_configs",
"chars": 147,
"preview": "config.yaml=2.2.0\ntimeline.yaml=1.2.0\nfrequency_words.txt=1.1.0\nai_interests.txt=1.0.0\nai_analysis_prompt.txt=2.0.0\nai_t"
},
{
"path": "version_mcp",
"chars": 5,
"preview": "4.0.0"
}
]
About this extraction
This page contains the full source code of the sansan0/TrendRadar GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 110 files (1.8 MB), approximately 499.7k tokens, and a symbol index with 908 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.