feat: 添加技能提及功能,更新相关组件和状态管理
This commit is contained in:
parent
ed76824191
commit
47cf5fac01
@ -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
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
// 检查行数
|
// 检查行数
|
||||||
|
|||||||
@ -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等)
|
* 获取可提及的资源(知识库、MCP、Skills)
|
||||||
*/
|
*/
|
||||||
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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user