1
This commit is contained in:
parent
d6ed66db7d
commit
6721cfddf1
@ -149,7 +149,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
"/rules/create": self.create_rule,
|
"/rules/create": self.create_rule,
|
||||||
"/rules/update": self.update_rule,
|
"/rules/update": self.update_rule,
|
||||||
"/rules/delete": self.delete_rule,
|
"/rules/delete": self.delete_rule,
|
||||||
"/rules/preview": self.preview_rule,
|
|
||||||
"/test/send": self.send_test,
|
"/test/send": self.send_test,
|
||||||
"/account/password": self.change_password,
|
"/account/password": self.change_password,
|
||||||
"/deliveries/retry": self.retry_deliveries,
|
"/deliveries/retry": self.retry_deliveries,
|
||||||
@ -421,8 +420,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
title: str,
|
title: str,
|
||||||
action: str,
|
action: str,
|
||||||
rule: dict[str, Any] | None = None,
|
rule: dict[str, Any] | None = None,
|
||||||
preview_html: str = "",
|
|
||||||
sample_payload: str | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
targets = self.list_targets()
|
targets = self.list_targets()
|
||||||
rule = rule or {
|
rule = rule or {
|
||||||
@ -440,11 +437,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
selected_targets = target_checkbox_options(targets, rule.get("target_ids", []))
|
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 ""
|
hidden_id = f'<input type="hidden" name="id" value="{rule["id"]}">' if rule.get("id") else ""
|
||||||
button_text = "保存修改" 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>
|
body = f"""<header><h1>{html.escape(title)}</h1><p>消息统一使用飞书卡片。周期、品种、策略至少填写一个,空字段表示不限。</p></header>
|
||||||
<form class="panel rule-form" method="post" action="{action}">
|
<form class="panel rule-form" method="post" action="{action}">
|
||||||
{hidden_id}
|
{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>卡片标题模板<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>
|
<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>
|
<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>
|
<label class="switch"><input name="enabled" type="checkbox" {'checked' if rule.get('enabled') else ''}><span></span><strong>启用规则</strong></label>
|
||||||
<h2>规则命中与卡片预览</h2>
|
<div class="actions"><button type="submit">{button_text}</button><a class="button-link secondary" href="/rules">返回列表</a></div>
|
||||||
<input type="hidden" name="source_action" value="{html.escape(action)}">
|
</form>"""
|
||||||
<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}"""
|
|
||||||
self.send_html(title, body)
|
self.send_html(title, body)
|
||||||
|
|
||||||
def render_rule_new(self) -> None:
|
def render_rule_new(self) -> None:
|
||||||
@ -500,72 +489,6 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
</section>"""
|
</section>"""
|
||||||
self.send_html("删除路由规则", body)
|
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:
|
def render_logs(self) -> None:
|
||||||
logs = self.list_logs()
|
logs = self.list_logs()
|
||||||
alert_rows = ""
|
alert_rows = ""
|
||||||
|
|||||||
@ -238,6 +238,55 @@ td textarea {
|
|||||||
width: auto;
|
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 {
|
.checks {
|
||||||
margin: 8px 0 12px;
|
margin: 8px 0 12px;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user