Skip to content

fix:fix the issue where incomplete cleanup of residual plugins occurs…#5462

Merged
Soulter merged 3 commits intoAstrBotDevs:masterfrom
Waterwzy:issue/5461
Feb 26, 2026
Merged

fix:fix the issue where incomplete cleanup of residual plugins occurs…#5462
Soulter merged 3 commits intoAstrBotDevs:masterfrom
Waterwzy:issue/5461

Conversation

@Waterwzy
Copy link
Contributor

@Waterwzy Waterwzy commented Feb 25, 2026

… in the failed loading of plugins

Modifications / 改动点

Close:#5461

#5334 #5043 中,失败插件的重载均没有移除插件的模块,handlers和插件工具

astrbot/core/star/star_manager.py

  • 增加了健壮的重载插件方式:移除插件模块,清理 handlers,清理工具
  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

在插件初始化时,raise一个RuntimeError,显示:

aec07e4c4e45fbc57bc9a0153ba8e9fe

修改raise的消息,重载插件,显示:
9a03b31894419f4b34c86ccfb73d668f

说明astrbot正确识别了插件的更改。


Checklist / 检查清单

  • 😊 如果 PR 中有新加入的功能,已经通过 Issue / 邮件等方式和作者讨论过。/ If there are new features added in the PR, I have discussed it with the authors through issues/emails, etc.
  • 👀 我的更改经过了良好的测试,并已在上方提供了“验证步骤”和“运行截图”。/ My changes have been well-tested, and "Verification Steps" and "Screenshots" have been provided above.
  • 🤓 我确保没有引入新依赖库,或者引入了新依赖库的同时将其添加到了 requirements.txtpyproject.toml 文件相应位置。/ I have ensured that no new dependencies are introduced, OR if new dependencies are introduced, they have been added to the appropriate locations in requirements.txt and pyproject.toml.
  • 😮 我的更改没有引入恶意代码。/ My changes do not introduce malicious code.

Summary by Sourcery

确保在重新加载之前对失败的插件进行完整清理,以便后续加载使用全新的状态。

Bug Fixes:

  • 修复在重新加载先前失败的插件时,对模块、处理器和工具清理不完整的问题。

Enhancements:

  • 新增集中式辅助方法,在尝试重新加载之前,为指定插件清除其模块、已注册的处理器和工具。
Original summary in English

Summary by Sourcery

Ensure failed plugins are fully cleaned up before reload so subsequent loads use a fresh state.

Bug Fixes:

  • Fix incomplete cleanup of modules, handlers, and tools when reloading previously failed plugins.

Enhancements:

  • Add a centralized helper to clear plugin modules, registered handlers, and tools for a given plugin before attempting reload.
Original summary in English

Summary by Sourcery

确保在重新加载之前对失败的插件进行完整清理,以便后续加载使用全新的状态。

Bug Fixes:

  • 修复在重新加载先前失败的插件时,对模块、处理器和工具清理不完整的问题。

Enhancements:

  • 新增集中式辅助方法,在尝试重新加载之前,为指定插件清除其模块、已注册的处理器和工具。
Original summary in English

Summary by Sourcery

Ensure failed plugins are fully cleaned up before reload so subsequent loads use a fresh state.

Bug Fixes:

  • Fix incomplete cleanup of modules, handlers, and tools when reloading previously failed plugins.

Enhancements:

  • Add a centralized helper to clear plugin modules, registered handlers, and tools for a given plugin before attempting reload.

@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Feb 25, 2026
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Waterwzy, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the reliability of plugin reloading by addressing an issue where incomplete cleanup of failed plugins led to persistent errors. By introducing a robust mechanism to clear associated modules, handlers, and tools, the system can now perform a clean slate reload, enhancing overall stability and user experience when managing plugins. This change ensures that previously failed plugins can be successfully reloaded without interference from their prior, incomplete state.

Highlights

  • Robust Plugin Reloading: Implemented a comprehensive cleanup mechanism for failed plugins, ensuring that residual modules, handlers, and tools are properly removed from the system before a reload attempt.
  • Module Cleanup: Added logic to iterate through sys.modules and remove any modules associated with the failed plugin, preventing stale code from interfering with new loads.
  • Handler and Tool Cleanup: Ensured that all registered handlers and LLM tools linked to the failed plugin are explicitly removed from their respective registries (star_handlers_registry and llm_tools.func_list).
