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

View File

@ -88,6 +88,19 @@
</div> </div>
</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 v-if="!hasAnyItems" class="mention-empty">暂无可引用的项</div>
</div> </div>
@ -197,14 +210,27 @@ const slots = useSlots()
// @ // @
const mentionEnabled = computed(() => { const mentionEnabled = computed(() => {
if (!props.mention) return false if (!props.mention) return false
const { files, knowledgeBases, mcps } = props.mention const { files, knowledgeBases, mcps, skills } = props.mention
return ( return (
(Array.isArray(files) && files.length > 0) || (Array.isArray(files) && files.length > 0) ||
(Array.isArray(knowledgeBases) && knowledgeBases.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) => { const checkMentionTrigger = (textarea) => {
console.log( console.log(
@ -238,38 +264,80 @@ const checkMentionTrigger = (textarea) => {
// //
const updateMentionItems = (query = '') => { const updateMentionItems = (query = '') => {
if (!props.mention) { if (!props.mention) {
mentionItems.value = { files: [], knowledgeBases: [], mcps: [] } mentionItems.value = { files: [], knowledgeBases: [], mcps: [], skills: [] }
return return
} }
const lowerQuery = query.toLowerCase() 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) => { list.filter((item) => {
const label = item.path || item.name || '' const searchTexts = [
return label.toLowerCase().includes(lowerQuery) item.label,
item.value,
item.description,
item.tokenLabel,
item.type,
mentionTypePrefixMap[item.type]
]
return searchTexts.some((text) => String(text || '').toLowerCase().includes(lowerQuery))
}) })
mentionItems.value = { const fileItems = files.map((f) => {
files: filter(files).map((f) => ({ const path = f.path || ''
value: f.path, const fileName = path.split('/').pop() || path
label: f.path.split('/').pop() || f.path, return {
value: path,
label: fileName,
type: 'file', type: 'file',
description: f.path insertValue: path || fileName,
})), tokenLabel: formatMentionToken('file', fileName),
knowledgeBases: filter(knowledgeBases).map((kb) => ({ description: path
value: kb.name, }
label: kb.name, })
const knowledgeItems = knowledgeBases.map((kb) => {
const kbName = kb.name || ''
return {
value: kbName,
label: kbName,
type: 'knowledge', type: 'knowledge',
insertValue: kbName,
tokenLabel: formatMentionToken('knowledge', kbName),
description: kb.db_id 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', type: 'mcp',
insertValue: mcpName,
tokenLabel: formatMentionToken('mcp', mcpName),
description: m.description || '' 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 filesLen = mentionItems.value.files.length
const kbLen = mentionItems.value.knowledgeBases.length const kbLen = mentionItems.value.knowledgeBases.length
const mcpLen = mentionItems.value.mcps.length
if (type === 'file') { if (type === 'file') {
return mentionSelectedIndex.value === index return mentionSelectedIndex.value === index
} else if (type === 'knowledge') { } else if (type === 'knowledge') {
return mentionSelectedIndex.value === filesLen + index return mentionSelectedIndex.value === filesLen + index
} else { } else if (type === 'mcp') {
return mentionSelectedIndex.value === filesLen + kbLen + index return mentionSelectedIndex.value === filesLen + kbLen + index
} else {
return mentionSelectedIndex.value === filesLen + kbLen + mcpLen + index
} }
} }
// //
const hasAnyItems = computed(() => { const hasAnyItems = computed(() => {
const items = mentionItems.value 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 textarea = inputRef.value
const cursorPos = textarea.selectionStart const cursorPos = textarea.selectionStart
const textBeforeCursor = inputValue.value.slice(0, cursorPos) 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 textAfterCursor = inputValue.value.slice(cursorPos)
const newValue = newTextBefore + textAfterCursor const newValue = newTextBefore + textAfterCursor
@ -366,7 +444,8 @@ const handleMentionNavigation = (e) => {
const allItems = [ const allItems = [
...mentionItems.value.files, ...mentionItems.value.files,
...mentionItems.value.knowledgeBases, ...mentionItems.value.knowledgeBases,
...mentionItems.value.mcps ...mentionItems.value.mcps,
...mentionItems.value.skills
] ]
const total = allItems.length const total = allItems.length
@ -499,7 +578,7 @@ const singleLineWidth = ref(0)
// @ // @
const mentionPopupVisible = ref(false) const mentionPopupVisible = ref(false)
const mentionQuery = ref('') const mentionQuery = ref('')
const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [] }) const mentionItems = ref({ files: [], knowledgeBases: [], mcps: [], skills: [] })
const mentionSelectedIndex = ref(0) const mentionSelectedIndex = ref(0)
// //

View File

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