Skip to content

fix: prevent accidental removal of MCP external tools due to name collisions with disabled built-in tools#5925

Merged
Soulter merged 4 commits intoAstrBotDevs:masterfrom
whatevertogo:fix/issue-5821-mcp-tool-conflict-v2
Mar 20, 2026
Merged

fix: prevent accidental removal of MCP external tools due to name collisions with disabled built-in tools#5925
Soulter merged 4 commits intoAstrBotDevs:masterfrom
whatevertogo:fix/issue-5821-mcp-tool-conflict-v2

Conversation

@whatevertogo
Copy link
Copy Markdown
Contributor

@whatevertogo whatevertogo commented Mar 8, 2026

Fixes #5821

Modifications / 改动点

问题根因
当 MCP 外接工具(如 web_search)与内置工具重名时,如果内置工具被禁用(active=False),get_full_tool_set() 会将所有同名工具都加入 ToolSet,随后 remove_tool() 会把所有同名工具全部移除,导致 MCP 工具也被"连坐"清除。

修复内容

  1. func_tool_manager.py - get_func 方法

    • 优先返回已激活的工具
    • 退化时返回最后一个同名工具(通常是后加载的 MCP 工具)
  2. func_tool_manager.py - get_full_tool_set 方法

    • 使用 add_tool() 进行填充,而非直接复制列表
    • 确保同名工具只保留一个
  3. tool.py - add_tool 方法

    • 同名工具冲突时优先保留激活状态的工具
    • 避免禁用工具覆盖激活工具

改动文件

  • astrbot/core/agent/tool.py - 修改 add_tool 逻辑

  • astrbot/core/provider/func_tool_manager.py - 修改 get_func 和 get_full_tool_set 逻辑

  • This is NOT a breaking change. / 这不是一个破坏性变更。

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

$ uv run pytest tests/ -v --tb=short -k "tool"
========== 58 passed, 587 deselected, 6 warnings in 62.80s ==========

场景验证

场景 内置工具 MCP 工具 结果
内置禁用,MCP启用 web_search(active=False) web_search(active=True) ✅ 保留 MCP
内置启用,MCP启用 web_search(active=True) web_search(active=True) ✅ 保留 MCP
内置启用,MCP禁用 web_search(active=True) web_search(active=False) ✅ 保留内置

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.

由 Sourcery 提供的摘要

通过调整工具选择和聚合逻辑,防止当 MCP 外部工具与被禁用的内置工具同名时被意外移除。

错误修复:

  • 确保在清理或更新工具集时,与被禁用的内置工具同名的 MCP 工具不会被移除。

增强内容:

  • 在工具查找和工具集处理中出现同名冲突时,优先考虑活跃工具,并始终让后加载的工具覆盖先加载的工具。
Original summary in English

Summary by Sourcery

解决 MCP 外部工具与同名内置工具之间的冲突,确保活跃工具能够被正确保留。

Bug 修复:

  • 防止在工具集清理和重建过程中,当 MCP 工具与已禁用的内置工具同名时,MCP 工具被错误移除的问题。

增强:

  • 统一工具冲突解决策略:优先保留处于激活状态的工具;当工具的激活状态相同时,后加载的工具覆盖先加载的工具。
  • 通过 ToolSet.add_tool 构建完整的函数工具集,以便在所有工具上统一应用去重和冲突处理规则。

测试:

  • 添加全面的单元测试,覆盖 ToolSet.add_toolFunctionToolManager 在各种名称与激活状态冲突场景下的行为,包括 MCP 工具与内置工具之间的交互。
Original summary in English

Summary by Sourcery

Resolve conflicts between MCP external tools and built-in tools with the same name to ensure active tools are retained correctly.

Bug Fixes:

  • Prevent MCP tools from being removed when they share a name with disabled built-in tools during tool set cleanup and reconstruction.

Enhancements:

  • Unify tool conflict resolution by preferring active tools and letting later-loaded tools overwrite earlier ones when activation state matches.
  • Build the full function tool set via ToolSet.add_tool so that deduplication and conflict rules are applied consistently across all tools.

Tests:

  • Add comprehensive unit tests covering ToolSet.add_tool and FunctionToolManager behavior for various name and active-state conflict scenarios, including MCP and built-in tool interactions.

Bug 修复:

  • 防止在清理和重建工具集时,当 MCP 工具与被禁用的内置工具同名时,MCP 工具被移除。

增强:

  • 统一工具冲突解决策略:优先选择处于激活状态的工具;当工具的激活状态相同时,后加载的工具会覆盖先加载的工具。
  • 确保 FunctionToolManager 通过 ToolSet.add_tool 构建完整工具集,从而在所有工具上统一应用去重规则。

测试:

  • 增加全面的单元测试,覆盖 ToolSet.add_tool 和 FunctionToolManager 在各种名称和激活状态冲突场景下的行为,包括 MCP 工具与内置工具的交互。
Original summary in English

Summary by Sourcery

解决 MCP 外部工具与同名内置工具之间的冲突,确保活跃工具能够被正确保留。

Bug 修复:

  • 防止在工具集清理和重建过程中,当 MCP 工具与已禁用的内置工具同名时,MCP 工具被错误移除的问题。

增强:

  • 统一工具冲突解决策略:优先保留处于激活状态的工具;当工具的激活状态相同时,后加载的工具覆盖先加载的工具。
  • 通过 ToolSet.add_tool 构建完整的函数工具集,以便在所有工具上统一应用去重和冲突处理规则。

测试:

  • 添加全面的单元测试,覆盖 ToolSet.add_toolFunctionToolManager 在各种名称与激活状态冲突场景下的行为,包括 MCP 工具与内置工具之间的交互。
Original summary in English

Summary by Sourcery

Resolve conflicts between MCP external tools and built-in tools with the same name to ensure active tools are retained correctly.

Bug Fixes:

  • Prevent MCP tools from being removed when they share a name with disabled built-in tools during tool set cleanup and reconstruction.

Enhancements:

  • Unify tool conflict resolution by preferring active tools and letting later-loaded tools overwrite earlier ones when activation state matches.
  • Build the full function tool set via ToolSet.add_tool so that deduplication and conflict rules are applied consistently across all tools.

Tests:

  • Add comprehensive unit tests covering ToolSet.add_tool and FunctionToolManager behavior for various name and active-state conflict scenarios, including MCP and built-in tool interactions.

- 修改 get_func 方法:优先返回已激活的工具
- 修改 get_full_tool_set 方法:使用 add_tool 防止同名冲突
- 修改 add_tool 方法:优先保留已激活的工具

Fixes AstrBotDevs#5821

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 8, 2026 18:21
@auto-assign auto-assign bot requested review from Fridemn and advent259141 March 8, 2026 18:21
@dosubot dosubot bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Mar 8, 2026
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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!

此拉取请求旨在解决当 MCP 外部工具与内置工具重名时,内置工具被禁用会导致 MCP 工具也一并被错误移除的问题。通过优化工具管理逻辑,特别是 add_toolget_funcget_full_tool_set 方法,确保系统能够正确识别并优先保留激活状态的工具,从而允许 MCP 工具在与内置工具重名时能够正常工作,避免了不必要的“连坐”效应。

Highlights

  • 工具冲突解决: 修复了 MCP 外部工具与内置工具重名时,因清洗规则导致 MCP 工具失效的问题。
  • get_func 方法优化: func_tool_manager.py 中的 get_func 方法现在优先返回已激活的工具,并在没有激活工具时退化返回最后一个同名工具。
  • get_full_tool_set 方法改进: func_tool_manager.py 中的 get_full_tool_set 方法现在使用 add_tool() 进行工具集填充,确保同名工具只保留一个。
  • add_tool 逻辑更新: tool.py 中的 add_tool 方法已更新,在处理同名工具冲突时,优先保留激活状态的工具,避免禁用工具覆盖激活工具。
