Repository navigation
fix: chat share #7789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: chat share #7789
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,22 +8,18 @@ import { usePortalConversationListStore } from './index' | |
| import RenameConversation from '../../components/rename/index.vue' | ||
|
|
||
| import { useMessageListStore } from '../../components/message-list/index' | ||
| import { useShareLinkStoreOptional } from '../../components/message-list/share/index' | ||
|
|
||
| defineOptions({ name: 'HistoryConversationDrawer' }) | ||
| const props = defineProps<{ applicationId: string }>() | ||
| const visible = defineModel<boolean>({ required: true }) | ||
| const portalList = usePortalConversationListStore() | ||
| const conversationList = useConversationListStore() | ||
| const msgs = useMessageListStore() | ||
| const canShare = computed(() => msgs.messages.value.length > 0 && !msgs.streamLoading.value) | ||
| const shareLink = useShareLinkStoreOptional() | ||
| const canShare = computed(() => msgs.messages.value.length > 0 && !msgs.streamLoading.value && !msgs.loading.value) | ||
|
|
||
| // 开启当前对话的分享选择(不预先勾选),并关闭历史抽屉。 | ||
| const handleShareConversation = () => { | ||
| if (!canShare.value) return | ||
| shareLink?.enterShare() | ||
| visible.value = false | ||
| // 激活所选会话并全选分享,成功后关闭历史抽屉。 | ||
| const handleShareConversation = async (conversation: PortalConversationSummary) => { | ||
| if (await portalList.shareConversation(selectedApplicationId.value, conversation.id)) visible.value = false | ||
| } | ||
| const selectedApplicationId = ref(props.applicationId) | ||
| const keyword = ref('') | ||
|
|
@@ -102,8 +98,11 @@ const handleDeleteConversation = (conversation: PortalConversationSummary) => { | |
| <el-button text><MkIcon name="icon_more_outlined" /></el-button> | ||
| <template #dropdown> | ||
| <MkDropdownMenu> | ||
| <!-- 分享当前激活对话 --> | ||
| <MkDropdownItem v-if="isActiveConversation(conversation)" :disabled="!canShare" @click="handleShareConversation"> | ||
| <!-- 激活所选对话并全选分享 --> | ||
| <MkDropdownItem | ||
| :disabled="msgs.loading.value || (isActiveConversation(conversation) && !canShare)" | ||
| @click="handleShareConversation(conversation)" | ||
| > | ||
| <template #icon><MkIcon name="icon_share_outlined" /></template> | ||
| <span>分享</span> | ||
| </MkDropdownItem> | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here is a review of the provided code diff, highlighting irregularities, potential issues, and optimization suggestions. 1. Logical & Functional IssuesA. Race Condition & State Management in
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| import { computed, watch } from 'vue' | ||
| import { computed, nextTick, watch } from 'vue' | ||
| import { useRoute, useRouter } from 'vue-router' | ||
| import ConversationApi from '@/api/chat/conversation' | ||
| import PortalApi from '@/api/chat/portal' | ||
|
|
@@ -62,6 +62,36 @@ export function createPortalConversation(options: PortalConversationOptions): Po | |
| currentApplicationId, | ||
| isApplicationListActive, | ||
| openApplication, | ||
| // 跨智能体通过导航激活,等待路由驱动的消息加载结束后再全选分享。 | ||
| shareConversation: async (applicationId, chatId) => { | ||
| if (bundle.msgs.loading.value) return false | ||
| if (applicationId !== currentApplicationId.value) { | ||
| const failure = await router.push({ name: 'portal-application-chat', params: { applicationId, chatId }, query: route.query }) | ||
| if (failure) return false | ||
| await nextTick() | ||
| if (bundle.msgs.loading.value) { | ||
| await new Promise<void>((resolve) => { | ||
| const stop = watch(bundle.msgs.loading, (loading) => { | ||
| if (!loading) { | ||
| stop() | ||
| resolve() | ||
| } | ||
| }) | ||
| }) | ||
| } | ||
| } else if (bundle.list.currentChatId.value !== chatId) { | ||
| await bundle.list.selectConversation(chatId) | ||
| } | ||
| if ( | ||
| currentApplicationId.value !== applicationId || | ||
| bundle.list.currentChatId.value !== chatId || | ||
| !bundle.msgs.messages.value.length || | ||
| bundle.msgs.streamLoading.value | ||
| ) | ||
| return false | ||
| bundle.share.enterShare() | ||
| return true | ||
| }, | ||
| openConversation: (applicationId, chatId) => | ||
| void router.push({ name: 'portal-application-chat', params: { applicationId, chatId }, query: route.query }), | ||
| openApplicationList: () => void router.push({ name: 'portal-application-list', query: route.query }), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Review1. Correctness / likely bugsa) Strict type comparison on route params (high) b) c) The d) Behavior asymmetry vs. e) The initial 2. Race conditions / robustnessf) The busy-wait on
g) h) No concurrency guard i) j) 3. Style / typing / minor
4. Suggested rewriteimport { computed, nextTick, watch } from 'vue'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
import { until } from '@vueuse/core' // optional helper
const SHARE_LOAD_TIMEOUT = 10_000
shareConversation: async (applicationId: string, chatId: string): Promise<boolean> => {
// Guard against concurrent invocations
if (bundle.share.enterShare.pending) return false
try {
if (String(applicationId) !== String(currentApplicationId.value)) {
const failure = await router.push({
name: 'portal-application-chat',
params: { applicationId, chatId },
query: route.query,
})
if (failure && !isNavigationFailure(failure, NavigationFailureType.duplicated)) {
return false
}
await nextTick()
// Prefer: await bundle.msgs.loaded(chatId)
if (bundle.msgs.loading.value) {
const settled = await until(() => !bundle.msgs.loading.value)
.toPromise()
.then(() => true)
.catch(() => false)
// add a timeout wrapper around this in practice
if (!settled) return false
}
} else if (bundle.list.currentChatId.value !== chatId) {
await bundle.list.selectConversation(chatId)
}
if (
String(currentApplicationId.value) !== String(applicationId) ||
bundle.list.currentChatId.value !== chatId ||
bundle.msgs.loading.value ||
bundle.msgs.streamLoading.value ||
bundle.msgs.messages.value.length === 0
) {
return false
}
bundle.share.enterShare()
return true
} catch {
return false
}
}Priority order: (a) type/string comparison, (f) watcher leak/hang + timeout, (b) duplicated-navigation handling, (c) unawaited |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a review of the provided code patch, focusing on potential issues, logic flaws, and optimization suggestions.
1. Potential Logic Issues & Race Conditions
Issue: Asynchronous Race Condition in
handleShareConversationThe function
handleShareConversationintroduces anawaitinside an event handler (implied by@click). If the user rapidly clicks the "Share" menu item for a conversation while it is loading or switching, multiple asynchronous operations might queue up. While the initial checkif (msgs.loading.value) returnhelps, there is no guard against concurrent calls toselectConversation.if (currentChatId.value !== conversation.id)check before the firstselectConversationcompletes, leading to redundant API calls or state inconsistencies.selectConversationitself handles concurrent requests idempotently. Alternatively, usenextTickor explicit waiting if the store method returns a promise that resolves only after full state sync.Refactored Suggestion:
Issue: Stale State Check After Async Operation
After
await selectConversation(conversation.id), the code checkscanShare.value. However,canSharedepends onmsgs.messageList.value.lengthandmsgs.loading.value. Depending on howuseMessageListStoreanduseViewTypereactively update, there might be a brief moment wherecurrentChatIdhas updated but the message list hasn't finished populating or updating theloadingflag immediately. Ensure thatcanShareaccurately reflects the newly loaded state immediately upon resolution ofselectConversation.2. Code Quality & Style
Positive Change: Formatting Fixes
type ViewType}→, type ViewType }) and spacing improvements are good hygiene practices.Comment Ambiguity
// 非当前会话先激活并加载消息,避免分享到其他会话。(Activate non-current sessions first and load messages to avoid sharing others.) is slightly misleading. The new logic allows sharing from non-current sessions by activating them first. A better phrasing might be: "Activate the target session if it's not current, ensuring its messages are loaded before entering share mode."3. UI/UX & Disable Logic
Complexity in
:disabledPropThe template logic for disabling the button is somewhat convoluted:
This means:
msgs.loading), disable it.canShareis false (e.g., empty messages/streaming), disable it.However, if you click a non-current conversation:
currentChatId !== conversation.id.msgs.loading.valueis checked. If false, the button is enabled.msgs.loadingrefers to the current view's messages, not the target view? Ifmsgs.loadingis a global store reference indicating "main view is loading", then clicking a background conversation share button might be allowed even if the main view is busy, potentially causing conflicts when switching contexts. Ensuremsgs.loadingis context-aware or intended to be global.Optimization: Consider extracting this logic into a computed property for readability.
4. Missing Definitions / Dependencies
Undefined Types and Functions
The snippet uses
Conversation,relativeTimeFormat,MkIcon,MkDropdownMenu,MkDropdownItem,el-button, andcomputed. These must be properly imported or defined elsewhere. Specifically:Conversation: Ensure it is imported.relativeTimeFormat: Ensure it is imported.useConversationListStore: Ensure it is imported.Missing Error Handling
selectConversationandshareLink?.enterShare()do not have.catch()blocks. IfselectConversationfails (network error, permission denied), the UI will remain disabled or stuck, and no feedback is given to the user.Suggestion:
Summary of Recommendations
handleShareConversation.msgs.loadingcorrectly prevents interaction with background conversations if intended. Extract complex disable logic to a helper/computed for readability if used in multiple places.