mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(agent): cache runtime tool name lookups
This commit is contained in:
parent
50c6eb92f9
commit
f629b04f2a
@ -179,6 +179,14 @@ public class AgentToolSet {
|
|||||||
return callbackByName;
|
return callbackByName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Every runtime identifier this set can resolve: function names plus any
|
||||||
|
* Spring bean / Java class aliases captured when the set was built.
|
||||||
|
*/
|
||||||
|
public Set<String> allNames() {
|
||||||
|
return aliasIndex.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取原始的 @Tool Bean 列表
|
* 获取原始的 @Tool Bean 列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.tool.annotation.Tool;
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import vip.mate.tool.model.ToolEntity;
|
import vip.mate.tool.model.ToolEntity;
|
||||||
import vip.mate.tool.repository.ToolMapper;
|
import vip.mate.tool.repository.ToolMapper;
|
||||||
@ -24,6 +26,7 @@ import java.util.LinkedHashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -49,6 +52,9 @@ public class ToolRegistry {
|
|||||||
/** Plugin-registered tool entries with lazy availability checks */
|
/** Plugin-registered tool entries with lazy availability checks */
|
||||||
private final CopyOnWriteArrayList<PluginToolEntry> pluginTools = new CopyOnWriteArrayList<>();
|
private final CopyOnWriteArrayList<PluginToolEntry> pluginTools = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
|
private final Object enabledToolSetLock = new Object();
|
||||||
|
private volatile AgentToolSet enabledToolSetCache;
|
||||||
|
|
||||||
/** A tool entry registered by a plugin */
|
/** A tool entry registered by a plugin */
|
||||||
public record PluginToolEntry(ToolCallback callback, Supplier<Boolean> availabilityCheck) {}
|
public record PluginToolEntry(ToolCallback callback, Supplier<Boolean> availabilityCheck) {}
|
||||||
|
|
||||||
@ -58,6 +64,7 @@ public class ToolRegistry {
|
|||||||
*/
|
*/
|
||||||
public void registerPluginTool(ToolCallback callback, Supplier<Boolean> availabilityCheck) {
|
public void registerPluginTool(ToolCallback callback, Supplier<Boolean> availabilityCheck) {
|
||||||
pluginTools.add(new PluginToolEntry(callback, availabilityCheck != null ? availabilityCheck : () -> true));
|
pluginTools.add(new PluginToolEntry(callback, availabilityCheck != null ? availabilityCheck : () -> true));
|
||||||
|
invalidateEnabledToolSetCache("plugin-tool-registered:" + callback.getToolDefinition().name());
|
||||||
log.info("Plugin tool registered: {}", callback.getToolDefinition().name());
|
log.info("Plugin tool registered: {}", callback.getToolDefinition().name());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,9 +73,37 @@ public class ToolRegistry {
|
|||||||
*/
|
*/
|
||||||
public void unregisterPluginTool(String toolName) {
|
public void unregisterPluginTool(String toolName) {
|
||||||
pluginTools.removeIf(entry -> entry.callback().getToolDefinition().name().equals(toolName));
|
pluginTools.removeIf(entry -> entry.callback().getToolDefinition().name().equals(toolName));
|
||||||
|
invalidateEnabledToolSetCache("plugin-tool-unregistered:" + toolName);
|
||||||
log.info("Plugin tool unregistered: {}", toolName);
|
log.info("Plugin tool unregistered: {}", toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void invalidateEnabledToolSetCache(String reason) {
|
||||||
|
enabledToolSetCache = null;
|
||||||
|
log.debug("Enabled AgentToolSet cache invalidated: {}", reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void prewarmEnabledToolSetCache() {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
try {
|
||||||
|
getEnabledToolSet();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("Enabled AgentToolSet prewarm skipped: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onMcpServerChanged(vip.mate.tool.mcp.event.McpServerChangedEvent event) {
|
||||||
|
invalidateEnabledToolSetCache("mcp-server-changed:" + event.reason());
|
||||||
|
prewarmEnabledToolSetCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener
|
||||||
|
public void onMcpConnectionLost(vip.mate.tool.mcp.event.McpConnectionLostEvent event) {
|
||||||
|
invalidateEnabledToolSetCache("mcp-connection-lost:" + event.serverId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有已启用的工具 Bean(Spring AI @Tool 注解方式)
|
* 获取所有已启用的工具 Bean(Spring AI @Tool 注解方式)
|
||||||
* 通过数据库 enabled 标志过滤,确保 UI 开关真正生效
|
* 通过数据库 enabled 标志过滤,确保 UI 开关真正生效
|
||||||
@ -224,6 +259,22 @@ public class ToolRegistry {
|
|||||||
* 2. 当前容器中所有 ToolCallbackProvider(MCP server 等)
|
* 2. 当前容器中所有 ToolCallbackProvider(MCP server 等)
|
||||||
*/
|
*/
|
||||||
public AgentToolSet getEnabledToolSet() {
|
public AgentToolSet getEnabledToolSet() {
|
||||||
|
AgentToolSet cached = enabledToolSetCache;
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
synchronized (enabledToolSetLock) {
|
||||||
|
cached = enabledToolSetCache;
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
AgentToolSet built = buildEnabledToolSet();
|
||||||
|
enabledToolSetCache = built;
|
||||||
|
return built;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolSet buildEnabledToolSet() {
|
||||||
// Build both the bean list and the identity-based name lookup in one pass — the
|
// Build both the bean list and the identity-based name lookup in one pass — the
|
||||||
// latter lets AgentToolSet's alias index resolve a saved binding like
|
// latter lets AgentToolSet's alias index resolve a saved binding like
|
||||||
// "BrowserUseTool" or "browserUseTool" back to the same callback as "browser_use".
|
// "BrowserUseTool" or "browserUseTool" back to the same callback as "browser_use".
|
||||||
@ -299,51 +350,7 @@ public class ToolRegistry {
|
|||||||
* accept whichever convention a skill happens to declare.
|
* accept whichever convention a skill happens to declare.
|
||||||
*/
|
*/
|
||||||
public Set<String> availableFunctionNames() {
|
public Set<String> availableFunctionNames() {
|
||||||
Set<String> names = new java.util.HashSet<>();
|
return getEnabledToolSet().allNames();
|
||||||
|
|
||||||
Set<String> disabledBeanNames = toolMapper.selectList(
|
|
||||||
new LambdaQueryWrapper<ToolEntity>()
|
|
||||||
.eq(ToolEntity::getEnabled, false)
|
|
||||||
.isNotNull(ToolEntity::getBeanName)
|
|
||||||
).stream().map(ToolEntity::getBeanName).collect(Collectors.toSet());
|
|
||||||
|
|
||||||
// 1. @Tool beans — register both the bean name and every function name exposed.
|
|
||||||
Map<String, Object> beans = applicationContext.getBeansWithAnnotation(Component.class);
|
|
||||||
for (Map.Entry<String, Object> entry : beans.entrySet()) {
|
|
||||||
String beanName = entry.getKey();
|
|
||||||
Object bean = entry.getValue();
|
|
||||||
if (disabledBeanNames.contains(beanName)) continue;
|
|
||||||
boolean hasToolMethod = java.util.Arrays.stream(bean.getClass().getMethods())
|
|
||||||
.anyMatch(m -> m.isAnnotationPresent(Tool.class));
|
|
||||||
if (!hasToolMethod) continue;
|
|
||||||
names.add(beanName);
|
|
||||||
for (ToolCallback cb : ToolCallbacks.from(bean)) {
|
|
||||||
names.add(cb.getToolDefinition().name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. MCP providers — only function names exist here.
|
|
||||||
Map<String, ToolCallbackProvider> providers = applicationContext.getBeansOfType(ToolCallbackProvider.class);
|
|
||||||
for (ToolCallbackProvider provider : providers.values()) {
|
|
||||||
ToolCallback[] cbs = provider.getToolCallbacks();
|
|
||||||
if (cbs == null) continue;
|
|
||||||
for (ToolCallback cb : cbs) {
|
|
||||||
names.add(cb.getToolDefinition().name());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Plugin-registered tools — evaluate availability lazily so disabled plugins drop out.
|
|
||||||
for (PluginToolEntry entry : pluginTools) {
|
|
||||||
try {
|
|
||||||
if (Boolean.TRUE.equals(entry.availabilityCheck().get())) {
|
|
||||||
names.add(entry.callback().getToolDefinition().name());
|
|
||||||
}
|
|
||||||
} catch (Exception ignored) {
|
|
||||||
// Unreachable plugin tools don't contribute to the set.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return names;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -47,6 +47,7 @@ public class ToolService {
|
|||||||
tool.setEnabled(true);
|
tool.setEnabled(true);
|
||||||
}
|
}
|
||||||
toolMapper.insert(tool);
|
toolMapper.insert(tool);
|
||||||
|
toolRegistry.invalidateEnabledToolSetCache("tool-created:" + tool.getName());
|
||||||
return enrichRuntimeNames(tool);
|
return enrichRuntimeNames(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,9 +56,11 @@ public class ToolService {
|
|||||||
if (Boolean.TRUE.equals(existing.getBuiltin())) {
|
if (Boolean.TRUE.equals(existing.getBuiltin())) {
|
||||||
existing.setEnabled(tool.getEnabled());
|
existing.setEnabled(tool.getEnabled());
|
||||||
toolMapper.updateById(existing);
|
toolMapper.updateById(existing);
|
||||||
|
toolRegistry.invalidateEnabledToolSetCache("builtin-tool-updated:" + existing.getName());
|
||||||
return enrichRuntimeNames(existing);
|
return enrichRuntimeNames(existing);
|
||||||
}
|
}
|
||||||
toolMapper.updateById(tool);
|
toolMapper.updateById(tool);
|
||||||
|
toolRegistry.invalidateEnabledToolSetCache("tool-updated:" + tool.getName());
|
||||||
return enrichRuntimeNames(tool);
|
return enrichRuntimeNames(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,12 +70,14 @@ public class ToolService {
|
|||||||
throw new MateClawException("err.tool.builtin_readonly", "内置工具不可删除");
|
throw new MateClawException("err.tool.builtin_readonly", "内置工具不可删除");
|
||||||
}
|
}
|
||||||
toolMapper.deleteById(id);
|
toolMapper.deleteById(id);
|
||||||
|
toolRegistry.invalidateEnabledToolSetCache("tool-deleted:" + tool.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ToolEntity toggleTool(Long id, boolean enabled) {
|
public ToolEntity toggleTool(Long id, boolean enabled) {
|
||||||
ToolEntity tool = getTool(id);
|
ToolEntity tool = getTool(id);
|
||||||
tool.setEnabled(enabled);
|
tool.setEnabled(enabled);
|
||||||
toolMapper.updateById(tool);
|
toolMapper.updateById(tool);
|
||||||
|
toolRegistry.invalidateEnabledToolSetCache("tool-toggled:" + tool.getName());
|
||||||
return enrichRuntimeNames(tool);
|
return enrichRuntimeNames(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user