Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions ncatbot/plugin/loader/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ async def run_startup(self) -> None:

async def load_plugin(self, name: str) -> Optional[BasePlugin]:
"""加载单个插件(必须已索引)。"""
if not self._is_plugin_enabled(name):
LOG.info("跳过插件 %s(已在配置中禁用)", name)
return None

manifest = self._indexer.get(name)
if manifest is None:
LOG.error("插件 %s 未索引,无法加载", name)
Expand Down Expand Up @@ -410,6 +414,14 @@ async def load_builtin_plugins(self) -> None:
# 内部方法
# ------------------------------------------------------------------

def _is_plugin_enabled(self, name: str) -> bool:
"""按配置中的黑白名单判断插件是否允许加载(黑名单优先)。"""
plugin_config = get_config_manager().plugin
if name in plugin_config.plugin_blacklist:
return False
whitelist = plugin_config.plugin_whitelist
return not whitelist or name in whitelist

async def _check_pip_deps_batch(self, manifests: Dict[str, PluginManifest]) -> set:
"""批量检查所有插件的 pip 依赖,返回应跳过的插件名集合。"""
config = get_config_manager()
Expand Down
4 changes: 2 additions & 2 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ tests/
│ ├── api/ # API 客户端 + 错误层级 + Sugar (A-01 ~ A-02, AE-01 ~ AE-07, SG-01 ~ SG-06, FL-01 ~ FL-06)
│ ├── core/ # 核心分发与注册 + 谓词 (D-01 ~ D-09, K-01 ~ K-21, H-01 ~ H-11, R-01 ~ R-09, PR-01 ~ PR-06, CSE-01 ~ CSE-02)
│ ├── service/ # 服务管理 + RBAC + 调度 (SM-01 ~ SM-08, SC-01 ~ SC-12, TS-01 ~ TS-06)
│ ├── plugin/ # 插件 Mixin + 导入去重 + Loader (M-01 ~ M-41, ID-01 ~ ID-02, LD-01 ~ LD-05)
│ ├── plugin/ # 插件 Mixin + 导入去重 + Loader (M-01 ~ M-41, ID-01 ~ ID-02, LD-01 ~ LD-09)
│ ├── adapter/ # 适配器解析 + 注册表 + 真实数据 + 事件日志格式 (P-01 ~ P-07, RF-01 ~ RF-08, AR-01 ~ AR-05, SL-01 ~ SL-04, GM-01 ~ GM-05, BL-01 ~ BL-25, GH-01 ~ GH-11, LK-01 ~ LK-09, LKP-01 ~ LKP-10, ELS-01 ~ ELS-17)
│ ├── config/ # 配置迁移 + 安全 + 分层 + 事件日志格式 (CF-01 ~ CF-05, CS-01 ~ CS-05, CE-01 ~ CE-05, BQ-01 ~ BQ-11, AI-03 ~ AI-31, ELF-01 ~ ELF-06)
│ ├── cli/ # CLI 冒烟 (CX-01 ~ CX-22)
Expand Down Expand Up @@ -89,7 +89,7 @@ python tests/e2e/napcat/run.py
| SC | RBAC 权限系统 | SC-01 ~ SC-12 |
| PR | Predicate 谓词 DSL | PR-01 ~ PR-06 |
| TS | TimeTaskParser 调度解析 | TS-01 ~ TS-06 |
| LD | PluginLoader 生命周期 | LD-01 ~ LD-08 |
| LD | PluginLoader 生命周期 | LD-01 ~ LD-09 |
| CS | Config 安全检查 | CS-01 ~ CS-05 |
| SG | QQ Sugar 便捷方法 | SG-01 ~ SG-06 |
| MA | MessageArray 容器 | MA-01 ~ MA-04 |
Expand Down
44 changes: 43 additions & 1 deletion tests/unit/plugin/test_plugin_loader.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Plugin Loader 生命周期单元测试

LD-01 ~ LD-08:覆盖 load/unload/load_all/热重载消费 的核心路径。
LD-01 ~ LD-09:覆盖 load/unload/load_all/热重载消费 的核心路径。
"""

import asyncio
Expand Down Expand Up @@ -347,3 +347,45 @@ async def mock_load(name):
task.cancel()
with pytest.raises(asyncio.CancelledError):
await task


class TestPluginEnableConfig:
"""LD-09: plugin_blacklist / plugin_whitelist 控制插件是否加载"""

@pytest.mark.asyncio
@pytest.mark.parametrize(
("blacklist", "whitelist", "expected"),
[
(["b"], [], {"a", "c"}),
([], ["a", "c"], {"a", "c"}),
(["a"], ["a", "b"], {"b"}),
],
)
async def test_ld09_blacklist_whitelist_skip_load(
self, loader: PluginLoader, blacklist, whitelist, expected
):
"""LD-09: load_all 跳过黑名单中的插件,白名单非空时只加载白名单内的插件"""
manifests = {name: _make_manifest(name) for name in ("a", "b", "c")}
loader._indexer._manifests = manifests

config = MagicMock()
config.plugin.plugin_blacklist = blacklist
config.plugin.plugin_whitelist = whitelist

with (
patch("ncatbot.plugin.loader.core.get_config_manager", return_value=config),
patch.object(loader._indexer, "scan", return_value=manifests),
patch.object(loader._importer, "add_plugin_root"),
patch.object(loader._importer, "load_module") as mock_load,
patch.object(
loader._importer, "find_plugin_class", return_value=_StubPlugin
),
patch.object(loader, "_check_pip_deps_batch", return_value=set()),
patch("ncatbot.plugin.loader.core.flush_pending"),
patch("ncatbot.plugin.loader.core.clear_pending"),
):
loaded = await loader.load_all(Path("plugins"))

assert set(loaded) == expected
assert set(loader.plugins) == expected
assert {call.args[0].name for call in mock_load.call_args_list} == expected
Loading