217 lines
6.0 KiB
Vue
217 lines
6.0 KiB
Vue
<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
|
|
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>
|