nodeseek关键词pushplus推送

首先,在你的 Debian 12 服务器上创建一个新文件,比如 setup.sh

nano setup.sh

然后将我为你提供的完整脚本内容复制并粘贴到这个文件中

#!/bin/bash
#
# RSS Monitor 服务安装与管理脚本
# 经过错误修正与全面优化增强了安全性健壮性和用户体验
#

# --- 脚本设置 ---
# set -e: 当任何命令返回非零退出码时立即退出
# set -u: 将任何未设置的变量视为错误
# set -o pipefail: 如果管道中的任何命令失败则整个管道的返回值为失败
set -euo pipefail

# --- 全局常量与配置 ---
# 使用大写只读变量来定义全局常量这是一种良好的编程实践
readonly SERVICE="rss_monitor"
readonly INSTALL_DIR="/opt/${SERVICE}"
readonly VENV_DIR="${INSTALL_DIR}/venv"
readonly BIN_FILE="/usr/local/bin/rssctl"
readonly RK_FILE="/usr/local/bin/rk"
readonly BASH_COMPLETION_DIR="/etc/bash_completion.d"
readonly ZSH_COMPLETION_DIR="/usr/share/zsh/site-functions"
readonly CONFIG_FILE="${INSTALL_DIR}/config.json"
readonly MONITOR_SCRIPT="${INSTALL_DIR}/rss_monitor.py"

# --- 辅助函数 ---
# 封装日志函数使输出更清晰更统一
info() { echo -e "\033[32m[信息]\033[0m $1"; }
warn() { echo -e "\033[33m[警告]\033[0m $1" >&2; }
error() { echo -e "\033[31m[错误]\033[0m $1" >&2; exit 1; }

# 检查脚本是否以 root 权限运行
check_root() {
    # ${EUID} 是一个环境变量代表有效用户ID0 代表 root 用户
    if [[ "${EUID}" -ne 0 ]]; then
        error "此脚本必须以 root 权限 (sudo) 运行。"
    fi
}

# --- 核心安装功能 ---

install_dependencies() {
    info "=== [1/8] 安装系统依赖 ==="
    # 使用 apt-get -qq 选项来减少不必要的输出让安装过程更整洁
    if ! apt-get update -qq 2>/dev/null || ! apt-get install -y -qq python3 python3-pip python3-venv bash-completion >/dev/null 2>&1; then
        error "依赖安装失败。请尝试手动运行 'apt-get update' 后重试。"
    fi
}

setup_directories_and_venv() {
    info "=== [2/8] 配置程序目录与 Python 虚拟环境 ==="
    # 【安全修复创建一个专用的系统用户来运行服务
    # 这个用户没有 shell 登录权限 (-s /bin/false)也无法作为普通用户登录
    if ! id -u "${SERVICE}" &>/dev/null; then
        info "创建专用的系统用户 '${SERVICE}'..."
        useradd -r -s /bin/false -d "${INSTALL_DIR}" "${SERVICE}"
    fi

    mkdir -p "${INSTALL_DIR}"
    
    info "创建 Python 虚拟环境..."
    python3 -m venv "${VENV_DIR}"

    info "安装 Python 依赖库 (requests, feedparser)..."
    # 将依赖安装到虚拟环境中避免污染全局 Python 环境
    if ! "${VENV_DIR}/bin/pip" install -q --upgrade pip || \
       ! "${VENV_DIR}/bin/pip" install -q requests feedparser; then
        error "在虚拟环境中安装 Python 依赖失败。"
    fi
    
    # 【安全修复将整个程序目录的所有权交给新创建的服务用户
    chown -R "${SERVICE}:${SERVICE}" "${INSTALL_DIR}"
}

create_config_file() {
    info "=== [3/8] 写入示例配置文件 ==="
    cat > "${CONFIG_FILE}" <<EOF
{
  "interval": 600,
  "keywords": ["AI", "ChatGPT", "开源"],
  "pushplus_token": "请在这里填写你的pushplus token"
}
EOF
    chown "${SERVICE}:${SERVICE}" "${CONFIG_FILE}"
    info "配置文件已创建于: ${CONFIG_FILE}"
    warn "请记得修改配置文件,填入你自己的 PushPlus Token!"
}

