fix(agent): reconcile agent stream mode and shared-agent readiness UX
Derive stream pipeline from selectedAgentId to fix quick-answer/isAgentEnabled drift, gate configure actions for shared agents, and polish selector detail panel positioning.
This commit is contained in:
@@ -181,7 +181,38 @@
|
||||
padding: 4px;
|
||||
min-width: 148px;
|
||||
animation: dropdownSlideIn 0.18s cubic-bezier(0.2, 0, 0, 1) both;
|
||||
overflow: hidden;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: transparent transparent;
|
||||
transition: scrollbar-color 0.2s ease;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
scrollbar-color: var(--td-scrollbar-color, rgba(0, 0, 0, 0.18)) transparent;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--td-scrollbar-color, rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--td-scrollbar-hover-color, rgba(0, 0, 0, 0.32));
|
||||
}
|
||||
}
|
||||
|
||||
.t-dropdown__item {
|
||||
@@ -493,5 +524,17 @@
|
||||
0 0 0 0.5px rgba(255, 255, 255, 0.05),
|
||||
0 2px 4px rgba(0, 0, 0, 0.12),
|
||||
0 8px 32px rgba(0, 0, 0, 0.28);
|
||||
|
||||
&:hover {
|
||||
scrollbar-color: rgba(255, 255, 255, 0.22) transparent;
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(255, 255, 255, 0.38);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="agent-selector-overlay" @click="$emit('close')">
|
||||
<div
|
||||
class="agent-selector-dropdown"
|
||||
:style="dropdownStyle"
|
||||
@click.stop
|
||||
>
|
||||
<div class="agent-selector-dropdown" :style="dropdownStyle" @click.stop>
|
||||
<div class="agent-selector-header">
|
||||
<span>{{ $t('agent.selectAgent') }}</span>
|
||||
<router-link to="/platform/agents" class="agent-selector-add" @click="$emit('close')">
|
||||
@@ -18,32 +14,22 @@
|
||||
<!-- 内置智能体 -->
|
||||
<div class="agent-group">
|
||||
<div class="agent-group-title">{{ $t('agent.builtinAgents') }}</div>
|
||||
<div
|
||||
v-for="agent in builtinAgents"
|
||||
:key="agent.id"
|
||||
class="agent-option"
|
||||
:class="{ selected: isMyAgentSelected(agent) }"
|
||||
@mouseenter="onOptionEnter(agent, $event)"
|
||||
@mouseleave="onOptionLeave"
|
||||
@click="selectAgent(agent)"
|
||||
>
|
||||
<div
|
||||
v-if="agent.id === BUILTIN_QUICK_ANSWER_ID || agent.id === BUILTIN_SMART_REASONING_ID"
|
||||
class="builtin-icon"
|
||||
:class="agent.config?.agent_mode === 'smart-reasoning' ? 'agent' : 'normal'"
|
||||
>
|
||||
<TIcon :name="agent.config?.agent_mode === 'smart-reasoning' ? 'control-platform' : 'chat'" size="14px" />
|
||||
<div v-for="agent in builtinAgents" :key="agent.id" class="agent-option"
|
||||
:class="{ selected: isMyAgentSelected(agent) }" @mouseenter="onOptionEnter(agent, $event)"
|
||||
@mouseleave="onOptionLeave" @click="selectAgent(agent)">
|
||||
<div v-if="agent.id === BUILTIN_QUICK_ANSWER_ID || agent.id === BUILTIN_SMART_REASONING_ID"
|
||||
class="builtin-icon" :class="agent.config?.agent_mode === 'smart-reasoning' ? 'agent' : 'normal'">
|
||||
<TIcon :name="agent.config?.agent_mode === 'smart-reasoning' ? 'control-platform' : 'chat'"
|
||||
size="13px" />
|
||||
</div>
|
||||
<div v-else-if="agent.avatar" class="builtin-avatar">{{ agent.avatar }}</div>
|
||||
<div v-else class="builtin-icon normal">
|
||||
<TIcon name="app" size="14px" />
|
||||
<TIcon name="app" size="13px" />
|
||||
</div>
|
||||
<span class="agent-option-name">{{ agent.name }}</span>
|
||||
<div v-if="getAgentNotReadyLabels(agent).length" class="agent-option-actions">
|
||||
<t-tooltip
|
||||
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
|
||||
placement="top"
|
||||
>
|
||||
<t-tooltip :content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
|
||||
placement="top">
|
||||
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
|
||||
</t-tooltip>
|
||||
</div>
|
||||
@@ -53,22 +39,14 @@
|
||||
<!-- 自定义智能体 -->
|
||||
<div v-if="customAgents.length > 0" class="agent-group">
|
||||
<div class="agent-group-title">{{ $t('agent.customAgents') }}</div>
|
||||
<div
|
||||
v-for="agent in customAgents"
|
||||
:key="agent.id"
|
||||
class="agent-option"
|
||||
:class="{ selected: isMyAgentSelected(agent) }"
|
||||
@mouseenter="onOptionEnter(agent, $event)"
|
||||
@mouseleave="onOptionLeave"
|
||||
@click="selectAgent(agent)"
|
||||
>
|
||||
<div v-for="agent in customAgents" :key="agent.id" class="agent-option"
|
||||
:class="{ selected: isMyAgentSelected(agent) }" @mouseenter="onOptionEnter(agent, $event)"
|
||||
@mouseleave="onOptionLeave" @click="selectAgent(agent)">
|
||||
<AgentAvatar :name="agent.name" size="small" />
|
||||
<span class="agent-option-name">{{ agent.name }}</span>
|
||||
<div v-if="getAgentNotReadyLabels(agent).length" class="agent-option-actions">
|
||||
<t-tooltip
|
||||
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
|
||||
placement="top"
|
||||
>
|
||||
<t-tooltip :content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
|
||||
placement="top">
|
||||
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
|
||||
</t-tooltip>
|
||||
</div>
|
||||
@@ -78,36 +56,26 @@
|
||||
<!-- 共享给我 -->
|
||||
<div v-if="sharedAgentsList.length > 0" class="agent-group">
|
||||
<div class="agent-group-title">{{ $t('agent.tabs.sharedToMe') }}</div>
|
||||
<div
|
||||
v-for="shared in sharedAgentsList"
|
||||
:key="`${shared.agent.id}-${shared.source_tenant_id}`"
|
||||
class="agent-option"
|
||||
:class="{ selected: isSharedAgentSelected(shared) }"
|
||||
@mouseenter="onSharedOptionEnter(shared, $event)"
|
||||
@mouseleave="onOptionLeave"
|
||||
@click="selectSharedAgent(shared)"
|
||||
>
|
||||
<div v-for="shared in sharedAgentsList" :key="`${shared.agent.id}-${shared.source_tenant_id}`"
|
||||
class="agent-option" :class="{ selected: isSharedAgentSelected(shared) }"
|
||||
@mouseenter="onSharedOptionEnter(shared, $event)" @mouseleave="onOptionLeave"
|
||||
@click="selectSharedAgent(shared)">
|
||||
<AgentAvatar :name="shared.agent.name" size="small" />
|
||||
<span class="agent-option-name">{{ shared.agent.name }}</span>
|
||||
<span class="shared-tag">{{ $t('agent.selector.sharedLabel') }}</span>
|
||||
<div
|
||||
v-if="getAgentNotReadyLabels(shared.agent, String(shared.source_tenant_id)).length"
|
||||
class="agent-option-actions"
|
||||
>
|
||||
<div v-if="getAgentNotReadyLabels(shared.agent, String(shared.source_tenant_id)).length"
|
||||
class="agent-option-actions">
|
||||
<t-tooltip
|
||||
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(shared.agent, String(shared.source_tenant_id)) })"
|
||||
placement="top"
|
||||
>
|
||||
placement="top">
|
||||
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
|
||||
</t-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="builtinAgents.length === 0 && customAgents.length === 0 && sharedAgentsList.length === 0"
|
||||
class="agent-option empty"
|
||||
>
|
||||
<div v-if="builtinAgents.length === 0 && customAgents.length === 0 && sharedAgentsList.length === 0"
|
||||
class="agent-option empty">
|
||||
{{ $t('agent.noAgents') }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,57 +83,41 @@
|
||||
</div>
|
||||
|
||||
<!-- 详情浮层 -->
|
||||
<div
|
||||
v-if="visible && activeDetail"
|
||||
class="agent-detail-panel"
|
||||
:style="detailPanelStyle"
|
||||
@mouseenter="onDetailPanelEnter"
|
||||
@mouseleave="onDetailPanelLeave"
|
||||
@click.stop
|
||||
>
|
||||
<div v-if="visible && activeDetail" ref="detailPanelRef" class="agent-detail-panel" :style="detailPanelStyle"
|
||||
@mouseenter="onDetailPanelEnter" @mouseleave="onDetailPanelLeave" @click.stop>
|
||||
<div class="agent-detail-panel-inner">
|
||||
<div class="agent-detail-content">
|
||||
<div class="detail-header">
|
||||
<template v-if="activeDetail.agent.id === BUILTIN_QUICK_ANSWER_ID || activeDetail.agent.id === BUILTIN_SMART_REASONING_ID">
|
||||
<div
|
||||
class="builtin-icon detail-icon"
|
||||
:class="activeDetail.agent.config?.agent_mode === 'smart-reasoning' ? 'agent' : 'normal'"
|
||||
>
|
||||
<TIcon
|
||||
:name="activeDetail.agent.config?.agent_mode === 'smart-reasoning' ? 'control-platform' : 'chat'"
|
||||
size="14px"
|
||||
/>
|
||||
<template
|
||||
v-if="activeDetail.agent.id === BUILTIN_QUICK_ANSWER_ID || activeDetail.agent.id === BUILTIN_SMART_REASONING_ID">
|
||||
<div class="builtin-icon detail-icon"
|
||||
:class="activeDetail.agent.config?.agent_mode === 'smart-reasoning' ? 'agent' : 'normal'">
|
||||
<TIcon :name="activeDetail.agent.config?.agent_mode === 'smart-reasoning' ? 'control-platform' : 'chat'"
|
||||
size="14px" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="activeDetail.agent.avatar" class="builtin-avatar detail-icon">{{ activeDetail.agent.avatar }}</div>
|
||||
<div v-else-if="activeDetail.agent.avatar" class="builtin-avatar detail-icon">{{ activeDetail.agent.avatar
|
||||
}}</div>
|
||||
<AgentAvatar v-else :name="activeDetail.agent.name" size="small" />
|
||||
<div class="detail-title-wrap">
|
||||
<div class="detail-title-row">
|
||||
<span class="detail-name">{{ activeDetail.agent.name }}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="detail-header-action"
|
||||
:class="{ 'detail-header-action--warn': activeDetailNotReadyLabels.length }"
|
||||
:title="activeDetailNotReadyLabels.length
|
||||
<button v-if="canShowDetailHeaderAction" type="button" class="detail-header-action"
|
||||
:class="{ 'detail-header-action--warn': activeDetailNotReadyLabels.length }" :title="activeDetailNotReadyLabels.length
|
||||
? $t('agent.selector.configureAction')
|
||||
: $t('agent.selector.goToSettings')"
|
||||
@click="goToSettings(activeDetail.agent, activeDetail.sourceTenantId)"
|
||||
>
|
||||
<TIcon
|
||||
:name="activeDetailNotReadyLabels.length ? 'jump' : 'setting'"
|
||||
size="14px"
|
||||
/>
|
||||
@click="goToSettings(activeDetail.agent, activeDetail.sourceTenantId)">
|
||||
<TIcon :name="activeDetailNotReadyLabels.length ? 'jump' : 'setting'" size="14px" />
|
||||
</button>
|
||||
</div>
|
||||
<span v-if="isDetailCurrent" class="detail-current">{{ $t('agent.selector.current') }}</span>
|
||||
<div v-else-if="activeDetailNotReadyLabels.length" class="detail-not-ready">
|
||||
<TIcon name="error-circle" size="13px" class="detail-not-ready-icon" />
|
||||
<span class="detail-not-ready-label">{{ $t('agent.selector.notReadyStatus') }}</span>
|
||||
<span
|
||||
v-for="item in activeDetailNotReadyLabels"
|
||||
:key="item"
|
||||
class="detail-not-ready-item"
|
||||
>{{ item }}</span>
|
||||
<span v-for="item in activeDetailNotReadyLabels" :key="item" class="detail-not-ready-item">{{ item
|
||||
}}</span>
|
||||
<span v-if="activeDetail.sourceTenantId && activeDetailNotReadyLabels.length"
|
||||
class="detail-not-ready-shared-hint">{{ $t('agent.selector.sharedNotReadyContact') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -174,15 +126,23 @@
|
||||
|
||||
<div class="detail-tags">
|
||||
<span class="detail-tag">
|
||||
{{ activeDetail.agent.config?.agent_mode === 'smart-reasoning' ? $t('agent.type.agent') : $t('agent.type.normal') }}
|
||||
{{ activeDetail.agent.config?.agent_mode === 'smart-reasoning' ? $t('agent.type.agent') :
|
||||
$t('agent.type.normal') }}
|
||||
</span>
|
||||
<span v-if="getKbCapability(activeDetail.agent)" class="detail-tag">{{ getKbCapability(activeDetail.agent) }}</span>
|
||||
<span v-if="activeDetail.agent.config?.web_search_enabled" class="detail-tag">{{ $t('agent.capabilities.webSearchOn') }}</span>
|
||||
<span v-if="getMcpCapability(activeDetail.agent)" class="detail-tag">{{ getMcpCapability(activeDetail.agent) }}</span>
|
||||
<span v-if="activeDetail.agent.config?.multi_turn_enabled" class="detail-tag">{{ $t('agent.capabilities.multiTurn') }}</span>
|
||||
<span v-if="getKbCapability(activeDetail.agent)" class="detail-tag">{{ getKbCapability(activeDetail.agent)
|
||||
}}</span>
|
||||
<span v-if="activeDetail.agent.config?.web_search_enabled" class="detail-tag">{{
|
||||
$t('agent.capabilities.webSearchOn')
|
||||
}}</span>
|
||||
<span v-if="getMcpCapability(activeDetail.agent)" class="detail-tag">{{ getMcpCapability(activeDetail.agent)
|
||||
}}</span>
|
||||
<span v-if="activeDetail.agent.config?.multi_turn_enabled" class="detail-tag">{{
|
||||
$t('agent.capabilities.multiTurn')
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="activeDetail.sharedMeta?.org_name || activeDetail.sharedMeta?.shared_by_username" class="detail-meta">
|
||||
<div v-if="activeDetail.sharedMeta?.org_name || activeDetail.sharedMeta?.shared_by_username"
|
||||
class="detail-meta">
|
||||
<div v-if="activeDetail.sharedMeta.org_name" class="detail-meta-row">
|
||||
<img src="@/assets/img/organization-green.svg" class="detail-meta-icon" alt="" aria-hidden="true" />
|
||||
<span>{{ activeDetail.sharedMeta.org_name }}</span>
|
||||
@@ -214,6 +174,7 @@ import {
|
||||
getAgentNotReadyReasonKeys,
|
||||
resolveAgentNotReadySection,
|
||||
resolveAgentNotReadyHighlight,
|
||||
canLocallyConfigureAgent,
|
||||
type AgentNotReadyReasonKey,
|
||||
} from '@/utils/agent-readiness';
|
||||
import { formatLocalizedList } from '@/utils/format-list';
|
||||
@@ -246,9 +207,14 @@ type AgentDetailTarget = {
|
||||
const dropdownStyle = ref<Record<string, string>>({});
|
||||
const activeDetail = ref<AgentDetailTarget | null>(null);
|
||||
const detailAnchorEl = ref<HTMLElement | null>(null);
|
||||
const detailPanelRef = ref<HTMLElement | null>(null);
|
||||
const detailPanelStyle = ref<Record<string, string>>({});
|
||||
let detailHideTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const DETAIL_PANEL_WIDTH = 200;
|
||||
const DETAIL_BRIDGE_OVERLAP = 10;
|
||||
const DETAIL_HIDE_DELAY_MS = 400;
|
||||
|
||||
const agentsList = computed(() => props.agents ?? []);
|
||||
|
||||
const builtinAgents = computed(() => {
|
||||
@@ -294,6 +260,13 @@ const activeDetailNotReadyLabels = computed(() => {
|
||||
return getAgentNotReadyLabels(detail.agent, detail.sourceTenantId);
|
||||
});
|
||||
|
||||
const canShowDetailHeaderAction = computed(() => {
|
||||
const detail = activeDetail.value;
|
||||
if (!detail) return false;
|
||||
if (canLocallyConfigureAgent(detail.sourceTenantId)) return true;
|
||||
return activeDetailNotReadyLabels.value.length === 0;
|
||||
});
|
||||
|
||||
const getKbCapability = (agent: CustomAgent): string => {
|
||||
const config = agent.config || {};
|
||||
if (config.kb_selection_mode === 'none') return '';
|
||||
@@ -371,34 +344,54 @@ const updateDetailPanelPosition = () => {
|
||||
if (!el || !activeDetail.value) return;
|
||||
|
||||
const zoom = getRootZoom();
|
||||
const rect = rectToCssPx(el.getBoundingClientRect(), zoom);
|
||||
const rowRect = rectToCssPx(el.getBoundingClientRect(), zoom);
|
||||
const { width: vw, height: vh } = cssViewportSize(zoom);
|
||||
const panelWidth = 200;
|
||||
const gap = 6;
|
||||
let left = rect.right + gap;
|
||||
let top = rect.top;
|
||||
|
||||
if (left + panelWidth > vw - 8) {
|
||||
left = Math.max(8, vw - panelWidth - 8);
|
||||
// 向左重叠一段透明区域,避免鼠标从选项移向浮层时经过空隙触发 mouseleave
|
||||
let left = rowRect.right - DETAIL_BRIDGE_OVERLAP;
|
||||
if (left + DETAIL_PANEL_WIDTH > vw - 8) {
|
||||
left = Math.max(8, vw - DETAIL_PANEL_WIDTH - 8);
|
||||
}
|
||||
if (top + 240 > vh - 8) {
|
||||
top = Math.max(8, vh - 248);
|
||||
|
||||
const panelHeight = detailPanelRef.value?.offsetHeight || 180;
|
||||
const rowCenter = rowRect.top + rowRect.height / 2;
|
||||
let top = rowCenter - panelHeight / 2;
|
||||
|
||||
const minTop = 8;
|
||||
const maxTop = vh - panelHeight - 8;
|
||||
if (top < minTop) {
|
||||
top = minTop;
|
||||
} else if (top > maxTop) {
|
||||
// 贴近当前行:优先让浮层与 hover 行在垂直方向仍有交集
|
||||
top = Math.min(rowRect.top, maxTop);
|
||||
top = Math.max(minTop, Math.min(top, rowRect.bottom - panelHeight));
|
||||
if (top + panelHeight < rowRect.top) {
|
||||
top = rowRect.top;
|
||||
}
|
||||
top = Math.max(minTop, Math.min(top, maxTop));
|
||||
}
|
||||
|
||||
detailPanelStyle.value = {
|
||||
position: 'fixed',
|
||||
left: `${Math.round(left)}px`,
|
||||
top: `${Math.round(top)}px`,
|
||||
width: `${panelWidth}px`,
|
||||
width: `${DETAIL_PANEL_WIDTH}px`,
|
||||
zIndex: '10002',
|
||||
};
|
||||
};
|
||||
|
||||
const scheduleDetailPanelPosition = () => {
|
||||
nextTick(() => {
|
||||
updateDetailPanelPosition();
|
||||
requestAnimationFrame(() => updateDetailPanelPosition());
|
||||
});
|
||||
};
|
||||
|
||||
const onOptionEnter = (agent: CustomAgent, event: MouseEvent, sourceTenantId?: string, sharedMeta?: AgentDetailTarget['sharedMeta']) => {
|
||||
clearDetailHideTimer();
|
||||
detailAnchorEl.value = event.currentTarget as HTMLElement;
|
||||
activeDetail.value = { agent, sourceTenantId, sharedMeta };
|
||||
nextTick(() => updateDetailPanelPosition());
|
||||
scheduleDetailPanelPosition();
|
||||
};
|
||||
|
||||
const onSharedOptionEnter = (shared: SharedAgentInfo, event: MouseEvent) => {
|
||||
@@ -412,7 +405,7 @@ const onOptionLeave = () => {
|
||||
detailHideTimer = setTimeout(() => {
|
||||
activeDetail.value = null;
|
||||
detailAnchorEl.value = null;
|
||||
}, 300);
|
||||
}, DETAIL_HIDE_DELAY_MS);
|
||||
};
|
||||
|
||||
const onDetailPanelEnter = () => {
|
||||
@@ -447,6 +440,9 @@ const selectSharedAgent = (shared: SharedAgentInfo) => {
|
||||
};
|
||||
|
||||
const goToSettings = (agent: CustomAgent, sourceTenantId?: string) => {
|
||||
if (!canLocallyConfigureAgent(sourceTenantId) && getAgentNotReadyLabels(agent, sourceTenantId).length > 0) {
|
||||
return;
|
||||
}
|
||||
const reasonKeys = getAgentNotReadyReasonKeysFor(agent, sourceTenantId);
|
||||
const section = reasonKeys.length > 0 ? resolveAgentNotReadySection(reasonKeys) : 'basic';
|
||||
const highlight = resolveAgentNotReadyHighlight(reasonKeys);
|
||||
@@ -470,15 +466,15 @@ const updateDropdownPosition = () => {
|
||||
const rect = rectToCssPx(props.anchorEl.getBoundingClientRect(), zoom);
|
||||
const { width: vw, height: vh } = cssViewportSize(zoom);
|
||||
|
||||
const dropdownWidth = 240;
|
||||
const offsetY = 8;
|
||||
const dropdownWidth = 196;
|
||||
const offsetY = 6;
|
||||
|
||||
let left = Math.floor(rect.left);
|
||||
const minLeft = 16;
|
||||
const maxLeft = Math.max(16, vw - dropdownWidth - 16);
|
||||
left = Math.max(minLeft, Math.min(maxLeft, left));
|
||||
|
||||
const preferredDropdownHeight = 320;
|
||||
const preferredDropdownHeight = 280;
|
||||
const minDropdownHeight = 100;
|
||||
const topMargin = 20;
|
||||
const spaceBelow = vh - rect.bottom;
|
||||
@@ -520,6 +516,12 @@ watch(() => props.visible, (newVal) => {
|
||||
hideDetailPanel();
|
||||
}
|
||||
});
|
||||
|
||||
watch(activeDetail, (detail) => {
|
||||
if (detail) {
|
||||
scheduleDetailPanelPosition();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -542,7 +544,7 @@ watch(() => props.visible, (newVal) => {
|
||||
position: fixed !important;
|
||||
background: var(--td-bg-color-container);
|
||||
border: .5px solid var(--td-component-border);
|
||||
border-radius: 10px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--td-shadow-2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -553,32 +555,45 @@ watch(() => props.visible, (newVal) => {
|
||||
}
|
||||
|
||||
@keyframes agentSelectorFadeIn {
|
||||
from { opacity: 0; transform: scale(0.98); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.agent-selector-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: 0 8px;
|
||||
border-bottom: .5px solid var(--td-component-stroke);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.agent-selector-add {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
gap: 2px;
|
||||
height: 22px;
|
||||
padding: 0 6px;
|
||||
border-radius: 5px;
|
||||
color: var(--td-brand-color);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
@@ -591,13 +606,13 @@ watch(() => props.visible, (newVal) => {
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding: 6px 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.agent-group {
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 6px;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 4px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: .5px solid var(--td-component-stroke);
|
||||
}
|
||||
}
|
||||
@@ -605,23 +620,20 @@ watch(() => props.visible, (newVal) => {
|
||||
.agent-group-title {
|
||||
font-size: 11px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
padding: 4px 8px 6px;
|
||||
padding: 6px 8px 4px;
|
||||
font-weight: 600;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.agent-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 8px;
|
||||
min-height: 32px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
border-radius: 5px;
|
||||
|
||||
&:hover,
|
||||
&.selected {
|
||||
@@ -632,12 +644,19 @@ watch(() => props.visible, (newVal) => {
|
||||
color: var(--td-text-color-placeholder);
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
padding: 20px 8px;
|
||||
padding: 16px 8px;
|
||||
min-height: auto;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.agent-avatar-small) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-option-name {
|
||||
@@ -648,19 +667,22 @@ watch(() => props.visible, (newVal) => {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.4;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.shared-tag {
|
||||
font-size: 10px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
flex-shrink: 0;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.agent-option-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -668,16 +690,24 @@ watch(() => props.visible, (newVal) => {
|
||||
flex-shrink: 0;
|
||||
color: var(--td-warning-color, #ed7b2f);
|
||||
cursor: default;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.builtin-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
|
||||
:deep(.t-icon) {
|
||||
display: block;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
&.normal {
|
||||
background: var(--td-brand-color-light);
|
||||
@@ -699,12 +729,14 @@ watch(() => props.visible, (newVal) => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 5px;
|
||||
flex-shrink: 0;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
background: var(--td-bg-color-secondarycontainer, #f5f5f5);
|
||||
overflow: hidden;
|
||||
|
||||
&.detail-icon {
|
||||
width: 28px;
|
||||
@@ -716,6 +748,17 @@ watch(() => props.visible, (newVal) => {
|
||||
/* 详情浮层 */
|
||||
.agent-detail-panel {
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
// 左侧透明桥接区:承接从选项移入的鼠标,避免经过间隙时浮层消失
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -12px;
|
||||
top: 0;
|
||||
width: 12px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-detail-panel-inner {
|
||||
@@ -830,6 +873,15 @@ watch(() => props.visible, (newVal) => {
|
||||
background: var(--td-bg-color-container);
|
||||
}
|
||||
|
||||
.detail-not-ready-shared-hint {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
color: var(--td-text-color-placeholder);
|
||||
}
|
||||
|
||||
.detail-desc {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
getAgentNotReadyReasonKeys,
|
||||
resolveAgentNotReadySection,
|
||||
resolveAgentNotReadyHighlight,
|
||||
canLocallyConfigureAgent,
|
||||
type AgentNotReadyReasonKey,
|
||||
} from '@/utils/agent-readiness';
|
||||
import { formatLocalizedList } from '@/utils/format-list';
|
||||
@@ -2047,25 +2048,34 @@ const showAgentNotReadyMessage = (
|
||||
sourceTenantId?: string,
|
||||
) => {
|
||||
const reasonsText = formatLocalizedList(reasons, locale.value)
|
||||
const section = resolveAgentNotReadySection(reasonKeys || ['summary_model'])
|
||||
const highlight = resolveAgentNotReadyHighlight(reasonKeys || ['summary_model'])
|
||||
const isRemoteShared = !canLocallyConfigureAgent(sourceTenantId)
|
||||
|
||||
const messageContent = h('div', { style: 'display: flex; flex-direction: column; gap: 8px; max-width: 320px;' }, [
|
||||
h('span', { style: 'color: var(--td-text-color-primary); line-height: 1.5;' }, t('input.agentNotReadyDetail', { agentName: agent.name, reasons: reasonsText })),
|
||||
h('a', {
|
||||
href: '#',
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault();
|
||||
goToAgentEditor(agent, section, highlight, sourceTenantId);
|
||||
},
|
||||
style: 'color: var(--td-brand-color); text-decoration: none; font-weight: 500; cursor: pointer; align-self: flex-start;',
|
||||
onMouseenter: (e: Event) => {
|
||||
(e.target as HTMLElement).style.textDecoration = 'underline';
|
||||
},
|
||||
onMouseleave: (e: Event) => {
|
||||
(e.target as HTMLElement).style.textDecoration = 'none';
|
||||
}
|
||||
}, t('input.goToAgentEditor'))
|
||||
h(
|
||||
'span',
|
||||
{ style: 'color: var(--td-text-color-primary); line-height: 1.5;' },
|
||||
isRemoteShared
|
||||
? t('input.sharedAgentNotReadyDetail', { agentName: agent.name, reasons: reasonsText })
|
||||
: t('input.agentNotReadyDetail', { agentName: agent.name, reasons: reasonsText }),
|
||||
),
|
||||
...(isRemoteShared ? [] : [
|
||||
h('a', {
|
||||
href: '#',
|
||||
onClick: (e: Event) => {
|
||||
e.preventDefault();
|
||||
const section = resolveAgentNotReadySection(reasonKeys || ['summary_model'])
|
||||
const highlight = resolveAgentNotReadyHighlight(reasonKeys || ['summary_model'])
|
||||
goToAgentEditor(agent, section, highlight, sourceTenantId);
|
||||
},
|
||||
style: 'color: var(--td-brand-color); text-decoration: none; font-weight: 500; cursor: pointer; align-self: flex-start;',
|
||||
onMouseenter: (e: Event) => {
|
||||
(e.target as HTMLElement).style.textDecoration = 'underline';
|
||||
},
|
||||
onMouseleave: (e: Event) => {
|
||||
(e.target as HTMLElement).style.textDecoration = 'none';
|
||||
}
|
||||
}, t('input.goToAgentEditor')),
|
||||
]),
|
||||
]);
|
||||
|
||||
MessagePlugin.warning({
|
||||
@@ -2233,9 +2243,7 @@ defineExpose({
|
||||
<!-- Agent 选择器下拉菜单 -->
|
||||
<AgentSelector :visible="showAgentModeSelector" :anchorEl="agentModeButtonRef"
|
||||
:currentAgentId="selectedAgentId" :agents="enabledAgents" :all-models="allModels"
|
||||
@close="closeAgentModeSelector"
|
||||
@select="handleSelectAgent"
|
||||
@not-ready="handleAgentNotReady" />
|
||||
@close="closeAgentModeSelector" @select="handleSelectAgent" @not-ready="handleAgentNotReady" />
|
||||
|
||||
<!-- WebSearch 开关按钮 -->
|
||||
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
|
||||
@@ -2243,7 +2251,7 @@ defineExpose({
|
||||
<div v-if="isWebSearchDisabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.webSearchDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('websearch')">{{ $t('input.goToAgentSettings')
|
||||
}}</a>
|
||||
}}</a>
|
||||
</div>
|
||||
<span v-else-if="isWebSearchConfigured">{{ isWebSearchEnabled ? $t('input.webSearch.toggleOff') :
|
||||
$t('input.webSearch.toggleOn') }}</span>
|
||||
@@ -2308,7 +2316,7 @@ defineExpose({
|
||||
d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
||||
</svg>
|
||||
<span v-if="uploadedAttachments.length > 0" class="attachment-count">{{ uploadedAttachments.length
|
||||
}}</span>
|
||||
}}</span>
|
||||
</div>
|
||||
</t-tooltip>
|
||||
|
||||
@@ -2318,7 +2326,7 @@ defineExpose({
|
||||
<div v-if="isKnowledgeBaseDisabledByAgent" class="tooltip-with-link">
|
||||
<span>{{ $t('input.kbDisabledByAgent') }}</span>
|
||||
<a href="#" @click.prevent="handleGoToAgentSettings('knowledge')">{{ $t('input.goToAgentSettings')
|
||||
}}</a>
|
||||
}}</a>
|
||||
</div>
|
||||
<span v-else>{{ allSelectedItems.length > 0 ? $t('input.knowledgeBaseWithCount', {
|
||||
count:
|
||||
@@ -2368,13 +2376,8 @@ defineExpose({
|
||||
</button>
|
||||
</div>
|
||||
<div class="model-selector-content">
|
||||
<div
|
||||
v-for="model in availableModels"
|
||||
:key="model.id"
|
||||
class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }"
|
||||
@click="handleModelChange(model.id || '')"
|
||||
>
|
||||
<div v-for="model in availableModels" :key="model.id" class="model-option"
|
||||
:class="{ selected: model.id === selectedModelId }" @click="handleModelChange(model.id || '')">
|
||||
<div class="model-option-left">
|
||||
<div class="model-option-icon">
|
||||
<t-icon name="chat" size="14px" />
|
||||
@@ -3191,8 +3194,15 @@ const getImgSrc = (url: string) => {
|
||||
}
|
||||
|
||||
@keyframes modelSelectorFadeIn {
|
||||
from { opacity: 0; transform: scale(0.98); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.model-selector-header {
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
<template>
|
||||
<t-dropdown
|
||||
:options="options"
|
||||
trigger="click"
|
||||
placement="bottom-left"
|
||||
attach="body"
|
||||
:max-column-width="240"
|
||||
@click="onSelect"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="integrations-agent-filter"
|
||||
:class="{ 'integrations-agent-filter--active': modelValue }"
|
||||
:aria-label="ariaLabel"
|
||||
>
|
||||
<t-dropdown :options="options" trigger="click" placement="bottom-left" attach="body" :max-column-width="240"
|
||||
:max-height="280" @click="onSelect">
|
||||
<button type="button" class="integrations-agent-filter" :class="{ 'integrations-agent-filter--active': modelValue }"
|
||||
:aria-label="ariaLabel">
|
||||
<t-icon name="filter" size="14px" class="integrations-agent-filter__icon" />
|
||||
<span v-if="selectedAgentName" class="integrations-agent-filter__name">{{ selectedAgentName }}</span>
|
||||
<t-icon name="chevron-down" size="12px" class="integrations-agent-filter__chevron" />
|
||||
|
||||
@@ -500,7 +500,7 @@ export function useChatStreamHandler(options: UseChatStreamHandlerOptions) {
|
||||
onAgentChunkBound?.(message, true)
|
||||
}
|
||||
|
||||
message.isAgentMode = true
|
||||
ensureAgentMessageShell(message, dataId)
|
||||
|
||||
if (
|
||||
loading.value &&
|
||||
|
||||
@@ -1124,6 +1124,7 @@ export default {
|
||||
notReadyStatus: 'Setup needed',
|
||||
notReadyMissing: 'Missing: {items}',
|
||||
configureAction: 'Configure',
|
||||
sharedNotReadyContact: 'Ask the sharing organization admin to finish setup',
|
||||
},
|
||||
// Built-in agent information
|
||||
builtinInfo: {
|
||||
@@ -2131,6 +2132,7 @@ export default {
|
||||
customAgentMissingRerankModel: 'Rerank model',
|
||||
goToAgentEditor: 'Go to configure →',
|
||||
agentNotReadyDetail: 'Agent "{agentName}" is not ready. Please configure: {reasons}',
|
||||
sharedAgentNotReadyDetail: 'Shared agent "{agentName}" is not ready (missing: {reasons}). Ask the sharing organization admin to finish setup.',
|
||||
builtinAgentNotReadyDetail: 'Built-in agent "{agentName}" is not ready. Please configure: {reasons}',
|
||||
builtinAgentSettingName: 'Intelligent Reasoning',
|
||||
builtinNormalSettingName: 'Quick Q&A',
|
||||
|
||||
@@ -2120,6 +2120,7 @@ export default {
|
||||
notReadyStatus: "구성 필요",
|
||||
notReadyMissing: "누락: {items}",
|
||||
configureAction: "구성",
|
||||
sharedNotReadyContact: "공유한 조직 관리자에게 구성을 요청하세요",
|
||||
},
|
||||
// 내장 에이전트 정보
|
||||
builtinInfo: {
|
||||
@@ -3904,6 +3905,8 @@ export default {
|
||||
goToAgentEditor: "구성으로 이동 →",
|
||||
agentNotReadyDetail:
|
||||
'Agent "{agentName}"가 준비되지 않았습니다. 다음 항목을 설정해야 합니다: {reasons}',
|
||||
sharedAgentNotReadyDetail:
|
||||
'공유 Agent "{agentName}"가 준비되지 않았습니다(누락: {reasons}). 공유한 조직 관리자에게 구성을 요청하세요.',
|
||||
builtinAgentNotReadyDetail:
|
||||
'내장 에이전트 "{agentName}"가 준비되지 않았습니다. 다음 항목을 설정해야 합니다: {reasons}',
|
||||
builtinAgentSettingName: "에이전트 추론",
|
||||
|
||||
@@ -1038,6 +1038,7 @@ export default {
|
||||
notReadyStatus: 'Setup needed',
|
||||
notReadyMissing: 'Missing: {items}',
|
||||
configureAction: 'Configure',
|
||||
sharedNotReadyContact: 'Ask the sharing organization admin to finish setup',
|
||||
},
|
||||
builtinInfo: {
|
||||
quickAnswer: {
|
||||
@@ -4242,6 +4243,7 @@ export default {
|
||||
agentModeDesc: 'Многошаговое мышление, глубокий анализ',
|
||||
agentNotReadyTooltip: 'Agent не готов. Пожалуйста, завершите настройку.',
|
||||
agentNotReadyDetail: 'Agent не готов. Пожалуйста, настройте следующее: {reasons}',
|
||||
sharedAgentNotReadyDetail: 'Общий agent «{agentName}» не готов (отсутствует: {reasons}). Обратитесь к администратору организации, которая поделилась agent.',
|
||||
agentMissingAllowedTools: 'Разрешённые инструменты',
|
||||
agentMissingSummaryModel: 'Модель беседы',
|
||||
agentMissingRerankModel: 'Модель переранжирования',
|
||||
|
||||
@@ -2116,6 +2116,7 @@ export default {
|
||||
notReadyStatus: "待配置",
|
||||
notReadyMissing: "缺少 {items}",
|
||||
configureAction: "去配置",
|
||||
sharedNotReadyContact: "请联系分享方管理员完成配置",
|
||||
},
|
||||
// 内置智能体信息
|
||||
builtinInfo: {
|
||||
@@ -3912,6 +3913,7 @@ export default {
|
||||
customAgentMissingRerankModel: "重排模型",
|
||||
goToAgentEditor: "前往配置 →",
|
||||
agentNotReadyDetail: "智能体「{agentName}」未就绪,需要配置以下内容:{reasons}",
|
||||
sharedAgentNotReadyDetail: "共享智能体「{agentName}」尚未就绪(缺少:{reasons}),请联系分享方管理员完成配置。",
|
||||
builtinAgentNotReadyDetail: "内置智能体「{agentName}」未就绪,需要配置以下内容:{reasons}",
|
||||
builtinAgentSettingName: "智能推理",
|
||||
builtinNormalSettingName: "快速问答",
|
||||
|
||||
@@ -246,6 +246,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
parsed.selectedAgentId = BUILTIN_QUICK_ANSWER_ID
|
||||
parsed.selectedAgentSourceTenantId = null
|
||||
parsed.isAgentEnabled = false
|
||||
if (parsed.conversationModels && typeof parsed.conversationModels === 'object') {
|
||||
parsed.conversationModels.summaryModelId = ''
|
||||
parsed.conversationModels.rerankModelId = ''
|
||||
|
||||
@@ -3,6 +3,7 @@ import { nextTick } from "vue";
|
||||
import { BUILTIN_QUICK_ANSWER_ID, BUILTIN_SMART_REASONING_ID } from "@/api/agent";
|
||||
import { getApiBaseUrl } from "@/utils/api-base";
|
||||
import { updateMyPreferences, type UserPreferences } from "@/api/auth";
|
||||
import { isAgentStreamAgentId, reconcileBuiltinAgentMode } from "@/utils/agent-mode";
|
||||
|
||||
// 定义设置接口
|
||||
interface Settings {
|
||||
@@ -102,10 +103,21 @@ const defaultSettings: Settings = {
|
||||
autoCheckUpdate: true,
|
||||
};
|
||||
|
||||
/** Keep builtin agent id and isAgentEnabled in sync after localStorage reload. */
|
||||
function loadAndReconcileSettings(): Settings {
|
||||
const loaded = JSON.parse(
|
||||
localStorage.getItem("WeKnora_settings") || JSON.stringify(defaultSettings),
|
||||
) as Settings;
|
||||
if (reconcileBuiltinAgentMode(loaded)) {
|
||||
localStorage.setItem("WeKnora_settings", JSON.stringify(loaded));
|
||||
}
|
||||
return loaded;
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore("settings", {
|
||||
state: () => ({
|
||||
// 从本地存储加载设置,如果没有则使用默认设置
|
||||
settings: JSON.parse(localStorage.getItem("WeKnora_settings") || JSON.stringify(defaultSettings)) as Settings,
|
||||
settings: loadAndReconcileSettings(),
|
||||
// 进入会话时拍下"全局默认"的快照;离开会话时还原。非持久化字段:
|
||||
// 刷新页面相当于重新走"进入会话"流程,自然会重新拍快照。
|
||||
_defaultsSnapshot: null as Settings | null,
|
||||
@@ -116,6 +128,17 @@ export const useSettingsStore = defineStore("settings", {
|
||||
getters: {
|
||||
// Agent 是否启用
|
||||
isAgentEnabled: (state) => state.settings.isAgentEnabled || false,
|
||||
|
||||
// 当前是否为内置快速问答(优先看 selectedAgentId,避免与 isAgentEnabled 漂移)
|
||||
isQuickAnswerMode: (state) =>
|
||||
(state.settings.selectedAgentId || BUILTIN_QUICK_ANSWER_ID) === BUILTIN_QUICK_ANSWER_ID,
|
||||
|
||||
// 是否走 Agent 流式管线(智能推理 / 自定义 Agent);快速问答走 RAG 管线
|
||||
isAgentStreamMode: (state) =>
|
||||
isAgentStreamAgentId(
|
||||
state.settings.selectedAgentId,
|
||||
state.settings.isAgentEnabled || false,
|
||||
),
|
||||
|
||||
// Agent 是否就绪(配置完整)
|
||||
// 需要满足:1) 配置了允许的工具 2) 设置了对话模型 3) 设置了重排模型
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
const BUILTIN_QUICK_ANSWER_ID = 'builtin-quick-answer'
|
||||
const BUILTIN_SMART_REASONING_ID = 'builtin-smart-reasoning'
|
||||
|
||||
function isQuickAnswerAgentId(agentId) {
|
||||
return (agentId || BUILTIN_QUICK_ANSWER_ID) === BUILTIN_QUICK_ANSWER_ID
|
||||
}
|
||||
|
||||
function isAgentStreamAgentId(agentId, isAgentEnabled) {
|
||||
const id = agentId || BUILTIN_QUICK_ANSWER_ID
|
||||
if (id === BUILTIN_QUICK_ANSWER_ID) return false
|
||||
if (id === BUILTIN_SMART_REASONING_ID) return true
|
||||
return isAgentEnabled
|
||||
}
|
||||
|
||||
function reconcileBuiltinAgentMode(settings) {
|
||||
const agentId = settings.selectedAgentId || BUILTIN_QUICK_ANSWER_ID
|
||||
if (agentId === BUILTIN_QUICK_ANSWER_ID && settings.isAgentEnabled) {
|
||||
settings.isAgentEnabled = false
|
||||
return true
|
||||
}
|
||||
if (agentId === BUILTIN_SMART_REASONING_ID && !settings.isAgentEnabled) {
|
||||
settings.isAgentEnabled = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
test('isQuickAnswerAgentId treats builtin quick-answer as RAG mode', () => {
|
||||
assert.equal(isQuickAnswerAgentId('builtin-quick-answer'), true)
|
||||
assert.equal(isQuickAnswerAgentId(undefined), true)
|
||||
assert.equal(isQuickAnswerAgentId('builtin-smart-reasoning'), false)
|
||||
})
|
||||
|
||||
test('isAgentStreamAgentId prefers selectedAgentId for builtins', () => {
|
||||
assert.equal(
|
||||
isAgentStreamAgentId('builtin-quick-answer', true),
|
||||
false,
|
||||
'stale isAgentEnabled must not flip quick-answer into agent stream',
|
||||
)
|
||||
assert.equal(isAgentStreamAgentId('builtin-smart-reasoning', false), true)
|
||||
assert.equal(isAgentStreamAgentId('custom-agent', true), true)
|
||||
assert.equal(isAgentStreamAgentId('custom-agent', false), false)
|
||||
})
|
||||
|
||||
test('reconcileBuiltinAgentMode repairs drifted localStorage flags', () => {
|
||||
const quick = { selectedAgentId: 'builtin-quick-answer', isAgentEnabled: true }
|
||||
assert.equal(reconcileBuiltinAgentMode(quick), true)
|
||||
assert.equal(quick.isAgentEnabled, false)
|
||||
|
||||
const reasoning = { selectedAgentId: 'builtin-smart-reasoning', isAgentEnabled: false }
|
||||
assert.equal(reconcileBuiltinAgentMode(reasoning), true)
|
||||
assert.equal(reasoning.isAgentEnabled, true)
|
||||
|
||||
const custom = { selectedAgentId: 'custom-agent', isAgentEnabled: true }
|
||||
assert.equal(reconcileBuiltinAgentMode(custom), false)
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import {
|
||||
BUILTIN_QUICK_ANSWER_ID,
|
||||
BUILTIN_SMART_REASONING_ID,
|
||||
} from '@/api/agent'
|
||||
|
||||
/** Whether the selected agent id is the builtin quick-answer (RAG) mode. */
|
||||
export function isQuickAnswerAgentId(agentId: string | null | undefined): boolean {
|
||||
return (agentId || BUILTIN_QUICK_ANSWER_ID) === BUILTIN_QUICK_ANSWER_ID
|
||||
}
|
||||
|
||||
/** Whether requests should use the Agent stream pipeline (not quick-answer RAG). */
|
||||
export function isAgentStreamAgentId(
|
||||
agentId: string | null | undefined,
|
||||
isAgentEnabled: boolean,
|
||||
): boolean {
|
||||
const id = agentId || BUILTIN_QUICK_ANSWER_ID
|
||||
if (id === BUILTIN_QUICK_ANSWER_ID) return false
|
||||
if (id === BUILTIN_SMART_REASONING_ID) return true
|
||||
return isAgentEnabled
|
||||
}
|
||||
|
||||
/** Reconcile builtin agent id with isAgentEnabled after localStorage reload. */
|
||||
export function reconcileBuiltinAgentMode(settings: {
|
||||
selectedAgentId?: string
|
||||
isAgentEnabled: boolean
|
||||
}): boolean {
|
||||
const agentId = settings.selectedAgentId || BUILTIN_QUICK_ANSWER_ID
|
||||
if (agentId === BUILTIN_QUICK_ANSWER_ID && settings.isAgentEnabled) {
|
||||
settings.isAgentEnabled = false
|
||||
return true
|
||||
}
|
||||
if (agentId === BUILTIN_SMART_REASONING_ID && !settings.isAgentEnabled) {
|
||||
settings.isAgentEnabled = true
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import test from 'node:test'
|
||||
import {
|
||||
agentHasConfiguredChatModel,
|
||||
agentRequiresRerankModel,
|
||||
canLocallyConfigureAgent,
|
||||
getAgentNotReadyReasonKeys,
|
||||
resolveAgentNotReadySection,
|
||||
resolveAgentNotReadyHighlight,
|
||||
@@ -100,3 +101,22 @@ test('getAgentNotReadyReasonKeys does not treat current shared context as shared
|
||||
{ isAgentMode: false, isSharedAgent: false },
|
||||
), ['summary_model'])
|
||||
})
|
||||
|
||||
test('getAgentNotReadyReasonKeys treats empty allowed_tools as ready via backend defaults', () => {
|
||||
assert.deepEqual(getAgentNotReadyReasonKeys(
|
||||
{
|
||||
model_id: 'chat-1',
|
||||
rerank_model_id: 'rerank-1',
|
||||
kb_selection_mode: 'all',
|
||||
allowed_tools: [],
|
||||
},
|
||||
[{ id: 'chat-1', type: 'KnowledgeQA' }, { id: 'rerank-1', type: 'Rerank' }],
|
||||
{ isAgentMode: true, isSharedAgent: false },
|
||||
), [])
|
||||
})
|
||||
|
||||
test('canLocallyConfigureAgent is false for shared agents', () => {
|
||||
assert.equal(canLocallyConfigureAgent('42'), false)
|
||||
assert.equal(canLocallyConfigureAgent(undefined), true)
|
||||
assert.equal(canLocallyConfigureAgent(''), true)
|
||||
})
|
||||
|
||||
@@ -67,13 +67,14 @@ export function getAgentNotReadyReasonKeys(
|
||||
}
|
||||
}
|
||||
|
||||
if (options.isAgentMode && (!config?.allowed_tools || config.allowed_tools.length === 0)) {
|
||||
reasons.push('allowed_tools')
|
||||
}
|
||||
|
||||
return reasons
|
||||
}
|
||||
|
||||
/** Shared agents are owned by another tenant; receivers cannot edit their config. */
|
||||
export function canLocallyConfigureAgent(sourceTenantId?: string): boolean {
|
||||
return !sourceTenantId
|
||||
}
|
||||
|
||||
/** Map missing-config reasons to the agent editor section that fixes them. */
|
||||
export function resolveAgentNotReadySection(reasons: AgentNotReadyReasonKey[]): string {
|
||||
if (reasons.includes('summary_model') || reasons.includes('rerank_model')) {
|
||||
|
||||
@@ -144,7 +144,7 @@ const isAgentStreamSession = () => {
|
||||
if (props.embeddedMode) {
|
||||
return !!(props.agentId && props.agentId !== 'builtin-quick-answer');
|
||||
}
|
||||
return useSettingsStoreInstance.isAgentEnabled;
|
||||
return useSettingsStoreInstance.isAgentStreamMode;
|
||||
};
|
||||
|
||||
const uiStore = useUIStore();
|
||||
@@ -623,8 +623,10 @@ const sendMsg = async (value, modelId = '', mentionedItems = [], imageFiles = []
|
||||
userHasScrolledUp.value = false;
|
||||
scrollToBottom(true);
|
||||
|
||||
// Get agent mode status from settings store
|
||||
const agentEnabled = props.embeddedMode ? (props.agentId && props.agentId !== 'builtin-quick-answer') : useSettingsStoreInstance.isAgentEnabled;
|
||||
// Get agent mode status from settings store (prefer selectedAgentId for builtins)
|
||||
const agentEnabled = props.embeddedMode
|
||||
? (props.agentId && props.agentId !== 'builtin-quick-answer')
|
||||
: useSettingsStoreInstance.isAgentStreamMode;
|
||||
|
||||
// Get web search status from settings store
|
||||
const webSearchEnabled = props.embeddedMode ? false : useSettingsStoreInstance.isWebSearchEnabled;
|
||||
|
||||
Reference in New Issue
Block a user