From c6d51f8cede287976f08d2082ae78d28760a9ac7 Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 19 May 2026 16:53:12 +0800 Subject: [PATCH] fix(wiki): resolve an agent's own knowledge base before shared ones --- .../mate/wiki/service/WikiContextService.java | 6 +- .../service/WikiKnowledgeBaseService.java | 26 +++++++ .../java/vip/mate/wiki/tool/WikiTool.java | 4 +- .../wiki/service/WikiContextServiceTest.java | 2 +- .../service/WikiKnowledgeBaseServiceTest.java | 67 +++++++++++++++++++ 5 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java index d40c2fa1..cfec94f7 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java @@ -61,12 +61,12 @@ public class WikiContextService { return ""; } - List kbs = kbService.listByAgentId(agentId); - if (kbs.isEmpty()) { + WikiKnowledgeBaseEntity primaryKb = kbService.resolvePrimaryKb(agentId); + if (primaryKb == null) { return ""; } - Long kbId = kbs.get(0).getId(); + Long kbId = primaryKb.getId(); List hits = hybridRetriever.search(kbId, userMessage, "hybrid", 5); if (hits.isEmpty()) { return ""; diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java index 53aefff2..bb142cab 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiKnowledgeBaseService.java @@ -112,6 +112,32 @@ public class WikiKnowledgeBaseService { .orderByDesc(WikiKnowledgeBaseEntity::getUpdateTime)); } + /** + * Resolve the single knowledge base an agent's wiki tools should operate on. + *

+ * Prefers a KB explicitly bound to the agent; a shared (agent-less) KB is + * only used as a fallback when the agent has no bound KB of its own. This + * matters because {@link #listByAgentId} also returns shared KBs, and a + * shared KB with a more recent {@code update_time} would otherwise win the + * {@code get(0)} pick over the agent's own KB. Within each tier the most + * recently updated KB wins. Returns {@code null} when the agent can reach + * no knowledge base at all. + */ + public WikiKnowledgeBaseEntity resolvePrimaryKb(Long agentId) { + List kbs = listByAgentId(agentId); + if (kbs.isEmpty()) { + return null; + } + if (agentId != null) { + for (WikiKnowledgeBaseEntity kb : kbs) { + if (agentId.equals(kb.getAgentId())) { + return kb; + } + } + } + return kbs.get(0); + } + public WikiKnowledgeBaseEntity getById(Long id) { return kbMapper.selectById(id); } diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java index b1502472..e16fb9da 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/tool/WikiTool.java @@ -817,8 +817,8 @@ public class WikiTool { // ==================== Helpers ==================== private Long resolveKbId(Long agentId) { - List kbs = kbService.listByAgentId(agentId); - return kbs.isEmpty() ? null : kbs.get(0).getId(); + WikiKnowledgeBaseEntity kb = kbService.resolvePrimaryKb(agentId); + return kb == null ? null : kb.getId(); } private JSONArray resolveSourceFiles(String sourceRawIdsJson) { diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiContextServiceTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiContextServiceTest.java index 98cd155d..547d15ad 100644 --- a/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiContextServiceTest.java +++ b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiContextServiceTest.java @@ -48,7 +48,7 @@ class WikiContextServiceTest { WikiKnowledgeBaseEntity kb = new WikiKnowledgeBaseEntity(); kb.setId(42L); - when(kbService.listByAgentId(any())).thenReturn(List.of(kb)); + when(kbService.resolvePrimaryKb(any())).thenReturn(kb); service = new WikiContextService(kbService, pageService, hybridRetriever, properties); } diff --git a/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java new file mode 100644 index 00000000..a012823c --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/wiki/service/WikiKnowledgeBaseServiceTest.java @@ -0,0 +1,67 @@ +package vip.mate.wiki.service; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import vip.mate.wiki.model.WikiKnowledgeBaseEntity; +import vip.mate.wiki.repository.WikiKnowledgeBaseMapper; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link WikiKnowledgeBaseService#resolvePrimaryKb(Long)}. + * + *

{@code listByAgentId} returns both the agent's own KBs and shared + * (agent-less) KBs, ordered by {@code update_time} descending. A naive + * {@code get(0)} pick therefore hands back whichever KB was touched most + * recently — which can be an unrelated shared KB. {@code resolvePrimaryKb} + * must still return the KB actually bound to the agent. + */ +class WikiKnowledgeBaseServiceTest { + + private final WikiKnowledgeBaseMapper kbMapper = mock(WikiKnowledgeBaseMapper.class); + private final WikiKnowledgeBaseService service = new WikiKnowledgeBaseService( + kbMapper, null, null, null, null, null); + + private static WikiKnowledgeBaseEntity kb(long id, Long agentId) { + WikiKnowledgeBaseEntity entity = new WikiKnowledgeBaseEntity(); + entity.setId(id); + entity.setAgentId(agentId); + return entity; + } + + @Test + @DisplayName("prefers the agent's bound KB even when a shared KB was updated more recently") + void prefersBoundKbOverNewerSharedKb() { + // listByAgentId order is update_time DESC: two shared KBs precede the bound one. + when(kbMapper.selectList(any())).thenReturn(List.of( + kb(900L, null), + kb(800L, null), + kb(100L, 7L))); + + assertThat(service.resolvePrimaryKb(7L)).isNotNull(); + assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(100L); + } + + @Test + @DisplayName("falls back to the most recent shared KB when the agent has no bound KB") + void fallsBackToSharedKbWhenNoneBound() { + when(kbMapper.selectList(any())).thenReturn(List.of( + kb(900L, null), + kb(800L, null))); + + assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(900L); + } + + @Test + @DisplayName("returns null when the agent can reach no knowledge base") + void returnsNullWhenNoKb() { + when(kbMapper.selectList(any())).thenReturn(List.of()); + + assertThat(service.resolvePrimaryKb(7L)).isNull(); + } +}