create_monitor_script() {
    info "=== [4/8] 写入主监控脚本 (rss_monitor.py) ==="
    cat > "${MONITOR_SCRIPT}" <<'EOF'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import json
import time
import signal
import requests
import feedparser
import sys

# --- 全局常量 ---
CONFIG_FILE = "/opt/rss_monitor/config.json"
RSS_URL = "https://rss.nodeseek.com"

# --- 全局状态变量 ---
running = True
reload_config_flag = False

def log(message):
    """将日志信息打印到标准输出,由 systemd 统一管理"""
    timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    print(f"[{timestamp}] {message}", flush=True)

def load_config():
    """加载并返回 JSON 配置文件内容"""
    try:
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except (FileNotFoundError, json.JSONDecodeError) as e:
        log(f"错误:无法加载或解析配置文件: {e}")
        return None

def send_pushplus(token, title, content):
    """通过 PushPlus 发送通知"""
    if not token or token == "请在这里填写你的pushplus token":
        log("警告:PushPlus token 未配置,跳过发送。")
        return

    url = "http://www.pushplus.plus/send"
    data = {
        "token": token,
        "title": title,
        "content": content,
        "template": "markdown",
    }
    try:
        response = requests.post(url, json=data, timeout=15)
        response.raise_for_status()
        log(f"成功发送通知: {title}")
    except requests.exceptions.RequestException as e:
        log(f"错误:PushPlus 通知发送失败: {e}")

def sigterm_handler(signum, frame):
    global running
    log("收到 SIGTERM 信号,服务准备停止...")
    running = False

def sighup_handler(signum, frame):
    global reload_config_flag
    log("收到 SIGHUP 信号,将重新加载配置...")
    reload_config_flag = True

signal.signal(signal.SIGTERM, sigterm_handler)
signal.signal(signal.SIGHUP, sighup_handler)

def main():
    global reload_config_flag
    
    log("RSS Monitor 服务已启动。")
    config = load_config()
    if not config:
        log("错误:启动时无法加载配置,服务退出。")
        sys.exit(1)
        
    seen_links = set()

    while running:
        try:
            if reload_config_flag:
                new_config = load_config()
                if new_config:
                    config = new_config
                    log("配置已成功重新加载。")
                else:
                    log("警告:尝试重载配置失败,继续使用旧配置。")
                reload_config_flag = False

            feed = feedparser.parse(RSS_URL)
            if feed.bozo:
                log(f"警告:RSS feed 解析可能存在问题: {feed.bozo_exception}")

            keywords = {kw.lower() for kw in config.get("keywords", [])}

            for entry in reversed(feed.entries):
                if entry.link not in seen_links:
                    seen_links.add(entry.link)
                    entry_title_lower = entry.title.lower()
                    
                    if any(kw in entry_title_lower for kw in keywords):
                        log(f"发现关键词匹配: '{entry.title}'")
                        content = f"**摘要**: {entry.summary[:150]}...\n\n[点击查看原文]({entry.link})"
                        send_pushplus(config.get("pushplus_token"), entry.title, content)
                        break
            
            time.sleep(config.get("interval", 600))

        except Exception as e:
            log(f"错误:主循环发生未知异常: {e}")
            time.sleep(60)

    log("RSS Monitor 服务已停止。")

if __name__ == "__main__":
    main()
EOF
    chmod +x "${MONITOR_SCRIPT}"
}

