Repository navigation
fix: chat bug - #7841
fix: chat bug#7841
Conversation
| }), | ||
| ]).finally(() => { | ||
| loading.value = false | ||
| }) |
There was a problem hiding this comment.
Review of the diff
Main issue: removed permission gating (behavior regression)
The diff removes the four canRead* computed guards and always calls every aggregation API. Before, in system scope, requests were only issued for resources the user had *.system.read() permission for; unauthorized resources were short-circuited with zero-value objects.
Consequences to consider:
- Unnecessary/unauthorized requests — users without system-level read permission now trigger 4 API calls that will likely return 401/403. They'll land in
.catch()anyway, so UI result isundefined, but you've traded a cheap client-side guard for server round-trips + error logging noise. - Lost "zero state" rendering — previously authorized-but-denied resources got
ZERO_*objects so the card could render 0s. Now they becomeundefined. Verify the template handlesundefinedfor all four refs gracefully (if it dereferences.total, it may crash). - If this removal was intentional (e.g., backend now scopes data per workspace and returns zeros itself), document that rationale in the commit. Otherwise, restoring the permission checks is safer.
Other observations
ZERO_*constants deleted — fine if no longer referenced, but confirm nothing else imports them. Also, they were shallow-copied ({ ...ZERO_X }); nested mutable fields (none here) would have been shared anyway.- Empty catch handlers swallow errors silently. Keep at least a
console.warnor set a per-resource error flag, otherwise debugging failures becomes hard — especially now that auth failures are expected. - No handling of stale results — if
loadResources()is called again while a prior call is in flight, theloadingguard prevents overlap, good. Butfinally(() => loading.value = false)runs even for the first settled branch? No —Promise.allSettled(...).finallyfires after all settle, so this is correct. - Repetition — four near-identical blocks could be DRY'd:
const tasks = [ [application, props.api.getApplicationAggregation], [knowledge, props.api.getKnowledgeAggregation], [tool, props.api.getToolAggregation], [model, props.api.getModelAggregation], ] as const ... return Promise.allSettled( tasks.map(([ref, fn]) => fn(props.workspaceId).then(v => { ref.value = v }, () => { ref.value = undefined }) ) ).finally(() => { loading.value = false })
- Race on prop change —
props.workspaceIdchanges don't auto-reload; presumably handled by a watcher elsewhere. Worth double-checking there's awatch(() => props.workspaceId, loadResources). isSystemScopenow appears unused — if nothing else references it, remove to avoid a lint warning.permimport may now be unused too — verify and clean up.
Recommendation
Unless the backend reliably returns zeroed aggregations for unauthorized resources, restore the canRead* guards. If it does, keep the simplification but add a brief comment explaining the backend guarantee and ensure the template renders undefined safely.
| > | ||
| <template #default> | ||
| <span>查看更多</span> | ||
| <span> |
There was a problem hiding this comment.
Code Review
Did the diff apply cleanly?
The provided diff is a delta, not standalone code, so I reviewed the changes in context. The changes are logically consistent, but there are a few issues worth flagging.
Issues & Observations
1. portalName meta handling is over-defensive but inconsistent
const meta = authStore.authProfile?.meta
const name = meta && typeof meta === 'object' && 'portal_name' in meta ? meta.portal_name : undefined- Improvement over the removed version (which did
meta.portal_nameon possiblyundefined— that was a latent runtime crash). Good fix. - However, consider typing
authProfile.metaproperly (e.g.{ portal_name?: string; [k: string]: unknown }) so you can drop the runtime guards. Thein+typeofchain is verbose here. - Suggest
(meta as Record<string, unknown> | undefined)?.portal_name.
2. groupConversations changed semantics — verify with consumers
Previously it always sliced to 5. Now it returns all conversations when total ≤ 20. It's used in:
historyApplicationGroupsfilter (.length > 0) — fine.- Template rendering of conversation items — now renders potentially more than 5 items per group. Ensure the UI/layout handles this (scroll, height). This looks intentional (matches "无需查看更多时展示全部已加载会话"), but confirm layout tests.
3. totalConversationCount > 20 threshold is a magic number
Extract constants for clarity:
const TOTAL_CONVERSATION_THRESHOLD = 20And use in both the computed and template (v-if). Right now 20 is duplicated in script and template.
4. groupConversationCount may double-count the current application
Math.max(group.count, isCurrentApplication(group) ? conversations.value.length : group.conversations.length)If applicationGroups includes a group whose count already reflects server-side totals, the sum in totalConversationCount could mismatch actual rendered counts. Confirm group.count semantics (server total vs. loaded). If the server count is larger than loaded, users may see "查看更多" even though only some are loaded — acceptable, but document intent.
5. Redundant import path change
@/stores/chat/index.ts → @/stores/chat is cleaner. ✅ But ensure no other file relies on the explicit .ts path consistency style; align project-wide.
6. RECENT_CONVERSATION_LIMIT comment now stale elsewhere?
The inline comment near line ~230 was updated ("总数超过 20 条…"). Good. But check that any docs/tests referencing "每个智能体最多展示最近 5 条会话" are updated to reflect the new conditional behavior.
7. Minor: template binding order
class="pl-8!" before v-if — put v-if first for readability (convention).
<MkListItem v-if="..." class="pl-8!" @click="...">8. portalLogo type
authStore.authProfile?.logo — if logo can be undefined, <PortalIcon :icon="portalLogo" /> must tolerate undefined. Verify PortalIcon prop allows optional icon (else add default).
Suggested refined snippet
const TOTAL_CONVERSATION_THRESHOLD = 20
const { auth: authStore } = useStore()
const portalName = computed(() => {
const name = (authStore.authProfile?.meta as Record<string, unknown> | undefined)?.portal_name
return typeof name === 'string' && name ? name : '智能体门户'
})
const portalLogo = computed(() => authStore.authProfile?.logo)
const groupConversations = (group: PortalApplicationGroup) => {
const list = isCurrentApplication(group) ? conversations.value : group.conversations
return totalConversationCount.value > TOTAL_CONVERSATION_THRESHOLD
? list.slice(0, RECENT_CONVERSATION_LIMIT)
: list
}Template:
<MkListItem
v-if="totalConversationCount > TOTAL_CONVERSATION_THRESHOLD && groupConversationCount(group) > RECENT_CONVERSATION_LIMIT"
...
>Summary
The refactor is an improvement — it fixes a real null-access bug (meta.portal_name) and moves the name/logo logic into computed properties. Main follow-ups: define named constants, type meta, confirm rendering of >5 items per group, and verify group.count semantics to avoid incorrect "查看更多" visibility.
| /> | ||
| </template> | ||
|
|
||
| <template #default="{ Header }"> |
There was a problem hiding this comment.
Code Review
Potential Issues
-
No error handling in
handleUserSearch
The.then/.finallychain has no.catch. A rejectedgetWorkspaceMemberscall will produce an unhandled promise rejection (though the global handler may catch it). Considertry/catch/awaitor adding a.catch. -
Race condition / stale response overwrites
loadingUsersguards against concurrent searches, but not against out-of-order responses. If the guard is ever bypassed (e.g., debounce removed later), a slow earlier request could overwrite a newer keyword's results. More importantly, rapid typing: since you early-return while loading, keystrokes during an in-flight request are silently dropped — the user's latest keyword may never be searched. Consider debouncing the input instead of dropping requests. -
inertattribute on a Vue component:inert="loadingUsers"—inertis a boolean HTML attribute; whenfalse, Vue rendersinert="false", which still activates inert behavior in browsers (presence-only semantics for older engines; modern browsers treatinert="false"as active because it's a valid attribute value string, not removed). Verify: Vue removes attributes bound tofalseonly for known boolean attributes.inertis standardized as a boolean attribute, so this generally works correctly in Vue 3 — but confirm your Vue version and that it lands on the root element ofUserAuthorizationList.inertprevents focus/interaction but does not announce loading state to screen readers. Pair witharia-busy.
-
v-loading+inertoverlap
Using both may be redundant;v-loadingalready overlays. Ensure the loading mask itself isn't inside the inert subtree (otherwise the spinner blocks interaction but keyboard users get no cue either). -
:key="selectedWorkspaceId"forces full re-render
This destroys and recreatesUserAuthorizationListon every workspace switch, resetting internal state (scroll position, local search input text, etc.). If that's intended, fine — but note that "search keyword" entered by the user will be lost on workspace change, which may surprise users. AlsoworkspaceMembersisn't cleared here, so a stale list flashes before the new load finishes unless the parent reloads it. -
selectedWorkspaceId.valuemay be empty on first search
No guard for missing workspace ID; the API would be called with an empty id. -
Stale
workspaceMemberson workspace switch
Recommend clearingworkspaceMembers.value = []and resettingselectedUserIdwhen workspace changes, to avoid showing users from the previous workspace. -
nick_namecasing inconsistency
Mixed snake_case (nick_name) vs. camelCase elsewhere (selectedUserId,userGroups). Confirm this matches the API contract and is consistent with other calls. -
Inconsistent style
Existing code usesasync/await(seen inloadAuthorizationTargets), but this new function uses.then().finally(). Match project conventions.
Optimization Suggestions
let searchTimer: ReturnType<typeof setTimeout> | undefined
async function handleUserSearch(keyword: string) {
clearTimeout(searchTimer)
searchTimer = setTimeout(async () => {
if (!selectedWorkspaceId.value) return
loadingUsers.value = true
try {
workspaceMembers.value = await CommonSystemApi.getWorkspaceMembers(
selectedWorkspaceId.value,
keyword ? { nick_name: keyword } : undefined,
)
} catch (e) {
// surface error / toast
} finally {
loadingUsers.value = false
}
}, 300)
}Also consider:
- Add
aria-busy="loadingUsers"alongsidev-loading. - Clear
workspaceMembersin awatch(selectedWorkspaceId, ...). - If
UserAuthorizationListsupports it, prefer debounced search over theinert/loadingUsersinteraction guard. - Avoid
:keyif you can reset internal state via a prop or exposed method instead.
Revert "feat: add application read permissions to homepage components" This reverts commit 1430103.
fix: chat bug