v3.5.0: 新增了一些功能,详细见【更新日志】

This commit is contained in:
sansan 2025-12-03 19:58:41 +08:00
parent 6266751bae
commit e6b95ada77
13 changed files with 2750 additions and 482 deletions

View File

@ -1,17 +1,33 @@
name: Build and Push Multi-Arch Docker Images name: Build and Push Docker Images
on: on:
push: push:
tags: ["v*"] tags:
- "v*" # 主项目版本
- "mcp-v*" # MCP 版本
workflow_dispatch: workflow_dispatch:
inputs:
image:
description: "选择要构建的镜像"
required: true
default: "all"
type: choice
options:
- all
- crawler
- mcp
env: env:
REGISTRY: docker.io REGISTRY: docker.io
IMAGE_NAME: wantcat/trendradar
jobs: jobs:
build: build-crawler:
runs-on: ubuntu-latest 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: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -35,12 +51,11 @@ jobs:
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ env.IMAGE_NAME }} images: wantcat/trendradar
tags: | tags: |
type=ref,event=branch
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}} type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
@ -55,5 +70,65 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max
build-args: |
BUILDKIT_INLINE_CACHE=1 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 diff suppressed because it is too large Load Diff

923
README.md

File diff suppressed because it is too large Load Diff

View File

@ -29,6 +29,7 @@ report:
rank_threshold: 5 # 排名高亮阈值 rank_threshold: 5 # 排名高亮阈值
sort_by_position_first: false # 排序优先级true=先按配置位置排序false=先按热点条数排序 sort_by_position_first: false # 排序优先级true=先按配置位置排序false=先按热点条数排序
max_news_per_keyword: 0 # 每个关键词最大显示数量0=不限制 max_news_per_keyword: 0 # 每个关键词最大显示数量0=不限制
reverse_content_order: false # 内容顺序false=热点词汇统计在前true=新增热点新闻在前
notification: notification:
enable_notification: true # 是否启用通知功能,如果 false则不发送手机通知 enable_notification: true # 是否启用通知功能,如果 false则不发送手机通知
@ -39,6 +40,7 @@ notification:
slack_batch_size: 4000 # Slack消息分批大小字节 slack_batch_size: 4000 # Slack消息分批大小字节
batch_send_interval: 3 # 批次发送间隔(秒) batch_send_interval: 3 # 批次发送间隔(秒)
feishu_message_separator: "━━━━━━━━━━━━━━━━━━━" # feishu 消息分割线 feishu_message_separator: "━━━━━━━━━━━━━━━━━━━" # feishu 消息分割线
max_accounts_per_channel: 3 # 每个渠道最大账号数量,建议不超过 3
# 🕐 推送时间窗口控制(可选功能) # 🕐 推送时间窗口控制(可选功能)
# 用途:限制推送的时间范围,避免非工作时间打扰 # 用途:限制推送的时间范围,避免非工作时间打扰
@ -71,23 +73,39 @@ notification:
# - Minor: Spam notifications flooding your devices # - Minor: Spam notifications flooding your devices
# - Severe: Webhook abuse leading to security incidents (malicious messages, phishing links, etc.) # - Severe: Webhook abuse leading to security incidents (malicious messages, phishing links, etc.)
# #
# ⚠️⚠️⚠️ 多账号推送说明 / MULTI-ACCOUNT PUSH NOTICE ⚠️⚠️⚠️
#
# 🔸 多账号支持:
# • 请使用分号(;)分隔多个账号,如:"url1;url2;url3"
# • 示例telegram_bot_token: "token1;token2" 对应 telegram_chat_id: "id1;id2"
# • 对于需要配对的配置(如 Telegram 的 token 和 chat_id数量必须一致
# • 每个渠道最多支持 max_accounts_per_channel 个账号(见上方配置)
# • 邮箱已支持多收件人(逗号分隔),保持不变
#
# 🔸 Multi-Account Support:
# • Use semicolon(;) to separate multiple accounts, e.g., "url1;url2;url3"
# • Example: telegram_bot_token: "token1;token2" with telegram_chat_id: "id1;id2"
# • For paired configs (e.g., Telegram token and chat_id), quantities must match
# • Each channel supports up to max_accounts_per_channel accounts (see above config)
# • Email already supports multiple recipients (comma-separated), unchanged
#
webhooks: webhooks:
feishu_url: "" # 飞书机器人的 webhook URL feishu_url: "" # 飞书机器人的 webhook URL(多账号用 ; 分隔)
dingtalk_url: "" # 钉钉机器人的 webhook URL dingtalk_url: "" # 钉钉机器人的 webhook URL(多账号用 ; 分隔)
wework_url: "" # 企业微信机器人的 webhook URL wework_url: "" # 企业微信机器人的 webhook URL(多账号用 ; 分隔)
wework_msg_type: "markdown" # 企业微信消息类型markdown(群机器人) 或 text(个人微信应用) wework_msg_type: "markdown" # 企业微信消息类型markdown(群机器人) 或 text(个人微信应用)
telegram_bot_token: "" # Telegram Bot Token telegram_bot_token: "" # Telegram Bot Token(多账号用 ; 分隔,需与 chat_id 数量一致)
telegram_chat_id: "" # Telegram Chat ID telegram_chat_id: "" # Telegram Chat ID(多账号用 ; 分隔,需与 bot_token 数量一致)
email_from: "" # 发件人邮箱地址 email_from: "" # 发件人邮箱地址
email_password: "" # 发件人邮箱密码或授权码 email_password: "" # 发件人邮箱密码或授权码
email_to: "" # 收件人邮箱地址,多个收件人用逗号分隔 email_to: "" # 收件人邮箱地址,多个收件人用逗号分隔
email_smtp_server: "" # SMTP服务器地址可选留空自动识别 email_smtp_server: "" # SMTP服务器地址可选留空自动识别
email_smtp_port: "" # SMTP端口可选留空自动识别 email_smtp_port: "" # SMTP端口可选留空自动识别
ntfy_server_url: "https://ntfy.sh" # ntfy服务器地址默认使用公共服务可改为自托管地址 ntfy_server_url: "https://ntfy.sh" # ntfy服务器地址默认使用公共服务可改为自托管地址
ntfy_topic: "" # ntfy主题名称 ntfy_topic: "" # ntfy主题名称(多账号用 ; 分隔)
ntfy_token: "" # ntfy访问令牌可选用于私有主题 ntfy_token: "" # ntfy访问令牌可选用于私有主题,多账号用 ; 分隔
bark_url: "" # Bark推送URL格式https://api.day.app/your_device_key 或自建服务器地址) bark_url: "" # Bark推送URL多账号用 ; 分隔,格式https://api.day.app/your_device_key 或自建服务器地址)
slack_webhook_url: "" # Slack Incoming Webhook URL格式https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX slack_webhook_url: "" # Slack Incoming Webhook URL多账号用 ; 分隔,格式https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
# 用于让关注度更高的新闻在更前面显示,即用算法重新组合不同平台的热搜排序形成你侧重的热搜,合起来是 1 就行 # 用于让关注度更高的新闻在更前面显示,即用算法重新组合不同平台的热搜排序形成你侧重的热搜,合起来是 1 就行
weight: weight:

