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} 是一个环境变量,代表有效用户ID。0 代表 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