feat: 新增智能体标签、上下文源功能

This commit is contained in:
zhuoqinglian
2026-03-04 16:33:22 +08:00
parent ba07c623df
commit ac887139ce
13 changed files with 770 additions and 18 deletions
@@ -183,3 +183,26 @@ export function createVoicePrint(data: { agentId: string, audioId: string, sourc
},
})
}
// 获取智能体标签
export function getAgentTags(agentId: string) {
return http.Get<any[]>(`/agent/${agentId}/tags`, {
meta: {
ignoreAuth: false,
toast: false,
},
cacheFor: {
expire: 0,
},
})
}
// 更新智能体标签
export function updateAgentTags(agentId: string, data) {
return http.Put(`/agent/${agentId}/tags`, data, {
meta: {
ignoreAuth: false,
isExposeError: true,
},
})
}
@@ -11,6 +11,7 @@ export interface Agent {
summaryMemory: string | null
lastConnectedAt: string | null
deviceCount: number
tags: { id: string, tagName: string }[]
}
// 智能体创建数据类型
@@ -0,0 +1,467 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { t } from '@/i18n'
interface Props {
visible?: boolean
providers?: Array<{
url: string
headers: Record<string, string>
}>
}
const props = withDefaults(defineProps<Props>(), {
visible: false,
providers: () => [],
})
const emit = defineEmits<{
'update:visible': [value: boolean]
'confirm': [value: Array<{
url: string
headers: Record<string, string>
}>]
}>()
const localProviders = ref<Array<{
url: string
headers: Array<{
key: string
value: string
}>
}>>([])
function initLocalData() {
localProviders.value = props.providers.map((p) => {
const headers = p.headers || {}
return {
url: p.url || '',
headers: Object.entries(headers).map(([key, value]) => ({ key, value })),
}
})
if (localProviders.value.length === 0) {
localProviders.value.push({ url: '', headers: [{ key: '', value: '' }] })
}
}
function addProvider(index: number) {
localProviders.value.splice(index, 0, {
url: '',
headers: [{ key: '', value: '' }],
})
}
function removeProvider(index: number) {
localProviders.value.splice(index, 1)
}
function addHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 0, { key: '', value: '' })
}
function removeHeader(pIndex: number, hIndex: number) {
localProviders.value[pIndex].headers.splice(hIndex, 1)
}
function handleConfirm() {
const result = localProviders.value
.filter(p => p.url.trim() !== '')
.map((p) => {
const headersObj: Record<string, string> = {}
p.headers.forEach((h) => {
if (h.key.trim()) {
headersObj[h.key.trim()] = h.value
}
})
return {
url: p.url.trim(),
headers: headersObj,
}
})
emit('confirm', result)
emit('update:visible', false)
}
function handleClose() {
emit('update:visible', false)
}
watch(() => props.visible, (val) => {
if (val) {
initLocalData()
}
})
</script>
<template>
<view v-if="visible" class="context-provider-dialog-mask" @click="handleClose">
<view class="context-provider-dialog" @click.stop>
<view class="dialog-header">
<text class="dialog-title">
{{ t('contextProviderDialog.title') }}
</text>
</view>
<view class="dialog-content">
<view v-if="localProviders.length === 0" class="empty-container">
<text class="empty-text">
{{ t('contextProviderDialog.noContextApi') }}
</text>
<wd-button type="primary" size="small" @click="addProvider(0)">
{{ t('contextProviderDialog.add') }}
</wd-button>
</view>
<view v-else class="providers-list">
<view v-for="(provider, pIndex) in localProviders" :key="pIndex" class="provider-item">
<view class="provider-card">
<view class="card-header">
<view class="card-controls">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#66b1ff] !text-[#fff]"
@click="addProvider(pIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[60rpx] !w-[60rpx] !bg-[#F56C6C] !text-[#fff]"
@click="removeProvider(pIndex)"
/>
</view>
</view>
<view class="input-row">
<text class="label-text">
{{ t('contextProviderDialog.apiUrl') }}
</text>
<wd-input
v-model="provider.url"
class="flex-1"
:placeholder="t('contextProviderDialog.apiUrlPlaceholder')"
/>
</view>
<view class="headers-section">
<view class="label-text" style="margin-top: 6px;">
{{ t('contextProviderDialog.requestHeaders') }}
</view>
<view class="headers-list">
<view
v-for="(header, hIndex) in provider.headers"
:key="hIndex"
class="header-row-vertical"
>
<view class="header-input-group">
<wd-input
v-model="header.key"
placeholder="key"
class="header-input-full"
/>
</view>
<view class="header-input-group">
<wd-input
v-model="header.value"
placeholder="value"
class="header-input-full"
/>
</view>
<view class="header-controls">
<wd-button
type="icon"
icon="add"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#ecf5ff] !text-[#b3d8ff]"
@click="addHeader(pIndex, hIndex + 1)"
/>
<wd-button
type="icon"
icon="decrease"
size="small"
class="!h-[50rpx] !w-[50rpx] !border-[1rpx] !border-solid !bg-[#fef0f0] !text-[#F56C6C]"
@click="removeHeader(pIndex, hIndex)"
/>
</view>
</view>
<view v-if="provider.headers.length === 0" class="header-row empty-header">
<text class="no-header-text">
{{ t('contextProviderDialog.noHeaders') }}
</text>
<wd-button type="text" size="small" @click="addHeader(pIndex, 0)">
{{ t('contextProviderDialog.addHeaders') }}
</wd-button>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
<view class="dialog-footer">
<wd-button class="cancel-btn" @click="handleClose">
{{ t('contextProviderDialog.cancel') }}
</wd-button>
<wd-button type="primary" class="confirm-btn" @click="handleConfirm">
{{ t('contextProviderDialog.confirm') }}
</wd-button>
</view>
</view>
</view>
</template>
<style scoped lang="scss">
.context-provider-dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.context-provider-dialog {
background: #fff;
// border-radius: 20rpx;
width: 100%;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
border-bottom: 1px solid #eee;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #232338;
}
.close-icon {
font-size: 40rpx;
color: #9d9ea3;
cursor: pointer;
}
.dialog-content {
flex: 1;
overflow-y: auto;
padding: 20rpx;
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
gap: 30rpx;
}
.empty-text {
font-size: 28rpx;
color: #9d9ea3;
}
.providers-list {
display: flex;
flex-direction: column;
}
.provider-item {
margin-bottom: 30rpx;
}
.provider-card {
flex: 1;
background: #fff;
border-radius: 16rpx;
border: 1px solid #eee;
border-left: 6rpx solid #336cff;
padding: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.card-title {
font-size: 30rpx;
font-weight: 600;
color: #232338;
}
.card-controls {
display: flex;
gap: 16rpx;
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 40rpx;
}
.headers-section {
display: flex;
gap: 20rpx;
align-items: flex-start;
}
.headers-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
background: #fcfcfc;
padding: 4rpx;
border-radius: 12rpx;
border: 1px dashed #dcdfe6;
}
.header-row-vertical {
display: flex;
flex-direction: column;
gap: 16rpx;
background: #fff;
// padding: 20rpx;
border-radius: 12rpx;
// border: 1px solid #e8e8e8;
}
.header-input-group {
display: flex;
align-items: center;
gap: 8rpx;
}
.header-key-label,
.header-value-label {
font-size: 24rpx;
font-weight: 500;
color: #606266;
}
.header-input-full {
width: 100%;
}
.header-controls {
display: flex;
gap: 12rpx;
// margin-top: 8rpx;
align-self: flex-start;
}
.block-controls {
display: flex;
flex-direction: column;
gap: 16rpx;
padding-top: 10rpx;
}
.input-row {
display: flex;
align-items: center;
gap: 20rpx;
margin-bottom: 20rpx;
}
.label-text {
font-size: 28rpx;
font-weight: 600;
color: #606266;
width: 140rpx;
}
.headers-section {
display: flex;
gap: 20rpx;
align-items: flex-start;
}
.headers-list {
flex: 1;
display: flex;
flex-direction: column;
gap: 20rpx;
background: #fcfcfc;
padding: 16rpx;
border-radius: 12rpx;
border: 1px dashed #dcdfe6;
}
.header-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.header-input {
width: 200rpx;
}
.separator {
color: #909399;
font-weight: bold;
font-size: 32rpx;
}
.row-controls {
display: flex;
gap: 12rpx;
margin-left: 8rpx;
flex-shrink: 0;
}
.empty-header {
justify-content: center;
padding: 20rpx;
color: #909399;
font-size: 26rpx;
}
.no-header-text {
margin-right: 16rpx;
}
.flex-1 {
flex: 1;
}
.dialog-footer {
display: flex;
gap: 20rpx;
padding: 30rpx;
border-top: 1px solid #eee;
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 28rpx;
}
</style>
@@ -107,6 +107,9 @@ const alovaInstance = createAlova({
throw new Error(`请求错误[${code}]${msg}`)
}
if (config.meta?.isExposeError) {
return Promise.reject(msg)
}
if (config.meta?.toast !== false) {
toast.warning(msg)
}
+23
View File
@@ -73,6 +73,8 @@ export default {
'home.minutesAgo': 'Minuten her',
'home.hoursAgo': 'Stunden her',
'home.daysAgo': 'Tage her',
'home.languageModel': 'LLM',
'home.voiceModel': 'TTS',
// Agentenseite
'agent.pageTitle': 'Agent',
@@ -92,6 +94,13 @@ export default {
'agent.agentName': 'Agentenname',
'agent.inputAgentName': 'Bitte Agenten-Namen eingeben',
'agent.roleMode': 'Rollenmodus',
'agent.agentTag': 'Smart Body-Etiketten',
'agent.addAgentTag': 'Tags hinzufügen',
'agent.inputAgentTag': 'Bitte Smart Body-Etiketten eingeben',
'agent.contextProvider': 'Kontext',
'agent.contextProviderSuccess': '{count} Quellen erfolgreich hinzugefügt.',
'agent.contextProviderDocLink': 'Wie man Kontextquellen bereitstellt',
'agent.editContextProvider': 'Quelle bearbeiten',
'agent.roleDescription': 'Rollenbeschreibung',
'agent.inputRoleDescription': 'Bitte Rollenbeschreibung eingeben',
'agent.modelConfig': 'Modellkonfiguration',
@@ -116,6 +125,20 @@ export default {
'agent.pleaseInputRoleDescription': 'Bitte Rollenbeschreibung eingeben',
'agent.pleaseSelect': 'Bitte auswählen',
// Context provider dialog related
'contextProviderDialog.title': 'Quelle bearbeiten',
'contextProviderDialog.noContextApi': 'Keine Kontext-API',
'contextProviderDialog.add': 'Hinzufügen',
'contextProviderDialog.apiUrl': 'API-URL',
'contextProviderDialog.apiUrlPlaceholder': 'http://api.example.com/data',
'contextProviderDialog.requestHeaders': 'Anfrage-Header',
'contextProviderDialog.headerKeyPlaceholder': 'Schlüssel',
'contextProviderDialog.headerValuePlaceholder': 'Wert',
'contextProviderDialog.noHeaders': 'Keine Headers',
'contextProviderDialog.addHeader': 'Header hinzufügen',
'contextProviderDialog.cancel': 'Abbrechen',
'contextProviderDialog.confirm': 'Bestätigen',
// Chat-Verlauf Seite
'chatHistory.getChatSessions': 'Chat-Sitzungsliste abrufen',
'chatHistory.noSelectedAgent': 'Kein Agent ausgewählt',
+23
View File
@@ -73,6 +73,8 @@ export default {
'home.minutesAgo': 'minutes ago',
'home.hoursAgo': 'hours ago',
'home.daysAgo': 'days ago',
'home.languageModel': 'LLM',
'home.voiceModel': 'TTS',
// Agent page
'agent.pageTitle': 'Agent',
@@ -92,6 +94,13 @@ export default {
'agent.agentName': 'Agent Name',
'agent.inputAgentName': 'Please input agent name',
'agent.roleMode': 'Role Mode',
'agent.agentTag': 'Intelligent agent label',
'agent.addAgentTag': 'Add label',
'agent.inputAgentTag': 'Please input agent label',
'agent.contextProvider': 'Context',
'agent.contextProviderSuccess': 'Successfully added {count} sources.',
'agent.contextProviderDocLink': 'How to deploy context provider',
'agent.editContextProvider': 'Edit Source',
'agent.roleDescription': 'Role Description',
'agent.inputRoleDescription': 'Please input role description',
'agent.modelConfig': 'Model Configuration',
@@ -116,6 +125,20 @@ export default {
'agent.pleaseInputRoleDescription': 'Please input role description',
'agent.pleaseSelect': 'Please select',
// Context provider dialog related
'contextProviderDialog.title': 'Edit Source',
'contextProviderDialog.noContextApi': 'No Context API',
'contextProviderDialog.add': 'Add',
'contextProviderDialog.apiUrl': 'API URL',
'contextProviderDialog.apiUrlPlaceholder': 'http://api.example.com/data',
'contextProviderDialog.requestHeaders': 'Request Headers',
'contextProviderDialog.headerKeyPlaceholder': 'Key',
'contextProviderDialog.headerValuePlaceholder': 'Value',
'contextProviderDialog.noHeaders': 'No Headers',
'contextProviderDialog.addHeader': 'Add Header',
'contextProviderDialog.cancel': 'Cancel',
'contextProviderDialog.confirm': 'Confirm',
// Chat History Page
'chatHistory.getChatSessions': 'Get chat session list',
'chatHistory.noSelectedAgent': 'No agent selected',
+23
View File
@@ -73,6 +73,8 @@ export default {
'home.minutesAgo': 'minutos atrás',
'home.hoursAgo': 'horas atrás',
'home.daysAgo': 'dias atrás',
'home.languageModel': 'LLM',
'home.voiceModel': 'TTS',
// Agent page
'agent.pageTitle': 'Agente',
@@ -92,6 +94,13 @@ export default {
'agent.agentName': 'Nome do Agente',
'agent.inputAgentName': 'Por favor, insira o nome do agente',
'agent.roleMode': 'Modo de Papel',
'agent.agentTag': 'Etiquetas de corpo inteligente',
'agent.addAgentTag': 'Adicionar etiqueta',
'agent.inputAgentTag': 'Por favor, insira o rótulo do corpo inteligente',
'agent.contextProvider': 'Contexto',
'agent.contextProviderSuccess': '{count} fontes adicionadas com sucesso.',
'agent.contextProviderDocLink': 'Como implantar provedor de contexto',
'agent.editContextProvider': 'Editar Fonte',
'agent.roleDescription': 'Descrição do Papel',
'agent.inputRoleDescription': 'Por favor, insira a descrição do papel',
'agent.modelConfig': 'Configuração do Modelo',
@@ -116,6 +125,20 @@ export default {
'agent.pleaseInputRoleDescription': 'Por favor, insira a descrição do papel',
'agent.pleaseSelect': 'Por favor, selecione',
// Diálogo de provedor de contexto
'contextProviderDialog.title': 'Editar Fonte',
'contextProviderDialog.noContextApi': 'Sem API de Contexto',
'contextProviderDialog.add': 'Adicionar',
'contextProviderDialog.apiUrl': 'URL da API',
'contextProviderDialog.apiUrlPlaceholder': 'http://api.example.com/data',
'contextProviderDialog.requestHeaders': 'Cabeçalhos da Requisição',
'contextProviderDialog.headerKeyPlaceholder': 'Chave',
'contextProviderDialog.headerValuePlaceholder': 'Valor',
'contextProviderDialog.noHeaders': 'Sem Cabeçalhos',
'contextProviderDialog.addHeader': 'Adicionar Cabeçalho',
'contextProviderDialog.cancel': 'Cancelar',
'contextProviderDialog.confirm': 'Confirmar',
// Chat History Page
'chatHistory.getChatSessions': 'Obter lista de sessões de conversa',
'chatHistory.noSelectedAgent': 'Nenhum agente selecionado',
+23
View File
@@ -73,6 +73,8 @@ export default {
'home.minutesAgo': 'phút trước',
'home.hoursAgo': 'giờ trước',
'home.daysAgo': 'ngày trước',
'home.languageModel': 'LLM',
'home.voiceModel': 'TTS',
// Trang đại lý
'agent.pageTitle': 'Đại lý',
@@ -92,6 +94,13 @@ export default {
'agent.agentName': 'Tên đại lý',
'agent.inputAgentName': 'Vui lòng nhập tên đại lý',
'agent.roleMode': 'Chế độ vai trò',
'agent.agentTag': 'Nhãn cơ thể thông minh',
'agent.addAgentTag': 'Thêm thẻ',
'agent.inputAgentTag': 'Vui lòng nhập thẻ cơ thể thông minh',
'agent.contextProvider': 'Bối cảnh',
'agent.contextProviderSuccess': 'Đã thêm thành công {count} nguồn.',
'agent.contextProviderDocLink': 'Cách triển khai nguồn ngữ cảnh',
'agent.editContextProvider': 'Chỉnh sửa nguồn',
'agent.roleDescription': 'Mô tả vai trò',
'agent.inputRoleDescription': 'Vui lòng nhập mô tả vai trò',
'agent.modelConfig': 'Cấu hình mô hình',
@@ -116,6 +125,20 @@ export default {
'agent.pleaseInputRoleDescription': 'Vui lòng nhập mô tả vai trò',
'agent.pleaseSelect': 'Vui lòng chọn',
// Context provider dialog related
'contextProviderDialog.title': 'Chỉnh sửa nguồn',
'contextProviderDialog.noContextApi': 'Không có API ngữ cảnh',
'contextProviderDialog.add': 'Thêm',
'contextProviderDialog.apiUrl': 'Địa chỉ API',
'contextProviderDialog.apiUrlPlaceholder': 'http://api.example.com/data',
'contextProviderDialog.requestHeaders': 'Header yêu cầu',
'contextProviderDialog.headerKeyPlaceholder': 'Khóa',
'contextProviderDialog.headerValuePlaceholder': 'Giá trị',
'contextProviderDialog.noHeaders': 'Không có Headers',
'contextProviderDialog.addHeader': 'Thêm Header',
'contextProviderDialog.cancel': 'Hủy bỏ',
'contextProviderDialog.confirm': 'Xác nhận',
// Trang lịch sử trò chuyện
'chatHistory.getChatSessions': 'Lấy danh sách phiên trò chuyện',
'chatHistory.noSelectedAgent': 'Chưa chọn đại lý',
+23
View File
@@ -73,6 +73,8 @@ export default {
'home.minutesAgo': '分钟前',
'home.hoursAgo': '小时前',
'home.daysAgo': '天前',
'home.languageModel': '语言模型',
'home.voiceModel': '音色模型',
// Agent页面
'agent.pageTitle': '智能体',
@@ -92,6 +94,13 @@ export default {
'agent.agentName': '助手昵称',
'agent.inputAgentName': '请输入助手昵称',
'agent.roleMode': '角色模式',
'agent.agentTag': '智能体标签',
'agent.addAgentTag': '添加标签',
'agent.inputAgentTag': '请输入智能体标签',
'agent.contextProvider': '上下文源',
'agent.contextProviderSuccess': '已成功添加 {count} 个源。',
'agent.contextProviderDocLink': '如何部署上下文源',
'agent.editContextProvider': '编辑源',
'agent.roleDescription': '角色介绍',
'agent.inputRoleDescription': '请输入角色介绍',
'agent.modelConfig': '模型配置',
@@ -116,6 +125,20 @@ export default {
'agent.pleaseInputRoleDescription': '请输入角色介绍',
'agent.pleaseSelect': '请选择',
// 上下文源对话框相关
'contextProviderDialog.title': '编辑源',
'contextProviderDialog.noContextApi': '暂无上下文API',
'contextProviderDialog.add': '添加',
'contextProviderDialog.apiUrl': '接口地址',
'contextProviderDialog.apiUrlPlaceholder': 'http://api.example.com/data',
'contextProviderDialog.requestHeaders': '请求头',
'contextProviderDialog.headerKeyPlaceholder': 'Key',
'contextProviderDialog.headerValuePlaceholder': 'Value',
'contextProviderDialog.noHeaders': '暂无 Headers',
'contextProviderDialog.addHeader': '添加 Header',
'contextProviderDialog.cancel': '取消',
'contextProviderDialog.confirm': '确定',
// 聊天历史页面
'chatHistory.getChatSessions': '获取聊天会话列表',
'chatHistory.noSelectedAgent': '没有选中的智能体',
+23
View File
@@ -94,6 +94,8 @@ export default {
'home.minutesAgo': '分鐘前',
'home.hoursAgo': '小時前',
'home.daysAgo': '天前',
'home.languageModel': '語言模型',
'home.voiceModel': '音色模型',
// Agent頁面
'agent.pageTitle': '智能體',
@@ -113,6 +115,13 @@ export default {
'agent.agentName': '助手暱稱',
'agent.inputAgentName': '請輸入助手暱稱',
'agent.roleMode': '角色模式',
'agent.agentTag': '智慧體標籤',
'agent.addAgentTag': '添加標籤',
'agent.inputAgentTag': '請輸入智慧體標籤',
'agent.contextProvider': '上下文源',
'agent.contextProviderSuccess': '已成功添加 {count} 個源。',
'agent.contextProviderDocLink': '如何部署上下文源',
'agent.editContextProvider': '編輯源',
'agent.roleDescription': '角色介紹',
'agent.inputRoleDescription': '請輸入角色介紹',
'agent.modelConfig': '模型配置',
@@ -137,6 +146,20 @@ export default {
'agent.pleaseInputRoleDescription': '請輸入角色介紹',
'agent.pleaseSelect': '請選擇',
// 上下文源对话框相关
'contextProviderDialog.title': '編輯源',
'contextProviderDialog.noContextApi': '暫無上下文API',
'contextProviderDialog.add': '添加',
'contextProviderDialog.apiUrl': '介面地址',
'contextProviderDialog.apiUrlPlaceholder': 'http://api.example.com/data',
'contextProviderDialog.requestHeaders': '請求頭',
'contextProviderDialog.headerKeyPlaceholder': 'Key',
'contextProviderDialog.headerValuePlaceholder': 'Value',
'contextProviderDialog.noHeaders': '暫無 Headers',
'contextProviderDialog.addHeader': '添加 Header',
'contextProviderDialog.cancel': '取消',
'contextProviderDialog.confirm': '確定',
// 聊天歷史頁面
'chatHistory.getChatSessions': '獲取聊天會話列表',
'chatHistory.noSelectedAgent': '沒有選中的智能體',
+125 -10
View File
@@ -1,10 +1,11 @@
<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 { getAgentDetail, getAgentTags, getModelOptions, getPluginFunctions, getRoleTemplates, getTTSVoices, updateAgent, updateAgentTags } from '@/api/agent/agent'
import ContextProviderDialog from '@/components/ContextProviderDialog.vue'
import { t } from '@/i18n'
import { usePluginStore } from '@/store'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
defineOptions({
name: 'AgentEdit',
@@ -88,6 +89,11 @@ const pickerShow = ref<{
})
const allFunctions = ref<PluginDefinition[]>([])
const dynamicTags = ref([])
const inputValue = ref('')
const inputVisible = ref(false)
const showContextProviderDialog = ref(false)
const currentContextProviders = ref([])
// 使用插件store
const pluginStore = usePluginStore()
@@ -119,6 +125,31 @@ const tabList = [
activeIcon: '/static/tabbar/voiceprint_activate.png',
},
]
function handleCloseTag(id: string) {
dynamicTags.value = dynamicTags.value.filter(tag => tag.id !== id)
}
function showInput() {
inputVisible.value = true
}
function handleInputConfirm() {
if (inputValue.value) {
dynamicTags.value.push({ id: new Date().getTime(), tagName: inputValue.value.trim() })
inputValue.value = ''
}
inputVisible.value = false
}
// 打开上下文源编辑弹窗
function openContextProviderDialog() {
showContextProviderDialog.value = true
}
// 处理上下文源更新
function handleUpdateContext(providers: any[]) {
currentContextProviders.value = providers
}
// 加载智能体详情
async function loadAgentDetail() {
@@ -134,6 +165,9 @@ async function loadAgentDetail() {
pluginStore.setCurrentAgentId(agentId.value)
pluginStore.setCurrentFunctions(detail.functions || [])
// 加载上下文配置
currentContextProviders.value = (detail as any).contextProviders || []
// 如果有TTS模型,加载对应的音色选项
if (detail.ttsModelId) {
await loadVoiceOptions(detail.ttsModelId)
@@ -369,9 +403,22 @@ async function saveAgent() {
return
}
try {
await handleUpdateAgentTags()
}
catch (err) {
toast.error(err)
return
}
try {
saving.value = true
await updateAgent(agentId.value, formData.value)
// 构建保存数据,包含上下文配置
const saveData = {
...formData.value,
contextProviders: currentContextProviders.value,
}
await updateAgent(agentId.value, saveData)
toast.success(t('agent.saveSuccess'))
}
@@ -423,9 +470,25 @@ function watchPluginUpdates() {
}, { deep: true })
}
// 获取智能体标签
async function loadAgentTags() {
try {
const res = await getAgentTags(agentId.value)
dynamicTags.value = res || []
}
catch (error) {}
}
// 更新智能体标签
async function handleUpdateAgentTags() {
const tagNames = dynamicTags.value.map(tag => tag.tagName)
await updateAgentTags(agentId.value, { tagNames })
}
onMounted(async () => {
// 初始化插件配置监听
watchPluginUpdates()
loadAgentTags()
// 先加载模型选项和角色模板
await Promise.all([
@@ -443,19 +506,19 @@ onMounted(async () => {
<template>
<view class="bg-[#f5f7fb] px-[20rpx]">
<!--// 基础信息标题
<!-- 基础信息标题
<view class="pb-[20rpx] first:pt-[20rpx]">
<text class="text-[32rpx] text-[#232338] font-bold">
{{ t('agent.basicInfo') }}
</text>
</view>
</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">
{{ t('agent.agentName') }}
</text>
{{ t('agent.agentName') }}
</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]"
@@ -466,8 +529,32 @@ onMounted(async () => {
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
{{ t('agent.roleMode') }}
</text>
{{ t('agent.agentTag') }}
</text>
<input
v-if="inputVisible"
v-model="inputValue"
class="mb-[10rpx] 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"
:maxlength="20"
:placeholder="t('agent.inputAgentTag')"
@keyup.enter="handleInputConfirm"
@blur="handleInputConfirm"
>
<view class="flex flex-wrap gap-[10rpx_10rpx]">
<wd-tag v-for="tag in dynamicTags" :key="tag.id" class="items-center border !flex !border-[rgba(51,108,255,0.2)] !bg-[rgba(51,108,255,0.1)] !text-[#336cff]" round closable @close="handleCloseTag(tag.id)">
{{ tag.tagName }}
</wd-tag>
<wd-button class="!bg-[rgba(51,108,255,0.1)] !text-[#336cff]" v-if="!inputVisible" size="small" icon="add" @click="showInput">
{{ t('agent.addAgentTag') }}
</wd-button>
</view>
</view>
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
{{ t('agent.roleMode') }}
</text>
<view class="mt-0 flex flex-wrap gap-[12rpx]">
<view
v-for="template in roleTemplates"
@@ -485,8 +572,25 @@ onMounted(async () => {
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
{{ t('agent.roleDescription') }}
{{ t('agent.contextProvider') }}
</text>
<view class="mt-0 flex flex-wrap items-center gap-[12rpx]">
<text class="text-[26rpx] text-[#65686f]">
{{ t('agent.contextProviderSuccess', { count: currentContextProviders.length }) }}
</text>
<a class="text-[26rpx] text-[#5778ff] no-underline" href="https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/context-provider-integration.md" target="_blank">
{{ t('agent.contextProviderDocLink') }}
</a>
<wd-button class="!bg-[rgba(51,108,255,0.1)] !text-[#336cff]" size="small" @click="openContextProviderDialog">
{{ t('agent.editContextProvider') }}
</wd-button>
</view>
</view>
<view class="mb-[24rpx] last:mb-0">
<text class="mb-[12rpx] block text-[28rpx] text-[#232338] font-medium">
{{ t('agent.roleDescription') }}
</text>
<textarea
v-model="formData.systemPrompt"
:maxlength="2000"
@@ -699,5 +803,16 @@ onMounted(async () => {
@close="onPickerCancel('voiceprint')"
@select="({ item }) => onPickerConfirm('voiceprint', item.value, item.name)"
/>
<ContextProviderDialog
v-model:visible="showContextProviderDialog"
:providers="currentContextProviders"
@confirm="handleUpdateContext"
/>
</view>
</template>
<style lang="scss" scoped>
::v-deep .wd-tag__close {
color: #336cff !important;
}
</style>
@@ -11,8 +11,8 @@
<script lang="ts" setup>
import { onLoad } from '@dcloudio/uni-app'
import { computed, onMounted, ref } from 'vue'
import { t } from '@/i18n'
import CustomTabs from '@/components/custom-tabs/index.vue'
import { t } from '@/i18n'
import ChatHistory from '@/pages/chat-history/index.vue'
import DeviceManagement from '@/pages/device/index.vue'
import VoiceprintManagement from '@/pages/voiceprint/index.vue'
+12 -7
View File
@@ -12,12 +12,13 @@
<script lang="ts" setup>
import type { Agent } from '@/api/agent/types'
import { ref } from 'vue'
import { onMounted, ref } from 'vue'
// 在组件挂载后设置导航栏标题
import { useMessage } from 'wot-design-uni'
import useZPaging from 'z-paging/components/z-paging/js/hooks/useZPaging.js'
import { createAgent, deleteAgent, getAgentList } from '@/api/agent/agent'
import { toast } from '@/utils/toast'
import { t } from '@/i18n'
import { toast } from '@/utils/toast'
defineOptions({
name: 'Home',
@@ -162,11 +163,9 @@ onShow(() => {
}
})
// 在组件挂载后设置导航栏标题
import { onMounted } from 'vue'
onMounted(() => {
uni.setNavigationBarTitle({
title: t('home.pageTitle')
title: t('home.pageTitle'),
})
})
</script>
@@ -223,10 +222,10 @@ onMounted(() => {
<view class="model-info">
<text class="model-text">
语言模型 {{ agent.llmModelName }}
{{ t('home.languageModel') }} {{ agent.llmModelName }}
</text>
<text class="model-text">
音色模型 {{ agent.ttsModelName }} ({{ agent.ttsVoiceName }})
{{ t('home.voiceModel') }} {{ agent.ttsModelName }} ({{ agent.ttsVoiceName }})
</text>
</view>
@@ -243,6 +242,9 @@ onMounted(() => {
{{ t('home.lastConversation') }}{{ formatTime(agent.lastConnectedAt) }}
</text>
</view>
<text v-if="agent.tags" class="flex-1 truncate text-[22rpx] text-[#666]">
{{ agent.tags.map(tag => tag.tagName).join(',') }}
</text>
</view>
</view>
@@ -433,6 +435,7 @@ onMounted(() => {
.card-main {
flex: 1;
width: 100%;
}
.agent-title {
@@ -465,7 +468,9 @@ onMounted(() => {
}
.stats-row {
width: 100%;
display: flex;
align-items: center;
gap: 12rpx;
flex-wrap: wrap;