From 47478491c995c1eedf16b51defaf87ba79dbd54f Mon Sep 17 00:00:00 2001 From: wizardchen Date: Thu, 25 Jun 2026 21:42:13 +0800 Subject: [PATCH] fix(agent): address readiness review findings Tighten shared-agent readiness checks, surface not-ready toasts in the selector, validate chat-model overrides in resolveChatModelID, and improve configure deep-links for shared agents. --- frontend/src/components/AgentSelector.vue | 31 +++++++-- frontend/src/components/Input-field.vue | 43 ++++++++++--- frontend/src/utils/agent-readiness.test.mjs | 8 +++ frontend/src/utils/format-list.ts | 12 ++++ frontend/src/views/agent/AgentEditorModal.vue | 11 +++- frontend/src/views/agent/AgentList.vue | 21 +++++- .../service/session_model_requirement_test.go | 64 +++++++++++++++++++ .../application/service/session_qa_helpers.go | 13 ++-- 8 files changed, 177 insertions(+), 26 deletions(-) create mode 100644 frontend/src/utils/format-list.ts diff --git a/frontend/src/components/AgentSelector.vue b/frontend/src/components/AgentSelector.vue index 96281f3f..4c418111 100644 --- a/frontend/src/components/AgentSelector.vue +++ b/frontend/src/components/AgentSelector.vue @@ -41,7 +41,7 @@ {{ agent.name }}
@@ -66,7 +66,7 @@ {{ agent.name }}
@@ -95,7 +95,7 @@ class="agent-option-actions" > @@ -216,8 +216,9 @@ import { resolveAgentNotReadyHighlight, type AgentNotReadyReasonKey, } from '@/utils/agent-readiness'; +import { formatLocalizedList } from '@/utils/format-list'; -const { t } = useI18n(); +const { t, locale } = useI18n(); const router = useRouter(); const orgStore = useOrganizationStore(); const settingsStore = useSettingsStore(); @@ -233,6 +234,7 @@ const props = defineProps<{ const emit = defineEmits<{ (e: 'close'): void; (e: 'select', agent: CustomAgent, sourceTenantId?: string): void; + (e: 'not-ready', agent: CustomAgent, labels: string[], keys: AgentNotReadyReasonKey[], sourceTenantId?: string): void; }>(); type AgentDetailTarget = { @@ -347,6 +349,16 @@ const getAgentNotReadyLabels = (agent: CustomAgent, sourceTenantId?: string): st ); }; +const formatNotReadyHint = (agent: CustomAgent, sourceTenantId?: string): string => { + return formatLocalizedList(getAgentNotReadyLabels(agent, sourceTenantId), locale.value); +}; + +const emitAgentNotReady = (agent: CustomAgent, sourceTenantId?: string) => { + const keys = getAgentNotReadyReasonKeysFor(agent, sourceTenantId); + const labels = formatAgentNotReadyReasons(keys, agent.is_builtin); + emit('not-ready', agent, labels, keys, sourceTenantId); +}; + const clearDetailHideTimer = () => { if (detailHideTimer) { clearTimeout(detailHideTimer); @@ -418,13 +430,19 @@ const hideDetailPanel = () => { }; const selectAgent = (agent: CustomAgent) => { - if (getAgentNotReadyLabels(agent).length > 0) return; + if (getAgentNotReadyLabels(agent).length > 0) { + emitAgentNotReady(agent); + return; + } emit('select', agent); }; const selectSharedAgent = (shared: SharedAgentInfo) => { const sourceTenantId = String(shared.source_tenant_id); - if (getAgentNotReadyLabels(shared.agent, sourceTenantId).length > 0) return; + if (getAgentNotReadyLabels(shared.agent, sourceTenantId).length > 0) { + emitAgentNotReady(shared.agent as CustomAgent, sourceTenantId); + return; + } emit('select', shared.agent as CustomAgent, sourceTenantId); }; @@ -440,6 +458,7 @@ const goToSettings = (agent: CustomAgent, sourceTenantId?: string) => { edit: agent.id, section, ...(highlight ? { highlight } : {}), + ...(sourceTenantId ? { sourceTenantId } : {}), }, }); }; diff --git a/frontend/src/components/Input-field.vue b/frontend/src/components/Input-field.vue index 2e97c3c5..fb48c969 100644 --- a/frontend/src/components/Input-field.vue +++ b/frontend/src/components/Input-field.vue @@ -32,6 +32,7 @@ import { resolveAgentNotReadyHighlight, type AgentNotReadyReasonKey, } from '@/utils/agent-readiness'; +import { formatLocalizedList } from '@/utils/format-list'; const route = useRoute(); const router = useRouter(); @@ -47,7 +48,7 @@ const { chatModels: availableModels, webSearchProviders, } = storeToRefs(chatResources); -const { t } = useI18n(); +const { t, locale } = useI18n(); let query = ref(""); const showKbSelector = ref(false); @@ -1631,9 +1632,18 @@ const createSession = async (val: string) => { actualAgent = builtin || agentToCheck; } const isAgentMode = actualAgent.config?.agent_mode === 'smart-reasoning'; - const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons(actualAgent, isAgentMode); + const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons( + actualAgent, + isAgentMode, + settingsStore.selectedAgentSourceTenantId ?? undefined, + ); if (notReadyReasons.length > 0) { - showAgentNotReadyMessage(actualAgent, notReadyReasons, notReadyKeys); + showAgentNotReadyMessage( + actualAgent, + notReadyReasons, + notReadyKeys, + settingsStore.selectedAgentSourceTenantId ?? undefined, + ); return; } // 获取@提及的知识库和文件信息 @@ -1780,12 +1790,13 @@ const selectAgentMode = async (mode: 'quick-answer' | 'smart-reasoning') => { const builtinAgent = agents.value.find(a => a.id === builtinAgentId); if (builtinAgent) { - const { labels: notReadyReasons } = collectAgentNotReadyReasons( + const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons( builtinAgent, mode === 'smart-reasoning', ); if (notReadyReasons.length > 0) { showAgentModeSelector.value = false; + showAgentNotReadyMessage(builtinAgent, notReadyReasons, notReadyKeys); return; } } @@ -1801,6 +1812,15 @@ const selectAgentMode = async (mode: 'quick-answer' | 'smart-reasoning') => { } // 选择智能体(新版);sourceTenantId 为共享智能体时传入 +const handleAgentNotReady = ( + agent: CustomAgent, + labels: string[], + keys: AgentNotReadyReasonKey[], + sourceTenantId?: string, +) => { + showAgentNotReadyMessage(agent, labels, keys, sourceTenantId); +}; + const handleSelectAgent = async (agent: CustomAgent, sourceTenantId?: string) => { if (!chatResources.isFresh('models')) { await loadChatModels() @@ -1814,7 +1834,7 @@ const handleSelectAgent = async (agent: CustomAgent, sourceTenantId?: string) => ? (agents.value.find(a => a.id === agent.id) || agent) : agent; - const { labels: notReadyReasons } = collectAgentNotReadyReasons( + const { keys: notReadyKeys, labels: notReadyReasons } = collectAgentNotReadyReasons( actualAgent, isAgentType, sourceTenantId, @@ -1822,6 +1842,7 @@ const handleSelectAgent = async (agent: CustomAgent, sourceTenantId?: string) => if (notReadyReasons.length > 0) { showAgentModeSelector.value = false; + showAgentNotReadyMessage(actualAgent, notReadyReasons, notReadyKeys, sourceTenantId); return; } @@ -1990,7 +2011,7 @@ const collectAgentNotReadyReasons = ( isAgentMode: boolean, sourceTenantId?: string, ): { keys: AgentNotReadyReasonKey[]; labels: string[] } => { - const isSharedAgent = !!(sourceTenantId || settingsStore.selectedAgentSourceTenantId); + const isSharedAgent = !!sourceTenantId; const keys = getAgentNotReadyReasonKeys(agent.config, allModels.value, { isAgentMode, isSharedAgent, @@ -2005,6 +2026,7 @@ const goToAgentEditor = ( agent: CustomAgent, section = 'model', highlight?: AgentNotReadyReasonKey, + sourceTenantId?: string, ) => { router.push({ path: '/platform/agents', @@ -2012,6 +2034,7 @@ const goToAgentEditor = ( edit: agent.id, section, ...(highlight ? { highlight } : {}), + ...(sourceTenantId ? { sourceTenantId } : {}), }, }); }; @@ -2021,8 +2044,9 @@ const showAgentNotReadyMessage = ( agent: CustomAgent, reasons: string[], reasonKeys?: AgentNotReadyReasonKey[], + sourceTenantId?: string, ) => { - const reasonsText = reasons.join('、') + const reasonsText = formatLocalizedList(reasons, locale.value) const section = resolveAgentNotReadySection(reasonKeys || ['summary_model']) const highlight = resolveAgentNotReadyHighlight(reasonKeys || ['summary_model']) @@ -2032,7 +2056,7 @@ const showAgentNotReadyMessage = ( href: '#', onClick: (e: Event) => { e.preventDefault(); - goToAgentEditor(agent, section, highlight); + 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) => { @@ -2210,7 +2234,8 @@ defineExpose({ + @select="handleSelectAgent" + @not-ready="handleAgentNotReady" /> diff --git a/frontend/src/utils/agent-readiness.test.mjs b/frontend/src/utils/agent-readiness.test.mjs index 9ebd75ba..a6f8924c 100644 --- a/frontend/src/utils/agent-readiness.test.mjs +++ b/frontend/src/utils/agent-readiness.test.mjs @@ -92,3 +92,11 @@ test('getAgentNotReadyReasonKeys requires rerank only in agent mode with KB sear { isAgentMode: true, isSharedAgent: false }, ), ['summary_model', 'rerank_model']) }) + +test('getAgentNotReadyReasonKeys does not treat current shared context as shared without sourceTenantId', () => { + assert.deepEqual(getAgentNotReadyReasonKeys( + { model_id: 'deleted-chat' }, + [{ id: 'chat-1', type: 'KnowledgeQA' }], + { isAgentMode: false, isSharedAgent: false }, + ), ['summary_model']) +}) diff --git a/frontend/src/utils/format-list.ts b/frontend/src/utils/format-list.ts new file mode 100644 index 00000000..0f48e9d1 --- /dev/null +++ b/frontend/src/utils/format-list.ts @@ -0,0 +1,12 @@ +/** Locale-aware conjunction for short UI lists (e.g. missing config items). */ +export function formatLocalizedList(items: string[], locale = 'en-US'): string { + if (items.length === 0) return '' + if (typeof Intl !== 'undefined' && typeof Intl.ListFormat !== 'undefined') { + try { + return new Intl.ListFormat(locale, { style: 'long', type: 'conjunction' }).format(items) + } catch { + // Unsupported locale — fall through. + } + } + return items.join(', ') +} diff --git a/frontend/src/views/agent/AgentEditorModal.vue b/frontend/src/views/agent/AgentEditorModal.vue index c9862c72..c19a9897 100644 --- a/frontend/src/views/agent/AgentEditorModal.vue +++ b/frontend/src/views/agent/AgentEditorModal.vue @@ -620,9 +620,9 @@
- +
{ return kbSelectionMode.value !== 'none'; }); +const showRerankModelField = computed(() => { + if (!isAgentMode.value) return hasKnowledgeBase.value; + return hasKnowledgeBase.value || agentRequiresRerankModel(formData.value.config); +}); + // 当前配置下进入到智能体作用域的知识库列表 // 注意:用户可能选了 knowledge_bases(按库级),也可能选了 knowledge_ids(按文档级) // 这里仅用于 UI 上的工具可用性判定,按库级来计算 diff --git a/frontend/src/views/agent/AgentList.vue b/frontend/src/views/agent/AgentList.vue index be39c929..91f74dc9 100644 --- a/frontend/src/views/agent/AgentList.vue +++ b/frontend/src/views/agent/AgentList.vue @@ -1119,7 +1119,9 @@ const fetchList = (force = false) => { return Promise.all([ chatResources.fetchAgentsForList({ creator: creatorFilter.value }, force).then(applyAgentListData), orgStore.fetchOrganizations({ force }), + orgStore.fetchSharedAgents({ force }), ]).finally(() => { loading.value = false }).then(() => { + checkAndOpenEditModal() // 各空间智能体数量已由 GET /organizations 的 resource_counts 带回,存于 orgStore.resourceCounts const counts = orgStore.resourceCounts?.agents?.by_organization if (counts) spaceAgentCountByOrg.value = { ...counts } @@ -1127,9 +1129,22 @@ const fetchList = (force = false) => { } // 检查 URL 参数并打开编辑模态框 +const resolveAgentForEdit = (editId: string, sourceTenantId?: string): CustomAgent | null => { + const own = agents.value.find(a => a.id === editId) + if (own) return own + if (sourceTenantId) { + const shared = sharedAgents.value.find( + s => s.agent?.id === editId && String(s.source_tenant_id) === sourceTenantId, + ) + if (shared?.agent) return shared.agent as CustomAgent + } + return null +} + const checkAndOpenEditModal = () => { const editId = route.query.edit as string const section = route.query.section as string + const sourceTenantId = route.query.sourceTenantId as string | undefined if (editId && (section === 'im' || section === 'embed' || section === 'integrations')) { const tab = section === 'embed' ? 'embed' : 'im' router.replace({ @@ -1139,7 +1154,7 @@ const checkAndOpenEditModal = () => { return } if (editId) { - const agent = agents.value.find(a => a.id === editId) + const agent = resolveAgentForEdit(editId, sourceTenantId) if (agent) { editingAgent.value = agent editorMode.value = 'edit' @@ -1149,7 +1164,7 @@ const checkAndOpenEditModal = () => { } // Drop the transient edit/section params but preserve other filter // state (scope / creator / q) so refreshing doesn't reset the view. - const { edit: _e, section: _s, highlight: _h, ...rest } = route.query + const { edit: _e, section: _s, highlight: _h, sourceTenantId: _st, ...rest } = route.query router.replace({ path: route.path, query: rest }) } } @@ -1161,7 +1176,7 @@ const checkAndOpenEditModal = () => { watch( () => route.query.edit, (v) => { - if (v && agents.value.length > 0) { + if (v && (agents.value.length > 0 || sharedAgents.value.length > 0)) { checkAndOpenEditModal() } }, diff --git a/internal/application/service/session_model_requirement_test.go b/internal/application/service/session_model_requirement_test.go index 2702a9e0..ee8897cf 100644 --- a/internal/application/service/session_model_requirement_test.go +++ b/internal/application/service/session_model_requirement_test.go @@ -83,3 +83,67 @@ func TestResolveChatModelIDUsesValidConfiguredAgentModel(t *testing.T) { require.NoError(t, err) assert.Equal(t, "agent-chat", modelID) } + +func TestResolveChatModelIDRejectsNonChatSummaryModelOverride(t *testing.T) { + svc := &sessionService{ + modelService: &stubModelService{ + modelsByID: map[string]*types.Model{ + "agent-chat": { + ID: "agent-chat", + Type: types.ModelTypeKnowledgeQA, + }, + "rerank-only": { + ID: "rerank-only", + Type: types.ModelTypeRerank, + }, + }, + }, + } + req := &types.QARequest{ + Session: &types.Session{}, + CustomAgent: &types.CustomAgent{ + ID: "agent-1", + Config: types.CustomAgentConfig{ + ModelID: "agent-chat", + }, + }, + SummaryModelID: "rerank-only", + } + + modelID, err := svc.resolveChatModelID(context.Background(), req, nil, nil) + + require.NoError(t, err) + assert.Equal(t, "agent-chat", modelID) +} + +func TestResolveChatModelIDUsesValidSummaryModelOverride(t *testing.T) { + svc := &sessionService{ + modelService: &stubModelService{ + modelsByID: map[string]*types.Model{ + "agent-chat": { + ID: "agent-chat", + Type: types.ModelTypeKnowledgeQA, + }, + "override-chat": { + ID: "override-chat", + Type: types.ModelTypeKnowledgeQA, + }, + }, + }, + } + req := &types.QARequest{ + Session: &types.Session{}, + CustomAgent: &types.CustomAgent{ + ID: "agent-1", + Config: types.CustomAgentConfig{ + ModelID: "agent-chat", + }, + }, + SummaryModelID: "override-chat", + } + + modelID, err := svc.resolveChatModelID(context.Background(), req, nil, nil) + + require.NoError(t, err) + assert.Equal(t, "override-chat", modelID) +} diff --git a/internal/application/service/session_qa_helpers.go b/internal/application/service/session_qa_helpers.go index e56d18a1..8f0cc0e7 100644 --- a/internal/application/service/session_qa_helpers.go +++ b/internal/application/service/session_qa_helpers.go @@ -3,6 +3,7 @@ package service import ( "context" "fmt" + "strings" "github.com/Tencent/WeKnora/internal/logger" "github.com/Tencent/WeKnora/internal/types" @@ -67,7 +68,7 @@ func (s *sessionService) resolveChatModelID( session := req.Session if customAgent != nil { - configuredModelID := customAgent.Config.ModelID + configuredModelID := strings.TrimSpace(customAgent.Config.ModelID) if configuredModelID == "" { return "", fmt.Errorf("chat model is not configured: please set model_id on agent %s", customAgent.ID) } @@ -77,16 +78,18 @@ func (s *sessionService) resolveChatModelID( } } + summaryModelID = strings.TrimSpace(summaryModelID) if summaryModelID != "" { - if model, err := s.modelService.GetModelByID(ctx, summaryModelID); err == nil && model != nil { + if model, err := s.modelService.GetModelByID(ctx, summaryModelID); err == nil && model != nil && + model.Type == types.ModelTypeKnowledgeQA { logger.Infof(ctx, "Using request's summary model override: %s", summaryModelID) return summaryModelID, nil } logger.Warnf(ctx, "Request provided invalid summary model ID %s, falling back", summaryModelID) } - if customAgent != nil && customAgent.Config.ModelID != "" { - logger.Infof(ctx, "Using custom agent's model_id: %s", customAgent.Config.ModelID) - return customAgent.Config.ModelID, nil + if customAgent != nil && strings.TrimSpace(customAgent.Config.ModelID) != "" { + logger.Infof(ctx, "Using custom agent's model_id: %s", strings.TrimSpace(customAgent.Config.ModelID)) + return strings.TrimSpace(customAgent.Config.ModelID), nil } return s.selectChatModelID(ctx, session, knowledgeBaseIDs, knowledgeIDs) }