update:调整文件夹

This commit is contained in:
hrz
2025-08-11 15:48:28 +08:00
parent 6e8f9ca22e
commit 8509e62114
138 changed files with 36 additions and 93 deletions
@@ -0,0 +1,701 @@
<script lang="ts" setup>
import type { AgentDetail, ModelOption, PluginDefinition, RoleTemplate } from '@/api/agent/types'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { getAgentDetail, getModelOptions, getPluginFunctions, getRoleTemplates, getTTSVoices, updateAgent } from '@/api/agent/agent'
import { usePluginStore } from '@/store'
import { toast } from '@/utils/toast'
defineOptions({
name: 'AgentEdit',
})
const props = withDefaults(defineProps<Props>(), {
agentId: '',
})
// 组件参数
interface Props {
agentId?: string
}
const agentId = computed(() => props.agentId)
// 表单数据
const formData = ref<Partial<AgentDetail>>({
agentName: '',
systemPrompt: '',
summaryMemory: '',
vadModelId: '',
asrModelId: '',
llmModelId: '',
vllmModelId: '',
intentModelId: '',
memModelId: '',
ttsModelId: '',
ttsVoiceId: '',
})
// 显示名称数据
const displayNames = ref({
vad: '请选择',
asr: '请选择',
llm: '请选择',
vllm: '请选择',
intent: '请选择',
memory: '请选择',
tts: '请选择',
voiceprint: '请选择',
})
// 角色模板数据
const roleTemplates = ref<RoleTemplate[]>([])
const selectedTemplateId = ref('')
// 加载状态
const loading = ref(false)
const saving = ref(false)
// 模型选项数据
const modelOptions = ref<{
[key: string]: ModelOption[]
}>({
VAD: [],
ASR: [],
LLM: [],
VLLM: [],
Intent: [],
Memory: [],
TTS: [],
})
// 音色选项数据
const voiceOptions = ref<{ id: string, name: string }[]>([])
// 选择器显示状态
const pickerShow = ref<{
[key: string]: boolean
}>({
vad: false,
asr: false,
llm: false,
vllm: false,
intent: false,
memory: false,
tts: false,
voiceprint: false,
})
const allFunctions = ref<PluginDefinition[]>([])
// 使用插件store
const pluginStore = usePluginStore()
// tabs
const tabList = [
{
label: '角色配置',
value: 'home',
icon: '/static/tabbar/robot.png',
activeIcon: '/static/tabbar/robot_activate.png',
},
{
label: '设备管理',
value: 'category',
icon: '/static/tabbar/device.png',
activeIcon: '/static/tabbar/device_activate.png',
},
{
label: '聊天记录',
value: 'settings',
icon: '/static/tabbar/chat.png',
activeIcon: '/static/tabbar/chat_activate.png',
},
{
label: '声纹管理',
value: 'profile',
icon: '/static/tabbar/voiceprint.png',
activeIcon: '/static/tabbar/voiceprint_activate.png',
},
]
// 加载智能体详情
async function loadAgentDetail() {
if (!agentId.value)
return
try {
loading.value = true
const detail = await getAgentDetail(agentId.value)
formData.value = { ...detail }
// 更新插件store
pluginStore.setCurrentAgentId(agentId.value)
pluginStore.setCurrentFunctions(detail.functions || [])
// 如果有TTS模型,加载对应的音色选项
if (detail.ttsModelId) {
await loadVoiceOptions(detail.ttsModelId)
}
// 等待模型选项加载完成后再更新显示名称
await nextTick()
updateDisplayNames()
}
catch (error) {
console.error('加载智能体详情失败:', error)
toast.error('加载失败')
}
finally {
loading.value = false
}
}
// 获取音色显示名称
function getVoiceDisplayName(ttsVoiceId: string) {
if (!ttsVoiceId)
return '请选择'
console.log('=== 音色映射调试 ===')
console.log('当前音色ID:', ttsVoiceId)
console.log('当前TTS模型:', formData.value.ttsModelId)
console.log('可用音色选项:', voiceOptions.value)
// 首先尝试直接从音色选项中匹配ID
const voice = voiceOptions.value.find(v => v.id === ttsVoiceId)
if (voice) {
console.log('直接匹配成功:', voice)
return voice.name
}
// 如果没找到,尝试兼容性映射
if (voiceOptions.value.length > 0) {
console.log('直接匹配失败,尝试兼容性映射')
// 创建索引映射:voice1 → 第1个音色,voice2 → 第2个音色
const indexMap = {
voice1: 0,
voice2: 1,
voice3: 2,
voice4: 3,
voice5: 4,
}
const index = indexMap[ttsVoiceId]
if (index !== undefined && voiceOptions.value[index]) {
const mappedVoice = voiceOptions.value[index]
console.log(`索引映射: ${ttsVoiceId} → index ${index}${mappedVoice.name}`)
return mappedVoice.name
}
}
console.log('所有映射方式都失败,返回原始ID:', ttsVoiceId)
return ttsVoiceId
}
// 更新显示名称
function updateDisplayNames() {
if (!formData.value)
return
displayNames.value.vad = getModelDisplayName('VAD', formData.value.vadModelId)
displayNames.value.asr = getModelDisplayName('ASR', formData.value.asrModelId)
displayNames.value.llm = getModelDisplayName('LLM', formData.value.llmModelId)
displayNames.value.vllm = getModelDisplayName('VLLM', formData.value.vllmModelId)
displayNames.value.intent = getModelDisplayName('Intent', formData.value.intentModelId)
displayNames.value.memory = getModelDisplayName('Memory', formData.value.memModelId)
displayNames.value.tts = getModelDisplayName('TTS', formData.value.ttsModelId)
// 角色音色特殊处理
displayNames.value.voiceprint = getVoiceDisplayName(formData.value.ttsVoiceId || '')
console.log('最终音色显示名称:', displayNames.value.voiceprint)
}
// 加载角色模板
async function loadRoleTemplates() {
try {
const templates = await getRoleTemplates()
roleTemplates.value = templates
}
catch (error) {
console.error('加载角色模板失败:', error)
}
}
// 加载模型选项
async function loadModelOptions() {
const modelTypes = ['VAD', 'ASR', 'LLM', 'VLLM', 'Intent', 'Memory', 'TTS']
try {
await Promise.all(
modelTypes?.map(async (type) => {
console.log(`加载模型类型: ${type}`)
const options = await getModelOptions(type)
modelOptions.value[type] = options
console.log(`${type} 选项:`, options)
}) || [],
)
console.log('所有模型选项加载完成:', modelOptions.value)
}
catch (error) {
console.error('加载模型选项失败:', error)
}
}
// 加载TTS音色选项
async function loadVoiceOptions(ttsModelId?: string) {
if (!ttsModelId)
return
try {
console.log(`加载音色选项: ${ttsModelId}`)
const voices = await getTTSVoices(ttsModelId)
voiceOptions.value = voices
console.log('音色选项:', voices)
}
catch (error) {
console.error('加载音色选项失败:', error)
voiceOptions.value = []
}
}
// 选择角色模板
function selectRoleTemplate(templateId: string) {
if (selectedTemplateId.value === templateId) {
selectedTemplateId.value = ''
return
}
selectedTemplateId.value = templateId
const template = roleTemplates.value.find(t => t.id === templateId)
if (template) {
formData.value.systemPrompt = template.systemPrompt
formData.value.vadModelId = template.vadModelId
formData.value.asrModelId = template.asrModelId
formData.value.llmModelId = template.llmModelId
formData.value.vllmModelId = template.vllmModelId
formData.value.intentModelId = template.intentModelId
formData.value.memModelId = template.memModelId
formData.value.ttsModelId = template.ttsModelId
formData.value.ttsVoiceId = template.ttsVoiceId
}
}
// 打开选择器
function openPicker(type: string) {
pickerShow.value[type] = true
}
// 选择器确认
async function onPickerConfirm(type: string, value: any, name: string) {
console.log('选择器确认:', type, value, name)
// 保存显示名称
displayNames.value[type] = name
switch (type) {
case 'vad':
formData.value.vadModelId = value
break
case 'asr':
formData.value.asrModelId = value
break
case 'llm':
formData.value.llmModelId = value
break
case 'vllm':
formData.value.vllmModelId = value
break
case 'intent':
formData.value.intentModelId = value
displayNames.value.intent = name // 确保显示名称正确更新
break
case 'memory':
formData.value.memModelId = value
displayNames.value.memory = name // 确保显示名称正确更新
break
case 'tts':
formData.value.ttsModelId = value
// 当选择TTS模型时,自动加载对应的音色选项
await loadVoiceOptions(value)
// 重置音色选择
formData.value.ttsVoiceId = ''
displayNames.value.voiceprint = '请选择'
break
case 'voiceprint':
formData.value.ttsVoiceId = value
displayNames.value.voiceprint = name // 确保显示名称正确更新
break
}
pickerShow.value[type] = false
}
// 选择器取消
function onPickerCancel(type: string) {
pickerShow.value[type] = false
}
// 获取模型显示名称
function getModelDisplayName(modelType: string, modelId: string) {
if (!modelId)
return '请选择'
// 直接从API配置数据中查找匹配的ID
const options = modelOptions.value[modelType]
if (!options || options.length === 0) {
return modelId
}
const option = options.find(opt => opt.id === modelId)
if (option) {
return option.modelName
}
return modelId
}
// 保存智能体
async function saveAgent() {
if (!formData.value.agentName?.trim()) {
toast.warning('请输入助手昵称')
return
}
if (!formData.value.systemPrompt?.trim()) {
toast.warning('请输入角色介绍')
return
}
try {
saving.value = true
await updateAgent(agentId.value, formData.value)
toast.success('保存成功')
}
catch (error) {
console.error('保存失败:', error)
toast.error('保存失败')
}
finally {
saving.value = false
}
}
function loadPluginFunctions() {
getPluginFunctions().then((res) => {
const processedFunctions = res?.map((item) => {
const meta = JSON.parse(item.fields || '[]')
const params = meta.reduce((m: any, f: any) => {
m[f.key] = f.default
return m
}, {})
return { ...item, fieldsMeta: meta, params }
}) || []
allFunctions.value = processedFunctions
// 同时更新到store
pluginStore.setAllFunctions(processedFunctions)
})
}
function handleTools() {
console.log('当前插件配置:', formData.value.functions)
// 确保store中有最新数据
pluginStore.setCurrentAgentId(agentId.value)
pluginStore.setCurrentFunctions(formData.value.functions || [])
pluginStore.setAllFunctions(allFunctions.value)
uni.navigateTo({
url: '/pages/agent/tools',
})
}
// 监听插件配置更新
function watchPluginUpdates() {
// 监听store中的插件配置变化
watch(() => pluginStore.currentFunctions, (newFunctions) => {
console.log('插件配置已更新:', newFunctions)
formData.value.functions = newFunctions
}, { deep: true })
}
onMounted(async () => {
// 初始化插件配置监听
watchPluginUpdates()
// 先加载模型选项和角色模板
await Promise.all([
loadRoleTemplates(),
loadModelOptions(),
loadPluginFunctions(),
])
// 然后加载智能体详情,这样可以正确映射显示名称
if (agentId.value) {
await loadAgentDetail()
}
})
</script>
<template>
<view class="bg-[#f5f7fb] px-[20rpx]">
<!-- 基础信息标题 -->
<view class="pb-[20rpx] first:pt-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
基础信息
</text>
</view>
<!-- 基础信息卡片 -->
<view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);">
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
助手昵称
</text>
<input
v-model="formData.agentName"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] leading-[1.4] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text"
placeholder="请输入助手昵称"
>
</view>
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
角色模式
</text>
<view class="mt-0 flex flex-wrap gap-[12rpx]">
<view
v-for="template in roleTemplates"
:key="template.id"
class="cursor-pointer rounded-[20rpx] px-[24rpx] py-[12rpx] text-[24rpx] transition-all duration-300"
:class="selectedTemplateId === template.id
? 'bg-[#336cff] text-white border border-[#336cff]'
: 'bg-[rgba(51,108,255,0.1)] text-[#336cff] border border-[rgba(51,108,255,0.2)]'"
@click="selectRoleTemplate(template.id)"
>
{{ template.agentName }}
</view>
</view>
</view>
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
角色介绍
</text>
<textarea
v-model="formData.systemPrompt"
:maxlength="2000"
placeholder="请输入角色介绍"
class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] outline-none focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
/>
<view class="mt-[8rpx] text-right text-[22rpx] text-[#9d9ea3]">
{{ (formData.systemPrompt || '').length }}/2000
</view>
</view>
</view>
<!-- 模型配置标题 -->
<view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
模型配置
</text>
</view>
<!-- 模型配置卡片 -->
<view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);">
<view class="flex flex-col gap-[16rpx]">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vad')">
<text class="text-[28rpx] text-[#232338] font-medium">
语音活动检测
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.vad }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('asr')">
<text class="text-[28rpx] text-[#232338] font-medium">
语音识别
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.asr }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('llm')">
<text class="text-[28rpx] text-[#232338] font-medium">
大语言模型
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.llm }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('vllm')">
<text class="text-[28rpx] text-[#232338] font-medium">
视觉大模型
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.vllm }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('intent')">
<text class="text-[28rpx] text-[#232338] font-medium">
意图识别
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.intent }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('memory')">
<text class="text-[28rpx] text-[#232338] font-medium">
记忆
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.memory }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
</view>
</view>
<!-- 语音设置标题 -->
<view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
语音设置
</text>
</view>
<!-- 语音设置卡片 -->
<view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);">
<view class="flex flex-col gap-[16rpx]">
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('tts')">
<text class="text-[28rpx] text-[#232338] font-medium">
语音合成
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.tts }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex cursor-pointer items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] transition-all duration-300 active:bg-[#eef3ff]" @click="openPicker('voiceprint')">
<text class="text-[28rpx] text-[#232338] font-medium">
角色音色
</text>
<text class="mx-[16rpx] flex-1 text-right text-[26rpx] text-[#65686f]">
{{ displayNames.voiceprint }}
</text>
<wd-icon name="arrow-right" custom-class="text-[20rpx] text-[#9d9ea3]" />
</view>
<view class="flex items-center justify-between border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx]">
<view class="text-[28rpx] text-[#232338] font-medium">
插件
</view>
<view class="cursor-pointer rounded-[20rpx] bg-[rgba(51,108,255,0.1)] px-[24rpx] py-[12rpx] text-[24rpx] text-[#336cff] transition-all duration-300 active:bg-[#336cff] active:text-white" @click="handleTools">
<text>编辑功能</text>
</view>
</view>
</view>
</view>
<!-- 记忆历史标题 -->
<view class="pb-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
历史记忆
</text>
</view>
<!-- 记忆历史卡片 -->
<view class="mb-[24rpx] border border-[#eeeeee] rounded-[20rpx] bg-[#fbfbfb] p-[24rpx]" style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);">
<view class="mb-[24rpx] last:mb-0">
<textarea
v-model="formData.summaryMemory"
placeholder="记忆内容"
disabled
class="box-border h-[500rpx] w-full resize-none break-words break-all border border-[#eeeeee] rounded-[12rpx] bg-[#f0f0f0] p-[20rpx] text-[26rpx] text-[#65686f] leading-[1.6] opacity-80 outline-none"
/>
</view>
</view>
<!-- 保存按钮 -->
<view class="mt-[40rpx] p-0">
<wd-button
type="primary"
:loading="saving"
:disabled="saving"
custom-class="w-full h-[80rpx] rounded-[16rpx] text-[30rpx] font-semibold bg-[#336cff] active:bg-[#2d5bd1]"
@click="saveAgent"
>
{{ saving ? '保存中...' : '保存' }}
</wd-button>
</view>
<!-- 模型选择器 -->
<wd-action-sheet
v-model="pickerShow.vad"
:actions="modelOptions.VAD && modelOptions.VAD.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('vad')"
@select="({ item }) => onPickerConfirm('vad', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.asr"
:actions="modelOptions.ASR && modelOptions.ASR.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('asr')"
@select="({ item }) => onPickerConfirm('asr', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.llm"
:actions="modelOptions.LLM && modelOptions.LLM.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('llm')"
@select="({ item }) => onPickerConfirm('llm', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.vllm"
:actions="modelOptions.VLLM && modelOptions.VLLM.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('vllm')"
@select="({ item }) => onPickerConfirm('vllm', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.intent"
:actions="modelOptions.Intent && modelOptions.Intent.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('intent')"
@select="({ item }) => onPickerConfirm('intent', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.memory"
:actions="modelOptions.Memory && modelOptions.Memory.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('memory')"
@select="({ item }) => onPickerConfirm('memory', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.tts"
:actions="modelOptions.TTS && modelOptions.TTS.map(item => ({ name: item.modelName, value: item.id }))"
@close="onPickerCancel('tts')"
@select="({ item }) => onPickerConfirm('tts', item.value, item.name)"
/>
<wd-action-sheet
v-model="pickerShow.voiceprint"
:actions="voiceOptions && voiceOptions.map(item => ({ name: item.name, value: item.id }))"
@close="onPickerCancel('voiceprint')"
@select="({ item }) => onPickerConfirm('voiceprint', item.value, item.name)"
/>
</view>
</template>
@@ -0,0 +1,214 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationBarTitleText": "智能体",
"navigationStyle": "custom"
}
}
</route>
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import CustomTabs from '@/components/custom-tabs/index.vue'
import ChatHistory from '@/pages/chat-history/index.vue'
import DeviceManagement from '@/pages/device/index.vue'
import VoiceprintManagement from '@/pages/voiceprint/index.vue'
import AgentEdit from './edit.vue'
defineOptions({
name: 'AgentIndex',
})
// 获取屏幕边界到安全区域距离
let safeAreaInsets: any
let systemInfo: any
// #ifdef MP-WEIXIN
systemInfo = uni.getWindowInfo()
safeAreaInsets = systemInfo.safeArea
? {
top: systemInfo.safeArea.top,
right: systemInfo.windowWidth - systemInfo.safeArea.right,
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
left: systemInfo.safeArea.left,
}
: null
// #endif
// #ifndef MP-WEIXIN
systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets
// #endif
// 智能体ID
const currentAgentId = ref('default')
// 当前 tab
const currentTab = ref('agent-config')
// 刷新和加载状态
const refreshing = ref(false)
// 计算是否启用下拉刷新(角色编辑页面不启用)
const refresherEnabled = computed(() => {
return currentTab.value !== 'agent-config'
})
// 子组件引用
const deviceRef = ref()
const chatRef = ref()
const voiceprintRef = ref()
// Tab 配置
const tabList = [
{
label: '角色配置',
value: 'agent-config',
icon: '/static/tabbar/robot.png',
activeIcon: '/static/tabbar/robot_activate.png',
},
{
label: '设备管理',
value: 'device-management',
icon: '/static/tabbar/device.png',
activeIcon: '/static/tabbar/device_activate.png',
},
{
label: '聊天记录',
value: 'chat-history',
icon: '/static/tabbar/chat.png',
activeIcon: '/static/tabbar/chat_activate.png',
},
{
label: '声纹管理',
value: 'voiceprint-management',
icon: '/static/tabbar/microphone.png',
activeIcon: '/static/tabbar/microphone_activate.png',
},
]
// 返回上一页
function goBack() {
uni.navigateBack()
}
// 处理 tab 切换
function handleTabChange(item: any) {
console.log('Tab changed:', item)
}
// 下拉刷新
async function onRefresh() {
// 角色编辑页面不需要刷新
if (currentTab.value === 'agent-config') {
return
}
refreshing.value = true
try {
switch (currentTab.value) {
case 'device-management':
if (deviceRef.value?.refresh) {
await deviceRef.value.refresh()
}
break
case 'chat-history':
if (chatRef.value?.refresh) {
await chatRef.value.refresh()
}
break
case 'voiceprint-management':
if (voiceprintRef.value?.refresh) {
await voiceprintRef.value.refresh()
}
break
}
}
catch (error) {
console.error('刷新失败:', error)
}
finally {
refreshing.value = false
}
}
// 触底加载更多
async function onLoadMore() {
// 只有聊天记录需要加载更多
if (currentTab.value === 'chat-history' && chatRef.value?.loadMore) {
await chatRef.value.loadMore()
}
}
// 接收页面参数
onLoad((options) => {
if (options?.agentId) {
currentAgentId.value = options.agentId
console.log('接收到智能体ID:', options.agentId)
}
})
onMounted(async () => {
// 页面初始化
})
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f7fb]">
<!-- 导航栏 -->
<wd-navbar title="智能体" safe-area-inset-top>
<template #left>
<wd-icon name="arrow-left" size="18" @click="goBack" />
</template>
</wd-navbar>
<!-- 自定义 Tabs -->
<CustomTabs
v-model="currentTab"
:tab-list="tabList"
@change="handleTabChange"
/>
<!-- 主内容滚动区域 -->
<scroll-view
scroll-y
:style="{ height: `calc(100vh - ${safeAreaInsets?.top || 0}px - 180rpx)` }"
class="box-border flex-1 bg-[#f5f7fb]"
enable-back-to-top
:refresher-enabled="refresherEnabled"
:refresher-triggered="refreshing"
@refresherrefresh="onRefresh"
@scrolltolower="onLoadMore"
>
<!-- Tab 内容 -->
<view class="flex-1">
<AgentEdit
v-if="currentTab === 'agent-config'"
:agent-id="currentAgentId"
/>
<DeviceManagement
v-else-if="currentTab === 'device-management'"
ref="deviceRef"
:agent-id="currentAgentId"
/>
<ChatHistory
v-else-if="currentTab === 'chat-history'"
ref="chatRef"
:agent-id="currentAgentId"
/>
<VoiceprintManagement
v-else-if="currentTab === 'voiceprint-management'"
ref="voiceprintRef"
:agent-id="currentAgentId"
/>
</view>
</scroll-view>
</view>
</template>
<style scoped>
</style>
@@ -0,0 +1,568 @@
<route lang="jsonc" type="page">
{
"layout": "default",
"style": {
"navigationBarTitleText": "编辑功能",
"navigationStyle": "custom",
},
}
</route>
<script lang="ts" setup>
import { useMessage } from 'wot-design-uni'
import { getMcpAddress, getMcpTools } from '@/api/agent/agent'
import { usePluginStore } from '@/store'
const message = useMessage()
const pluginStore = usePluginStore()
const segmentedList = ref<string[]>(['未选', '已选'])
const currentSegmented = ref('未选')
const notSelectedList = ref<any[]>([])
const selectedList = ref<any[]>([])
// 使用计算属性从store获取数据
const allFunctions = computed(() => pluginStore.allFunctions)
const functions = computed(() => pluginStore.currentFunctions)
const agentId = computed(() => pluginStore.currentAgentId)
const mcpAddress = ref('')
const mcpTools = ref<string[]>([])
// 参数编辑相关
const showParamDialog = ref(false)
const currentFunction = ref<any>(null)
const tempParams = ref<Record<string, any>>({})
const arrayTextCache = ref<Record<string, string>>({})
const jsonTextCache = ref<Record<string, string>>({})
async function mergeFunctions() {
selectedList.value = functions.value.map((mapping) => {
const meta = allFunctions.value.find(f => f.id === mapping.pluginId)
if (!meta) {
return { id: mapping.pluginId, name: mapping.pluginId, params: {} }
}
return {
id: mapping.pluginId,
name: meta.name,
params: mapping.paramInfo || { ...meta.params },
fieldsMeta: meta.fieldsMeta,
}
})
// 未选的插件
notSelectedList.value = allFunctions.value.filter(
item => !selectedList.value.some(f => f.id === item.id),
)
if (agentId.value) {
const [address, tools] = await Promise.all([
getMcpAddress(agentId.value),
getMcpTools(agentId.value),
])
mcpAddress.value = address
mcpTools.value = tools || []
}
}
// 添加插件到已选
function selectFunction(func: any) {
// 添加到已选列表
selectedList.value.push({
id: func.id,
name: func.name,
params: { ...func.params },
fieldsMeta: func.fieldsMeta,
})
// 从未选列表中移除
notSelectedList.value = notSelectedList.value.filter(
item => item.id !== func.id,
)
}
// 从已选中移除插件
function removeFunction(func: any) {
// 从已选列表中移除
selectedList.value = selectedList.value.filter(item => item.id !== func.id)
// 添加回未选列表
const originalFunc = allFunctions.value.find(f => f.id === func.id)
if (originalFunc) {
notSelectedList.value.push(originalFunc)
}
}
// 编辑插件参数
function editFunction(func: any) {
currentFunction.value = func
// 直接使用当前函数的参数
tempParams.value = { ...func.params }
// 初始化文本缓存
if (func.fieldsMeta) {
func.fieldsMeta.forEach((field: any) => {
if (field.type === 'array') {
const value = tempParams.value[field.key]
arrayTextCache.value[field.key] = Array.isArray(value)
? value.join('\n')
: value || ''
}
else if (field.type === 'json') {
const value = tempParams.value[field.key]
try {
jsonTextCache.value[field.key] = JSON.stringify(value || {}, null, 2)
}
catch {
jsonTextCache.value[field.key] = '{}'
}
}
})
}
showParamDialog.value = true
}
// 处理参数变化 - 实时保存
function handleParamChange(key: string, value: any, field: any) {
tempParams.value[key] = value
// 实时更新到 selectedList
if (currentFunction.value) {
const index = selectedList.value.findIndex(
f => f.id === currentFunction.value.id,
)
if (index >= 0) {
selectedList.value[index].params = { ...tempParams.value }
}
}
}
// 处理数组类型参数变化 - 实时保存
function handleArrayChange(key: string, value: string, field: any) {
arrayTextCache.value[key] = value
// 转换为数组存储
const arrayValue = value.split('\n').filter(Boolean)
tempParams.value[key] = arrayValue
// 实时更新到 selectedList
if (currentFunction.value) {
const index = selectedList.value.findIndex(
f => f.id === currentFunction.value.id,
)
if (index >= 0) {
selectedList.value[index].params = { ...tempParams.value }
}
}
}
// 处理JSON类型参数变化 - 实时保存
function handleJsonChange(key: string, value: string, field: any) {
jsonTextCache.value[key] = value
try {
const jsonValue = JSON.parse(value)
tempParams.value[key] = jsonValue
// 实时更新到 selectedList
if (currentFunction.value) {
const index = selectedList.value.findIndex(
f => f.id === currentFunction.value.id,
)
if (index >= 0) {
selectedList.value[index].params = { ...tempParams.value }
}
}
}
catch {
message.alert('JSON格式错误')
}
}
// 关闭参数编辑弹窗
function closeParamEdit() {
showParamDialog.value = false
tempParams.value = {}
arrayTextCache.value = {}
jsonTextCache.value = {}
}
// 返回上一页并更新配置
function goBack() {
const finalFunctions = selectedList.value.map(f => ({
pluginId: f.id,
paramInfo: f.params,
}))
// 更新到store中
pluginStore.updateFunctions(finalFunctions)
// 直接返回
uni.navigateBack()
}
// 复制MCP地址
function copyMcpAddress() {
if (!mcpAddress.value) {
message.alert('暂无MCP地址可复制')
return
}
uni.setClipboardData({
data: mcpAddress.value,
showToast: false,
success: () => {
message.alert('MCP地址已复制到剪贴板')
},
fail: () => {
message.alert('复制失败,请重试')
},
})
}
// 渲染参数字段的辅助函数
function getFieldDisplayValue(field: any, value: any) {
if (field.type === 'array') {
return Array.isArray(value) ? value.join('\n') : value || ''
}
return value || ''
}
// 字段说明
function getFieldRemark(field: any) {
let description = field.label || ''
if (field.default) {
description += `(默认值:${field.default}`
}
return description
}
onMounted(async () => {
// 直接从store获取数据并合并
await mergeFunctions()
})
</script>
<template>
<view class="h-screen flex flex-col bg-[#f5f7fb]">
<!-- 头部导航 -->
<wd-navbar
title=""
safe-area-inset-top
left-arrow
:bordered="false"
@click-left="goBack"
>
<template #left>
<wd-icon name="arrow-left" size="18" />
</template>
</wd-navbar>
<!-- 内容区域 -->
<scroll-view
scroll-y
class="box-border flex-1 bg-transparent px-[20rpx]"
:style="{ height: 'calc(100vh - 120rpx)' }"
:scroll-with-animation="true"
>
<!-- 内置插件区域 -->
<view class="mt-[20rpx] flex flex-1 flex-col">
<view class="text-[32rpx] text-[#333] font-medium">
内置插件
</view>
<view
class="mt-[20rpx] box-border flex flex-1 flex-col rounded-[10rpx] bg-white p-[20rpx]"
>
<!-- 分段控制器 -->
<wd-segmented
v-model:value="currentSegmented"
:options="segmentedList"
/>
<!-- 插件列表 -->
<view class="mt-[20rpx] flex-1 overflow-hidden">
<!-- 未选插件 -->
<scroll-view
v-if="currentSegmented === '未选'"
class="max-h-[600rpx] bg-transparent"
scroll-y
>
<view
v-if="notSelectedList.length === 0"
class="h-[400rpx] flex items-center justify-center"
>
<wd-status-tip image="content" tip="暂无更多插件" />
</view>
<view v-else class="p-[20rpx] space-y-[20rpx]">
<view
v-for="func in notSelectedList"
:key="func.id"
class="flex items-center justify-between border border-[#e9ecef] rounded-[10rpx] bg-[#f8f9fa] p-[20rpx]"
@click="selectFunction(func)"
>
<view class="flex-1">
<view
class="mb-[10rpx] text-[30rpx] text-[#333] font-medium"
>
{{ func.name }}
</view>
<view class="text-[24rpx] text-[#666]">
{{ func.providerCode }}
</view>
</view>
<view
class="h-[60rpx] w-[60rpx] flex items-center justify-center rounded-full bg-[#1677ff]"
>
<text class="text-[36rpx] text-white">
+
</text>
</view>
</view>
</view>
</scroll-view>
<!-- 已选插件 -->
<scroll-view v-else class="max-h-[600rpx] bg-transparent" scroll-y>
<view
v-if="selectedList.length === 0"
class="h-[400rpx] flex items-center justify-center"
>
<wd-status-tip image="content" tip="请选择插件功能" />
</view>
<view v-else class="p-[20rpx] space-y-[20rpx]">
<view
v-for="func in selectedList"
:key="func.id"
class="border border-[#d4edff] rounded-[10rpx] bg-[#f0f7ff] p-[20rpx]"
>
<view class="flex items-center justify-between">
<view class="flex-1" @click="editFunction(func)">
<view
class="mb-[10rpx] text-[30rpx] text-[#333] font-medium"
>
{{ func.name }}
</view>
<view class="text-[24rpx] text-[#1677ff]">
点击配置参数
</view>
</view>
<view class="flex space-x-[20rpx]">
<!-- 配置按钮 -->
<view
class="h-[60rpx] w-[60rpx] flex items-center justify-center rounded-full bg-[#1677ff]"
@click="editFunction(func)"
>
<text class="text-[24rpx] text-white">
</text>
</view>
<!-- 移除按钮 -->
<view
class="h-[60rpx] w-[60rpx] flex items-center justify-center rounded-full bg-[#ff4757]"
@click="removeFunction(func)"
>
<text class="text-[32rpx] text-white">
×
</text>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
<!-- MCP接入点区域 -->
<view class="mt-[20rpx] flex flex-1 flex-col">
<view class="text-[32rpx] text-[#333] font-medium">
mcp接入点
</view>
<view
class="mt-[20rpx] box-border flex flex-1 flex-col rounded-[10rpx] bg-white p-[20rpx]"
>
<view class="flex items-center justify-between text-[24rpx]">
<input
v-model="mcpAddress"
type="text"
disabled
class="flex-1 rounded-[10rpx] bg-[#f5f7fb] p-[20rpx]"
>
<view
class="ml-[20rpx] h-[70rpx] flex items-center justify-center rounded-[10rpx] bg-[#1677ff] px-[20rpx] text-[24rpx] text-white"
@click="copyMcpAddress"
>
复制
</view>
</view>
<!-- 工具列表 -->
<view class="mt-[20rpx] flex-1 overflow-hidden">
<scroll-view class="max-h-[600rpx] bg-transparent" scroll-y>
<view
v-if="mcpTools && mcpTools.length === 0"
class="h-[400rpx] flex items-center justify-center"
>
<wd-status-tip image="content" tip="暂无工具" />
</view>
<view v-else class="p-[20rpx]">
<view class="flex flex-wrap">
<view
v-for="tool in mcpTools"
:key="tool"
class="mb-[20rpx] mr-[20rpx] rounded-[10rpx] bg-[#f5f7fb] p-[20rpx]"
>
{{ tool }}
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</scroll-view>
<!-- 参数编辑弹窗 -->
<wd-action-sheet
v-model="showParamDialog"
:title="`参数配置 - ${currentFunction?.name || ''}`"
custom-header-class="h-[75vh]"
@close="closeParamEdit"
>
<scroll-view
scroll-y
class="bg-[#f5f7fb]"
:style="{ height: 'calc(75vh - 60rpx)' }"
>
<view class="p-[30rpx] pb-[40rpx]">
<!-- 无参数提示 -->
<view
v-if="
!currentFunction?.fieldsMeta
|| currentFunction.fieldsMeta.length === 0
"
class="h-[400rpx] flex items-center justify-center"
>
<text class="text-[28rpx] text-[#999]">
{{ currentFunction?.name }} 无需配置参数
</text>
</view>
<!-- 参数表单 - 卡片式布局 -->
<view v-else class="flex flex-col gap-[24rpx]">
<view
v-for="field in currentFunction.fieldsMeta"
:key="field.key"
class="border border-[#eeeeee] rounded-[20rpx] bg-white p-[30rpx]"
style="box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);"
>
<!-- 字段信息 -->
<view class="mb-[24rpx]">
<text class="mb-[8rpx] block text-[32rpx] text-[#232338] font-medium">
{{ field.label }}
</text>
<text v-if="getFieldRemark(field)" class="block text-[24rpx] text-[#65686f] leading-[1.5]">
{{ getFieldRemark(field) }}
</text>
</view>
<!-- 输入控件 -->
<view>
<!-- 字符串类型 -->
<input
v-if="field.type === 'string'"
v-model="tempParams[field.key]"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text"
:placeholder="`请输入${field.label}`"
@input="
handleParamChange(field.key, $event.detail.value, field)
"
>
<!-- 数组类型 -->
<view v-else-if="field.type === 'array'">
<text class="mb-[16rpx] block text-[24rpx] text-[#65686f]">
每行输入一个项目
</text>
<textarea
v-model="arrayTextCache[field.key]"
class="box-border min-h-[200rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
:placeholder="`请输入${field.label},每行一个`"
@input="
handleArrayChange(field.key, $event.detail.value, field)
"
/>
</view>
<!-- JSON类型 -->
<view v-else-if="field.type === 'json'">
<text class="mb-[16rpx] block text-[24rpx] text-[#65686f]">
请输入有效的JSON格式
</text>
<textarea
v-model="jsonTextCache[field.key]"
class="box-border min-h-[300rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[20rpx] text-[26rpx] text-[#232338] leading-[1.6] font-mono focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
placeholder="请输入合法的JSON格式"
@blur="
handleJsonChange(field.key, $event.detail.value, field)
"
/>
</view>
<!-- 数字类型 -->
<input
v-else-if="field.type === 'number'"
v-model="tempParams[field.key]"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="number"
:placeholder="`请输入${field.label}`"
@input="
handleParamChange(
field.key,
Number($event.detail.value),
field,
)
"
>
<!-- 布尔类型 -->
<view
v-else-if="field.type === 'boolean' || field.type === 'bool'"
class="flex items-center justify-between py-[20rpx]"
>
<view class="flex-1">
<text class="mb-[8rpx] block text-[28rpx] text-[#232338]">
启用功能
</text>
<text class="block text-[24rpx] text-[#65686f]">
开启或关闭此功能
</text>
</view>
<switch
:checked="tempParams[field.key]"
@change="
handleParamChange(field.key, $event.detail.value, field)
"
/>
</view>
<!-- 默认字符串类型 -->
<input
v-else
v-model="tempParams[field.key]"
class="box-border h-[80rpx] w-full border border-[#eeeeee] rounded-[12rpx] bg-[#f5f7fb] p-[16rpx_20rpx] text-[28rpx] text-[#232338] focus:border-[#336cff] focus:bg-white placeholder:text-[#9d9ea3]"
type="text"
:placeholder="`请输入${field.label}`"
@input="
handleParamChange(field.key, $event.detail.value, field)
"
>
</view>
</view>
</view>
</view>
</scroll-view>
</wd-action-sheet>
</view>
</template>