File size: 6,197 Bytes
8ede856 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 | <template>
<BaseFolderItemSelector
:model-value="modelValue"
@update:model-value="handleUpdate"
:folder-tree="folderTree"
:items="currentPersonas as any"
:tree-loading="treeLoading"
:items-loading="itemsLoading"
:labels="labels"
:show-create-button="true"
:show-edit-button="true"
:default-item="defaultPersona"
item-id-field="persona_id"
item-name-field="persona_id"
item-description-field="system_prompt"
:display-value-formatter="formatDisplayValue"
@navigate="handleNavigate"
@create="openCreatePersona"
@edit="openEditPersona"
/>
<!-- 创建/编辑人格对话框 -->
<PersonaForm
v-model="showPersonaDialog"
:editing-persona="editingPersona ?? undefined"
:current-folder-id="currentFolderId ?? undefined"
:current-folder-name="currentFolderName ?? undefined"
@saved="handlePersonaSaved"
@error="handleError" />
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
import BaseFolderItemSelector from '@/components/folder/BaseFolderItemSelector.vue'
import PersonaForm from './PersonaForm.vue'
import { useI18n, useModuleI18n } from '@/i18n/composables'
import type { FolderTreeNode, SelectableItem } from '@/components/folder/types'
interface Persona {
persona_id: string
system_prompt: string
custom_error_message?: string | null
folder_id?: string | null
[key: string]: any
}
const props = defineProps({
modelValue: {
type: String,
default: ''
},
buttonText: {
type: String,
default: ''
}
})
const emit = defineEmits(['update:modelValue'])
const { t } = useI18n()
const { tm } = useModuleI18n('core.shared')
// 状态
const folderTree = ref<FolderTreeNode[]>([])
const currentPersonas = ref<Persona[]>([])
const treeLoading = ref(false)
const itemsLoading = ref(false)
const showPersonaDialog = ref(false)
const editingPersona = ref<Persona | null>(null)
const currentFolderId = ref<string | null>(null)
// 默认人格
const defaultPersona: SelectableItem = {
id: 'default',
persona_id: 'default',
name: tm('personaSelector.defaultPersona'),
system_prompt: 'You are a helpful and friendly assistant.'
}
// 递归查找文件夹名称
function findFolderName(nodes: FolderTreeNode[], folderId: string): string | null {
for (const node of nodes) {
if (node.folder_id === folderId) {
return node.name
}
if (node.children && node.children.length > 0) {
const found = findFolderName(node.children, folderId)
if (found) return found
}
}
return null
}
// 当前文件夹名称
const currentFolderName = computed(() => {
if (!currentFolderId.value) {
return null // 根目录,PersonaForm 会使用 tm('form.rootFolder')
}
return findFolderName(folderTree.value, currentFolderId.value)
})
// 标签配置
const labels = computed(() => ({
dialogTitle: tm('personaSelector.dialogTitle'),
notSelected: tm('personaSelector.notSelected'),
buttonText: props.buttonText || tm('personaSelector.buttonText'),
noItems: tm('personaSelector.noPersonas'),
defaultItem: tm('personaSelector.defaultPersona'),
noDescription: tm('personaSelector.noDescription'),
createButton: tm('personaSelector.createPersona'),
editButton: tm('personaSelector.editPersona') || 'Edit',
confirmButton: t('core.common.confirm'),
cancelButton: t('core.common.cancel'),
rootFolder: tm('personaSelector.rootFolder') || '全部人格',
emptyFolder: tm('personaSelector.emptyFolder') || '此文件夹为空'
}))
// 格式化显示值
function formatDisplayValue(value: string): string {
if (value === 'default') {
return tm('personaSelector.defaultPersona')
}
return value
}
// 处理值更新
function handleUpdate(value: string) {
emit('update:modelValue', value)
}
// 加载文件夹树
async function loadFolderTree() {
treeLoading.value = true
try {
const response = await axios.get('/api/persona/folder/tree')
if (response.data.status === 'ok') {
folderTree.value = response.data.data || []
}
} catch (error) {
console.error('加载文件夹树失败:', error)
folderTree.value = []
} finally {
treeLoading.value = false
}
}
// 加载指定文件夹的人格
async function loadPersonasInFolder(folderId: string | null) {
itemsLoading.value = true
try {
// 使用 /api/persona/list 端点,通过 folder_id 参数筛选
const params = new URLSearchParams()
if (folderId !== null) {
params.set('folder_id', folderId)
} else {
// 根目录:folder_id 为空字符串表示获取根目录下的人格
params.set('folder_id', '')
}
const response = await axios.get(`/api/persona/list?${params.toString()}`)
if (response.data.status === 'ok') {
currentPersonas.value = response.data.data || []
}
} catch (error) {
console.error('加载人格列表失败:', error)
currentPersonas.value = []
} finally {
itemsLoading.value = false
}
}
// 处理文件夹导航
async function handleNavigate(folderId: string | null) {
currentFolderId.value = folderId
await loadPersonasInFolder(folderId)
}
// 打开创建人格对话框
function openCreatePersona() {
editingPersona.value = null
showPersonaDialog.value = true
}
// 打开编辑人格对话框
function openEditPersona(persona: Persona) {
editingPersona.value = persona
showPersonaDialog.value = true
}
// 人格保存成功(创建或编辑)
async function handlePersonaSaved(message: string) {
console.log('人格保存成功:', message)
const savedPersonaId = editingPersona.value?.persona_id || ''
showPersonaDialog.value = false
editingPersona.value = null
// 刷新当前文件夹的人格列表
await loadPersonasInFolder(currentFolderId.value)
window.dispatchEvent(
new CustomEvent('astrbot:persona-saved', {
detail: { persona_id: savedPersonaId }
})
)
}
// 错误处理
function handleError(error: string) {
console.error('创建人格失败:', error)
}
// 初始化加载文件夹树
onMounted(() => {
loadFolderTree()
})
</script>
<style scoped>
/* 样式继承自 BaseFolderItemSelector */
</style>
|