mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(tool): show runtime names in tools admin
This commit is contained in:
parent
123d912f84
commit
3fc121c6b7
@ -86,16 +86,28 @@ public class ToolRegistry {
|
||||
* which {@link AgentToolSet} relies on (built-in tools first, MCP tools second).
|
||||
*/
|
||||
private LinkedHashMap<String, Object> getEnabledToolBeansByName() {
|
||||
return getToolBeansByName(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate Spring beans once, returning a {@code beanName → bean} map of
|
||||
* every @Tool bean. When {@code enabledOnly} is true, DB rows with
|
||||
* {@code enabled=false} are excluded; when false, disabled rows are kept for
|
||||
* admin metadata use cases where the UI still needs to resolve aliases.
|
||||
*/
|
||||
private LinkedHashMap<String, Object> getToolBeansByName(boolean enabledOnly) {
|
||||
// 1. 从数据库获取明确禁用的 beanName 黑名单
|
||||
// 逻辑:只有 DB 中存在记录且 enabled=false 的才跳过
|
||||
// DB 中没有记录的 bean 默认启用(向后兼容 + 新工具自动可用)
|
||||
Set<String> disabledBeanNames = toolMapper.selectList(
|
||||
new LambdaQueryWrapper<ToolEntity>()
|
||||
.eq(ToolEntity::getEnabled, false)
|
||||
.isNotNull(ToolEntity::getBeanName)
|
||||
).stream()
|
||||
.map(ToolEntity::getBeanName)
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> disabledBeanNames = enabledOnly
|
||||
? toolMapper.selectList(
|
||||
new LambdaQueryWrapper<ToolEntity>()
|
||||
.eq(ToolEntity::getEnabled, false)
|
||||
.isNotNull(ToolEntity::getBeanName)
|
||||
).stream()
|
||||
.map(ToolEntity::getBeanName)
|
||||
.collect(Collectors.toSet())
|
||||
: Set.of();
|
||||
|
||||
LinkedHashMap<String, Object> enabled = new LinkedHashMap<>();
|
||||
|
||||
@ -120,10 +132,29 @@ public class ToolRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Total enabled tools: {}", enabled.size());
|
||||
log.info("Total {} tools: {}", enabledOnly ? "enabled" : "registered", enabled.size());
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an alias index for every registered {@code @Tool} bean, including
|
||||
* rows disabled in DB. This is for admin display only; runtime tool
|
||||
* exposure must continue to use {@link #getEnabledToolSet()}.
|
||||
*/
|
||||
public AgentToolSet getAllToolBeanSetForAdmin() {
|
||||
LinkedHashMap<String, Object> beansByName = getToolBeansByName(false);
|
||||
List<Object> toolBeans = new ArrayList<>(beansByName.values());
|
||||
IdentityHashMap<Object, String> nameByBean = new IdentityHashMap<>();
|
||||
for (Map.Entry<String, Object> e : beansByName.entrySet()) {
|
||||
nameByBean.put(e.getValue(), e.getKey());
|
||||
}
|
||||
List<ToolCallback> callbacks = new ArrayList<>();
|
||||
for (Object bean : toolBeans) {
|
||||
Collections.addAll(callbacks, ToolCallbacks.from(bean));
|
||||
}
|
||||
return AgentToolSet.fromCallbacks(toolBeans, callbacks, nameByBean::get);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统一的 AgentToolSet(包含 @Tool Bean + ToolCallbackProvider)
|
||||
* <p>
|
||||
|
||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工具实体
|
||||
@ -68,6 +69,14 @@ public class ToolEntity {
|
||||
*/
|
||||
private String disclosureTier;
|
||||
|
||||
/**
|
||||
* Runtime {@code @Tool} function names exposed by this row's bean/class
|
||||
* aliases. Not persisted; populated for admin UI so tier changes can be
|
||||
* correlated with the names the model actually sees.
|
||||
*/
|
||||
@TableField(exist = false)
|
||||
private List<String> runtimeNames;
|
||||
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
|
||||
@ -9,6 +9,8 @@ import vip.mate.tool.model.ToolEntity;
|
||||
import vip.mate.tool.repository.ToolMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工具业务服务
|
||||
@ -24,11 +26,11 @@ public class ToolService {
|
||||
private final ToolRegistry toolRegistry;
|
||||
|
||||
public List<ToolEntity> listTools() {
|
||||
return toolRegistry.listToolEntities();
|
||||
return enrichRuntimeNames(toolRegistry.listToolEntities());
|
||||
}
|
||||
|
||||
public List<ToolEntity> listEnabledTools() {
|
||||
return toolRegistry.listEnabledToolEntities();
|
||||
return enrichRuntimeNames(toolRegistry.listEnabledToolEntities());
|
||||
}
|
||||
|
||||
public ToolEntity getTool(Long id) {
|
||||
@ -36,7 +38,7 @@ public class ToolService {
|
||||
if (tool == null) {
|
||||
throw new MateClawException("err.tool.not_found", "工具不存在: " + id);
|
||||
}
|
||||
return tool;
|
||||
return enrichRuntimeNames(tool);
|
||||
}
|
||||
|
||||
public ToolEntity createTool(ToolEntity tool) {
|
||||
@ -45,7 +47,7 @@ public class ToolService {
|
||||
tool.setEnabled(true);
|
||||
}
|
||||
toolMapper.insert(tool);
|
||||
return tool;
|
||||
return enrichRuntimeNames(tool);
|
||||
}
|
||||
|
||||
public ToolEntity updateTool(ToolEntity tool) {
|
||||
@ -53,10 +55,10 @@ public class ToolService {
|
||||
if (Boolean.TRUE.equals(existing.getBuiltin())) {
|
||||
existing.setEnabled(tool.getEnabled());
|
||||
toolMapper.updateById(existing);
|
||||
return existing;
|
||||
return enrichRuntimeNames(existing);
|
||||
}
|
||||
toolMapper.updateById(tool);
|
||||
return tool;
|
||||
return enrichRuntimeNames(tool);
|
||||
}
|
||||
|
||||
public void deleteTool(Long id) {
|
||||
@ -71,7 +73,7 @@ public class ToolService {
|
||||
ToolEntity tool = getTool(id);
|
||||
tool.setEnabled(enabled);
|
||||
toolMapper.updateById(tool);
|
||||
return tool;
|
||||
return enrichRuntimeNames(tool);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,6 +85,52 @@ public class ToolService {
|
||||
ToolEntity tool = getTool(id);
|
||||
tool.setDisclosureTier(vip.mate.tool.disclosure.DisclosureTier.fromToken(tier).token());
|
||||
toolMapper.updateById(tool);
|
||||
return enrichRuntimeNames(tool);
|
||||
}
|
||||
|
||||
private List<ToolEntity> enrichRuntimeNames(List<ToolEntity> tools) {
|
||||
if (tools == null || tools.isEmpty()) {
|
||||
return tools;
|
||||
}
|
||||
vip.mate.agent.AgentToolSet set;
|
||||
try {
|
||||
set = toolRegistry.getAllToolBeanSetForAdmin();
|
||||
} catch (Exception e) {
|
||||
log.debug("Unable to enrich tool runtime names: {}", e.getMessage());
|
||||
return tools;
|
||||
}
|
||||
for (ToolEntity tool : tools) {
|
||||
enrichRuntimeNames(tool, set);
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
private ToolEntity enrichRuntimeNames(ToolEntity tool) {
|
||||
if (tool == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
enrichRuntimeNames(tool, toolRegistry.getAllToolBeanSetForAdmin());
|
||||
} catch (Exception e) {
|
||||
log.debug("Unable to enrich tool runtime names for {}: {}", tool.getName(), e.getMessage());
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
private static void enrichRuntimeNames(ToolEntity tool, vip.mate.agent.AgentToolSet set) {
|
||||
if (tool == null || set == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> aliases = new LinkedHashSet<>();
|
||||
if (tool.getName() != null && !tool.getName().isBlank()) {
|
||||
aliases.add(tool.getName());
|
||||
}
|
||||
if (tool.getBeanName() != null && !tool.getBeanName().isBlank()) {
|
||||
aliases.add(tool.getBeanName());
|
||||
}
|
||||
Set<String> names = set.functionNamesFor(aliases);
|
||||
if (!names.isEmpty()) {
|
||||
tool.setRuntimeNames(List.copyOf(names));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,76 @@
|
||||
package vip.mate.tool.service;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.ai.support.ToolCallbacks;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import vip.mate.agent.AgentToolSet;
|
||||
import vip.mate.tool.ToolRegistry;
|
||||
import vip.mate.tool.model.ToolEntity;
|
||||
import vip.mate.tool.repository.ToolMapper;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ToolServiceRuntimeNamesTest {
|
||||
|
||||
static class ImageGenerateTool {
|
||||
@Tool(description = "text to image")
|
||||
public String image_generate() {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("listTools enriches DB class/bean rows with runtime function names")
|
||||
void listToolsEnrichesRuntimeNames() {
|
||||
ToolMapper mapper = mock(ToolMapper.class);
|
||||
ToolRegistry registry = mock(ToolRegistry.class);
|
||||
ToolEntity row = new ToolEntity();
|
||||
row.setName("ImageGenerateTool");
|
||||
row.setBeanName("imageGenerateTool");
|
||||
|
||||
ImageGenerateTool bean = new ImageGenerateTool();
|
||||
AgentToolSet set = AgentToolSet.fromCallbacks(
|
||||
List.of(bean),
|
||||
List.of(ToolCallbacks.from(bean)),
|
||||
Map.of(bean, "imageGenerateTool")::get);
|
||||
when(registry.listToolEntities()).thenReturn(List.of(row));
|
||||
when(registry.getAllToolBeanSetForAdmin()).thenReturn(set);
|
||||
|
||||
ToolService service = new ToolService(mapper, registry);
|
||||
|
||||
ToolEntity result = service.listTools().get(0);
|
||||
|
||||
assertEquals(List.of("image_generate"), result.getRuntimeNames());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("runtime names are still enriched for disabled tool rows")
|
||||
void disabledRowsStillGetRuntimeNames() {
|
||||
ToolMapper mapper = mock(ToolMapper.class);
|
||||
ToolRegistry registry = mock(ToolRegistry.class);
|
||||
ToolEntity row = new ToolEntity();
|
||||
row.setName("ImageGenerateTool");
|
||||
row.setBeanName("imageGenerateTool");
|
||||
row.setEnabled(false);
|
||||
|
||||
ImageGenerateTool bean = new ImageGenerateTool();
|
||||
AgentToolSet adminSet = AgentToolSet.fromCallbacks(
|
||||
List.of(bean),
|
||||
List.of(ToolCallbacks.from(bean)),
|
||||
Map.of(bean, "imageGenerateTool")::get);
|
||||
when(registry.listToolEntities()).thenReturn(List.of(row));
|
||||
when(registry.getAllToolBeanSetForAdmin()).thenReturn(adminSet);
|
||||
|
||||
ToolService service = new ToolService(mapper, registry);
|
||||
|
||||
ToolEntity result = service.listTools().get(0);
|
||||
|
||||
assertEquals(List.of("image_generate"), result.getRuntimeNames());
|
||||
}
|
||||
}
|
||||
@ -503,6 +503,8 @@ export interface Tool {
|
||||
description?: string
|
||||
beanName?: string
|
||||
toolType: string
|
||||
/** Runtime @Tool function names shown to the model, when resolvable. */
|
||||
runtimeNames?: string[]
|
||||
icon?: string
|
||||
mcpEndpoint?: string
|
||||
paramsSchema?: string
|
||||
|
||||
@ -43,9 +43,12 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="tool-name">{{ tool.name }}</div>
|
||||
<div class="tool-name">{{ primaryToolName(tool) }}</div>
|
||||
<div class="tool-desc">{{ tool.description }}</div>
|
||||
<code class="tool-bean">{{ tool.beanName }}</code>
|
||||
<div class="tool-meta-line">
|
||||
<code v-if="tool.name && tool.name !== primaryToolName(tool)" class="tool-bean">{{ tool.name }}</code>
|
||||
<code v-if="tool.beanName" class="tool-bean">{{ tool.beanName }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@ -160,6 +163,11 @@ function effectiveTier(tool: Tool): Tier {
|
||||
return tool.disclosureTier === 'extension' ? 'extension' : 'core'
|
||||
}
|
||||
|
||||
function primaryToolName(tool: Tool): string {
|
||||
const runtimeName = Array.isArray(tool.runtimeNames) ? tool.runtimeNames[0] : ''
|
||||
return runtimeName || tool.name
|
||||
}
|
||||
|
||||
// Only builtin / channel atomic tools are tiered on the row itself; MCP / ACP /
|
||||
// skill tools are tiered at their owning source, so their control is locked.
|
||||
function canEditTier(tool: Tool): boolean {
|
||||
@ -285,8 +293,9 @@ async function toggleTool(tool: Tool) {
|
||||
.tool-icon-wrap { width: 36px; height: 36px; background: linear-gradient(135deg, rgba(217,109,87,0.12), rgba(24,74,69,0.08)); border-radius: 12px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: var(--mc-text-secondary); }
|
||||
.tool-name { font-weight: 500; color: var(--mc-text-primary); }
|
||||
.tool-desc { font-size: 12px; color: var(--mc-text-tertiary); margin-top: 1px; }
|
||||
.tool-meta-line { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
|
||||
.bean-name { background: var(--mc-bg-sunken); padding: 2px 8px; border-radius: 4px; font-size: 12px; color: var(--mc-text-primary); }
|
||||
.tool-bean { display: inline-block; margin-top: 4px; padding: 1px 6px; background: var(--mc-bg-sunken); border-radius: 4px; font-size: 11px; color: var(--mc-text-tertiary); font-family: var(--mc-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
|
||||
.tool-bean { display: inline-block; padding: 1px 6px; background: var(--mc-bg-sunken); border-radius: 4px; font-size: 11px; color: var(--mc-text-tertiary); font-family: var(--mc-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
|
||||
.type-badge { padding: 3px 10px; border-radius: 10px; font-size: 12px; font-weight: 500; }
|
||||
.type-builtin { background: var(--mc-primary-bg); color: var(--mc-primary); }
|
||||
.type-mcp { background: var(--mc-primary-bg); color: var(--mc-primary-hover); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user