Changelog
  • astrbot/core/star/star_manager.py
    • Enhanced the reload_failed_plugin method to include explicit cleanup of sys.modules, star_handlers_registry, and llm_tools.func_list for the specified plugin.
    • Refactored the plugin reloading logic to ensure a clean state before attempting to load a previously failed plugin.
Activity
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并给出了一些整体性的反馈:

  • 新的清理逻辑周围多出来的 #''' 注释标记看起来像是调试时遗留下来的脚手架代码,应当移除以避免造成困惑。
  • plugin_root_name = "data.plugins." 这个字符串目前是硬编码的;如果项目里已经有插件包根路径的集中配置或常量,建议在这里复用,以避免未来产生偏差。
给 AI Agent 的提示
请根据本次代码评审中的评论进行修改:

## 总体意见
- 新的清理逻辑周围多出来的 `#'''` 注释标记看起来像是调试时遗留下来的脚手架代码,应当移除以避免造成困惑。
- `plugin_root_name = "data.plugins."` 这个字符串目前是硬编码的;如果项目里已经有插件包根路径的集中配置或常量,建议在这里复用,以避免未来产生偏差。

## 具体评论

### 评论 1
<location path="astrbot/core/star/star_manager.py" line_range="406" />
<code_context>
+            if not dir_name in self.failed_plugin_dict:
+                return False, "插件不存在于失败列表中"
+            #'''
+            plugin_root_name = "data.plugins."
+            # 清理 sys.modules
+            for key in list(sys.modules.keys()):
</code_context>
<issue_to_address>
**issue (complexity):** 建议将插件清理代码块提取到一个单独的辅助方法中,这样可以让 `reload_failed_plugin` 更专注于流程编排,也更方便复用。

你可以通过将清理逻辑提取到一个独立的辅助方法中来降低新增的复杂度,同时让 `reload_failed_plugin` 只专注于流程编排。如果之后需要从 `reload` 中复用,同样会非常简单。

例如:

```python
def _cleanup_plugin_state(self, dir_name: str) -> None:
    plugin_root_name = "data.plugins."

    # 清理 sys.modules
    for key in list(sys.modules.keys()):
        if key.startswith(f"{plugin_root_name}{dir_name}"):
            logger.info(f"清除了插件{dir_name}中的{key}模块")
            del sys.modules[key]

    possible_paths = [
        f"{plugin_root_name}{dir_name}.main",
        f"{plugin_root_name}{dir_name}.{dir_name}",
    ]

    # 清理 handlers
    for path in possible_paths:
        handlers = star_handlers_registry.get_handlers_by_module_name(path)
        for handler in handlers:
            star_handlers_registry.remove(handler)
            logger.info(f"清理处理器: {handler.handler_name}")

    # 清理工具
    for tool in list(llm_tools.func_list):
        if tool.handler_module_path in possible_paths:
            llm_tools.func_list.remove(tool)
            logger.info(f"清理工具: {tool.name}")
```

然后 `reload_failed_plugin` 会变得更简单,也更单一职责:

```python
async def reload_failed_plugin(self, dir_name):
    async with self._pm_lock:
        if dir_name not in self.failed_plugin_dict:
            return False, "插件不存在于失败列表中"

        self._cleanup_plugin_state(dir_name)

        success, error = await self.load(specified_dir_name=dir_name)
        if success:
            self.failed_plugin_dict.pop(dir_name, None)
            if not self.failed_plugin_dict:
                self.failed_plugin_info = ""
            return success, None
        return False, error
```

如果之后在普通的 `reload` 流程中也需要同样的清理逻辑,只要调用 `self._cleanup_plugin_state(dir_name)` 即可,而不需要再次内联这些逻辑。
</issue_to_address>

Sourcery 对开源项目免费 —— 如果你觉得这次评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈持续改进评审质量。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The stray #''' comment markers around the new cleanup logic look like leftover debugging scaffolding and should be removed to avoid confusion.
  • The plugin_root_name = "data.plugins." string is hardcoded; if there is a central configuration or constant for the plugin package root, consider reusing it here to avoid future drift.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The stray `#'''` comment markers around the new cleanup logic look like leftover debugging scaffolding and should be removed to avoid confusion.
- The `plugin_root_name = "data.plugins."` string is hardcoded; if there is a central configuration or constant for the plugin package root, consider reusing it here to avoid future drift.

## Individual Comments

