diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue
index 1c4b3396..2578aaa7 100644
--- a/web/src/components/AgentChatComponent.vue
+++ b/web/src/components/AgentChatComponent.vue
@@ -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
}
})
diff --git a/web/src/components/MessageInputComponent.vue b/web/src/components/MessageInputComponent.vue
index 5b5ebb32..328e09cb 100644
--- a/web/src/components/MessageInputComponent.vue
+++ b/web/src/components/MessageInputComponent.vue
@@ -88,6 +88,19 @@
+
+
+
Skills
+
+ {{ item.label }}
+
+
+
暂无可引用的项
@@ -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)
// 检查行数
diff --git a/web/src/stores/agent.js b/web/src/stores/agent.js
index 6f6d0c93..11d414cb 100644
--- a/web/src/stores/agent.js
+++ b/web/src/stores/agent.js
@@ -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等)
+ * 获取可提及的资源(知识库、MCP、Skills)
*/
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,