mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +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).
|
* which {@link AgentToolSet} relies on (built-in tools first, MCP tools second).
|
||||||
*/
|
*/
|
||||||
private LinkedHashMap<String, Object> getEnabledToolBeansByName() {
|
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 黑名单
|
// 1. 从数据库获取明确禁用的 beanName 黑名单
|
||||||
// 逻辑:只有 DB 中存在记录且 enabled=false 的才跳过
|
// 逻辑:只有 DB 中存在记录且 enabled=false 的才跳过
|
||||||
// DB 中没有记录的 bean 默认启用(向后兼容 + 新工具自动可用)
|
// DB 中没有记录的 bean 默认启用(向后兼容 + 新工具自动可用)
|
||||||
Set<String> disabledBeanNames = toolMapper.selectList(
|
Set<String> disabledBeanNames = enabledOnly
|
||||||
new LambdaQueryWrapper<ToolEntity>()
|
? toolMapper.selectList(
|
||||||
.eq(ToolEntity::getEnabled, false)
|
new LambdaQueryWrapper<ToolEntity>()
|
||||||
.isNotNull(ToolEntity::getBeanName)
|
.eq(ToolEntity::getEnabled, false)
|
||||||
).stream()
|
.isNotNull(ToolEntity::getBeanName)
|
||||||
.map(ToolEntity::getBeanName)
|
).stream()
|
||||||
.collect(Collectors.toSet());
|
.map(ToolEntity::getBeanName)
|
||||||
|
.collect(Collectors.toSet())
|
||||||
|
: Set.of();
|
||||||
|
|
||||||
LinkedHashMap<String, Object> enabled = new LinkedHashMap<>();
|
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;
|
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)
|
* 获取统一的 AgentToolSet(包含 @Tool Bean + ToolCallbackProvider)
|
||||||
* <p>
|
* <p>
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.*;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具实体
|
* 工具实体
|
||||||
@ -68,6 +69,14 @@ public class ToolEntity {
|
|||||||
*/
|
*/
|
||||||
private String disclosureTier;
|
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)
|
@TableField(fill = FieldFill.INSERT)
|
||||||
private LocalDateTime createTime;
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import vip.mate.tool.model.ToolEntity;
|
|||||||
import vip.mate.tool.repository.ToolMapper;
|
import vip.mate.tool.repository.ToolMapper;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工具业务服务
|
* 工具业务服务
|
||||||
@ -24,11 +26,11 @@ public class ToolService {
|
|||||||
private final ToolRegistry toolRegistry;
|
private final ToolRegistry toolRegistry;
|
||||||
|
|
||||||
public List<ToolEntity> listTools() {
|
public List<ToolEntity> listTools() {
|
||||||
return toolRegistry.listToolEntities();
|
return enrichRuntimeNames(toolRegistry.listToolEntities());
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ToolEntity> listEnabledTools() {
|
public List<ToolEntity> listEnabledTools() {
|
||||||
return toolRegistry.listEnabledToolEntities();
|
return enrichRuntimeNames(toolRegistry.listEnabledToolEntities());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ToolEntity getTool(Long id) {
|
public ToolEntity getTool(Long id) {
|
||||||
@ -36,7 +38,7 @@ public class ToolService {
|
|||||||
if (tool == null) {
|
if (tool == null) {
|
||||||
throw new MateClawException("err.tool.not_found", "工具不存在: " + id);
|
throw new MateClawException("err.tool.not_found", "工具不存在: " + id);
|
||||||
}
|
}
|
||||||
return tool;
|
return enrichRuntimeNames(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ToolEntity createTool(ToolEntity tool) {
|
public ToolEntity createTool(ToolEntity tool) {
|
||||||
@ -45,7 +47,7 @@ public class ToolService {
|
|||||||
tool.setEnabled(true);
|
tool.setEnabled(true);
|
||||||
}
|
}
|
||||||
toolMapper.insert(tool);
|
toolMapper.insert(tool);
|
||||||
return tool;
|
return enrichRuntimeNames(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ToolEntity updateTool(ToolEntity tool) {
|
public ToolEntity updateTool(ToolEntity tool) {
|
||||||
@ -53,10 +55,10 @@ 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);
|
||||||
return existing;
|
return enrichRuntimeNames(existing);
|
||||||
}
|
}
|
||||||
toolMapper.updateById(tool);
|
toolMapper.updateById(tool);
|
||||||
return tool;
|
return enrichRuntimeNames(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteTool(Long id) {
|
public void deleteTool(Long id) {
|
||||||
@ -71,7 +73,7 @@ public class ToolService {
|
|||||||
ToolEntity tool = getTool(id);
|
ToolEntity tool = getTool(id);
|
||||||
tool.setEnabled(enabled);
|
tool.setEnabled(enabled);
|
||||||
toolMapper.updateById(tool);
|
toolMapper.updateById(tool);
|
||||||
return tool;
|
return enrichRuntimeNames(tool);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,6 +85,52 @@ public class ToolService {
|
|||||||
ToolEntity tool = getTool(id);
|
ToolEntity tool = getTool(id);
|
||||||
tool.setDisclosureTier(vip.mate.tool.disclosure.DisclosureTier.fromToken(tier).token());
|
tool.setDisclosureTier(vip.mate.tool.disclosure.DisclosureTier.fromToken(tier).token());
|
||||||
toolMapper.updateById(tool);
|
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;
|
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
|
description?: string
|
||||||
beanName?: string
|
beanName?: string
|
||||||
toolType: string
|
toolType: string
|
||||||
|
/** Runtime @Tool function names shown to the model, when resolvable. */
|
||||||
|
runtimeNames?: string[]
|
||||||
icon?: string
|
icon?: string
|
||||||
mcpEndpoint?: string
|
mcpEndpoint?: string
|
||||||
paramsSchema?: string
|
paramsSchema?: string
|
||||||
|
|||||||
@ -43,9 +43,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="tool-name">{{ tool.name }}</div>
|
<div class="tool-name">{{ primaryToolName(tool) }}</div>
|
||||||
<div class="tool-desc">{{ tool.description }}</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>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -160,6 +163,11 @@ function effectiveTier(tool: Tool): Tier {
|
|||||||
return tool.disclosureTier === 'extension' ? 'extension' : 'core'
|
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 /
|
// 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.
|
// skill tools are tiered at their owning source, so their control is locked.
|
||||||
function canEditTier(tool: Tool): boolean {
|
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-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-name { font-weight: 500; color: var(--mc-text-primary); }
|
||||||
.tool-desc { font-size: 12px; color: var(--mc-text-tertiary); margin-top: 1px; }
|
.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); }
|
.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-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-builtin { background: var(--mc-primary-bg); color: var(--mc-primary); }
|
||||||
.type-mcp { background: var(--mc-primary-bg); color: var(--mc-primary-hover); }
|
.type-mcp { background: var(--mc-primary-bg); color: var(--mc-primary-hover); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user