### Comment 1
<location path="astrbot/core/star/star_manager.py" line_range="406" />
<code_context>
+            if not dir_name in self.failed_plugin_dict:
+                return False, "插件不存在于失败列表中"
+            #'''
+            plugin_root_name = "data.plugins."
+            # 清理 sys.modules
+            for key in list(sys.modules.keys()):
</code_context>
<issue_to_address>
**issue (complexity):** Consider extracting the plugin cleanup block into a dedicated helper method to keep `reload_failed_plugin` focused on orchestration and easier to reuse.

You can reduce the added complexity by extracting the cleanup logic into a dedicated helper and keeping `reload_failed_plugin` focused on orchestration. That also makes reuse from `reload` trivial if needed later.

For example:

```python
def _cleanup_plugin_state(self, dir_name: str) -> None:
    plugin_root_name = "data.plugins."

    # 清理 sys.modules
    for key in list(sys.modules.keys()):
        if key.startswith(f"{plugin_root_name}{dir_name}"):
            logger.info(f"清除了插件{dir_name}中的{key}模块")
            del sys.modules[key]

    possible_paths = [
        f"{plugin_root_name}{dir_name}.main",
        f"{plugin_root_name}{dir_name}.{dir_name}",
    ]

    # 清理 handlers
    for path in possible_paths:
        handlers = star_handlers_registry.get_handlers_by_module_name(path)
        for handler in handlers:
            star_handlers_registry.remove(handler)
            logger.info(f"清理处理器: {handler.handler_name}")

    # 清理工具
    for tool in list(llm_tools.func_list):
        if tool.handler_module_path in possible_paths:
            llm_tools.func_list.remove(tool)
            logger.info(f"清理工具: {tool.name}")
```

Then `reload_failed_plugin` becomes simpler and single‑purpose:

```python
async def reload_failed_plugin(self, dir_name):
    async with self._pm_lock:
        if dir_name not in self.failed_plugin_dict:
            return False, "插件不存在于失败列表中"

        self._cleanup_plugin_state(dir_name)

        success, error = await self.load(specified_dir_name=dir_name)
        if success:
            self.failed_plugin_dict.pop(dir_name, None)
            if not self.failed_plugin_dict:
                self.failed_plugin_info = ""
            return success, None
        return False, error
```

If you later need the same cleanup during a normal `reload`, you can just call `self._cleanup_plugin_state(dir_name)` there as well instead of re‑embedding the logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot
Copy link

dosubot bot commented Feb 25, 2026

Related Documentation

Checked 1 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

@dosubot dosubot bot added the area:core The bug / feature is about astrbot's core, backend label Feb 25, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 修复了重载失败插件时资源清理不完全的问题,通过在重载前手动清理模块、处理器和工具,确保了插件状态的纯净。这是一个重要的修复,可以避免因状态残留导致的潜在问题。

代码审查发现了一些可以改进的地方:

  1. 存在一个 bug,即硬编码了插件的根路径,导致无法正确处理内置插件(reserved plugins)。
  2. 一些代码风格和调试遗留问题。

具体的修改建议请见代码评论。

Comment on lines 406 to 429
plugin_root_name = "data.plugins."
# 清理 sys.modules
for key in list(sys.modules.keys()):
if key.startswith(f"{plugin_root_name}{dir_name}"):
logger.info(f"清除了插件{dir_name}中的{key}模块")
del sys.modules[key]

possible_paths = [
f"{plugin_root_name}{dir_name}.main",
f"{plugin_root_name}{dir_name}.{dir_name}",
]

# 清理 handlers
for path in possible_paths:
handlers = star_handlers_registry.get_handlers_by_module_name(path)
for handler in handlers:
star_handlers_registry.remove(handler)
logger.info(f"清理处理器: {handler.handler_name}")

