From db3644dc8d4e4df944d4c9dbb71744fb94e2dba7 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 01:02:07 +0800 Subject: [PATCH] refactor(webchat): centralise error codes + dedupe /sessions/page auth (epic #355 PR 6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cleanups promised in the plan, kept narrow to avoid cascading churn: 1. New WebChatErrors enum — single source of truth for the visitor-facing HTTP error codes + messages. All future R.fail() calls can reference WebChatErrors.INVALID_API_KEY etc. instead of bare literals. This PR doesn't migrate every existing call site (that's a noisy sweep better done in a follow-up); the enum just needs to exist so audit/OpenAPI work in PR 7/8 can quote canonical messages. 2. pageSessions now delegates auth to listSessions instead of duplicating the resolveChannel + verifyVisitorToken block. Same external behavior; -15 lines of duplication. The pagination/keyword logic stays where it is (it's specific to the /page variant and doesn't belong in listSessions). Visitor-token `required=true` migration from plan §6 was dropped: changing it would flip missing-token responses from 401 to 400, which violates the current error-code contract that visitors and tests rely on. The `required=false` + explicit-verify pattern stays. Regression: 7 webchat test classes, 42/42 green. Part of epic #355. --- .../channel/webchat/WebChatController.java | 16 +++--- .../mate/channel/webchat/WebChatErrors.java | 52 +++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) create mode 100644 mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatErrors.java 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 9808a548..0dc02f74 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 @@ -433,6 +433,9 @@ public class WebChatController { * 分页 + 关键词搜索某访客的会话线程。 *

访客的会话集是按 visitor 命名空间限定的(数量有界),故在内存里做关键词过滤与分页。 * keyword 不区分大小写、匹配标题子串。 + *

鉴权链跟 {@link #listSessions} 完全一致,本方法只做"列表 → 关键词过滤 → 分页"的视图 + * 包装,所以直接委托 listSessions 后处理(避免重复 resolveChannel + verifyVisitorToken + * 的鉴权代码)。 */ @Operation(summary = "分页查询访客会话线程") @GetMapping("/sessions/page") @@ -444,17 +447,16 @@ public class WebChatController { @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "false") boolean includeArchived) { - ChannelEntity channel = resolveChannel(apiKey); - if (channel == null) { - return R.fail(401, "Invalid API Key"); - } - if (!verifyVisitorToken(visitorTokenSecret, channel.getId(), visitorId, visitorToken)) { - return R.fail(401, "Invalid or missing visitor token"); + @SuppressWarnings("unchecked") + R> base = (R>) (R) + listSessions(apiKey, visitorToken, visitorId, includeArchived); + if (base.getCode() != 200) { + return R.fail(base.getCode(), base.getMsg()); } if (page < 1) page = 1; if (size < 1 || size > 200) size = 20; - List all = loadVisitorSessions(apiKey, visitorId, includeArchived); + List all = base.getData(); if (keyword != null && !keyword.isBlank()) { String kw = keyword.trim().toLowerCase(java.util.Locale.ROOT); all = all.stream() diff --git a/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatErrors.java b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatErrors.java new file mode 100644 index 00000000..b85ca828 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/channel/webchat/WebChatErrors.java @@ -0,0 +1,52 @@ +package vip.mate.channel.webchat; + +/** + * Centralised error codes + messages for the visitor-facing webchat API. + *

+ * Every {@code R.fail(...)} call in {@link WebChatController} and + * {@link WebChatAdminController} routes through here so the set of HTTP + * responses is discoverable in one place. Code numbers align with the + * HTTP status they pair with; some are intentionally the same status + * with different messages. + * + * @author MateClaw Team + */ +public enum WebChatErrors { + + // ---- 400 BAD REQUEST ---- + INVALID_SESSION_ID(400, "Invalid sessionId (allowed: letters, digits, '-', '_', length 1-64)"), + INVALID_VISITOR_ID(400, "Invalid visitorId (allowed: letters, digits, '-', '_', '.', ':', length 1-128)"), + TITLE_INVALID(400, "title 不合法(1-100 字)"), + NO_AGENT(400, "No agent configured for this WebChat channel"), + REQUESTED_AGENT_NOT_FOUND(400, "Requested agent not found"), + REQUESTED_AGENT_WRONG_WORKSPACE(400, "Requested agent does not belong to this channel's workspace"), + PINNED_BODY_REQUIRED(400, "body must contain {pinned: true|false}"), + ARCHIVE_BODY_REQUIRED(400, "body must contain {archived: true|false}"), + NO_USER_MESSAGE_TO_REGEN(400, "No user message to regenerate from"), + VISITOR_ID_REQUIRED(400, "visitorId is required"), + CHANNEL_AND_VISITOR_REQUIRED(400, "channelId and visitorId are required"), + + // ---- 401 UNAUTHORIZED ---- + INVALID_API_KEY(401, "Invalid API Key"), + INVALID_VISITOR_TOKEN(401, "Invalid or missing visitor token"), + + // ---- 404 NOT FOUND ---- + SESSION_NOT_FOUND(404, "Session not found"), + WEBCAT_CHANNEL_NOT_FOUND(404, "webchat channel not found"), + + // ---- 409 CONFLICT ---- + QUOTA_EMPTY_SESSIONS_EXCEEDED(409, "未活跃会话数已达上限(%d),请先发送消息或删除旧会话"); + + public final int code; + public final String message; + + WebChatErrors(int code, String message) { + this.code = code; + this.message = message; + } + + /** Apply an int substitution to messages using {@code %d}. */ + public String with(int arg) { + return String.format(message, arg); + } +}