Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/src/api/admin/core/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Binary file added ui/src/assets/error/404.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added ui/src/assets/error/500.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 12 additions & 7 deletions ui/src/router/ROUTE_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 导航拦截器

Expand All @@ -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`。
Expand Down Expand Up @@ -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` 记录的原地址。
Expand Down
8 changes: 6 additions & 2 deletions ui/src/router/admin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() : '')) {
Expand Down
13 changes: 5 additions & 8 deletions ui/src/router/chat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -52,12 +47,13 @@ chatRouter.beforeEach(async (to) => {
: { name: 'chat-home', params: { accessToken: scope }, query, replace: true }
}

// accessToken 无效、应用已停用或门户未开放时,认证前置配置与匿名认证会失败,统一进入 404 页面
// accessToken 无效、应用已停用或门户未开放时,认证前置配置与匿名认证会失败,在原地址展示服务不可用页面
let authProfile: AuthProfile<unknown>
try {
authProfile = await auth.fetchAuthProfile(scope)
} catch {
return getNotFoundRoute(to)
auth.unavailablePath = to.fullPath
return true
}

// 登录页始终放行:开启认证时必须登录,未开启认证时匿名用户也可主动登录
Expand All @@ -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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Findings

🔴 Potential Issues

1. Returning true allows navigation to a route that may not exist / shouldn't render
Previously, on auth failure the code redirected to a dedicated 404 route. Now it sets auth.unavailablePath and returns true, letting the original route proceed to render. If downstream components assume an authenticated user or valid app context, they may crash or leak internal error states. You rely entirely on some other component watching unavailablePath to intercept rendering — this coupling is implicit and fragile.

  • Suggestion: Verify there is a consumer (e.g., an app-level guard/wrapper) that reads unavailablePath and renders an "unavailable" view instead of the normal component tree. Document this contract.
  • Consider returning a dedicated /service-unavailable route as a fallback if no such guard exists.

2. Mutating shared state (auth.unavailablePath) inside a navigation guard
Writing to global reactive state during beforeEach can cause race conditions with concurrent navigations. If two navigations happen in-flight, the second may overwrite unavailablePath set by the first.

  • Suggestion: Clear unavailablePath at the start of beforeEach (or when navigation resolves successfully), and consider using route meta / query to carry the reason instead of a shared mutable field.

3. to.fullPath may include hash/query that leaks sensitive data
Storing the full path (including tokens in query, e.g. accessToken) into persistent/reactive state could expose credentials in the UI or logs.

  • Suggestion: Sanitize before storing — strip query/hash, or only store to.path.

4. Removed getNotFoundRoute — confirm no other callers
The diff removes the helper but there might be other references elsewhere; ensure it's fully cleaned up (no dangling imports/usages).


🟡 Minor / Style

5. Inconsistent replace: true handling
In the earlier block you use { ..., replace: true } for redirects, but when returning true here navigation semantics differ. Confirm whether prior history entries should be replaced to avoid "back button" returning to a broken page.

6. Comment says "在原地址展示服务不可用页面" but code just returns true
The behavior described (render unavailable page at original URL) is not implemented in this diff — it depends on external logic. The comment overstates what the patch does; clarify.


🟢 Optimization

7. Duplicate auth-failure handling
The same try/catch → set unavailablePath → return true pattern appears twice. Extract a small helper:

function handleAuthFailure(to: RouteLocationNormalized) {
  auth.unavailablePath = sanitize(to)
  return true
}

8. Early unavailablePath reset
Add auth.unavailablePath = undefined near the top of beforeEach so stale values don't persist across successful navigations.


Summary

The main risk is that returning true on auth failure lets unauthenticated/failed-context requests reach application routes without an explicit render-intercept guarantee. Verify the consuming guard exists, sanitize stored paths, and reset shared state per-navigation.

Expand Down
9 changes: 5 additions & 4 deletions ui/src/router/chat/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const applicationRoutes: RouteRecordRaw[] = [
component: () => import('@/views/chat/login/index.vue'),
meta: { title: '登录' },
},
{
{
path: '/:accessToken',
redirect: (to) => {
return `/${to.params.accessToken}/`
Expand All @@ -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' } },
]
5 changes: 4 additions & 1 deletion ui/src/stores/chat/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,13 @@ interface ChatAuthState {
token: string
/** 本次页面中已失效(401)的 token;仍保留在本地存储中,供重新认证时携带。 */
expiredToken: string
/** 初始化失败的对话地址;只在该地址展示服务不可用,避免影响其他应用或分享页。 */
unavailablePath: string
authProfile: AuthProfile<unknown> | 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),
Expand Down Expand Up @@ -100,6 +102,7 @@ export const useChatAuthStore = defineStore('chat-auth', {
/** 切换当前场景,由守卫在放行前调用。 */
activate(scope: string, authProfile: AuthProfile<unknown>) {
if (scope !== this.scope) useChatUserStore().clearCurrentUser()
this.unavailablePath = ''
this.scope = scope
this.authProfile = authProfile
this.token = this.getStoredToken(scope)
Expand Down
3 changes: 2 additions & 1 deletion ui/src/views/VIEW_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/` | 临时执行详情预览 |

## 资源列表与详情
Expand Down
12 changes: 12 additions & 0 deletions ui/src/views/chat/error/404.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
defineOptions({ name: 'NotFoundPage' })
</script>

<template>
<div class="flex-center mt-24">
<div>
<img src="@/assets/error/404.png" width="250" alt="" />
<h4 class="text-center">404</h4>
</div>
</div>
</template>
8 changes: 8 additions & 0 deletions ui/src/views/chat/error/NoService.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div class="flex-center min-h-screen">
<div>
<img src="@/assets/error/500.png" width="250" alt="" />
<h4 class="text-center">暂时无法访问服务</h4>
</div>
</div>
</template>
5 changes: 0 additions & 5 deletions ui/src/views/chat/error/index.vue

This file was deleted.

15 changes: 10 additions & 5 deletions ui/src/views/chat/index.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
<script setup lang="ts">
import { watch } from 'vue'
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import NoService from '@/views/chat/error/NoService.vue'
import ConversationChat from '@/conversation-panel/view/chat/index.vue'
import { useStore } from '@/stores/chat'

defineOptions({ name: 'ChatApplication' })

// 单应用对话:智能体 ID 与会话地址同步由对话面板 chat 模式处理,页面只加载侧栏所需的当前用户
const { auth, user } = useStore()
const route = useRoute()
const unavailable = computed(() => auth.unavailablePath === route.fullPath)

// 当前用户随 token 变化:认证 Store 切换 token 时会清除用户档案,这里重新加载
watch(
() => auth.token,
(token) => {
if (token && !user.chatUserProfile) user.loadCurrentUser()
[() => auth.token, unavailable],
([token, serviceUnavailable]) => {
if (!serviceUnavailable && token && !user.chatUserProfile) user.loadCurrentUser()
},
{ immediate: true },
)
</script>

<template>
<main class="h-screen bg-white">
<NoService v-if="unavailable" />
<main v-else class="h-screen bg-white">
<!-- token 变化(重新认证或登录)时重建面板,重新加载智能体与会话数据;token 清除后等待重新认证 -->
<ConversationChat v-if="auth.token" :key="auth.token" />
</main>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Overall the change is sound. Below are observations, potential issues, and suggestions.

Correctness / Logic

  1. watch array source with immediate: true — OK but subtle. When watching an array of getters, both values are provided as a tuple on first run. That's correct here, but note that unavailable is a computed ref; combining it in the array along with a plain getter works, but mixing forms can be confusing. Consider a single getter returning an object/tuple for clarity:

    watch(
      () => [auth.token, unavailable.value] as const,
      ([token, serviceUnavailable]) => { ... },
      { immediate: true },
    )
  2. Race / transient state: If route.fullPath changes while a loadCurrentUser() is in flight, the user may still load even though the page just flipped to unavailable. Probably acceptable, but if strictness matters, guard after await or use a cancellation flag.

  3. unavailable reactivity relies on auth.unavailablePath being reactive. Ensure unavailablePath lives inside the store's reactive state (ref/reactive), otherwise computed won't recompute. Worth verifying.

  4. watch will fire whenever token changes OR unavailable flips. That means navigating to/away from the unavailable route triggers the callback even when the token hasn't changed. The !user.chatUserProfile guard makes this harmless, but it's implicit. A comment explaining the intent would help.

  5. Dependency on route inside setup is fine, but note the component must be under a router context (it is, since useRoute is used). Also confirm this view isn't rendered outside <router-view>.

Template / Rendering

  1. <NoService v-if="unavailable" /> replaces the entire page including layout. If NoService is expected to render inside the standard chat layout/shell (sidebar + nav), rendering it as a sibling of <main> may break layout. If it's meant to be full-screen, consider wrapping consistently:

    <main class="h-screen bg-white">
      <NoService v-if="unavailable" />
      <ConversationChat v-else-if="auth.token" :key="auth.token" />
    </main>

    This keeps the white background and screen sizing applied uniformly. Otherwise NoService needs its own h-screen.

  2. Comment/image drift: The original comment says "页面只加载侧栏所需的当前用户". With the new unavailable branch, the sidebar isn't loaded either. Update the comment.

  3. v-if + v-else swap unmounts/remounts ConversationChat internally? No — since the whole <main> is toggled, going unavailable and back remounts ConversationChat, resetting panel state. If preserving state matters (e.g., draft input), prefer keeping <main> mounted and only hiding one child (see 创建模型时,添加 Ollama 供应商报错。 #6).

  4. display:none vs v-if: if you want to keep chat state alive across temporary unavailability, use v-show on the panel side.

Minor / Style

  1. Naming: unavailable is vague. Prefer isServiceUnavailable or isOnUnavailableRoute.

  2. String comparison route.fullPath is exact-match — trailing slash, query parameters, or hash will cause mismatches. If unavailablePath is configured storage value, ensure both sides are normalized (strip trailing /, ignore query/hash) or compare route.path instead of fullPath.

  3. watch import ordering: import { computed, watch } from 'vue' — fine, but if lint enforces alphabetical named imports, verify rule.

  4. Store destructuring: const { auth, user } = useStore() is fine for Pinia but bypasses some tooling; ensure the store returns settled refs.

  5. Type safety: ([token, serviceUnavailable]) types should be inferred; no action needed but adding explicit tuple type would catch regressions in destructuring.

  6. Error handling: user.loadCurrentUser() return promise isn't awaited/caught. In a watcher, unhandled rejections can leak. Add .catch(...) or make the callback async with try/catch.

Suggested refined version

const route = useRoute()
const isServiceUnavailable = computed(
  () => auth.unavailablePath === route.path, // or normalized fullPath
)

watch(
  () => [auth.token, isServiceUnavailable.value] as const,
  async ([token, serviceUnavailable]) => {
    if (serviceUnavailable || !token || user.chatUserProfile) return
    try {
      await user.loadCurrentUser()
    } catch (err) {
      console.error('[ChatApplication] failed to load current user', err)
    }
  },
  { immediate: true },
)

Template:

<template>
  <main class="h-screen bg-white">
    <NoService v-if="isServiceUnavailable" />
    <ConversationChat
      v-else-if="auth.token"
      :key="auth.token"
    />
  </main>
</template>

Summary of priorities

Expand Down
6 changes: 5 additions & 1 deletion ui/src/views/chat/login/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { CHAT_API_BASE_PATH } from '@/api/constants'
import { LOGIN_METHOD } from '@/api/enums'
import type { ApplicationAuthProfileMeta, LoginConfig, LoginMethod, PortalAuthProfileMeta } from '@/api/types'
import { useStore } from '@/stores/chat'
import NoService from '@/views/chat/error/NoService.vue'
import LoginModePanel from '@/views/login/components/LoginModePanel.vue'
import type { LoginService } from '@/views/login/types'

Expand All @@ -20,6 +21,7 @@ const route = useRoute()
const { t } = useI18n({ useScope: 'global' })
const router = useRouter()
const { auth } = useStore()
const unavailable = computed(() => auth.unavailablePath === route.fullPath)
const isClientLoggingIn = ref(false)
console.log(auth)
const loginTitle = computed(() => {
Expand Down Expand Up @@ -128,6 +130,7 @@ const loginService: LoginService = {
}
// 客户端授权期间暂停普通登录,失败后恢复原登录方式。
onBeforeMount(async () => {
if (unavailable.value) return
isClientLoggingIn.value = true
try {
const code = await getDingTalkClientAuthCode(route.query, {
Expand Down Expand Up @@ -157,7 +160,8 @@ const goBack = () => {
</script>

<template>
<div class="chat-login flex-center flex-col">
<NoService v-if="unavailable" />
<div v-else class="chat-login flex-center flex-col">
<div class="mb-6 flex-align-center gap-4 max-w-full">
<ApplicationIcon :icon="auth.authProfile?.logo" :size="32" class="shrink-0" />
<h2 class="max-w-full truncate" :title="loginTitle">{{ loginTitle }}</h2>
Expand Down
12 changes: 12 additions & 0 deletions ui/src/views/error/404.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script setup lang="ts">
defineOptions({ name: 'NotFoundPage' })
</script>

<template>
<div class="flex-center mt-24">
<div>
<img src="@/assets/error/404.png" width="250" alt="" />
<h4 class="text-center">404</h4>
</div>
</div>
</template>
8 changes: 8 additions & 0 deletions ui/src/views/error/NoPermission.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div class="flex-center mt-24">
<div>
<img src="@/assets/error/500.png" width="250" alt="" />
<h4 class="text-center">当前用户暂无权限访问,请联系管理员</h4>
</div>
</div>
</template>
8 changes: 8 additions & 0 deletions ui/src/views/error/NoService.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<template>
<div class="flex-center mt-24">
<div>
<img src="@/assets/error/500.png" width="250" alt="" />
<h4 class="text-center">暂时无法访问服务</h4>
</div>
</div>
</template>
20 changes: 0 additions & 20 deletions ui/src/views/error/index.vue

This file was deleted.

15 changes: 10 additions & 5 deletions ui/src/views/portal/index.vue
Original file line number Diff line number Diff line change
@@ -1,25 +1,30 @@
<script setup lang="ts">
import { watch } from 'vue'
import { computed, watch } from 'vue'
import { useRoute } from 'vue-router'
import NoService from '@/views/chat/error/NoService.vue'
import PortalConversation from '@/conversation-panel/view/portal/index.vue'
import { useStore } from '@/stores/chat'

defineOptions({ name: 'PortalApplicationChat' })

// 门户对话:智能体与会话地址同步由对话面板 portal 模式处理,页面只加载侧栏所需的当前用户
const { auth, user } = useStore()
const route = useRoute()
const unavailable = computed(() => auth.unavailablePath === route.fullPath)

// 当前用户随 token 变化:认证 Store 切换 token 时会清除用户档案,这里重新加载
watch(
() => auth.token,
(token) => {
if (token && !user.chatUserProfile) user.loadCurrentUser()
[() => auth.token, unavailable],
([token, serviceUnavailable]) => {
if (!serviceUnavailable && token && !user.chatUserProfile) user.loadCurrentUser()
},
{ immediate: true },
)
</script>

<template>
<main class="h-screen bg-white">
<NoService v-if="unavailable" />
<main v-else class="h-screen bg-white">
<!-- token 变化(重新认证或登录)时重建面板,重新加载智能体与会话数据;token 清除后等待重新认证 -->
<PortalConversation v-if="auth.token" :key="auth.token" />
</main>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issues / irregularities:

  1. Watcher fires on unavailable change but logic depends on route.fullPath indirectly — if the route changes to a different path while token stays the same and no profile exists, it won't retry loading. Acceptable, but note that navigating away from unavailablePath with token present and no profile won't trigger the reload unless unavailable transitions. Consider whether reload should also occur on route change.

  2. Race condition / redundant loads — user.loadCurrentUser() isn't awaited or guarded; rapid token changes (or token + unavailable flips) can trigger concurrent requests. Consider a loading flag in the store.

  3. NoService renders outside <main> — markup differs (<main> vs bare component). If NoService doesn't provide a root <main>, semantic/screen-reader structure may differ. Minor.

  4. English comment mismatch — comments are in Chinese; fine but ensure consistency if English is required.

Optimization suggestions:

  • Use computed for the watcher source; already done — good.
  • Prefer watchEffect for conciseness, or keep array watcher (current is fine).
  • Add await/error handling: void user.loadCurrentUser().catch(...) to avoid unhandled rejection.
  • Consider extracting "should load user" into a computed predicate:
    const shouldLoadUser = computed(
      () => !unavailable.value && !!auth.token && !user.chatUserProfile,
    )
    watch(shouldLoadUser, (v) => { if (v) void user.loadCurrentUser() }, { immediate: true })
  • Verify unavailablePath uses fullPath semantics consistently (query/hash included); route.path might be safer if query params shouldn't affect availability.
  • The :key="auth.token" remount is fine; just confirm remounting on every token refresh is intended (could be expensive).

Summary: Logic is sound; main risks are unhandled promise rejection and missing reload when only the route path leaves the unavailable state without token/profile changes.

Expand Down
Loading