1737 lines
67 KiB
Python
1737 lines
67 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
import os
|
||
import json
|
||
import time
|
||
import subprocess
|
||
import threading
|
||
import sys
|
||
from datetime import datetime, timedelta
|
||
import logging
|
||
import webbrowser
|
||
from flask import Flask, render_template, request, jsonify, session, redirect, url_for, make_response, send_from_directory, flash
|
||
import threading
|
||
from threading import Lock
|
||
import signal
|
||
import socket
|
||
import requests
|
||
import re
|
||
from flask_socketio import SocketIO # 添加WebSocket支持
|
||
import hashlib
|
||
|
||
# 全局应用状态变量
|
||
shutdown_flag = False # 控制应用程序关闭的标志
|
||
|
||
# 初始化 SocketIO
|
||
socketio = SocketIO()
|
||
|
||
# 常量定义:警报阈值和系统版本
|
||
ALARM_THRESHOLD = 2750
|
||
# 系统版本固定在代码中
|
||
VERSION = "v20250412034434-dev" # 系统版本号 - 这是固定的本地版本
|
||
|
||
# 获取或创建版本文件,格式为"当前时间(v版本号)",例如"20250408125815(v1.1)"。
|
||
def ensure_version_file():
|
||
"""确保版本文件存在,写入固定版本号"""
|
||
try:
|
||
version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION.txt')
|
||
|
||
# 使用固定版本号
|
||
version_content = VERSION
|
||
|
||
# 写入版本文件
|
||
with open(version_file, 'w') as f:
|
||
f.write(version_content)
|
||
logging.info(f"更新版本文件: {version_content}")
|
||
|
||
return version_content
|
||
except Exception as e:
|
||
logging.error(f"处理版本文件时出错: {e}")
|
||
return VERSION # 返回固定版本号
|
||
|
||
# 获取线上版本进行比较
|
||
def get_online_version():
|
||
"""获取线上版本信息"""
|
||
try:
|
||
log(f"正在从 {VERSION_CHECK_URL} 获取线上版本信息...")
|
||
|
||
# 获取系统代理设置
|
||
proxies = {
|
||
'http': os.environ.get('HTTP_PROXY', ''),
|
||
'https': os.environ.get('HTTPS_PROXY', '')
|
||
}
|
||
|
||
# 如果没有代理设置,尝试不使用代理直接访问
|
||
if not any(proxies.values()):
|
||
log("未检测到系统代理设置,尝试直接连接...")
|
||
response = requests.get(VERSION_CHECK_URL, timeout=5, verify=False)
|
||
else:
|
||
log(f"使用系统代理设置: {proxies}")
|
||
response = requests.get(VERSION_CHECK_URL, timeout=5, proxies=proxies, verify=False)
|
||
|
||
log(f"获取线上版本响应状态码: {response.status_code}")
|
||
|
||
if response.status_code == 200:
|
||
version = response.text.strip()
|
||
log(f"成功获取线上版本: {version}")
|
||
return version
|
||
elif response.status_code == 407:
|
||
log("需要代理认证,尝试不使用代理直接访问...")
|
||
# 尝试不使用代理直接访问
|
||
response = requests.get(VERSION_CHECK_URL, timeout=5, proxies={'http': None, 'https': None}, verify=False)
|
||
if response.status_code == 200:
|
||
version = response.text.strip()
|
||
log(f"直接访问成功获取线上版本: {version}")
|
||
return version
|
||
else:
|
||
log(f"直接访问失败,HTTP状态码: {response.status_code}")
|
||
return None
|
||
else:
|
||
log(f"获取线上版本失败,HTTP状态码: {response.status_code}")
|
||
return None
|
||
except requests.exceptions.Timeout:
|
||
log("获取线上版本超时")
|
||
return None
|
||
except requests.exceptions.ConnectionError:
|
||
log("连接线上版本服务器失败,尝试不使用代理直接访问...")
|
||
try:
|
||
# 尝试不使用代理直接访问
|
||
response = requests.get(VERSION_CHECK_URL, timeout=5, proxies={'http': None, 'https': None}, verify=False)
|
||
if response.status_code == 200:
|
||
version = response.text.strip()
|
||
log(f"直接访问成功获取线上版本: {version}")
|
||
return version
|
||
else:
|
||
log(f"直接访问失败,HTTP状态码: {response.status_code}")
|
||
return None
|
||
except Exception as e:
|
||
log(f"直接访问失败: {str(e)}")
|
||
return None
|
||
except Exception as e:
|
||
log(f"获取线上版本时发生未知错误: {str(e)}")
|
||
return None
|
||
|
||
VERSION_CHECK_URL = "http://cos.ui-beam.com/work_scripts/monitor/dev/VERSION.txt" # 测试版本地址
|
||
# VERSION_CHECK_URL = "http://cos.ui-beam.com/work_scripts/monitor/releases/VERSION.txt" # 正式版本地址
|
||
version_status = {
|
||
'last_check_time': None,
|
||
'online_version': None,
|
||
'has_update': False
|
||
}
|
||
version_lock = Lock()
|
||
|
||
# 版本检测线程标志
|
||
version_check_thread_running = False
|
||
|
||
# 检查并安装依赖
|
||
def check_and_install_dependencies():
|
||
try:
|
||
# 检查plyer库是否安装
|
||
try:
|
||
import plyer
|
||
except ImportError:
|
||
log("正在安装plyer库...")
|
||
subprocess.check_call([sys.executable, "-m", "pip", "install", "plyer"])
|
||
log("plyer库安装成功")
|
||
except Exception as e:
|
||
log(f"依赖检查/安装过程中出错: {str(e)}")
|
||
|
||
# 配置日志
|
||
def setup_logging():
|
||
"""设置日志系统"""
|
||
try:
|
||
log_file = os.path.join('logs', f'dashboard_{datetime.now().strftime("%Y%m%d")}.log')
|
||
logging.basicConfig(
|
||
level=logging.INFO,
|
||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||
handlers=[
|
||
logging.FileHandler(log_file, encoding='utf-8'),
|
||
logging.StreamHandler()
|
||
]
|
||
)
|
||
return True
|
||
except Exception as e:
|
||
print(f"设置日志系统失败: {str(e)}")
|
||
return False
|
||
|
||
# 禁用 Flask 的访问日志
|
||
logging.getLogger('werkzeug').setLevel(logging.ERROR)
|
||
|
||
# 创建Flask应用
|
||
app = Flask(__name__)
|
||
app.secret_key = 'netease-dashboard-secret-key' # 用于session加密
|
||
app.permanent_session_lifetime = timedelta(days=30) # 设置会话有效期为30天
|
||
socketio.init_app(app) # 初始化 SocketIO
|
||
|
||
# 告警阈值
|
||
ALARM_THRESHOLD = 1900 # 两个系统折算总和的告警阈值
|
||
alarm_status = {
|
||
'last_alarm_time': None,
|
||
'alarm_count': 0,
|
||
'is_alarming': False,
|
||
'alarm_type': None
|
||
}
|
||
alarm_lock = Lock()
|
||
|
||
# 进程对象
|
||
processes = {
|
||
'breeze': None,
|
||
'cms': None
|
||
}
|
||
|
||
def log(message):
|
||
"""记录日志"""
|
||
try:
|
||
logging.info(message)
|
||
except Exception as e:
|
||
print("Log error: " + str(e))
|
||
print("Original message: " + message)
|
||
|
||
def start_monitor_processes():
|
||
"""启动监控进程"""
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# 获取会话中的凭据
|
||
breeze_cookie = session.get('breeze_cookie', '')
|
||
cms_cookie = session.get('cms_cookie', '')
|
||
username = session.get('username', '')
|
||
|
||
# 获取Python可执行文件路径
|
||
python_executable = sys.executable
|
||
|
||
# 启动breeze监控
|
||
if processes['breeze'] is None or processes['breeze'].poll() is not None:
|
||
breeze_script = os.path.join(script_dir, "breeze_monitor.py")
|
||
# 在命令行参数中传递环境变量
|
||
cmd = [python_executable, breeze_script]
|
||
env = os.environ.copy()
|
||
env['BREEZE_COOKIE'] = breeze_cookie
|
||
env['BREEZE_USERNAME'] = username
|
||
|
||
processes['breeze'] = subprocess.Popen(
|
||
cmd,
|
||
env=env,
|
||
shell=False
|
||
)
|
||
log(f"Breeze监控进程已启动, PID: {processes['breeze'].pid}")
|
||
|
||
# 启动cms监控
|
||
if processes['cms'] is None or processes['cms'].poll() is not None:
|
||
cms_script = os.path.join(script_dir, "cms_monitor.py")
|
||
# 在命令行参数中传递环境变量
|
||
cmd = [python_executable, cms_script]
|
||
env = os.environ.copy()
|
||
env['CMS_COOKIE'] = cms_cookie
|
||
env['CMS_USERNAME'] = username
|
||
|
||
processes['cms'] = subprocess.Popen(
|
||
cmd,
|
||
env=env,
|
||
shell=False
|
||
)
|
||
log(f"CMS监控进程已启动, PID: {processes['cms'].pid}")
|
||
|
||
return True
|
||
except Exception as e:
|
||
log(f"启动监控进程失败: {str(e)}")
|
||
return False
|
||
|
||
def add_no_cache_headers(response):
|
||
"""添加禁止缓存的响应头"""
|
||
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
|
||
response.headers['Pragma'] = 'no-cache'
|
||
response.headers['Expires'] = '0'
|
||
return response
|
||
|
||
@app.after_request
|
||
def after_request(response):
|
||
"""每个响应添加禁止缓存的头部"""
|
||
return add_no_cache_headers(response)
|
||
|
||
@app.route('/favicon.ico')
|
||
def favicon():
|
||
"""提供网站图标"""
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
static_path = os.path.join(script_dir, 'static')
|
||
return send_from_directory(static_path, 'dashboard-favicon.ico', mimetype='image/vnd.microsoft.icon')
|
||
except Exception as e:
|
||
log("Failed to serve favicon: " + str(e))
|
||
return '', 204
|
||
|
||
@app.route('/')
|
||
def index():
|
||
"""首页路由"""
|
||
if not session.get('logged_in'):
|
||
return redirect(url_for('login'))
|
||
|
||
staff_name = session.get('staff_name', '')
|
||
staff_id = session.get('username', '') # 获取工号
|
||
# 添加登录成功的提示消息
|
||
message = None
|
||
if session.get('just_logged_in'):
|
||
# 不再在这里设置消息,而是在前端处理
|
||
session['just_logged_in'] = False # 清除标记,确保消息只显示一次
|
||
|
||
return render_template('dashboard.html',
|
||
staff_name=f"{staff_name}({staff_id})", # 修改显示格式,添加工号
|
||
message=message,
|
||
version=VERSION)
|
||
|
||
def get_staff_name(staff_id):
|
||
"""根据工号获取员工姓名"""
|
||
try:
|
||
# 从在线链接获取staff.json
|
||
url = "http://cos.ui-beam.com/work_scripts/monitor/config/staff.json"
|
||
|
||
# 尝试不使用代理直接获取
|
||
try:
|
||
response = requests.get(url, timeout=5, proxies={'http': None, 'https': None}, verify=False)
|
||
except:
|
||
# 如果直接获取失败,尝试使用系统代理
|
||
proxies = {
|
||
'http': os.environ.get('HTTP_PROXY', ''),
|
||
'https': os.environ.get('HTTPS_PROXY', '')
|
||
}
|
||
response = requests.get(url, timeout=5, proxies=proxies, verify=False)
|
||
|
||
if response.status_code == 200:
|
||
staff_data = response.json()
|
||
return staff_data.get(staff_id)
|
||
else:
|
||
log(f"获取在线staff.json失败,HTTP状态码: {response.status_code}")
|
||
return None
|
||
except Exception as e:
|
||
log(f"获取员工姓名失败: {str(e)}")
|
||
return None
|
||
|
||
@app.route('/login', methods=['GET', 'POST'])
|
||
def login():
|
||
"""登录路由"""
|
||
if request.method == 'POST':
|
||
staff_id = request.form.get('username', '').strip() # 从username字段获取工号
|
||
breeze_cookie = request.form.get('breeze_cookie', '').strip() # 获取Breeze Cookie
|
||
cms_cookie = request.form.get('cms_cookie', '').strip() # 获取CMS Cookie
|
||
staff_name = get_staff_name(staff_id)
|
||
|
||
if staff_name:
|
||
session['logged_in'] = True
|
||
session['staff_name'] = staff_name
|
||
session['username'] = staff_id # 保存工号
|
||
session['breeze_cookie'] = breeze_cookie # 保存Breeze Cookie
|
||
session['cms_cookie'] = cms_cookie # 保存CMS Cookie
|
||
session['just_logged_in'] = True # 添加标记,用于显示欢迎消息
|
||
|
||
# 添加详细的系统启动日志
|
||
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
print(f"\n{current_time} - INFO - Breeze监控日志系统初始化成功")
|
||
print(f"{current_time} - INFO - CMS监控日志系统初始化成功")
|
||
print(f"{current_time} - INFO - 监控服务启动")
|
||
print(f"{current_time} - INFO - 初始化监控服务...")
|
||
print(f"{current_time} - INFO - 初始化用户凭据...")
|
||
print(f"{current_time} - INFO - [Breeze] 已从环境变量加载用户凭据: {staff_id}")
|
||
print(f"{current_time} - INFO - [CMS] 已从环境变量加载用户凭据: {staff_id}")
|
||
print(f"{current_time} - INFO - 加载系数配置...")
|
||
|
||
# 加载并显示 Breeze 系数
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
breeze_coefficients_file = os.path.join(script_dir, 'breeze_coefficients.json')
|
||
if os.path.exists(breeze_coefficients_file):
|
||
with open(breeze_coefficients_file, 'r', encoding='utf-8') as f:
|
||
breeze_coefficients = json.load(f)
|
||
print(f"{current_time} - INFO - [Breeze] 从配置文件加载系数: {breeze_coefficients}")
|
||
print(f"{current_time} - INFO - [Breeze] 当前使用的系数已更新")
|
||
except Exception as e:
|
||
print(f"{current_time} - ERROR - [Breeze] 加载系数配置失败: {str(e)}")
|
||
|
||
# 加载并显示 CMS 系数
|
||
try:
|
||
cms_coefficients_file = os.path.join(script_dir, 'cms_coefficients.json')
|
||
if os.path.exists(cms_coefficients_file):
|
||
with open(cms_coefficients_file, 'r', encoding='utf-8') as f:
|
||
cms_coefficients = json.load(f)
|
||
print(f"{current_time} - INFO - [CMS] 从配置文件加载系数: {cms_coefficients}")
|
||
print(f"{current_time} - INFO - [CMS] 当前使用的系数: 评论={cms_coefficients['comment']}, 动态={cms_coefficients['feed']}, 举报={cms_coefficients['complaint']}")
|
||
except Exception as e:
|
||
print(f"{current_time} - ERROR - [CMS] 加载系数配置失败: {str(e)}")
|
||
|
||
# 启动监控进程
|
||
start_monitor_processes()
|
||
|
||
print(f"{current_time} - INFO - Breeze监控线程启动")
|
||
print(f"{current_time} - INFO - CMS监控线程启动")
|
||
|
||
# 返回成功响应,让前端显示欢迎消息
|
||
return jsonify({
|
||
'success': True,
|
||
'staff_name': staff_name,
|
||
'redirect': url_for('index')
|
||
})
|
||
else:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'您的账号 ({staff_id}) 尚未在系统上注册或为不可用状态,无法登录,请联系系统管理员 (od0269@mail.go.com) 协助'
|
||
})
|
||
|
||
return render_template('login.html')
|
||
|
||
@app.route('/logout')
|
||
def logout():
|
||
"""登出"""
|
||
try:
|
||
# 记录登出用户
|
||
username = session.get('username', '未知用户')
|
||
log("用户 [" + username + "] 登出系统")
|
||
|
||
# 清除会话
|
||
session.clear()
|
||
|
||
# 标记系统即将退出
|
||
global shutdown_flag
|
||
shutdown_flag = True
|
||
|
||
# 尝试停止监控进程
|
||
if processes['breeze'] and processes['breeze'].poll() is None:
|
||
try:
|
||
processes['breeze'].terminate()
|
||
log("已停止Breeze监控进程")
|
||
except Exception as e:
|
||
log("停止Breeze监控进程失败: " + str(e))
|
||
|
||
if processes['cms'] and processes['cms'].poll() is None:
|
||
try:
|
||
processes['cms'].terminate()
|
||
log("已停止CMS监控进程")
|
||
except Exception as e:
|
||
log("停止CMS监控进程失败: " + str(e))
|
||
|
||
# 删除共享数据文件
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
data_files = ['breeze_daily.json', 'breeze_hourly.json', 'cms_daily.json', 'cms_hourly.json']
|
||
|
||
for file_name in data_files:
|
||
file_path = os.path.join(script_dir, file_name)
|
||
if os.path.exists(file_path):
|
||
os.remove(file_path)
|
||
log(f"已删除共享数据文件: {file_name}")
|
||
log("成功清理所有共享数据文件")
|
||
except Exception as e:
|
||
log(f"删除共享数据文件时出错: {str(e)}")
|
||
|
||
# 在一个后台线程中等待几秒后退出应用程序
|
||
def shutdown_app():
|
||
time.sleep(3) # 等待3秒
|
||
log("用户登出,系统正在完全关闭...")
|
||
# 关闭所有子进程和当前进程
|
||
if sys.platform.startswith('win'):
|
||
# 在Windows系统下,强制关闭当前进程及所有子进程
|
||
try:
|
||
# 获取当前进程PID
|
||
current_pid = os.getpid()
|
||
# 使用taskkill命令强制结束进程树
|
||
subprocess.run(f'taskkill /F /T /PID {current_pid}', shell=True)
|
||
except Exception as e:
|
||
log(f"关闭进程时出错: {str(e)}")
|
||
else:
|
||
# 非Windows系统使用标准退出方式
|
||
os._exit(0) # 强制关闭整个程序
|
||
|
||
# 启动关闭线程
|
||
threading.Thread(target=shutdown_app).start()
|
||
|
||
# 返回包含关闭倒计时的页面
|
||
return """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>正在关闭系统...</title>
|
||
<style>
|
||
body {
|
||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||
background-color: #f5f5f5;
|
||
text-align: center;
|
||
padding-top: 100px;
|
||
}
|
||
.logout-message {
|
||
background-color: white;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||
padding: 30px;
|
||
max-width: 500px;
|
||
margin: 0 auto;
|
||
}
|
||
h2 {
|
||
color: #1890ff;
|
||
margin-top: 0;
|
||
}
|
||
.spinner {
|
||
border: 4px solid rgba(0, 0, 0, 0.1);
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
border-left-color: #1890ff;
|
||
animation: spin 1s linear infinite;
|
||
margin: 20px auto;
|
||
}
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
.countdown {
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
color: #1890ff;
|
||
margin-top: 15px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="logout-message">
|
||
<h2>正在关闭系统</h2>
|
||
<p>数据清理完毕,系统即将退出,请手动关闭浏览器页面和控制台</p>
|
||
<div class="spinner"></div>
|
||
<div class="countdown" id="countdown">3</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 显示倒计时
|
||
var seconds = 3;
|
||
var countdownElement = document.getElementById('countdown');
|
||
|
||
var interval = setInterval(function() {
|
||
seconds--;
|
||
countdownElement.textContent = seconds;
|
||
|
||
if (seconds <= 0) {
|
||
clearInterval(interval);
|
||
window.close(); // 尝试关闭窗口
|
||
}
|
||
}, 1000);
|
||
</script>
|
||
</body>
|
||
</html>
|
||
"""
|
||
except Exception as e:
|
||
log("Error in logout route: " + str(e))
|
||
return "Logout error. Please check logs.", 500
|
||
|
||
def read_data_file(filename):
|
||
"""读取数据文件"""
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
file_path = os.path.join(script_dir, filename)
|
||
|
||
if not os.path.exists(file_path):
|
||
return None
|
||
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
return data
|
||
except Exception as e:
|
||
log("Failed to read file {0}: {1}".format(filename, str(e)))
|
||
return None
|
||
|
||
def check_version():
|
||
"""检查系统版本并与在线版本比较"""
|
||
global version_check_thread_running
|
||
|
||
try:
|
||
log("开始检查系统版本更新...")
|
||
with version_lock:
|
||
version_status['last_check_time'] = datetime.now()
|
||
|
||
# 确保VERSION.txt文件存在且内容与VERSION一致
|
||
local_version = ensure_version_file()
|
||
log(f"当前本地版本: {local_version}")
|
||
|
||
# 获取在线版本
|
||
online_version = get_online_version()
|
||
log(f"获取到的在线版本: {online_version}")
|
||
|
||
if online_version:
|
||
with version_lock:
|
||
version_status['online_version'] = online_version
|
||
# 比较版本号,只在线上版本比本地版本新时才提示更新
|
||
local_timestamp = int(local_version[1:].split('-')[0]) # 去掉'v'前缀并转换为数字
|
||
online_timestamp = int(online_version[1:].split('-')[0]) # 去掉'v'前缀并转换为数字
|
||
version_status['has_update'] = online_timestamp > local_timestamp
|
||
log(f"版本比较结果 - 在线版本: {online_version}, 本地版本: {local_version}, 是否有更新: {version_status['has_update']}")
|
||
|
||
# 如果有更新且没有正在运行的线程,启动告警线程
|
||
if version_status['has_update'] and not version_check_thread_running:
|
||
version_check_thread_running = True
|
||
threading.Thread(target=notify_version_update).start()
|
||
|
||
return {
|
||
'current_version': VERSION,
|
||
'online_version': online_version,
|
||
'has_update': version_status['has_update'],
|
||
'last_check_time': version_status['last_check_time'].strftime('%Y-%m-%d %H:%M:%S') if version_status['last_check_time'] else None
|
||
}
|
||
else:
|
||
log("无法获取在线版本,返回本地版本信息")
|
||
return {
|
||
'current_version': VERSION,
|
||
'online_version': None,
|
||
'has_update': False,
|
||
'last_check_time': version_status['last_check_time'].strftime('%Y-%m-%d %H:%M:%S') if version_status['last_check_time'] else None
|
||
}
|
||
except Exception as e:
|
||
log(f"版本检查过程中发生错误: {str(e)}")
|
||
return {
|
||
'current_version': VERSION,
|
||
'online_version': None,
|
||
'has_update': False,
|
||
'last_check_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||
'error': str(e)
|
||
}
|
||
finally:
|
||
# 确保线程标志被重置
|
||
if version_check_thread_running:
|
||
version_check_thread_running = False
|
||
|
||
def notify_version_update():
|
||
"""通知用户有新版本可用"""
|
||
try:
|
||
# 确保VERSION.txt文件存在
|
||
ensure_version_file()
|
||
with version_lock:
|
||
online_version = version_status['online_version']
|
||
|
||
if online_version:
|
||
message = f"系统有新版本可用: {online_version},当前版本: {VERSION}"
|
||
log(message)
|
||
|
||
try:
|
||
# 发送桌面通知
|
||
from plyer import notification
|
||
notification.notify(
|
||
title="系统版本更新",
|
||
message=message + "\n点击'检测更新'按钮了解详情",
|
||
app_icon=None,
|
||
timeout=10,
|
||
toast=False
|
||
)
|
||
|
||
# 通过WebSocket通知前端弹出版本检测窗口
|
||
socketio.emit('version_update', {
|
||
'type': 'show_version_dialog',
|
||
'current_version': VERSION,
|
||
'online_version': online_version
|
||
})
|
||
|
||
except Exception as e:
|
||
log(f"显示桌面通知失败: {str(e)}")
|
||
except Exception as e:
|
||
log(f"通知版本更新失败: {str(e)}")
|
||
finally:
|
||
# 重置线程运行标志
|
||
global version_check_thread_running
|
||
version_check_thread_running = False
|
||
|
||
def monitor_version_thread():
|
||
"""版本检测后台线程"""
|
||
log("版本监控线程启动")
|
||
while True:
|
||
try:
|
||
# 每30分钟检查一次版本更新
|
||
check_version()
|
||
time.sleep(1800) # 30分钟
|
||
except Exception as e:
|
||
log(f"版本监控线程异常: {str(e)}")
|
||
time.sleep(600) # 出错后等待10分钟重试
|
||
|
||
@app.route('/api/get-version')
|
||
def get_version():
|
||
"""获取系统版本信息"""
|
||
try:
|
||
# 确保VERSION.txt文件存在
|
||
ensure_version_file()
|
||
|
||
with version_lock:
|
||
status = {
|
||
'current_version': VERSION,
|
||
'local_version': VERSION, # 固定的本地版本号
|
||
'online_version': version_status['online_version'],
|
||
'has_update': version_status['has_update'],
|
||
'last_check_time': version_status['last_check_time'].strftime('%Y-%m-%d %H:%M:%S') if version_status['last_check_time'] else None
|
||
}
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': status
|
||
})
|
||
except Exception as e:
|
||
log("Error in get_version route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
@app.route('/api/check-version')
|
||
def check_version_api():
|
||
"""手动检查版本更新的API"""
|
||
try:
|
||
result = check_version()
|
||
|
||
if result:
|
||
return jsonify({
|
||
'success': True,
|
||
'data': result
|
||
})
|
||
else:
|
||
# 至少返回当前版本信息
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '获取在线版本信息失败',
|
||
'data': {
|
||
'current_version': VERSION,
|
||
'local_version': VERSION,
|
||
'online_version': None,
|
||
'has_update': False,
|
||
'last_check_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
})
|
||
except Exception as e:
|
||
log("Error in check_version_api route: " + str(e))
|
||
return jsonify({'success': False, 'message': f'检测版本更新失败: {str(e)}'}), 500
|
||
|
||
@app.route('/api/get-stats')
|
||
def get_stats():
|
||
"""获取统计数据"""
|
||
try:
|
||
breeze_hourly = read_data_file('breeze_hourly.json')
|
||
breeze_daily = read_data_file('breeze_daily.json')
|
||
cms_hourly = read_data_file('cms_hourly.json')
|
||
cms_daily = read_data_file('cms_daily.json')
|
||
|
||
# 获取时间戳
|
||
breeze_hourly_time = datetime.strptime(breeze_hourly['timestamp'], '%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') if breeze_hourly else ''
|
||
breeze_daily_time = datetime.strptime(breeze_daily['timestamp'], '%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') if breeze_daily else ''
|
||
cms_hourly_time = datetime.strptime(cms_hourly['timestamp'], '%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') if cms_hourly else ''
|
||
cms_daily_time = datetime.strptime(cms_daily['timestamp'], '%Y-%m-%d %H:%M:%S').strftime('%H:%M:%S') if cms_daily else ''
|
||
|
||
# 构建返回数据
|
||
data = {
|
||
'breeze_hourly': {
|
||
'total': breeze_hourly['stats']['total'] if breeze_hourly else 0,
|
||
'weighted_total': round(breeze_hourly['stats']['weighted_total'], 2) if breeze_hourly else 0,
|
||
'categories': breeze_hourly['stats']['categories'] if breeze_hourly else {},
|
||
'timestamp': breeze_hourly_time
|
||
},
|
||
'breeze_daily': {
|
||
'total': breeze_daily['stats']['total'] if breeze_daily else 0,
|
||
'weighted_total': round(breeze_daily['stats']['weighted_total'], 2) if breeze_daily else 0,
|
||
'categories': breeze_daily['stats']['categories'] if breeze_daily else {},
|
||
'timestamp': breeze_daily_time
|
||
},
|
||
'cms_hourly': {
|
||
'stats': cms_hourly['stats']['stats'] if cms_hourly else {'comment': 0, 'feed': 0, 'complaint': 0},
|
||
'weighted_total': round(cms_hourly['stats']['weighted_total'], 2) if cms_hourly else 0,
|
||
'total_count': cms_hourly['stats']['total_count'] if cms_hourly else 0,
|
||
'coefficients': cms_hourly['stats']['coefficients'] if cms_hourly else {'comment': 0.55, 'feed': 1.54, 'complaint': 5.4},
|
||
'timestamp': cms_hourly_time
|
||
},
|
||
'cms_daily': {
|
||
'stats': cms_daily['stats']['stats'] if cms_daily else {'comment': 0, 'feed': 0, 'complaint': 0},
|
||
'weighted_total': round(cms_daily['stats']['weighted_total'], 2) if cms_daily else 0,
|
||
'total_count': cms_daily['stats']['total_count'] if cms_daily else 0,
|
||
'coefficients': cms_daily['stats']['coefficients'] if cms_daily else {'comment': 0.55, 'feed': 1.54, 'complaint': 5.4},
|
||
'timestamp': cms_daily_time
|
||
}
|
||
}
|
||
|
||
# 计算总折算量
|
||
total_weighted = (data['breeze_hourly']['weighted_total'] + data['cms_hourly']['weighted_total'])
|
||
|
||
return jsonify({
|
||
'code': 0,
|
||
'data': data,
|
||
'total_weighted': round(total_weighted, 2)
|
||
})
|
||
except Exception as e:
|
||
log(f"获取统计数据时发生错误: {str(e)}")
|
||
return jsonify({'code': 1, 'message': '获取统计数据失败'})
|
||
|
||
def check_alarm(total_weighted):
|
||
"""检查并触发告警"""
|
||
try:
|
||
with alarm_lock:
|
||
now = datetime.now()
|
||
|
||
# 如果最近30秒内已经告警过,增加计数但不重复告警
|
||
if alarm_status['last_alarm_time'] and (now - alarm_status['last_alarm_time']).total_seconds() < 30:
|
||
return
|
||
|
||
# 设置告警状态
|
||
alarm_status['last_alarm_time'] = now
|
||
alarm_status['alarm_count'] += 1
|
||
alarm_status['is_alarming'] = True
|
||
|
||
# 如果已经达到3次告警,不再继续
|
||
if alarm_status['alarm_count'] > 3:
|
||
return
|
||
|
||
# 启动线程播放告警声音和显示通知
|
||
# 将当前加权总值传递给函数
|
||
threading.Thread(target=lambda: show_alarm_notification(total_weighted)).start()
|
||
log("Alarm triggered: Current weighted total {0:.2f}, exceeded threshold {1}".format(total_weighted, ALARM_THRESHOLD))
|
||
except Exception as e:
|
||
log("Error in check_alarm: " + str(e))
|
||
|
||
def show_alarm_notification(total_weighted, is_test=False, alarm_type="实时告警"):
|
||
"""显示告警通知和播放告警声音"""
|
||
try:
|
||
# 播放系统告警声音
|
||
try:
|
||
import winsound
|
||
winsound.PlaySound("SystemExclamation", winsound.SND_ALIAS)
|
||
except:
|
||
# 如果winsound不可用,尝试使用beep
|
||
print('\a')
|
||
|
||
# 添加桌面通知
|
||
try:
|
||
from plyer import notification
|
||
|
||
# 计算超出阈值的百分比
|
||
over_percentage = ((total_weighted - ALARM_THRESHOLD) / ALARM_THRESHOLD) * 100
|
||
|
||
# 根据是否为测试调整标题
|
||
title = "测试告警" if is_test else "审核数量告警"
|
||
|
||
# 根据是否为测试调整消息前缀
|
||
prefix = "[测试数据] " if is_test else ""
|
||
|
||
notification.notify(
|
||
title=title,
|
||
message=f"{prefix}{alarm_type}当前小时加权总计:{total_weighted:.2f}\n阈值:{ALARM_THRESHOLD}\n超出:{over_percentage:.1f}%\n请立即检查数据监控看板!",
|
||
app_icon=None,
|
||
timeout=15,
|
||
toast=False
|
||
)
|
||
except Exception as e:
|
||
log("Failed to show desktop notification: " + str(e))
|
||
|
||
except Exception as e:
|
||
log("Failed to play alarm sound: " + str(e))
|
||
|
||
@app.route('/api/get-alarm-status')
|
||
def get_alarm_status():
|
||
"""获取告警状态"""
|
||
try:
|
||
with alarm_lock:
|
||
status = {
|
||
'is_alarming': alarm_status['is_alarming'],
|
||
'alarm_count': alarm_status['alarm_count'],
|
||
'last_alarm_time': alarm_status['last_alarm_time'].strftime('%Y-%m-%d %H:%M:%S') if alarm_status['last_alarm_time'] else None,
|
||
'threshold': ALARM_THRESHOLD,
|
||
'alarm_type': alarm_status['alarm_type']
|
||
}
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': status
|
||
})
|
||
except Exception as e:
|
||
log("Error in get_alarm_status route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
@app.route('/api/reset-alarm')
|
||
def reset_alarm():
|
||
"""重置告警状态"""
|
||
try:
|
||
with alarm_lock:
|
||
alarm_status['is_alarming'] = False
|
||
alarm_status['alarm_count'] = 0
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Alarm has been reset'
|
||
})
|
||
except Exception as e:
|
||
log("Error in reset_alarm route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
@app.route('/api/restart-monitoring')
|
||
def restart_monitoring():
|
||
"""重启监控进程"""
|
||
try:
|
||
if 'logged_in' not in session:
|
||
return jsonify({'success': False, 'message': 'Not logged in'})
|
||
|
||
log("正在尝试重启监控进程...")
|
||
|
||
# 停止现有进程
|
||
if processes['breeze'] and processes['breeze'].poll() is None:
|
||
try:
|
||
processes['breeze'].terminate()
|
||
log("Breeze监控进程已停止")
|
||
except Exception as e:
|
||
log(f"停止Breeze监控进程出错: {str(e)}")
|
||
|
||
if processes['cms'] and processes['cms'].poll() is None:
|
||
try:
|
||
processes['cms'].terminate()
|
||
log("CMS监控进程已停止")
|
||
except Exception as e:
|
||
log(f"停止CMS监控进程出错: {str(e)}")
|
||
|
||
# 等待进程完全终止
|
||
time.sleep(2)
|
||
|
||
# 获取Python可执行文件路径
|
||
python_executable = sys.executable
|
||
log(f"使用Python可执行文件: {python_executable}")
|
||
|
||
# 重新启动
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# 获取会话中的凭据
|
||
breeze_cookie = session.get('breeze_cookie', '')
|
||
cms_cookie = session.get('cms_cookie', '')
|
||
username = session.get('username', '')
|
||
|
||
# 启动breeze监控
|
||
breeze_script = os.path.join(script_dir, "breeze_monitor.py")
|
||
env = os.environ.copy()
|
||
env['BREEZE_COOKIE'] = breeze_cookie
|
||
env['BREEZE_USERNAME'] = username
|
||
|
||
processes['breeze'] = subprocess.Popen(
|
||
[python_executable, breeze_script],
|
||
env=env,
|
||
shell=False
|
||
)
|
||
log(f"Breeze监控进程已启动, PID: {processes['breeze'].pid}")
|
||
|
||
# 启动cms监控
|
||
cms_script = os.path.join(script_dir, "cms_monitor.py")
|
||
env = os.environ.copy()
|
||
env['CMS_COOKIE'] = cms_cookie
|
||
env['CMS_USERNAME'] = username
|
||
|
||
processes['cms'] = subprocess.Popen(
|
||
[python_executable, cms_script],
|
||
env=env,
|
||
shell=False
|
||
)
|
||
log(f"CMS监控进程已启动, PID: {processes['cms'].pid}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '监控进程已成功重启'
|
||
})
|
||
except Exception as e:
|
||
log(f"重启监控进程时出错: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f"重启监控进程失败: {str(e)}"
|
||
}), 500
|
||
except Exception as e:
|
||
log(f"重启监控进程时出错: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f"重启监控进程失败: {str(e)}"
|
||
}), 500
|
||
|
||
def open_browser():
|
||
"""在新线程中打开浏览器"""
|
||
time.sleep(1) # 等待服务器启动
|
||
try:
|
||
webbrowser.open('http://localhost:8000')
|
||
except Exception as e:
|
||
log("Failed to open browser: " + str(e))
|
||
|
||
@app.route('/api/check-now')
|
||
def check_now():
|
||
"""立即检查当前小时数据"""
|
||
try:
|
||
if 'logged_in' not in session:
|
||
return jsonify({'success': False, 'message': 'Not logged in'})
|
||
|
||
# 获取Python可执行文件路径
|
||
python_executable = sys.executable
|
||
|
||
# 向子进程发送检查请求
|
||
if processes['breeze'] and processes['breeze'].poll() is None:
|
||
# 获取会话中的凭据
|
||
breeze_cookie = session.get('breeze_cookie', '')
|
||
username = session.get('username', '')
|
||
|
||
# 创建临时环境
|
||
env = os.environ.copy()
|
||
env['BREEZE_COOKIE'] = breeze_cookie
|
||
env['BREEZE_USERNAME'] = username
|
||
|
||
# 启动一个临时进程来执行检查,添加--force参数强制使用整点时间
|
||
# 添加--no-config-check参数防止频繁检查配置文件
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
subprocess.Popen(
|
||
[python_executable, os.path.join(script_dir, "breeze_monitor.py"), "--check-now", "--force", "--no-config-check"],
|
||
env=env,
|
||
shell=False
|
||
)
|
||
log("开始执行Breeze系统手动检查")
|
||
|
||
if processes['cms'] and processes['cms'].poll() is None:
|
||
# 获取会话中的凭据
|
||
cms_cookie = session.get('cms_cookie', '')
|
||
username = session.get('username', '')
|
||
|
||
# 创建临时环境
|
||
env = os.environ.copy()
|
||
env['CMS_COOKIE'] = cms_cookie
|
||
env['CMS_USERNAME'] = username
|
||
|
||
# 启动一个临时进程来执行检查,添加--force参数强制使用整点时间
|
||
# 添加--no-config-check参数防止频繁检查配置文件
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
subprocess.Popen(
|
||
[python_executable, os.path.join(script_dir, "cms_monitor.py"), "--check-now", "--force", "--no-config-check"],
|
||
env=env,
|
||
shell=False
|
||
)
|
||
log("开始执行CMS系统手动检查")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '已启动数据检查',
|
||
'threshold': ALARM_THRESHOLD
|
||
})
|
||
except Exception as e:
|
||
log(f"立即检查功能出错: {str(e)}")
|
||
return jsonify({'success': False, 'message': f'数据检查启动失败: {str(e)}'}), 500
|
||
|
||
@app.route('/api/test-alarm')
|
||
def test_alarm():
|
||
"""测试告警功能"""
|
||
try:
|
||
if 'logged_in' not in session:
|
||
return jsonify({'success': False, 'message': 'Not logged in'})
|
||
|
||
# 检查是否为实际数据告警
|
||
is_real_data = request.args.get('real_data', 'false').lower() == 'true'
|
||
|
||
# 根据请求类型选择不同的消息
|
||
message = "实时告警" if is_real_data else "测试告警"
|
||
|
||
# 立即触发第一次告警
|
||
if not is_real_data:
|
||
# 手动触发告警并传递测试告警值
|
||
show_alarm_notification(ALARM_THRESHOLD + 100, is_test=True, alarm_type="测试告警")
|
||
else:
|
||
# 从API获取当前真实的小时总量
|
||
try:
|
||
breeze_hourly = read_data_file('breeze_hourly.json') or {'stats': {'weighted_total': 0}}
|
||
cms_hourly = read_data_file('cms_hourly.json') or {'stats': {'weighted_total': 0}}
|
||
|
||
total_hourly = 0
|
||
if 'stats' in breeze_hourly and 'weighted_total' in breeze_hourly['stats']:
|
||
total_hourly += breeze_hourly['stats']['weighted_total']
|
||
if 'stats' in cms_hourly and 'weighted_total' in cms_hourly['stats']:
|
||
total_hourly += cms_hourly['stats']['weighted_total']
|
||
|
||
# 只有在实际超过阈值时才显示通知
|
||
if total_hourly >= ALARM_THRESHOLD:
|
||
show_alarm_notification(total_hourly, is_test=False, alarm_type="实时告警")
|
||
except Exception as e:
|
||
log(f"Error getting real data for alarm: {str(e)}")
|
||
|
||
# 设置告警状态,启用连续告警
|
||
with alarm_lock:
|
||
now = datetime.now()
|
||
alarm_status['last_alarm_time'] = now
|
||
alarm_status['is_alarming'] = True
|
||
alarm_status['alarm_type'] = message # 保存告警类型
|
||
|
||
# 设置告警计数为1,开始计数
|
||
alarm_status['alarm_count'] = 1
|
||
|
||
# 创建后台线程发送连续告警
|
||
if not is_real_data: # 仅对测试告警启用连续通知
|
||
threading.Thread(target=lambda: send_sequential_test_alarms("测试告警")).start()
|
||
|
||
log(f"{message} triggered by user")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': f'{message} successful'
|
||
})
|
||
except Exception as e:
|
||
log("Error in test_alarm route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Failed to trigger alarm: ' + str(e)}), 500
|
||
|
||
@app.route('/api/get-coefficients')
|
||
def get_coefficients():
|
||
"""获取CMS系数配置"""
|
||
try:
|
||
if 'logged_in' not in session:
|
||
return jsonify({'success': False, 'message': 'Not logged in'})
|
||
|
||
# 从CMS系数配置文件读取
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
coefficients_file = os.path.join(script_dir, 'cms_coefficients.json')
|
||
|
||
if not os.path.exists(coefficients_file):
|
||
# 如果配置文件不存在,返回默认系数
|
||
default_coefficients = {
|
||
'comment': 0.55,
|
||
'feed': 1.54,
|
||
'complaint': 5.4
|
||
}
|
||
return jsonify({
|
||
'success': True,
|
||
'data': default_coefficients,
|
||
'message': 'Using default coefficients'
|
||
})
|
||
|
||
with open(coefficients_file, 'r', encoding='utf-8') as f:
|
||
coefficients = json.load(f)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': coefficients
|
||
})
|
||
except Exception as e:
|
||
log(f"Error reading coefficients file: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Error reading coefficients: {str(e)}'
|
||
})
|
||
|
||
except Exception as e:
|
||
log("Error in get_coefficients route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
@app.route('/api/update-coefficients', methods=['POST'])
|
||
def update_coefficients():
|
||
"""更新CMS系数配置"""
|
||
try:
|
||
if 'logged_in' not in session:
|
||
return jsonify({'success': False, 'message': 'Not logged in'})
|
||
|
||
# 获取请求中的系数
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'No data provided'
|
||
})
|
||
|
||
# 验证系数格式
|
||
required_keys = ['comment', 'feed', 'complaint']
|
||
for key in required_keys:
|
||
if key not in data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Missing required key: {key}'
|
||
})
|
||
|
||
# 验证值是数字
|
||
try:
|
||
data[key] = float(data[key])
|
||
except ValueError:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Value for {key} must be a number'
|
||
})
|
||
|
||
# 确保只有必要的键
|
||
coefficients = {
|
||
'comment': data['comment'],
|
||
'feed': data['feed'],
|
||
'complaint': data['complaint']
|
||
}
|
||
|
||
# 保存到配置文件
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
coefficients_file = os.path.join(script_dir, 'cms_coefficients.json')
|
||
|
||
with open(coefficients_file, 'w', encoding='utf-8') as f:
|
||
json.dump(coefficients, f, indent=4, ensure_ascii=False)
|
||
|
||
log(f"CMS系数已更新: 评论={coefficients['comment']}, 动态={coefficients['feed']}, 举报={coefficients['complaint']}")
|
||
|
||
# 使用命令行参数方式通知CMS监控进程更新系数
|
||
try:
|
||
# 获取Python可执行文件路径
|
||
python_executable = sys.executable
|
||
|
||
# 获取CMS监控脚本路径
|
||
cms_script = os.path.join(script_dir, "cms_monitor.py")
|
||
|
||
# 获取环境变量
|
||
env = os.environ.copy()
|
||
env['CMS_COOKIE'] = session.get('cms_cookie', '')
|
||
env['CMS_USERNAME'] = session.get('username', '')
|
||
|
||
# 启动进程更新系数
|
||
cmd = [
|
||
python_executable,
|
||
cms_script,
|
||
"--update-coefficients",
|
||
str(coefficients['comment']),
|
||
str(coefficients['feed']),
|
||
str(coefficients['complaint'])
|
||
]
|
||
|
||
subprocess.Popen(
|
||
cmd,
|
||
env=env,
|
||
shell=False
|
||
)
|
||
|
||
log("已发送系数更新命令给CMS监控进程")
|
||
except Exception as e:
|
||
log(f"发送系数更新命令失败: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Coefficients updated successfully',
|
||
'data': coefficients
|
||
})
|
||
|
||
except Exception as e:
|
||
log(f"更新系数失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Error updating coefficients: {str(e)}'
|
||
})
|
||
|
||
except Exception as e:
|
||
log("Error in update_coefficients route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
def send_sequential_test_alarms(alarm_type):
|
||
"""发送连续的测试告警通知"""
|
||
try:
|
||
# 等待10秒后发送第二次告警
|
||
time.sleep(10)
|
||
|
||
with alarm_lock:
|
||
if alarm_status['is_alarming'] and alarm_status['alarm_count'] < 3:
|
||
alarm_status['alarm_count'] = 2
|
||
# 发送第二次告警
|
||
show_alarm_notification(ALARM_THRESHOLD + 120, is_test=True, alarm_type=alarm_type)
|
||
log("Second test alarm notification sent")
|
||
|
||
# 再等待10秒后发送第三次告警
|
||
time.sleep(10)
|
||
|
||
with alarm_lock:
|
||
if alarm_status['is_alarming'] and alarm_status['alarm_count'] < 3:
|
||
alarm_status['alarm_count'] = 3
|
||
# 发送第三次告警
|
||
show_alarm_notification(ALARM_THRESHOLD + 150, is_test=True, alarm_type=alarm_type)
|
||
log("Third test alarm notification sent")
|
||
except Exception as e:
|
||
log(f"Error in sequential test alarms: {str(e)}")
|
||
|
||
@app.route('/api/get-breeze-coefficients')
|
||
def get_breeze_coefficients():
|
||
"""获取Breeze系数配置"""
|
||
try:
|
||
if 'logged_in' not in session:
|
||
return jsonify({'success': False, 'message': 'Not logged in'})
|
||
|
||
# 从Breeze系数配置文件读取
|
||
try:
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
coefficients_file = os.path.join(script_dir, 'breeze_coefficients.json')
|
||
|
||
if not os.path.exists(coefficients_file):
|
||
# 如果配置文件不存在,返回错误信息
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Breeze coefficients file not found'
|
||
})
|
||
|
||
with open(coefficients_file, 'r', encoding='utf-8') as f:
|
||
coefficients = json.load(f)
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': coefficients
|
||
})
|
||
except Exception as e:
|
||
log(f"Error reading Breeze coefficients file: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Error reading coefficients: {str(e)}'
|
||
})
|
||
|
||
except Exception as e:
|
||
log("Error in get_breeze_coefficients route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
@app.route('/api/update-breeze-coefficients', methods=['POST'])
|
||
def update_breeze_coefficients():
|
||
"""更新Breeze系数配置"""
|
||
try:
|
||
if 'logged_in' not in session:
|
||
return jsonify({'success': False, 'message': 'Not logged in'})
|
||
|
||
# 获取请求中的系数
|
||
try:
|
||
data = request.get_json()
|
||
if not data:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'No data provided'
|
||
})
|
||
|
||
# 验证系数格式
|
||
if not isinstance(data, dict):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'Invalid data format, expected a dictionary'
|
||
})
|
||
|
||
# 验证所有值是数字
|
||
for key, value in data.items():
|
||
try:
|
||
data[key] = float(value)
|
||
except ValueError:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Value for {key} must be a number'
|
||
})
|
||
|
||
# 保存到配置文件
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
coefficients_file = os.path.join(script_dir, 'breeze_coefficients.json')
|
||
|
||
with open(coefficients_file, 'w', encoding='utf-8') as f:
|
||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||
|
||
log(f"Breeze系数已更新")
|
||
|
||
# 使用命令行参数方式通知Breeze监控进程更新系数
|
||
try:
|
||
# 获取Python可执行文件路径
|
||
python_executable = sys.executable
|
||
|
||
# 获取Breeze监控脚本路径
|
||
breeze_script = os.path.join(script_dir, "breeze_monitor.py")
|
||
|
||
# 获取环境变量
|
||
env = os.environ.copy()
|
||
env['BREEZE_COOKIE'] = session.get('breeze_cookie', '')
|
||
env['BREEZE_USERNAME'] = session.get('username', '')
|
||
|
||
# 启动进程更新系数 - 一次只更新一个系数
|
||
for coefficient_type, coefficient_value in data.items():
|
||
cmd = [
|
||
python_executable,
|
||
breeze_script,
|
||
"--update-coefficients",
|
||
coefficient_type,
|
||
str(coefficient_value)
|
||
]
|
||
|
||
subprocess.Popen(
|
||
cmd,
|
||
env=env,
|
||
shell=False
|
||
)
|
||
|
||
log("已发送系数更新命令给Breeze监控进程")
|
||
except Exception as e:
|
||
log(f"发送系数更新命令失败: {str(e)}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Breeze coefficients updated successfully',
|
||
'data': data
|
||
})
|
||
|
||
except Exception as e:
|
||
log(f"更新Breeze系数失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'Error updating coefficients: {str(e)}'
|
||
})
|
||
|
||
except Exception as e:
|
||
log("Error in update_breeze_coefficients route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
@app.route('/api/acknowledge-alarm')
|
||
def acknowledge_alarm():
|
||
"""确认并重置告警状态"""
|
||
try:
|
||
with alarm_lock:
|
||
# 完全重置告警状态
|
||
alarm_status['is_alarming'] = False
|
||
alarm_status['alarm_count'] = 0
|
||
alarm_status['last_alarm_time'] = None
|
||
|
||
# 记录告警已被确认
|
||
log("告警已被用户确认并重置")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'Alarm has been acknowledged and reset'
|
||
})
|
||
except Exception as e:
|
||
log("Error in acknowledge_alarm route: " + str(e))
|
||
return jsonify({'success': False, 'message': 'Internal server error'}), 500
|
||
|
||
def is_port_available(port):
|
||
"""检查端口是否可用"""
|
||
try:
|
||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||
s.settimeout(1)
|
||
result = s.connect_ex(('127.0.0.1', port))
|
||
return result != 0
|
||
except Exception as e:
|
||
log(f"检查端口可用性出错: {str(e)}")
|
||
return False
|
||
|
||
def free_port(port):
|
||
"""尝试释放被占用的端口"""
|
||
try:
|
||
if sys.platform.startswith('win'):
|
||
# Windows系统
|
||
os.system(f'for /f "tokens=5" %a in (\'netstat -ano ^| findstr :{port}\') do taskkill /f /pid %a')
|
||
return True
|
||
else:
|
||
# Linux/Mac系统
|
||
os.system(f"kill $(lsof -t -i:{port})")
|
||
return True
|
||
except Exception as e:
|
||
log(f"释放端口时出错: {str(e)}")
|
||
return False
|
||
|
||
def find_available_port(start_port, end_port):
|
||
"""查找可用端口"""
|
||
for port in range(start_port, end_port + 1):
|
||
if is_port_available(port):
|
||
return port
|
||
return None
|
||
|
||
@app.route('/api/check-cms-daily', methods=['POST'])
|
||
def check_cms_daily_data():
|
||
"""立即检查CMS每日数据"""
|
||
if 'user' not in session:
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '请先登录系统'
|
||
})
|
||
|
||
try:
|
||
# 准备环境变量
|
||
env = os.environ.copy()
|
||
env['CMS_COOKIE'] = session.get('cms_cookie', '')
|
||
env['CMS_USERNAME'] = session.get('username', '')
|
||
|
||
# 设置--check-daily参数运行CMS监控脚本
|
||
cmd = [sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'cms_monitor.py'), '--check-daily']
|
||
|
||
# 运行子进程
|
||
process = subprocess.Popen(
|
||
cmd,
|
||
env=env,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE
|
||
)
|
||
|
||
# 等待进程完成
|
||
stdout, stderr = process.communicate(timeout=60)
|
||
|
||
if process.returncode == 0:
|
||
app.logger.info('成功执行CMS每日数据检查')
|
||
return jsonify({
|
||
'success': True,
|
||
'message': 'CMS每日数据检查已执行'
|
||
})
|
||
else:
|
||
app.logger.error(f'CMS每日数据检查失败: {stderr.decode("utf-8", errors="ignore")}')
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'CMS每日数据检查失败: {stderr.decode("utf-8", errors="ignore")}'
|
||
})
|
||
|
||
except subprocess.TimeoutExpired:
|
||
app.logger.error('CMS每日数据检查超时')
|
||
return jsonify({
|
||
'success': False,
|
||
'message': 'CMS每日数据检查超时,请检查系统日志'
|
||
})
|
||
except Exception as e:
|
||
app.logger.error(f'执行CMS每日数据检查时发生错误: {str(e)}')
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'执行CMS每日数据检查时发生错误: {str(e)}'
|
||
})
|
||
|
||
@app.route('/api/check-version-service')
|
||
def check_version_service():
|
||
"""检查版本检测服务的可用性"""
|
||
try:
|
||
log("正在检查版本检测服务可用性...")
|
||
|
||
# 测试连接
|
||
try:
|
||
response = requests.get(VERSION_CHECK_URL, timeout=5)
|
||
log(f"版本检测服务响应状态码: {response.status_code}")
|
||
|
||
service_status = {
|
||
'url': VERSION_CHECK_URL,
|
||
'is_available': response.status_code == 200,
|
||
'status_code': response.status_code,
|
||
'response_time': response.elapsed.total_seconds(),
|
||
'content_length': len(response.content) if response.status_code == 200 else 0,
|
||
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
|
||
if response.status_code == 200:
|
||
service_status['content'] = response.text.strip()
|
||
log(f"版本检测服务正常,返回内容: {service_status['content']}")
|
||
else:
|
||
log(f"版本检测服务异常,HTTP状态码: {response.status_code}")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'data': service_status
|
||
})
|
||
|
||
except requests.exceptions.Timeout:
|
||
log("版本检测服务连接超时")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '版本检测服务连接超时',
|
||
'data': {
|
||
'url': VERSION_CHECK_URL,
|
||
'is_available': False,
|
||
'error': 'timeout',
|
||
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
})
|
||
except requests.exceptions.ConnectionError:
|
||
log("无法连接到版本检测服务")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '无法连接到版本检测服务',
|
||
'data': {
|
||
'url': VERSION_CHECK_URL,
|
||
'is_available': False,
|
||
'error': 'connection_error',
|
||
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
})
|
||
except Exception as e:
|
||
log(f"检查版本检测服务时发生未知错误: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'检查版本检测服务时发生错误: {str(e)}',
|
||
'data': {
|
||
'url': VERSION_CHECK_URL,
|
||
'is_available': False,
|
||
'error': 'unknown',
|
||
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
log(f"版本检测服务检查接口发生错误: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '版本检测服务检查失败',
|
||
'error': str(e)
|
||
}), 500
|
||
|
||
@app.route('/api/update-system', methods=['POST'])
|
||
def update_system():
|
||
"""处理系统更新请求"""
|
||
try:
|
||
# 获取桌面路径和快捷方式路径
|
||
desktop_path = os.path.join(os.path.expanduser('~'), 'Desktop')
|
||
temp_dir = 'monitor_temp'
|
||
temp_path = os.path.join(desktop_path, temp_dir)
|
||
shortcut_path = os.path.join(temp_path, '网易大神审核数据监控看板一键安装.lnk')
|
||
|
||
# 下载快捷方式
|
||
shortcut_url = 'http://cos.ui-beam.com/work_scripts/monitor/%E7%BD%91%E6%98%93%E5%A4%A7%E7%A5%9E%E5%AE%A1%E6%A0%B8%E6%95%B0%E6%8D%AE%E7%9B%91%E6%8E%A7%E7%9C%8B%E6%9D%BF%E4%B8%80%E9%94%AE%E5%AE%89%E8%A3%85.lnk'
|
||
|
||
log(f"开始下载更新文件: {shortcut_url}")
|
||
|
||
# 获取系统代理设置
|
||
proxies = {
|
||
'http': os.environ.get('HTTP_PROXY', ''),
|
||
'https': os.environ.get('HTTPS_PROXY', '')
|
||
}
|
||
|
||
# 首先尝试不使用代理直接下载
|
||
try:
|
||
log("尝试直接下载更新文件...")
|
||
response = requests.get(shortcut_url, timeout=30, proxies={'http': None, 'https': None}, verify=False)
|
||
if response.status_code == 200:
|
||
with open(shortcut_path, 'wb') as f:
|
||
f.write(response.content)
|
||
log("更新文件下载成功")
|
||
else:
|
||
raise requests.exceptions.RequestException(f"下载失败,HTTP状态码: {response.status_code}")
|
||
except (requests.exceptions.RequestException, IOError) as e:
|
||
log(f"直接下载失败: {str(e)},尝试使用系统代理...")
|
||
|
||
# 如果直接下载失败,尝试使用系统代理
|
||
try:
|
||
response = requests.get(shortcut_url, timeout=30, proxies=proxies, verify=False)
|
||
if response.status_code == 200:
|
||
with open(shortcut_path, 'wb') as f:
|
||
f.write(response.content)
|
||
log("使用代理下载更新文件成功")
|
||
elif response.status_code == 407:
|
||
log("代理需要认证,尝试最后一次直接下载...")
|
||
# 最后一次尝试直接下载
|
||
response = requests.get(shortcut_url, timeout=30, proxies={'http': None, 'https': None}, verify=False)
|
||
if response.status_code == 200:
|
||
with open(shortcut_path, 'wb') as f:
|
||
f.write(response.content)
|
||
log("最后尝试下载成功")
|
||
else:
|
||
raise requests.exceptions.RequestException(f"所有下载尝试都失败,最后状态码: {response.status_code}")
|
||
else:
|
||
raise requests.exceptions.RequestException(f"使用代理下载失败,HTTP状态码: {response.status_code}")
|
||
except Exception as proxy_error:
|
||
log(f"使用代理下载失败: {str(proxy_error)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'下载更新文件失败: {str(proxy_error)}'
|
||
}), 500
|
||
|
||
# 检查文件是否成功下载
|
||
if not os.path.exists(shortcut_path):
|
||
return jsonify({
|
||
'success': False,
|
||
'message': '更新文件下载成功但未找到文件'
|
||
}), 500
|
||
|
||
# 启动快捷方式
|
||
try:
|
||
log("正在启动更新程序...")
|
||
subprocess.Popen(['cmd', '/c', 'start', '', shortcut_path], shell=True)
|
||
log("更新程序启动成功")
|
||
|
||
return jsonify({
|
||
'success': True,
|
||
'message': '更新程序已启动'
|
||
})
|
||
except Exception as e:
|
||
log(f"启动更新程序失败: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'启动更新程序失败: {str(e)}'
|
||
}), 500
|
||
|
||
except Exception as e:
|
||
log(f"系统更新过程中发生错误: {str(e)}")
|
||
return jsonify({
|
||
'success': False,
|
||
'message': f'系统更新失败: {str(e)}'
|
||
}), 500
|
||
|
||
def main():
|
||
"""主函数"""
|
||
try:
|
||
print("\n" + "="*50)
|
||
print("网易大神审核数据监控服务启动中...")
|
||
print("="*50)
|
||
print("\n[系统状态]")
|
||
print("1. 正在初始化监控服务...")
|
||
|
||
# 初始化版本文件
|
||
timestamp_version = ensure_version_file()
|
||
print(f"2. 当前系统版本: {VERSION}")
|
||
|
||
# 检查并安装依赖
|
||
print("3. 正在检查系统依赖...")
|
||
check_and_install_dependencies()
|
||
|
||
# 启动版本检测线程
|
||
print("4. 启动版本检测服务...")
|
||
version_thread = threading.Thread(target=monitor_version_thread)
|
||
version_thread.daemon = True
|
||
version_thread.start()
|
||
|
||
# 获取端口
|
||
port = 8000
|
||
|
||
# 检查端口是否被占用
|
||
if not is_port_available(port):
|
||
print(f"5. 端口 {port} 已被占用,尝试释放...")
|
||
free_port(port)
|
||
|
||
# 再次检查端口
|
||
if not is_port_available(port):
|
||
port = find_available_port(8001, 8100)
|
||
print(f" - 使用替代端口 {port}")
|
||
else:
|
||
print(f"5. 端口 {port} 可用")
|
||
|
||
print("\n[启动完成]")
|
||
print(f"* 监控系统已启动,请打开浏览器访问: http://localhost:{port}")
|
||
print("* 等待用户登录...")
|
||
print("\n" + "!"*50)
|
||
print("! 警告:请勿关闭此窗口 !")
|
||
print("! 此窗口是系统监控的核心进程 !")
|
||
print("! 一旦关闭,所有监控服务将停止工作 !")
|
||
print("!"*50 + "\n")
|
||
|
||
# 在新线程中启动浏览器
|
||
threading.Thread(target=open_browser).start()
|
||
|
||
try:
|
||
# 使用 SocketIO 启动服务器
|
||
socketio.run(app, debug=False, host='0.0.0.0', port=port)
|
||
except Exception as e:
|
||
log(f"启动服务器出错: {str(e)}")
|
||
finally:
|
||
# 关闭所有子进程
|
||
for name, process in processes.items():
|
||
if process and process.poll() is None:
|
||
try:
|
||
process.terminate()
|
||
log(f"已关闭 {name} 监控进程")
|
||
except Exception as e:
|
||
log(f"关闭 {name} 监控进程失败: {str(e)}")
|
||
|
||
# 清理临时文件
|
||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||
data_files = ['breeze_daily.json', 'breeze_hourly.json', 'cms_daily.json', 'cms_hourly.json']
|
||
|
||
for file_name in data_files:
|
||
file_path = os.path.join(script_dir, file_name)
|
||
if os.path.exists(file_path):
|
||
try:
|
||
os.remove(file_path)
|
||
log(f"已删除临时文件: {file_name}")
|
||
except Exception as e:
|
||
log(f"删除临时文件 {file_name} 失败: {str(e)}")
|
||
|
||
log("监控系统已关闭")
|
||
except Exception as e:
|
||
log("Error in main function: " + str(e))
|
||
|
||
# 信号处理函数
|
||
def signal_handler(sig, frame):
|
||
"""处理信号,用于优雅退出"""
|
||
log("收到退出信号,正在关闭系统...")
|
||
global shutdown_flag
|
||
shutdown_flag = True
|
||
# 通过os._exit强制终止进程
|
||
os._exit(0)
|
||
|
||
if __name__ == "__main__":
|
||
# 注册信号处理器
|
||
signal.signal(signal.SIGINT, signal_handler)
|
||
signal.signal(signal.SIGTERM, signal_handler)
|
||
|
||
# 执行主函数
|
||
main() |