Changelog
  • astrbot/core/agent/tool.py
    • 调整了 add_tool 方法的逻辑,使其在遇到同名工具时,优先保留已激活的工具,并防止已禁用的工具覆盖已激活的工具。
  • astrbot/core/provider/func_tool_manager.py
    • 修改了 get_func 方法,使其首先查找并返回已激活的同名工具,如果不存在则返回列表中最后一个同名工具。
    • 更新了 get_full_tool_set 方法,使其通过调用 add_tool 来构建工具集,以确保同名工具的正确处理和优先级。
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
Copy Markdown
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 - 我给出了一些整体性的反馈:

  • get_full_tool_set 的文档字符串目前仍然描述的是简单的「最后一个获胜」行为,但在新的 add_tool 逻辑下,实际规则变成了「优先使用激活的工具,否则采用最后一个」;建议更新该文档字符串,使其准确反映现在的行为,避免让后续维护者产生困惑。
  • FuncToolManager.func_list 的顺序、get_funcToolSet.add_tool 之间的交互,现在隐含了比较微妙的优先级规则(激活 vs 未激活、内置 vs MCP、较早 vs 较晚);可以考虑把这一决策逻辑集中到一个小的辅助函数中,或者在注释中更明确地说明优先级规则,以降低将来修改 add_tool 时出现回归问题的风险。
面向 AI 代理的提示
Please address the comments from this code review:

## Overall Comments
- The docstring for `get_full_tool_set` still describes a simple "last one wins" behavior, but with the new `add_tool` logic the actual rule is "prefer active tools, otherwise last one wins"; consider updating the docstring to accurately reflect this to avoid confusion for future maintainers.
- The interaction between `FuncToolManager.func_list` ordering, `get_func`, and `ToolSet.add_tool` now encodes fairly subtle precedence rules (active vs inactive, built-in vs MCP, earlier vs later); it may help to centralize this resolution logic in a small helper or make the precedence rules more explicit in comments to reduce the risk of regressions if `add_tool` is changed later.

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

Hey - I've left some high level feedback:

  • The docstring for get_full_tool_set still describes a simple "last one wins" behavior, but with the new add_tool logic the actual rule is "prefer active tools, otherwise last one wins"; consider updating the docstring to accurately reflect this to avoid confusion for future maintainers.
  • The interaction between FuncToolManager.func_list ordering, get_func, and ToolSet.add_tool now encodes fairly subtle precedence rules (active vs inactive, built-in vs MCP, earlier vs later); it may help to centralize this resolution logic in a small helper or make the precedence rules more explicit in comments to reduce the risk of regressions if add_tool is changed later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The docstring for `get_full_tool_set` still describes a simple "last one wins" behavior, but with the new `add_tool` logic the actual rule is "prefer active tools, otherwise last one wins"; consider updating the docstring to accurately reflect this to avoid confusion for future maintainers.
- The interaction between `FuncToolManager.func_list` ordering, `get_func`, and `ToolSet.add_tool` now encodes fairly subtle precedence rules (active vs inactive, built-in vs MCP, earlier vs later); it may help to centralize this resolution logic in a small helper or make the precedence rules more explicit in comments to reduce the risk of regressions if `add_tool` is changed later.

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 area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. label Mar 8, 2026
Copy link
Copy Markdown
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 旨在解决当 MCP 工具与禁用的内置工具重名时,MCP 工具被意外移除的问题。通过修改 add_tool 的逻辑以优先保留激活的工具,以及调整 get_full_tool_set 使用 add_tool 来处理同名工具,从根源上解决了该问题。同时,get_func 也被优化,以优先返回激活的工具。

然而,本次修改引入了一个重要的安全隐患:工具选择逻辑存在不一致性。具体来说,用于执行工具的 get_func 方法和用于 LLM 展示工具的 get_full_tool_set 方法在处理同名且激活的工具时,采用了不同的冲突解决策略。这种不一致可能导致 LLM 看到的工具定义与实际执行的工具不符,从而引发意外行为或潜在的“工具劫持”问题。建议将 get_func 的逻辑与 ToolSet.add_tool 中使用的“后激活者胜出”策略对齐,以确保一致性。此外,我还提出了一些关于代码可读性和效率的改进建议。

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

该 PR 旨在修复 MCP 外接工具与内置工具重名时,由于工具集合构建与清洗(remove)逻辑导致 同名工具被“连坐”全部移除、进而使 MCP 工具彻底失效的问题,确保工具集合在输出给 LLM 前能正确保留可用工具。

Changes:

  • 调整 FunctionToolManager.get_func():优先返回已激活工具,并在退化场景下选择后加载的同名工具。
  • 调整 FunctionToolManager.get_full_tool_set():通过 ToolSet.add_tool() 构建工具集以去重同名工具。
  • 调整 ToolSet.add_tool():同名冲突时优先保留 active=True 的工具,避免禁用工具覆盖启用工具。

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
astrbot/core/provider/func_tool_manager.py 调整工具检索与完整工具集构建方式,尝试避免同名工具在清洗时被整体移除
astrbot/core/agent/tool.py 更新 ToolSet.add_tool() 的同名覆盖策略,使其优先保留激活工具

- Fix inconsistency: get_func now uses reversed() to match ToolSet.add_tool's
  "last-active-wins" logic, preventing potential "tool hijacking" issues
- Improve readability: replace double negative condition with clearer logic
- Add compatibility: use getattr with default for tools without 'active' attribute
- Remove unnecessary deepcopy: MCPTool runtime objects should not be deep copied
- Update docstring: accurately describe the actual tool resolution behavior

Addresses review comments from sourcery-ai, gemini-code-assist, and Copilot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dosubot dosubot bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Mar 8, 2026
@whatevertogo
Copy link
Copy Markdown
Contributor Author

已根据评审意见修复

感谢 @sourcery-ai@gemini-code-assist@copilot 的评审意见。已修复以下问题:

安全问题

  • get_funcget_full_tool_set 逻辑不一致get_func 现在使用 reversed() 遍历,与 ToolSet.add_tool 的"后激活者胜出"策略保持一致,避免潜在的"工具劫持"问题

性能问题

  • 移除不必要的 deepcopy:MCPTool 上的运行时对象不应被深拷贝,直接使用 add_tool(tool) 即可

代码质量

  • 可读性改进:将双重否定条件 if not (existing_tool.active and not tool.active) 简化为 if new_active or not existing_active
  • 兼容性改进:使用 getattr(tool, "active", True) 兼容可能缺少 active 属性的工具对象
  • 文档更新get_full_tool_set() 的 docstring 现在准确描述了实际的工具解析行为

所有测试通过 ✅

@gemini-code-assist
Copy link
Copy Markdown
Contributor

感谢您的更新,whatevertogo!很高兴看到您根据评审意见进行了修复。我已仔细审阅了您的更改,并确认以下问题已得到妥善解决:

  • 安全问题get_funcget_full_tool_set 的逻辑现在与 ToolSet.add_tool 的策略保持一致,通过优先处理激活状态的工具并使用 reversed() 遍历,有效避免了潜在的“工具劫持”问题。
  • 性能问题:通过优化 get_full_tool_set 的实现,避免了不必要的 deepcopy 操作,提升了效率。
  • 代码质量
    • 双重否定条件 if not (existing_tool.active and not tool.active) 已成功简化为更清晰的 if new_active or not existing_active
    • getattr(tool, "active", True) 的使用增强了代码的兼容性,能够更好地处理缺少 active 属性的工具对象。
    • get_full_tool_set() 的文档字符串也已更新,准确反映了其新的工具解析行为。

这些改进使得工具管理逻辑更加健壮、高效且易于理解。做得非常棒!

Add comprehensive tests for ToolSet.add_tool, get_func, and get_full_tool_set
to verify the conflict resolution behavior when MCP tools share names with
built-in tools.

