fix(integrations): polish agent filter UX and embed session rebind
Add compact agent filter beside channel list titles with selected name display, fix embed localStorage agentId read on rebind, add IM agent rebind tests, and deep-link agent editor counts to the matching tab.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
<div class="channels-section">
|
||||
<div class="channels-header">
|
||||
<span class="channels-title">{{ $t('embedPublish.channelsTitle') }}</span>
|
||||
<IntegrationsAgentFilter v-model="filterAgentId" :agents="agents" />
|
||||
<span class="channels-count">{{ channels.length }}</span>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +30,7 @@
|
||||
{{ $t('embedPublish.disabled') }}
|
||||
</t-tag>
|
||||
</div>
|
||||
<span v-if="tenantMode && agentDisplayName(ch)" class="channel-card__agent-name">
|
||||
<span v-if="agentDisplayName(ch)" class="channel-card__agent-name">
|
||||
{{ agentDisplayName(ch) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -135,7 +136,7 @@
|
||||
<section class="setting-drawer__section embed-drawer__section">
|
||||
<h4 class="setting-drawer__section-title">{{ $t('embedPublish.sectionChannel') }}</h4>
|
||||
|
||||
<div v-if="tenantMode" class="form-item">
|
||||
<div class="form-item">
|
||||
<label class="form-label required">{{ $t('integrations.boundAgent') }}</label>
|
||||
<div class="agent-field-row">
|
||||
<t-select
|
||||
@@ -542,14 +543,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, reactive, ref, watch } from 'vue'
|
||||
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import SettingDrawer from '@/components/settings/SettingDrawer.vue'
|
||||
import EmbedChannelPreview from '@/components/EmbedChannelPreview.vue'
|
||||
import {
|
||||
listEmbedChannels,
|
||||
listAllEmbedChannels,
|
||||
createEmbedChannel,
|
||||
updateEmbedChannel,
|
||||
@@ -575,16 +575,9 @@ import {
|
||||
type AllowedOriginsValidationError,
|
||||
} from '@/utils/embedAllowedOrigins'
|
||||
import { listAgents, type CustomAgent } from '@/api/agent'
|
||||
import IntegrationsAgentFilter from '@/components/IntegrationsAgentFilter.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
agentId?: string
|
||||
tenantMode?: boolean
|
||||
initialAgentId?: string
|
||||
agentWebSearchEnabled?: boolean
|
||||
agentImageUploadEnabled?: boolean
|
||||
}>(), {
|
||||
tenantMode: false,
|
||||
})
|
||||
const filterAgentId = defineModel<string>('filterAgentId', { default: '' })
|
||||
|
||||
const { t } = useI18n()
|
||||
const authStore = useAuthStore()
|
||||
@@ -592,7 +585,12 @@ const isAdmin = computed(() => authStore.hasRole('admin'))
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const channels = ref<EmbedChannel[]>([])
|
||||
const allChannels = ref<EmbedChannel[]>([])
|
||||
const channels = computed(() => {
|
||||
const filter = filterAgentId.value?.trim()
|
||||
if (!filter) return allChannels.value
|
||||
return allChannels.value.filter((ch) => ch.agent_id === filter)
|
||||
})
|
||||
const tokenByChannel = ref<Record<string, string>>({})
|
||||
const revealedTokens = reactive<Record<string, boolean>>({})
|
||||
const previewVisible = ref(false)
|
||||
@@ -621,7 +619,6 @@ const agentOptions = computed(() =>
|
||||
)
|
||||
|
||||
const drawerAgentId = computed(() => {
|
||||
if (!props.tenantMode) return props.agentId || ''
|
||||
if (editingId.value) {
|
||||
return drawerChannel.value?.agent_id || ''
|
||||
}
|
||||
@@ -632,19 +629,13 @@ const drawerAgent = computed(() =>
|
||||
agents.value.find((agent) => agent.id === drawerAgentId.value),
|
||||
)
|
||||
|
||||
const agentWebSearchEnabledEffective = computed(() => {
|
||||
if (props.tenantMode) {
|
||||
return drawerAgent.value?.config?.web_search_enabled === true
|
||||
}
|
||||
return props.agentWebSearchEnabled === true
|
||||
})
|
||||
const agentWebSearchEnabledEffective = computed(() =>
|
||||
drawerAgent.value?.config?.web_search_enabled === true,
|
||||
)
|
||||
|
||||
const agentImageUploadEnabledEffective = computed(() => {
|
||||
if (props.tenantMode) {
|
||||
return drawerAgent.value?.config?.image_upload_enabled === true
|
||||
}
|
||||
return props.agentImageUploadEnabled === true
|
||||
})
|
||||
const agentImageUploadEnabledEffective = computed(() =>
|
||||
drawerAgent.value?.config?.image_upload_enabled === true,
|
||||
)
|
||||
|
||||
const EMBED_TOKEN_STORAGE = 'weknora_embed_publish_tokens'
|
||||
const WEKNORA_BRAND_COLOR = '#07C05F'
|
||||
@@ -724,9 +715,7 @@ function handleChannelMenuClick(data: { value?: string }, ch: EmbedChannel) {
|
||||
const drawerTitle = computed(() => {
|
||||
if (!editingId.value) return t('embedPublish.createTitle')
|
||||
const ch = drawerChannel.value
|
||||
const agentId = props.tenantMode
|
||||
? (createAgentId.value || ch?.agent_id || '')
|
||||
: (ch?.agent_id || props.agentId || '')
|
||||
const agentId = createAgentId.value || ch?.agent_id || ''
|
||||
return channelDisplayName({
|
||||
id: editingId.value,
|
||||
name: form.value.name,
|
||||
@@ -734,7 +723,7 @@ const drawerTitle = computed(() => {
|
||||
} as EmbedChannel)
|
||||
})
|
||||
|
||||
function defaultEmbedChannelName(agentId = props.tenantMode ? createAgentId.value : props.agentId): string {
|
||||
function defaultEmbedChannelName(agentId = createAgentId.value): string {
|
||||
const agent = agentId ? agents.value.find((item) => item.id === agentId) : undefined
|
||||
if (agent?.name?.trim()) {
|
||||
return t('embedPublish.defaultChannelNameWithAgent', { agent: agent.name.trim() })
|
||||
@@ -776,7 +765,7 @@ const drawerConfirmText = computed(() =>
|
||||
)
|
||||
|
||||
function validateWizardStep(step: number): boolean {
|
||||
if (step === 0 && props.tenantMode && !createAgentId.value) {
|
||||
if (step === 0 && !createAgentId.value) {
|
||||
MessagePlugin.warning(t('integrations.selectAgentHint'))
|
||||
return false
|
||||
}
|
||||
@@ -845,22 +834,16 @@ const storeToken = (channelId: string, token: string) => {
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
if (!props.tenantMode && !props.agentId) return
|
||||
loading.value = true
|
||||
try {
|
||||
if (props.tenantMode) {
|
||||
const [res, agentRes] = await Promise.all([
|
||||
listAllEmbedChannels(),
|
||||
listAgents(),
|
||||
])
|
||||
channels.value = res?.data || []
|
||||
agents.value = agentRes?.data || []
|
||||
} else {
|
||||
const res = await listEmbedChannels(props.agentId!)
|
||||
channels.value = res?.data || []
|
||||
}
|
||||
const [res, agentRes] = await Promise.all([
|
||||
listAllEmbedChannels(),
|
||||
listAgents(),
|
||||
])
|
||||
allChannels.value = res?.data || []
|
||||
agents.value = agentRes?.data || []
|
||||
tokenByChannel.value = { ...loadStoredTokens(), ...tokenByChannel.value }
|
||||
await Promise.all(channels.value.map(async (ch) => {
|
||||
await Promise.all(allChannels.value.map(async (ch) => {
|
||||
try {
|
||||
const statsRes = await getEmbedChannelStats(ch.id)
|
||||
if (statsRes?.data?.session_count != null) {
|
||||
@@ -875,9 +858,15 @@ const load = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => [props.tenantMode, props.agentId], () => {
|
||||
if (props.tenantMode || props.agentId) load()
|
||||
}, { immediate: true })
|
||||
watch(filterAgentId, (id) => {
|
||||
if (!showDrawer.value && !editingId.value && id) {
|
||||
createAgentId.value = id
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void load()
|
||||
})
|
||||
|
||||
watch(stepTitles, (titles) => {
|
||||
if (wizardStep.value >= titles.length) {
|
||||
@@ -1010,7 +999,7 @@ const openCreate = () => {
|
||||
channelNameTouched.value = false
|
||||
editingId.value = ''
|
||||
editingEnabled.value = true
|
||||
createAgentId.value = props.tenantMode ? (props.initialAgentId || '') : ''
|
||||
createAgentId.value = filterAgentId.value || ''
|
||||
form.value = defaultForm()
|
||||
applyDefaultChannelNameIfNeeded()
|
||||
originsText.value = ''
|
||||
@@ -1056,7 +1045,7 @@ const mapOriginsApiError = (message: string): string | null => {
|
||||
|
||||
const saveForm = async () => {
|
||||
if (!isAdmin.value) return
|
||||
if (props.tenantMode && !createAgentId.value) {
|
||||
if (!createAgentId.value) {
|
||||
MessagePlugin.warning(t('integrations.selectAgentHint'))
|
||||
return
|
||||
}
|
||||
@@ -1086,7 +1075,7 @@ const saveForm = async () => {
|
||||
webhook_url: form.value.webhook_url || '',
|
||||
webhook_secret: form.value.webhook_secret || undefined,
|
||||
enabled: editingId.value ? editingEnabled.value : true,
|
||||
...(props.tenantMode ? { agent_id: createAgentId.value } : {}),
|
||||
agent_id: createAgentId.value,
|
||||
}
|
||||
if (editingId.value) {
|
||||
const previousAgentId = drawerChannel.value?.agent_id
|
||||
@@ -1106,7 +1095,7 @@ const saveForm = async () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const targetAgentId = props.tenantMode ? createAgentId.value : props.agentId
|
||||
const targetAgentId = createAgentId.value
|
||||
if (!targetAgentId) {
|
||||
MessagePlugin.warning(t('integrations.selectAgentHint'))
|
||||
return
|
||||
@@ -1157,9 +1146,7 @@ async function openPreviewForChannel(
|
||||
) {
|
||||
previewLoading.value = true
|
||||
try {
|
||||
const agentId = props.tenantMode
|
||||
? ((opts?.useDraft ? createAgentId.value : '') || ch.agent_id)
|
||||
: ch.agent_id
|
||||
const agentId = ((opts?.useDraft ? createAgentId.value : '') || ch.agent_id)
|
||||
if (agentId) {
|
||||
clearEmbedStoredChatSessionIfAgentMismatch(ch.id, agentId)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<div class="channels-section">
|
||||
<div class="channels-header">
|
||||
<span class="channels-title">{{ $t('agentEditor.im.channelsTitle') }}</span>
|
||||
<IntegrationsAgentFilter v-model="filterAgentId" :agents="agents" />
|
||||
<span class="channels-count">{{ channels.length }}</span>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +36,7 @@
|
||||
{{ $t('agentEditor.im.disabled') }}
|
||||
</t-tag>
|
||||
</div>
|
||||
<span v-if="tenantMode && agentDisplayName(channel)" class="channel-card__agent-name">
|
||||
<span v-if="agentDisplayName(channel)" class="channel-card__agent-name">
|
||||
{{ agentDisplayName(channel) }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -148,7 +149,7 @@
|
||||
<section class="setting-drawer__section im-drawer__section">
|
||||
<h4 class="setting-drawer__section-title">{{ $t('agentEditor.im.sectionChannel') }}</h4>
|
||||
|
||||
<div v-if="tenantMode" class="form-item">
|
||||
<div class="form-item">
|
||||
<label class="form-label required">{{ $t('integrations.boundAgent') }}</label>
|
||||
<div class="agent-field-row">
|
||||
<t-select
|
||||
@@ -595,6 +596,7 @@ import { useChatResourcesStore } from '@/stores/chatResources';
|
||||
import type { IMChannel } from '@/api/agent';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import SettingDrawer from '@/components/settings/SettingDrawer.vue';
|
||||
import IntegrationsAgentFilter from '@/components/IntegrationsAgentFilter.vue';
|
||||
import wecomLogo from '@/assets/img/im/wecom.svg';
|
||||
import feishuLogo from '@/assets/img/im/feishu.svg';
|
||||
import slackLogo from '@/assets/img/im/slack.svg';
|
||||
@@ -620,20 +622,19 @@ const platformLogo = (platform: string): string => (platform ? PLATFORM_LOGO[pla
|
||||
const { t } = useI18n();
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
agentId?: string;
|
||||
tenantMode?: boolean;
|
||||
initialAgentId?: string;
|
||||
}>(), {
|
||||
tenantMode: false,
|
||||
});
|
||||
const filterAgentId = defineModel<string>('filterAgentId', { default: '' });
|
||||
|
||||
const agents = ref<CustomAgent[]>([]);
|
||||
const agentOptions = computed(() =>
|
||||
agents.value.map((agent) => ({ label: agent.name, value: agent.id })),
|
||||
);
|
||||
|
||||
const channels = ref<Array<IMChannel | IMChannelOverview>>([]);
|
||||
const allChannels = ref<Array<IMChannel | IMChannelOverview>>([]);
|
||||
const channels = computed(() => {
|
||||
const filter = filterAgentId.value?.trim();
|
||||
if (!filter) return allChannels.value;
|
||||
return allChannels.value.filter((channel) => channel.agent_id === filter);
|
||||
});
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const showCreateDialog = ref(false);
|
||||
@@ -673,7 +674,7 @@ const drawerTitle = computed(() => {
|
||||
});
|
||||
|
||||
function validateWizardStep(step: number): boolean {
|
||||
if (step === 0 && props.tenantMode && !formData.value.target_agent_id) {
|
||||
if (step === 0 && !formData.value.target_agent_id) {
|
||||
MessagePlugin.warning(t('integrations.selectAgentHint'));
|
||||
return false;
|
||||
}
|
||||
@@ -886,26 +887,16 @@ async function loadChannels() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const chatResources = useChatResourcesStore();
|
||||
if (props.tenantMode) {
|
||||
const [channelRes, agentRes] = await Promise.all([
|
||||
listAllIMChannels(),
|
||||
listAgents(),
|
||||
chatResources.ensureKnowledgeBases(),
|
||||
]);
|
||||
channels.value = channelRes.data || [];
|
||||
agents.value = agentRes?.data || [];
|
||||
} else if (props.agentId) {
|
||||
const [channelRes] = await Promise.all([
|
||||
listIMChannels(props.agentId),
|
||||
chatResources.ensureKnowledgeBases(),
|
||||
]);
|
||||
channels.value = channelRes.data || [];
|
||||
} else {
|
||||
channels.value = [];
|
||||
}
|
||||
const [channelRes, agentRes] = await Promise.all([
|
||||
listAllIMChannels(),
|
||||
listAgents(),
|
||||
chatResources.ensureKnowledgeBases(),
|
||||
]);
|
||||
allChannels.value = channelRes.data || [];
|
||||
agents.value = agentRes?.data || [];
|
||||
knowledgeBases.value = chatResources.rawKnowledgeBases.map((kb: any) => ({ id: kb.id, name: kb.name }));
|
||||
} catch {
|
||||
channels.value = [];
|
||||
allChannels.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -940,8 +931,8 @@ async function copyUrl(channel: IMChannel) {
|
||||
|
||||
function openCreate() {
|
||||
resetForm();
|
||||
if (props.tenantMode && props.initialAgentId) {
|
||||
formData.value.target_agent_id = props.initialAgentId;
|
||||
if (filterAgentId.value) {
|
||||
formData.value.target_agent_id = filterAgentId.value;
|
||||
}
|
||||
showCreateDialog.value = true;
|
||||
}
|
||||
@@ -953,7 +944,7 @@ function openDrawer(channel: IMChannel | IMChannelOverview) {
|
||||
async function editChannel(channel: IMChannel | IMChannelOverview) {
|
||||
wizardStep.value = 0;
|
||||
let fullChannel: IMChannel | null = null;
|
||||
if (props.tenantMode && !('credentials' in channel)) {
|
||||
if (!('credentials' in channel)) {
|
||||
try {
|
||||
const res = await listIMChannels(channel.agent_id);
|
||||
fullChannel = (res.data || []).find((item) => item.id === channel.id) || null;
|
||||
@@ -994,7 +985,7 @@ function resetForm() {
|
||||
wechatQRCode.value = '';
|
||||
wechatQRStatus.value = '';
|
||||
formData.value = {
|
||||
target_agent_id: props.tenantMode ? (props.initialAgentId || '') : '',
|
||||
target_agent_id: filterAgentId.value || '',
|
||||
platform: 'wecom',
|
||||
name: defaultChannelName('wecom'),
|
||||
mode: 'websocket',
|
||||
@@ -1023,13 +1014,11 @@ async function handleSave() {
|
||||
knowledge_base_id: formData.value.knowledge_base_id,
|
||||
credentials: formData.value.credentials,
|
||||
enabled: editingEnabled.value,
|
||||
...(props.tenantMode && formData.value.target_agent_id
|
||||
? { agent_id: formData.value.target_agent_id }
|
||||
: {}),
|
||||
...(formData.value.target_agent_id ? { agent_id: formData.value.target_agent_id } : {}),
|
||||
});
|
||||
MessagePlugin.success(t('common.updateSuccess'));
|
||||
} else {
|
||||
const targetAgentId = props.tenantMode ? formData.value.target_agent_id : props.agentId;
|
||||
const targetAgentId = formData.value.target_agent_id;
|
||||
if (!targetAgentId) {
|
||||
MessagePlugin.warning(t('integrations.selectAgentHint'));
|
||||
return;
|
||||
@@ -1076,19 +1065,14 @@ async function handleDelete(id: string) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.tenantMode || props.agentId) {
|
||||
loadChannels();
|
||||
}
|
||||
loadChannels();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.tenantMode, props.agentId],
|
||||
() => {
|
||||
if (props.tenantMode || props.agentId) {
|
||||
loadChannels();
|
||||
}
|
||||
},
|
||||
);
|
||||
watch(filterAgentId, (id) => {
|
||||
if (!showCreateDialog.value && !editingChannel.value && id) {
|
||||
formData.value.target_agent_id = id;
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
stopWeChatPolling();
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<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-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" />
|
||||
</button>
|
||||
</t-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { CustomAgent } from '@/api/agent'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string
|
||||
agents: CustomAgent[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const selectedAgentName = computed(() => {
|
||||
if (!props.modelValue) return ''
|
||||
return props.agents.find((item) => item.id === props.modelValue)?.name || ''
|
||||
})
|
||||
|
||||
const ariaLabel = computed(() =>
|
||||
selectedAgentName.value
|
||||
? t('integrations.filterByAgentWithName', { name: selectedAgentName.value })
|
||||
: t('integrations.filterByAgent'),
|
||||
)
|
||||
|
||||
const options = computed(() => [
|
||||
{ content: t('integrations.filterAllAgents'), value: '' },
|
||||
...props.agents.map((agent) => ({
|
||||
content: agent.name,
|
||||
value: agent.id,
|
||||
active: agent.id === props.modelValue,
|
||||
})),
|
||||
])
|
||||
|
||||
function onSelect(data: { value?: string }) {
|
||||
emit('update:modelValue', data?.value || '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.integrations-agent-filter {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 220px;
|
||||
padding: 2px 6px 2px 4px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--td-text-color-placeholder);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--td-bg-color-container-hover);
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
&--active {
|
||||
color: var(--td-brand-color);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
|
||||
&:hover {
|
||||
color: var(--td-brand-color);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&__chevron {
|
||||
flex-shrink: 0;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,7 +33,12 @@ function readStoredSession(channelId: string): StoredSession | null {
|
||||
if (!raw) return null
|
||||
const parsed = JSON.parse(raw)
|
||||
if (parsed && typeof parsed.id === 'string' && typeof parsed.sig === 'string' && parsed.id) {
|
||||
return { id: parsed.id, sig: parsed.sig }
|
||||
const agentId = typeof parsed.agentId === 'string' ? parsed.agentId.trim() : ''
|
||||
return {
|
||||
id: parsed.id,
|
||||
sig: parsed.sig,
|
||||
...(agentId ? { agentId } : {}),
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Malformed / legacy (plain-string) entry: ignore so a fresh signed
|
||||
|
||||
@@ -5615,6 +5615,9 @@ export default {
|
||||
selectAgentPlaceholder: 'Choose an agent',
|
||||
selectAgentHint: 'Please select an agent first',
|
||||
boundAgent: 'Bound agent',
|
||||
filterByAgent: 'Filter by agent',
|
||||
filterByAgentWithName: 'Filter by agent: {name}',
|
||||
filterAllAgents: 'All agents',
|
||||
manageChannel: 'Manage channel',
|
||||
goToCenter: 'Open Integrations',
|
||||
imOverview: {
|
||||
|
||||
@@ -5626,6 +5626,9 @@ export default {
|
||||
selectAgentPlaceholder: "에이전트를 선택하세요",
|
||||
selectAgentHint: "먼저 에이전트를 선택하세요",
|
||||
boundAgent: "연결된 에이전트",
|
||||
filterByAgent: "에이전트별 필터",
|
||||
filterByAgentWithName: "에이전트별 필터: {name}",
|
||||
filterAllAgents: "전체 에이전트",
|
||||
manageChannel: "채널 관리",
|
||||
goToCenter: "통합 센터로 이동",
|
||||
imOverview: {
|
||||
|
||||
@@ -5448,6 +5448,9 @@ export default {
|
||||
selectAgentPlaceholder: 'Выберите агента',
|
||||
selectAgentHint: 'Сначала выберите агента',
|
||||
boundAgent: 'Привязанный агент',
|
||||
filterByAgent: 'Фильтр по агенту',
|
||||
filterByAgentWithName: 'Фильтр по агенту: {name}',
|
||||
filterAllAgents: 'Все агенты',
|
||||
manageChannel: 'Управлять каналом',
|
||||
goToCenter: 'Открыть интеграции',
|
||||
imOverview: {
|
||||
|
||||
@@ -5633,6 +5633,9 @@ export default {
|
||||
selectAgentPlaceholder: "请选择智能体",
|
||||
selectAgentHint: "请先选择一个智能体",
|
||||
boundAgent: "绑定智能体",
|
||||
filterByAgent: "按智能体筛选",
|
||||
filterByAgentWithName: "按智能体筛选:{name}",
|
||||
filterAllAgents: "全部智能体",
|
||||
manageChannel: "管理渠道",
|
||||
goToCenter: "前往集成中心",
|
||||
imOverview: {
|
||||
|
||||
@@ -79,15 +79,13 @@
|
||||
</div>
|
||||
<div class="setting-control">
|
||||
<div class="integration-inline">
|
||||
<span class="integration-inline__stat">
|
||||
{{ $t('integrations.tabs.im') }} · {{ agentIMChannelCount }}
|
||||
</span>
|
||||
<button type="button" class="integration-inline__stat integration-inline__link" @click="gotoIntegrations('im')">
|
||||
<span>{{ $t('integrations.tabs.im') }} · {{ agentIMChannelCount }}</span>
|
||||
<t-icon name="chevron-right" size="14px" />
|
||||
</button>
|
||||
<span class="integration-inline__sep" aria-hidden="true">|</span>
|
||||
<span class="integration-inline__stat">
|
||||
{{ $t('integrations.tabs.embed') }} · {{ agentEmbedChannelCount }}
|
||||
</span>
|
||||
<button type="button" class="integration-inline__link" @click="gotoIntegrations">
|
||||
<span>{{ $t('integrations.agentEditor.manage') }}</span>
|
||||
<button type="button" class="integration-inline__stat integration-inline__link" @click="gotoIntegrations('embed')">
|
||||
<span>{{ $t('integrations.tabs.embed') }} · {{ agentEmbedChannelCount }}</span>
|
||||
<t-icon name="chevron-right" size="14px" />
|
||||
</button>
|
||||
</div>
|
||||
@@ -2084,11 +2082,11 @@ async function loadAgentIntegrationCounts(agentId: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function gotoIntegrations() {
|
||||
function gotoIntegrations(tab: 'im' | 'embed') {
|
||||
const agentId = editorAgent.value?.id;
|
||||
if (!agentId) return;
|
||||
handleClose();
|
||||
router.push({ path: '/platform/integrations', query: { agentId } });
|
||||
router.push({ path: '/platform/integrations', query: { agentId, tab } });
|
||||
}
|
||||
|
||||
const filteredIntentPlaceholders = computed(() => {
|
||||
@@ -4285,6 +4283,22 @@ const handleSave = async () => {
|
||||
&__stat {
|
||||
font-size: 13px;
|
||||
color: var(--td-text-color-secondary);
|
||||
|
||||
&.integration-inline__link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
line-height: 1;
|
||||
color: var(--td-brand-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__sep {
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<IMChannelPanel tenant-mode :initial-agent-id="initialAgentId" />
|
||||
<IMChannelPanel v-model:filter-agent-id="filterAgentId" />
|
||||
</div>
|
||||
|
||||
<div v-if="currentSection === 'embed'" class="section">
|
||||
@@ -54,7 +54,7 @@
|
||||
<h2>{{ $t('agentEditor.embed.title') }}</h2>
|
||||
<p class="section-description">{{ $t('agentEditor.embed.description') }}</p>
|
||||
</div>
|
||||
<AgentEmbedChannelPanel tenant-mode :initial-agent-id="initialAgentId" />
|
||||
<AgentEmbedChannelPanel v-model:filter-agent-id="filterAgentId" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,7 +77,7 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const currentSection = ref<'im' | 'embed'>('im');
|
||||
const initialAgentId = ref('');
|
||||
const filterAgentId = ref('');
|
||||
|
||||
const visible = computed(() => route.name === 'integrations');
|
||||
|
||||
@@ -91,13 +91,13 @@ function applyRouteQuery() {
|
||||
if (tab === 'im' || tab === 'embed') {
|
||||
currentSection.value = tab;
|
||||
}
|
||||
initialAgentId.value = (route.query.agentId as string) || '';
|
||||
filterAgentId.value = (route.query.agentId as string) || '';
|
||||
}
|
||||
|
||||
function syncRouteQuery() {
|
||||
const query: Record<string, string> = { tab: currentSection.value };
|
||||
if (initialAgentId.value) {
|
||||
query.agentId = initialAgentId.value;
|
||||
if (filterAgentId.value) {
|
||||
query.agentId = filterAgentId.value;
|
||||
}
|
||||
router.replace({ path: route.path, query });
|
||||
}
|
||||
@@ -119,6 +119,10 @@ watch(currentSection, () => {
|
||||
if (visible.value) syncRouteQuery();
|
||||
});
|
||||
|
||||
watch(filterAgentId, () => {
|
||||
if (visible.value) syncRouteQuery();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [route.query.tab, route.query.agentId],
|
||||
() => {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/Tencent/WeKnora/internal/types"
|
||||
"github.com/Tencent/WeKnora/internal/types/interfaces"
|
||||
)
|
||||
|
||||
type stubIMAgentService struct {
|
||||
interfaces.CustomAgentService
|
||||
agent *types.CustomAgent
|
||||
}
|
||||
|
||||
func (s *stubIMAgentService) GetAgentByID(_ context.Context, id string) (*types.CustomAgent, error) {
|
||||
if s.agent != nil && s.agent.ID == id {
|
||||
return s.agent, nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func TestSetChannelAgentID(t *testing.T) {
|
||||
svc := &Service{
|
||||
agentService: &stubIMAgentService{
|
||||
agent: &types.CustomAgent{ID: "agent-new", TenantID: 42},
|
||||
},
|
||||
}
|
||||
channel := &IMChannel{
|
||||
ID: "im-1",
|
||||
TenantID: 42,
|
||||
AgentID: "agent-old",
|
||||
}
|
||||
|
||||
if err := svc.SetChannelAgentID(context.Background(), channel, "agent-new"); err != nil {
|
||||
t.Fatalf("SetChannelAgentID() error = %v", err)
|
||||
}
|
||||
if channel.AgentID != "agent-new" {
|
||||
t.Fatalf("channel.AgentID = %q, want agent-new", channel.AgentID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChannelAgentIDRejectsForeignTenant(t *testing.T) {
|
||||
svc := &Service{
|
||||
agentService: &stubIMAgentService{
|
||||
agent: &types.CustomAgent{ID: "agent-new", TenantID: 99},
|
||||
},
|
||||
}
|
||||
channel := &IMChannel{
|
||||
ID: "im-1",
|
||||
TenantID: 42,
|
||||
AgentID: "agent-old",
|
||||
}
|
||||
|
||||
if err := svc.SetChannelAgentID(context.Background(), channel, "agent-new"); err == nil {
|
||||
t.Fatal("SetChannelAgentID() expected error for foreign tenant agent")
|
||||
}
|
||||
if channel.AgentID != "agent-old" {
|
||||
t.Fatalf("channel.AgentID = %q, want unchanged agent-old", channel.AgentID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetChannelAgentIDRequiresAgentID(t *testing.T) {
|
||||
svc := &Service{agentService: &stubIMAgentService{}}
|
||||
channel := &IMChannel{ID: "im-1", TenantID: 42, AgentID: "agent-old"}
|
||||
|
||||
if err := svc.SetChannelAgentID(context.Background(), channel, " "); err == nil {
|
||||
t.Fatal("SetChannelAgentID() expected error for empty agent_id")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user