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.
This commit is contained in:
wizardchen
2026-06-25 21:42:13 +08:00
committed by lyingbug
parent 9ca4e1f23a
commit 47478491c9
8 changed files with 177 additions and 26 deletions
+25 -6
View File
@@ -41,7 +41,7 @@
<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: getAgentNotReadyLabels(agent).join('、') })"
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
placement="top"
>
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
@@ -66,7 +66,7 @@
<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: getAgentNotReadyLabels(agent).join('、') })"
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(agent) })"
placement="top"
>
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
@@ -95,7 +95,7 @@
class="agent-option-actions"
>
<t-tooltip
:content="$t('agent.selector.notReadyHint', { items: getAgentNotReadyLabels(shared.agent, String(shared.source_tenant_id)).join('、') })"
:content="$t('agent.selector.notReadyHint', { items: formatNotReadyHint(shared.agent, String(shared.source_tenant_id)) })"
placement="top"
>
<TIcon name="error-circle" size="14px" class="not-ready-icon" @click.stop />
@@ -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 } : {}),
},
});
};
+34 -9
View File
@@ -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({
<AgentSelector :visible="showAgentModeSelector" :anchorEl="agentModeButtonRef"
:currentAgentId="selectedAgentId" :agents="enabledAgents" :all-models="allModels"
@close="closeAgentModeSelector"
@select="handleSelectAgent" />
@select="handleSelectAgent"
@not-ready="handleAgentNotReady" />
<!-- WebSearch 开关按钮 -->
<t-tooltip placement="top" theme="light" :popupProps="{ overlayClassName: 'input-field-tooltip' }">
@@ -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'])
})
+12
View File
@@ -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(', ')
}
@@ -620,9 +620,9 @@
</div>
</div>
<!-- ReRank 模型(启用知识库时显示) -->
<!-- ReRank 模型(启用知识库或 knowledge_search 工具时显示) -->
<div
v-if="hasKnowledgeBase"
v-if="showRerankModelField"
class="setting-row"
data-agent-field="rerank_model"
:class="{ 'setting-row--field-highlight': highlightedField === 'rerank_model' }"
@@ -1394,7 +1394,7 @@ import {
type KBCapabilities,
} from '@/api/agent';
import { type ModelConfig } from '@/api/model';
import { type AgentNotReadyReasonKey } from '@/utils/agent-readiness';
import { type AgentNotReadyReasonKey, agentRequiresRerankModel } from '@/utils/agent-readiness';
import { type MCPService } from '@/api/mcp-service';
import { type SkillInfo } from '@/api/skill';
import { type WebSearchProviderEntity } from '@/api/web-search-provider';
@@ -1675,6 +1675,11 @@ const hasKnowledgeBase = computed(() => {
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 上的工具可用性判定,按库级来计算
+18 -3
View File
@@ -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()
}
},
@@ -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)
}
@@ -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)
}