diff --git a/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java index dea37f98..f1a22f15 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatController.java @@ -166,7 +166,8 @@ public class WebChatController { // 创建或获取会话(workspace 从 agent 获取) var webAgent = agentService.getAgent(resolvedAgentId); Long webWsId = webAgent != null ? webAgent.getWorkspaceId() : 1L; - var conv = conversationService.getOrCreateConversation(conversationId, resolvedAgentId, webchatUsername(visitorId), webWsId); + var conv = conversationService.getOrCreateWebchatConversation( + conversationId, resolvedAgentId, webchatUsername(visitorId), webWsId, effectiveSessionId); // 保存用户消息(含访客本轮引用的附件)。附件元数据一律服务端按 fileId 回查, // 不信客户端传入;path 用于 Agent 侧工具读取,对外消息视图会被剥离。 @@ -392,23 +393,50 @@ public class WebChatController { * Load this visitor's session threads (own namespace only), mapped to the * compact view. Sorted as {@code listConversations} returns them (pinned * desc, last-active desc). Shared by the list and paginated endpoints. + *
+ * Enumeration is keyed by the visitor's username plus the channel prefix
+ * ({@code webchat: 不传 limit 时返回全部消息(向后兼容);传 limit 返回最新 limit 条 + hasMore;
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 585187cc..8853c76a 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
@@ -300,6 +300,26 @@ public class ConversationService {
return conv;
}
+ /**
+ * WebChat get-or-create that also records the thread's {@code sessionId} on
+ * insert, so the visitor's /sessions listing can recover it even when the
+ * conversationId hashes (long visitorId + sessionId). The session id is
+ * written only when the row is first created; an existing row is left as-is.
+ */
+ @Transactional
+ public ConversationEntity getOrCreateWebchatConversation(String conversationId, Long agentId,
+ String username, Long workspaceId,
+ String sessionId) {
+ boolean existed = conversationMapper.selectOne(new LambdaQueryWrapper
diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/vo/ConversationVO.java b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/vo/ConversationVO.java
index 1b35bd05..3f583827 100644
--- a/mateclaw-server/src/main/java/vip/mate/workspace/conversation/vo/ConversationVO.java
+++ b/mateclaw-server/src/main/java/vip/mate/workspace/conversation/vo/ConversationVO.java
@@ -68,6 +68,7 @@ public class ConversationVO extends ConversationEntity {
vo.setPinned(entity.getPinned() != null ? entity.getPinned() : 0);
vo.setModelProvider(entity.getModelProvider());
vo.setModelName(entity.getModelName());
+ vo.setWebchatSessionId(entity.getWebchatSessionId());
vo.setCreateTime(entity.getCreateTime());
vo.setUpdateTime(entity.getUpdateTime());
// 补充关联字段
diff --git a/mateclaw-server/src/main/resources/db/migration/h2/V147__webchat_session_id.sql b/mateclaw-server/src/main/resources/db/migration/h2/V147__webchat_session_id.sql
new file mode 100644
index 00000000..8d2ff752
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/h2/V147__webchat_session_id.sql
@@ -0,0 +1,6 @@
+-- WebChat per-thread sessionId, persisted so it can be recovered even when the
+-- conversationId hashes (visitorId + sessionId > 64 chars folds into a hash,
+-- which is otherwise unrecoverable — making the thread invisible/unaddressable
+-- in the visitor's /sessions listing). NULL for non-webchat rows and for a
+-- visitor's default (no-session) thread.
+ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS webchat_session_id VARCHAR(64);
diff --git a/mateclaw-server/src/main/resources/db/migration/kingbase/V147__webchat_session_id.sql b/mateclaw-server/src/main/resources/db/migration/kingbase/V147__webchat_session_id.sql
new file mode 100644
index 00000000..36c7f171
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/kingbase/V147__webchat_session_id.sql
@@ -0,0 +1,3 @@
+-- See the H2 copy for context. KingbaseES (PostgreSQL) supports
+-- ADD COLUMN IF NOT EXISTS natively.
+ALTER TABLE mate_conversation ADD COLUMN IF NOT EXISTS webchat_session_id VARCHAR(64);
diff --git a/mateclaw-server/src/main/resources/db/migration/mysql/V147__webchat_session_id.sql b/mateclaw-server/src/main/resources/db/migration/mysql/V147__webchat_session_id.sql
new file mode 100644
index 00000000..1b7b02f1
--- /dev/null
+++ b/mateclaw-server/src/main/resources/db/migration/mysql/V147__webchat_session_id.sql
@@ -0,0 +1,15 @@
+-- See the H2 file for context. MySQL 8.0 doesn't support
+-- `ADD COLUMN IF NOT EXISTS`, so the existence check goes through
+-- INFORMATION_SCHEMA + a prepared statement.
+SET @col_exists := (
+ SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'mate_conversation'
+ AND COLUMN_NAME = 'webchat_session_id'
+);
+SET @ddl := IF(@col_exists = 0,
+ 'ALTER TABLE mate_conversation ADD COLUMN webchat_session_id VARCHAR(64) NULL COMMENT ''WebChat per-thread sessionId (recoverable even when conversationId hashes)''',
+ 'SELECT 1');
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/mateclaw-server/src/test/java/vip/mate/channel/webchat/WebChatSessionManagementTest.java b/mateclaw-server/src/test/java/vip/mate/channel/webchat/WebChatSessionManagementTest.java
new file mode 100644
index 00000000..c0fe81f2
--- /dev/null
+++ b/mateclaw-server/src/test/java/vip/mate/channel/webchat/WebChatSessionManagementTest.java
@@ -0,0 +1,136 @@
+package vip.mate.channel.webchat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.TestPropertySource;
+import vip.mate.MateClawApplication;
+import vip.mate.channel.webchat.WebChatController.WebChatSessionView;
+import vip.mate.common.result.R;
+import vip.mate.workspace.conversation.ConversationService;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * End-to-end verification of the WebChat visitor session-management endpoints
+ * against a booted context + real H2 (migrations incl. V147 run). Exercises the
+ * controller's real auth (channel lookup + visitor-token HMAC), pagination,
+ * keyword search, rename, and — the key case — that a thread whose
+ * conversationId hashed (long visitorId + sessionId) is still listed with its
+ * sessionId recovered from the persisted column.
+ */
+@SpringBootTest(
+ classes = MateClawApplication.class,
+ webEnvironment = SpringBootTest.WebEnvironment.NONE
+)
+@TestPropertySource(properties = {
+ "spring.datasource.url=jdbc:h2:mem:webchat_sess_test_${random.uuid};MODE=MySQL;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE;DB_CLOSE_DELAY=-1",
+ "spring.ai.dashscope.api-key=test-key",
+ "spring.main.web-application-type=none",
+ "mateclaw.jwt.secret=webchat-it-secret-0123456789"
+})
+class WebChatSessionManagementTest {
+
+ private static final String SECRET = "webchat-it-secret-0123456789";
+ private static final String API_KEY = "testkey1abcdefgh"; // key8 = "testkey1"
+ private static final String VISITOR = "visitorAAAA";
+ private static final long CHANNEL_ID = 9_147_001L;
+ // Long enough that "webchat:testkey1:visitorAAAA:> r = (R
>) (R>) controller.listSessions(API_KEY, token, VISITOR);
+ assertThat(r.getCode()).isEqualTo(200);
+ List
> r = (R
>) (R>) controller.listSessions(API_KEY, "bogus", VISITOR);
+ assertThat(r.getCode()).isEqualTo(401);
+ }
+
+ @Test
+ @DisplayName("pageSessions paginates and keyword-searches by title")
+ void pagesAndSearches() {
+ R