mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 20:08:18 +08:00
fix(ui): resolve state confusion and cross-user data leak when switching menus/workspaces
This commit is contained in:
parent
6252dfb81a
commit
d950d54b00
@ -47,6 +47,7 @@ COPY pom.xml ./pom.xml
|
|||||||
COPY mateclaw-plugin-api/pom.xml mateclaw-plugin-api/pom.xml
|
COPY mateclaw-plugin-api/pom.xml mateclaw-plugin-api/pom.xml
|
||||||
COPY mateclaw-server/pom.xml mateclaw-server/pom.xml
|
COPY mateclaw-server/pom.xml mateclaw-server/pom.xml
|
||||||
COPY mateclaw-plugin-sample/pom.xml mateclaw-plugin-sample/pom.xml
|
COPY mateclaw-plugin-sample/pom.xml mateclaw-plugin-sample/pom.xml
|
||||||
|
COPY mateclaw-plugin-search-sample/pom.xml mateclaw-plugin-search-sample/pom.xml
|
||||||
|
|
||||||
# Pre-fetch backend dependencies through the reactor so the parent POM,
|
# Pre-fetch backend dependencies through the reactor so the parent POM,
|
||||||
# dependencyManagement, and internal module versions all resolve consistently.
|
# dependencyManagement, and internal module versions all resolve consistently.
|
||||||
|
|||||||
@ -204,12 +204,18 @@ const selectedAgent = computed<PickableAgent | null>(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
/** modelValue is set but resolves to no known agent — the referenced
|
/** modelValue is set but resolves to no known agent — the referenced
|
||||||
* employee was likely renamed or removed. */
|
* employee was likely renamed or removed.
|
||||||
const isUnknown = computed(() => hasValue.value && !selectedAgent.value)
|
* Requires agents to have loaded first: while the list is still empty
|
||||||
|
* (component just rebuilt, /agents in flight) we must NOT treat a missing
|
||||||
|
* match as "unknown" — otherwise the trigger flashes the raw numeric id
|
||||||
|
* until the list lands. Falling through to the placeholder during that
|
||||||
|
* window keeps the trigger readable and self-heals once agents arrive. */
|
||||||
|
const isUnknown = computed(() =>
|
||||||
|
hasValue.value && !selectedAgent.value && props.agents.length > 0)
|
||||||
|
|
||||||
const triggerLabel = computed(() => {
|
const triggerLabel = computed(() => {
|
||||||
if (selectedAgent.value) return selectedAgent.value.name
|
if (selectedAgent.value) return selectedAgent.value.name
|
||||||
if (isUnknown.value) return props.unknownLabel || String(props.modelValue)
|
if (isUnknown.value) return props.unknownLabel || t('agentContext.unknownAgent')
|
||||||
return props.placeholder || t('agentContext.selectAgent')
|
return props.placeholder || t('agentContext.selectAgent')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,125 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import type { PickableAgent } from '../AgentPickerDialog.vue'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取 AgentPickerDialog 中 isUnknown / triggerLabel 的核心判定逻辑做纯函数测试。
|
||||||
|
* 这些 computed 的判定决定了触发器在「列表未加载」「员工被删除」等边界下显示什么,
|
||||||
|
* 历史上因为缺少 agents.length > 0 的守卫,组件重建瞬间会闪现原始数字 ID(Bug 1)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
const AGENTS: PickableAgent[] = [
|
||||||
|
{ id: 1000000001, name: '通用助手' },
|
||||||
|
{ id: 1000000002, name: '代码助手' },
|
||||||
|
]
|
||||||
|
|
||||||
|
// hasValue:复刻组件中的判定
|
||||||
|
function hasValue(modelValue: string | number | null | undefined): boolean {
|
||||||
|
return modelValue !== '' && modelValue !== null && modelValue !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectedAgent:复刻组件中的查找
|
||||||
|
function findSelected(
|
||||||
|
modelValue: string | number | null | undefined,
|
||||||
|
agents: PickableAgent[],
|
||||||
|
): PickableAgent | null {
|
||||||
|
if (!hasValue(modelValue)) return null
|
||||||
|
return agents.find(a => String(a.id) === String(modelValue)) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
// isUnknown:复刻组件中的 computed —— 关键修复点
|
||||||
|
// hasValue && !selectedAgent && agents.length > 0
|
||||||
|
function computeIsUnknown(
|
||||||
|
modelValue: string | number | null | undefined,
|
||||||
|
agents: PickableAgent[],
|
||||||
|
): boolean {
|
||||||
|
const selected = findSelected(modelValue, agents)
|
||||||
|
return hasValue(modelValue) && !selected && agents.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerLabel:复刻组件中的优先级链
|
||||||
|
function computeTriggerLabel(
|
||||||
|
modelValue: string | number | null | undefined,
|
||||||
|
agents: PickableAgent[],
|
||||||
|
placeholder: string,
|
||||||
|
unknownLabel: string,
|
||||||
|
): string {
|
||||||
|
const selected = findSelected(modelValue, agents)
|
||||||
|
if (selected) return selected.name
|
||||||
|
if (computeIsUnknown(modelValue, agents)) return unknownLabel
|
||||||
|
return placeholder
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AgentPickerDialog isUnknown 判定', () => {
|
||||||
|
describe('空值场景', () => {
|
||||||
|
it('modelValue 为 null 时不判定为未知', () => {
|
||||||
|
expect(computeIsUnknown(null, AGENTS)).toBe(false)
|
||||||
|
})
|
||||||
|
it('modelValue 为空字符串时不判定为未知', () => {
|
||||||
|
expect(computeIsUnknown('', AGENTS)).toBe(false)
|
||||||
|
})
|
||||||
|
it('modelValue 为 undefined 时不判定为未知', () => {
|
||||||
|
expect(computeIsUnknown(undefined, AGENTS)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('正常匹配场景', () => {
|
||||||
|
it('数字 id 匹配到员工时不判定为未知', () => {
|
||||||
|
expect(computeIsUnknown(1000000001, AGENTS)).toBe(false)
|
||||||
|
})
|
||||||
|
it('字符串 id 匹配到员工时不判定为未知(Snowflake 精度场景)', () => {
|
||||||
|
expect(computeIsUnknown('1000000001', AGENTS)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('列表为空场景(Bug 1 核心修复点)', () => {
|
||||||
|
// 组件被 keepAlive 重建后 /agents 尚在飞行中,此时 agents=[]
|
||||||
|
// 旧逻辑:hasValue && !selectedAgent → true → 显示原始 ID
|
||||||
|
// 新逻辑:追加 agents.length > 0 → false → 走 placeholder
|
||||||
|
it('modelValue 有值但员工列表为空时不判定为未知', () => {
|
||||||
|
expect(computeIsUnknown(1000000001, [])).toBe(false)
|
||||||
|
expect(computeIsUnknown('1000000001', [])).toBe(false)
|
||||||
|
})
|
||||||
|
it('modelValue 为无效值且列表为空时也不判定为未知', () => {
|
||||||
|
expect(computeIsUnknown(9999999999, [])).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('员工被删除/改名场景', () => {
|
||||||
|
it('modelValue 有值、列表非空但无匹配时判定为未知', () => {
|
||||||
|
expect(computeIsUnknown(9999999999, AGENTS)).toBe(true)
|
||||||
|
expect(computeIsUnknown('9999999999', AGENTS)).toBe(true)
|
||||||
|
})
|
||||||
|
it('被删除的员工在恢复列表后仍显示未知标记,直到用户重新选择', () => {
|
||||||
|
// 模拟:员工 1000000001 被从列表移除
|
||||||
|
const reduced = AGENTS.filter(a => a.id !== 1000000001)
|
||||||
|
expect(computeIsUnknown(1000000001, reduced)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('AgentPickerDialog triggerLabel 优先级链', () => {
|
||||||
|
const PLACEHOLDER = '请选择员工'
|
||||||
|
const UNKNOWN_LABEL = '未知员工'
|
||||||
|
|
||||||
|
it('选中员工时显示员工名称(最高优先级)', () => {
|
||||||
|
expect(computeTriggerLabel(1000000001, AGENTS, PLACEHOLDER, UNKNOWN_LABEL))
|
||||||
|
.toBe('通用助手')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('列表为空 + modelValue 有值时显示 placeholder(Bug 1 修复核心)', () => {
|
||||||
|
// 旧逻辑会显示 "1000000001",新逻辑走 placeholder
|
||||||
|
expect(computeTriggerLabel(1000000001, [], PLACEHOLDER, UNKNOWN_LABEL))
|
||||||
|
.toBe(PLACEHOLDER)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('列表非空但无匹配时显示 unknownLabel', () => {
|
||||||
|
expect(computeTriggerLabel(9999999999, AGENTS, PLACEHOLDER, UNKNOWN_LABEL))
|
||||||
|
.toBe(UNKNOWN_LABEL)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('未选择时显示 placeholder', () => {
|
||||||
|
expect(computeTriggerLabel(null, AGENTS, PLACEHOLDER, UNKNOWN_LABEL))
|
||||||
|
.toBe(PLACEHOLDER)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -1218,6 +1218,7 @@ export default {
|
|||||||
title: 'Agent Context',
|
title: 'Agent Context',
|
||||||
desc: 'Manage prompt files and memory for agents',
|
desc: 'Manage prompt files and memory for agents',
|
||||||
selectAgent: 'Select Employee',
|
selectAgent: 'Select Employee',
|
||||||
|
unknownAgent: 'Unknown employee',
|
||||||
noAgent: 'Please select an agent first',
|
noAgent: 'Please select an agent first',
|
||||||
files: 'Files',
|
files: 'Files',
|
||||||
coreFiles: 'Core Files',
|
coreFiles: 'Core Files',
|
||||||
|
|||||||
@ -1092,6 +1092,7 @@ export default {
|
|||||||
title: '智能体上下文',
|
title: '智能体上下文',
|
||||||
desc: '管理智能体的提示文件和记忆',
|
desc: '管理智能体的提示文件和记忆',
|
||||||
selectAgent: '选择员工',
|
selectAgent: '选择员工',
|
||||||
|
unknownAgent: '未知员工',
|
||||||
noAgent: '请先选择一个智能体',
|
noAgent: '请先选择一个智能体',
|
||||||
files: '文件列表',
|
files: '文件列表',
|
||||||
coreFiles: '核心文件',
|
coreFiles: '核心文件',
|
||||||
|
|||||||
@ -25,7 +25,7 @@ const router = createRouter({
|
|||||||
path: 'chat',
|
path: 'chat',
|
||||||
name: 'Chat',
|
name: 'Chat',
|
||||||
component: () => import('@/views/ChatConsole.vue'),
|
component: () => import('@/views/ChatConsole.vue'),
|
||||||
meta: { title: 'Chat', requiredCapability: 'chat' },
|
meta: { title: 'Chat', requiredCapability: 'chat', keepAlive: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'dashboard',
|
path: 'dashboard',
|
||||||
|
|||||||
@ -0,0 +1,112 @@
|
|||||||
|
// @vitest-environment happy-dom
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
|
||||||
|
// vi.mock 在顶层会被提升,工厂内部不能引用外部变量,所以直接返回固定
|
||||||
|
// 的 mock 实现,具体的返回数据在测试用例中通过 mockResolvedValue 设置。
|
||||||
|
vi.mock('@/api/index', () => ({
|
||||||
|
wikiApi: {
|
||||||
|
listKBs: vi.fn(),
|
||||||
|
listRaw: vi.fn().mockResolvedValue({ data: [] }),
|
||||||
|
listPages: vi.fn().mockResolvedValue({ data: [] }),
|
||||||
|
listPageRefs: vi.fn().mockResolvedValue({ data: { items: [] } }),
|
||||||
|
getBrokenLinksReport: vi.fn().mockRejectedValue({ code: 404 }),
|
||||||
|
getPageTypeProfile: vi.fn().mockRejectedValue(new Error('no profile')),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 必须在 mock 之后导入,否则 store 初始化时拿到的是真实 wikiApi
|
||||||
|
import { useWikiStore } from '../useWikiStore'
|
||||||
|
import { wikiApi } from '@/api/index'
|
||||||
|
|
||||||
|
const KB_WS_A = { id: 100, name: 'WS-A-Wiki' }
|
||||||
|
const KB_WS_B = { id: 200, name: 'WS-B-Wiki' }
|
||||||
|
|
||||||
|
describe('useWikiStore 工作区切换 KB 清理', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('切换工作区后 currentKB 不在新列表中时清理旧 KB 上下文', async () => {
|
||||||
|
const store = useWikiStore()
|
||||||
|
// 模拟工作区 A:已选中 KB 并加载了 pages / pageRefs
|
||||||
|
store.currentKB = KB_WS_A as any
|
||||||
|
store.pages = [{ id: 1, title: 'page-A' }] as any
|
||||||
|
store.pageRefs = [{ slug: 'page-a', title: 'Page A', archived: false }] as any
|
||||||
|
|
||||||
|
// 切到工作区 B:listKBs 返回的列表不含 id=100
|
||||||
|
;(wikiApi.listKBs as any).mockResolvedValue({ data: [KB_WS_B] })
|
||||||
|
|
||||||
|
await store.fetchKnowledgeBases()
|
||||||
|
|
||||||
|
// backToLibrary 应被触发:currentKB / pages / pageRefs 全部归零
|
||||||
|
expect(store.currentKB).toBeNull()
|
||||||
|
expect(store.pages).toEqual([])
|
||||||
|
expect(store.pageRefs).toEqual([])
|
||||||
|
// 新列表已写入
|
||||||
|
expect(store.knowledgeBases).toEqual([KB_WS_B])
|
||||||
|
expect(store.loading).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('同一工作区刷新 KB 列表时保留当前 currentKB', async () => {
|
||||||
|
const store = useWikiStore()
|
||||||
|
store.currentKB = KB_WS_A as any
|
||||||
|
store.pages = [{ id: 1, title: 'page-A' }] as any
|
||||||
|
|
||||||
|
// 同一工作区:listKBs 返回的列表包含 id=100
|
||||||
|
;(wikiApi.listKBs as any).mockResolvedValue({
|
||||||
|
data: [KB_WS_A, { id: 101, name: 'WS-A-Wiki-2' }],
|
||||||
|
})
|
||||||
|
|
||||||
|
await store.fetchKnowledgeBases()
|
||||||
|
|
||||||
|
// currentKB 保留,pages 保留(不误清理)
|
||||||
|
expect(store.currentKB).not.toBeNull()
|
||||||
|
expect(store.currentKB?.id).toBe(100)
|
||||||
|
expect(store.pages).toEqual([{ id: 1, title: 'page-A' }])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('首次进入(无 currentKB)时不触发 backToLibrary', async () => {
|
||||||
|
const store = useWikiStore()
|
||||||
|
// currentKB 初始为 null
|
||||||
|
expect(store.currentKB).toBeNull()
|
||||||
|
|
||||||
|
;(wikiApi.listKBs as any).mockResolvedValue({ data: [KB_WS_A, KB_WS_B] })
|
||||||
|
|
||||||
|
await store.fetchKnowledgeBases()
|
||||||
|
|
||||||
|
expect(store.knowledgeBases).toHaveLength(2)
|
||||||
|
expect(store.loading).toBe(false)
|
||||||
|
// 无异常即可,backToLibrary 对 null currentKB 本身也是安全空操作
|
||||||
|
})
|
||||||
|
|
||||||
|
it('listKBs 接口异常时不崩溃且 loading 复位', async () => {
|
||||||
|
const store = useWikiStore()
|
||||||
|
store.currentKB = KB_WS_A as any
|
||||||
|
|
||||||
|
;(wikiApi.listKBs as any).mockRejectedValue(new Error('network down'))
|
||||||
|
|
||||||
|
await store.fetchKnowledgeBases()
|
||||||
|
|
||||||
|
// 异常路径:catch 吞错,knowledgeBases 不变,loading 复位
|
||||||
|
expect(store.loading).toBe(false)
|
||||||
|
expect(store.currentKB?.id).toBe(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selectKB 后切换工作区触发 fetchKnowledgeBases 能正确清理', async () => {
|
||||||
|
// 验证 selectKB → fetchKnowledgeBases 的组合路径
|
||||||
|
const store = useWikiStore()
|
||||||
|
;(wikiApi.listKBs as any).mockResolvedValue({ data: [KB_WS_B] })
|
||||||
|
|
||||||
|
// 先在工作区 A 选了 KB
|
||||||
|
store.currentKB = KB_WS_A as any
|
||||||
|
store.pages = [{ id: 1, title: 'page-A' }] as any
|
||||||
|
|
||||||
|
await store.fetchKnowledgeBases()
|
||||||
|
|
||||||
|
// 旧 KB 上下文被清理
|
||||||
|
expect(store.currentKB).toBeNull()
|
||||||
|
expect(store.pages).toEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -177,7 +177,13 @@ export const useWikiStore = defineStore('wiki', () => {
|
|||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await wikiApi.listKBs()
|
const res: any = await wikiApi.listKBs()
|
||||||
knowledgeBases.value = res.data || []
|
const next: WikiKB[] = res.data || []
|
||||||
|
// 工作区切换时清理上一个工作区的 KB 上下文,避免显示其内容。
|
||||||
|
// backToLibrary 已清理 pages/pageRefs/rawMaterials 等关联状态,这里复用。
|
||||||
|
if (currentKB.value && !next.some(kb => kb.id === currentKB.value!.id)) {
|
||||||
|
backToLibrary()
|
||||||
|
}
|
||||||
|
knowledgeBases.value = next
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch knowledge bases', e)
|
console.error('Failed to fetch knowledge bases', e)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@ -263,8 +263,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
let cachedAgents: import('@/types').Agent[] = []
|
||||||
|
</script>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, onActivated, onDeactivated, watch, nextTick } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { mcToast } from '@/composables/useMcToast'
|
import { mcToast } from '@/composables/useMcToast'
|
||||||
@ -359,7 +363,7 @@ const router = useRouter()
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const agents = ref<Agent[]>([])
|
const agents = ref<Agent[]>(cachedAgents.length > 0 ? [...cachedAgents] : [])
|
||||||
const conversations = ref<Conversation[]>([])
|
const conversations = ref<Conversation[]>([])
|
||||||
const selectedAgentId = ref<string | number>('')
|
const selectedAgentId = ref<string | number>('')
|
||||||
const currentConversationId = ref<string>('')
|
const currentConversationId = ref<string>('')
|
||||||
@ -1162,6 +1166,45 @@ onBeforeUnmount(() => {
|
|||||||
revokeAllPreviewUrls()
|
revokeAllPreviewUrls()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onDeactivated(() => {
|
||||||
|
if (activityPollTimer !== null) {
|
||||||
|
clearInterval(activityPollTimer)
|
||||||
|
activityPollTimer = null
|
||||||
|
}
|
||||||
|
if (elapsedTickTimer !== null) {
|
||||||
|
clearInterval(elapsedTickTimer)
|
||||||
|
elapsedTickTimer = null
|
||||||
|
}
|
||||||
|
document.removeEventListener('click', handleCodeCopy)
|
||||||
|
disposeECharts()
|
||||||
|
disposeKatex()
|
||||||
|
disposeMermaid()
|
||||||
|
revokeAllPreviewUrls()
|
||||||
|
resetForNewConversation()
|
||||||
|
})
|
||||||
|
|
||||||
|
onActivated(async () => {
|
||||||
|
document.addEventListener('click', handleCodeCopy)
|
||||||
|
startECharts()
|
||||||
|
startKatex()
|
||||||
|
startMermaid()
|
||||||
|
activityPollTimer = window.setInterval(pollActivity, ACTIVITY_POLL_MS)
|
||||||
|
elapsedTickTimer = window.setInterval(() => {
|
||||||
|
if (activeCronRuns.value.length > 0) elapsedNow.value = Date.now()
|
||||||
|
}, 1000)
|
||||||
|
// 登出已改为 window.location.href(刷新页面),所以切回时不会有跨用户残留
|
||||||
|
if (currentConversationId.value && !isEphemeralConversation(currentConversationId.value)) {
|
||||||
|
try {
|
||||||
|
const statusRes: any = await conversationApi.getStatus(currentConversationId.value)
|
||||||
|
if (currentConversationId.value && statusRes.data?.streamStatus === 'running') {
|
||||||
|
await reconnectStream(currentConversationId.value)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
watch(() => route.query, () => {
|
watch(() => route.query, () => {
|
||||||
// If a fresh action arrives (e.g. user re-fires Ctrl+K via the URL while
|
// If a fresh action arrives (e.g. user re-fires Ctrl+K via the URL while
|
||||||
// the view is already alive), pick it up immediately.
|
// the view is already alive), pick it up immediately.
|
||||||
@ -1331,6 +1374,7 @@ async function loadAgents() {
|
|||||||
// a confusing failure path. The admin Agents view passes no filter.
|
// a confusing failure path. The admin Agents view passes no filter.
|
||||||
const res: any = await agentApi.list({ enabled: true })
|
const res: any = await agentApi.list({ enabled: true })
|
||||||
agents.value = res.data || []
|
agents.value = res.data || []
|
||||||
|
if (agents.value.length > 0) cachedAgents = [...agents.value]
|
||||||
// 只有在 URL 没有指定 agentId 且当前无选中时,才默认选第一个
|
// 只有在 URL 没有指定 agentId 且当前无选中时,才默认选第一个
|
||||||
if (agents.value.length > 0 && !selectedAgentId.value && !route.query.agentId) {
|
if (agents.value.length > 0 && !selectedAgentId.value && !route.query.agentId) {
|
||||||
selectedAgentId.value = agents.value[0].id
|
selectedAgentId.value = agents.value[0].id
|
||||||
|
|||||||
@ -578,7 +578,9 @@ function logout() {
|
|||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
localStorage.removeItem('username')
|
localStorage.removeItem('username')
|
||||||
localStorage.removeItem('role')
|
localStorage.removeItem('role')
|
||||||
router.push('/login')
|
// 刷新页面而非 router.push:确保 keepAlive 缓存的 ChatConsole、
|
||||||
|
// 模块级变量(cachedAgents 等)全部清空,杜绝跨用户数据泄漏。
|
||||||
|
window.location.href = '/login'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeLocale(locale: AppLocale) {
|
async function changeLocale(locale: AppLocale) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user