View File

@ -12,6 +12,21 @@ REPORT_MODE=
SORT_BY_POSITION_FIRST= SORT_BY_POSITION_FIRST=
# 每个关键词最大显示数量 (0=不限制,>0=限制数量) # 每个关键词最大显示数量 (0=不限制,>0=限制数量)
MAX_NEWS_PER_KEYWORD= MAX_NEWS_PER_KEYWORD=
# 内容顺序false=热点词汇统计在前true=新增热点新闻在前
REVERSE_CONTENT_ORDER=
# ============================================
# Web 服务器配置
# ============================================
# 是否自动启动 Web 服务器托管 output 目录 (true/false)
# 启用后可通过 http://localhost:{WEBSERVER_PORT} 访问生成的报告
# 手动控制docker exec -it trend-radar python manage.py start_webserver
ENABLE_WEBSERVER=false
# Web 服务器端口(默认 8080可自定义避免冲突
# 注意:修改后需要重启容器生效
WEBSERVER_PORT=8080
# ============================================ # ============================================
# 推送时间窗口配置 # 推送时间窗口配置
@ -29,36 +44,47 @@ PUSH_WINDOW_ONCE_PER_DAY=
PUSH_WINDOW_RETENTION_DAYS= PUSH_WINDOW_RETENTION_DAYS=
# ============================================ # ============================================
# 通知渠道配置 # 多账号配置
# ============================================ # ============================================
# 推送配置 # 每个渠道最大账号数量(建议不超过 3避免fork用户触发账号风险
MAX_ACCOUNTS_PER_CHANNEL=
# ============================================
# 通知渠道配置(多账号用 ; 分隔)
# ============================================
# 飞书机器人 webhook URL多账号用 ; 分隔)
FEISHU_WEBHOOK_URL= FEISHU_WEBHOOK_URL=
# Telegram Bot Token多账号用 ; 分隔,需与 chat_id 数量一致)
TELEGRAM_BOT_TOKEN= TELEGRAM_BOT_TOKEN=
# Telegram Chat ID多账号用 ; 分隔,需与 bot_token 数量一致)
TELEGRAM_CHAT_ID= TELEGRAM_CHAT_ID=
# 钉钉机器人 webhook URL多账号用 ; 分隔)
DINGTALK_WEBHOOK_URL= DINGTALK_WEBHOOK_URL=
# 企业微信机器人 webhook URL多账号用 ; 分隔)
WEWORK_WEBHOOK_URL= WEWORK_WEBHOOK_URL=
# 企业微信消息类型markdown 或 text
WEWORK_MSG_TYPE= WEWORK_MSG_TYPE=
# 邮件配置(邮箱已支持多收件人,逗号分隔)
EMAIL_FROM= EMAIL_FROM=
EMAIL_PASSWORD= EMAIL_PASSWORD=
EMAIL_TO= EMAIL_TO=
EMAIL_SMTP_SERVER= EMAIL_SMTP_SERVER=
EMAIL_SMTP_PORT= EMAIL_SMTP_PORT=
# ntfy 推送配置 # ntfy 推送配置(多账号用 ; 分隔topic 和 token 数量需一致)
NTFY_SERVER_URL=https://ntfy.sh NTFY_SERVER_URL=https://ntfy.sh
# ntfy主题名称 # ntfy主题名称(多账号用 ; 分隔)
NTFY_TOPIC= NTFY_TOPIC=
# 可选:访问令牌(用于私有主题) # 可选:访问令牌(用于私有主题,多账号用 ; 分隔,无令牌的留空占位如 ";token2")
NTFY_TOKEN= NTFY_TOKEN=
# Bark 推送配置 # Bark 推送配置(多账号用 ; 分隔)
# Bark推送URL格式https://api.day.app/your_device_key 或自建服务器地址)
BARK_URL= BARK_URL=
# Slack 推送配置 # Slack 推送配置(多账号用 ; 分隔)
# Slack Incoming Webhook URL格式https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
SLACK_WEBHOOK_URL= SLACK_WEBHOOK_URL=
# ============================================ # ============================================