Test cases:
- ToolSet.add_tool: active/inactive priority, last-one-wins for same state
- get_func: returns last active tool, fallback to last matching tool
- get_full_tool_set: deduplication logic, no deepcopy, MCP overrides disabled builtin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dosubot dosubot bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Mar 8, 2026
@whatevertogo
Copy link
Copy Markdown
Contributor Author

如果要更好的修复方式的话要引入命名空间,所以我未采用

@zouyonghe
Copy link
Copy Markdown
Member

@sourcery-ai review

Copy link
Copy Markdown
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 - 我发现了 2 个问题

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/func_tool_manager.py" line_range="314-315" />
<code_context>

     def get_func(self, name) -> FuncTool | None:
-        for f in self.func_list:
+        # 优先返回已激活的工具(后加载的覆盖前面的,与 ToolSet.add_tool 保持一致)
+        for f in reversed(self.func_list):
+            if f.name == name and getattr(f, "active", False):
+                return f
</code_context>
<issue_to_address>
**issue (bug_risk):**`get_func``ToolSet.add_tool` 中对 `active` 的默认处理方式对齐,以避免产生不一致的行为。

这里 `getattr(f, "active", False)` 会将没有 `active` 属性的工具视为未激活,而 `ToolSet.add_tool` 使用的是 `getattr(..., True)`,在冲突处理时会将它们视为已激活。这种不一致意味着,当存在一个同名且 `active=False` 的工具时,一个工具在集合中可能被视为已激活,但在 `get_func` 中却会被跳过。请在这两个位置对齐默认值,并相应调整注释/行为,以保证冲突处理与查找逻辑保持一致。
</issue_to_address>

### Comment 2
<location path="tests/unit/test_tool_conflict_resolution.py" line_range="44-52" />
<code_context>
+        assert len(toolset.tools) == 1
+        assert toolset.tools[0].active is True
+
+    def test_both_active_last_one_wins(self):
+        """When both tools are active, the new one should overwrite."""
+        toolset = ToolSet()
+        toolset.add_tool(make_tool("web_search", active=True))
+        toolset.add_tool(make_tool("web_search", active=True))
+
+        assert len(toolset.tools) == 1
+        # The second tool should be the one kept
+        assert toolset.tools[0].description == "Test tool web_search"
+
+    def test_both_inactive_last_one_wins(self):
</code_context>
<issue_to_address>
**issue (testing):** 该测试实际上并不能证明被保留的是第二个处于激活状态的工具。

由于这两个工具的创建方式完全相同,即使保留的是第一个工具,这里的断言依然会通过。为了真正验证覆盖行为,需要将两个实例区分开,并断言第二个实例被保留下来,例如:

```python
first = make_tool("web_search", active=True)
second = make_tool("web_search", active=True)
second.description = "Second web search"

toolset = ToolSet()
toolset.add_tool(first)
toolset.add_tool(second)

assert len(toolset.tools) == 1
assert toolset.tools[0] is second
assert toolset.tools[0].description == "Second web search"
```

如果实现实际上保留的是第一个工具而不是最后一个,这个测试就会失败。
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
请帮我变得更有用!请对每条评论点击 👍 或 👎,我会根据这些反馈改进后续的代码审查。
Original comment in English

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/provider/func_tool_manager.py" line_range="314-315" />
<code_context>

     def get_func(self, name) -> FuncTool | None:
-        for f in self.func_list:
+        # 优先返回已激活的工具(后加载的覆盖前面的,与 ToolSet.add_tool 保持一致)
+        for f in reversed(self.func_list):
+            if f.name == name and getattr(f, "active", False):
+                return f
</code_context>
<issue_to_address>
**issue (bug_risk):** Align default `active` handling between `get_func` and `ToolSet.add_tool` to avoid inconsistent behavior.

Here `getattr(f, "active", False)` treats tools without an `active` attribute as inactive, while `ToolSet.add_tool` uses `getattr(..., True)`, treating them as active in conflict resolution. This mismatch means a tool can be active in the set but skipped by `get_func` when a same-name tool has `active=False`. Please align the default in both places and adjust comments/behavior so conflict resolution and lookup remain consistent.
</issue_to_address>

### Comment 2
<location path="tests/unit/test_tool_conflict_resolution.py" line_range="44-52" />
<code_context>
+        assert len(toolset.tools) == 1
+        assert toolset.tools[0].active is True
+
+    def test_both_active_last_one_wins(self):
+        """When both tools are active, the new one should overwrite."""
+        toolset = ToolSet()
+        toolset.add_tool(make_tool("web_search", active=True))
+        toolset.add_tool(make_tool("web_search", active=True))
+
+        assert len(toolset.tools) == 1
+        # The second tool should be the one kept
+        assert toolset.tools[0].description == "Test tool web_search"
+
+    def test_both_inactive_last_one_wins(self):
</code_context>
<issue_to_address>
**issue (testing):** This test does not actually prove that the second active tool is the one kept.

Because both tools are created identically, this assertion would still pass if the first tool were kept. To really validate the overwrite behavior, differentiate the two instances and assert that the second is the one retained, for example:

```python
first = make_tool("web_search", active=True)
second = make_tool("web_search", active=True)
second.description = "Second web search"

toolset = ToolSet()
toolset.add_tool(first)
toolset.add_tool(second)

assert len(toolset.tools) == 1
assert toolset.tools[0] is second
assert toolset.tools[0].description == "Second web search"
```

This will fail if the implementation keeps the first tool instead of the last.
</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.

@zouyonghe
Copy link
Copy Markdown
Member

@sourcery-ai review

@zouyonghe
Copy link
Copy Markdown
Member

你可以自己跑一下,如果只有重构建议就没什么问题了

Copy link
Copy Markdown
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 - 我在这里给出一些整体性的反馈:

  • FunctionToolManager.get_func 中,你可以通过一次自后向前遍历来避免对 func_list 进行两次迭代:在反向遍历时记录最后一次遇到的工具,并在找到第一个处于激活状态的匹配项时立即返回;如果没有处于激活状态的匹配项,则回退到最后一次遇到的匹配项。这样可以在保持相同语义的同时,让逻辑更简单,也稍微更高效一些。
给 AI Agent 的提示词
Please address the comments from this code review:

## Overall Comments
- In `FunctionToolManager.get_func`, you can avoid iterating `func_list` twice by doing a single reverse pass that tracks the last-seen tool and immediately returns on the first active match, falling back to the last-seen match if none are active, which keeps the same semantics with simpler and slightly more efficient logic.

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

Hey - I've left some high level feedback:

  • In FunctionToolManager.get_func, you can avoid iterating func_list twice by doing a single reverse pass that tracks the last-seen tool and immediately returns on the first active match, falling back to the last-seen match if none are active, which keeps the same semantics with simpler and slightly more efficient logic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `FunctionToolManager.get_func`, you can avoid iterating `func_list` twice by doing a single reverse pass that tracks the last-seen tool and immediately returns on the first active match, falling back to the last-seen match if none are active, which keeps the same semantics with simpler and slightly more efficient logic.

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.

