fix(skill): resolve tool deps by runtime function name, not class/bean name

This commit is contained in:
matevip 2026-04-24 23:24:34 +08:00
parent 52a9a785c1
commit 4c861006dc
2 changed files with 63 additions and 11 deletions

View File

@ -5,8 +5,10 @@ import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import vip.mate.skill.runtime.SkillFrontmatterParser.SkillDependencies;
import vip.mate.tool.ToolRegistry;
import vip.mate.tool.model.ToolEntity;
import vip.mate.tool.repository.ToolMapper;
@ -15,6 +17,7 @@ import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
@ -23,10 +26,20 @@ import java.util.concurrent.TimeUnit;
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SkillDependencyChecker {
private final ToolMapper toolMapper;
private final ToolRegistry toolRegistry;
/**
* {@code @Lazy} on {@link ToolRegistry}: this bean is constructed during startup,
* and ToolRegistry transitively depends on MCP / plugin infrastructure that also
* runs early the lazy proxy breaks that cycle.
*/
public SkillDependencyChecker(ToolMapper toolMapper, @Lazy ToolRegistry toolRegistry) {
this.toolMapper = toolMapper;
this.toolRegistry = toolRegistry;
}
private static final String CURRENT_OS = detectOS();
@ -75,9 +88,13 @@ public class SkillDependencyChecker {
}
}
// 4. 内部工具检查
// 4. 内部工具检查 fetch the runtime function-name set once per skill
// so we don't hit reflection N times when a skill lists many tools.
Set<String> runtimeFunctionNames = dependencies.getTools().isEmpty()
? Set.of()
: fetchRuntimeFunctionNames();
for (String toolName : dependencies.getTools()) {
if (!isToolAvailable(toolName)) {
if (!isToolAvailable(toolName, runtimeFunctionNames)) {
missing.add("tool:" + toolName);
allSatisfied = false;
}
@ -118,18 +135,25 @@ public class SkillDependencyChecker {
}
}
private boolean isToolAvailable(String toolName) {
/**
* The runtime registry (ToolRegistry) is authoritative: it knows the exact
* function names LLMs and skills call by ({@code @Tool} method name / MCP
* tool id / plugin tool name). The {@code mate_tool} DB overlay stores
* class names + bean names and does NOT match that vocabulary, so checking
* the DB alone mis-reports every real skill dependency as "missing".
*
* <p>We keep the DB lookup as a secondary fallback for the edge case where
* someone has inserted a custom row whose {@code name} happens to equal the
* function name.
*/
private boolean isToolAvailable(String toolName, Set<String> runtimeFunctionNames) {
if (runtimeFunctionNames.contains(toolName)) {
return true;
}
try {
// 先按 name 精确匹配
Long count = toolMapper.selectCount(new LambdaQueryWrapper<ToolEntity>()
.eq(ToolEntity::getName, toolName)
.eq(ToolEntity::getEnabled, true));
if (count > 0) return true;
// 再按 beanName 匹配兼容 Spring Bean 名称
count = toolMapper.selectCount(new LambdaQueryWrapper<ToolEntity>()
.eq(ToolEntity::getBeanName, toolName)
.eq(ToolEntity::getEnabled, true));
return count > 0;
} catch (Exception e) {
log.debug("Tool check failed for '{}': {}", toolName, e.getMessage());
@ -137,6 +161,16 @@ public class SkillDependencyChecker {
}
}
private Set<String> fetchRuntimeFunctionNames() {
try {
return toolRegistry.availableFunctionNames();
} catch (Exception e) {
log.warn("Failed to fetch runtime tool function names, falling back to DB check only: {}",
e.getMessage());
return Set.of();
}
}
private static boolean isWindows() {
return CURRENT_OS.equals("windows");
}

View File

@ -167,6 +167,24 @@ public class ToolRegistry {
return AgentToolSet.fromCallbacks(toolBeans, localizedCallbacks);
}
/**
* Returns the set of runtime tool function names i.e. the exact identifiers
* LLMs and skill manifests use to reference tools (the @Tool method name, the
* MCP tool name, or the registered plugin tool name).
*
* <p>Use this for dependency / availability checks instead of querying the
* {@code mate_tool} table directly: the DB overlay stores class/bean names,
* which do not match the function-name vocabulary skills declare against.
*/
public Set<String> availableFunctionNames() {
Set<String> names = new java.util.HashSet<>();
AgentToolSet toolSet = getEnabledToolSet();
for (ToolCallback cb : toolSet.callbacks()) {
names.add(cb.getToolDefinition().name());
}
return names;
}
/**
* 获取数据库中的工具配置列表全部
*/