from __future__ import annotations import html import json import mimetypes import os import urllib.error import urllib.request from http import HTTPStatus from http.cookies import SimpleCookie from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from typing import Any from urllib.parse import parse_qs, urlparse from app.auth import COOKIE_NAME, check_credentials, hash_password, is_valid_session, make_session_cookie from app.config import Settings, get_settings from app.db import Database, from_json, now_iso, to_json from app.dispatcher import Dispatcher, ValidationError, build_feishu_message, normalize_alert class AppContext: def __init__(self, settings: Settings): self.settings = settings self.db = Database(settings) self.db.migrate(settings) self.dispatcher = Dispatcher(self.db, settings) def json_response(handler: BaseHTTPRequestHandler, status: int, payload: dict[str, Any] | list[Any]) -> None: body = json.dumps(payload, ensure_ascii=False).encode() handler.send_response(status) handler.send_header("Content-Type", "application/json; charset=utf-8") handler.send_header("Content-Length", str(len(body))) handler.end_headers() handler.wfile.write(body) def redirect(handler: BaseHTTPRequestHandler, location: str) -> None: handler.send_response(HTTPStatus.SEE_OTHER) handler.send_header("Location", location) handler.end_headers() def read_body(handler: BaseHTTPRequestHandler) -> bytes: length = int(handler.headers.get("Content-Length", "0") or "0") return handler.rfile.read(length) def parse_form(handler: BaseHTTPRequestHandler) -> dict[str, str]: data = read_body(handler).decode() return {key: values[-1] for key, values in parse_qs(data).items()} def parse_form_multi(handler: BaseHTTPRequestHandler) -> dict[str, list[str]]: return parse_qs(read_body(handler).decode()) def parse_json_body(handler: BaseHTTPRequestHandler) -> dict[str, Any]: try: value = json.loads(read_body(handler).decode() or "{}") except json.JSONDecodeError as exc: raise ValidationError("Request body must be valid JSON") from exc if not isinstance(value, dict): raise ValidationError("Request body must be a JSON object") return value def target_checkbox_options( targets: list[dict[str, Any]], selected_ids: list[int] | None = None, ) -> str: selected_ids = selected_ids or [] if not targets: return '

还没有可用的飞书 Webhook,请先到飞书 Webhook 页面创建。

' options = [] for target in targets: checked = "checked" if target["id"] in selected_ids else "" options.append( f'' ) return "".join(options) def template_picker(templates: list[dict[str, Any]]) -> str: if not templates: return '

还没有飞书内容模板,可以直接填写标题和正文。

' options = [''] for template in templates: options.append( "" ) return ( '
' '' '新增模板' '
' ) class Handler(BaseHTTPRequestHandler): context: AppContext def log_message(self, format: str, *args: Any) -> None: print("%s - - [%s] %s" % (self.address_string(), self.log_date_time_string(), format % args)) def do_GET(self) -> None: parsed = urlparse(self.path) if parsed.path == "/health": json_response(self, 200, {"ok": True}) return if parsed.path == "/login": self.render_login() return if parsed.path.startswith("/static/"): self.serve_static(parsed.path) return if not self.require_auth(): return if parsed.path in ("/", "/dashboard"): self.render_dashboard() elif parsed.path == "/targets": self.render_targets() elif parsed.path == "/targets/delete": self.render_target_delete(parsed) elif parsed.path == "/templates": self.render_templates() elif parsed.path == "/templates/new": self.render_template_new() elif parsed.path == "/templates/edit": self.render_template_edit(parsed) elif parsed.path == "/templates/delete": self.render_template_delete(parsed) elif parsed.path == "/rules": self.render_rules() elif parsed.path == "/rules/new": self.render_rule_new() elif parsed.path == "/rules/edit": self.render_rule_edit(parsed) elif parsed.path == "/rules/delete": self.render_rule_delete(parsed) elif parsed.path == "/logs": self.render_logs() elif parsed.path == "/test": self.render_test() elif parsed.path == "/account": self.render_account() elif parsed.path == "/api/targets": json_response(self, 200, self.list_targets()) elif parsed.path == "/api/rules": json_response(self, 200, self.list_rules()) elif parsed.path == "/api/logs": json_response(self, 200, self.list_logs()) else: self.send_error(404) def do_POST(self) -> None: parsed = urlparse(self.path) if parsed.path == "/webhook/tradingview": self.handle_tradingview_webhook() return if parsed.path == "/login": self.handle_login() return if not self.require_auth(): return routes = { "/targets/create": self.create_target, "/targets/update": self.update_target, "/targets/delete": self.delete_target, "/targets/test": self.test_target, "/templates/create": self.create_template, "/templates/update": self.update_template, "/templates/delete": self.delete_template, "/rules/create": self.create_rule, "/rules/update": self.update_rule, "/rules/delete": self.delete_rule, "/test/send": self.send_test, "/account/password": self.change_password, "/deliveries/retry": self.retry_deliveries, "/logout": self.logout, } handler = routes.get(parsed.path) if not handler: self.send_error(404) return handler() def require_auth(self) -> bool: if is_valid_session(self.context.settings, self.headers.get("Cookie")): return True redirect(self, "/login") return False def layout(self, title: str, body: str) -> bytes: nav = [ ("/dashboard", "概览"), ("/rules", "路由规则"), ("/templates", "飞书模板"), ("/targets", "飞书 Webhook"), ("/logs", "日志"), ("/test", "测试发送"), ("/account", "账号安全"), ] items = "".join(f'{label}' for href, label in nav) return f""" {html.escape(title)}
{body}
""".encode() def send_html(self, title: str, body: str) -> None: content = self.layout(title, body) self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) def render_login(self) -> None: content = """ Login

