From ba70fec9a59bbbfb030a35b11239337ce421da20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Wed, 6 May 2026 19:22:21 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96=E5=85=AC=E5=BC=80?= =?UTF-8?q?=E8=81=8A=E5=A4=A9=E9=A1=B5=E7=99=BB=E5=BD=95=E6=80=81=E6=81=A2?= =?UTF-8?q?=E5=A4=8D=E4=B8=8E=E9=89=B4=E6=9D=83=E5=BC=95=E5=AF=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持复用现有登录态并恢复 refresh token - 未认证访问时补充跳转登录提示与引导文案 --- .../app/src/locales/langs/en-US/bot.json | 2 + .../app/src/locales/langs/zh-CN/bot.json | 2 + .../app/src/views/publicChat/index.vue | 191 +++++++++++++++--- 3 files changed, 164 insertions(+), 31 deletions(-) diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json index 5ea984c..bdcd318 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json @@ -50,6 +50,8 @@ "embedUsageTip2": "Embed only in trusted sites and combine with rate limiting.", "publicPageBlocked": "Anonymous access is disabled for this bot", "publicPageBlockedTip": "Enable anonymous access in publish settings before using external links.", + "publicPageLoginRequired": "Authentication required", + "publicPageLoginRequiredTip": "This chat page requires a valid login session or access token before it can be used.", "publicChatTitle": "Public Chat Assistant", "publicChatSubtitle": "Powered by EasyFlow", "publicChatPlaceholder": "Type your question and press Enter to send", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json index 8e8cea9..81ed0d1 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json @@ -50,6 +50,8 @@ "embedUsageTip2": "建议仅在可信站点嵌入,并结合限流策略使用。", "publicPageBlocked": "该助手未开放匿名访问", "publicPageBlockedTip": "请在发布设置中开启“允许匿名访问”后再通过外链访问。", + "publicPageLoginRequired": "用户未认证,请登录", + "publicPageLoginRequiredTip": "当前聊天页需要有效登录态或访问令牌后才可继续使用。", "publicChatTitle": "公开聊天助手", "publicChatSubtitle": "由 EasyFlow 驱动", "publicChatPlaceholder": "输入你的问题,按 Enter 发送", diff --git a/easyflow-ui-admin/app/src/views/publicChat/index.vue b/easyflow-ui-admin/app/src/views/publicChat/index.vue index 38e17a6..01643f8 100644 --- a/easyflow-ui-admin/app/src/views/publicChat/index.vue +++ b/easyflow-ui-admin/app/src/views/publicChat/index.vue @@ -16,13 +16,16 @@ import ElXMarkdown from 'vue-element-plus-x/es/XMarkdown/index.js'; import { useRoute, useRouter } from 'vue-router'; import { ChatThinkingBlock } from '@easyflow/common-ui'; +import { LOGIN_PATH } from '@easyflow/constants'; import { IconifyIcon } from '@easyflow/icons'; import { $t } from '@easyflow/locales'; +import { useAccessStore } from '@easyflow/stores'; import { uuid } from '@easyflow/utils'; import { useTitle } from '@vueuse/core'; import { ElAvatar, ElButton, ElInput, ElSkeleton } from 'element-plus'; +import { refreshTokenApi } from '#/api/core'; import { baseRequestClient, sseClient } from '#/api/request'; import defaultBotAvatar from '#/assets/ai/bot/defaultBotAvatar.png'; @@ -106,12 +109,21 @@ interface PublicChatSessionRestoreResult { messages?: PublicChatMessageRecord[]; } +type AuthResolutionStatus = 'authenticated' | 'error' | 'unauthenticated'; + +interface AuthResolutionResult { + message?: string; + nextToken?: string; + status: AuthResolutionStatus; +} + const PUBLIC_CHAT_API_KEY_QUERY_KEYS = ['token', 'apikey', 'apiKey'] as const; const PUBLIC_CHAT_CONTEXT_VERSION = 1; const route = useRoute(); const router = useRouter(); const pageTitle = useTitle(); +const accessStore = useAccessStore(); const botInfo = ref(null); const conversationId = ref(''); @@ -149,6 +161,28 @@ const setPageTitle = () => { pageTitle.value = botTitle || $t('bot.publicChatTitle'); }; +const getUnauthenticatedMessage = () => $t('bot.publicPageLoginRequired'); + +const normalizePublicChatErrorMessage = (message?: string) => { + const trimmed = String(message || '').trim(); + if (!trimmed) { + return $t('bot.publicChatInitError'); + } + if (trimmed === '请登录' || trimmed === '用户未认证,请先登录') { + return getUnauthenticatedMessage(); + } + return trimmed; +}; + +const goToLogin = async () => { + await router.push({ + path: LOGIN_PATH, + query: { + redirect: encodeURIComponent(route.fullPath), + }, + }); +}; + const isAtBottom = () => { if (!messageContainerRef.value) return true; const { scrollTop, scrollHeight, clientHeight } = messageContainerRef.value; @@ -438,34 +472,109 @@ const exchangeApiKeyToAccessToken = async (apiKey: string) => { return accessToken; }; -const resolveAuthenticatedAccess = async (token: string) => { +const extractErrorStatus = (error: any) => { + const status = Number(error?.response?.status); + return Number.isFinite(status) ? status : 0; +}; + +const extractErrorMessage = (error: any) => { + const responseData = error?.response?.data ?? {}; + const message = String( + responseData?.message || responseData?.error || error?.message || '', + ).trim(); + return message; +}; + +const isUnauthorizedError = (error: any) => { + const responseData = error?.response?.data ?? {}; + return ( + extractErrorStatus(error) === 401 || Number(responseData?.errorCode) === 401 + ); +}; + +const probeAuthenticatedAccess = async (token: string) => { + if (!token.trim()) { + return { status: 'unauthenticated' } as AuthResolutionResult; + } try { const profileResp = await getResponseBody( baseRequestClient.get('/api/v1/sysAccount/myProfile', { headers: buildRequestHeaders(token), }), ); - return profileResp.errorCode === 0 && !!profileResp.data; - } catch { - return false; + if (profileResp.errorCode === 0 && profileResp.data) { + return { + nextToken: token, + status: 'authenticated', + } as AuthResolutionResult; + } + if (profileResp.errorCode === 401) { + return { status: 'unauthenticated' } as AuthResolutionResult; + } + return { + message: normalizePublicChatErrorMessage(profileResp.message), + status: 'error', + } as AuthResolutionResult; + } catch (error) { + if (isUnauthorizedError(error)) { + return { status: 'unauthenticated' } as AuthResolutionResult; + } + return { + message: normalizePublicChatErrorMessage(extractErrorMessage(error)), + status: 'error', + } as AuthResolutionResult; } }; +const tryRefreshCurrentAccessToken = async () => { + try { + const refreshed = await refreshTokenApi(); + const nextToken = String(refreshed?.data || '').trim(); + if (!nextToken) { + return { status: 'unauthenticated' } as AuthResolutionResult; + } + accessStore.setAccessToken(nextToken); + return await probeAuthenticatedAccess(nextToken); + } catch (error) { + if (isUnauthorizedError(error)) { + return { status: 'unauthenticated' } as AuthResolutionResult; + } + return { + message: normalizePublicChatErrorMessage(extractErrorMessage(error)), + status: 'error', + } as AuthResolutionResult; + } +}; + +const resolveAuthenticatedAccess = async ( + token: string, + options?: { allowRefresh?: boolean }, +) => { + const initialResult = await probeAuthenticatedAccess(token); + if (initialResult.status !== 'unauthenticated' || !options?.allowRefresh) { + return initialResult; + } + return await tryRefreshCurrentAccessToken(); +}; + const ensureRequestAccessToken = async () => { requestAccessToken.value = ''; authenticatedAccess.value = false; const urlApiKey = readUrlApiKey(); if (urlApiKey) { const exchangedToken = await exchangeApiKeyToAccessToken(urlApiKey); - const authenticated = await resolveAuthenticatedAccess(exchangedToken); - if (!authenticated) { + const authResult = await resolveAuthenticatedAccess(exchangedToken); + if (authResult.status === 'error') { + throw new Error(authResult.message || $t('bot.publicChatInitError')); + } + if (authResult.status !== 'authenticated') { throw new Error($t('bot.publicChatTokenInvalid')); } - requestAccessToken.value = exchangedToken; + requestAccessToken.value = authResult.nextToken || exchangedToken; authenticatedAccess.value = true; upsertPublicChatContext( { - accessToken: exchangedToken, + accessToken: requestAccessToken.value, authenticatedAccess: true, }, { resetConversation: true }, @@ -476,36 +585,45 @@ const ensureRequestAccessToken = async () => { const storedContext = readPublicChatContext(botId.value); if (storedContext?.accessToken) { - const tokenValid = await resolveAuthenticatedAccess( + const authResult = await resolveAuthenticatedAccess( storedContext.accessToken, ); - if (tokenValid) { - requestAccessToken.value = storedContext.accessToken; - authenticatedAccess.value = storedContext.authenticatedAccess; + if (authResult.status === 'error') { + throw new Error(authResult.message || $t('bot.publicChatInitError')); + } + if (authResult.status === 'authenticated') { + requestAccessToken.value = + authResult.nextToken || storedContext.accessToken; + authenticatedAccess.value = true; upsertPublicChatContext({ - accessToken: storedContext.accessToken, - authenticatedAccess: storedContext.authenticatedAccess, + accessToken: requestAccessToken.value, + authenticatedAccess: true, conversationId: storedContext.conversationId, }); return; } clearPublicChatContext(botId.value); } - - const tokenResp = await getResponseBody( - baseRequestClient.get('/api/temp-token/create'), - ); - if (tokenResp.errorCode !== 0 || !tokenResp.data) { - throw new Error($t('bot.publicChatInitError')); + const currentAccessToken = String(accessStore.accessToken || '').trim(); + if (currentAccessToken) { + const authResult = await resolveAuthenticatedAccess(currentAccessToken, { + allowRefresh: true, + }); + if (authResult.status === 'error') { + throw new Error(authResult.message || $t('bot.publicChatInitError')); + } + if (authResult.status === 'authenticated') { + requestAccessToken.value = authResult.nextToken || currentAccessToken; + authenticatedAccess.value = true; + upsertPublicChatContext( + { + accessToken: requestAccessToken.value, + authenticatedAccess: true, + }, + { resetConversation: true }, + ); + } } - requestAccessToken.value = String(tokenResp.data); - upsertPublicChatContext( - { - accessToken: requestAccessToken.value, - authenticatedAccess: false, - }, - { resetConversation: true }, - ); }; const resetConversationState = () => { @@ -1116,7 +1234,7 @@ const initPublicChat = async () => { console.error(error); initError.value = error instanceof Error && error.message - ? error.message + ? normalizePublicChatErrorMessage(error.message) : $t('bot.publicChatInitError'); setPageTitle(); } finally { @@ -1221,9 +1339,16 @@ onBeforeUnmount(() => { class="public-chat-banner public-chat-banner-warning" >

- {{ $t('bot.publicPageBlocked') }} + {{ $t('bot.publicPageLoginRequired') }}

-

{{ $t('bot.publicPageBlockedTip') }}

+

{{ $t('bot.publicPageLoginRequiredTip') }}

+ + {{ $t('authentication.goToLogin') }} +