create_control_tool() {
    info "=== [5/8] 安装管理工具 (rssctl) ==="
    cat > "${BIN_FILE}" <<'EOF'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import os
import json
import subprocess

# --- 全局常量 ---
CONFIG_FILE = "/opt/rss_monitor/config.json"
SERVICE_NAME = "rss_monitor.service"

def run_system_command(command):
    """安全地执行一个系统命令"""
    try:
        # 使用 check=True如果命令失败会抛出异常
        result = subprocess.run(command, check=True, text=True, capture_output=True)
        return result.stdout.strip()
    except FileNotFoundError:
        print(f"错误:命令 '{command[0]}' 未找到。", file=sys.stderr)
        sys.exit(1)
    except subprocess.CalledProcessError as e:
        print(f"错误:执行命令 '{' '.join(command)}' 失败:", file=sys.stderr)
        print(e.stderr, file=sys.stderr)
        sys.exit(1)

def reload_service():
    """通知 systemd 服务重载配置"""
    print("正在通知服务重新加载配置...")
    # 【BUG修复移除命令开头的空字符串 ""
    run_system_command(["systemctl", "kill", "-s", "HUP", SERVICE_NAME])
    print("服务已收到重载指令。")

def load_config():
    """加载 JSON 配置文件"""
    try:
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        print(f"错误:配置文件 '{CONFIG_FILE}' 未找到。", file=sys.stderr)
        sys.exit(1)
    except json.JSONDecodeError:
        print(f"错误:配置文件 '{CONFIG_FILE}' 格式无效。", file=sys.stderr)
        sys.exit(1)

def save_config(cfg):
    """保存配置到 JSON 文件"""
    try:
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            json.dump(cfg, f, ensure_ascii=False, indent=2)
    except IOError as e:
        print(f"错误:无法写入配置文件: {e}", file=sys.stderr)
        sys.exit(1)

def list_keywords(cfg):
    keywords = cfg.get("keywords", [])
    if not keywords:
        print("当前没有配置任何关键词。")
    else:
        print("当前关键词列表:")
        for kw in sorted(keywords):
            print(f"- {kw}")

def add_keyword(cfg, keyword):
    keywords = set(cfg.get("keywords", []))
    if keyword in keywords:
        print(f"关键词 '{keyword}' 已存在。")
        return
    keywords.add(keyword)
    cfg["keywords"] = sorted(list(keywords))
    save_config(cfg)
    print(f"成功添加关键词: {keyword}")
    reload_service()

def remove_keyword(cfg, keyword):
    keywords = cfg.get("keywords", [])
    if keyword not in keywords:
        print(f"关键词 '{keyword}' 不存在。")
        return
    keywords.remove(keyword)
    cfg["keywords"] = keywords
    save_config(cfg)
    print(f"成功删除关键词: {keyword}")
    reload_service()

def set_token(cfg, token):
    if not token:
        print("错误:token 不能为空。")
        return
    cfg['pushplus_token'] = token
    save_config(cfg)
    print("PushPlus token 已成功更新。")
    reload_service()

def show_menu():
    """显示交互式管理菜单"""
    while True:
        print("\n=== RSS Monitor 服务管理菜单 ===")
        print("1. 查看关键词列表")
        print("2. 添加关键词")
        print("3. 删除关键词")
        print("4. 修改 PushPlus Token")
        print("-------------------------------")
        print("5. 查看服务状态")
        print("6. 重启服务")
        print("7. 查看实时日志")
        print("0. 退出菜单")
        
        choice = input("请输入选项序号: ").strip()

        if choice == '1':
            list_keywords(load_config())
        elif choice == '2':
            kw = input("请输入要添加的关键词: ").strip()
            if kw: add_keyword(load_config(), kw)
        elif choice == '3':
            # 【BUG修复修正了变量名与函数名冲突的问题
            keyword_to_remove = input("请输入要删除的关键词: ").strip()
            if keyword_to_remove: remove_keyword(load_config(), keyword_to_remove)
        elif choice == '4':
            token = input("请输入新的 PushPlus Token: ").strip()
            if token: set_token(load_config(), token)
        elif choice == '5':
            # 【BUG修复移除命令开头的空字符串 ""
            os.system(f"systemctl status {SERVICE_NAME}")
        elif choice == '6':
            print("正在重启服务...")
            # 【BUG修复移除命令开头的空字符串 ""
            run_system_command(["systemctl", "restart", SERVICE_NAME])
            print("服务已重启。")
        elif choice == '7':
            print("按 Ctrl+C 退出日志查看。")
            os.system(f"journalctl -u {SERVICE_NAME} -f -n 50")
        elif choice == '0':
            print("已退出管理菜单。")
            break
        else:
            print("无效输入,请输入菜单中的数字。")

def usage():
    print(f"用法: {os.path.basename(sys.argv[0])} [命令] [参数]")
    print("\n可用命令:")
    print("  list-keywords          - 列出所有关键词")
    print("  add-keyword <关键词>   - 添加一个关键词")
    print("  remove-keyword <关键词> - 删除一个关键词")
    print("  set-token <Token>      - 设置 PushPlus token")
    print("  reload                 - 重新加载服务配置")
    print("  menu                   - 显示交互式管理菜单")

def main():
    if len(sys.argv) < 2: usage(); sys.exit(1)
    cmd = sys.argv[1]
    
    if cmd == "menu":
        show_menu()
    elif cmd == "reload":
        reload_service()
    elif cmd in ["list-keywords", "add-keyword", "remove-keyword", "set-token"]:
        cfg = load_config()
        if cmd == "list-keywords":
            list_keywords(cfg)
        elif cmd in ["add-keyword", "remove-keyword", "set-token"]:
            if len(sys.argv) != 3: usage(); sys.exit(1)
            arg = sys.argv[2]
            if cmd == "add-keyword": add_keyword(cfg, arg)
            elif cmd == "remove-keyword": remove_keyword(cfg, arg)
            elif cmd == "set-token": set_token(cfg, arg)
    else:
        usage()

if __name__ == "__main__":
    main()
EOF
    chmod +x "${BIN_FILE}"
}