View File

@ -2,9 +2,9 @@ FROM python:3.10-slim
WORKDIR /app WORKDIR /app
# https://github.com/aptible/supercronic # Latest releases available at https://github.com/aptible/supercronic/releases
ARG TARGETARCH ARG TARGETARCH
ENV SUPERCRONIC_VERSION=v0.2.34 ENV SUPERCRONIC_VERSION=v0.2.39
RUN set -ex && \ RUN set -ex && \
apt-get update && \ apt-get update && \
@ -12,12 +12,12 @@ RUN set -ex && \
case ${TARGETARCH} in \ case ${TARGETARCH} in \
amd64) \ amd64) \
export SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-amd64; \ export SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-amd64; \
export SUPERCRONIC_SHA1SUM=e8631edc1775000d119b70fd40339a7238eece14; \ export SUPERCRONIC_SHA1SUM=c98bbf82c5f648aaac8708c182cc83046fe48423; \
export SUPERCRONIC=supercronic-linux-amd64; \ export SUPERCRONIC=supercronic-linux-amd64; \
;; \ ;; \
arm64) \ arm64) \
export SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-arm64; \ export SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/${SUPERCRONIC_VERSION}/supercronic-linux-arm64; \
export SUPERCRONIC_SHA1SUM=4ab6343b52bf9da592e8b4bb7ae6eb5a8e21b71e; \ export SUPERCRONIC_SHA1SUM=5ef4ccc3d43f12d0f6c3763758bc37cc4e5af76e; \
export SUPERCRONIC=supercronic-linux-arm64; \ export SUPERCRONIC=supercronic-linux-arm64; \
;; \ ;; \
*) \ *) \
@ -26,26 +26,25 @@ RUN set -ex && \
;; \ ;; \
esac && \ esac && \
echo "Downloading supercronic for ${TARGETARCH} from ${SUPERCRONIC_URL}" && \ echo "Downloading supercronic for ${TARGETARCH} from ${SUPERCRONIC_URL}" && \
# 添加重试机制和超时设置 # 重试机制最多3次每次超时30秒
for i in 1 2 3 4 5; do \ for i in 1 2 3; do \
echo "Download attempt $i/5"; \ echo "Download attempt $i/3"; \
if curl --fail --silent --show-error --location --retry 3 --retry-delay 2 --connect-timeout 30 --max-time 120 -o "$SUPERCRONIC" "$SUPERCRONIC_URL"; then \ if curl -fsSL --connect-timeout 30 --max-time 60 -o "$SUPERCRONIC" "$SUPERCRONIC_URL"; then \
echo "Download successful"; \ echo "Download successful"; \
break; \ break; \
else \ else \
echo "Download attempt $i failed, exit code: $?"; \ echo "Download attempt $i failed"; \
if [ $i -eq 5 ]; then \ if [ $i -eq 3 ]; then \
echo "All download attempts failed"; \ echo "All download attempts failed"; \
exit 1; \ exit 1; \
fi; \ fi; \
sleep $((i * 2)); \ sleep 2; \
fi; \ fi; \
done && \ done && \
echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - && \ echo "${SUPERCRONIC_SHA1SUM} ${SUPERCRONIC}" | sha1sum -c - && \
chmod +x "$SUPERCRONIC" && \ chmod +x "$SUPERCRONIC" && \
mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" && \ mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" && \
ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic && \ ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic && \
# 验证安装
supercronic -version && \ supercronic -version && \
apt-get remove -y curl && \ apt-get remove -y curl && \
apt-get clean && \ apt-get clean && \

