Skip to content

Commit a2c0cfd

Browse files
authored
bugfix:修复命令前缀判断问题、修复超级用户无法豁免问题 (#2141)
* bugfix:修复命令前缀判断问题、修复超级用户无法豁免问题 * bugfix:修复格式问题
1 parent 8afc8f8 commit a2c0cfd

18 files changed

Lines changed: 223 additions & 50 deletions

zhenxun/builtin_plugins/hooks/auth/auth_limit.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ async def __reserve(
346346
limit = limit_model.limit
347347
limiter = limit_model.limiter
348348
is_limit = (
349-
LimitWatchType.ALL
349+
limit.watch_type == LimitWatchType.ALL
350350
or (group_id and limit.watch_type == LimitWatchType.GROUP)
351351
or (not group_id and limit.watch_type == LimitWatchType.USER)
352352
)

zhenxun/builtin_plugins/hooks/auth_activation.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class HandlerDescriptor:
7171
class AlconnaShortcutDescriptor:
7272
pattern: str
7373
fuzzy: bool = False
74+
prefix: bool = False
7475
flags: int = 0
7576

7677

@@ -1059,6 +1060,7 @@ def _extract_alconna_shortcut_descriptors(
10591060
AlconnaShortcutDescriptor(
10601061
pattern=pattern,
10611062
fuzzy=bool(getattr(args, "fuzzy", False)),
1063+
prefix=bool(getattr(args, "prefix", False)),
10621064
flags=int(getattr(args, "flags", 0) or 0),
10631065
)
10641066
)
@@ -1140,7 +1142,7 @@ def matcher_alconna_head_matches(
11401142
if decision == "unknown":
11411143
saw_unknown = True
11421144
for shortcut in alconna.shortcuts:
1143-
decision = _alconna_shortcut_matches(text, shortcut)
1145+
decision = _alconna_shortcut_matches(text, shortcut, alconna.prefixes)
11441146
if decision == "match":
11451147
return "match"
11461148
if decision == "unknown":
@@ -1194,11 +1196,43 @@ def _alconna_literal_head_matches(text: str, head: str, *, compact: bool) -> boo
11941196
def _alconna_shortcut_matches(
11951197
text: str,
11961198
shortcut: AlconnaShortcutDescriptor,
1199+
prefixes: tuple[str, ...] = (),
11971200
) -> ActivationDecision:
11981201
pattern = shortcut.pattern.strip()
11991202
if not pattern:
12001203
return "unknown"
1204+
saw_unknown = False
1205+
for normalized in _alconna_shortcut_patterns(pattern, shortcut, prefixes):
1206+
decision = _alconna_shortcut_pattern_matches(text, normalized, shortcut)
1207+
if decision == "match":
1208+
return "match"
1209+
if decision == "unknown":
1210+
saw_unknown = True
1211+
return "unknown" if saw_unknown else "miss"
1212+
1213+
1214+
def _alconna_shortcut_patterns(
1215+
pattern: str,
1216+
shortcut: AlconnaShortcutDescriptor,
1217+
prefixes: tuple[str, ...],
1218+
) -> tuple[str, ...]:
12011219
normalized = normalize_shortcut_pattern(pattern)
1220+
if not normalized:
1221+
return ()
1222+
patterns = [normalized]
1223+
if shortcut.prefix:
1224+
for prefix in prefixes or ("",):
1225+
candidate = normalize_shortcut_pattern(f"{prefix}{normalized}")
1226+
if candidate and candidate not in patterns:
1227+
patterns.append(candidate)
1228+
return tuple(patterns)
1229+
1230+
1231+
def _alconna_shortcut_pattern_matches(
1232+
text: str,
1233+
normalized: str,
1234+
shortcut: AlconnaShortcutDescriptor,
1235+
) -> ActivationDecision:
12021236
placeholder_match = _placeholder_shortcut_decision(text, normalized)
12031237
if placeholder_match == "match":
12041238
return "match"
@@ -1211,8 +1245,10 @@ def _alconna_shortcut_matches(
12111245
return "match"
12121246
try:
12131247
if shortcut.fuzzy:
1214-
return "match" if re.match(f"^{pattern}", text, shortcut.flags) else "miss"
1215-
return "match" if re.fullmatch(pattern, text, shortcut.flags) else "miss"
1248+
return (
1249+
"match" if re.match(f"^{normalized}", text, shortcut.flags) else "miss"
1250+
)
1251+
return "match" if re.fullmatch(normalized, text, shortcut.flags) else "miss"
12161252
except re.error:
12171253
return "unknown"
12181254

zhenxun/builtin_plugins/hooks/auth_checker.py

Lines changed: 36 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -496,10 +496,11 @@ def add(text: object) -> None:
496496
candidates.append(normalized)
497497

498498
add(_trie_command_text_from_state(state))
499-
add(_trie_raw_command_from_state(state))
499+
trie_raw = _trie_raw_command_from_state(state)
500+
add(trie_raw)
500501
trie_arg = _trie_command_arg_text_from_state(state)
501-
if _trie_raw_command_from_state(state) and trie_arg:
502-
add(f"{_trie_raw_command_from_state(state)} {trie_arg}")
502+
if trie_raw and trie_arg:
503+
add(f"{trie_raw} {trie_arg}")
503504
add(plain_text)
504505
if event is not None:
505506
with contextlib.suppress(Exception):
@@ -794,9 +795,10 @@ def _prepare_handle_event_state(event: Event, state: dict) -> None:
794795

795796

796797
def _build_matcher_state(base_state: dict) -> dict:
798+
# 第一次调用即在 base_state 写入副作用缓存对象;copy() 后 matcher_state
799+
# 与之共享同一引用,无需二次 get(B7)。
797800
get_permission_side_effect_cache(state=base_state)
798801
matcher_state = base_state.copy()
799-
get_permission_side_effect_cache(state=matcher_state)
800802
return matcher_state
801803

802804

@@ -965,25 +967,28 @@ async def _db_section():
965967
DB_ACTIVE_COUNT = max(DB_ACTIVE_COUNT - 1, 0)
966968

967969

970+
_POLICY_SKIP_MESSAGES = {
971+
"user_or_group_banned": "user or group banned (cached)",
972+
"superuser_required": "超级管理员权限不足...",
973+
"admin_required": "管理员权限不足...",
974+
"bot_not_found": "Bot不存在,阻断权限检测...",
975+
"bot_sleeping": "Bot休眠中阻断权限检测...",
976+
"bot_plugin_blocked": "Bot插件权限检查结果为关闭...",
977+
"group_not_found": "群组信息不存在...",
978+
"group_blacklisted": "群组黑名单, 目标群组群权限权限-1...",
979+
"group_sleeping": "群组休眠状态...",
980+
"group_level_low": "群等级限制...",
981+
"admin_level_low": "管理员权限不足...",
982+
"plugin_disabled_in_group": "该插件在群组中已被禁用...",
983+
"plugin_superuser_blocked_in_group": "超级管理员禁用了该群此功能...",
984+
"plugin_blocked_in_group": "该群未开启此功能...",
985+
"plugin_disabled_in_private": "该插件在私聊中已被禁用...",
986+
"plugin_global_disabled": "全局未开启此功能...",
987+
}
988+
989+
968990
def _policy_skip_message(reason: str) -> str:
969-
return {
970-
"user_or_group_banned": "user or group banned (cached)",
971-
"superuser_required": "超级管理员权限不足...",
972-
"admin_required": "管理员权限不足...",
973-
"bot_not_found": "Bot不存在,阻断权限检测...",
974-
"bot_sleeping": "Bot休眠中阻断权限检测...",
975-
"bot_plugin_blocked": "Bot插件权限检查结果为关闭...",
976-
"group_not_found": "群组信息不存在...",
977-
"group_blacklisted": "群组黑名单, 目标群组群权限权限-1...",
978-
"group_sleeping": "群组休眠状态...",
979-
"group_level_low": "群等级限制...",
980-
"admin_level_low": "管理员权限不足...",
981-
"plugin_disabled_in_group": "该插件在群组中已被禁用...",
982-
"plugin_superuser_blocked_in_group": "超级管理员禁用了该群此功能...",
983-
"plugin_blocked_in_group": "该群未开启此功能...",
984-
"plugin_disabled_in_private": "该插件在私聊中已被禁用...",
985-
"plugin_global_disabled": "全局未开启此功能...",
986-
}.get(reason, reason or "permission denied")
991+
return _POLICY_SKIP_MESSAGES.get(reason, reason or "permission denied")
987992

988993

989994
# 超时装饰器
@@ -1342,18 +1347,24 @@ async def _check_ban_from_snapshot(
13421347
hook_recorder: HookTraceRecorder,
13431348
session: Uninfo,
13441349
) -> None:
1350+
# skip_ban 上移到 cached 判断之前(A7),否则豁免参数对 cached 命中形同虚设。
1351+
if skip_ban:
1352+
hook_recorder.set("auth_ban", "skipped")
1353+
return
1354+
is_superuser = bool(getattr(prep.permission_context, "is_superuser", False))
13451355
ban_cache_state = prep.snapshot.ban_state
13461356
if event_cache is not None:
13471357
ban_cache_state = event_cache.get("ban_state")
13481358
if ban_cache_state is True:
1359+
# 超级用户豁免(A7):与 PDP / 旧轨权威路径保持一致,避免被 ban 后无法自救。
1360+
if is_superuser:
1361+
hook_recorder.set("auth_ban", "cached_superuser_exempt")
1362+
return
13491363
hook_recorder.set("auth_ban", "cached")
13501364
raise SkipPluginException("user or group banned (cached)")
13511365
if ban_cache_state is False:
13521366
hook_recorder.set("auth_ban", "cached")
13531367
return
1354-
if skip_ban:
1355-
hook_recorder.set("auth_ban", "skipped")
1356-
return
13571368

13581369
ban_start = time.time()
13591370
try:

zhenxun/builtin_plugins/hooks/auth_pipeline.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,11 @@ async def route_gate_stage(
246246
if deps.is_hidden_plugin(ctx.matcher):
247247
ctx.stop(allowed=True, effect="allow", reason="hidden_plugin")
248248
return
249-
if ctx.event_cache is not None and ctx.event_cache.get("ban_state") is True:
249+
if (
250+
ctx.event_cache is not None
251+
and ctx.event_cache.get("ban_state") is True
252+
and not ctx.event_context.is_superuser
253+
):
250254
ctx.decision_effect = "skip"
251255
ctx.decision_reason = "ban_cached"
252256
raise SkipPluginException("user or group banned (cached)")

zhenxun/builtin_plugins/hooks/auth_policy.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import contextlib
34
from dataclasses import dataclass, field
45
from typing import Any, Literal
56

@@ -116,8 +117,14 @@ def decide_bot(self, context: PolicyContext) -> PolicyDecision:
116117
if not bot_data.status and not context.allow_sleep_bypass:
117118
return PolicyDecision("deny", "bot_sleeping")
118119
module = snapshot.profile.module
119-
if module and self._module_in_block_string(module, bot_data.block_plugins):
120-
return PolicyDecision("deny", "bot_plugin_blocked")
120+
if module:
121+
value = bot_data.block_plugins or ""
122+
# 缓存解析后的 frozenset,避免每次 bot 检查重复 split(B8-3);
123+
# 仍保留原子串判定以保持行为等价。
124+
if CommonUtils.format(module) in value or module in self._bot_block_set(
125+
bot_data
126+
):
127+
return PolicyDecision("deny", "bot_plugin_blocked")
121128
return PolicyDecision("allow", "bot_allowed")
122129

123130
def decide_group(self, context: PolicyContext) -> PolicyDecision:
@@ -208,6 +215,17 @@ def _group_block_sets(group: object) -> tuple[frozenset[str], frozenset[str]]:
208215
setattr(group, "superuser_block_plugin_set", super_block_set)
209216
return block_set, super_block_set
210217

218+
@staticmethod
219+
def _bot_block_set(bot_data: object) -> frozenset[str]:
220+
block_set = getattr(bot_data, "block_plugin_set", None)
221+
if block_set is None:
222+
block_set = _parse_block_modules(
223+
getattr(bot_data, "block_plugins", "") or ""
224+
)
225+
with contextlib.suppress(Exception):
226+
setattr(bot_data, "block_plugin_set", block_set)
227+
return block_set
228+
211229
@staticmethod
212230
def _module_in_block_string(module: str, value: str | None) -> bool:
213231
if not value:

zhenxun/builtin_plugins/hooks/auth_runtime_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class AuthDispatchRuntimeConfig:
2323
circuit_reset_time: int = 300
2424
matcher_route_prefilter_ttl: int = 2
2525
prefilter_stats_log_interval: float = 10.0
26-
cache_sweep_interval: float = 1.0
26+
cache_sweep_interval: float = 45.0
2727
dispatch_stats_log_interval: float = 10.0
2828

2929

zhenxun/builtin_plugins/hooks/chkdsk_hook.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,11 @@ async def _(
8282
if matcher.type == "notice":
8383
return
8484

85+
# AI 重路由注入的合成事件不计入恶意检测(A6):AI 链路有自己的预算/审批,
86+
# 不应被人类反垃圾逻辑封禁(此前批量转发误封超级用户的事故根因之一)。
87+
if getattr(event, "_ai_triggered", False):
88+
return
89+
8590
# 提前判断插件类型,跳过不需要检测的插件
8691
if plugin := matcher.plugin:
8792
if metadata := plugin.metadata:
@@ -99,6 +104,13 @@ async def _(
99104

100105
user_id = resolve_actor_user_id(event, session.id1)
101106
group_id = resolve_event_group_id(event, session.id3 or session.id2)
107+
# 超级用户豁免恶意检测(A6):与权威权限路径保持一致,避免误封管理者。
108+
if user_id:
109+
is_superuser = state.get("_zx_is_superuser")
110+
if not isinstance(is_superuser, bool):
111+
is_superuser = user_id in bot.config.superusers
112+
if is_superuser:
113+
return
102114
malicious_check_time = float(_get_positive_config("MALICIOUS_CHECK_TIME", float))
103115
malicious_ban_count = int(_get_positive_config("MALICIOUS_BAN_COUNT", int))
104116
malicious_ban_time = int(_get_positive_config("MALICIOUS_BAN_TIME", int))

zhenxun/builtin_plugins/statistics/statistics_hook.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,12 @@ async def _flush_statistics_buffer(reason: str) -> int:
5353

5454

5555
async def _append_statistics(record: Statistics) -> None:
56-
TEMP_LIST.append(record)
57-
if len(TEMP_LIST) >= STATS_BUFFER_FLUSH_SIZE and not _STATS_FLUSH_LOCK.locked():
56+
# 在锁内追加(B8),与 flush 的 copy+clear 串行,消除逻辑窗口;
57+
# flush 自身再次获取同一把锁,故在锁外触发避免重入。
58+
async with _STATS_FLUSH_LOCK:
59+
TEMP_LIST.append(record)
60+
should_flush = len(TEMP_LIST) >= STATS_BUFFER_FLUSH_SIZE
61+
if should_flush:
5862
await _flush_statistics_buffer("缓冲区触发")
5963

6064

zhenxun/models/statistics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Statistics(Model):
1515
"""群聊id"""
1616
plugin_name = fields.CharField(255)
1717
"""插件名称"""
18-
create_time = fields.DatetimeField(auto_now=True)
18+
create_time = fields.DatetimeField(auto_now_add=True)
1919
"""添加日期"""
2020
bot_id = fields.CharField(255, null=True)
2121
"""Bot Id"""

zhenxun/models/user_console.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,11 @@ async def add_gold(
151151
source: 来源
152152
platform: 平台.
153153
"""
154-
user = await cls._get_user_for_write(user_id=user_id, platform=platform)
155-
user.gold += gold
156-
await user.save(update_fields=["gold"])
154+
await cls._get_user_for_write(user_id=user_id, platform=platform)
155+
# 原子自增,避免并发 read-modify-write 丢币(A2);filter().update()
156+
# 不触发基类 save() 的缓存失效,需手动失效。
157+
await cls.filter(user_id=user_id).update(gold=F("gold") + gold)
158+
await cls.invalidate_user_cache(user_id)
157159
await append_user_gold_log(
158160
user_id=user_id, gold=gold, handle=GoldHandle.GET, source=source
159161
)
@@ -182,8 +184,14 @@ async def reduce_gold(
182184
user = await cls._get_user_for_write(user_id=user_id, platform=platform)
183185
if user.gold < gold:
184186
raise InsufficientGold()
185-
user.gold -= gold
186-
await user.save(update_fields=["gold"])
187+
# 原子扣减 + gold__gte 守卫,防并发超扣(A2);未命中说明余额已被
188+
# 其他协程扣走,按金币不足处理。
189+
updated = await cls.filter(user_id=user_id, gold__gte=gold).update(
190+
gold=F("gold") - gold
191+
)
192+
if not updated:
193+
raise InsufficientGold()
194+
await cls.invalidate_user_cache(user_id)
187195
await append_user_gold_log(
188196
user_id=user_id, gold=gold, handle=handle, source=plugin_module
189197
)

0 commit comments

Comments
 (0)