Skip to content

Commit 2c19e43

Browse files
committed
修复优先级低插件的相同指令不会分发
1 parent 2f2e7d2 commit 2c19e43

7 files changed

Lines changed: 179 additions & 24 deletions

File tree

PLUGIN_DEVELOPMENT.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ from core.plugin.decorators import handler, on_load, on_unload, interceptor
9999
```python
100100
@handler(pattern, *, name='', desc='', priority=0, owner_only=False,
101101
group_only=False, direct_only=False, channel_only=False,
102-
event_types=None, cooldown=0, ignore_at_check=False)
102+
event_types=None, cooldown=0, ignore_at_check=False, block=False)
103103
```
104104

105105
| 参数 | 类型 | 默认 | 说明 |
@@ -115,6 +115,7 @@ from core.plugin.decorators import handler, on_load, on_unload, interceptor
115115
| `event_types` | `list[str]` | `None` | 仅响应指定事件类型 (见下表) |
116116
| `cooldown` | `int` | `0` | 冷却时间 (秒, 0 = 无冷却) |
117117
| `ignore_at_check` | `bool` | `False` | 全量模式: 不需@机器人也触发 |
118+
| `block` | `bool` | `False` | 命中后是否拦截后续处理器 (见 3.1.1) |
118119

119120
**事件类型常量** (`event_types` 可选值):
120121

@@ -152,6 +153,21 @@ async def check_in(event, match):
152153
await event.reply("✅ 签到成功!")
153154
```
154155

156+
#### 3.1.1 `block` 放行 / 拦截
157+
158+
多个插件注册相同指令时, `block=False` (默认) 放行让所有命中处理器按 `priority` 顺序执行, `block=True` 命中即拦截后续低优先级处理器。
159+
160+
```python
161+
@handler(r'^状态$', name='系统状态', priority=10, block=True) # 命中即拦截, 只有它响应
162+
async def status(event, match):
163+
await event.reply("✅ 系统正常")
164+
165+
166+
@handler(r'^状态$', name='天气状态', priority=0) # 被上面 block 拦截, 不会触发
167+
async def weather(event, match):
168+
await event.reply("☀️ 今天晴")
169+
```
170+
155171
### 3.2 `@on_load` / `@on_unload` 生命周期钩子
156172

157173
```python

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ ElainaBot V2是一个基于 Python 的 QQ 官方机器人框架,采用纯异
1616
</p>
1717
<br clear="left" />
1818

19-
> 项目仅供学习交流使用,严禁用于非法行为
19+
> 项目仅供学习交流使用,严禁用于任何商业用途和非法行为
2020
2121
## 📢 交流群
2222

core/plugin/_dispatch.py

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ async def dispatch(self, event: Event, sender: Any) -> bool:
102102
if not handlers:
103103
return False
104104
scene: int = _event_scene(event)
105+
matched: list[tuple[dict[str, Any], re.Match[str]]] = []
105106
for h in handlers:
106107
ab = h['_allowed_bots']
107108
if ab is not None and appid not in ab:
@@ -111,13 +112,13 @@ async def dispatch(self, event: Event, sender: Any) -> bool:
111112
continue
112113
if h['_smask'] & ~scene:
113114
continue
114-
plugin_name: str = h['name'] or h.get('_plugin', '')
115-
log_service = self._get_log_service(event)
116-
event._reply_log_cb = _make_reply_log_cb(plugin_name, log_service)
117-
event._reply_plugin_name = plugin_name or ''
118-
asyncio.create_task(self._run_handler(h, event, m, plugin_name, user_id, et, content))
119-
return True
120-
return False
115+
matched.append((h, m))
116+
if h.get('block', False): # 默认放行, block=True 时拦截后续
117+
break
118+
if not matched:
119+
return False
120+
asyncio.create_task(self._run_chain(matched, event, user_id, et, content))
121+
return True
121122