23
docker/Dockerfile.mcp Normal file
View File

@ -0,0 +1,23 @@
FROM python:3.10-slim
WORKDIR /app
# 安装依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制 MCP 服务器代码
COPY mcp_server/ ./mcp_server/
# 创建必要目录
RUN mkdir -p /app/config /app/output
ENV PYTHONUNBUFFERED=1 \
CONFIG_PATH=/app/config/config.yaml \
FREQUENCY_WORDS_PATH=/app/config/frequency_words.txt
# MCP HTTP 服务端口
EXPOSE 3333
# 启动 MCP 服务器HTTP 模式)
CMD ["python", "-m", "mcp_server.server", "--transport", "http", "--host", "0.0.0.0", "--port", "3333"]

View File

@ -6,6 +6,9 @@ services:
container_name: trend-radar container_name: trend-radar
restart: unless-stopped restart: unless-stopped
ports:
- "127.0.0.1:${WEBSERVER_PORT:-8080}:${WEBSERVER_PORT:-8080}"
volumes: volumes:
- ../config:/app/config:ro - ../config:/app/config:ro
- ../output:/app/output - ../output:/app/output
@ -18,6 +21,12 @@ services:
- REPORT_MODE=${REPORT_MODE:-} - REPORT_MODE=${REPORT_MODE:-}
- SORT_BY_POSITION_FIRST=${SORT_BY_POSITION_FIRST:-} - SORT_BY_POSITION_FIRST=${SORT_BY_POSITION_FIRST:-}
- MAX_NEWS_PER_KEYWORD=${MAX_NEWS_PER_KEYWORD:-} - MAX_NEWS_PER_KEYWORD=${MAX_NEWS_PER_KEYWORD:-}
- REVERSE_CONTENT_ORDER=${REVERSE_CONTENT_ORDER:-}
# Web 服务器
- ENABLE_WEBSERVER=${ENABLE_WEBSERVER:-false}
- WEBSERVER_PORT=${WEBSERVER_PORT:-8080}
# 多账号配置
- MAX_ACCOUNTS_PER_CHANNEL=${MAX_ACCOUNTS_PER_CHANNEL:-}
# 推送时间窗口 # 推送时间窗口
- PUSH_WINDOW_ENABLED=${PUSH_WINDOW_ENABLED:-} - PUSH_WINDOW_ENABLED=${PUSH_WINDOW_ENABLED:-}
- PUSH_WINDOW_START=${PUSH_WINDOW_START:-} - PUSH_WINDOW_START=${PUSH_WINDOW_START:-}
@ -49,3 +58,20 @@ services:
- CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *} - CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *}
- RUN_MODE=${RUN_MODE:-cron} - RUN_MODE=${RUN_MODE:-cron}
- IMMEDIATE_RUN=${IMMEDIATE_RUN:-true} - IMMEDIATE_RUN=${IMMEDIATE_RUN:-true}
trend-radar-mcp:
build:
context: ..
dockerfile: docker/Dockerfile.mcp
container_name: trend-radar-mcp
restart: unless-stopped
ports:
- "127.0.0.1:3333:3333"
volumes:
- ../config:/app/config:ro
- ../output:/app/output:ro
environment:
- TZ=Asia/Shanghai

