系统设置增加中控企业微信告警,后台巡检离线/故障并冷却推送。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
|
||||
@@ -34,7 +36,8 @@ from lib.bridge_client_lib import (
|
||||
pull_all_enabled,
|
||||
)
|
||||
from lib.db_lib import init_db
|
||||
from lib.env_lib import apply_env_updates
|
||||
from lib.env_lib import apply_env_updates, env_get
|
||||
from lib.health_watch_lib import inspect_overview_rows
|
||||
from lib.instances_lib import (
|
||||
create_instance,
|
||||
delete_instance,
|
||||
@@ -43,6 +46,13 @@ from lib.instances_lib import (
|
||||
update_instance,
|
||||
)
|
||||
from lib.sso_lib import build_embed_open_url
|
||||
from lib.wechat_lib import (
|
||||
hub_alert_cooldown_sec,
|
||||
hub_health_fail_threshold,
|
||||
hub_wechat_enabled,
|
||||
hub_wechat_webhook,
|
||||
send_hub_wechat,
|
||||
)
|
||||
|
||||
APP_HOST = (os.getenv("APP_HOST") or "0.0.0.0").strip() or "0.0.0.0"
|
||||
APP_PORT = int(os.getenv("APP_PORT") or 5100)
|
||||
@@ -155,48 +165,120 @@ def instances_page():
|
||||
return render_template("instances.html", instances=list_instances())
|
||||
|
||||
|
||||
def _wechat_settings_view() -> dict:
|
||||
enabled_raw = (env_get(ENV_PATH, "HUB_WECHAT_ENABLED") or os.getenv("HUB_WECHAT_ENABLED") or "").strip().lower()
|
||||
if enabled_raw in ("0", "false", "no", "off"):
|
||||
enabled = False
|
||||
elif enabled_raw in ("1", "true", "yes", "on"):
|
||||
enabled = True
|
||||
else:
|
||||
enabled = bool(hub_wechat_webhook())
|
||||
return {
|
||||
"enabled": enabled,
|
||||
"webhook": hub_wechat_webhook() or env_get(ENV_PATH, "HUB_WECHAT_WEBHOOK", ""),
|
||||
"cooldown_sec": hub_alert_cooldown_sec(),
|
||||
"fail_threshold": hub_health_fail_threshold(),
|
||||
}
|
||||
|
||||
|
||||
def _apply_runtime_env(updates: dict[str, str]) -> None:
|
||||
for key, val in updates.items():
|
||||
os.environ[key] = str(val)
|
||||
|
||||
|
||||
def _handle_account_settings() -> None:
|
||||
current_pw = request.form.get("current_password") or ""
|
||||
new_username = (request.form.get("new_username") or "").strip()
|
||||
new_password = request.form.get("new_password") or ""
|
||||
confirm = request.form.get("confirm_password") or ""
|
||||
|
||||
if current_pw != hub_password():
|
||||
flash("当前密码错误")
|
||||
return
|
||||
if len(new_username) < 2 or len(new_username) > 64:
|
||||
flash("用户名长度需为 2–64")
|
||||
return
|
||||
if any(ch.isspace() for ch in new_username):
|
||||
flash("用户名不能包含空格")
|
||||
return
|
||||
|
||||
updates: dict[str, str] = {"HUB_USERNAME": new_username}
|
||||
final_password = hub_password()
|
||||
if new_password or confirm:
|
||||
if len(new_password) < 6:
|
||||
flash("新密码至少 6 位")
|
||||
return
|
||||
if new_password != confirm:
|
||||
flash("两次输入的新密码不一致")
|
||||
return
|
||||
updates["HUB_PASSWORD"] = new_password
|
||||
final_password = new_password
|
||||
|
||||
try:
|
||||
apply_env_updates(ENV_PATH, updates)
|
||||
except OSError as exc:
|
||||
flash(f"写入 .env 失败: {exc}")
|
||||
return
|
||||
|
||||
set_hub_credentials(new_username, final_password)
|
||||
flash("账号已更新,立即生效")
|
||||
|
||||
|
||||
def _handle_wechat_settings(*, send_test: bool = False) -> None:
|
||||
enabled = "1" if (request.form.get("wechat_enabled") or "") in ("1", "on", "true") else "0"
|
||||
webhook = (request.form.get("wechat_webhook") or "").strip()
|
||||
try:
|
||||
cooldown = max(60, int(float(request.form.get("alert_cooldown_sec") or 600)))
|
||||
except (TypeError, ValueError):
|
||||
flash("告警冷却须为数字(秒)")
|
||||
return
|
||||
try:
|
||||
threshold = max(1, int(float(request.form.get("health_fail_threshold") or 2)))
|
||||
except (TypeError, ValueError):
|
||||
flash("连续失败阈值须为正整数")
|
||||
return
|
||||
|
||||
updates = {
|
||||
"HUB_WECHAT_ENABLED": "true" if enabled == "1" else "false",
|
||||
"HUB_WECHAT_WEBHOOK": webhook,
|
||||
"HUB_ALERT_COOLDOWN_SEC": str(cooldown),
|
||||
"HUB_HEALTH_FAIL_THRESHOLD": str(threshold),
|
||||
}
|
||||
try:
|
||||
apply_env_updates(ENV_PATH, updates)
|
||||
except OSError as exc:
|
||||
flash(f"写入 .env 失败: {exc}")
|
||||
return
|
||||
_apply_runtime_env(updates)
|
||||
|
||||
if send_test:
|
||||
result = send_hub_wechat("中控企业微信测试消息\n配置已保存并立即生效")
|
||||
if result.get("ok"):
|
||||
flash("告警配置已保存,测试消息已发送")
|
||||
else:
|
||||
flash(f"告警配置已保存,但测试发送失败: {result.get('msg') or '未知错误'}")
|
||||
return
|
||||
flash("企业微信告警配置已保存,立即生效")
|
||||
|
||||
|
||||
@app.route("/settings", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def settings_page():
|
||||
if request.method == "POST":
|
||||
current_pw = request.form.get("current_password") or ""
|
||||
new_username = (request.form.get("new_username") or "").strip()
|
||||
new_password = request.form.get("new_password") or ""
|
||||
confirm = request.form.get("confirm_password") or ""
|
||||
|
||||
if current_pw != hub_password():
|
||||
flash("当前密码错误")
|
||||
return redirect(url_for("settings_page"))
|
||||
if len(new_username) < 2 or len(new_username) > 64:
|
||||
flash("用户名长度需为 2–64")
|
||||
return redirect(url_for("settings_page"))
|
||||
if any(ch.isspace() for ch in new_username):
|
||||
flash("用户名不能包含空格")
|
||||
return redirect(url_for("settings_page"))
|
||||
|
||||
updates: dict[str, str] = {"HUB_USERNAME": new_username}
|
||||
final_password = hub_password()
|
||||
if new_password or confirm:
|
||||
if len(new_password) < 6:
|
||||
flash("新密码至少 6 位")
|
||||
return redirect(url_for("settings_page"))
|
||||
if new_password != confirm:
|
||||
flash("两次输入的新密码不一致")
|
||||
return redirect(url_for("settings_page"))
|
||||
updates["HUB_PASSWORD"] = new_password
|
||||
final_password = new_password
|
||||
|
||||
try:
|
||||
apply_env_updates(ENV_PATH, updates)
|
||||
except OSError as exc:
|
||||
flash(f"写入 .env 失败: {exc}")
|
||||
return redirect(url_for("settings_page"))
|
||||
|
||||
set_hub_credentials(new_username, final_password)
|
||||
flash("账号已更新,立即生效")
|
||||
action = (request.form.get("form_action") or "account").strip()
|
||||
if action == "wechat":
|
||||
_handle_wechat_settings(send_test=False)
|
||||
elif action == "wechat_test":
|
||||
_handle_wechat_settings(send_test=True)
|
||||
else:
|
||||
_handle_account_settings()
|
||||
return redirect(url_for("settings_page"))
|
||||
|
||||
return render_template("settings.html", current_username=hub_username())
|
||||
return render_template(
|
||||
"settings.html",
|
||||
current_username=hub_username(),
|
||||
wechat=_wechat_settings_view(),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/desk")
|
||||
@@ -233,6 +315,32 @@ def api_overview():
|
||||
return jsonify({"ok": True, "instances": rows})
|
||||
|
||||
|
||||
def _health_pull_interval_sec() -> float:
|
||||
try:
|
||||
return max(5.0, float(os.getenv("HUB_PULL_INTERVAL_SEC") or 10))
|
||||
except (TypeError, ValueError):
|
||||
return 10.0
|
||||
|
||||
|
||||
def _health_watch_loop() -> None:
|
||||
"""后台巡检:即使无人打开总览也会告警."""
|
||||
while True:
|
||||
try:
|
||||
rows = pull_all_enabled()
|
||||
inspect_overview_rows(rows)
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(_health_pull_interval_sec())
|
||||
|
||||
|
||||
def start_health_watch() -> None:
|
||||
t = threading.Thread(target=_health_watch_loop, name="hub-health-watch", daemon=True)
|
||||
t.start()
|
||||
|
||||
|
||||
start_health_watch()
|
||||
|
||||
|
||||
@app.route("/api/instances/<int:iid>/open_url")
|
||||
@login_required
|
||||
def api_open_url(iid: int):
|
||||
|
||||
Reference in New Issue
Block a user