This commit is contained in:
aaron 2026-05-15 21:38:36 +08:00
parent d6ed66db7d
commit 6721cfddf1
2 changed files with 52 additions and 80 deletions

View File

@ -149,7 +149,6 @@ class Handler(BaseHTTPRequestHandler):
"/rules/create": self.create_rule,
"/rules/update": self.update_rule,
"/rules/delete": self.delete_rule,
"/rules/preview": self.preview_rule,
"/test/send": self.send_test,
"/account/password": self.change_password,
"/deliveries/retry": self.retry_deliveries,
@ -421,8 +420,6 @@ class Handler(BaseHTTPRequestHandler):
title: str,
action: str,
rule: dict[str, Any] | None = None,
preview_html: str = "",
sample_payload: str | None = None,
) -> None:
targets = self.list_targets()
rule = rule or {
@ -440,11 +437,6 @@ class Handler(BaseHTTPRequestHandler):
selected_targets = target_checkbox_options(targets, rule.get("target_ids", []))
hidden_id = f'<input type="hidden" name="id" value="{rule["id"]}">' if rule.get("id") else ""
button_text = "保存修改" if rule.get("id") else "创建规则"
sample_payload = sample_payload or json.dumps(
{"timeframe": "5m", "symbol": "BTCUSDT", "strategy": "breakout", "action": "buy", "price": 68000},
ensure_ascii=False,
indent=2,
)
body = f"""<header><h1>{html.escape(title)}</h1><p>消息统一使用飞书卡片。周期、品种、策略至少填写一个,空字段表示不限。</p></header>
<form class="panel rule-form" method="post" action="{action}">
{hidden_id}
@ -458,12 +450,9 @@ class Handler(BaseHTTPRequestHandler):
<label>卡片标题模板<input name="card_title_template" value="{html.escape(str(rule['card_title_template']))}" required></label>
<label>卡片正文模板<textarea name="card_body_template" rows="6">{html.escape(str(rule['card_body_template']))}</textarea></label>
<div class="field-target"><span class="field-label">发送到</span><div class="target-choices">{selected_targets}</div></div>
<label class="check"><input name="enabled" type="checkbox" {'checked' if rule.get('enabled') else ''}> 启用</label>
<h2>规则命中与卡片预览</h2>
<input type="hidden" name="source_action" value="{html.escape(action)}">
<label>样例 Alert JSON<textarea name="sample_payload" rows="9">{html.escape(sample_payload)}</textarea></label>
<div class="actions"><button type="submit">{button_text}</button><button type="submit" formaction="/rules/preview">预览命中和卡片</button><a class="button-link secondary" href="/rules">返回列表</a></div>
</form>{preview_html}"""
<label class="switch"><input name="enabled" type="checkbox" {'checked' if rule.get('enabled') else ''}><span></span><strong>启用规则</strong></label>
<div class="actions"><button type="submit">{button_text}</button><a class="button-link secondary" href="/rules">返回列表</a></div>
</form>"""
self.send_html(title, body)
def render_rule_new(self) -> None:
@ -500,72 +489,6 @@ class Handler(BaseHTTPRequestHandler):
</section>"""
self.send_html("删除路由规则", body)
def build_rule_from_form(self, form: dict[str, list[str]]) -> dict[str, Any]:
return {
"id": form.get("id", [""])[-1],
"name": form.get("name", [""])[-1].strip(),
"timeframe": form.get("timeframe", [""])[-1].strip(),
"symbol": form.get("symbol", [""])[-1].strip().upper(),
"strategy": form.get("strategy", [""])[-1].strip(),
"priority": int(form.get("priority", ["100"])[-1] or 100),
"card_title_template": form.get("card_title_template", ["TradingView {{symbol}} {{action}}"])[-1].strip(),
"card_body_template": form.get("card_body_template", [""])[-1].strip(),
"target_ids": [int(value) for value in form.get("target_ids", []) if value],
"enabled": 1 if form.get("enabled", [""])[-1] == "on" else 0,
}
def preview_rule(self) -> None:
form = parse_form_multi(self)
rule = self.build_rule_from_form(form)
sample_payload = form.get("sample_payload", ["{}"])[-1]
source_action = form.get("source_action", ["/rules/create"])[-1]
title = "编辑路由规则" if rule.get("id") else "新增路由规则"
try:
alert = normalize_alert(json.loads(sample_payload))
message = build_feishu_message(alert, rule)
matched = self.context.dispatcher.find_matching_rule(alert)
current_matches = self.rule_matches_alert(rule, alert)
preview_html = self.render_preview_result(rule, message, matched, current_matches)
except (json.JSONDecodeError, ValidationError, ValueError) as exc:
preview_html = f"""<section class="result-panel error"><h2>预览失败</h2><p>{html.escape(str(exc))}</p></section>"""
self.render_rule_form(title, source_action, rule, preview_html, sample_payload)
def rule_matches_alert(self, rule: dict[str, Any], alert: dict[str, Any]) -> bool:
if not any((rule.get("timeframe"), rule.get("symbol"), rule.get("strategy"))):
return False
if rule.get("timeframe") and rule["timeframe"] != alert.get("timeframe"):
return False
if rule.get("symbol") and rule["symbol"].upper() != alert.get("symbol"):
return False
if rule.get("strategy") and rule["strategy"] != alert.get("strategy"):
return False
return True
def render_preview_result(
self,
rule: dict[str, Any],
message: dict[str, Any],
matched: dict[str, Any] | None,
current_matches: bool,
) -> str:
title = message["card"]["header"]["title"]["content"]
content = message["card"]["elements"][0]["text"]["content"]
matched_text = f"当前已保存规则 #{matched['id']} {matched['name']}" if matched else "没有已保存规则会命中"
current_text = "当前表单会匹配样例 Alert" if current_matches else "当前表单不会匹配样例 Alert"
return f"""<section class="result-panel success">
<h2>预览结果</h2>
<div class="result-grid">
<div><span>当前表单</span><strong>{html.escape(current_text)}</strong></div>
<div><span>系统实际命中</span><strong>{html.escape(matched_text)}</strong></div>
<div><span>规则优先级</span><strong>{rule.get('priority')}</strong></div>
<div><span>消息类型</span><strong>飞书卡片</strong></div>
</div>
<div class="feishu-preview">
<div class="feishu-preview-header">{html.escape(title)}</div>
<pre>{html.escape(content)}</pre>
</div>
</section>"""
def render_logs(self) -> None:
logs = self.list_logs()
alert_rows = ""

View File

@ -238,6 +238,55 @@ td textarea {
width: auto;
}
.switch {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 4px 0 18px;
cursor: pointer;
}
.switch input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.switch span {
position: relative;
width: 52px;
height: 30px;
border-radius: 999px;
background: #c8c2b5;
transition: background 0.18s ease;
}
.switch span::after {
content: "";
position: absolute;
top: 4px;
left: 4px;
width: 22px;
height: 22px;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.22);
transition: transform 0.18s ease;
}
.switch input:checked + span {
background: var(--accent);
}
.switch input:checked + span::after {
transform: translateX(22px);
}
.switch strong {
color: var(--ink);
font: 800 14px ui-sans-serif, system-ui, sans-serif;
}
.checks {
margin: 8px 0 12px;
}