View File

@ -4,6 +4,9 @@ services:
container_name: trend-radar container_name: trend-radar
restart: unless-stopped restart: unless-stopped
ports:
- "127.0.0.1:${WEBSERVER_PORT:-8080}:${WEBSERVER_PORT:-8080}"
volumes: volumes:
- ../config:/app/config:ro - ../config:/app/config:ro
- ../output:/app/output - ../output:/app/output
@ -16,6 +19,12 @@ services:
- REPORT_MODE=${REPORT_MODE:-} - REPORT_MODE=${REPORT_MODE:-}
- SORT_BY_POSITION_FIRST=${SORT_BY_POSITION_FIRST:-} - SORT_BY_POSITION_FIRST=${SORT_BY_POSITION_FIRST:-}
- MAX_NEWS_PER_KEYWORD=${MAX_NEWS_PER_KEYWORD:-} - MAX_NEWS_PER_KEYWORD=${MAX_NEWS_PER_KEYWORD:-}
- REVERSE_CONTENT_ORDER=${REVERSE_CONTENT_ORDER:-}
# Web 服务器
- ENABLE_WEBSERVER=${ENABLE_WEBSERVER:-false}
- WEBSERVER_PORT=${WEBSERVER_PORT:-8080}
# 多账号配置
- MAX_ACCOUNTS_PER_CHANNEL=${MAX_ACCOUNTS_PER_CHANNEL:-}
# 推送时间窗口 # 推送时间窗口
- PUSH_WINDOW_ENABLED=${PUSH_WINDOW_ENABLED:-} - PUSH_WINDOW_ENABLED=${PUSH_WINDOW_ENABLED:-}
- PUSH_WINDOW_START=${PUSH_WINDOW_START:-} - PUSH_WINDOW_START=${PUSH_WINDOW_START:-}
@ -47,3 +56,18 @@ services:
- CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *} - CRON_SCHEDULE=${CRON_SCHEDULE:-*/5 * * * *}
- RUN_MODE=${RUN_MODE:-cron} - RUN_MODE=${RUN_MODE:-cron}
- IMMEDIATE_RUN=${IMMEDIATE_RUN:-true} - IMMEDIATE_RUN=${IMMEDIATE_RUN:-true}
trend-radar-mcp:
image: wantcat/trendradar-mcp:latest
container_name: trend-radar-mcp
restart: unless-stopped
ports:
- "127.0.0.1:3333:3333"
volumes:
- ../config:/app/config:ro
- ../output:/app/output:ro
environment:
- TZ=Asia/Shanghai