122123
# ── 消息事件: 完整检查链 ──
123124
_get = cfg.get_bot_setting
@@ -212,7 +213,8 @@ def _match_handlers(
212213
content: str,
213214
skip_at_other: bool = False,
214215
) -> bool:
215-
"""内循环: 遍历 handler 尝试匹配, 匹配成功则 fire-and-forget 并返回 True"""
216+
"""内循环: 收集命中的 handler, 遇到 block=True 即停止, 随后顺序执行"""
217+
matched: list[tuple[dict[str, Any], re.Match[str]]] = []
216218
for h in handlers:
217219
# 快速过滤: bot 白名单
218220
ab = h['_allowed_bots']
@@ -230,8 +232,9 @@ def _match_handlers(
230232
continue
231233
# 场景过滤 (位掩码): handler 要求的场景位 & 事件不具备的场景位 → 不匹配
232234
if h['_smask'] & ~scene:
233-
# 群聊专属指令在私聊环境 → 明确告知用户
235+
# 群聊专属指令在私聊环境 → 告知用户并终止
234236
if h['group_only'] and not is_non_at:
237+
self._fire_chain(matched, event, user_id, et, content)
235238
asyncio.create_task(
236239
event.reply(
237240
template_name='group_only',
@@ -242,6 +245,7 @@ def _match_handlers(
242245
continue
243246
# 权限
244247
if h['owner_only'] and not self._is_owner(event):
248+
self._fire_chain(matched, event, user_id, et, content)
245249
if not is_non_at:
246250
asyncio.create_task(
247251
event.reply(
@@ -250,16 +254,46 @@ def _match_handlers(
250254
)
251255
)
252256
return True
253-
# 日志绑定
254-
plugin_name: str = h['name'] or h.get('_plugin', '')
255-
log_service = self._get_log_service(event)
256-
event._reply_log_cb = _make_reply_log_cb(plugin_name, log_service)
257-
event._reply_plugin_name = plugin_name or ''
258-
asyncio.create_task(self._run_handler(h, event, m, plugin_name, user_id, et, content))
259-
return True
260-
return False
257+
matched.append((h, m))
258+
if h.get('block', False): # 默认放行, block=True 时拦截后续
259+
break
260+
if not matched:
261+
return False
262+
self._fire_chain(matched, event, user_id, et, content)
263+
return True
264+
265+
def _fire_chain(
266+
self,
267+
matched: list[tuple[dict[str, Any], re.Match[str]]],
268+
event: Event,
269+
user_id: str,
270+
et: str,
271+
content: str,
272+
) -> None:
273+
"""调度命中的 handler 链顺序执行 (空则跳过)"""
274+
if matched:
275+
asyncio.create_task(self._run_chain(matched, event, user_id, et, content))
276+
277+
async def _run_chain(
278+
self,
279+
matched: list[tuple[dict[str, Any], re.Match[str]]],
280+
event: Event,
281+
user_id: str,
282+
et: str,
283+
content: str,
284+
) -> None:
285+
"""顺序执行 handler 链: 逐个绑定日志上下文, 结束后清理 event 引用"""
286+
try:
287+
for h, match in matched:
288+
plugin_name: str = h['name'] or h.get('_plugin', '')
289+
log_service = self._get_log_service(event)
290+
event._reply_log_cb = _make_reply_log_cb(plugin_name, log_service)
291+
event._reply_plugin_name = plugin_name or ''
292+
await self._exec_handler(h, event, match, plugin_name, user_id, et, content)
293+
finally:
294+
event.raw = event._reply_log_cb = None
261295

262-
async def _run_handler(
296+
async def _exec_handler(
263297
self,
264298
h: dict[str, Any],
265299
event: Event,

core/plugin/decorators.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ def handler(
2323
event_types=None,
2424
cooldown=0,
2525
ignore_at_check=False,
26+
block=False,
2627
):
27-
"""注册消息处理器"""
28+
"""注册消息处理器 (block=True 命中即拦截后续插件, 默认 False 放行)"""
2829

2930
def decorator(func):
3031
_pending_handlers.append(
@@ -43,6 +44,7 @@ def decorator(func):
4344
'event_types': frozenset(event_types) if event_types else None,
4445
'cooldown': cooldown,
4546
'ignore_at_check': ignore_at_check,
47+
'block': block,
4648
}
4749
)
4850
return func

core/storage/statistics.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import asyncio
1212
import contextlib
13+
import gc
1314
import json
1415
import os
1516
import re
@@ -252,9 +253,11 @@ def _aggregate_sync(self, appid, date_str):
252253
return False
253254

254255
self._merge_into_db(appid, date_str, day_user, day_group)
255-
log.info(
256-
f'[{appid}] {date_str} 已累加: 用户 {len(day_user)}, 群 {len(day_group)}'
257-
)
256+
user_count, group_count = len(day_user), len(day_group)
257+
# 统计完成后主动释放大对象并触发 GC, 避免内存长期占用不回收
258+
del day_user, day_group
259+
gc.collect()
260+
log.info(f'[{appid}] {date_str} 已累加: 用户 {user_count}, 群 {group_count}')
258261
return True
259262

260263
def _scan_day(self, db_path, date_str):

plugins/alone/示例插件.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,3 +411,16 @@ async def panel_broadcast(event, match):
411411
import web.ws as ws
412412
ws.broadcast({'type': 'notification', 'message': match.group(1)})
413413
await event.reply(f"✅ 已广播: {match.group(1)}")
414+
415+
416+
# block 示例: 两个处理器同注册 "^拦截示例$", 高优先级设 block=True 命中即拦截, 低优先级不会触发
417+
@handler(r'^拦截示例$', name='拦截示例-高优先级', desc='block=True 命中即拦截后续插件',
418+
priority=10, block=True)
419+
async def block_demo_high(event, match):
420+
await event.reply("🛑 高优先级处理器 (block=True): 已拦截, 低优先级不会再触发")
421+
422+
423+
@handler(r'^拦截示例$', name='拦截示例-低优先级', desc='被高优先级 block 拦截, 不会触发',
424+
priority=0)
425+
async def block_demo_low(event, match):
426+
await event.reply("⬇️ 低优先级处理器: 你不应该看到这条 (已被 block 拦截)")

tests/core/test_dispatch_block.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""分发 block 语义测试 — 多个插件注册相同指令时的放行/拦截行为"""
2+
3+
import asyncio
4+
import os
5+
import sys
6+
import tempfile
7+
8+
import pytest
9+
10+
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
11+
if ROOT not in sys.path:
12+
sys.path.insert(0, ROOT)
13+
14+
from core.base.config import cfg # noqa: E402
15+
from core.plugin.decorators import _pending_handlers, handler # noqa: E402
16+
from core.plugin.manager import PluginManager # noqa: E402
17+
from tests.stress.mocks.event_factory import EventFactory # noqa: E402
18+
from tests.stress.mocks.message_sender import MockMessageSender # noqa: E402
19+
20+
APPID = '102000001'
21+
22+
23+
@pytest.fixture(autouse=True)
24+
def _init_cfg():
25+
"""dispatch() 依赖全局 cfg.get_bot_setting()"""
26+
if not cfg._ready:
27+
cfg.init(os.path.join(ROOT, 'config'))
28+
29+
30+
def _build_two_handlers(sink, block_first):
31+
"""注册两个 pattern 相同的 handler (高优先级 First, 低优先级 Second)"""
32+
_pending_handlers.clear()
33+
34+
@handler(r'test', name='First', priority=10, block=block_first)
35+
async def _first(event, match):
36+
sink.append('first')
37+
38+
@handler(r'test', name='Second', priority=0)
39+
async def _second(event, match):
40+
sink.append('second')
41+
42+
collected = list(_pending_handlers)
43+
_pending_handlers.clear()
44+
return collected
45+
46+
47+
def _make_manager(handlers):
48+
tmp = tempfile.mkdtemp(prefix='block_test_')
49+
pm = PluginManager(plugins_dir=tmp, bot_appid=APPID)
50+
for h in handlers:
51+
h['_plugin'] = 'test_plugin'
52+
pm._all_handlers = sorted(handlers, key=lambda h: -h['priority'])
53+
pm._apply_bot_bindings()
54+
pm._build_dispatch_index()
55+
return pm
56+
57+
58+
async def _dispatch_and_collect(pm, content='test'):
59+
sender = MockMessageSender(APPID)
60+
event = EventFactory.group_at_message(content, appid=APPID)
61+
await pm.dispatch(event, sender)
62+
# 等待 fire-and-forget 的 handler 链执行完毕
63+
for _ in range(50):
64+
await asyncio.sleep(0.01)
65+
pending = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
66+
if not pending:
67+
break
68+
69+
70+
@pytest.mark.integration
71+
@pytest.mark.asyncio
72+
async def test_default_block_false_runs_all_matching_plugins():
73+
"""默认 block=False: 相同指令的多个插件都会响应 (按 priority 顺序)"""
74+
sink: list[str] = []
75+
pm = _make_manager(_build_two_handlers(sink, block_first=False))
76+
await _dispatch_and_collect(pm)
77+
assert sink == ['first', 'second']
78+
79+
80+
@pytest.mark.integration
81+
@pytest.mark.asyncio
82+
async def test_block_true_intercepts_subsequent_plugins():
83+
"""高优先级 handler block=True: 命中后拦截, 只有它响应"""
84+
sink: list[str] = []
85+
pm = _make_manager(_build_two_handlers(sink, block_first=True))
86+
await _dispatch_and_collect(pm)
87+
assert sink == ['first']

0 commit comments

Comments
 (0)