create_systemd_service() {
    info "=== [6/8] 创建 systemd 服务 ==="
    cat > "/etc/systemd/system/${SERVICE}.service" <<EOF
[Unit]
Description=RSS Monitor Service
After=network.target

[Service]
Type=simple
# 【安全修复指定服务以我们创建的低权限用户运行
User=${SERVICE}
Group=${SERVICE}
WorkingDirectory=${INSTALL_DIR}
ExecStart=${VENV_DIR}/bin/python3 ${MONITOR_SCRIPT}
Restart=always
RestartSec=10
# 【健壮性改进将日志重定向到 systemd journald方便管理
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

    info "重新加载 systemd 配置并启动服务..."
    systemctl daemon-reload
    systemctl enable "${SERVICE}.service"
    systemctl restart "${SERVICE}.service"
}

setup_shell_integration() {
    info "=== [7/8] 配置 Shell 自动补全与快捷别名 ==="
    cat > "${BASH_COMPLETION_DIR}/rssctl" <<'EOF'
_rssctl_completion() {
    local cur prev
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"

    if [[ ${COMP_CWORD} -eq 1 ]]; then
        COMPREPLY=( $(compgen -W "list-keywords add-keyword remove-keyword set-token reload menu" -- "${cur}") )
        return 0
    fi

    if [[ ${prev} == "remove-keyword" ]]; then
        local keywords=$(rssctl list-keywords 2>/dev/null | grep '^-' | cut -d ' ' -f 2-)
        COMPREPLY=( $(compgen -W "${keywords}" -- "${cur}") )
    fi
}
complete -F _rssctl_completion rssctl rk
EOF

    mkdir -p "${ZSH_COMPLETION_DIR}"
    ln -sf "${BASH_COMPLETION_DIR}/rssctl" "${ZSH_COMPLETION_DIR}/_rssctl"

    info "=== [8/8] 创建快捷命令 (rk) 与别名 ==="
    ln -sf "${BIN_FILE}" "${RK_FILE}"
    
    local aliases_snippet
    aliases_snippet=$(cat <<'EOF'

# RSS Monitor 快捷别名
alias rk='rssctl'
alias rkl='rssctl list-keywords'
alias rka='rssctl add-keyword'
alias rkr='rssctl remove-keyword'
alias rkm='rssctl menu'
EOF
)
    # 遍历当前用户和 root 用户的主目录 bash  zsh 添加别名
    for homedir in "/root" "${HOME:-/root}"; do
        for rc_file in "${homedir}/.bashrc" "${homedir}/.zshrc"; do
            if [ -f "${rc_file}" ] && ! grep -q "# RSS Monitor 快捷别名" "${rc_file}"; then
                info "添加别名到 ${rc_file}..."
                echo "${aliases_snippet}" >> "${rc_file}"
            fi
        done
    done
}

