feat: 添加技能提及功能,更新相关组件和状态管理

This commit is contained in:
Wenjie Zhang 2026-03-16 19:18:45 +08:00
parent ed76824191
commit 47cf5fac01
3 changed files with 128 additions and 34 deletions

View File

@ -218,7 +218,7 @@ import { useAgentStore } from '@/stores/agent'
import { useChatUIStore } from '@/stores/chatUI'
import { storeToRefs } from 'pinia'
import { MessageProcessor } from '@/utils/messageProcessor'
import { agentApi, threadApi, databaseApi, mcpApi } from '@/apis'
import { agentApi, threadApi } from '@/apis'
import HumanApprovalModal from '@/components/HumanApprovalModal.vue'
import { useApproval } from '@/composables/useApproval'
import { useAgentStreamHandler } from '@/composables/useAgentStreamHandler'
@ -242,7 +242,8 @@ const {
agentConfig,
configurableItems,
availableKnowledgeBases,
availableMcps
availableMcps,
availableSkills
} = storeToRefs(agentStore)
// ==================== LOCAL CHAT & UI STATE ====================
@ -421,6 +422,7 @@ const mentionConfig = computed(() => {
const currentConfig = agentConfig.value || {}
const allowedKbNames = new Set()
const allowedMcpNames = new Set()
const allowedSkillNames = new Set()
Object.entries(configItems).forEach(([key, item]) => {
const kind = item?.template_metadata?.kind
@ -431,19 +433,27 @@ const mentionConfig = computed(() => {
val.forEach((v) => allowedKbNames.add(v))
} else if (kind === 'mcps') {
val.forEach((v) => allowedMcpNames.add(v))
} else if (kind === 'skills' || key === 'skills') {
val.forEach((v) => allowedSkillNames.add(v))
}
}
})
const knowledgeBases = availableKnowledgeBases.value.filter((kb) => allowedKbNames.has(kb.name))
const mcps = availableMcps.value.filter((mcp) => allowedMcpNames.has(mcp.name))
const skills = availableSkills.value.filter((skill) => {
const skillName = skill.name || ''
const skillSlug = skill.slug || ''
return allowedSkillNames.has(skillName) || allowedSkillNames.has(skillSlug)
})
if (!files.length && !knowledgeBases.length && !mcps.length) return null
if (!files.length && !knowledgeBases.length && !mcps.length && !skills.length) return null
return {
files,
knowledgeBases,
mcps
mcps,
skills
}
})

View File

