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>