# --- 完整安装流程 ---
install_all() {
    check_root
    info "开始安装 RSS Monitor 服务..."
    
    install_dependencies
    setup_directories_and_venv
    create_config_file
    create_monitor_script
    create_control_tool
    create_systemd_service
    setup_shell_integration
    
    info "=========================================="
    info "🎉 RSS Monitor 安装完成!"
    info "=========================================="
    echo ""
    info "请执行 'source ~/.bashrc'  'source ~/.zshrc' 来让快捷别名生效或重新打开终端。"
    echo ""
    info "常用命令:"
    info "  rkm              - 显示交互式管理菜单 (推荐)"
    info "  systemctl status ${SERVICE} - 查看服务运行状态"
    info "  journalctl -u ${SERVICE} -f - 查看实时日志"
    echo ""
    
    "${BIN_FILE}" menu
}


# --- 卸载功能 ---
uninstall_all() {
    check_root
    warn "即将彻底卸载 RSS Monitor 服务!"
    read -p "这将删除所有配置文件脚本和专用用户确定要继续吗?(y/N): " choice
    if [[ "${choice}" != "y" && "${choice}" != "Y" ]]; then
        info "操作已取消。"
        exit 0
    fi

    info "停止并禁用 systemd 服务..."
    systemctl stop "${SERVICE}.service" &>/dev/null || true
    systemctl disable "${SERVICE}.service" &>/dev/null || true
    rm -f "/etc/systemd/system/${SERVICE}.service"
    systemctl daemon-reload

    info "删除程序目录: ${INSTALL_DIR}..."
    rm -rf "${INSTALL_DIR}"

    info "删除命令、软链接和自动补全脚本..."
    rm -f "${BIN_FILE}" "${RK_FILE}" "${BASH_COMPLETION_DIR}/rssctl" "${ZSH_COMPLETION_DIR}/_rssctl"

    info "从 shell 配置文件中移除别名..."
    for homedir in "/root" "${HOME:-/root}"; do
        for rc_file in "${homedir}/.bashrc" "${homedir}/.zshrc"; do
            if [ -f "${rc_file}" ]; then
                # 使用 sed 命令来删除我们之前添加的别名区块
                sed -i '/# RSS Monitor 快捷别名/,+5d' "${rc_file}"
            fi
        done
    done
    
    if id -u "${SERVICE}" &>/dev/null; then
        info "删除专用用户 '${SERVICE}'..."
        userdel "${SERVICE}"
    fi

    info "卸载完成。"
}

# --- 脚本入口 ---
usage() {
    echo "用法: $0 [install|uninstall]"
    echo "  install:   安装或更新 RSS Monitor 服务"
    echo "  uninstall: 卸载 RSS Monitor 服务"
    exit 1
}

main() {
    if [ "$#" -ne 1 ]; then
        usage
    fi

    case "$1" in
        install)
            install_all
            ;;
        uninstall)
            uninstall_all
            ;;
        *)
            usage
            ;;
    esac
}

# 执行主函数并将所有参数传递给它
main "$@"

赋予脚本执行权限

chmod +x setup.sh

运行安装脚本,脚本会自动完成所有安装步骤,包括安装依赖、创建目录、写入服务文件等

./setup.sh install

配置服务 安装完成后,你需要修改配置文件,填入你的 PushPlus Token 和关键词

nano /opt/rss_monitor/config.json

脚本其他命令

#脚本管理菜单快捷键
rkm

#查看脚本运行状态
systemctl status rss_monitor

#停止运行脚本
systemctl stop rss_monitor

#开始运行脚本
systemctl start rss_monitor

#卸载脚本
./setup.sh uninstall

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注