mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(skill): give CJK-only MCP/ACP names a stable id-based slug
This commit is contained in:
parent
729ac3bae7
commit
139a478bcd
@ -212,7 +212,7 @@ public class AcpSkillBridge {
|
||||
|
||||
private void registerWrappers(AcpEndpointEntity ep) {
|
||||
if (ep == null || !Boolean.TRUE.equals(ep.getEnabled())) return;
|
||||
String slug = slugify(ep.getName());
|
||||
String slug = slugForEndpoint(ep);
|
||||
if (slug.isEmpty()) {
|
||||
log.warn("ACP endpoint id={} has blank name; cannot register wrapper", ep.getId());
|
||||
return;
|
||||
@ -289,7 +289,7 @@ public class AcpSkillBridge {
|
||||
private SkillEntity endpointToEntity(AcpEndpointEntity ep) {
|
||||
SkillEntity s = new SkillEntity();
|
||||
s.setId(virtualIdFor(ep));
|
||||
s.setName(slugify(ep.getName()));
|
||||
s.setName(slugForEndpoint(ep));
|
||||
s.setNameEn(displayName(ep));
|
||||
s.setNameZh(ep.getDescription() != null && !ep.getDescription().isBlank()
|
||||
? displayName(ep) : null);
|
||||
@ -342,7 +342,7 @@ public class AcpSkillBridge {
|
||||
|
||||
return ResolvedSkill.builder()
|
||||
.id(virtualIdFor(ep))
|
||||
.name(slugify(ep.getName()))
|
||||
.name(slugForEndpoint(ep))
|
||||
.description(buildDescription(ep))
|
||||
.content("") // no SKILL.md
|
||||
.source("acp")
|
||||
@ -374,7 +374,7 @@ public class AcpSkillBridge {
|
||||
* it up the same way as a hand-authored skill manifest.
|
||||
*/
|
||||
private SkillManifest buildManifest(AcpEndpointEntity ep) {
|
||||
String slug = slugify(ep.getName());
|
||||
String slug = slugForEndpoint(ep);
|
||||
String toolName = "acp_" + slug + "_prompt";
|
||||
List<String> tools = List.of(toolName);
|
||||
|
||||
@ -447,6 +447,28 @@ public class AcpSkillBridge {
|
||||
return raw.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable slug for an ACP endpoint. Falls back to {@code acp-{id}} when
|
||||
* the source name has no ASCII letter/digit (e.g. pure CJK), because
|
||||
* the naive slugify would otherwise return a run of dashes and two
|
||||
* differently-named all-CJK endpoints would collide on the same slug,
|
||||
* which is also the basis for the {@code acp_<slug>_prompt} wrapper
|
||||
* tool name registered in the global tool registry.
|
||||
*/
|
||||
private String slugForEndpoint(AcpEndpointEntity ep) {
|
||||
String slug = slugify(ep.getName());
|
||||
return hasAsciiAlphaNumeric(slug) ? slug : "acp-" + ep.getId();
|
||||
}
|
||||
|
||||
private static boolean hasAsciiAlphaNumeric(String s) {
|
||||
if (s == null || s.isEmpty()) return false;
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String displayName(AcpEndpointEntity ep) {
|
||||
if (ep.getDisplayName() != null && !ep.getDisplayName().isBlank()) return ep.getDisplayName();
|
||||
return ep.getName() != null ? ep.getName() : "acp-" + ep.getId();
|
||||
|
||||
@ -133,7 +133,7 @@ public class McpSkillBridge {
|
||||
private SkillEntity serverToEntity(McpServerEntity server) {
|
||||
SkillEntity s = new SkillEntity();
|
||||
s.setId(virtualIdFor(server));
|
||||
s.setName(slugify(server.getName()));
|
||||
s.setName(slugForServer(server));
|
||||
s.setNameEn(displayName(server));
|
||||
s.setNameZh(displayName(server));
|
||||
s.setDescription(buildDescription(server));
|
||||
@ -175,7 +175,7 @@ public class McpSkillBridge {
|
||||
|
||||
return ResolvedSkill.builder()
|
||||
.id(virtualIdFor(server))
|
||||
.name(slugify(server.getName()))
|
||||
.name(slugForServer(server))
|
||||
.description(buildDescription(server))
|
||||
.content("") // no SKILL.md
|
||||
.source("mcp")
|
||||
@ -242,9 +242,10 @@ public class McpSkillBridge {
|
||||
.description("MCP server '" + server.getName() + "' must be connected. Configure in Settings ▸ MCP Connections.")
|
||||
.build();
|
||||
|
||||
String slug = slugForServer(server);
|
||||
return SkillManifest.builder()
|
||||
.id(slugify(server.getName()))
|
||||
.name(slugify(server.getName()))
|
||||
.id(slug)
|
||||
.name(slug)
|
||||
.description(buildDescription(server))
|
||||
.icon(iconFor(server))
|
||||
.version("1.0.0")
|
||||
@ -322,6 +323,27 @@ public class McpSkillBridge {
|
||||
return raw.toLowerCase(Locale.ROOT).replaceAll("[^a-z0-9_-]", "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stable slug for an MCP server. Falls back to {@code mcp-{id}} when
|
||||
* the source name has no ASCII letter/digit (e.g. pure CJK), because
|
||||
* the naive slugify would otherwise return a run of dashes — making
|
||||
* two differently-named all-CJK servers collide on the same display
|
||||
* key and breaking name-based skill lookup.
|
||||
*/
|
||||
private String slugForServer(McpServerEntity server) {
|
||||
String slug = slugify(server.getName());
|
||||
return hasAsciiAlphaNumeric(slug) ? slug : "mcp-" + server.getId();
|
||||
}
|
||||
|
||||
private static boolean hasAsciiAlphaNumeric(String s) {
|
||||
if (s == null || s.isEmpty()) return false;
|
||||
for (int i = 0; i < s.length(); i++) {
|
||||
char c = s.charAt(i);
|
||||
if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String displayName(McpServerEntity server) {
|
||||
return server.getName() != null ? server.getName() : "mcp-" + server.getId();
|
||||
}
|
||||
|
||||
@ -108,6 +108,37 @@ class McpSkillBridgeManifestTest {
|
||||
"no prefixed tool name expected, got: " + entity.getManifestJson());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("CJK-only server names slug to a stable id-based fallback instead of an all-dash collision")
|
||||
void cjkOnlyNameFallsBackToIdSlug() {
|
||||
McpServerEntity a = newServer(42L, "知识图谱对象数据查询服务");
|
||||
McpServerEntity b = newServer(43L, "客户档案信息查询服务");
|
||||
a.setToolsCacheJson(toolsJson("search"));
|
||||
b.setToolsCacheJson(toolsJson("search"));
|
||||
when(mcpServerService.listEnabled()).thenReturn(List.of(a, b));
|
||||
|
||||
List<SkillEntity> entities = bridge.listMcpDerivedSkillEntities();
|
||||
|
||||
assertEquals("mcp-42", entities.get(0).getName(),
|
||||
"all-CJK name should fall back to id-based slug");
|
||||
assertEquals("mcp-43", entities.get(1).getName(),
|
||||
"second all-CJK name must not collide with the first");
|
||||
assertTrue(entities.get(0).getManifestJson().contains("\"id\":\"mcp-42\""),
|
||||
"manifest id should mirror the fallback slug, got: " + entities.get(0).getManifestJson());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ASCII server names keep their existing slug — no regression for English names")
|
||||
void asciiNamePreservesExistingSlug() {
|
||||
McpServerEntity server = newServer(42L, "GitHub");
|
||||
server.setToolsCacheJson(toolsJson("create_issue"));
|
||||
when(mcpServerService.listEnabled()).thenReturn(List.of(server));
|
||||
|
||||
SkillEntity entity = bridge.listMcpDerivedSkillEntities().get(0);
|
||||
|
||||
assertEquals("github", entity.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("two servers exposing the same raw tool name produce distinct prefixed names")
|
||||
void twoServersSameRawNameDistinct() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user