feat(mcp): 工具注册同步支持更新已有工具元数据
- 已存在工具自动更新 category/kind/description,无需手动改库 - status 仍由管理端维护,同步不覆盖;只插不删语义保留
This commit is contained in:
@@ -11,11 +11,13 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 工具注册同步(设计文档 §3.1):启动时把 @Tool + @XqTool 注解声明的工具
|
||||
* 同步进 tool 表(name/category/kind/description/status),只插不删——
|
||||
* 已存在的工具(含被管理员停用的)保持原状。
|
||||
* 同步进 tool 表(name/category/kind/description/status)。新工具插入;
|
||||
* 已存在的工具更新 category/kind/description 元数据(代码内客观属性),
|
||||
* 但保留管理员手动设置的 status,且不删除任何已有工具。
|
||||
*/
|
||||
@Component
|
||||
public class ToolSyncService implements ApplicationRunner {
|
||||
@@ -33,6 +35,7 @@ public class ToolSyncService implements ApplicationRunner {
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
int inserted = 0;
|
||||
int updated = 0;
|
||||
for (Object toolObject : toolObjects) {
|
||||
for (Method method : toolObject.getClass().getMethods()) {
|
||||
org.springframework.ai.tool.annotation.Tool toolAnnotation =
|
||||
@@ -42,23 +45,36 @@ public class ToolSyncService implements ApplicationRunner {
|
||||
}
|
||||
XqTool xqTool = method.getAnnotation(XqTool.class);
|
||||
String name = toolAnnotation.name().isBlank() ? method.getName() : toolAnnotation.name();
|
||||
Long exists = toolMapper.selectCount(new LambdaQueryWrapper<Tool>()
|
||||
String category = xqTool == null ? "uncategorized" : xqTool.category();
|
||||
String kind = xqTool == null ? "read" : xqTool.kind();
|
||||
String description = toolAnnotation.description();
|
||||
Tool existing = toolMapper.selectOne(new LambdaQueryWrapper<Tool>()
|
||||
.eq(Tool::getSoftDelete, 0)
|
||||
.eq(Tool::getName, name));
|
||||
if (exists != null && exists > 0) {
|
||||
continue;
|
||||
if (existing == null) {
|
||||
Tool tool = new Tool();
|
||||
tool.setName(name);
|
||||
tool.setCategory(category);
|
||||
tool.setDescription(description);
|
||||
tool.setKind(kind);
|
||||
tool.setStatus((byte) 1);
|
||||
toolMapper.insert(tool);
|
||||
inserted++;
|
||||
log.info("工具注册同步:新增 tool {}", name);
|
||||
} else if (!Objects.equals(existing.getCategory(), category)
|
||||
|| !Objects.equals(existing.getKind(), kind)
|
||||
|| !Objects.equals(existing.getDescription(), description)) {
|
||||
Tool patch = new Tool();
|
||||
patch.setId(existing.getId());
|
||||
patch.setCategory(category);
|
||||
patch.setKind(kind);
|
||||
patch.setDescription(description);
|
||||
toolMapper.updateById(patch);
|
||||
updated++;
|
||||
log.info("工具注册同步:更新 tool {} 元数据(status 保持 {})", name, existing.getStatus());
|
||||
}
|
||||
Tool tool = new Tool();
|
||||
tool.setName(name);
|
||||
tool.setCategory(xqTool == null ? "uncategorized" : xqTool.category());
|
||||
tool.setDescription(toolAnnotation.description());
|
||||
tool.setKind(xqTool == null ? "read" : xqTool.kind());
|
||||
tool.setStatus((byte) 1);
|
||||
toolMapper.insert(tool);
|
||||
inserted++;
|
||||
log.info("工具注册同步:新增 tool {}", name);
|
||||
}
|
||||
}
|
||||
log.info("工具注册同步完成,新增 {} 个", inserted);
|
||||
log.info("工具注册同步完成,新增 {} 个,更新 {} 个", inserted, updated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package me.iiv.agent.mcp;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import me.iiv.agent.entity.svc.Tool;
|
||||
import me.iiv.agent.mapper.svc.ToolMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ToolSyncServiceTest {
|
||||
|
||||
@Mock
|
||||
ToolMapper toolMapper;
|
||||
|
||||
static class FakeTools implements ToolObject {
|
||||
|
||||
@org.springframework.ai.tool.annotation.Tool(name = "demo.ping", description = "演示工具")
|
||||
@XqTool(category = "customer", kind = "read")
|
||||
public String ping() {
|
||||
return "pong";
|
||||
}
|
||||
}
|
||||
|
||||
private ToolSyncService newService() {
|
||||
return new ToolSyncService(toolMapper, List.of(new FakeTools()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void insertsNewToolWithDeclaredMetadata() {
|
||||
when(toolMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(null);
|
||||
|
||||
newService().run(null);
|
||||
|
||||
ArgumentCaptor<Tool> captor = ArgumentCaptor.forClass(Tool.class);
|
||||
verify(toolMapper).insert(captor.capture());
|
||||
Tool inserted = captor.getValue();
|
||||
assertEquals("demo.ping", inserted.getName());
|
||||
assertEquals("customer", inserted.getCategory());
|
||||
assertEquals("read", inserted.getKind());
|
||||
assertEquals("演示工具", inserted.getDescription());
|
||||
assertEquals((byte) 1, inserted.getStatus());
|
||||
verify(toolMapper, never()).updateById(any(Tool.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatesMetadataButKeepsAdminManagedStatus() {
|
||||
Tool existing = new Tool();
|
||||
existing.setId(7L);
|
||||
existing.setName("demo.ping");
|
||||
existing.setCategory("hardware");
|
||||
existing.setKind("read");
|
||||
existing.setDescription("旧描述");
|
||||
existing.setStatus((byte) 0); // 管理员手动停用
|
||||
when(toolMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existing);
|
||||
|
||||
newService().run(null);
|
||||
|
||||
ArgumentCaptor<Tool> captor = ArgumentCaptor.forClass(Tool.class);
|
||||
verify(toolMapper).updateById(captor.capture());
|
||||
Tool patch = captor.getValue();
|
||||
assertEquals(7L, patch.getId());
|
||||
assertEquals("customer", patch.getCategory());
|
||||
assertEquals("read", patch.getKind());
|
||||
assertEquals("演示工具", patch.getDescription());
|
||||
assertNull(patch.getStatus(), "status 必须由管理端维护,同步不得覆盖");
|
||||
verify(toolMapper, never()).insert(any(Tool.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void leavesUnchangedToolAlone() {
|
||||
Tool existing = new Tool();
|
||||
existing.setId(7L);
|
||||
existing.setName("demo.ping");
|
||||
existing.setCategory("customer");
|
||||
existing.setKind("read");
|
||||
existing.setDescription("演示工具");
|
||||
existing.setStatus((byte) 0);
|
||||
when(toolMapper.selectOne(any(LambdaQueryWrapper.class))).thenReturn(existing);
|
||||
|
||||
newService().run(null);
|
||||
|
||||
verify(toolMapper, never()).insert(any(Tool.class));
|
||||
verify(toolMapper, never()).updateById(any(Tool.class));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user