From 03c85849103dffc471e8e5aa3cea005942b58b14 Mon Sep 17 00:00:00 2001 From: matevip Date: Mon, 27 Apr 2026 19:31:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(channels):=20resolve=20issue=20#19=20?= =?UTF-8?q?=E2=80=94=20non-admin=20member=20errors=20on=20channel=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs surfaced when a non-admin workspace member opened the channel admin page: - vue-i18n "Invalid linked format" when '@' appeared in message strings without the linked-format escape. Replaced literal '@' with vue-i18n v9 literal interpolation {'@'} in both zh-CN.ts and en-US.ts (6 strings: QQ guide step3, accessControl requireMention/Tooltip). - 403 from WorkspaceAccessInterceptor was being treated as 401 by the axios interceptor and the chat SSE handler, clearing the token and redirecting to /login. Split the two: * 401 = authentication failure -> handleAuthFailure (logout) * 403 = authorization failure -> keep session, surface to caller Now a member who lacks workspace permission sees a toast instead of being silently logged out. - Two backend exception sites threw with the default code=500 for what is semantically an auth/authz event, contradicting the codes returned elsewhere for the same business event: * AuthService.login() bad credentials 500 -> 401 * WorkspaceService.requirePermission() 500 -> 403 This aligns service-layer denials with SecurityConfig (401 for missing JWT) and WorkspaceAccessInterceptor (403 for permission denied), so the same business event always produces the same code. --- .../src/main/java/vip/mate/auth/service/AuthService.java | 2 +- .../vip/mate/workspace/core/service/WorkspaceService.java | 2 +- mateclaw-ui/src/api/index.ts | 6 ++++-- mateclaw-ui/src/composables/chat/useStream.ts | 5 +++-- mateclaw-ui/src/i18n/locales/en-US.ts | 6 +++--- mateclaw-ui/src/i18n/locales/zh-CN.ts | 6 +++--- 6 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java b/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java index 25f9642c..465d8e9c 100644 --- a/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java +++ b/mateclaw-server/src/main/java/vip/mate/auth/service/AuthService.java @@ -51,7 +51,7 @@ public class AuthService { .eq(UserEntity::getEnabled, true)); if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) { - throw new MateClawException("err.auth.invalid_credentials", "用户名或密码错误"); + throw new MateClawException("err.auth.invalid_credentials", 401, "用户名或密码错误"); } String token = generateToken(user); diff --git a/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java b/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java index 8ec886a9..1e1e493f 100644 --- a/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java +++ b/mateclaw-server/src/main/java/vip/mate/workspace/core/service/WorkspaceService.java @@ -209,7 +209,7 @@ public class WorkspaceService { */ public void requirePermission(Long workspaceId, Long userId, String minRole) { if (!hasPermission(workspaceId, userId, minRole)) { - throw new MateClawException("err.workspace.insufficient_permission", "权限不足:需要 " + minRole + " 或更高角色"); + throw new MateClawException("err.workspace.insufficient_permission", 403, "权限不足:需要 " + minRole + " 或更高角色"); } } diff --git a/mateclaw-ui/src/api/index.ts b/mateclaw-ui/src/api/index.ts index 2f532171..25e52dd1 100644 --- a/mateclaw-ui/src/api/index.ts +++ b/mateclaw-ui/src/api/index.ts @@ -30,7 +30,9 @@ http.interceptors.response.use( // 后端统一响应格式 R: { code: number, msg: string, data: T } if (data && typeof data === 'object' && 'code' in data) { if (data.code === 200) return data - if (data.code === 401 || data.code === 403) { + // 401 = authentication failure → log out + // 403 = authorization failure (e.g. workspace permission denied) → keep session, surface error to caller + if (data.code === 401) { handleAuthFailure() return Promise.reject(new Error(data.msg || 'Unauthorized')) } @@ -39,7 +41,7 @@ http.interceptors.response.use( return data }, (err) => { - if (err.response?.status === 401 || err.response?.status === 403) { + if (err.response?.status === 401) { handleAuthFailure() } return Promise.reject(err.response?.data?.msg || err.message) diff --git a/mateclaw-ui/src/composables/chat/useStream.ts b/mateclaw-ui/src/composables/chat/useStream.ts index b356f5f0..926c818a 100644 --- a/mateclaw-ui/src/composables/chat/useStream.ts +++ b/mateclaw-ui/src/composables/chat/useStream.ts @@ -240,8 +240,9 @@ export function useStream(options: UseStreamOptions): UseStreamReturn { errorInfo.requestId = response.headers.get('X-Request-Id') || errorBody?.requestId || errorInfo.requestId - // 401/403 时清除 token 并跳转登录页(与 http.ts 保持一致) - if (response.status === 401 || response.status === 403) { + // 401 = authentication failure → log out (consistent with http.ts). + // 403 = authorization failure (e.g. workspace permission denied) → keep session. + if (response.status === 401) { handleAuthFailure() } throw Object.assign(new Error(errorMsg), { errorInfo }) diff --git a/mateclaw-ui/src/i18n/locales/en-US.ts b/mateclaw-ui/src/i18n/locales/en-US.ts index 0e6c9b1b..86ed0582 100644 --- a/mateclaw-ui/src/i18n/locales/en-US.ts +++ b/mateclaw-ui/src/i18n/locales/en-US.ts @@ -1561,8 +1561,8 @@ export default { allowFromPlaceholder: 'Comma-separated user IDs (empty = allow all)', denyMessage: 'Deny Message', denyMessagePlaceholder: 'Sorry, you do not have permission', - requireMention: 'Require @mention', - requireMentionTooltip: 'Whether the bot requires @mention in group chats', + requireMention: "Require {'@'}mention", + requireMentionTooltip: "Whether the bot requires {'@'}mention in group chats", }, messageFilter: { title: 'Message Filter', @@ -1639,7 +1639,7 @@ export default { qq: { step1: 'Go to QQ Open Platform and create a bot application', step2: 'Get AppID and AppSecret from the app management page, fill in below', - step3: 'Enable required message types in "Feature Config → Message Subscription" (C2C, Group @, Channel messages, etc.)', + step3: "Enable required message types in \"Feature Config → Message Subscription\" (C2C, Group {'@'}, Channel messages, etc.)", step4: 'After starting the channel, auto-receives messages via WebSocket long connection, no public IP or callback URL required', }, }, diff --git a/mateclaw-ui/src/i18n/locales/zh-CN.ts b/mateclaw-ui/src/i18n/locales/zh-CN.ts index 34d1f639..3faf0d5f 100644 --- a/mateclaw-ui/src/i18n/locales/zh-CN.ts +++ b/mateclaw-ui/src/i18n/locales/zh-CN.ts @@ -1571,8 +1571,8 @@ export default { allowFromPlaceholder: '逗号分隔用户 ID(留空 = 全部允许)', denyMessage: '拒绝提示', denyMessagePlaceholder: '抱歉,您没有使用权限', - requireMention: '需要 @提及', - requireMentionTooltip: '群聊中是否需要 @机器人才响应', + requireMention: "需要 {'@'}提及", + requireMentionTooltip: "群聊中是否需要 {'@'}机器人才响应", }, messageFilter: { title: '消息过滤', @@ -1649,7 +1649,7 @@ export default { qq: { step1: '前往 QQ 开放平台 创建机器人应用', step2: '在应用管理页面获取 AppIDAppSecret,填入下方配置', - step3: '在「功能配置 → 消息订阅」中开启需要的消息类型(C2C 消息、群聊 @消息、频道消息等)', + step3: "在「功能配置 → 消息订阅」中开启需要的消息类型(C2C 消息、群聊 {'@'}消息、频道消息等)", step4: '启动渠道后通过 WebSocket 长连接自动接收消息,无需公网 IP 和回调 URL', }, },