refactor: extract is_ban_cached method to BanConsole

This commit is contained in:
HibiKier
2025-12-24 14:54:39 +08:00
parent ff0b37123e
commit be316a5caf
17 changed files with 478 additions and 1360 deletions
@@ -1,249 +0,0 @@
# 权限检查系统 - 缓存优化指南
## DataAccess 缓存机制说明
### 已实现的缓存功能
`DataAccess` 类已经实现了完整的 Redis 缓存机制,包括:
1. **自动缓存查询结果**
```python
# 第一次查询:从数据库获取并缓存
plugin = await plugin_dao.safe_get_or_none(module="example")
# 后续查询:直接从缓存获取(如果缓存未过期)
plugin = await plugin_dao.safe_get_or_none(module="example")
```
2. **空结果缓存(5分钟TTL)**
```python
# 查询不存在的记录时,会缓存空结果标记
# 避免频繁查询数据库确认记录不存在
user = await user_dao.safe_get_or_none(user_id="nonexistent")
# 5分钟内再次查询相同记录,直接从缓存返回 None
```
3. **缓存统计**
```python
from zhenxun.services.data_access import DataAccess
# 获取缓存统计信息
stats = DataAccess.get_cache_stats()
# 返回格式:
# [
# {
# "cache_type": "GLOBAL_ALL_PLUGINS",
# "hits": 1000, # 缓存命中次数
# "null_hits": 50, # 空结果缓存命中次数
# "misses": 200, # 缓存未命中次数
# "hit_rate": "84.00%" # 缓存命中率
# },
# ...
# ]
```
4. **自动缓存失效**
- 数据创建、更新、删除时自动清除相关缓存
- 支持手动清除:`await dao.clear_cache(**kwargs)`
## 权限检查中的缓存优化
### 当前缓存使用情况
在权限检查系统中,以下数据访问已经自动使用缓存:
1. **PluginInfo(插件信息)**
```python
plugin_dao = DataAccess(PluginInfo)
plugin = await plugin_dao.safe_get_or_none(module=module)
# ✅ 自动缓存,基于 module 字段
```
2. **UserConsole(用户信息)**
```python
user_dao = DataAccess(UserConsole)
user = await user_dao.get_by_func_or_none(
UserConsole.get_user, False, user_id=user_id
)
# ✅ 自动缓存,基于 user_id 字段
```
3. **GroupConsole(群组信息)**
```python
group_dao = DataAccess(GroupConsole)
group = await group_dao.safe_get_or_none(
group_id=group_id, channel_id__isnull=True
)
# ✅ 自动缓存,基于 group_id 字段
```
4. **BanConsole(ban记录)**
```python
ban_dao = DataAccess(BanConsole)
ban = await ban_dao.safe_get_or_none(
user_id=user_id, group_id=group_id
)
# ✅ 自动缓存,基于 user_id 和 group_id
```
5. **BotConsole(Bot信息)**
```python
bot_dao = DataAccess(BotConsole)
bot = await bot_dao.safe_get_or_none(bot_id=bot_id)
# ✅ 自动缓存,基于 bot_id 字段
```
6. **LevelUser(用户权限)**
```python
level_dao = DataAccess(LevelUser)
level = await level_dao.safe_get_or_none(
user_id=user_id, group_id=group_id
)
# ✅ 自动缓存,基于 user_id 和 group_id
```
### 优化建议
#### 1. 并行获取数据,提高缓存命中率
**当前实现**:
```python
# 串行获取,可能错过缓存优势
plugin = await plugin_dao.safe_get_or_none(module=module)
user = await user_dao.get_by_func_or_none(...)
group = await group_dao.safe_get_or_none(...)
```
**优化后**:
```python
# 并行获取,DataAccess 会自动处理缓存
plugin, user, group = await asyncio.gather(
plugin_dao.safe_get_or_none(module=module),
user_dao.get_by_func_or_none(UserConsole.get_user, False, user_id=user_id),
group_dao.safe_get_or_none(group_id=group_id, channel_id__isnull=True)
if group_id else asyncio.sleep(0)
)
```
#### 2. 提前获取所有需要的数据
**当前实现**:
```python
# group 数据在 hooks 执行时才获取
# 在 auth 函数中
group = None
if entity.group_id:
group = await group_dao.safe_get_or_none(...)
```
**优化后**:
```python
# 在获取 plugin 和 user 时,同时获取 group
# 统一数据获取,充分利用缓存
async def get_all_auth_data(module, user_id, group_id):
plugin_dao = DataAccess(PluginInfo)
user_dao = DataAccess(UserConsole)
group_dao = DataAccess(GroupConsole) if group_id else None
tasks = [
plugin_dao.safe_get_or_none(module=module),
user_dao.get_by_func_or_none(UserConsole.get_user, False, user_id=user_id),
]
if group_id and group_dao:
tasks.append(
group_dao.safe_get_or_none(group_id=group_id, channel_id__isnull=True)
)
results = await asyncio.gather(*tasks)
return results
```
#### 3. 监控缓存效果
**实现方案**:
```python
from zhenxun.services.data_access import DataAccess
async def auth(...):
# 记录开始时的缓存统计
cache_stats_before = DataAccess.get_cache_stats()
try:
# 执行权限检查
...
finally:
# 记录结束时的缓存统计
cache_stats_after = DataAccess.get_cache_stats()
# 分析缓存效果
for before, after in zip(cache_stats_before, cache_stats_after):
cache_type = after["cache_type"]
hits_diff = after["hits"] - before["hits"]
misses_diff = after["misses"] - before["misses"]
total = hits_diff + misses_diff
if total > 0:
hit_rate = (hits_diff / total) * 100
logger.debug(
f"缓存统计 - {cache_type}: "
f"命中率={hit_rate:.1f}%, "
f"命中={hits_diff}, 未命中={misses_diff}",
LOGGER_COMMAND
)
# 如果缓存命中率过低,记录警告
if hit_rate < 30:
logger.warning(
f"缓存命中率过低: {cache_type} = {hit_rate:.1f}%",
LOGGER_COMMAND
)
```
#### 4. 调整空结果缓存时间
**根据实际需求调整**:
```python
from zhenxun.services.data_access import DataAccess
# 对于频繁查询但可能不存在的记录,可以增加空结果缓存时间
# 例如:ban记录查询,如果用户未被ban,可以缓存更长时间
DataAccess.set_null_result_ttl(600) # 10分钟
# 对于需要实时性的数据,可以减少缓存时间
# 例如:用户权限查询
DataAccess.set_null_result_ttl(60) # 1分钟
```
## 缓存性能优化检查清单
- [ ] 所有数据访问都通过 `DataAccess`,而不是直接使用模型类
- [ ] 并行获取多个数据,而不是串行获取
- [ ] 提前获取所有需要的数据,避免在检查过程中重复获取
- [ ] 监控缓存命中率,确保缓存效果良好
- [ ] 根据实际需求调整空结果缓存时间
- [ ] 定期查看缓存统计,优化缓存策略
## 预期缓存效果
### 高并发场景下的缓存命中率预期
- **PluginInfo(插件信息)**:90-95%(插件信息变化不频繁)
- **UserConsole(用户信息)**:80-90%(用户信息相对稳定)
- **GroupConsole(群组信息)**:85-95%(群组信息变化不频繁)
- **BanConsole(ban记录)**:70-85%(ban状态可能变化)
- **BotConsole(Bot信息)**:95-99%(Bot信息很少变化)
- **LevelUser(用户权限)**:75-85%(权限可能变化)
### 性能提升预期
- **缓存命中时**:查询时间从 10-50ms 降低到 1-5ms(减少 80-90%)
- **总体性能**:在高并发场景下,总体执行时间减少 30-50%
- **数据库压力**:减少数据库查询 40-60%
## 注意事项
1. **缓存一致性**:DataAccess 在数据更新时会自动清除缓存,确保一致性
2. **缓存失效**:如果需要强制刷新缓存,可以使用 `await dao.clear_cache(**kwargs)`
3. **监控缓存**:定期查看缓存统计,确保缓存效果良好
4. **空结果缓存**:空结果缓存有助于减少对不存在记录的查询,但需要根据实际需求调整TTL
@@ -1,373 +0,0 @@
# 权限检查系统 - 立即可以实施的改进
## 当前问题总结
经过分析,当前权限检查系统存在以下主要问题:
### 1. 检查执行顺序不合理
- **问题**:所有检查并行执行,ban 检查应该优先执行
- **影响**:用户被 ban 后,其他检查(金币、限制等)仍然会执行,浪费资源
- **优先级**:高
### 2. 缺乏早期退出机制
- **问题**:即使某个检查失败,其他检查仍会继续执行
- **影响**:浪费 CPU 和数据库资源
- **优先级**:高
### 3. 数据获取可以优化
- **问题**:group 数据在 hooks 执行时才获取,可以提前获取
- **影响**:增加总体执行时间
- **优先级**:中
### 4. 充分利用 DataAccess 的 Redis 缓存
- **现状**:`DataAccess` 已经实现了完整的 Redis 缓存机制(包括空结果缓存 5 分钟)
- **问题**:没有充分利用缓存,数据获取顺序不够优化
- **影响**:缓存命中率可能不够高,重复查询数据库
- **优先级**:高
## 立即可以实施的改进
### 改进 1:调整检查顺序,ban 检查优先执行
**位置**:`auth_checker.py` 第 498-514 行
**当前代码**:
```python
hook_tasks = [
time_hook(auth_ban(...), "auth_ban", hook_times),
time_hook(auth_bot(...), "auth_bot", hook_times),
# ... 其他检查
]
await asyncio.gather(*hook_tasks) # 并行执行
```
**改进方案**:
```python
# 1. 先执行关键检查(ban、bot),如果失败立即退出
critical_checks = [
("auth_ban", lambda: time_hook(auth_ban(...), "auth_ban", hook_times)),
("auth_bot", lambda: time_hook(auth_bot(...), "auth_bot", hook_times)),
]
for check_name, check_func in critical_checks:
try:
await check_func()
except SkipPluginException:
raise # 立即退出
except Exception as e:
logger.error(f"{check_name} 检查失败: {e}", LOGGER_COMMAND, session=session)
# 关键检查失败,可以选择继续或退出
# 2. 关键检查通过后,再并行执行其他检查
other_checks = [
time_hook(auth_group(...), "auth_group", hook_times),
time_hook(auth_admin(...), "auth_admin", hook_times),
time_hook(auth_plugin(...), "auth_plugin", hook_times),
time_hook(auth_limit(...), "auth_limit", hook_times),
]
await asyncio.gather(*other_checks)
```
### 改进 2:提前获取 group 数据
**位置**:`auth_checker.py` 第 485-493 行
**当前代码**:
```python
# 在hooks执行时才获取group
group = None
if entity.group_id:
group_dao = DataAccess(GroupConsole)
group = await with_timeout(...)
```
**改进方案**:
```python
# 在获取plugin和user时,同时获取group
# 在 get_plugin_and_user 函数中或auth函数开始处
group = None
if entity.group_id:
group_dao = DataAccess(GroupConsole)
group_task = group_dao.safe_get_or_none(
group_id=entity.group_id, channel_id__isnull=True
)
# 可以并行获取
plugin, user, group = await asyncio.gather(
plugin_task, user_task, group_task
)
```
### 改进 3:充分利用 DataAccess 的 Redis 缓存机制
**重要说明**:`DataAccess` 已经实现了完整的 Redis 缓存机制,包括:
- ✅ 自动缓存查询结果
- ✅ 空结果缓存(5 分钟 TTL,避免频繁查询不存在的记录)
- ✅ 缓存统计(命中率、命中次数等)
- ✅ 自动处理缓存的获取和设置
**优化策略**:
#### 3.1 优化数据获取顺序,提高缓存命中率
**位置**:`auth_checker.py` 的 `get_plugin_and_user` 函数和 `auth` 函数
**当前问题**:
- plugin、user、group 数据分别获取,可能错过并行获取的缓存优势
- group 数据在 hooks 执行时才获取,应该提前获取
**改进方案**:
```python
async def get_plugin_user_and_group(
module: str, user_id: str, group_id: str | None
) -> tuple[PluginInfo, UserConsole, GroupConsole | None]:
"""统一获取插件、用户和群组数据(充分利用 DataAccess 缓存)
注意:DataAccess 会自动使用 Redis 缓存,包括:
- 查询结果缓存
- 空结果缓存(5分钟)
- 缓存统计
"""
user_dao = DataAccess(UserConsole)
plugin_dao = DataAccess(PluginInfo)
group_dao = DataAccess(GroupConsole) if group_id else None
# 并行获取所有数据,DataAccess 会自动处理缓存
tasks = [
plugin_dao.safe_get_or_none(module=module),
user_dao.get_by_func_or_none(
UserConsole.get_user, False, user_id=user_id
)
]
if group_id and group_dao:
tasks.append(
group_dao.safe_get_or_none(
group_id=group_id, channel_id__isnull=True
)
)
else:
tasks.append(asyncio.create_task(asyncio.sleep(0))) # 占位
try:
results = await with_timeout(
asyncio.gather(*tasks), name="get_plugin_user_group"
)
plugin = results[0]
user = results[1]
group = results[2] if len(results) > 2 and group_id else None
# 验证数据
if not plugin:
raise PermissionExemption(f"插件:{module} 数据不存在...")
if plugin.plugin_type == PluginType.HIDDEN:
raise PermissionExemption(f"插件: {plugin.name}:{plugin.module} 为HIDDEN...")
if not user:
raise PermissionExemption("用户数据不存在...")
return plugin, user, group
except asyncio.TimeoutError:
# 超时时尝试从缓存获取(DataAccess 已经缓存了)
logger.warning(
f"数据获取超时,尝试从缓存获取,模块: {module}",
LOGGER_COMMAND
)
# DataAccess 的缓存会自动生效,这里只需要重试一次
plugin = await plugin_dao.safe_get_or_none(module=module)
user = await user_dao.get_by_func_or_none(
UserConsole.get_user, False, user_id=user_id
)
group = None
if group_id and group_dao:
group = await group_dao.safe_get_or_none(
group_id=group_id, channel_id__isnull=True
)
if not plugin or not user:
raise PermissionExemption("获取数据失败,请稍后再试...")
return plugin, user, group
```
#### 3.2 监控缓存命中率
**位置**:在权限检查中添加缓存统计监控
**实现方案**:
```python
from zhenxun.services.data_access import DataAccess
# 在 auth 函数开始和结束时记录缓存统计
async def auth(...):
# 记录开始时的缓存统计
cache_stats_before = DataAccess.get_cache_stats()
try:
# ... 执行权限检查 ...
pass
finally:
# 记录结束时的缓存统计
cache_stats_after = DataAccess.get_cache_stats()
# 计算本次检查的缓存命中情况
for before, after in zip(cache_stats_before, cache_stats_after):
hits_diff = after["hits"] - before["hits"]
misses_diff = after["misses"] - before["misses"]
total = hits_diff + misses_diff
if total > 0:
hit_rate = (hits_diff / total) * 100
if hit_rate < 50: # 缓存命中率低于50%时记录警告
logger.warning(
f"缓存命中率较低: {after['cache_type']} = {hit_rate:.1f}%",
LOGGER_COMMAND
)
```
#### 3.3 优化检查函数中的数据访问
**说明**:各个检查函数(auth_ban、auth_bot 等)中使用的 `DataAccess` 已经自动缓存,无需额外处理。
**建议**:
- 确保所有数据访问都通过 `DataAccess`,而不是直接使用模型类
- 对于频繁查询的数据(如 ban 记录),`DataAccess` 会自动缓存
- 空结果也会被缓存 5 分钟,避免频繁查询不存在的记录
### 改进 4:统一错误处理策略
**位置**:各个检查函数
**当前问题**:
- `auth_ban` 超时时不阻塞
- `auth_bot` 超时时不阻塞
- `auth_limit` 超时时不阻塞
- 但 `get_plugin_and_user` 超时时会抛出异常
**改进方案**:
```python
# 定义统一的超时处理策略
class TimeoutStrategy:
FAIL_FAST = "fail_fast" # 超时立即失败
CONTINUE = "continue" # 超时继续执行
CACHE_FALLBACK = "cache_fallback" # 超时使用缓存
# 根据检查的重要性设置策略
CHECK_TIMEOUT_STRATEGY = {
"auth_ban": TimeoutStrategy.FAIL_FAST, # 关键检查,超时失败
"auth_bot": TimeoutStrategy.FAIL_FAST, # 关键检查,超时失败
"auth_group": TimeoutStrategy.CONTINUE, # 非关键,超时继续
"auth_limit": TimeoutStrategy.CONTINUE, # 非关键,超时继续
"get_plugin_and_user": TimeoutStrategy.CACHE_FALLBACK, # 使用缓存降级
}
```
## 实施优先级
### 高优先级(立即实施)
1. ✅ **调整检查顺序**:ban 检查优先执行
- 风险:低
- 收益:高
- 工作量:小(1-2 小时)
### 中优先级(本周实施)
2. ✅ **提前获取 group 数据,统一数据获取**
- 风险:低
- 收益:高(充分利用 DataAccess 缓存)
- 工作量:小(1-2 小时)
3. ✅ **优化数据获取顺序,提高缓存命中率**
- 风险:低(DataAccess 已实现缓存)
- 收益:高(减少数据库查询)
- 工作量:小(1 小时)
4. ✅ **添加缓存统计监控**
- 风险:低
- 收益:中(可以监控缓存效果)
- 工作量:小(30 分钟)
### 低优先级(后续优化)
4. ⏳ **统一错误处理策略**
- 风险:中(可能影响现有逻辑)
- 收益:中
- 工作量:中(3-4 小时)
## 代码示例:改进后的 auth 函数结构
```python
async def auth(...):
# 1. 获取所有需要的数据(并行)
plugin, user, group = await gather_all_data(...)
# 2. 关键检查(串行,失败立即退出)
await critical_checks(plugin, user, group, ...)
# 3. 其他检查(并行)
await other_checks(plugin, user, group, ...)
# 4. 金币检查(最后执行,超时不影响)
cost_gold = await check_cost(user, plugin, ...)
```
## 注意事项
1. **向后兼容**:确保改进不影响现有功能
2. **充分测试**:特别是缓存相关的改进
3. **监控指标**:记录改进前后的性能指标
4. **逐步实施**:先实施低风险改进,再实施高风险改进
## 预期效果
- **性能提升**:总体执行时间减少 20-30%
- **资源节省**:减少不必要的检查执行 30-50%
- **缓存优化**:充分利用 DataAccess 的 Redis 缓存,缓存命中率提升 40-60%
- **数据库压力**:减少数据库查询 30-50%(通过缓存)
- **用户体验**:响应速度提升,特别是在高并发场景下
## 关于 DataAccess 缓存的说明
### DataAccess 已实现的缓存机制
1. **自动缓存查询结果**
- 所有通过 `DataAccess.safe_get_or_none()` 等方法的查询结果都会自动缓存
- 缓存键基于模型的主键字段(如 `user_id`、`module` 等)
2. **空结果缓存**
- 查询结果为 `None` 时,会缓存一个特殊标记(`_NULL_RESULT`)
- 默认 TTL 为 5 分钟,避免频繁查询不存在的记录
- 可通过 `DataAccess.set_null_result_ttl()` 调整
3. **缓存统计**
- 自动统计缓存命中率、命中次数、未命中次数等
- 可通过 `DataAccess.get_cache_stats()` 获取统计信息
4. **缓存失效**
- 数据更新、创建、删除时自动清除相关缓存
- 支持手动清除:`DataAccess.clear_cache(**kwargs)`
### 优化建议
1. **统一使用 DataAccess**:确保所有数据访问都通过 `DataAccess`,而不是直接使用模型类
2. **并行获取数据**:利用 `asyncio.gather()` 并行获取多个数据,提高缓存命中率
3. **监控缓存效果**:定期查看 `DataAccess.get_cache_stats()`,了解缓存命中率
4. **调整空结果 TTL**:根据实际需求调整 `DataAccess.set_null_result_ttl()`,平衡缓存效果和实时性
@@ -1,234 +0,0 @@
# 权限检查系统优化方案
## 当前设计分析
### 优点
1. ✅ 模块化设计,每个检查独立
2. ✅ 并行执行检查,提高效率
3. ✅ 有超时控制和重试机制
4. ✅ 有缓存降级策略
5. ✅ 有并发控制(信号量)
### 存在的问题
#### 1. **缺乏优先级机制**
- 所有检查并行执行,没有优先级区分
- ban检查应该优先执行,如果用户被ban,其他检查就不需要执行
- 浪费计算资源
#### 2. **缺乏早期退出机制**
- 即使某个检查已经失败,其他检查仍然会继续执行
- 例如:用户被ban了,但金币检查、限制检查等仍然会执行
#### 3. **数据获取分散**
- 每个检查函数可能都需要查询数据库
- 缺乏统一的数据上下文管理
- 可能导致重复查询
#### 4. **错误处理不一致**
- 有些检查超时时抛出异常
- 有些检查超时时只是记录日志继续执行
- 缺乏统一的错误处理策略
#### 5. **缺乏检查结果缓存**
- 对于相同的请求(相同的user_id, group_id, plugin),可能会重复执行相同的检查
- ban检查、bot检查等结果可以缓存一段时间
## 优化方案
### 方案1:优先级分层检查(推荐)
#### 核心思想
将检查分为多个优先级阶段,高优先级检查失败时立即退出,不执行后续检查。
#### 检查优先级划分
```
阶段1(CRITICAL - 关键检查):
- auth_ban: 用户/群组ban检查
- auth_bot: Bot状态检查
→ 如果失败,立即退出
阶段2(HIGH - 高优先级):
- auth_plugin: 插件状态检查
- auth_group: 群组状态检查
→ 并行执行,任一失败则退出
阶段3(MEDIUM - 中等优先级):
- auth_admin: 管理员权限检查
- auth_limit: 限制检查
→ 并行执行,任一失败则退出
阶段4(LOW - 低优先级):
- auth_cost: 金币检查
→ 超时不影响其他检查
```
#### 实现要点
1. **统一数据上下文**
```python
@dataclass
class AuthContext:
"""统一管理所有需要的数据"""
plugin: PluginInfo
user: UserConsole
group: GroupConsole | None
bot_id: str
entity: EntityIDs
# ... 其他数据
```
2. **早期退出机制**
```python
# 阶段1检查
for check in critical_checks:
result = await execute_check(check)
if not result.success:
raise result.error # 立即退出,不执行后续检查
```
3. **检查结果缓存**
```python
# 对ban、bot等检查结果进行缓存
cache_key = f"ban:{user_id}:{group_id}:{module}"
if cached_result := get_cached_result(cache_key):
return cached_result
```
### 方案2:责任链模式
#### 核心思想
使用责任链模式,按优先级顺序执行检查,任一检查失败则中断链。
#### 优点
- 清晰的检查顺序
- 易于添加新的检查
- 支持动态调整检查顺序
#### 缺点
- 无法充分利用并行执行的优势
- 可能增加总体执行时间
### 方案3:混合模式(最佳实践)
#### 核心思想
结合优先级和并行执行:
- 关键检查(ban、bot)串行执行,失败立即退出
- 同优先级检查并行执行
- 不同优先级按顺序执行
## 具体优化建议
### 1. 立即实施的优化(低风险)
#### a) 调整检查顺序
```python
# 在 auth_checker.py 中调整检查顺序
# 1. 先执行ban检查(如果失败,立即退出)
# 2. 再执行其他检查
```
#### b) 统一数据获取
```python
# 在auth函数开始处,统一获取所有需要的数据
plugin, user, group = await gather_all_data(...)
# 然后传递给各个检查函数
```
#### c) 添加检查结果缓存
```python
# 对ban检查结果缓存30秒
@lru_cache(maxsize=1000, ttl=30)
async def check_ban_cached(user_id, group_id):
return await is_ban(user_id, group_id)
```
### 2. 中期优化(需要重构)
#### a) 实现优先级机制
- 将检查函数注册到优先级系统
- 按优先级顺序执行
- 高优先级失败时立即退出
#### b) 统一错误处理
- 定义统一的检查结果类型
- 所有检查返回统一格式的结果
- 统一的超时处理策略
### 3. 长期优化(架构级改进)
#### a) 检查结果持久化
- 将检查结果存储到Redis
- 支持分布式缓存
- 减少数据库查询
#### b) 异步检查队列
- 对于非关键检查,可以异步执行
- 使用消息队列处理检查任务
- 提高响应速度
## 性能提升预期
### 当前性能
- 所有检查并行执行:~200-500ms
- 高并发时可能超时
### 优化后预期
- 关键检查优先:~50-100ms(如果失败)
- 完整检查:~150-300ms
- 缓存命中:~10-20ms
### 资源节省
- 减少不必要的检查执行:~30-50%
- 减少数据库查询:~20-30%
- 提高并发处理能力:~40-60%
## 实施步骤
### 阶段1:准备(1-2天)
1. 分析当前检查函数的依赖关系
2. 确定优先级划分
3. 设计统一的数据上下文
### 阶段2:实现(3-5天)
1. 实现优先级机制
2. 实现早期退出机制
3. 实现检查结果缓存
4. 统一错误处理
### 阶段3:测试(2-3天)
1. 单元测试
2. 集成测试
3. 性能测试
4. 压力测试
### 阶段4:部署(1天)
1. 灰度发布
2. 监控性能指标
3. 逐步全量
## 风险评估
### 低风险
- 调整检查顺序
- 添加缓存
- 统一数据获取
### 中风险
- 实现优先级机制(需要充分测试)
- 早期退出机制(可能影响某些边界情况)
### 高风险
- 架构级重构(需要完整的测试覆盖)
## 建议
1. **先实施低风险优化**:立即调整检查顺序,添加缓存
2. **逐步实施中风险优化**:实现优先级机制,充分测试
3. **长期规划高风险优化**:根据实际需求决定是否进行架构级重构
## 参考实现
详见 `auth_checker_optimized.py`(设计示例,仅供参考)
+3 -19
View File
@@ -9,11 +9,9 @@ from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
from zhenxun.models.ban_console import BanConsole
from zhenxun.models.plugin_info import PluginInfo
from zhenxun.services.cache import CacheRoot
from zhenxun.services.data_access import DataAccess
from zhenxun.services.db_context import DB_TIMEOUT_SECONDS
from zhenxun.services.log import logger
from zhenxun.utils.enum import CacheType, PluginType
from zhenxun.utils.enum import PluginType
from zhenxun.utils.utils import EntityIDs, get_entity_ids
from .config import LOGGER_COMMAND, WARNING_THRESHOLD
@@ -164,23 +162,9 @@ async def auth_ban(
if entity.user_id in bot.config.superusers:
return
cache_key = f"{entity.user_id}_{entity.group_id}"
results = await CacheRoot.get(CacheType.BAN, cache_key)
results = await BanConsole.is_ban_cached(entity.user_id, entity.group_id)
if not results:
results = await BanConsole.is_ban(entity.user_id, entity.group_id)
await CacheRoot.set(
CacheType.BAN,
cache_key,
results or DataAccess._NULL_RESULT,
)
else:
if results == DataAccess._NULL_RESULT:
return
tmp_results: list[BanConsole] = []
for r in results:
tmp_results.append(CacheRoot._deserialize_value(r, BanConsole))
results = tmp_results
return
for result in results:
if not result.user_id and result.group_id:
+59
View File
@@ -0,0 +1,59 @@
import asyncio
import random
from arclet.alconna import Args
from nonebot import get_driver
from nonebot.adapters.onebot.v11 import (
Bot,
Event,
GroupMessageEvent,
Message,
PrivateMessageEvent,
)
from nonebot.compat import model_dump, type_validate_python
from nonebot_plugin_alconna import Alconna, on_alconna
from zhenxun.services.log import logger
tasks: set["asyncio.Task"] = set()
@get_driver().on_shutdown
async def cancel_tasks():
for task in tasks:
if not task.done():
task.cancel()
await asyncio.gather(
*(asyncio.wait_for(task, timeout=10) for task in tasks),
return_exceptions=True,
)
def push_event(bot: Bot, event: PrivateMessageEvent | GroupMessageEvent):
event.message = Message("签到")
event.user_id = random.randint(1, 99999999999) + random.randint(1, 99999999999)
task = asyncio.create_task(bot.handle_event(event))
task.add_done_callback(tasks.discard)
tasks.add(task)
logger.info(f"发送消息 --> {event.user_id} {event.message}")
return event
_matcher = on_alconna(
Alconna("test", Args["n", int]), priority=5, block=True, temp=True
)
@_matcher.handle()
async def handle_event(event: Event, bot: Bot, n: int):
for _ in range(n):
data = model_dump(event)
if data.get("message_type") == "private":
data["post_type"] = "message"
push_event(bot, type_validate_python(PrivateMessageEvent, data))
elif data.get("message_type") == "group":
data["post_type"] = "message"
push_event(bot, type_validate_python(GroupMessageEvent, data))
await asyncio.sleep(0.1)
logger.info(f"发送消息次数 --> {_ + 1}")
+34
View File
@@ -181,6 +181,40 @@ class BanConsole(Model):
return results
@classmethod
async def is_ban_cached(
cls, user_id: str | None, group_id: str | None
) -> list[Self]:
"""带缓存的 ban 状态检查
参数:
user_id: 用户id
group_id: 群组id
返回:
list[Self]: ban记录列表,空列表表示未被ban
"""
from zhenxun.services.cache import CacheRoot
from zhenxun.services.data_access import DataAccess
from zhenxun.utils.enum import CacheType
cache_key = f"{user_id}_{group_id}"
results = await CacheRoot.get(CacheType.BAN, cache_key)
if not results:
results = await cls.is_ban(user_id, group_id)
await CacheRoot.set(
CacheType.BAN,
cache_key,
results or DataAccess._NULL_RESULT,
)
return results
if results == DataAccess._NULL_RESULT:
return []
return [CacheRoot._deserialize_value(r, cls) for r in results]
@classmethod
async def ban(
cls,
-187
View File
@@ -1,187 +0,0 @@
from collections import OrderedDict
import random
from typing import Any
from nonebot import on_message
from nonebot.adapters import Event
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Image, UniMsg
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import Config
from zhenxun.configs.path_config import TEMP_PATH
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.enum import PluginType
from zhenxun.utils.image_utils import get_download_image_hash
from zhenxun.utils.message import MessageUtils
__plugin_meta__ = PluginMetadata(
name="复读",
description="群友的本质是什么?是复读机哒!",
usage="""
usage:
重复3次相同的消息时会复读
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.2",
menu_type="其他",
plugin_type=PluginType.DEPENDANT,
tasks=[Task(module="fudu", name="复读")],
ignore_prompt=True,
configs=[
RegisterConfig(
key="FUDU_PROBABILITY",
value=0.7,
help="复读概率",
default_value=0.7,
type=float,
),
RegisterConfig(
module="_task",
key="DEFAULT_FUDU",
value=True,
help="被动 复读 进群默认开关状态",
default_value=True,
type=bool,
),
],
).to_dict(),
)
class Fudu:
"""复读数据管理器,使用 LRU 策略限制内存占用"""
MAX_GROUPS = 500 # 最大缓存群组数量
def __init__(self):
# 使用 OrderedDict 实现 LRU 缓存
self._data: OrderedDict[str, dict[str, Any]] = OrderedDict()
def _get_or_create(self, key: str) -> dict[str, Any]:
"""获取或创建群组数据,同时维护 LRU 顺序"""
if key in self._data:
# 移动到末尾(最近使用)
self._data.move_to_end(key)
return self._data[key]
# 如果超出最大限制,删除最旧的条目
while len(self._data) >= self.MAX_GROUPS:
self._data.popitem(last=False)
self._data[key] = {"is_repeater": False, "data": []}
return self._data[key]
def append(self, key: str, content: str) -> None:
"""添加消息内容"""
self._get_or_create(key)["data"].append(content)
def clear(self, key: str) -> None:
"""清空群组的复读数据"""
group_data = self._get_or_create(key)
group_data["data"] = []
group_data["is_repeater"] = False
def size(self, key: str) -> int:
"""获取当前消息数量"""
return len(self._get_or_create(key)["data"])
def check(self, key: str, content: str) -> bool:
"""检查内容是否与第一条消息相同"""
data_list = self._get_or_create(key)["data"]
return bool(data_list) and data_list[0] == content
def get_first(self, key: str) -> str | None:
"""获取第一条消息内容"""
data_list = self._get_or_create(key)["data"]
return data_list[0] if data_list else None
def is_repeater(self, key: str) -> bool:
"""检查是否已经复读过"""
return self._get_or_create(key)["is_repeater"]
def set_repeater(self, key: str) -> None:
"""标记已复读"""
self._get_or_create(key)["is_repeater"] = True
_manager = Fudu()
base_config = Config.get("fudu")
async def rule(message: UniMsg, session: Uninfo, event: Event) -> bool:
"""消息匹配规则:仅匹配群聊中的有效消息"""
if not session.group:
return False
if event.is_tome():
return False
plain_text = message.extract_plain_text().strip()
image_list = [m.url for m in message if isinstance(m, Image) and m.url]
if not plain_text and not image_list:
return False
return not await CommonUtils.task_is_block(
session, "fudu", session.group.id if session.group else None
)
_matcher = on_message(rule=rule, priority=999)
@_matcher.handle()
async def _(message: UniMsg, session: Uninfo):
# rule 已经确保 session.group 存在
group_id = session.group.id # type: ignore
plain_text = message.extract_plain_text().strip()
image_list = [m.url for m in message if isinstance(m, Image) and m.url]
# 计算图片哈希(如果有图片)
img_hash = ""
if image_list:
img_hash = await get_download_image_hash(image_list[0], group_id)
add_msg = f"{plain_text}|-|{img_hash}"
# 更新复读状态
if _manager.size(group_id) == 0 or _manager.check(group_id, add_msg):
_manager.append(group_id, add_msg)
else:
_manager.clear(group_id)
_manager.append(group_id, add_msg)
# 检查是否触发复读
if _manager.size(group_id) <= 2:
return
if _manager.is_repeater(group_id):
return
if random.random() >= base_config.get("FUDU_PROBABILITY"):
return
# 20% 概率打断施法
if random.random() < 0.2:
is_interrupt = plain_text.replace("打断", "").strip() == "施法"
if plain_text.startswith("打断") and is_interrupt:
await MessageUtils.build_message(f"打断{plain_text}").finish()
else:
await MessageUtils.build_message("打断施法!").finish()
# 执行复读
_manager.set_repeater(group_id)
if image_list and plain_text:
result = MessageUtils.build_message(
[plain_text, TEMP_PATH / f"compare_download_{group_id}_img.jpg"]
)
elif image_list:
result = MessageUtils.build_message(
TEMP_PATH / f"compare_download_{group_id}_img.jpg"
)
elif plain_text:
result = MessageUtils.build_message(plain_text)
else:
return
await result.finish()
-283
View File
@@ -1,283 +0,0 @@
import base64
from collections import OrderedDict, deque
from pathlib import Path
import random
from typing import Any
from nonebot import on_message
from nonebot.adapters import Bot, Event
from nonebot.adapters.onebot.v11 import Message as V11Message
from nonebot.adapters.onebot.v11 import MessageSegment
from nonebot.plugin import PluginMetadata
from nonebot.utils import run_sync
from nonebot_plugin_alconna import Image as alcImg
from nonebot_plugin_alconna import UniMsg
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import BotConfig, Config
from zhenxun.configs.path_config import DATA_PATH, TEMP_PATH
from zhenxun.configs.utils import PluginExtraData, RegisterConfig, Task
from zhenxun.services.log import logger
from zhenxun.utils.common_utils import CommonUtils
from zhenxun.utils.enum import PluginType
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.image_utils import get_img_hash
from zhenxun.utils.message import MessageUtils
FUDU_IMAGE_PATH = DATA_PATH / "fudu"
FUDU_IMAGE_PATH.mkdir(parents=True, exist_ok=True)
FUDU_CACHE_PATH = TEMP_PATH / "fudu"
FUDU_CACHE_PATH.mkdir(parents=True, exist_ok=True)
# 支持的图片扩展名
IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
__plugin_meta__ = PluginMetadata(
name="复读",
description="群友的本质是什么?是复读机哒!",
usage="""
usage:
重复3次相同的消息时会复读
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.3",
menu_type="其他",
plugin_type=PluginType.DEPENDANT,
tasks=[Task(module="fudu", name="复读")],
ignore_prompt=True,
configs=[
RegisterConfig(
key="FUDU_PROBABILITY",
value=0.8,
help="复读概率",
default_value=0.8,
type=float,
),
RegisterConfig(
module="_task",
key="DEFAULT_FUDU",
value=True,
help="被动 复读 进群默认开关状态",
default_value=True,
type=bool,
),
RegisterConfig(
key="FUDU_TRIGGER_COUNT",
value=3,
help="触发复读所需的消息重复次数",
default_value=3,
type=int,
),
RegisterConfig(
key="FUDU_BREAK_PROBABILITY",
value=0.2,
help="打断复读的概率(基于复读概率)",
default_value=0.2,
type=float,
),
RegisterConfig(
key="FUDU_BREAK_TEXTS",
value=["打断施法!"],
help="用于打断复读时随机发送的文本列表",
type=list[str],
),
RegisterConfig(
key="FUDU_BREAK_USE_IMAGE",
value=True,
help="是否启用图片作为打断复读的内容 (图片存放于 data/fudu/ 目录下)",
default_value=True,
type=bool,
),
RegisterConfig(
key="FUDU_BREAK_USE_TEXT",
value=True,
help="是否启用文本作为打断复读的内容",
default_value=True,
type=bool,
),
],
).to_dict(),
)
class Fudu:
"""复读数据管理器,使用 LRU 策略限制内存占用"""
MAX_GROUPS = 500 # 最大缓存群组数量
def __init__(self):
self._data: OrderedDict[str, dict[str, Any]] = OrderedDict()
def _get_or_create(self, key: str) -> dict[str, Any]:
"""获取或创建群组数据,同时维护 LRU 顺序"""
if key in self._data:
self._data.move_to_end(key)
return self._data[key]
# 超出限制时删除最旧的条目
while len(self._data) >= self.MAX_GROUPS:
self._data.popitem(last=False)
trigger_count = Config.get_config("fudu", "FUDU_TRIGGER_COUNT", 3)
self._data[key] = {
"is_repeater": False,
"data": deque(maxlen=trigger_count),
"message_obj": None,
"reply_info": None,
}
return self._data[key]
def append(self, key: str, content: str, msg_obj: Any, reply_info: Any) -> None:
"""添加消息内容及原始消息对象"""
group_data = self._get_or_create(key)
group_data["data"].append(content)
group_data["message_obj"] = msg_obj
group_data["reply_info"] = reply_info
def clear(self, key: str) -> None:
"""清空群组的复读数据"""
group_data = self._get_or_create(key)
group_data["data"].clear()
group_data["is_repeater"] = False
group_data["message_obj"] = None
group_data["reply_info"] = None
def size(self, key: str) -> int:
"""获取当前消息数量"""
return len(self._get_or_create(key)["data"])
def check(self, key: str, content: str) -> bool:
"""检查内容是否与第一条消息相同"""
data_list = self._get_or_create(key)["data"]
return bool(data_list) and data_list[0] == content
def get_repeat_target(self, key: str) -> tuple[Any, Any]:
"""获取要复读的消息对象和回复信息"""
group_data = self._get_or_create(key)
return group_data["message_obj"], group_data["reply_info"]
def is_repeater(self, key: str) -> bool:
"""检查是否已经复读过"""
return self._get_or_create(key)["is_repeater"]
def set_repeater(self, key: str) -> None:
"""标记已复读"""
self._get_or_create(key)["is_repeater"] = True
_manager = Fudu()
base_config = Config.get("fudu")
def get_break_images() -> list[Path]:
"""获取打断复读用的图片列表"""
if not FUDU_IMAGE_PATH.exists():
return []
return [
f
for f in FUDU_IMAGE_PATH.iterdir()
if f.is_file() and f.suffix.lower() in IMAGE_EXTENSIONS
]
async def send_break_response() -> None:
"""发送打断复读的响应"""
response_pool: list[str | Path] = []
if base_config.get("FUDU_BREAK_USE_TEXT"):
if break_texts := base_config.get("FUDU_BREAK_TEXTS"):
response_pool.extend(break_texts)
if base_config.get("FUDU_BREAK_USE_IMAGE"):
response_pool.extend(get_break_images())
if not response_pool:
response_pool.append("打断施法!")
response = random.choice(response_pool)
if isinstance(response, Path):
file_data = f"base64://{base64.b64encode(response.read_bytes()).decode()}"
await _matcher.finish(
MessageSegment("image", {"file": file_data, "sub_type": "1"})
)
else:
await MessageUtils.build_message(response).finish()
async def rule(message: UniMsg, session: Uninfo, event: Event) -> bool:
"""消息匹配规则:仅匹配群聊中的有效消息"""
if not session.group:
return False
if event.is_tome():
return False
plain_text = message.extract_plain_text()
image_list = [m.url for m in message if isinstance(m, alcImg) and m.url]
if not plain_text and not image_list:
return False
return not await CommonUtils.task_is_block(session, "fudu")
_matcher = on_message(rule=rule, priority=999)
@_matcher.handle()
async def _(bot: Bot, message: UniMsg, event: Event, session: Uninfo):
# rule 已确保 session.group 存在
group_id = session.group.id # type: ignore
raw_message = event.get_message()
reply_info = getattr(event, "reply", None)
plain_text = message.extract_plain_text()
image_list = [m.url for m in message if isinstance(m, alcImg) and m.url]
# 检测虚空艾特
if plain_text and plain_text.startswith(f"@可爱的{BotConfig.self_nickname}"):
await MessageUtils.build_message("复制粘贴的虚空艾特?").send(reply_to=True)
# 计算图片哈希
img_hash = ""
if image_list:
temp_image_path = FUDU_CACHE_PATH / f"fudu_cache_{group_id}.jpg"
try:
if await AsyncHttpx.download_file(image_list[0], temp_image_path):
img_hash = await run_sync(get_img_hash)(temp_image_path)
except Exception as e:
logger.warning("下载复读图片以获取Hash时出错", "复读", e=e)
add_msg = f"{plain_text}|-|{img_hash}"
# 更新复读状态
if _manager.size(group_id) == 0 or _manager.check(group_id, add_msg):
_manager.append(group_id, add_msg, raw_message, reply_info)
else:
_manager.clear(group_id)
_manager.append(group_id, add_msg, raw_message, reply_info)
# 检查是否触发复读
trigger_count = base_config.get("FUDU_TRIGGER_COUNT")
if _manager.size(group_id) < trigger_count:
return
if _manager.is_repeater(group_id):
return
if random.random() >= base_config.get("FUDU_PROBABILITY"):
return
# 判断是否打断复读
if random.random() < base_config.get("FUDU_BREAK_PROBABILITY"):
_manager.clear(group_id)
await send_break_response()
# 执行复读
message_to_send, reply = _manager.get_repeat_target(group_id)
if not message_to_send:
return
_manager.set_repeater(group_id)
# 处理回复消息
if reply and isinstance(message_to_send, V11Message):
message_to_send.insert(0, MessageSegment.reply(reply.message_id))
await _matcher.finish(message_to_send)
+20
View File
@@ -0,0 +1,20 @@
from pathlib import Path
import nonebot
from nonebot.plugin import PluginMetadata
from zhenxun.configs.utils import PluginExtraData
from zhenxun.utils.enum import PluginType
__plugin_meta__ = PluginMetadata(
name="刷屏禁言检测",
description="",
usage="",
extra=PluginExtraData(
author="HibiKier",
version="0.1-473ecd8",
plugin_type=PluginType.PARENT,
).to_dict(),
)
nonebot.load_plugins(str(Path(__file__).parent.resolve()))
+122
View File
@@ -0,0 +1,122 @@
import time
from pydantic import BaseModel
import ujson as json
from zhenxun.configs.config import Config
from zhenxun.configs.path_config import DATA_PATH
base_config = Config.get("mute_setting")
class GroupData(BaseModel):
count: int
"""次数"""
time: int
"""检测时长"""
duration: int
"""禁言时长"""
message_data: dict = {}
"""消息存储"""
class MuteManager:
file = DATA_PATH / "group_mute_data.json"
def __init__(self) -> None:
self._group_data: dict[str, GroupData] = {}
if self.file.exists():
_data = json.load(open(self.file))
for gid in _data:
self._group_data[gid] = GroupData(
count=_data[gid]["count"],
time=_data[gid]["time"],
duration=_data[gid]["duration"],
)
def get_group_data(self, group_id: str) -> GroupData:
"""获取群组数据
参数:
group_id: 群组id
返回:
GroupData: GroupData
"""
if group_id not in self._group_data:
self._group_data[group_id] = GroupData(
count=base_config.get("MUTE_DEFAULT_COUNT", 10) or 10,
time=base_config.get("MUTE_DEFAULT_TIME", 7) or 7,
duration=base_config.get("MUTE_DEFAULT_DURATION", 10) or 10,
)
return self._group_data[group_id]
def reset(self, user_id: str, group_id: str):
"""重置用户检查次数
参数:
user_id: 用户id
group_id: 群组id
"""
if group_data := self._group_data.get(group_id):
if user_id in group_data.message_data:
group_data.message_data[user_id]["count"] = 0
def save_data(self):
"""保存数据"""
data = {}
for gid in self._group_data:
data[gid] = {
"count": self._group_data[gid].count,
"time": self._group_data[gid].time,
"duration": self._group_data[gid].duration,
}
with open(self.file, "w") as f:
json.dump(data, f, indent=4, ensure_ascii=False)
def add_message(self, user_id: str, group_id: str, message: str) -> int:
"""添加消息
参数:
user_id: 用户id
group_id: 群组id
message: 消息内容
返回:
int: 禁言时长
"""
if group_id not in self._group_data:
self._group_data[group_id] = GroupData(
count=base_config.get("MUTE_DEFAULT_COUNT"),
time=base_config.get("MUTE_DEFAULT_TIME"),
duration=base_config.get("MUTE_DEFAULT_DURATION"),
)
group_data = self._group_data[group_id]
if group_data.duration == 0:
return 0
message_data = group_data.message_data
if not message_data.get(user_id):
message_data[user_id] = {
"time": time.time(),
"count": 1,
"message": message,
}
else:
if message.find(message_data[user_id]["message"]) != -1:
message_data[user_id]["count"] += 1
else:
message_data[user_id]["time"] = time.time()
message_data[user_id]["count"] = 1
message_data[user_id]["message"] = message
if time.time() - message_data[user_id]["time"] > group_data.time:
message_data[user_id]["time"] = time.time()
message_data[user_id]["count"] = 1
if (
message_data[user_id]["count"] > group_data.count
and time.time() - message_data[user_id]["time"] < group_data.time
):
return group_data.duration
return 0
mute_manager = MuteManager()
+78
View File
@@ -0,0 +1,78 @@
from nonebot import on_message
from nonebot.adapters import Bot
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Image, UniMsg
from nonebot_plugin_uninfo import Uninfo
from zhenxun.configs.config import BotConfig
from zhenxun.configs.utils import PluginExtraData
from zhenxun.models.ban_console import BanConsole
from zhenxun.services.data_access import DataAccess
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.image_utils import get_download_image_hash
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.platform import PlatformUtils
from zhenxun.utils.utils import FreqLimiter, get_entity_ids
from ._data_source import mute_manager
__plugin_meta__ = PluginMetadata(
name="刷屏监听",
description="",
usage="",
extra=PluginExtraData(
author="HibiKier",
version="0.1-473ecd8",
menu_type="其他",
plugin_type=PluginType.DEPENDANT,
).to_dict(),
)
async def rule(session: Uninfo) -> bool:
entity_ids = get_entity_ids(session)
if not session.group:
return False
ban_dao = DataAccess(BanConsole)
if not await ban_dao.safe_get_or_none(
user_id=entity_ids.user_id, group_id=entity_ids.group_id
):
return False
if not await ban_dao.safe_get_or_none(user_id="", group_id=entity_ids.group_id):
return False
return True
_matcher = on_message(rule=rule, priority=1, block=False)
_flmt = FreqLimiter(30)
@_matcher.handle()
async def _(bot: Bot, session: Uninfo, message: UniMsg):
entity_ids = get_entity_ids(session)
plain_text = message.extract_plain_text()
image_list = [m.url for m in message if isinstance(m, Image) and m.url]
img_hash = ""
for url in image_list:
img_hash += await get_download_image_hash(url, "_mute_")
_message = plain_text + img_hash
if duration := mute_manager.add_message(
entity_ids.user_id, entity_ids.group_id or "0", _message
):
try:
if _flmt.check(entity_ids.user_id):
_flmt.start_cd(entity_ids.user_id)
await PlatformUtils.ban_user(
bot, entity_ids.user_id, entity_ids.group_id or "0", duration
)
await MessageUtils.build_message(
f"检测到恶意刷屏,{BotConfig.self_nickname}要把你关进小黑屋!"
).send(at_sender=True)
mute_manager.reset(entity_ids.user_id, entity_ids.group_id or "0")
logger.info(
f"检测刷屏 被禁言 {duration} 分钟", "禁言检查", session=session
)
except Exception as e:
logger.error("禁言发送错误", "禁言检测", session=session, e=e)
+117
View File
@@ -0,0 +1,117 @@
from nonebot.plugin import PluginMetadata
from nonebot_plugin_alconna import Alconna, Args, Arparma, Match, Option, on_alconna
from nonebot_plugin_session import EventSession
from zhenxun.configs.config import BotConfig
from zhenxun.configs.utils import PluginExtraData, RegisterConfig
from zhenxun.services.log import logger
from zhenxun.utils.enum import PluginType
from zhenxun.utils.message import MessageUtils
from zhenxun.utils.rules import ensure_group
from ._data_source import base_config, mute_manage
__plugin_meta__ = PluginMetadata(
name="刷屏禁言",
description="刷屏禁言相关操作",
usage=f"""
刷屏禁言相关操作,需要 {BotConfig.self_nickname} 有群管理员权限
指令:
刷屏设置: 查看当前设置
-c [count]: 检测最大次数
-t [time]: 规定时间内
-d [duration]: 禁言时长
示例:
刷屏设置 -c 10: 设置最大次数为10
刷屏设置 -t 100 -d 20: 设置规定时间和禁言时长
刷屏设置 -d 10: 设置禁言时长为10
* 即 X 秒内发送同样消息 N 次,禁言 M 分钟 *
""".strip(),
extra=PluginExtraData(
author="HibiKier",
version="0.1-473ecd8",
menu_type="其他",
plugin_type=PluginType.ADMIN,
admin_level=base_config.get("MUTE_LEVEL", 5),
configs=[
RegisterConfig(
key="MUTE_LEVEL",
value=5,
help="更改禁言设置的管理权限",
default_value=5,
type=int,
),
RegisterConfig(
key="MUTE_DEFAULT_COUNT",
value=10,
help="刷屏禁言默认检测次数",
default_value=10,
type=int,
),
RegisterConfig(
key="MUTE_DEFAULT_TIME",
value=7,
help="刷屏检测默认规定时间",
default_value=7,
type=int,
),
RegisterConfig(
key="MUTE_DEFAULT_DURATION",
value=10,
help="刷屏检测默禁言时长(分钟)",
default_value=10,
type=int,
),
],
).to_dict(),
)
_setting_matcher = on_alconna(
Alconna(
"刷屏设置",
Option("-t|--time", Args["time", int], help_text="检测时长"),
Option("-c|--count", Args["count", int], help_text="检测次数"),
Option("-d|--duration", Args["duration", int], help_text="禁言时长"),
),
rule=ensure_group,
block=True,
priority=5,
)
@_setting_matcher.handle()
async def _(
session: EventSession,
arparma: Arparma,
time: Match[int],
count: Match[int],
duration: Match[int],
):
group_id = session.id2
if not session.id1 or not group_id:
return
_time = time.result if time.available else None
_count = count.result if count.available else None
_duration = duration.result if duration.available else None
group_data = mute_manage.get_group_data(group_id)
if _time is None and _count is None and _duration is None:
await MessageUtils.build_message(
f"最大次数:{group_data.count} 次\n"
f"规定时间:{group_data.time} 秒\n"
f"禁言时长:{group_data.duration:.2f} 分钟\n"
f"【在规定时间内发送相同消息超过最大次数则禁言\n当禁言时长为0时关闭此功能】"
).finish(reply_to=True)
if _time is not None:
group_data.time = _time
if _count is not None:
group_data.count = _count
if _duration is not None:
group_data.duration = _duration
await MessageUtils.build_message("设置成功!").send(reply_to=True)
logger.info(
f"设置禁言配置 time: {_time}, count: {_count}, duration: {_duration}",
arparma.header_result,
session=session,
)
mute_manage.save_data()
+2
View File
@@ -64,6 +64,8 @@ async def _():
_client = get_async_client(
headers=get_user_agent(),
follow_redirects=True,
limits=httpx.Limits(max_connections=200, max_keepalive_connections=100),
timeout=httpx.Timeout(10),
**client_kwargs,
)
+12
View File
@@ -3,6 +3,7 @@ from io import BytesIO
from pathlib import Path
import nonebot
from nonebot.adapters import Bot
from nonebot.adapters.onebot.v11 import Message, MessageSegment
from nonebot_plugin_alconna import (
At,
@@ -16,6 +17,7 @@ from nonebot_plugin_alconna import (
Video,
Voice,
)
from nonebot_plugin_uninfo import Uninfo
from pydantic import BaseModel
import ujson as json
@@ -104,22 +106,32 @@ class MessageUtils:
cls,
msg_list: MESSAGE_TYPE | list[MESSAGE_TYPE | list[MESSAGE_TYPE]],
format_args: dict | None = None,
auto_forward_msg: Bot | Uninfo | None = None,
) -> UniMessage:
"""构造消息
参数:
msg_list: 消息列表
format_args: 用于格式化字符串的参数字典.
auto_forward_msg: 是否自动转发消息
返回:
UniMessage: 构造完成的消息列表
"""
from zhenxun.utils.platform import PlatformUtils
message_list = []
if not isinstance(msg_list, list):
msg_list = [msg_list]
for m in msg_list:
_data = m if isinstance(m, list) else [m]
message_list += cls.__build_message(_data, format_args)
if auto_forward_msg and PlatformUtils.is_forward_merge_supported(
auto_forward_msg
):
message_list = cls.alc_forward_msg(
message_list, auto_forward_msg.self_id, auto_forward_msg.self_id
)
return UniMessage(message_list)
@classmethod
+2 -1
View File
@@ -18,7 +18,6 @@ from zhenxun.models.friend_user import FriendUser
from zhenxun.models.group_console import GroupConsole
from zhenxun.services.log import logger
from zhenxun.utils.exception import NotFindSuperuser
from zhenxun.utils.http_utils import AsyncHttpx
from zhenxun.utils.message import MessageUtils
driver = nonebot.get_driver()
@@ -226,6 +225,8 @@ class PlatformUtils:
user_id: 用户id
platform: 平台
"""
from zhenxun.utils.http_utils import AsyncHttpx
url = None
if platform == "qq":
if user_id.isdigit():
+17 -11
View File
@@ -9,6 +9,7 @@ from types import TracebackType
from typing import Any, ClassVar
import httpx
from nonebot_plugin_session import EventSession, Session
from nonebot_plugin_uninfo import Uninfo
import pypinyin
@@ -209,7 +210,7 @@ def is_valid_date(date_text: str, separator: str = "-") -> bool:
return False
def get_entity_ids(session: Uninfo) -> EntityIDs:
def get_entity_ids(session: Uninfo | EventSession) -> EntityIDs:
"""获取用户id,群组id,频道id
参数:
@@ -218,16 +219,21 @@ def get_entity_ids(session: Uninfo) -> EntityIDs:
返回:
EntityIDs: 用户id,群组id,频道id
"""
user_id = session.user.id
group_id = None
channel_id = None
if session.group:
if session.group.parent:
group_id = session.group.parent.id
channel_id = session.group.id
else:
group_id = session.group.id
return EntityIDs(user_id=user_id, group_id=group_id, channel_id=channel_id)
if isinstance(session, Session):
user_id = session.id1
group_id = session.id2
channel_id = session.id3
else:
user_id = session.user.id
group_id = session.group.id if session.group else None
channel_id = session.channel.id if session.channel else None
if session.group:
if session.group.parent:
group_id = session.group.parent.id
channel_id = session.group.id
else:
group_id = session.group.id
return EntityIDs(user_id=user_id or "", group_id=group_id, channel_id=channel_id)
def is_number(text: str) -> bool:
+12 -3
View File
@@ -8,9 +8,11 @@ from nonebot.adapters import Bot
from nonebot.adapters.onebot.v11 import Bot as v11Bot
from nonebot.adapters.onebot.v12 import Bot as v12Bot
from nonebot_plugin_session import EventSession
from nonebot_plugin_uninfo import Uninfo
from ruamel.yaml.comments import CommentedSeq
from zhenxun.services.log import logger
from zhenxun.utils.utils import get_entity_ids
class WithdrawManager:
@@ -18,7 +20,9 @@ class WithdrawManager:
_index = 0
@classmethod
def check(cls, session: EventSession, withdraw_time: tuple[int, int]) -> bool:
def check(
cls, session: Uninfo | EventSession, withdraw_time: tuple[int, int]
) -> bool:
"""配置项检查
参数:
@@ -28,12 +32,17 @@ class WithdrawManager:
返回:
bool: 是否允许撤回
"""
entity_ids = get_entity_ids(session)
if withdraw_time[0] and withdraw_time[0] > 0:
if withdraw_time[1] == 2:
return True
if withdraw_time[1] == 1 and (session.id2 or session.id3):
if withdraw_time[1] == 1 and entity_ids.group_id:
return True
if withdraw_time[1] == 0 and not session.id2 and not session.id3:
if (
withdraw_time[1] == 0
and not entity_ids.group_id
and not entity_ids.channel_id
):
return True
return False