View File

@ -33,6 +33,12 @@ case "${RUN_MODE:-cron}" in
/usr/local/bin/python main.py /usr/local/bin/python main.py
fi fi
# 启动 Web 服务器(如果配置了)
if [ "${ENABLE_WEBSERVER:-false}" = "true" ]; then
echo "🌐 启动 Web 服务器..."
/usr/local/bin/python manage.py start_webserver
fi
echo "⏰ 启动supercronic: ${CRON_SCHEDULE:-*/30 * * * *}" echo "⏰ 启动supercronic: ${CRON_SCHEDULE:-*/30 * * * *}"
echo "🎯 supercronic 将作为 PID 1 运行" echo "🎯 supercronic 将作为 PID 1 运行"

View File

@ -8,8 +8,14 @@ import os
import sys import sys
import subprocess import subprocess
import time import time
import signal
from pathlib import Path from pathlib import Path
# Web 服务器配置
WEBSERVER_PORT = int(os.environ.get("WEBSERVER_PORT", "8080"))
WEBSERVER_DIR = "/app/output"
WEBSERVER_PID_FILE = "/tmp/webserver.pid"
def run_command(cmd, shell=True, capture_output=True): def run_command(cmd, shell=True, capture_output=True):
"""执行系统命令""" """执行系统命令"""
@ -394,6 +400,139 @@ def restart_supercronic():
print(" 💡 建议重启容器: docker restart trend-radar") print(" 💡 建议重启容器: docker restart trend-radar")
def start_webserver():
"""启动 Web 服务器托管 output 目录"""
print(f"🌐 启动 Web 服务器 (端口: {WEBSERVER_PORT})...")
print(f" 🔒 安全提示:仅提供静态文件访问,限制在 {WEBSERVER_DIR} 目录")
# 检查是否已经运行
if Path(WEBSERVER_PID_FILE).exists():
try:
with open(WEBSERVER_PID_FILE, 'r') as f:
old_pid = int(f.read().strip())
try:
os.kill(old_pid, 0) # 检查进程是否存在
print(f" ⚠️ Web 服务器已在运行 (PID: {old_pid})")
print(f" 💡 访问: http://localhost:{WEBSERVER_PORT}")
print(" 💡 停止服务: python manage.py stop_webserver")
return
except OSError:
# 进程不存在,删除旧的 PID 文件
os.remove(WEBSERVER_PID_FILE)
except Exception as e:
print(f" ⚠️ 清理旧的 PID 文件: {e}")
try:
os.remove(WEBSERVER_PID_FILE)
except:
pass
# 检查目录是否存在
if not Path(WEBSERVER_DIR).exists():
print(f" ❌ 目录不存在: {WEBSERVER_DIR}")
return
try:
# 启动 HTTP 服务器
# 使用 --bind 绑定到 0.0.0.0 使容器内部可访问
# 工作目录限制在 WEBSERVER_DIR防止访问其他目录
process = subprocess.Popen(
[sys.executable, '-m', 'http.server', str(WEBSERVER_PORT), '--bind', '0.0.0.0'],
cwd=WEBSERVER_DIR,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
# 等待一下确保服务器启动
time.sleep(1)
# 检查进程是否还在运行
if process.poll() is None:
# 保存 PID
with open(WEBSERVER_PID_FILE, 'w') as f:
f.write(str(process.pid))
print(f" ✅ Web 服务器已启动 (PID: {process.pid})")
print(f" 📁 服务目录: {WEBSERVER_DIR} (只读,仅静态文件)")
print(f" 🌐 访问地址: http://localhost:{WEBSERVER_PORT}")
print(f" 📄 首页: http://localhost:{WEBSERVER_PORT}/index.html")
print(" 💡 停止服务: python manage.py stop_webserver")
else:
print(f" ❌ Web 服务器启动失败")
except Exception as e:
print(f" ❌ 启动失败: {e}")
def stop_webserver():
"""停止 Web 服务器"""
print("🛑 停止 Web 服务器...")
if not Path(WEBSERVER_PID_FILE).exists():
print(" Web 服务器未运行")
return
try:
with open(WEBSERVER_PID_FILE, 'r') as f:
pid = int(f.read().strip())
try:
# 尝试终止进程
os.kill(pid, signal.SIGTERM)
time.sleep(0.5)
# 检查进程是否已终止
try:
os.kill(pid, 0)
# 进程还在,强制杀死
os.kill(pid, signal.SIGKILL)
print(f" ⚠️ 强制停止 Web 服务器 (PID: {pid})")
except OSError:
print(f" ✅ Web 服务器已停止 (PID: {pid})")
except OSError as e:
if e.errno == 3: # No such process
print(f" 进程已不存在 (PID: {pid})")
else:
raise
# 删除 PID 文件
os.remove(WEBSERVER_PID_FILE)
except Exception as e:
print(f" ❌ 停止失败: {e}")
# 尝试清理 PID 文件
try:
os.remove(WEBSERVER_PID_FILE)
except:
pass
def webserver_status():
"""查看 Web 服务器状态"""
print("🌐 Web 服务器状态:")
if not Path(WEBSERVER_PID_FILE).exists():
print(" ⭕ 未运行")
print(f" 💡 启动服务: python manage.py start_webserver")
return
try:
with open(WEBSERVER_PID_FILE, 'r') as f:
pid = int(f.read().strip())
try:
os.kill(pid, 0) # 检查进程是否存在
print(f" ✅ 运行中 (PID: {pid})")
print(f" 📁 服务目录: {WEBSERVER_DIR}")
print(f" 🌐 访问地址: http://localhost:{WEBSERVER_PORT}")
print(f" 📄 首页: http://localhost:{WEBSERVER_PORT}/index.html")
print(" 💡 停止服务: python manage.py stop_webserver")
except OSError:
print(f" ⭕ 未运行 (PID 文件存在但进程不存在)")
os.remove(WEBSERVER_PID_FILE)
print(" 💡 启动服务: python manage.py start_webserver")
except Exception as e:
print(f" ❌ 状态检查失败: {e}")
def show_help(): def show_help():
"""显示帮助信息""" """显示帮助信息"""
help_text = """ help_text = """
@ -406,6 +545,9 @@ def show_help():
files - 显示输出文件 files - 显示输出文件
logs - 实时查看日志 logs - 实时查看日志
restart - 重启说明 restart - 重启说明
start_webserver - 启动 Web 服务器托管 output 目录
stop_webserver - 停止 Web 服务器
webserver_status - 查看 Web 服务器状态
help - 显示此帮助 help - 显示此帮助
📖 使用示例: 📖 使用示例:
@ -413,10 +555,12 @@ def show_help():
python manage.py run python manage.py run
python manage.py status python manage.py status
python manage.py logs python manage.py logs
python manage.py start_webserver
# 在宿主机执行 # 在宿主机执行
docker exec -it trend-radar python manage.py run docker exec -it trend-radar python manage.py run
docker exec -it trend-radar python manage.py status docker exec -it trend-radar python manage.py status
docker exec -it trend-radar python manage.py start_webserver
docker logs trend-radar docker logs trend-radar
💡 常用操作指南: 💡 常用操作指南:
@ -436,6 +580,12 @@ def show_help():
4. 重启服务: restart 4. 重启服务: restart
- 由于 supercronic PID 1需要重启整个容器 - 由于 supercronic PID 1需要重启整个容器
- 使用: docker restart trend-radar - 使用: docker restart trend-radar
5. Web 服务器管理:
- 启动: start_webserver
- 停止: stop_webserver
- 状态: webserver_status
- 访问: http://localhost:8080
""" """
print(help_text) print(help_text)
@ -453,6 +603,9 @@ def main():
"files": show_files, "files": show_files,
"logs": show_logs, "logs": show_logs,
"restart": restart_supercronic, "restart": restart_supercronic,
"start_webserver": start_webserver,
"stop_webserver": stop_webserver,
"webserver_status": webserver_status,
"help": show_help, "help": show_help,
} }

872
main.py

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
3.4.1 3.5.0