1
This commit is contained in:
parent
17a5828266
commit
829905842a
@ -68,7 +68,7 @@ X-Webhook-Token: your-shared-secret
|
|||||||
|
|
||||||
标题和正文模板支持 `{{field}}` 占位符,字段来自 TradingView alert JSON。嵌套字段可以写成 `{{order.id}}`。
|
标题和正文模板支持 `{{field}}` 占位符,字段来自 TradingView alert JSON。嵌套字段可以写成 `{{order.id}}`。
|
||||||
|
|
||||||
每条路由规则通过「发送到」下拉框选择一个飞书 Webhook。`timeframe`、`symbol`、`strategy` 至少填写一个,空字段表示不限。例如只填 `symbol=BTCUSDT` 会匹配所有 BTCUSDT 信号。需要同一个信号发到多个群时,可以建多条匹配条件相同、目标不同的规则,并用优先级控制命中顺序;当前默认路由逻辑只发送最高优先级命中的规则。
|
每条路由规则通过「发送到」多选框选择一个或多个飞书 Webhook。`timeframe`、`symbol`、`strategy` 至少填写一个,空字段表示不限。例如只填 `symbol=BTCUSDT` 会匹配所有 BTCUSDT 信号。当前默认路由逻辑只匹配最高优先级的规则,但这条规则可以同时分发到多个飞书目标。
|
||||||
|
|
||||||
示例正文模板:
|
示例正文模板:
|
||||||
|
|
||||||
|
|||||||
@ -400,7 +400,9 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
f"品种={html.escape(rule['symbol'])}" if rule["symbol"] else "",
|
f"品种={html.escape(rule['symbol'])}" if rule["symbol"] else "",
|
||||||
f"策略={html.escape(rule['strategy'])}" if rule["strategy"] else "",
|
f"策略={html.escape(rule['strategy'])}" if rule["strategy"] else "",
|
||||||
]
|
]
|
||||||
target_name = target_names.get(rule["target_ids"][0], "-") if rule["target_ids"] else "-"
|
target_name = "<br>".join(
|
||||||
|
html.escape(target_names.get(target_id, f"#{target_id}")) for target_id in rule["target_ids"]
|
||||||
|
) or "-"
|
||||||
rows += f"""<tr>
|
rows += f"""<tr>
|
||||||
<td>{rule['id']}</td>
|
<td>{rule['id']}</td>
|
||||||
<td>{html.escape(rule['name'])}</td>
|
<td>{html.escape(rule['name'])}</td>
|
||||||
@ -434,7 +436,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
"target_ids": [],
|
"target_ids": [],
|
||||||
"enabled": 1,
|
"enabled": 1,
|
||||||
}
|
}
|
||||||
selected_targets = target_select_options(targets, rule.get("target_ids", []), placeholder=True)
|
selected_targets = target_select_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(
|
sample_payload = sample_payload or json.dumps(
|
||||||
@ -454,7 +456,7 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
<label class="field-target">发送到<select class="select-target" name="target_ids" required>{selected_targets}</select></label>
|
<label class="field-target">发送到<select class="select-target multi" name="target_ids" multiple required size="5">{selected_targets}</select></label>
|
||||||
<label class="check"><input name="enabled" type="checkbox" {'checked' if rule.get('enabled') else ''}> 启用</label>
|
<label class="check"><input name="enabled" type="checkbox" {'checked' if rule.get('enabled') else ''}> 启用</label>
|
||||||
<h2>规则命中与卡片预览</h2>
|
<h2>规则命中与卡片预览</h2>
|
||||||
<input type="hidden" name="source_action" value="{html.escape(action)}">
|
<input type="hidden" name="source_action" value="{html.escape(action)}">
|
||||||
|
|||||||
@ -197,6 +197,10 @@ td textarea {
|
|||||||
width: clamp(220px, 34vw, 360px);
|
width: clamp(220px, 34vw, 360px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.select-target.multi {
|
||||||
|
min-height: 138px;
|
||||||
|
}
|
||||||
|
|
||||||
.check {
|
.check {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -42,6 +42,17 @@ class DispatcherTest(unittest.TestCase):
|
|||||||
timeframe: str = "5m",
|
timeframe: str = "5m",
|
||||||
symbol: str = "BTCUSDT",
|
symbol: str = "BTCUSDT",
|
||||||
strategy: str = "breakout",
|
strategy: str = "breakout",
|
||||||
|
) -> int:
|
||||||
|
return self.add_rule_with_targets([target_id], priority, name, timeframe, symbol, strategy)
|
||||||
|
|
||||||
|
def add_rule_with_targets(
|
||||||
|
self,
|
||||||
|
target_ids: list[int],
|
||||||
|
priority: int = 100,
|
||||||
|
name: str = "rule",
|
||||||
|
timeframe: str = "5m",
|
||||||
|
symbol: str = "BTCUSDT",
|
||||||
|
strategy: str = "breakout",
|
||||||
) -> int:
|
) -> int:
|
||||||
now = now_iso()
|
now = now_iso()
|
||||||
with self.db.connect() as conn:
|
with self.db.connect() as conn:
|
||||||
@ -55,7 +66,7 @@ class DispatcherTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?, ?, 'card', 'Signal {{symbol}}', 'Price {{price}}', 1, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, 'card', 'Signal {{symbol}}', 'Price {{price}}', 1, ?, ?, ?)
|
||||||
""",
|
""",
|
||||||
(name, timeframe, symbol, strategy, priority, to_json([target_id]), now, now),
|
(name, timeframe, symbol, strategy, priority, to_json(target_ids), now, now),
|
||||||
)
|
)
|
||||||
return int(cur.lastrowid)
|
return int(cur.lastrowid)
|
||||||
|
|
||||||
@ -125,6 +136,20 @@ class DispatcherTest(unittest.TestCase):
|
|||||||
self.assertEqual(delivery["attempts"], 1)
|
self.assertEqual(delivery["attempts"], 1)
|
||||||
self.assertIsNotNone(delivery["error"])
|
self.assertIsNotNone(delivery["error"])
|
||||||
|
|
||||||
|
def test_rule_can_dispatch_to_multiple_targets(self) -> None:
|
||||||
|
target_a = self.add_target("ops-a")
|
||||||
|
target_b = self.add_target("ops-b")
|
||||||
|
self.add_rule_with_targets([target_a, target_b])
|
||||||
|
|
||||||
|
result = self.dispatcher.receive_alert(
|
||||||
|
{"timeframe": "5m", "symbol": "BTCUSDT", "strategy": "breakout", "action": "buy"}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(result["delivery_ids"]), 2)
|
||||||
|
with self.db.connect() as conn:
|
||||||
|
count = conn.execute("SELECT COUNT(*) AS c FROM deliveries WHERE alert_id = ?", (result["alert_id"],)).fetchone()["c"]
|
||||||
|
self.assertEqual(count, 2)
|
||||||
|
|
||||||
def test_card_template_uses_alert_fields(self) -> None:
|
def test_card_template_uses_alert_fields(self) -> None:
|
||||||
message = build_feishu_message(
|
message = build_feishu_message(
|
||||||
{"timeframe": "5m", "symbol": "BTCUSDT", "strategy": "breakout", "action": "buy", "price": 68000},
|
{"timeframe": "5m", "symbol": "BTCUSDT", "strategy": "breakout", "action": "buy", "price": 68000},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user