diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java index 5599aefb..bc9c703b 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/ConversationService.java @@ -210,9 +210,18 @@ public class ConversationService { * Showing them in the console surfaces threads that 500/403 on open * because the trailing ":" makes some reverse proxies strip the path * tail (issue #369). + * + *

Uses {@code notLikeLeft} rather than {@code notLike(...,"%:")}: the + * latter auto-wraps the value with extra {@code %} on both sides AND + * escapes the user-supplied {@code %}, producing a {@code %%:%} pattern + * that matches any id containing a colon — silently filtering + * out every {@code webchat:…}, {@code feishu:…}, {@code cron:…} + * conversation from the list. {@code notLikeLeft} only prepends the + * wildcard, giving the intended {@code NOT LIKE '%:'} ("does not end + * with a colon"). */ private void applyMalformedIdGuard(LambdaQueryWrapper w) { - w.notLike(ConversationEntity::getConversationId, "%:"); + w.notLikeLeft(ConversationEntity::getConversationId, ":"); } /** diff --git a/mateclaw-server/src/test/java/vip/mate/workspace/conversation/ConversationServiceWebchatVisibilityTest.java b/mateclaw-server/src/test/java/vip/mate/workspace/conversation/ConversationServiceWebchatVisibilityTest.java index 48a4c73b..9e698582 100644 --- a/mateclaw-server/src/test/java/vip/mate/workspace/conversation/ConversationServiceWebchatVisibilityTest.java +++ b/mateclaw-server/src/test/java/vip/mate/workspace/conversation/ConversationServiceWebchatVisibilityTest.java @@ -156,12 +156,12 @@ class ConversationServiceWebchatVisibilityTest { service.listConversations("admin", 1L, true); - // Assert on the rendered SQL (not the param values, which MyBatis-Plus - // percent-escapes internally) so the test stays independent of that - // implementation detail. - String sql = captor.getValue().getTargetSql().toLowerCase(); - assertThat(sql).contains("not like"); - assertThat(sql).contains("conversation_id"); + // The bound LIKE pattern must be exactly "%:" (ends-with colon), not + // "%%:%" (contains colon). The earlier notLike("%:") form produced + // the latter via MyBatis-Plus auto-wrapping + percent-escape, which + // filtered out every webchat:… / feishu:… / cron:… conversation. + assertThat(captor.getValue().getTargetSql().toLowerCase()).contains("not like"); + assertThat(captor.getValue().getParamNameValuePairs().values()).contains("%:"); } @Test @@ -175,9 +175,10 @@ class ConversationServiceWebchatVisibilityTest { service.pageConversations("admin", 1L, 1, 20, null); - String sql = captor.getValue().getTargetSql().toLowerCase(); - assertThat(sql).contains("not like"); - assertThat(sql).contains("conversation_id"); + // Force the nested-wrapper param merge: getParamNameValuePairs() is + // empty until getTargetSql() (or equivalent) has been called once. + assertThat(captor.getValue().getTargetSql().toLowerCase()).contains("not like"); + assertThat(captor.getValue().getParamNameValuePairs().values()).contains("%:"); } @Test @@ -189,9 +190,9 @@ class ConversationServiceWebchatVisibilityTest { service.listConversations("admin", 1L); // strict 2-arg - String sql = captor.getValue().getTargetSql().toLowerCase(); - assertThat(sql).contains("not like"); - assertThat(sql).contains("conversation_id"); + // Force the nested-wrapper param merge before checking values. + assertThat(captor.getValue().getTargetSql().toLowerCase()).contains("not like"); + assertThat(captor.getValue().getParamNameValuePairs().values()).contains("%:"); } private static UserEntity user(String role) {