1
This commit is contained in:
parent
d6ed66db7d
commit
6721cfddf1
@ -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 = ""
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user