@ -88,6 +88,19 @@
</div>
</div>
<!-- Skills 列表 -->
<div v-if="mentionItems.skills.length > 0" class="mention-group">
<div class="mention-group-title">Skills</div>
<div
v-for="(item, index) in mentionItems.skills"
:key="'skill-' + item.value"
:class="['mention-item', { active: isItemSelected('skill', index) }]"
@click="insertMention(item)"
>
{{ item.label }}
</div>
</div>
<!-- 无结果 -->
<div v-if="!hasAnyItems" class="mention-empty">暂无可引用的项</div>
</div>
@ -197,14 +210,27 @@ const slots = useSlots()
// @
const mentionEnabled = computed(() => {
if (!props.mention) return false
const { files, knowledgeBases, mcps } = props.mention
const { files, knowledgeBases, mcps, skills } = props.mention
return (
(Array.isArray(files) && files.length > 0) ||
(Array.isArray(knowledgeBases) && knowledgeBases.length > 0) ||
(Array.isArray(mcps) && mcps.length > 0)
(Array.isArray(mcps) && mcps.length > 0) ||
(Array.isArray(skills) && skills.length > 0)
)
})
const mentionTypePrefixMap = {
file: 'file',
knowledge: 'knowledge',
mcp: 'mcp',
skill: 'skill'
}
const formatMentionToken = (type, value) => {
const prefix = mentionTypePrefixMap[type] || type
return `@${prefix}:${value}`
}
// @
const checkMentionTrigger = (textarea) => {
console.log(
@ -238,38 +264,80 @@ const checkMentionTrigger = (textarea) => {
//
const updateMentionItems = (query = '') => {
if (!props.mention) {
mentionItems.value = { files: [], knowledgeBases: [], mcps: [] }
mentionItems.value = { files: [], knowledgeBases: [], mcps: [], skills: [] }
return
}
const lowerQuery = query.toLowerCase()
const { files = [], knowledgeBases = [], mcps = [] } = props.mention
const { files = [], knowledgeBases = [], mcps = [], skills = [] } = props.mention
const filter = (list) =>
const filterItems = (list) =>
list.filter((item) => {
const label = item.path || item.name || ''
return label.toLowerCase().includes(lowerQuery)
const searchTexts = [
item.label,
item.value,
item.description,
item.tokenLabel,
item.type,
mentionTypePrefixMap[item.type]
]
return searchTexts.some((text) => String(text || '').toLowerCase().includes(lowerQuery))
})
mentionItems.value = {
files: filter(files).map((f) => ({
value: f.path,
label: f.path.split('/').pop() || f.path,
const fileItems = files.map((f) => {
const path = f.path || ''
const fileName = path.split('/').pop() || path
return {
value: path,
label: fileName,
type: 'file',
description: f.path
})),
knowledgeBases: filter(knowledgeBases).map((kb) => ({
value: kb.name,
label: kb.name,
insertValue: path || fileName,
tokenLabel: formatMentionToken('file', fileName),
description: path
}
})
const knowledgeItems = knowledgeBases.map((kb) => {
const kbName = kb.name || ''
return {
value: kbName,
label: kbName,
type: 'knowledge',
insertValue: kbName,
tokenLabel: formatMentionToken('knowledge', kbName),
description: kb.db_id
})),
mcps: filter(mcps).map((m) => ({
value: m.name,
label: m.name,
}
})
const mcpItems = mcps.map((m) => {
const mcpName = m.name || ''
return {
value: mcpName,
label: mcpName,
type: 'mcp',
insertValue: mcpName,
tokenLabel: formatMentionToken('mcp', mcpName),
description: m.description || ''
}))
}
})
const skillItems = skills.map((skill) => {
const skillValue = skill.slug || skill.name || skill.id || ''
return {
value: skillValue,
label: skillValue,
type: 'skill',
insertValue: skillValue,
tokenLabel: formatMentionToken('skill', skillValue),
description: skill.description || ''
}
})
mentionItems.value = {
files: filterItems(fileItems),
knowledgeBases: filterItems(knowledgeItems),
mcps: filterItems(mcpItems),
skills: filterItems(skillItems)
}
}
@ -279,20 +347,28 @@ const isItemSelected = (type, index) => {
const filesLen = mentionItems.value.files.length
const kbLen = mentionItems.value.knowledgeBases.length
const mcpLen = mentionItems.value.mcps.length
if (type === 'file') {
return mentionSelectedIndex.value === index
} else if (type === 'knowledge') {
return mentionSelectedIndex.value === filesLen + index
} else {
} else if (type === 'mcp') {
return mentionSelectedIndex.value === filesLen + kbLen + index
} else {
return mentionSelectedIndex.value === filesLen + kbLen + mcpLen + index
}
}
//
const hasAnyItems = computed(() => {
const items = mentionItems.value
return items.files.length > 0 || items.knowledgeBases.length > 0 || items.mcps.length > 0
return (
items.files.length > 0 ||
items.knowledgeBases.length > 0 ||
items.mcps.length > 0 ||
items.skills.length > 0
)
})
//
@ -316,9 +392,11 @@ const insertMention = (item) => {
const textarea = inputRef.value
const cursorPos = textarea.selectionStart
const textBeforeCursor = inputValue.value.slice(0, cursorPos)
const mentionValue = item.insertValue || item.value
const mentionText = formatMentionToken(item.type, mentionValue)
// @
const newTextBefore = textBeforeCursor.replace(/@(\S*)$/, `@${item.value} `)
const newTextBefore = textBeforeCursor.replace(/@(\S*)$/, `${mentionText} `)
const textAfterCursor = inputValue.value.slice(cursorPos)
const newValue = newTextBefore + textAfterCursor
@ -366,7 +444,8 @@ const handleMentionNavigation = (e) => {
const allItems = [
...mentionItems.value.files,
...mentionItems.value.knowledgeBases,
...mentionItems.value.mcps
...mentionItems.value.mcps,
...mentionItems.value.skills
]
const total = allItems.length
@ -499,7 +578,7 @@ const singleLineWidth = ref(0)
// @
const mentionPopupVisible = ref(false)
const mentionQuery = ref('')
const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [] })
const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [], skills: [] })
const mentionSelectedIndex = ref(0)
//

View File

@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { agentApi, databaseApi, mcpApi } from '@/apis'
import { agentApi, databaseApi, mcpApi, skillApi } from '@/apis'
import { handleChatError } from '@/utils/errorHandler'
import { useUserStore } from '@/stores/user'
@ -17,6 +17,7 @@ export const useAgentStore = defineStore(
// 资源相关状态
const availableKnowledgeBases = ref([])
const availableMcps = ref([])
const availableSkills = ref([])
// 智能体配置相关状态
const agentConfig = ref({})
@ -96,16 +97,18 @@ export const useAgentStore = defineStore(
// ==================== 方法 ====================
/**
* 获取可提及的资源知识库MCP
* 获取可提及的资源知识库MCPSkills
*/
async function fetchMentionResources() {
try {
const [dbsRes, mcpsRes] = await Promise.all([
const [dbsRes, mcpsRes, skillsRes] = await Promise.all([
databaseApi.getAccessibleDatabases().catch(() => ({ databases: [] })),
mcpApi.getMcpServers().catch(() => ({ data: [] }))
mcpApi.getMcpServers().catch(() => ({ data: [] })),
skillApi.listSkills().catch(() => ({ data: [] }))
])
availableKnowledgeBases.value = dbsRes.databases || []
availableMcps.value = mcpsRes.data || []
availableSkills.value = skillsRes.data || []
} catch (e) {
console.warn('Failed to fetch mention resources:', e)
}
@ -472,6 +475,7 @@ export const useAgentStore = defineStore(
defaultAgentId.value = null
availableKnowledgeBases.value = []
availableMcps.value = []
availableSkills.value = []
agentConfig.value = {}
originalAgentConfig.value = {}
agentConfigs.value = {}
@ -493,6 +497,7 @@ export const useAgentStore = defineStore(
defaultAgentId,
availableKnowledgeBases,
availableMcps,
availableSkills,
agentConfig,
originalAgentConfig,
agentDetails,