diff --git a/ui/src/api/admin/core/request.ts b/ui/src/api/admin/core/request.ts index 5ed369db5e4..8ac4e123ecc 100644 --- a/ui/src/api/admin/core/request.ts +++ b/ui/src/api/admin/core/request.ts @@ -127,7 +127,7 @@ request.interceptors.response.use( console.error(error) } if (status === 404 && !requestUrl.includes('/application/authentication')) { - void router.replace({ name: 'not-found', params: { pathMatch: ['404'] } }) + void router.replace({ name: '404' }) } if (status === 401 && !requestUrl.includes('application/profile')) { const { auth } = useStore() diff --git a/ui/src/assets/error/404.png b/ui/src/assets/error/404.png new file mode 100644 index 00000000000..251157c1c66 Binary files /dev/null and b/ui/src/assets/error/404.png differ diff --git a/ui/src/assets/error/500.png b/ui/src/assets/error/500.png new file mode 100644 index 00000000000..0717ad6a1f0 Binary files /dev/null and b/ui/src/assets/error/500.png differ diff --git a/ui/src/router/ROUTE_README.md b/ui/src/router/ROUTE_README.md index aa3a2983642..e15f861eeaf 100644 --- a/ui/src/router/ROUTE_README.md +++ b/ui/src/router/ROUTE_README.md @@ -77,8 +77,11 @@ Login、Workflow 路由没有挂载 `AppLayout`,因此不会出现在左侧导 登录页面地址为 `/login`;忘记密码使用独立页面 `/forgot-password`,两者均复用登录布局且不挂载 `AppLayout`。 -Admin 使用末尾 catch-all 路由 `not-found` 渲染 `views/error/index.vue`。直接访问不存在的 -页面或 Admin API 返回需要全局处理的 404 时,统一进入该页面。 +Admin 的 `/404`(路由名 `404`)渲染 `views/error/404.vue`。未知地址由末尾 catch-all +重定向到该页;Admin API 返回需要全局处理的 404 时也进入该页。 +`/no-permission`(`no-permission`)仅供 Admin 权限守卫在没有可访问后续页面时使用,渲染 +`views/error/NoPermission.vue`。`/no-service`(`no-service`)渲染 `views/error/NoService.vue`, +作为备用入口,不统一拦截接口 500。两者仍需 Admin 登录。 ## Admin 导航拦截器 @@ -87,7 +90,7 @@ Admin Router 在 `admin/index.ts` 中统一处理导航初始化: 1. 路由 path 变化时调用 `NProgress.start()`;同一路径只更新 query 或 hash 时不启动顶部进度条。 导航完成或发生路由错误时调用 `NProgress.done()`;统一关闭 Spinner,视觉样式由 `styles/nprogress.scss` 维护。 -2. `login`、`forgot-password`、`not-found` 为公开路由,不校验登录状态。 +2. `login`、`forgot-password`、`404` 为公开路由,不校验登录状态。 3. URL 查询参数包含 `token` 时,先写入 Admin 认证 Store,用于外部认证回跳,然后使用替换 导航清理地址栏中的 token。 4. 访问非公开路由且没有 token 时跳转到 `login`,并通过 `redirect` 查询参数保留原地址,同时透传钉钉客户端入口参数 `client`、`corpId`。 @@ -115,16 +118,18 @@ Chat 使用独立入口 `src/chat.ts` 和独立 Router,不要把 Chat 路由 `chat-home-detail`(`/:accessToken/c/:chatId`)和 `chat-login`(`/:accessToken/login`)。 静态段 `/portal` 优先于动态参数匹配;后端生成的 accessToken 为 16 位十六进制字符串,不会等于 `portal`。 -根路径 `/` 重定向到 `portal-home`;匹配不到的地址由末尾 catch-all 路由 `chat-not-found` 渲染 -`views/chat/error/index.vue`,保留原地址。Router base 优先读取 `window.MaxKB.prefix`(后端按 `CHAT_PATH` -替换),再回退到 `VITE_BASE_PATH`。 +根路径 `/` 重定向到 `portal-home`;匹配不到的地址由末尾 catch-all 重定向到 `/404`(路由名 `404`), +渲染 Chat 自己的 `views/chat/error/404.vue`。门户、单应用和登录页在认证初始化失败时,直接在原地址展示 +`views/chat/error/NoService.vue`(500 图片与“暂时无法访问服务”),刷新后重新尝试初始化。 +Chat 不注册 `/no-service` 和无权限页路由。 +Router base 优先读取 `window.MaxKB.prefix`(后端按 `CHAT_PATH` 替换),再回退到 `VITE_BASE_PATH`。 认证场景(scope)由路由决定:门户路由为 `PORTAL_AUTH_SCOPE`,单应用路由为其 accessToken。 Chat 导航守卫(`chat/index.ts`): 1. `/portal` 下未定义的子路径会被单应用路由匹配为 `accessToken = 'portal'`,重定向到 `portal-home`。 2. 获取目标场景的认证前置配置(`AuthProfile`),与当前场景相同时复用。accessToken 无效、应用已停用或门户 - 未开放导致获取失败时,进入 `chat-not-found` 并保留原地址;匿名认证失败时同样处理。 + 未开放导致获取失败时,记录失败地址并放行,由页面在原地址展示服务不可用;匿名认证失败时同样处理。 3. 判断时只读取目标场景在本地存储中的 token,不切换 Store 的当前场景。 4. 登录页始终放行:`enable_auth` 为 true 时必须登录;为 false 时匿名用户也可从账户菜单主动登录, 登录成功后回到 `redirect` 记录的原地址。 diff --git a/ui/src/router/admin/index.ts b/ui/src/router/admin/index.ts index ce345f1c774..921043b532b 100644 --- a/ui/src/router/admin/index.ts +++ b/ui/src/router/admin/index.ts @@ -27,13 +27,17 @@ const router = createRouter({ ...workflowRoutes, workspaceRoutes, systemRoutes, - { path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('@/views/error/index.vue'), meta: { title: '页面不存在' } }, + // 承接权限守卫在没有可访问后续页面时的兜底跳转 + { path: '/no-permission', name: 'no-permission', component: () => import('@/views/error/NoPermission.vue'), meta: { title: '无访问权限' } }, + { path: '/no-service', name: 'no-service', component: () => import('@/views/error/NoService.vue'), meta: { title: '服务不可用' } }, + { path: '/404', name: '404', component: () => import('@/views/error/404.vue'), meta: { title: '页面不存在' } }, + { path: '/:pathMatch(.*)*', redirect: { name: '404' } }, ], }) router.beforeEach(async (to, from) => { if (to.path !== from.path) NProgress.start() - const notAuthRouteNameList = ['login', 'forgot-password', 'not-found', 'details-demo'] + const notAuthRouteNameList = ['login', 'forgot-password', '404', 'details-demo'] const { auth, user } = useStore() if (!notAuthRouteNameList.includes(to.name ? to.name.toString() : '')) { diff --git a/ui/src/router/chat/index.ts b/ui/src/router/chat/index.ts index a475903f7c4..3c924591390 100644 --- a/ui/src/router/chat/index.ts +++ b/ui/src/router/chat/index.ts @@ -28,11 +28,6 @@ function getLoginRoute(scope: string, to: RouteLocationNormalized) { return scope === PORTAL_AUTH_SCOPE ? { name: 'portal-login', query } : { name: 'chat-login', params: { accessToken: scope }, query } } -/** 渲染 404 页面并保留原地址,刷新时可重新尝试。 */ -function getNotFoundRoute(to: RouteLocationNormalized) { - return { name: 'chat-not-found', params: { pathMatch: to.path.substring(1).split('/') }, query: to.query, hash: to.hash } -} - chatRouter.beforeEach(async (to) => { if (isUnknownPortalPath(to)) return { name: 'portal-home', replace: true } @@ -52,12 +47,13 @@ chatRouter.beforeEach(async (to) => { : { name: 'chat-home', params: { accessToken: scope }, query, replace: true } } - // accessToken 无效、应用已停用或门户未开放时,认证前置配置与匿名认证会失败,统一进入 404 页面 + // accessToken 无效、应用已停用或门户未开放时,认证前置配置与匿名认证会失败,在原地址展示服务不可用页面 let authProfile: AuthProfile try { authProfile = await auth.fetchAuthProfile(scope) } catch { - return getNotFoundRoute(to) + auth.unavailablePath = to.fullPath + return true } // 登录页始终放行:开启认证时必须登录,未开启认证时匿名用户也可主动登录 @@ -76,7 +72,8 @@ chatRouter.beforeEach(async (to) => { try { await auth.postAnonymousAuth(scope) } catch { - return getNotFoundRoute(to) + auth.unavailablePath = to.fullPath + return true } auth.activate(scope, authProfile) return true diff --git a/ui/src/router/chat/routes.ts b/ui/src/router/chat/routes.ts index cb9c94a5208..9035aee0607 100644 --- a/ui/src/router/chat/routes.ts +++ b/ui/src/router/chat/routes.ts @@ -41,7 +41,7 @@ const applicationRoutes: RouteRecordRaw[] = [ component: () => import('@/views/chat/login/index.vue'), meta: { title: '登录' }, }, - { + { path: '/:accessToken', redirect: (to) => { return `/${to.params.accessToken}/` @@ -66,9 +66,10 @@ export const chatRoutes: RouteRecordRaw[] = [ ...portalRoutes, ...applicationRoutes, { - path: '/:pathMatch(.*)*', - name: 'chat-not-found', - component: () => import('@/views/chat/error/index.vue'), + path: '/404', + name: '404', + component: () => import('@/views/chat/error/404.vue'), meta: { title: '页面不存在' }, }, + { path: '/:pathMatch(.*)*', redirect: { name: '404' } }, ] diff --git a/ui/src/stores/chat/auth.ts b/ui/src/stores/chat/auth.ts index 1b404c743a9..1c79915b3eb 100644 --- a/ui/src/stores/chat/auth.ts +++ b/ui/src/stores/chat/auth.ts @@ -57,11 +57,13 @@ interface ChatAuthState { token: string /** 本次页面中已失效(401)的 token;仍保留在本地存储中,供重新认证时携带。 */ expiredToken: string + /** 初始化失败的对话地址;只在该地址展示服务不可用,避免影响其他应用或分享页。 */ + unavailablePath: string authProfile: AuthProfile | null } export const useChatAuthStore = defineStore('chat-auth', { - state: (): ChatAuthState => ({ scope: '', token: '', expiredToken: '', authProfile: null }), + state: (): ChatAuthState => ({ scope: '', token: '', expiredToken: '', unavailablePath: '', authProfile: null }), getters: { isAuthenticated: (state) => Boolean(state.token), @@ -100,6 +102,7 @@ export const useChatAuthStore = defineStore('chat-auth', { /** 切换当前场景,由守卫在放行前调用。 */ activate(scope: string, authProfile: AuthProfile) { if (scope !== this.scope) useChatUserStore().clearCurrentUser() + this.unavailablePath = '' this.scope = scope this.authProfile = authProfile this.token = this.getStoredToken(scope) diff --git a/ui/src/views/VIEW_README.md b/ui/src/views/VIEW_README.md index 69eab787139..6874fb847f1 100644 --- a/ui/src/views/VIEW_README.md +++ b/ui/src/views/VIEW_README.md @@ -63,7 +63,8 @@ | `login/`、`login/forgot-password/` | Admin 登录与找回密码 | | `chat/`、`portal/` | 单应用与门户对话 | | `chat/share/` | 公开分享链接的只读问答展示与失败重试 | -| `chat/login/`、`chat/error/`、`error/` | Chat 登录、Chat/Admin 错误页 | +| `chat/login/`、`chat/error/` | Chat 登录、独立 404 与服务不可用展示 | +| `error/404.vue`、`error/NoPermission.vue`、`error/NoService.vue` | Admin 页面不存在、无访问权限与服务不可用展示 | | `details-demo/` | 临时执行详情预览 | ## 资源列表与详情 diff --git a/ui/src/views/chat/error/404.vue b/ui/src/views/chat/error/404.vue new file mode 100644 index 00000000000..1618039228c --- /dev/null +++ b/ui/src/views/chat/error/404.vue @@ -0,0 +1,12 @@ + + + diff --git a/ui/src/views/chat/error/NoService.vue b/ui/src/views/chat/error/NoService.vue new file mode 100644 index 00000000000..8e380143bd3 --- /dev/null +++ b/ui/src/views/chat/error/NoService.vue @@ -0,0 +1,8 @@ + diff --git a/ui/src/views/chat/error/index.vue b/ui/src/views/chat/error/index.vue deleted file mode 100644 index bd94c07c1cf..00000000000 --- a/ui/src/views/chat/error/index.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/ui/src/views/chat/index.vue b/ui/src/views/chat/index.vue index 76c99ce25c3..8a3064a8477 100644 --- a/ui/src/views/chat/index.vue +++ b/ui/src/views/chat/index.vue @@ -1,5 +1,7 @@