From 829905842a554634c2882d9fd793ac4c4fc612b9 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 15 May 2026 17:09:15 +0800 Subject: [PATCH] 1 --- README.md | 2 +- app/server.py | 8 +++++--- app/static/styles.css | 4 ++++ tests/test_dispatcher.py | 27 ++++++++++++++++++++++++++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5c7436b..e26b52a 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ X-Webhook-Token: your-shared-secret 标题和正文模板支持 `{{field}}` 占位符,字段来自 TradingView alert JSON。嵌套字段可以写成 `{{order.id}}`。 -每条路由规则通过「发送到」下拉框选择一个飞书 Webhook。`timeframe`、`symbol`、`strategy` 至少填写一个,空字段表示不限。例如只填 `symbol=BTCUSDT` 会匹配所有 BTCUSDT 信号。需要同一个信号发到多个群时,可以建多条匹配条件相同、目标不同的规则,并用优先级控制命中顺序;当前默认路由逻辑只发送最高优先级命中的规则。 +每条路由规则通过「发送到」多选框选择一个或多个飞书 Webhook。`timeframe`、`symbol`、`strategy` 至少填写一个,空字段表示不限。例如只填 `symbol=BTCUSDT` 会匹配所有 BTCUSDT 信号。当前默认路由逻辑只匹配最高优先级的规则,但这条规则可以同时分发到多个飞书目标。 示例正文模板: diff --git a/app/server.py b/app/server.py index 73c471b..fdc0d38 100644 --- a/app/server.py +++ b/app/server.py @@ -400,7 +400,9 @@ class Handler(BaseHTTPRequestHandler): f"品种={html.escape(rule['symbol'])}" if rule["symbol"] 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 = "
".join( + 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'])} @@ -434,7 +436,7 @@ class Handler(BaseHTTPRequestHandler): "target_ids": [], "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'' if rule.get("id") else "" button_text = "保存修改" if rule.get("id") else "创建规则" sample_payload = sample_payload or json.dumps( @@ -454,7 +456,7 @@ class Handler(BaseHTTPRequestHandler): - +

规则命中与卡片预览

diff --git a/app/static/styles.css b/app/static/styles.css index 68ae4f4..e9ccadf 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -197,6 +197,10 @@ td textarea { width: clamp(220px, 34vw, 360px); } +.select-target.multi { + min-height: 138px; +} + .check { display: inline-flex; align-items: center; diff --git a/tests/test_dispatcher.py b/tests/test_dispatcher.py index 733e9cf..1eca97b 100644 --- a/tests/test_dispatcher.py +++ b/tests/test_dispatcher.py @@ -42,6 +42,17 @@ class DispatcherTest(unittest.TestCase): timeframe: str = "5m", symbol: str = "BTCUSDT", 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: now = now_iso() with self.db.connect() as conn: @@ -55,7 +66,7 @@ class DispatcherTest(unittest.TestCase): ) 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) @@ -125,6 +136,20 @@ class DispatcherTest(unittest.TestCase): self.assertEqual(delivery["attempts"], 1) 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: message = build_feishu_message( {"timeframe": "5m", "symbol": "BTCUSDT", "strategy": "breakout", "action": "buy", "price": 68000},