# 清理工具
for tool in list(llm_tools.func_list):
if tool.handler_module_path in possible_paths:
llm_tools.func_list.remove(tool)
logger.info(f"清理工具: {tool.name}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

plugin_root_name 被硬编码为 data.plugins.,这在重载失败的内置插件(reserved plugin,来自 astrbot/builtin_stars)时会引发问题。代码应该判断插件是否为内置插件,并使用正确的模块路径前缀。这也适用于为清理 handlers 和 tools 构建 possible_paths

            is_reserved = os.path.exists(os.path.join(self.reserved_plugin_path, dir_name))
            plugin_root_name = "astrbot.builtin_stars." if is_reserved else "data.plugins."
            # 清理 sys.modules
            for key in list(sys.modules.keys()):
                if key.startswith(f"{plugin_root_name}{dir_name}"):
                    logger.info(f"清除了插件{dir_name}中的{key}模块")
                    del sys.modules[key]

            possible_paths = [
                f"{plugin_root_name}{dir_name}.main",
                f"{plugin_root_name}{dir_name}.{dir_name}",
            ]

            # 清理 handlers
            for path in possible_paths:
                handlers = star_handlers_registry.get_handlers_by_module_name(path)
                for handler in handlers:
                    star_handlers_registry.remove(handler)
                    logger.info(f"清理处理器: {handler.handler_name}")

            # 清理工具
            for tool in list(llm_tools.func_list):
                if tool.handler_module_path in possible_paths:
                    llm_tools.func_list.remove(tool)
                    logger.info(f"清理工具: {tool.name}")

return False, "插件不存在于失败列表中"
if not dir_name in self.failed_plugin_dict:
return False, "插件不存在于失败列表中"
#'''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处的 #''' 标记以及在 430 行的类似标记,看起来是调试时遗留的产物,建议移除以保持代码整洁。

if tool.handler_module_path in possible_paths:
llm_tools.func_list.remove(tool)
logger.info(f"清理工具: {tool.name}")
#'''
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

此处的 #''' 标记看起来是调试时遗留的产物,建议移除以保持代码整洁。

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
@Waterwzy
Copy link
Contributor Author

@sourcery-ai review

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我在这里给出一些高层反馈:

  • 建议复用或扩展现有的 _purge_modules 逻辑,而不是新增一个与模块清理行为部分重复的 _cleanup_plugin_state,这样可以让模块移除的语义保持集中且一致。
  • possible_paths 列表目前只覆盖 <dir_name>.main<dir_name>.<dir_name>;如果插件可以从其他模块暴露 handlers/tools,你可能需要一种更通用的方式来推导所有相关的模块路径,以避免留下未清理的 handlers/tools。
  • _cleanup_plugin_state 中,每次重载失败都扫描整个 sys.modules 在更大的部署中可能会比较昂贵;如果这条路径会被频繁触发,可以考虑进一步收窄搜索范围(例如使用已加载插件模块的缓存列表)。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider reusing or extending the existing `_purge_modules` logic rather than introducing a separate `_cleanup_plugin_state` that partially duplicates module-cleanup behavior, so module-removal semantics stay centralized and consistent.
- The `possible_paths` list only covers `<dir_name>.main` and `<dir_name>.<dir_name>`; if plugins can expose handlers/tools from other modules, you may want a more general way to derive all relevant module paths to avoid leaving residual handlers/tools.
- In `_cleanup_plugin_state`, scanning all of `sys.modules` on every failed reload may become expensive in larger deployments; consider restricting the search further (e.g., using a cached list of loaded plugin modules) if this path is expected to be hit frequently.

Sourcery 对开源项目是免费的——如果你觉得我们的代码审查有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续的审查。
Original comment in English

Hey - I've left some high level feedback:

  • Consider reusing or extending the existing _purge_modules logic rather than introducing a separate _cleanup_plugin_state that partially duplicates module-cleanup behavior, so module-removal semantics stay centralized and consistent.
  • The possible_paths list only covers <dir_name>.main and <dir_name>.<dir_name>; if plugins can expose handlers/tools from other modules, you may want a more general way to derive all relevant module paths to avoid leaving residual handlers/tools.
  • In _cleanup_plugin_state, scanning all of sys.modules on every failed reload may become expensive in larger deployments; consider restricting the search further (e.g., using a cached list of loaded plugin modules) if this path is expected to be hit frequently.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider reusing or extending the existing `_purge_modules` logic rather than introducing a separate `_cleanup_plugin_state` that partially duplicates module-cleanup behavior, so module-removal semantics stay centralized and consistent.
- The `possible_paths` list only covers `<dir_name>.main` and `<dir_name>.<dir_name>`; if plugins can expose handlers/tools from other modules, you may want a more general way to derive all relevant module paths to avoid leaving residual handlers/tools.
- In `_cleanup_plugin_state`, scanning all of `sys.modules` on every failed reload may become expensive in larger deployments; consider restricting the search further (e.g., using a cached list of loaded plugin modules) if this path is expected to be hit frequently.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Feb 26, 2026
@Soulter Soulter merged commit 98b89eb into AstrBotDevs:master Feb 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core The bug / feature is about astrbot's core, backend lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants