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) {