TV Dispatch

TradingView alert routing console

""".encode() self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) def handle_login(self) -> None: form = parse_form(self) if not check_credentials( self.context.settings, form.get("username", ""), form.get("password", ""), self.get_admin_password_hash(), ): redirect(self, "/login") return cookie = SimpleCookie() cookie[COOKIE_NAME] = make_session_cookie(self.context.settings) cookie[COOKIE_NAME]["path"] = "/" cookie[COOKIE_NAME]["httponly"] = True cookie[COOKIE_NAME]["samesite"] = "Lax" self.send_response(HTTPStatus.SEE_OTHER) self.send_header("Location", "/dashboard") self.send_header("Set-Cookie", cookie.output(header="").strip()) self.end_headers() def get_admin_password_hash(self) -> str: with self.context.db.connect() as conn: row = conn.execute("SELECT password_hash FROM admin_settings WHERE id = 1").fetchone() return row["password_hash"] def logout(self) -> None: self.send_response(HTTPStatus.SEE_OTHER) self.send_header("Location", "/login") self.send_header("Set-Cookie", f"{COOKIE_NAME}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax") self.end_headers() def serve_static(self, path: str) -> None: local_path = os.path.join(os.path.dirname(__file__), "static", os.path.basename(path)) if not os.path.exists(local_path): self.send_error(404) return with open(local_path, "rb") as file: content = file.read() self.send_response(200) self.send_header("Content-Type", mimetypes.guess_type(local_path)[0] or "application/octet-stream") self.send_header("Content-Length", str(len(content))) self.end_headers() self.wfile.write(content) def handle_tradingview_webhook(self) -> None: if self.context.settings.webhook_token: query = parse_qs(urlparse(self.path).query) token = self.headers.get("X-Webhook-Token") or query.get("token", [""])[-1] if token != self.context.settings.webhook_token: json_response(self, 401, {"error": "Invalid webhook token"}) return try: payload = parse_json_body(self) result = self.context.dispatcher.receive_alert(payload) json_response(self, 202, result) except ValidationError as exc: json_response(self, 400, {"error": str(exc)}) def list_targets(self) -> list[dict[str, Any]]: with self.context.db.connect() as conn: rows = conn.execute("SELECT * FROM webhook_targets ORDER BY id DESC").fetchall() return [dict(row) for row in rows] def list_rules(self) -> list[dict[str, Any]]: with self.context.db.connect() as conn: rows = conn.execute("SELECT * FROM routing_rules ORDER BY priority ASC, id DESC").fetchall() rules = [] for row in rows: item = dict(row) item["target_ids"] = from_json(item["target_ids"], []) rules.append(item) return rules def list_templates(self) -> list[dict[str, Any]]: with self.context.db.connect() as conn: rows = conn.execute("SELECT * FROM message_templates ORDER BY id DESC").fetchall() return [dict(row) for row in rows] def list_logs(self) -> dict[str, list[dict[str, Any]]]: with self.context.db.connect() as conn: alerts = conn.execute("SELECT * FROM alerts ORDER BY id DESC LIMIT 100").fetchall() deliveries = conn.execute("SELECT * FROM deliveries ORDER BY id DESC LIMIT 200").fetchall() return {"alerts": [dict(row) for row in alerts], "deliveries": [dict(row) for row in deliveries]} def render_dashboard(self) -> None: host = self.headers.get("Host", f"localhost:{self.context.settings.port}") scheme = self.headers.get("X-Forwarded-Proto", "http") base_url = f"{scheme}://{host}" webhook_url = f"{base_url}/webhook/tradingview" token = self.context.settings.webhook_token webhook_url_with_token = f"{webhook_url}?token={token}" if token else webhook_url token_block = ( f"""
Webhook Token{html.escape(token)}
Header 方式X-Webhook-Token: {html.escape(token)}
""" if token else """

