From 7c36b0d7520f7ca52f76df2fbfb0478ec3e6645c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=80=AA=E7=A8=8B=E4=BC=9F?= Date: Thu, 18 Jun 2026 19:16:51 +0800 Subject: [PATCH] fix(conversation): use notLikeLeft to avoid over-broad malformed-id filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous notLike(column, "%:") form auto-wraps the value with extra % on both sides AND escapes the user-supplied %, producing a %%:% pattern that matches any id CONTAINING a colon — silently filtering out every webchat::, feishu:, cron: conversation from the admin list / page. The frontend sidebar ends up empty. Switch to notLikeLeft(column, ":") which only prepends the wildcard, giving the intended NOT LIKE '%:' (does not end with a colon). Strengthen the three malformedIdGuard tests to assert on the bound param value ("%:" — ends-with colon) in addition to the SQL keyword, so this regression cannot return silently. The assertions must call getTargetSql() first to trigger MyBatis-Plus's nested-wrapper param merge — getParamNameValuePairs() is empty on the parent until then. --- .../conversation/ConversationService.java | 11 +++++++- ...versationServiceWebchatVisibilityTest.java | 25 ++++++++++--------- 2 files changed, 23 insertions(+), 13 deletions(-) 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) {