@Soulter Soulter changed the title fix: 解决 MCP 工具与内置工具重名时的连坐问题 fix: prevent accidental removal of MCP external tools due to name collisions with disabled built-in tools Mar 20, 2026
@dosubot dosubot bot added the lgtm This PR has been approved by a maintainer label Mar 20, 2026
@Soulter Soulter merged commit 2c279ab into AstrBotDevs:master Mar 20, 2026
6 checks passed
KBVsent pushed a commit to KBVsent/AstrBot that referenced this pull request Mar 21, 2026
…lisions with disabled built-in tools (AstrBotDevs#5925)

* fix: 解决 MCP 工具与内置工具重名时的连坐问题

- 修改 get_func 方法:优先返回已激活的工具
- 修改 get_full_tool_set 方法:使用 add_tool 防止同名冲突
- 修改 add_tool 方法:优先保留已激活的工具

Fixes AstrBotDevs#5821

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: address PR review feedback for tool conflict resolution

- Fix inconsistency: get_func now uses reversed() to match ToolSet.add_tool's
  "last-active-wins" logic, preventing potential "tool hijacking" issues
- Improve readability: replace double negative condition with clearer logic
- Add compatibility: use getattr with default for tools without 'active' attribute
- Remove unnecessary deepcopy: MCPTool runtime objects should not be deep copied
- Update docstring: accurately describe the actual tool resolution behavior

Addresses review comments from sourcery-ai, gemini-code-assist, and Copilot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add tests for tool conflict resolution (issue AstrBotDevs#5821)

Add comprehensive tests for ToolSet.add_tool, get_func, and get_full_tool_set
to verify the conflict resolution behavior when MCP tools share names with
built-in tools.

Test cases:
- ToolSet.add_tool: active/inactive priority, last-one-wins for same state
- get_func: returns last active tool, fallback to last matching tool
- get_full_tool_set: deduplication logic, no deepcopy, MCP overrides disabled builtin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复工具冲突处理逻辑,确保未激活工具不被错误移除

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
xkeyC added a commit to xkeyC/AstrBot that referenced this pull request Mar 28, 2026
* perf: onebot, satori docs improvement

* ci: add pr check

* chore: Delete .github/workflows/pr-checklist-check.yml

* feat: localize session management group & interval method texts (AstrBotDevs#6471)

* fix(ui): localize session management group texts

Replace hardcoded Chinese strings in SessionManagementPage with i18n
lookups for group management labels, dialogs, and action feedback.

Add and align translation keys in en-US, ru-RU, and zh-CN for group
management and batch operation messages to ensure consistent multilingual
UI behavior.

* fix(ui): localize interval method hint text

* fix: SQLite 'database is locked' by adding busy timeout (AstrBotDevs#6474)

The async engine is created without a busy timeout, so concurrent
writes (agent responses, metrics, session updates) fail instantly
with 'database is locked' instead of waiting for the lock.

Add connect_args={'timeout': 30} for SQLite engines so the driver
waits up to 30 seconds for the write lock. Combined with the existing
WAL journal mode, this handles the typical concurrent write bursts
from agent + metrics + session operations.

Fixes AstrBotDevs#6443

* fix: parse multiline frontmatter description in SKILL.md (AstrBotDevs#6460)

* fix(skills): support multiline frontmatter descriptions

* fix(skills): 修复多行 frontmatter 描述解析

* style(skills): clean up frontmatter parser follow-ups

---------

Co-authored-by: RhoninSeiei <RhoninSeiei@users.noreply.github.com>

* chore(deps): bump the github-actions group with 2 updates (AstrBotDevs#6461)

Bumps the github-actions group with 2 updates: [ncipollo/release-action](https://github.com/ncipollo/release-action) and [actions/github-script](https://github.com/actions/github-script).


Updates `ncipollo/release-action` from 1.20.0 to 1.21.0
- [Release notes](https://github.com/ncipollo/release-action/releases)
- [Commits](ncipollo/release-action@v1.20.0...v1.21.0)

Updates `actions/github-script` from 7 to 8
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](actions/github-script@v7...v8)

---
updated-dependencies:
- dependency-name: ncipollo/release-action
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore: remove deprecated version field from compose.yml (AstrBotDevs#5495)

The version field is no longer required in Docker Compose v2 and has been deprecated.

* fix: reading skills on Windows (AstrBotDevs#6490)

There is an issue with reading the skill directory on the Windows system, which results in a high probability of files under the skill directory being unrecognizable, now fix it.

* fix: subagent lookup failure when using default persona (AstrBotDevs#5672)

* fix: resolve subagent persona lookup for 'default' and unify resolution logic

- Add PersonaManager.get_persona_v3_by_id() to centralize v3 persona resolution
- Handle 'default' persona_id mapping to DEFAULT_PERSONALITY in subagent orchestrator
- Fix HandoffTool.default_description using agent_name parameter correctly
- Add tests for default persona in subagent config and tool deduplication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: simplify get_default_persona_v3 using get_persona_v3_by_id

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix: register_agent decorator NameError (AstrBotDevs#5765)

* fix: 修改 register_agent 以避免运行时导入 AstrAgentContext

* test: improve register_agent test robustness

- Add fixture for llm_tools cleanup to avoid test interference
- Use multiple import patterns to make guard more robust to refactors
- Add assertion to verify decorated coroutine is wired as handoff handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* 删除测试文件: 移除 register_agent 装饰器的运行时行为测试

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: only pass dimensions when explicitly configured in embedding config (AstrBotDevs#6432)

* fix: only pass dimensions param when explicitly configured

Models like bge-m3 don't support the dimensions parameter in the
embedding API, causing HTTP 400 errors. Previously dimensions was
always sent with a default value of 1024, even when the user never
configured it. Now dimensions is only included in the request when
embedding_dimensions is explicitly set in provider config.

Closes AstrBotDevs#6421

Signed-off-by: JiangNan <1394485448@qq.com>

* fix: handle invalid dimensions config and align get_dim return

- Add try-except around int() conversion in _embedding_kwargs to
  gracefully handle invalid embedding_dimensions config values
- Update get_dim() to return 0 when embedding_dimensions is not
  explicitly configured, so callers know dimensions weren't specified
  and can handle it accordingly
- Both methods now share consistent logic for reading the config

Signed-off-by: JiangNan <1394485448@qq.com>

* fix: improve logging for invalid embedding_dimensions configuration

---------

Signed-off-by: JiangNan <1394485448@qq.com>
Co-authored-by: Soulter <905617992@qq.com>

* perf: Implement Pydantic data models for the KOOK adapter to enhance data retrieval and message schema validation (AstrBotDevs#5719)

* refactor: 给kook适配器添加kook事件数据类

* format: 使用StrEnum替换kook适配器中的(str,enum)

* docs: add aiocqhttp and satori protocol documentation; remove outdated lagrange and napcat guides

* refactor: downgrade StrEnum to (str, Enum) in kook_type for backward compatibility  (AstrBotDevs#6512)

我那时候搓 AstrBotDevs#5719 的时候 AstrBotDevs#5729 已经合并了, 既然ruff的py限制版本里是`3.12`,那我那时候干脆用的StrEnum,现在发现那个pr revert了,那我也降级回旧Enum写法好了

* feat: install plugin using metadata name and validate importable identifiers (AstrBotDevs#6530)

* feat: install plugin using metadata name and validate importable identifiers

* fix: cleanup temporary upload extraction directory on plugin install failure

* Update astrbot/core/star/star_manager.py

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

* fix: avoid unnecessary install when repository directory already exists

---------

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

* fix: restrict workflows to upstream repo (AstrBotDevs#6531)

* Clarify FileUpload/DownloadTool descriptions to fix LLM tool selection (AstrBotDevs#6527)

Multiple models (Gemini 3, GPT-5.2, Claude Sonnet, Kimi K2.5) consistently
pick FileDownloadTool when they should pick FileUploadTool. The old
descriptions used "upload/download" which is ambiguous from the LLM's
perspective — it doesn't know which side is "local" vs "remote".

Rewrite descriptions to use explicit directional language:
- Upload: "Transfer FROM host INTO sandbox" + "when user sends a file"
- Download: "Transfer FROM sandbox OUT to host" + "ONLY when user asks
  to retrieve/export"

Also improve parameter descriptions with the same directional clarity.

Fixes AstrBotDevs#6497

Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>

* perf(dashboard): subset MDI icon font and self-host Google Fonts (AstrBotDevs#6532)

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font and self-host Google Fonts

* perf(dashboard): subset MDI icon font cr fix

* chore: update lockfile

* enhance:更改未完成更新的文档用词问题(多处“消息平台”已更名为“机器人”) (AstrBotDevs#6568)

* Update kubernetes.md

* Update discord.md

* Update kubernetes.md

* Update AstrBot setup instructions in Kubernetes doc

* fix: set packaged Windows runtime build env for pip native builds (AstrBotDevs#6575)

* Fix Windows packaged runtime pip build env

* test(pip): cover packaged runtime env injection edges

* refactor(pip): tighten packaged runtime env handling

* test(pip): cover missing runtime build dirs

* fix(pip): build runtime env inside locked section

* test(pip): expand windows path normalization coverage

* refactor(pip): build runtime env from snapshots

* fix(pip): preserve windows env key semantics

* refactor(pip): simplify windows runtime env handling

Keep the in-process pip environment mutation and case-insensitive INCLUDE/LIB handling localized so packaged Windows builds are easier to follow. Add a UNC no-op regression case to guard path normalization.

* refactor(pip): streamline runtime env mutation helpers

Keep packaged Windows pip environment handling easier to follow by reusing a temporary environment context manager, isolating case-insensitive INCLUDE/LIB lookup, and documenting native path normalization behavior.

* feat (doc) : Add doc for shipyard-neo sandbox driver (AstrBotDevs#6590)

* fix(ui): localize session management group texts

Replace hardcoded Chinese strings in SessionManagementPage with i18n
lookups for group management labels, dialogs, and action feedback.

Add and align translation keys in en-US, ru-RU, and zh-CN for group
management and batch operation messages to ensure consistent multilingual
UI behavior.

* fix(ui): localize interval method hint text

* docs(sandbox): document shipyard neo setup

Expand the Chinese sandbox guide to cover Shipyard Neo as the
recommended driver and distinguish it from legacy Shipyard.

Add deployment and configuration guidance for standalone and
compose-based setups, include a full annotated config example,
and clarify profile selection, TTL behavior, workspace paths,
and persistence semantics.

* docs(sandbox): recommend standalone shipyard neo

Clarify that Shipyard Neo is best deployed on a separate,
better-provisioned host for long-term use.

Update the setup steps and AstrBot connection guidance, and
remove the earlier combined Docker Compose deployment flow.

* docs(sandbox): expand shipyard neo guide

Document Shipyard Neo as the recommended sandbox driver and
clarify how it differs from the legacy Shipyard setup.

Add guidance for deployment, performance requirements, Bay
configuration, profile selection, TTL behavior, workspace
persistence, and browser capability support.

Also reorganize the sandbox configuration section and keep the
legacy Shipyard instructions for compatibility.

* docs(sandbox): fix shipyard neo doc links

Update the sandbox guides in English and Chinese to link
directly to the upstream `config.yaml` example.

Replace duplicated TTL and persistence notes with references
to the dedicated sections to keep the guide concise and easier
to maintain.

* docs(sandbox): clarify section references in guides (AstrBotDevs#6591)

* fix: prevent wecom ai bot long connection replies from disappearing (AstrBotDevs#6606)

* fix: prevent empty fallback replies from clearing wecom ai bot output

* fix: 优化消息发送逻辑,避免发送空消息

---------

Co-authored-by: shijianhuai <shijianhuai@simuwang.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix(wecom-aibot): significantly improve streaming readability and speed via add throttling (AstrBotDevs#6610)

* fix(wecom-ai): add 0.5s interval for streaming responses

* fix(wecom-ai): correct event type checking and add spacing in WecomAIBotMessageEvent

* feat: context token counting support for multimodal content (images, audio, and chain-of-thought) (AstrBotDevs#6596)

EstimateTokenCounter 之前只计算 TextPart,完全忽略 ImageURLPart、
AudioURLPart 和 ThinkPart。多模态对话中图片占 500-2000 token,
不被计入会导致 context 压缩触发过晚,API 先报 context_length_exceeded。

改动:
- ImageURLPart 按 765 token 估算(OpenAI vision 低/高分辨率中位数)
- AudioURLPart 按 500 token 估算
- ThinkPart 的文本内容正常计算
- 10 个新测试覆盖各类型单独和混合场景

Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>

* fix(openai): Token usage not working when using MoonshotAI official API (AstrBotDevs#6618)

fixes: AstrBotDevs#6614

* fix: update hint for ID whitelist configuration to clarify behavior when empty (AstrBotDevs#6611)

* fix: update hint for ID whitelist configuration to clarify behavior when empty

* fix: update whitelist hint

---------

Co-authored-by: machina <1531829828@qq.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: 截断器丢失唯一 user 消息导致智谱等 provider 返回 400 (AstrBotDevs#6581)

* fix: 截断器丢失唯一 user 消息导致 API 400

修复 AstrBotDevs#6196

当对话只有一条 user 消息(长 tool chain 场景:system → user → assistant
→ tool → assistant → tool → ...),三个截断方法都会把这条 user 消息丢掉,
导致智谱、Gemini 等要求 user 消息的 provider 返回 400。

改动:
- 提取 `_split_system_rest()` 去掉三个方法里重复的 system/non-system 拆分
- 新增 `_ensure_user_message()`:截断后如果没有 user 了,从原始消息里补回
  第一条 user,避免违反 API 格式要求
- 删掉 `truncate_by_dropping_oldest_turns` 里把没有 user 就清空全部消息的逻辑
- 5 个新测试覆盖单 user + 长 tool chain 场景,3 个旧测试更新断言

* style: format code

---------

Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>
Co-authored-by: RC-CHN <1051989940@qq.com>

* fix: prevent truncation logic from removing the only user message in long tool-calling conversations (AstrBotDevs#6198)

* fix: 压缩算法删除 user 消息 Bug 修复

* perf: improve truncate algo

---------

Co-authored-by: Soulter <905617992@qq.com>

* feat: add Kimi Coding Plan provider with Anthropic API compatibility (AstrBotDevs#6559)

* Add Kimi Code provider

* Add icon mapping for Kimi Code provider

* Clarify Kimi CodingPlan provider labeling

* Refine Kimi Code header handling

* modified docker compose

* fix: correct Kimi Coding Plan label and update API base URL

---------

Co-authored-by: Soulter <905617992@qq.com>

* fix(openai): improve logging for proxy and API base configuration (AstrBotDevs#6669)

fix: AstrBotDevs#6558

* fix(dashboard): simplify persona selector layout for mobile screens (AstrBotDevs#5907)

* fix: Follow-up logic persists after /stop trigger (AstrBotDevs#6656)

/stop 设置 agent_stop_requested 标记,但 runner 直到当前工具调用
超时才从 _ACTIVE_AGENT_RUNNERS 注销。在此窗口期内,用户发的新消息
被 try_capture_follow_up() 当作 follow-up 吞掉。

在 follow-up 捕获前检查 stop 标记:一旦用户请求停止,就不再把后续
消息注入到正在终止的 agent 上下文中。

Fixes AstrBotDevs#6626

* fix: auto-restart telegram polling loop on failure (AstrBotDevs#6648)

* fix: auto-restart telegram polling loop on failure (AstrBotDevs#373)

* fix: auto-restart telegram polling loop on failure

* fix: harden telegram polling restart lifecycle

* fix(telegram): 根据建议优化轮询鲁棒性并处理 Token 失效错误

* fix: 补全配置元数据及 i18n

* feat: add xiaomi MiMo TTS & STT providers (AstrBotDevs#6643)

* feat: add mimo tts provider support

* fix: handle empty mimo tts choices

* feat: add mimo stt provider support

* chore: rename "OpenAI" provider to "OpenAI Compatible" (AstrBotDevs#6707)

* fix: prevent accidental removal of MCP external tools due to name collisions with disabled built-in tools (AstrBotDevs#5925)

* fix: 解决 MCP 工具与内置工具重名时的连坐问题

- 修改 get_func 方法:优先返回已激活的工具
- 修改 get_full_tool_set 方法:使用 add_tool 防止同名冲突
- 修改 add_tool 方法:优先保留已激活的工具

Fixes AstrBotDevs#5821

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: address PR review feedback for tool conflict resolution

- Fix inconsistency: get_func now uses reversed() to match ToolSet.add_tool's
  "last-active-wins" logic, preventing potential "tool hijacking" issues
- Improve readability: replace double negative condition with clearer logic
- Add compatibility: use getattr with default for tools without 'active' attribute
- Remove unnecessary deepcopy: MCPTool runtime objects should not be deep copied
- Update docstring: accurately describe the actual tool resolution behavior

Addresses review comments from sourcery-ai, gemini-code-assist, and Copilot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add tests for tool conflict resolution (issue AstrBotDevs#5821)

Add comprehensive tests for ToolSet.add_tool, get_func, and get_full_tool_set
to verify the conflict resolution behavior when MCP tools share names with
built-in tools.

Test cases:
- ToolSet.add_tool: active/inactive priority, last-one-wins for same state
- get_func: returns last active tool, fallback to last matching tool
- get_full_tool_set: deduplication logic, no deepcopy, MCP overrides disabled builtin

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复工具冲突处理逻辑,确保未激活工具不被错误移除

---------

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add a toggle to disable thinking mode in Ollama (AstrBotDevs#5941)

* feat: add ollama thinking toggle

* fix: simplify hint for ollama_disable_thinking configuration

---------

Co-authored-by: Gargantua <22532097@zju.edu.cn>
Co-authored-by: Soulter <905617992@qq.com>

* fix: preserve PATHEXT for stdio mcp servers on windows (AstrBotDevs#5822)

* fix: preserve PATHEXT for stdio mcp servers on windows

* chore: delete test_mcp_client.py

---------

Co-authored-by: Soulter <905617992@qq.com>

* fix(core): interrupt subagent tool waits on stop (AstrBotDevs#5850)

* fix(core): interrupt subagent tool waits on stop

* test: relax subagent handoff timeout

* test: cover stop-aware tool interruption

* refactor: unify runner stop state

* refactor: simplify tool executor interruption

* fix: preserve tool interruption propagation

* refactor: tighten interruption helpers

---------

Co-authored-by: idiotsj <idiotsj@users.noreply.github.com>

* fix(agent): reject follow-up messages after stop request (AstrBotDevs#6704)

* fix: reject follow-up messages after stop requested (AstrBotDevs#6626)

Once a user sends /stop, follow-up messages should no longer be
accepted for that runner. Previously, there was a race window where
messages sent after stop could still be queued as follow-ups.

This fix gates the follow_up() method to check both done() and
_stop_requested before accepting a new follow-up message.

Acceptance criteria met:
- After /stop, later follow-up messages return None (rejected)
- Post-stop follow-ups are not added to _pending_follow_ups
- No post-stop text is injected into tool results
- Graceful-stop behavior otherwise unchanged
- Follow-ups submitted before stop retain current behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: add regression tests for issue AstrBotDevs#6626 follow-up rejection

Add focused tests that verify the complete tool-result injection path
for follow-up messages after stop is requested:

- test_follow_up_rejected_and_runner_stops_without_execution: Verifies
  that when stop is requested before any execution, follow-ups are
  rejected and the runner stops gracefully without executing tools.

- test_follow_up_merged_into_tool_result_before_stop: Verifies that
  follow-ups queued before stop are properly merged into tool results
  via _merge_follow_up_notice().

- test_follow_up_after_stop_not_merged_into_tool_result: Regression
  test that simulates the race condition from issue AstrBotDevs#6626. Verifies
  that only pre-stop follow-ups are merged into tool results, and
  post-stop follow-ups are rejected at the admission point.

These tests validate the fix in ToolLoopAgentRunner.follow_up() that
checks both self.done() and self._stop_requested before accepting
new follow-up messages.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(agent): update stop request check in ToolLoopAgentRunner

---------

Co-authored-by: ccsang <ccsang@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: skills-like re-query missing extra_user_content_parts causes image_caption not to be injected (AstrBotDevs#6710)

当使用 skills-like tool mode 时,_resolve_tool_exec 的 re-query 调用没有
传递 extra_user_content_parts,导致图片描述等附加内容丢失。

fixes AstrBotDevs#6702

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* perf(webchat): enhance message handling with proactive saving and streaming completion (AstrBotDevs#6698)

* fix(config): respect disabled system functions in web search tools (AstrBotDevs#6584)

Co-authored-by: BillionClaw <billionclaw@cl OSS.dev>

* fix(agent): pass tool_call_timeout to subagent handsoff, cron and background task execution, and increase default timeout from 60 to 120 (AstrBotDevs#6713)

* fix(agent): pass tool_call_timeout to SubAgent handoff execution

- Add tool_call_timeout parameter to _execute_handoff method
- Pass run_context.tool_call_timeout to ctx.tool_loop_agent
- Add unit test to verify tool_call_timeout is correctly passed
- Fixes AstrBotDevs#6711: SubAgent MCP tool call timeout now respects configured timeout

The SubAgent handoff execution was using the default 60-second timeout
instead of the configured tool_call_timeout from provider settings.
This change ensures that SubAgent MCP tool calls respect the user's
configured timeout settings.

* test: add unit test for tool_call_timeout in SubAgent handoff

* fix: restore deleted test and fix test assertion

- Restore test_collect_handoff_image_urls_filters_extensionless_missing_event_file
- Fix test_collect_handoff_image_urls_keeps_extensionless_existing_event_file assertion
- Keep new test_execute_handoff_passes_tool_call_timeout_to_tool_loop_agent

* refactor: simplify tool_call_timeout passing in _execute_handoff

- Pass run_context.tool_call_timeout directly to ctx.tool_loop_agent
- Remove unnecessary local variable assignment
- Addresses review feedback from Sourcery AI

* fix(config): increase default tool call timeout from 60 to 120 seconds

---------

Co-authored-by: LehaoLin <linlehao@cuhk.edu.cn>
Co-authored-by: Soulter <905617992@qq.com>

* docs: update README.md to add separator in links section

* fix(skills): use actual sandbox path from cache instead of hardcoded workspace root (AstrBotDevs#6331)

* fix(skills): use actual sandbox path from cache instead of hardcoded workspace root

Fixes AstrBotDevs#6273

When using Shipyard booter, the sandbox workspace directory is
`/home/ship_{session_id}/workspace/` instead of the hardcoded `/workspace`.
This caused Agent to fail reading SKILL.md files with 'No such file or directory'.

Changes:
- In build_skills_prompt: prefer skill.path (from sandbox cache) over
  hardcoded SANDBOX_WORKSPACE_ROOT for sandbox_only skills
- In list_skills: always prefer sandbox_cached_paths over hardcoded path
  for sandbox_only skills

The actual path is resolved at sandbox scan time via Path.resolve() in
_build_scan_command, which returns the correct absolute path based on
the sandbox's actual working directory.

* docs: add comment explaining show_sandbox_path behavior for sandbox_only skills

Address Sourcery AI review comment:
- Clarify that show_sandbox_path is implicitly True for sandbox_only skills
- Explain why the flag is effectively ignored (no local path exists)

* refactor: simplify path_str fallback using or operator

Address review feedback: use single-line fallback instead of if-not pattern.

* style: format skill_manager.py with ruff

Fix ruff format-check failure

* fix(skills): sanitize cached sandbox skill paths

Normalize sandbox cache paths before reading or writing them so invalid,
empty, or mismatched entries fall back to a safe default SKILL.md path.

This avoids using malformed cached paths, keeps path rendering
consistent, and ensures sandbox skill listings always point to the
expected workspace location.

---------

Co-authored-by: ccsang <ccsang@users.noreply.github.com>
Co-authored-by: RC-CHN <1051989940@qq.com>

* fix: ensure Gemini array schemas always include items (AstrBotDevs#6051)

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>

* fix(webchat): render standalone HTML replies as code (AstrBotDevs#6074)

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix: fall back on Windows skill file encodings (AstrBotDevs#6058)

Co-authored-by: stablegenius49 <185121704+stablegenius49@users.noreply.github.com>

* fix(lark): Defer card creation and renew on tool call break (AstrBotDevs#6743)

* fix(lark): defer streaming card creation and renew card on tool call break

- Defer CardKit streaming card creation until the first text token
  arrives, preventing an empty card from rendering before content.
- Handle `type="break"` signal in send_streaming: close the current
  card and lazily create a new one for post-tool-call text, so the
  new card appears below the tool status message in correct order.
- Only emit "break" signal when show_tool_use is enabled; when tool
  output is hidden, the AI response continues on the same card.

* style: format ruff

* fix: cr bug

* fix: cr

* fix: convert Feishu opus files for Whisper API STT (AstrBotDevs#6078)

* fix: convert lark opus files for whisper api

* chore: ruff format

---------

Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: Soulter <905617992@qq.com>

* fix: skip empty knowledge-base embedding batches (AstrBotDevs#6106)

Co-authored-by: stablegenius49 <185121704+stablegenius49@users.noreply.github.com>

* feat(skill_manager): normalize and rename legacy skill markdown files to `SKILL.md` (AstrBotDevs#6757)

* feat(skill_manager): normalize and rename legacy skill markdown files to `SKILL.md`

* fix(vec_db): format debug log message for empty batch insert

* feat(extension): add category filtering for market plugins and enhance UI components (AstrBotDevs#6762)

* chore: bump version to 4.21.0

* feat: supports weixin personal account (AstrBotDevs#6777)

* feat: supports weixin personal account

* feat(weixin): update documentation for personal WeChat integration and add QR code image

* feat(weixin): refactor send method to streamline message handling

* fix(weixin): correct AES key encoding in media payload construction

* feat(weixin): update weixin_oc_base_url description for clarity in config metadata

* feat(weixin): enhance WeChat integration with QR code support and configuration updates

* feat(weixin): implement WeixinOCClient for improved media handling and API requests

* feat(platform): update platform status refresh interval to 5 seconds

* fix(platform.tg_adapter): import Forbidden instead of deprecated Unauthorized (AstrBotDevs#6765) (AstrBotDevs#6769)

* feat: skip search when the entire knowledge base is empty (AstrBotDevs#6750)

* feat:增加知识库全为空时的跳过检索

* apply bot suggestions

* style:reformat code

* feat: fix preserve escaped newlines in frontmatter & update tests & ci workflows (AstrBotDevs#6783)

* Feat(webui): support pinning and dragging for installed plugins (AstrBotDevs#6649) (AstrBotDevs#6776)

* refactor(persona): replace local folder components with shared folder components

* feat(webui): implement draggable reordering with animation for pinned plugins

* refactor(webui): extract PinnedPluginItem into a standalone component

* fix: handle potential None values for token usage metrics in OpenAI provider (AstrBotDevs#6788)

Such as: unsupported operand type(s) for -: 'int' and 'NoneType'

fixes: AstrBotDevs#6772

* feat: supports image compressing (AstrBotDevs#6794)

* feat: supports image compressing (AstrBotDevs#6463)

Co-authored-by: Soulter <905617992@qq.com>

* feat: 增加图像压缩最大尺寸至1280

* Update astrbot/core/astr_main_agent.py

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

* feat: 增强临时文件管理,添加图像压缩路径跟踪与清理功能

* feat: 更新图片压缩功能提示,移除对 chat_completion 提供商的限制说明

---------

Co-authored-by: Chen <42998804+a61995987@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* fix: keep all CallToolResult content items (AstrBotDevs#6149)

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* chore: bump version to 4.22.0

* docs: update wechat app version requirements for WeChat adapter and add instructions for profile photo/remark modifications

* chore: gitignore .env warker.js

* fix: remove privacy data from test case (AstrBotDevs#6803)

Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>

* fix: align mimo tts style payload with official docs (AstrBotDevs#6814)

* feat(dashboard): add log and cache cleanup in settings (AstrBotDevs#6822)

* feat(dashboard): add log and cache cleanup in settings

* refactor: simplify storage cleaner log config handling

* fix: Repair abnormal indentation

* fix(storage): harden cleanup config handling

Use typed config value access to avoid treating invalid values as
enabled flags or log paths during storage cleanup.

Also stop exposing raw backend exceptions in the dashboard storage
status API and direct users to server logs for details.

---------

Co-authored-by: RC-CHN <1051989940@qq.com>

* fix(t2i): sync active template across all configs (AstrBotDevs#6824)

* fix(t2i): sync active template across all configs

apply template activation and reset to every config profile instead of only
the default one, and reload each pipeline scheduler so changes take effect
consistently in multi-config setups

add a dashboard test that creates extra configs and verifies active template
updates and scheduler reload coverage across all config ids

* fix(t2i): reload all schedulers on template changes

extract a shared helper to reload pipeline schedulers for every config.
when syncing or resetting the active template, persist each config and
then reload all schedulers to keep mappings consistent.

also reload all schedulers when updating the currently active template,
and add dashboard tests to verify cross-config sync and scheduler
replacement behavior.

* fix: cannot use tools in siliconflow provider (AstrBotDevs#6829)

* fix: cannot use tools in siliconflow provider

* fix: handle empty choices in ChatCompletionStreamState

* fix: correct voice message support status in WeChat adapter documentation

* feat(lark): add collapsible reasoning panel support and enhance message handling (AstrBotDevs#6831)

* feat(lark): add collapsible reasoning panel support and enhance message handling

* feat(lark): refactor collapsible panel creation for improved readability and maintainability

* chore: ruff format

* perf: validate config_path before checking existence (AstrBotDevs#6722)

Add a check for empty config_path in check_exist method

* chore(deps): bump pnpm/action-setup in the github-actions group (AstrBotDevs#6862)

Bumps the github-actions group with 1 update: [pnpm/action-setup](https://github.com/pnpm/action-setup).


Updates `pnpm/action-setup` from 4.4.0 to 5.0.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](pnpm/action-setup@v4.4.0...v5.0.0)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix: wrong index in ObjectEditor updateKey causing false 'key exists' error

* fix: wrong index in ObjectEditor updateKey causing false 'key exists' error

* fix: same index mismatch issue in updateJSON

* fix(ui): stabilize ObjectEditor pair keys

Use generated ids for key-value pairs instead of array indexes to
prevent mismatch issues during editing and rendering.

Also replace duplicate-key alerts with toast warnings for a more
consistent UI experience.

---------

Co-authored-by: RC-CHN <1051989940@qq.com>

* feat(api): add GET file endpoint and update file route to support multiple methods (AstrBotDevs#6874)

* fix(openapi): rename route view function

* fix(ui): include vuetify radiobox icons (AstrBotDevs#6892)

Add the radiobox icons used indirectly by Vuetify internals
to the required MDI subset so they are kept during font
generation.

Regenerate the subset CSS and font files to prevent missing
radio button icons at runtime.

* fix(tests): update scanUsedIcons tests to include required radio icons (AstrBotDevs#6894)

* doc: Update docs/zh/platform/lark.md (AstrBotDevs#6897)

* 补充飞书配置群聊机器人的部分

- 移除了 im:message:send 权限,因为似乎飞书已经移除了该权限
- 新增关于飞书群聊如何配置权限的部分

* Update docs/zh/platform/lark.md

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

---------

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

* Feat(webui): show plugin author on cards & pinned item (AstrBotDevs#5802) (AstrBotDevs#6875)

* feat: 为卡片视图增加作者信息

* feat:置顶列表面板新增作者名称与插件名称

* docs(compshare): correct typos (AstrBotDevs#6878)

* Fix(WebUi): allow batch resetting provider config to "follow" (iss#6749) (AstrBotDevs#6825)

* feat(webui): use explicit 'follow' status for provider settings and improve batch operation logic

* fix: allow batch resetting provider config to "follow config"

* fix(AstrBotDevs#6749): use a unique constant for 'follow' status to avoid collisions with provider IDs

* fix: remove config.use_reloader = True

* refactor(ui): extract follow config sentinel constant

---------

Co-authored-by: RC-CHN <1051989940@qq.com>

* fix: keep weixin_oc polling after inbound timeouts (AstrBotDevs#6915)

* fix: keep weixin_oc polling after inbound timeouts

* Delete tests/test_weixin_oc_adapter.py

---------

Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>

* fix(i18n): update OpenAI embedding hint for better compatibility guidance

fixes: AstrBotDevs#6855

* feat: auto-append /v1 to embedding_api_base in OpenAI embedding provider (AstrBotDevs#6863)

* fix: auto-append /v1 to embedding_api_base in OpenAI embedding provider (AstrBotDevs#6855)

When users configure `embedding_api_base` without the `/v1` suffix,
the OpenAI SDK does not auto-complete it, causing request path errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: ensure API base URL for OpenAI embedding ends with /v1 or /v4

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Soulter <905617992@qq.com>

* Fix payload handling for msg_id in QQ API (AstrBotDevs#6604)

Remove msg_id from payload to prevent errors with proactive tool-call path and avoid permission issues.

Co-authored-by: Naer <88199249+V-YOP@users.noreply.github.com>

* fix(provider): add missing index field to streaming tool_call deltas (AstrBotDevs#6661) (AstrBotDevs#6692)

* fix(provider): add missing index field to streaming tool_call deltas

- Fix AstrBotDevs#6661: Streaming tool_call arguments lost when OpenAI-compatible proxy omits index field
- Gemini and some proxies (e.g. Continue) don't include index field in tool_call deltas
- Add default index=0 when missing to prevent ChatCompletionStreamState.handle_chunk() from rejecting chunks

Fixes AstrBotDevs#6661

* fix(provider): use enumerate for multi-tool-call index assignment

- Use enumerate() to assign correct index based on list position
- Iterate over all choices (not just the first) for completeness
- Addresses review feedback from sourcery-ai and gemini-code-assist

---------

Co-authored-by: Yaohua-Leo <3067173925@qq.com>
Co-authored-by: Soulter <905617992@qq.com>

* feat(skills): enhance skill installation to support multiple top-level folders and add duplicate handling, and Chinese skill name support (AstrBotDevs#6952)

* feat(skills): enhance skill installation to support multiple top-level folders and add duplicate handling

closes: AstrBotDevs#6949

* refactor(skill_manager): streamline skill name normalization and validation logic

* fix(skill_manager): update skill name regex to allow underscores in skill names

* fix(skill_manager): improve skill name normalization and validation logic

* chore: bump version to 4.22.1

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: JiangNan <1394485448@qq.com>
Co-authored-by: Soulter <905617992@qq.com>
Co-authored-by: LIghtJUNction <lightjunction.me@gmail.com>
Co-authored-by: Ruochen Pan <1051989940@qq.com>
Co-authored-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Co-authored-by: Rhonin Wang <33801807+RhoninSeiei@users.noreply.github.com>
Co-authored-by: RhoninSeiei <RhoninSeiei@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: YYMa <118096301+YuanyuanMa03@users.noreply.github.com>
Co-authored-by: linzhengtian <907305684@qq.com>
Co-authored-by: whatevertogo <149563971+whatevertogo@users.noreply.github.com>
Co-authored-by: whatevertogo <whatevertogo@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Soulter <37870767+Soulter@users.noreply.github.com>
Co-authored-by: jnMetaCode <1394485448@qq.com>
Co-authored-by: shuiping233 <49360196+shuiping233@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: 鸦羽 <Raven95676@gmail.com>
Co-authored-by: Yufeng He <40085740+universeplayer@users.noreply.github.com>
Co-authored-by: camera-2018 <40380042+camera-2018@users.noreply.github.com>
Co-authored-by: 糯米茨 <143102889+nuomicici@users.noreply.github.com>
Co-authored-by: エイカク <1259085392z@gmail.com>
Co-authored-by: Ruochen Pan <sorainygreen@gmail.com>
Co-authored-by: Scofield <59475095+shijianhuai@users.noreply.github.com>
Co-authored-by: shijianhuai <shijianhuai@simuwang.com>
Co-authored-by: machina <53079908+machinad@users.noreply.github.com>
Co-authored-by: machina <1531829828@qq.com>
Co-authored-by: leonforcode <leonbeyourside01@gmail.com>
Co-authored-by: daniel5u <danielsuuuuuu@gmail.com>
Co-authored-by: letr <123731298+letr007@users.noreply.github.com>
Co-authored-by: Helian Nuits <sxp20061207@163.com>
Co-authored-by: RichardLiu <97330937+RichardLiuda@users.noreply.github.com>
Co-authored-by: _Kerman <kermanx@qq.com>
Co-authored-by: Gargantua <124801228+catDforD@users.noreply.github.com>
Co-authored-by: Gargantua <22532097@zju.edu.cn>
Co-authored-by: 晴空 <3103908461@qq.com>
Co-authored-by: SJ <idiotgyz@gmail.com>
Co-authored-by: idiotsj <idiotsj@users.noreply.github.com>
Co-authored-by: qingyun <codingtsunami@gmail.com>
Co-authored-by: ccsang <ccsang@users.noreply.github.com>
Co-authored-by: BillionToken <hydr0codone@proton.me>
Co-authored-by: BillionClaw <billionclaw@cl OSS.dev>
Co-authored-by: LIU Yaohua <12531035@mail.sustech.edu.cn>
Co-authored-by: LehaoLin <linlehao@cuhk.edu.cn>
Co-authored-by: Stable Genius <stablegenius043@gmail.com>
Co-authored-by: Stable Genius <259448942+stablegenius49@users.noreply.github.com>
Co-authored-by: stablegenius49 <185121704+stablegenius49@users.noreply.github.com>
Co-authored-by: Lockinwize Lolite <mzwing@mzwing.eu.org>
Co-authored-by: Waterwzy <2916963017@qq.com>
Co-authored-by: M1LKT <144798909+M1LKT@users.noreply.github.com>
Co-authored-by: Chen <42998804+a61995987@users.noreply.github.com>
Co-authored-by: Frank <97429702+tsubasakong@users.noreply.github.com>
Co-authored-by: bread <104435263+bread-ovO@users.noreply.github.com>
Co-authored-by: Stardust <1441308506a@gmail.com>
Co-authored-by: Vorest <147138388+Vorest3679@users.noreply.github.com>
Co-authored-by: GH <BoneAsh@iCloud.com>
Co-authored-by: Zeng Qingwen <143274079+fishwww-ww@users.noreply.github.com>
Co-authored-by: Rainor_da! <51012640+1zzxy1@users.noreply.github.com>
Co-authored-by: Izayoi9 <105905446+Izayoi9@users.noreply.github.com>
Co-authored-by: naer-lily <88199249+naer-lily@users.noreply.github.com>
Co-authored-by: Naer <88199249+V-YOP@users.noreply.github.com>
Co-authored-by: Yaohua-Leo <3067173925@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider The bug / feature is about AI Provider, Models, LLM Agent, LLM Agent Runner. lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]MCP 外接工具与内置工具重名时,会因提取规则与清洗规则导致彻底失效

4 participants