当前未设置 WEBHOOK_TOKEN,任何知道地址的人都可以提交 alert。生产环境建议设置。

""" ) webhook_panel = f"""

TradingView Webhook 配置

Webhook URL{html.escape(webhook_url_with_token)}
纯 URL{html.escape(webhook_url)}
{token_block}
""" with self.context.db.connect() as conn: counts = { "alerts": conn.execute("SELECT COUNT(*) AS c FROM alerts").fetchone()["c"], "rules": conn.execute("SELECT COUNT(*) AS c FROM routing_rules").fetchone()["c"], "targets": conn.execute("SELECT COUNT(*) AS c FROM webhook_targets").fetchone()["c"], "pending": conn.execute("SELECT COUNT(*) AS c FROM deliveries WHERE status IN ('pending','retry')").fetchone()["c"], } recent = conn.execute("SELECT * FROM alerts ORDER BY id DESC LIMIT 8").fetchall() cards = "".join(f'
{label}{value}
' for label, value in [ ("Alerts", counts["alerts"]), ("Rules", counts["rules"]), ("Targets", counts["targets"]), ("Pending", counts["pending"]), ]) rows = "".join( f"{row['id']}{html.escape(row['symbol'])}{html.escape(row['timeframe'])}{html.escape(row['strategy'])}{html.escape(row['status'])}{row['created_at']}" for row in recent ) self.send_html("概览", f"

概览

结构化 alert 分发、飞书转发和重试状态。

{webhook_panel}
{cards}

最近 Alert

{rows}
ID品种周期策略状态时间
") def render_targets(self) -> None: targets = self.list_targets() rows = "".join( f""" {target['id']}
删除 """ for target in targets ) form = """

新增飞书 Webhook

""" notice = getattr(self, "_target_notice", "") self.send_html("飞书 Webhook", f"

飞书 Webhook

维护所有可分发的飞书机器人地址。

{notice}{form}{rows}
ID名称URL操作
") def render_target_delete(self, parsed: Any) -> None: target_id = parse_qs(parsed.query).get("id", [""])[-1] with self.context.db.connect() as conn: target = conn.execute("SELECT * FROM webhook_targets WHERE id = ?", (target_id,)).fetchone() if not target: self.send_error(404) return body = f"""

删除飞书 Webhook

请确认是否删除这个飞书目标。

{html.escape(target['name'])}

{html.escape(target['webhook_url'])}

取消
""" self.send_html("删除飞书 Webhook", body) def render_templates(self) -> None: templates = self.list_templates() rows = "".join( f""" {template['id']} {html.escape(template['name'])} {html.escape(template['description'] or '-')} {html.escape(template['card_title_template'])} 编辑删除 """ for template in templates ) header = """""" self.send_html("飞书内容模板", f"{header}{rows}
ID名称说明标题模板操作
") def render_template_form( self, title: str, action: str, template: dict[str, Any] | None = None, ) -> None: template = template or { "id": "", "name": "", "description": "", "card_title_template": "{{title}}", "card_body_template": "**{{symbol}} · {{timeframe}} · {{direction}}**\n\n> {{signal_type}}\n\n**价格区**\n- 试仓位:{{probe_entry}}\n- 优先位:{{priority_entry}}\n- 防守位:{{defense_price}}\n- 止损位:{{stop_loss}}\n- 当前价:{{current_price}}\n\n**说明**\n{{description}}\n\n**风险提示**\n{{risk_tip}}", } hidden_id = f'' if template.get("id") else "" button_text = "保存修改" if template.get("id") else "创建模板" body = f"""

{html.escape(title)}

模板支持 {{{{field}}}}{{field}} 占位符,字段来自 TradingView JSON。

{hidden_id}
返回列表
""" self.send_html(title, body) def render_template_new(self) -> None: self.render_template_form("新增飞书内容模板", "/templates/create") def render_template_edit(self, parsed: Any) -> None: template_id = parse_qs(parsed.query).get("id", [""])[-1] with self.context.db.connect() as conn: template = conn.execute("SELECT * FROM message_templates WHERE id = ?", (template_id,)).fetchone() if not template: self.send_error(404) return self.render_template_form("编辑飞书内容模板", "/templates/update", dict(template)) def render_template_delete(self, parsed: Any) -> None: template_id = parse_qs(parsed.query).get("id", [""])[-1] with self.context.db.connect() as conn: template = conn.execute("SELECT * FROM message_templates WHERE id = ?", (template_id,)).fetchone() if not template: self.send_error(404) return body = f"""

删除飞书内容模板

请确认是否删除这个模板。

{html.escape(template['name'])}

删除模板不会影响已经创建的路由规则,只是后续不能再套用它。

取消
""" self.send_html("删除飞书内容模板", body) def render_rules(self) -> None: targets = self.list_targets() rules = self.list_rules() target_names = {target["id"]: target["name"] for target in targets} rows = "" for rule in rules: conditions = [ f"周期={html.escape(rule['timeframe'])}" if rule["timeframe"] else "", f"品种={html.escape(rule['symbol'])}" if rule["symbol"] else "", f"策略={html.escape(rule['strategy'])}" if rule["strategy"] else "", ] target_badges = "".join( f'{html.escape(target_names.get(target_id, f"#{target_id}"))}' for target_id in rule["target_ids"] ) or "-" rows += f""" {rule['id']} {html.escape(rule['name'])} {'
'.join(item for item in conditions if item) or '-'} {rule['priority']}
{target_badges}
{'启用' if rule['enabled'] else '停用'} 编辑删除 """ header = """""" self.send_html("路由规则", f"{header}{rows}
ID名称匹配条件优先级发送到状态操作
") def render_rule_form( self, title: str, action: str, rule: dict[str, Any] | None = None, ) -> None: targets = self.list_targets() templates = self.list_templates() rule = rule or { "id": "", "name": "", "timeframe": "", "symbol": "", "strategy": "", "priority": 100, "card_title_template": "TradingView {{symbol}} {{action}}", "card_body_template": "**品种**: {{symbol}}\n**周期**: {{timeframe}}\n**策略**: {{strategy}}\n**动作**: {{action}}\n**价格**: {{price}}", "target_ids": [], "enabled": 1, } selected_targets = target_checkbox_options(targets, rule.get("target_ids", [])) picker = template_picker(templates) hidden_id = f'' if rule.get("id") else "" button_text = "保存修改" if rule.get("id") else "创建规则" body = f"""

{html.escape(title)}

消息统一使用飞书卡片。周期、品种、策略至少填写一个,空字段表示不限。

{hidden_id}
{picker}
发送到
{selected_targets}
返回列表
""" self.send_html(title, body) def render_rule_new(self) -> None: self.render_rule_form("新增路由规则", "/rules/create") def render_rule_edit(self, parsed: Any) -> None: query = parse_qs(parsed.query) rule_id = query.get("id", [""])[-1] with self.context.db.connect() as conn: rule = conn.execute("SELECT * FROM routing_rules WHERE id = ?", (rule_id,)).fetchone() if not rule: self.send_error(404) return rule_dict = dict(rule) rule_dict["target_ids"] = from_json(rule_dict["target_ids"], []) self.render_rule_form("编辑路由规则", "/rules/update", rule_dict) def render_rule_delete(self, parsed: Any) -> None: rule_id = parse_qs(parsed.query).get("id", [""])[-1] with self.context.db.connect() as conn: rule = conn.execute("SELECT * FROM routing_rules WHERE id = ?", (rule_id,)).fetchone() if not rule: self.send_error(404) return body = f"""

删除路由规则

请确认是否删除这条规则。

{html.escape(rule['name'])}

删除后不会再匹配对应 alert,已有日志不受影响。

取消
""" self.send_html("删除路由规则", body) def render_logs(self) -> None: logs = self.list_logs() alert_rows = "" for row in logs["alerts"]: try: raw_payload = json.dumps(json.loads(row["payload"]), ensure_ascii=False, indent=2) except Exception: raw_payload = row["payload"] or "" alert_rows += f""" {row['id']} {html.escape(row['symbol'])} {html.escape(row['timeframe'])} {html.escape(row['strategy'])} {html.escape(row['status'])} {html.escape(row['error'] or '')} {row['created_at']}
查看
{html.escape(raw_payload)}
""" delivery_rows = "".join( f"{row['id']}{row['alert_id']}{html.escape(row['target_name'])}{html.escape(row['status'])}{row['attempts']}{html.escape(str(row['response_code'] or ''))}{html.escape(row['error'] or '')}{html.escape(row['next_attempt_at'] or '')}" for row in logs["deliveries"] ) body = f"""

日志

最近 100 条 alert 和 200 条分发任务。

Alert 日志

{alert_rows}
ID品种周期策略状态错误时间原始 Alert

Delivery 日志

{delivery_rows}
IDAlert目标状态次数HTTP错误下次重试
""" self.send_html("日志", body) def render_test(self) -> None: sample = html.escape(json.dumps({"timeframe": "5m", "symbol": "BTCUSDT", "strategy": "breakout", "action": "buy", "price": 68000}, indent=2)) result = getattr(self, "_test_result_html", "") body = f"""

测试发送

提交一条模拟 TradingView alert,走完整匹配和飞书转发流程。

""" if result: body += result self.send_html("测试发送", body) def render_account(self) -> None: body = """

账号安全

修改当前管理员密码,修改成功后会退出登录。

修改密码

""" self.send_html("账号安全", body) def create_target(self) -> None: form = parse_form(self) now = now_iso() with self.context.db.connect() as conn: conn.execute( "INSERT INTO webhook_targets (name, webhook_url, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?)", (form["name"].strip(), form["webhook_url"].strip(), 1, now, now), ) redirect(self, "/targets") def update_target(self) -> None: form = parse_form(self) with self.context.db.connect() as conn: conn.execute( "UPDATE webhook_targets SET name = ?, webhook_url = ?, enabled = ?, updated_at = ? WHERE id = ?", (form["name"].strip(), form["webhook_url"].strip(), 1, now_iso(), form["id"]), ) redirect(self, "/targets") def delete_target(self) -> None: form = parse_form(self) with self.context.db.connect() as conn: conn.execute("DELETE FROM webhook_targets WHERE id = ?", (form["id"],)) redirect(self, "/targets") def test_target(self) -> None: form = parse_form(self) with self.context.db.connect() as conn: target = conn.execute("SELECT * FROM webhook_targets WHERE id = ?", (form.get("id"),)).fetchone() if not target: self.send_error(404) return message = build_feishu_message( {"symbol": "TEST", "timeframe": "5m", "strategy": "webhook-test", "action": "test"}, { "card_title_template": "TV Dispatch 测试消息", "card_body_template": "**目标**: {{symbol}}\n**动作**: {{action}}\n这是一条飞书 Webhook 连通性测试。", }, ) try: data = json.dumps(message, ensure_ascii=False).encode() request = urllib.request.Request( target["webhook_url"], data=data, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(request, timeout=self.context.settings.feishu_timeout_seconds) as response: status = response.getcode() self.render_targets_with_notice(f"测试消息已发送到 {html.escape(target['name'])},HTTP {status}", success=True) except urllib.error.HTTPError as exc: self.render_targets_with_notice(f"测试发送失败:HTTP {exc.code}", success=False) except Exception as exc: self.render_targets_with_notice(f"测试发送失败:{html.escape(str(exc))}", success=False) def render_targets_with_notice(self, message: str, success: bool) -> None: self._target_notice = f"""

Webhook 测试

{message}

""" self.render_targets() def create_template(self) -> None: form = parse_form(self) now = now_iso() with self.context.db.connect() as conn: conn.execute( """ INSERT INTO message_templates ( name, description, card_title_template, card_body_template, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?) """, ( form["name"].strip(), form.get("description", "").strip(), form["card_title_template"].strip(), form["card_body_template"].strip(), now, now, ), ) redirect(self, "/templates") def update_template(self) -> None: form = parse_form(self) with self.context.db.connect() as conn: conn.execute( """ UPDATE message_templates SET name = ?, description = ?, card_title_template = ?, card_body_template = ?, updated_at = ? WHERE id = ? """, ( form["name"].strip(), form.get("description", "").strip(), form["card_title_template"].strip(), form["card_body_template"].strip(), now_iso(), form["id"], ), ) redirect(self, "/templates") def delete_template(self) -> None: form = parse_form(self) with self.context.db.connect() as conn: conn.execute("DELETE FROM message_templates WHERE id = ?", (form["id"],)) redirect(self, "/templates") def create_rule(self) -> None: form = parse_form_multi(self) target_ids = [int(value) for value in form.get("target_ids", [])] timeframe = form.get("timeframe", [""])[-1].strip() symbol = form.get("symbol", [""])[-1].strip().upper() strategy = form.get("strategy", [""])[-1].strip() if not any((timeframe, symbol, strategy)): self.send_error(400, "周期、品种、策略至少填写一个") return if not target_ids: self.send_error(400, "至少选择一个飞书 Webhook") return now = now_iso() with self.context.db.connect() as conn: conn.execute( """ INSERT INTO routing_rules ( name, timeframe, symbol, strategy, priority, message_type, card_title_template, card_body_template, enabled, target_ids, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( form.get("name", [""])[-1].strip(), timeframe, symbol, strategy, int(form.get("priority", ["100"])[-1]), "card", form.get("card_title_template", ["TradingView {{symbol}} {{action}}"])[-1].strip(), form.get("card_body_template", [""])[-1].strip(), 1 if form.get("enabled", [""])[-1] == "on" else 0, to_json(target_ids), now, now, ), ) redirect(self, "/rules") def delete_rule(self) -> None: form = parse_form(self) with self.context.db.connect() as conn: conn.execute("DELETE FROM routing_rules WHERE id = ?", (form["id"],)) redirect(self, "/rules") def update_rule(self) -> None: form = parse_form_multi(self) target_ids = [int(value) for value in form.get("target_ids", [])] timeframe = form.get("timeframe", [""])[-1].strip() symbol = form.get("symbol", [""])[-1].strip().upper() strategy = form.get("strategy", [""])[-1].strip() if not any((timeframe, symbol, strategy)): self.send_error(400, "周期、品种、策略至少填写一个") return if not target_ids: self.send_error(400, "至少选择一个飞书 Webhook") return with self.context.db.connect() as conn: conn.execute( """ UPDATE routing_rules SET name = ?, timeframe = ?, symbol = ?, strategy = ?, priority = ?, message_type = ?, card_title_template = ?, card_body_template = ?, enabled = ?, target_ids = ?, updated_at = ? WHERE id = ? """, ( form.get("name", [""])[-1].strip(), timeframe, symbol, strategy, int(form.get("priority", ["100"])[-1]), "card", form.get("card_title_template", ["TradingView {{symbol}} {{action}}"])[-1].strip(), form.get("card_body_template", [""])[-1].strip(), 1 if form.get("enabled", [""])[-1] == "on" else 0, to_json(target_ids), now_iso(), form.get("id", [""])[-1], ), ) redirect(self, "/rules") def send_test(self) -> None: form = parse_form(self) payload_text = form.get("payload", "{}") try: payload = json.loads(payload_text) result = self.context.dispatcher.receive_alert(payload) delivery_text = ", ".join(str(item) for item in result.get("delivery_ids", [])) or "-" self._test_result_html = f"""

测试结果

Alert ID{result.get("alert_id")}
状态{html.escape(str(result.get("status")))}
命中规则{html.escape(str(result.get("matched_rule_id") or "-"))}
Delivery{html.escape(delivery_text)}
查看响应 JSON
{html.escape(json.dumps(result, ensure_ascii=False, indent=2))}
""" self.render_test() except (json.JSONDecodeError, ValidationError) as exc: self._test_result_html = f"""

测试失败

{html.escape(str(exc))}

""" self.render_test() def change_password(self) -> None: form = parse_form(self) current_password = form.get("current_password", "") new_password = form.get("new_password", "") confirm_password = form.get("confirm_password", "") if not check_credentials( self.context.settings, self.context.settings.admin_username, current_password, self.get_admin_password_hash(), ): json_response(self, 400, {"error": "当前密码不正确"}) return if len(new_password) < 8: json_response(self, 400, {"error": "新密码至少需要 8 位"}) return if new_password != confirm_password: json_response(self, 400, {"error": "两次输入的新密码不一致"}) return with self.context.db.connect() as conn: conn.execute( "UPDATE admin_settings SET password_hash = ?, updated_at = ? WHERE id = 1", (hash_password(new_password), now_iso()), ) self.logout() def retry_deliveries(self) -> None: self.context.dispatcher.process_due_deliveries(limit=100) redirect(self, "/logs") def make_handler(context: AppContext) -> type[Handler]: class BoundHandler(Handler): pass BoundHandler.context = context return BoundHandler def run() -> None: settings = get_settings() context = AppContext(settings) context.db.cleanup_old_logs(settings.retention_days) server = ThreadingHTTPServer((settings.host, settings.port), make_handler(context)) print(f"Serving {settings.app_name} on http://{settings.host}:{settings.port}") server.serve_forever() if __name__ == "__main__": run()